unibuf 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +178 -330
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.adoc +443 -254
- data/docs/CAPNPROTO.adoc +436 -0
- data/docs/FLATBUFFERS.adoc +430 -0
- data/docs/PROTOBUF.adoc +515 -0
- data/docs/TXTPROTO.adoc +369 -0
- data/lib/unibuf/commands/convert.rb +60 -2
- data/lib/unibuf/commands/schema.rb +68 -11
- data/lib/unibuf/errors.rb +23 -26
- data/lib/unibuf/models/capnproto/enum_definition.rb +72 -0
- data/lib/unibuf/models/capnproto/field_definition.rb +81 -0
- data/lib/unibuf/models/capnproto/interface_definition.rb +70 -0
- data/lib/unibuf/models/capnproto/method_definition.rb +81 -0
- data/lib/unibuf/models/capnproto/schema.rb +84 -0
- data/lib/unibuf/models/capnproto/struct_definition.rb +96 -0
- data/lib/unibuf/models/capnproto/union_definition.rb +62 -0
- data/lib/unibuf/models/flatbuffers/enum_definition.rb +69 -0
- data/lib/unibuf/models/flatbuffers/field_definition.rb +88 -0
- data/lib/unibuf/models/flatbuffers/schema.rb +102 -0
- data/lib/unibuf/models/flatbuffers/struct_definition.rb +70 -0
- data/lib/unibuf/models/flatbuffers/table_definition.rb +73 -0
- data/lib/unibuf/models/flatbuffers/union_definition.rb +60 -0
- data/lib/unibuf/models/message.rb +10 -0
- data/lib/unibuf/models/values/scalar_value.rb +2 -2
- data/lib/unibuf/parsers/binary/wire_format_parser.rb +199 -19
- data/lib/unibuf/parsers/capnproto/binary_parser.rb +267 -0
- data/lib/unibuf/parsers/capnproto/grammar.rb +272 -0
- data/lib/unibuf/parsers/capnproto/list_reader.rb +208 -0
- data/lib/unibuf/parsers/capnproto/pointer_decoder.rb +163 -0
- data/lib/unibuf/parsers/capnproto/processor.rb +348 -0
- data/lib/unibuf/parsers/capnproto/segment_reader.rb +131 -0
- data/lib/unibuf/parsers/capnproto/struct_reader.rb +199 -0
- data/lib/unibuf/parsers/flatbuffers/binary_parser.rb +325 -0
- data/lib/unibuf/parsers/flatbuffers/grammar.rb +235 -0
- data/lib/unibuf/parsers/flatbuffers/processor.rb +299 -0
- data/lib/unibuf/parsers/textproto/grammar.rb +1 -1
- data/lib/unibuf/parsers/textproto/processor.rb +10 -0
- data/lib/unibuf/serializers/binary_serializer.rb +218 -0
- data/lib/unibuf/serializers/capnproto/binary_serializer.rb +402 -0
- data/lib/unibuf/serializers/capnproto/list_writer.rb +199 -0
- data/lib/unibuf/serializers/capnproto/pointer_encoder.rb +118 -0
- data/lib/unibuf/serializers/capnproto/segment_builder.rb +124 -0
- data/lib/unibuf/serializers/capnproto/struct_writer.rb +139 -0
- data/lib/unibuf/serializers/flatbuffers/binary_serializer.rb +167 -0
- data/lib/unibuf/validators/type_validator.rb +1 -1
- data/lib/unibuf/version.rb +1 -1
- data/lib/unibuf.rb +27 -0
- metadata +36 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Serializers
|
|
5
|
+
module Capnproto
|
|
6
|
+
# Encoder for Cap'n Proto pointer words
|
|
7
|
+
# Encodes pointer information into 64-bit words
|
|
8
|
+
class PointerEncoder
|
|
9
|
+
# Pointer type constants (bits 0-1)
|
|
10
|
+
POINTER_TYPE_STRUCT = 0
|
|
11
|
+
POINTER_TYPE_LIST = 1
|
|
12
|
+
POINTER_TYPE_FAR = 2
|
|
13
|
+
POINTER_TYPE_OTHER = 3
|
|
14
|
+
|
|
15
|
+
# List element size constants
|
|
16
|
+
ELEMENT_SIZE_VOID = 0
|
|
17
|
+
ELEMENT_SIZE_BIT = 1
|
|
18
|
+
ELEMENT_SIZE_BYTE = 2
|
|
19
|
+
ELEMENT_SIZE_TWO_BYTES = 3
|
|
20
|
+
ELEMENT_SIZE_FOUR_BYTES = 4
|
|
21
|
+
ELEMENT_SIZE_EIGHT_BYTES = 5
|
|
22
|
+
ELEMENT_SIZE_POINTER = 6
|
|
23
|
+
ELEMENT_SIZE_INLINE_COMPOSITE = 7
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Encode a null pointer
|
|
27
|
+
# @return [Integer] 64-bit word
|
|
28
|
+
def encode_null
|
|
29
|
+
0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Encode a struct pointer
|
|
33
|
+
# @param offset [Integer] Signed word offset (relative to pointer position)
|
|
34
|
+
# @param data_words [Integer] Number of data words
|
|
35
|
+
# @param pointer_words [Integer] Number of pointer words
|
|
36
|
+
# @return [Integer] 64-bit pointer word
|
|
37
|
+
def encode_struct(offset, data_words, pointer_words)
|
|
38
|
+
# Bits: [pointer_words:16][data_words:16][offset:30][type:2]
|
|
39
|
+
|
|
40
|
+
# Convert signed offset to 30-bit representation
|
|
41
|
+
offset_bits = offset & 0x3FFFFFFF
|
|
42
|
+
|
|
43
|
+
word = POINTER_TYPE_STRUCT |
|
|
44
|
+
(offset_bits << 2) |
|
|
45
|
+
(data_words << 32) |
|
|
46
|
+
(pointer_words << 48)
|
|
47
|
+
|
|
48
|
+
word & 0xFFFFFFFFFFFFFFFF
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Encode a list pointer
|
|
52
|
+
# @param offset [Integer] Signed word offset
|
|
53
|
+
# @param element_size [Integer] Element size code (0-7)
|
|
54
|
+
# @param element_count [Integer] Number of elements
|
|
55
|
+
# @return [Integer] 64-bit pointer word
|
|
56
|
+
def encode_list(offset, element_size, element_count)
|
|
57
|
+
# Bits: [element_count:29][element_size:3][offset:30][type:2]
|
|
58
|
+
|
|
59
|
+
# Convert signed offset to 30-bit representation
|
|
60
|
+
offset_bits = offset & 0x3FFFFFFF
|
|
61
|
+
|
|
62
|
+
word = POINTER_TYPE_LIST |
|
|
63
|
+
(offset_bits << 2) |
|
|
64
|
+
(element_size << 32) |
|
|
65
|
+
(element_count << 35)
|
|
66
|
+
|
|
67
|
+
word & 0xFFFFFFFFFFFFFFFF
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Encode a far pointer
|
|
71
|
+
# @param segment_id [Integer] Target segment ID
|
|
72
|
+
# @param offset [Integer] Word offset within target segment
|
|
73
|
+
# @param double_far [Boolean] Landing pad flag
|
|
74
|
+
# @return [Integer] 64-bit pointer word
|
|
75
|
+
def encode_far(segment_id, offset, double_far: false)
|
|
76
|
+
# Bits: [segment_id:32][offset:29][double_far:1][type:2]
|
|
77
|
+
|
|
78
|
+
double_far_bit = double_far ? 1 : 0
|
|
79
|
+
|
|
80
|
+
word = POINTER_TYPE_FAR |
|
|
81
|
+
(double_far_bit << 2) |
|
|
82
|
+
(offset << 3) |
|
|
83
|
+
(segment_id << 32)
|
|
84
|
+
|
|
85
|
+
word & 0xFFFFFFFFFFFFFFFF
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Encode a capability pointer
|
|
89
|
+
# @param index [Integer] Capability index
|
|
90
|
+
# @return [Integer] 64-bit pointer word
|
|
91
|
+
def encode_capability(index)
|
|
92
|
+
# Bits: [index:32][reserved:30][type:2]
|
|
93
|
+
|
|
94
|
+
word = POINTER_TYPE_OTHER |
|
|
95
|
+
(index << 32)
|
|
96
|
+
|
|
97
|
+
word & 0xFFFFFFFFFFFFFFFF
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get element size code for a type
|
|
101
|
+
# @param type_name [String, Symbol] Type name
|
|
102
|
+
# @return [Integer] Element size code
|
|
103
|
+
def element_size_for_type(type_name)
|
|
104
|
+
case type_name.to_s
|
|
105
|
+
when "Void" then ELEMENT_SIZE_VOID
|
|
106
|
+
when "Bool" then ELEMENT_SIZE_BIT
|
|
107
|
+
when "Int8", "UInt8" then ELEMENT_SIZE_BYTE
|
|
108
|
+
when "Int16", "UInt16" then ELEMENT_SIZE_TWO_BYTES
|
|
109
|
+
when "Int32", "UInt32", "Float32" then ELEMENT_SIZE_FOUR_BYTES
|
|
110
|
+
when "Int64", "UInt64", "Float64" then ELEMENT_SIZE_EIGHT_BYTES
|
|
111
|
+
else ELEMENT_SIZE_POINTER # For user types and Text/Data
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Serializers
|
|
5
|
+
module Capnproto
|
|
6
|
+
# Builder for Cap'n Proto binary segments
|
|
7
|
+
# Manages segment allocation and word-aligned writes
|
|
8
|
+
class SegmentBuilder
|
|
9
|
+
WORD_SIZE = 8 # Cap'n Proto uses 8-byte words
|
|
10
|
+
|
|
11
|
+
attr_reader :segments
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@segments = []
|
|
15
|
+
@current_segment = []
|
|
16
|
+
@current_segment_id = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get all segments including current
|
|
20
|
+
def segments
|
|
21
|
+
if @current_segment.empty?
|
|
22
|
+
@segments
|
|
23
|
+
else
|
|
24
|
+
@segments + [@current_segment]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Allocate words in current segment
|
|
29
|
+
# @param word_count [Integer] Number of words to allocate
|
|
30
|
+
# @return [Array<Integer>] [segment_id, word_offset]
|
|
31
|
+
def allocate(word_count)
|
|
32
|
+
segment_id = @current_segment_id
|
|
33
|
+
word_offset = @current_segment.length
|
|
34
|
+
|
|
35
|
+
# Add placeholder words
|
|
36
|
+
word_count.times { @current_segment << 0 }
|
|
37
|
+
|
|
38
|
+
[segment_id, word_offset]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Write a word at specific location
|
|
42
|
+
# @param segment_id [Integer] Segment index
|
|
43
|
+
# @param word_offset [Integer] Word offset within segment
|
|
44
|
+
# @param value [Integer] 64-bit word value
|
|
45
|
+
def write_word(segment_id, word_offset, value)
|
|
46
|
+
if segment_id == @current_segment_id
|
|
47
|
+
# Writing to current segment
|
|
48
|
+
@current_segment[word_offset] = value & 0xFFFFFFFFFFFFFFFF
|
|
49
|
+
elsif segment_id < @segments.length
|
|
50
|
+
# Writing to finalized segment
|
|
51
|
+
@segments[segment_id][word_offset] = value & 0xFFFFFFFFFFFFFFFF
|
|
52
|
+
else
|
|
53
|
+
raise "Invalid segment ID: #{segment_id}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Write multiple words
|
|
58
|
+
# @param segment_id [Integer] Segment index
|
|
59
|
+
# @param word_offset [Integer] Starting word offset
|
|
60
|
+
# @param values [Array<Integer>] Word values
|
|
61
|
+
def write_words(segment_id, word_offset, values)
|
|
62
|
+
values.each_with_index do |value, i|
|
|
63
|
+
write_word(segment_id, word_offset + i, value)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Finalize current segment and start a new one
|
|
68
|
+
def finalize_segment
|
|
69
|
+
@segments << @current_segment unless @current_segment.empty?
|
|
70
|
+
@current_segment = []
|
|
71
|
+
@current_segment_id = @segments.length
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Build final binary output
|
|
75
|
+
# @return [String] Binary data
|
|
76
|
+
def build
|
|
77
|
+
# Finalize current segment if not empty
|
|
78
|
+
finalize_segment unless @current_segment.empty?
|
|
79
|
+
|
|
80
|
+
return "" if @segments.empty?
|
|
81
|
+
|
|
82
|
+
# Build segment table
|
|
83
|
+
segment_count = @segments.length
|
|
84
|
+
segment_sizes = @segments.map(&:length)
|
|
85
|
+
|
|
86
|
+
# Segment table header
|
|
87
|
+
output = [segment_count - 1].pack("L<") # segment count - 1
|
|
88
|
+
|
|
89
|
+
# Segment sizes
|
|
90
|
+
segment_sizes.each do |size|
|
|
91
|
+
output += [size].pack("L<")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Add padding if needed (when segment count is even)
|
|
95
|
+
output += [0].pack("L<") unless segment_count.odd?
|
|
96
|
+
|
|
97
|
+
# Write segment data
|
|
98
|
+
@segments.each do |segment|
|
|
99
|
+
segment.each do |word|
|
|
100
|
+
output += [word].pack("Q<")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
output
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get current position for relative offset calculation
|
|
108
|
+
# @return [Array<Integer>] [segment_id, word_offset]
|
|
109
|
+
def current_position
|
|
110
|
+
[@current_segment_id, @current_segment.length]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def ensure_segment(segment_id)
|
|
116
|
+
return if segment_id == @current_segment_id
|
|
117
|
+
return if segment_id < @segments.length
|
|
118
|
+
|
|
119
|
+
raise "Invalid segment ID: #{segment_id}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "segment_builder"
|
|
4
|
+
require_relative "pointer_encoder"
|
|
5
|
+
|
|
6
|
+
module Unibuf
|
|
7
|
+
module Serializers
|
|
8
|
+
module Capnproto
|
|
9
|
+
# Writer for Cap'n Proto struct data
|
|
10
|
+
# Writes data section (primitives) and pointer section (references)
|
|
11
|
+
class StructWriter
|
|
12
|
+
attr_reader :segment_builder, :segment_id, :word_offset, :data_words,
|
|
13
|
+
:pointer_words
|
|
14
|
+
|
|
15
|
+
# Initialize struct writer
|
|
16
|
+
# @param segment_builder [SegmentBuilder] Segment builder
|
|
17
|
+
# @param segment_id [Integer] Segment ID
|
|
18
|
+
# @param word_offset [Integer] Word offset in segment
|
|
19
|
+
# @param data_words [Integer] Number of data words
|
|
20
|
+
# @param pointer_words [Integer] Number of pointer words
|
|
21
|
+
def initialize(segment_builder, segment_id, word_offset, data_words,
|
|
22
|
+
pointer_words)
|
|
23
|
+
@segment_builder = segment_builder
|
|
24
|
+
@segment_id = segment_id
|
|
25
|
+
@word_offset = word_offset
|
|
26
|
+
@data_words = data_words
|
|
27
|
+
@pointer_words = pointer_words
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Write a primitive value to data section
|
|
31
|
+
# @param word_index [Integer] Word index in data section
|
|
32
|
+
# @param bit_offset [Integer] Bit offset within word (0-63)
|
|
33
|
+
# @param bit_width [Integer] Width in bits
|
|
34
|
+
# @param value [Integer] Value to write
|
|
35
|
+
def write_data_field(word_index, bit_offset, bit_width, value)
|
|
36
|
+
return if word_index >= @data_words
|
|
37
|
+
|
|
38
|
+
# Read current word
|
|
39
|
+
all_segments = @segment_builder.segments
|
|
40
|
+
current = if @segment_id < all_segments.length
|
|
41
|
+
all_segments[@segment_id][@word_offset + word_index] || 0
|
|
42
|
+
else
|
|
43
|
+
0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Create mask and insert value
|
|
47
|
+
mask = ((1 << bit_width) - 1) << bit_offset
|
|
48
|
+
cleared = current & ~mask
|
|
49
|
+
new_value = cleared | ((value << bit_offset) & mask)
|
|
50
|
+
|
|
51
|
+
@segment_builder.write_word(@segment_id, @word_offset + word_index,
|
|
52
|
+
new_value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Write unsigned 8-bit integer
|
|
56
|
+
def write_uint8(word_index, byte_offset, value)
|
|
57
|
+
write_data_field(word_index, byte_offset * 8, 8, value & 0xFF)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Write signed 8-bit integer
|
|
61
|
+
def write_int8(word_index, byte_offset, value)
|
|
62
|
+
unsigned = value.negative? ? value + 256 : value
|
|
63
|
+
write_uint8(word_index, byte_offset, unsigned)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Write unsigned 16-bit integer
|
|
67
|
+
def write_uint16(word_index, half_word_offset, value)
|
|
68
|
+
write_data_field(word_index, half_word_offset * 16, 16,
|
|
69
|
+
value & 0xFFFF)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Write signed 16-bit integer
|
|
73
|
+
def write_int16(word_index, half_word_offset, value)
|
|
74
|
+
unsigned = value.negative? ? value + 65536 : value
|
|
75
|
+
write_uint16(word_index, half_word_offset, unsigned)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Write unsigned 32-bit integer
|
|
79
|
+
def write_uint32(word_index, dword_offset, value)
|
|
80
|
+
write_data_field(word_index, dword_offset * 32, 32,
|
|
81
|
+
value & 0xFFFFFFFF)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Write signed 32-bit integer
|
|
85
|
+
def write_int32(word_index, dword_offset, value)
|
|
86
|
+
unsigned = value.negative? ? value + 4294967296 : value
|
|
87
|
+
write_uint32(word_index, dword_offset, unsigned)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Write unsigned 64-bit integer
|
|
91
|
+
def write_uint64(word_index, value)
|
|
92
|
+
@segment_builder.write_word(@segment_id, @word_offset + word_index,
|
|
93
|
+
value & 0xFFFFFFFFFFFFFFFF)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Write signed 64-bit integer
|
|
97
|
+
def write_int64(word_index, value)
|
|
98
|
+
unsigned = value.negative? ? value + 18446744073709551616 : value
|
|
99
|
+
write_uint64(word_index, unsigned)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Write 32-bit float
|
|
103
|
+
def write_float32(word_index, dword_offset, value)
|
|
104
|
+
bits = [value].pack("f").unpack1("L")
|
|
105
|
+
write_uint32(word_index, dword_offset, bits)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Write 64-bit float
|
|
109
|
+
def write_float64(word_index, value)
|
|
110
|
+
bits = [value].pack("d").unpack1("Q")
|
|
111
|
+
write_uint64(word_index, bits)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Write boolean
|
|
115
|
+
def write_bool(word_index, bit_offset, value)
|
|
116
|
+
write_data_field(word_index, bit_offset, 1, value ? 1 : 0)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Write a pointer to pointer section
|
|
120
|
+
# @param pointer_index [Integer] Pointer index in pointer section
|
|
121
|
+
# @param pointer_word [Integer] Encoded pointer word
|
|
122
|
+
def write_pointer(pointer_index, pointer_word)
|
|
123
|
+
return if pointer_index >= @pointer_words
|
|
124
|
+
|
|
125
|
+
pointer_word_offset = @word_offset + @data_words + pointer_index
|
|
126
|
+
@segment_builder.write_word(@segment_id, pointer_word_offset,
|
|
127
|
+
pointer_word)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get pointer position for offset calculations
|
|
131
|
+
# @param pointer_index [Integer] Pointer index
|
|
132
|
+
# @return [Integer] Absolute word offset of pointer
|
|
133
|
+
def pointer_position(pointer_index)
|
|
134
|
+
@word_offset + @data_words + pointer_index
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Serializers
|
|
5
|
+
module Flatbuffers
|
|
6
|
+
# FlatBuffers binary format serializer - Pure Ruby
|
|
7
|
+
class BinarySerializer
|
|
8
|
+
attr_reader :schema
|
|
9
|
+
|
|
10
|
+
def initialize(schema)
|
|
11
|
+
@schema = schema
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serialize(data)
|
|
15
|
+
raise ArgumentError, "Data cannot be nil" if data.nil?
|
|
16
|
+
|
|
17
|
+
root_table_def = schema.find_table(schema.root_type)
|
|
18
|
+
unless root_table_def
|
|
19
|
+
raise Unibuf::SerializationError,
|
|
20
|
+
"Root type '#{schema.root_type}' not found in schema"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Manual buffer construction
|
|
24
|
+
[]
|
|
25
|
+
|
|
26
|
+
# Serialize table and collect parts
|
|
27
|
+
table_parts = serialize_table(data, root_table_def)
|
|
28
|
+
|
|
29
|
+
# Calculate positions
|
|
30
|
+
root_offset = 4 # After root offset itself
|
|
31
|
+
|
|
32
|
+
# Build final buffer
|
|
33
|
+
buffer = [root_offset].pack("L<") # Root offset
|
|
34
|
+
buffer += table_parts.pack("C*")
|
|
35
|
+
|
|
36
|
+
buffer
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def serialize_to_file(data, path)
|
|
40
|
+
File.binwrite(path, serialize(data))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def serialize_table(data, table_def)
|
|
46
|
+
# PHASE 1: Write strings (out-of-line data)
|
|
47
|
+
string_data = {}
|
|
48
|
+
string_bytes_total = []
|
|
49
|
+
|
|
50
|
+
table_def.fields.each do |field_def|
|
|
51
|
+
value = data[field_def.name]
|
|
52
|
+
next unless value && field_def.type == "string"
|
|
53
|
+
|
|
54
|
+
# Serialize string
|
|
55
|
+
str_bytes = []
|
|
56
|
+
str_bytes.concat([value.bytesize].pack("L<").bytes) # length
|
|
57
|
+
str_bytes.concat(value.bytes) # content
|
|
58
|
+
str_bytes << 0 # null terminator
|
|
59
|
+
|
|
60
|
+
# Align to 4
|
|
61
|
+
while (str_bytes.size % 4) != 0
|
|
62
|
+
str_bytes << 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
string_data[field_def.name] = {
|
|
66
|
+
bytes: str_bytes,
|
|
67
|
+
start_in_string_section: string_bytes_total.size,
|
|
68
|
+
}
|
|
69
|
+
string_bytes_total.concat(str_bytes)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# PHASE 2: Build table
|
|
73
|
+
table_bytes = []
|
|
74
|
+
|
|
75
|
+
# Reserve vtable offset
|
|
76
|
+
table_bytes.push(0, 0, 0, 0)
|
|
77
|
+
|
|
78
|
+
# Calculate vtable size
|
|
79
|
+
vtable_size = 4 + (table_def.fields.size * 2)
|
|
80
|
+
|
|
81
|
+
# Write fields
|
|
82
|
+
field_offsets = []
|
|
83
|
+
current_pos = 4 # Start after vtable offset
|
|
84
|
+
|
|
85
|
+
table_def.fields.each do |field_def|
|
|
86
|
+
value = data[field_def.name]
|
|
87
|
+
|
|
88
|
+
if value.nil?
|
|
89
|
+
field_offsets << 0
|
|
90
|
+
elsif field_def.scalar?
|
|
91
|
+
field_offsets << current_pos
|
|
92
|
+
# Write scalar value
|
|
93
|
+
case field_def.type
|
|
94
|
+
when "byte"
|
|
95
|
+
table_bytes.concat([value].pack("c").bytes)
|
|
96
|
+
current_pos += 1
|
|
97
|
+
when "ubyte", "bool"
|
|
98
|
+
val = if value.is_a?(TrueClass)
|
|
99
|
+
1
|
|
100
|
+
else
|
|
101
|
+
(value.is_a?(FalseClass) ? 0 : value)
|
|
102
|
+
end
|
|
103
|
+
table_bytes << (val & 0xFF)
|
|
104
|
+
current_pos += 1
|
|
105
|
+
when "short"
|
|
106
|
+
table_bytes.concat([value].pack("s<").bytes)
|
|
107
|
+
current_pos += 2
|
|
108
|
+
when "ushort"
|
|
109
|
+
table_bytes.concat([value].pack("S<").bytes)
|
|
110
|
+
current_pos += 2
|
|
111
|
+
when "int"
|
|
112
|
+
table_bytes.concat([value].pack("l<").bytes)
|
|
113
|
+
current_pos += 4
|
|
114
|
+
when "uint"
|
|
115
|
+
table_bytes.concat([value].pack("L<").bytes)
|
|
116
|
+
current_pos += 4
|
|
117
|
+
when "long"
|
|
118
|
+
table_bytes.concat([value].pack("q<").bytes)
|
|
119
|
+
current_pos += 8
|
|
120
|
+
when "ulong"
|
|
121
|
+
table_bytes.concat([value].pack("Q<").bytes)
|
|
122
|
+
current_pos += 8
|
|
123
|
+
when "float"
|
|
124
|
+
table_bytes.concat([value].pack("e").bytes)
|
|
125
|
+
current_pos += 4
|
|
126
|
+
when "double"
|
|
127
|
+
table_bytes.concat([value].pack("E").bytes)
|
|
128
|
+
current_pos += 8
|
|
129
|
+
end
|
|
130
|
+
elsif field_def.type == "string" && string_data[field_def.name]
|
|
131
|
+
field_offsets << current_pos
|
|
132
|
+
# Calculate uoffset from current field position to string
|
|
133
|
+
# String will be at: 4 (root) + table_size + vtable_size + string_start
|
|
134
|
+
str_info = string_data[field_def.name]
|
|
135
|
+
table_size = table_bytes.size + 4 # +4 for remaining field
|
|
136
|
+
string_abs_pos = 4 + table_size + vtable_size + str_info[:start_in_string_section]
|
|
137
|
+
# uoffset from field position in final buffer
|
|
138
|
+
field_abs_pos = 4 + current_pos
|
|
139
|
+
uoffset = string_abs_pos - field_abs_pos
|
|
140
|
+
table_bytes.concat([uoffset].pack("L<").bytes)
|
|
141
|
+
current_pos += 4
|
|
142
|
+
else
|
|
143
|
+
field_offsets << 0
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# PHASE 3: Build vtable
|
|
148
|
+
vtable_bytes = []
|
|
149
|
+
object_size = table_bytes.size
|
|
150
|
+
|
|
151
|
+
vtable_bytes.concat([vtable_size].pack("S<").bytes)
|
|
152
|
+
vtable_bytes.concat([object_size].pack("S<").bytes)
|
|
153
|
+
field_offsets.each do |off|
|
|
154
|
+
vtable_bytes.concat([off].pack("S<").bytes)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Patch vtable offset
|
|
158
|
+
vtable_offset = -object_size
|
|
159
|
+
table_bytes[0..3] = [vtable_offset].pack("l<").bytes
|
|
160
|
+
|
|
161
|
+
# Return table + vtable + strings
|
|
162
|
+
table_bytes + vtable_bytes + string_bytes_total
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/unibuf/version.rb
CHANGED
data/lib/unibuf.rb
CHANGED
|
@@ -21,6 +21,10 @@ module Unibuf
|
|
|
21
21
|
# FlatBuffers schema parser
|
|
22
22
|
module Flatbuffers
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Cap'n Proto schema parser
|
|
26
|
+
module Capnproto
|
|
27
|
+
end
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
# Module for all models
|
|
@@ -31,6 +35,10 @@ module Unibuf
|
|
|
31
35
|
module Validators
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
# Module for serializers
|
|
39
|
+
module Serializers
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
class << self
|
|
35
43
|
# ===== TEXT FORMAT PARSING (no schema required) =====
|
|
36
44
|
|
|
@@ -123,6 +131,22 @@ module Unibuf
|
|
|
123
131
|
Parsers::Flatbuffers::BinaryParser.new(schema).parse(content)
|
|
124
132
|
end
|
|
125
133
|
|
|
134
|
+
# ===== CAP'N PROTO =====
|
|
135
|
+
|
|
136
|
+
# Parse Cap'n Proto schema file
|
|
137
|
+
# @param path [String] Path to .capnp file
|
|
138
|
+
# @return [Models::Capnproto::Schema] Cap'n Proto schema
|
|
139
|
+
def parse_capnproto_schema(path)
|
|
140
|
+
require_relative "unibuf/parsers/capnproto/grammar"
|
|
141
|
+
require_relative "unibuf/parsers/capnproto/processor"
|
|
142
|
+
|
|
143
|
+
grammar = Parsers::Capnproto::Grammar.new
|
|
144
|
+
content = File.read(path)
|
|
145
|
+
ast = grammar.parse(content)
|
|
146
|
+
Parsers::Capnproto::Processor.process(ast)
|
|
147
|
+
end
|
|
148
|
+
alias parse_capnp parse_capnproto_schema
|
|
149
|
+
|
|
126
150
|
# ===== AUTO-DETECTION (convenience methods) =====
|
|
127
151
|
|
|
128
152
|
# Auto-detect format and parse
|
|
@@ -158,6 +182,9 @@ module Unibuf
|
|
|
158
182
|
when ".fbs"
|
|
159
183
|
raise ArgumentError,
|
|
160
184
|
".fbs files are schemas, use parse_flatbuffers_schema()"
|
|
185
|
+
when ".capnp"
|
|
186
|
+
raise ArgumentError,
|
|
187
|
+
".capnp files are schemas, use parse_capnproto_schema()"
|
|
161
188
|
when ".pb"
|
|
162
189
|
# Ambiguous extension - try to detect
|
|
163
190
|
detect_and_parse_pb(path, schema)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unibuf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ronald Tse
|
|
@@ -78,8 +78,13 @@ files:
|
|
|
78
78
|
- ".rspec"
|
|
79
79
|
- ".rubocop.yml"
|
|
80
80
|
- ".rubocop_todo.yml"
|
|
81
|
+
- CODE_OF_CONDUCT.md
|
|
81
82
|
- README.adoc
|
|
82
83
|
- Rakefile
|
|
84
|
+
- docs/CAPNPROTO.adoc
|
|
85
|
+
- docs/FLATBUFFERS.adoc
|
|
86
|
+
- docs/PROTOBUF.adoc
|
|
87
|
+
- docs/TXTPROTO.adoc
|
|
83
88
|
- exe/unibuf
|
|
84
89
|
- lib/unibuf.rb
|
|
85
90
|
- lib/unibuf/cli.rb
|
|
@@ -88,9 +93,22 @@ files:
|
|
|
88
93
|
- lib/unibuf/commands/schema.rb
|
|
89
94
|
- lib/unibuf/commands/validate.rb
|
|
90
95
|
- lib/unibuf/errors.rb
|
|
96
|
+
- lib/unibuf/models/capnproto/enum_definition.rb
|
|
97
|
+
- lib/unibuf/models/capnproto/field_definition.rb
|
|
98
|
+
- lib/unibuf/models/capnproto/interface_definition.rb
|
|
99
|
+
- lib/unibuf/models/capnproto/method_definition.rb
|
|
100
|
+
- lib/unibuf/models/capnproto/schema.rb
|
|
101
|
+
- lib/unibuf/models/capnproto/struct_definition.rb
|
|
102
|
+
- lib/unibuf/models/capnproto/union_definition.rb
|
|
91
103
|
- lib/unibuf/models/enum_definition.rb
|
|
92
104
|
- lib/unibuf/models/field.rb
|
|
93
105
|
- lib/unibuf/models/field_definition.rb
|
|
106
|
+
- lib/unibuf/models/flatbuffers/enum_definition.rb
|
|
107
|
+
- lib/unibuf/models/flatbuffers/field_definition.rb
|
|
108
|
+
- lib/unibuf/models/flatbuffers/schema.rb
|
|
109
|
+
- lib/unibuf/models/flatbuffers/struct_definition.rb
|
|
110
|
+
- lib/unibuf/models/flatbuffers/table_definition.rb
|
|
111
|
+
- lib/unibuf/models/flatbuffers/union_definition.rb
|
|
94
112
|
- lib/unibuf/models/message.rb
|
|
95
113
|
- lib/unibuf/models/message_definition.rb
|
|
96
114
|
- lib/unibuf/models/schema.rb
|
|
@@ -100,11 +118,28 @@ files:
|
|
|
100
118
|
- lib/unibuf/models/values/message_value.rb
|
|
101
119
|
- lib/unibuf/models/values/scalar_value.rb
|
|
102
120
|
- lib/unibuf/parsers/binary/wire_format_parser.rb
|
|
121
|
+
- lib/unibuf/parsers/capnproto/binary_parser.rb
|
|
122
|
+
- lib/unibuf/parsers/capnproto/grammar.rb
|
|
123
|
+
- lib/unibuf/parsers/capnproto/list_reader.rb
|
|
124
|
+
- lib/unibuf/parsers/capnproto/pointer_decoder.rb
|
|
125
|
+
- lib/unibuf/parsers/capnproto/processor.rb
|
|
126
|
+
- lib/unibuf/parsers/capnproto/segment_reader.rb
|
|
127
|
+
- lib/unibuf/parsers/capnproto/struct_reader.rb
|
|
128
|
+
- lib/unibuf/parsers/flatbuffers/binary_parser.rb
|
|
129
|
+
- lib/unibuf/parsers/flatbuffers/grammar.rb
|
|
130
|
+
- lib/unibuf/parsers/flatbuffers/processor.rb
|
|
103
131
|
- lib/unibuf/parsers/proto3/grammar.rb
|
|
104
132
|
- lib/unibuf/parsers/proto3/processor.rb
|
|
105
133
|
- lib/unibuf/parsers/textproto/grammar.rb
|
|
106
134
|
- lib/unibuf/parsers/textproto/parser.rb
|
|
107
135
|
- lib/unibuf/parsers/textproto/processor.rb
|
|
136
|
+
- lib/unibuf/serializers/binary_serializer.rb
|
|
137
|
+
- lib/unibuf/serializers/capnproto/binary_serializer.rb
|
|
138
|
+
- lib/unibuf/serializers/capnproto/list_writer.rb
|
|
139
|
+
- lib/unibuf/serializers/capnproto/pointer_encoder.rb
|
|
140
|
+
- lib/unibuf/serializers/capnproto/segment_builder.rb
|
|
141
|
+
- lib/unibuf/serializers/capnproto/struct_writer.rb
|
|
142
|
+
- lib/unibuf/serializers/flatbuffers/binary_serializer.rb
|
|
108
143
|
- lib/unibuf/validators/schema_validator.rb
|
|
109
144
|
- lib/unibuf/validators/type_validator.rb
|
|
110
145
|
- lib/unibuf/version.rb
|