fast-protowire 0.1.0 → 0.3.0
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/AGENTS.md +3 -1
- data/CHANGELOG.md +85 -0
- data/README.md +19 -0
- data/lib/fast/protowire/field.rb +162 -109
- data/lib/fast/protowire/message.rb +25 -12
- data/lib/fast/protowire/reader.rb +100 -26
- data/lib/fast/protowire/version.rb +1 -1
- data/lib/fast/protowire/wire.rb +98 -18
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d6f10862b8163b7b4d5210479a6b27b62298e0951b24425db97cdec94356a10
|
|
4
|
+
data.tar.gz: 5a882709766e557ae6a65b3107ec3cc8de6fba07f90de3293607518cf880b4f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a54a74df181260136e00d9227f050c1ef785096515b4a91197f939af58df1d865bea3eb5e660db97335111a1728407edccfce11afd8b60a4335076e95b1a258
|
|
7
|
+
data.tar.gz: 5048f2b557f2e3c4f44b4a2cb95110b11142c6b7c94975dbca68f7a3fc1550ea2e9b120460b4e36bb2ed7f316bdb476b60b2b334f449728c2a8ba869242a8036
|
data/AGENTS.md
CHANGED
|
@@ -8,7 +8,9 @@ Standing context for agents working in this repository.
|
|
|
8
8
|
bundle install
|
|
9
9
|
bundle exec sus
|
|
10
10
|
bundle exec rubocop
|
|
11
|
-
|
|
11
|
+
bundle exec ruby benchmark/messages.rb # encode, build and decode against google-protobuf; BENCH_QUICK=1 for a short run
|
|
12
|
+
protoc --proto_path=fixtures/proto --proto_path=/opt/homebrew/include --ruby_out=fixtures/pb fixtures/proto/*.proto
|
|
13
|
+
# regenerates the reference schemas; the second path is where protoc keeps google/protobuf/timestamp.proto
|
|
12
14
|
```
|
|
13
15
|
|
|
14
16
|
## What this is
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- A declared field arriving with a wire type it does not accept is kept as
|
|
6
|
+
an unknown field and written back verbatim, where it used to raise
|
|
7
|
+
`DecodeError`: schema drift is not corruption, and the reference keeps it.
|
|
8
|
+
So is a map entry carrying more than a key and a value — an undeclared
|
|
9
|
+
subfield, or a key or value with a wire type the entry does not accept —
|
|
10
|
+
kept whole among the parent message's unknown fields and left out of the
|
|
11
|
+
map. An entry that omits its key or its value decodes to that type's
|
|
12
|
+
default, an empty message for a message-typed value.
|
|
13
|
+
- Hostile input is bounded rather than fatal. Nesting deeper than
|
|
14
|
+
`Reader::MAX_DEPTH` (100, the reference's limit; nested messages, map
|
|
15
|
+
entries and groups count, a packed field does not) raises `DecodeError`
|
|
16
|
+
instead of overflowing the VM stack with `SystemStackError`. Field number
|
|
17
|
+
0 anywhere in the input, a group closed by an `END_GROUP` carrying another
|
|
18
|
+
number, and a proto3 `string` whose bytes are not valid UTF-8 raise
|
|
19
|
+
`DecodeError` too. A varint carrying bits above 64 is truncated rather
|
|
20
|
+
than read wide, as every implementation does, and a `sint32` is truncated
|
|
21
|
+
to 32 bits before its zigzag is undone.
|
|
22
|
+
- Decoding is about bytes, so a `Reader` reads its input whatever the String
|
|
23
|
+
is tagged: a non-binary one is read through a binary view made once on
|
|
24
|
+
construction, and the input is never modified. `encode` holds its buffer
|
|
25
|
+
to the same rule: an empty String of any encoding is retagged binary, and
|
|
26
|
+
one already holding text raises `ArgumentError` rather than widening the
|
|
27
|
+
bytes it appends into that encoding's characters.
|
|
28
|
+
- `Message#==`, `eql?` and `hash` compare the declared fields only. Unknown
|
|
29
|
+
fields are no longer part of the comparison, as the reference does not
|
|
30
|
+
compare them; they are still kept, still readable through
|
|
31
|
+
`unknown_fields`, and still written back by `encode`. Byte identity is
|
|
32
|
+
`a.encode == b.encode`.
|
|
33
|
+
- A `float` field narrows to single precision on assignment rather than on
|
|
34
|
+
encode, so what it reads back is what the wire carries and
|
|
35
|
+
`decode(encode(m)) == m` holds: `0.1` reads back `0.10000000149011612`,
|
|
36
|
+
`3.5e38` reads `Infinity`, `1e-50` reads `0.0`. A 32-bit NaN is still
|
|
37
|
+
written canonical (`7fc00000`) because Ruby's `pack("e")` drops the sign
|
|
38
|
+
and payload bits the reference preserves; that one is documented, not
|
|
39
|
+
fixed. `double` NaNs round-trip whole.
|
|
40
|
+
- A declaration the wire format has no form for raises `ArgumentError` where
|
|
41
|
+
it is written rather than at the first encode: a field number outside
|
|
42
|
+
1 ... 2²⁹−1 or inside 19000 ... 19999, which the specification reserves for
|
|
43
|
+
the implementation, and `packed: true` on a `string`, `bytes` or message
|
|
44
|
+
field.
|
|
45
|
+
- `Reader#read_packed` is back for packed fields, which are read in place
|
|
46
|
+
and cost no depth level (0.2.0's entry recorded it removed).
|
|
47
|
+
`Reader#read_key` returns a tag as the raw key holding number and wire
|
|
48
|
+
type together; `Reader#skip` takes the field number the value arrived
|
|
49
|
+
under, `skip(wire_type, number = nil)`, so a group is closed only by its
|
|
50
|
+
own number; `Field#accepts?(wire_type)`, `Wire.binary_buffer` and
|
|
51
|
+
`Wire::UINT64_MASK` are public.
|
|
52
|
+
- Decoding costs what it did on 0.2.0, guards included: the 36,000-metric
|
|
53
|
+
family decodes in 0.801 s where 0.2.0 takes 0.784 s on the same machine
|
|
54
|
+
and Ruby (4.0.7, five timed calls, same session). A field's wire type is
|
|
55
|
+
computed once, when the field is declared, rather than on every read,
|
|
56
|
+
where the per-field check had been paying for it twice. What it allocates
|
|
57
|
+
is unchanged, 1,404,005 objects for that family and one object to encode
|
|
58
|
+
it, and so are the budgets in `test/fast/protowire/allocations.rb`; the
|
|
59
|
+
benchmarks page carries the re-measured tables.
|
|
60
|
+
|
|
61
|
+
## 0.2.0
|
|
62
|
+
|
|
63
|
+
- Encoding allocates nothing per nested message, packed field or map
|
|
64
|
+
entry: each is written into the parent buffer behind a length prefix
|
|
65
|
+
filled in afterwards. A family of 36,000 metrics x 12 labels encodes in
|
|
66
|
+
one object, its output, where it took 647,000, and 22% faster.
|
|
67
|
+
`Wire.append_length_delimited_from` now appends in place and returns the
|
|
68
|
+
width of the prefix it wrote, a hint to pass back for the next value;
|
|
69
|
+
`Wire.reserve_length` and `Wire.close_length` are its pieces, and
|
|
70
|
+
`Wire.varint_size` and `Wire.append_bytes` are public.
|
|
71
|
+
- Fixed-width values pack with literal formats, so a double costs no Array
|
|
72
|
+
on Ruby 3.4+; text is appended with `String#append_as_bytes` there, tag
|
|
73
|
+
and one-byte size in the same call and no binary copy for UTF-8.
|
|
74
|
+
Zigzag no longer allocates for negative values.
|
|
75
|
+
- Decoding reads nested messages, packed fields and map entries in place
|
|
76
|
+
(`Reader#read_nested`), unpacks fixed-width values at an offset
|
|
77
|
+
(`Reader#read_fixed`) and splits tags without an Array, so it allocates
|
|
78
|
+
only the messages, containers and Strings it returns: the same family
|
|
79
|
+
decodes in 1.4M objects where it took 7.1M, 1.7x faster.
|
|
80
|
+
- `Message.new` no longer allocates an empty keyword Hash per message.
|
|
81
|
+
- Removed: `Reader#read_packed` (use `read_nested`) and the interpretive
|
|
82
|
+
`Field#encode`, which the compiled encoder had superseded.
|
|
83
|
+
- `test/fast/protowire/allocations.rb` holds the budgets; the Prometheus
|
|
84
|
+
client model (`fixtures/proto/metrics.proto`, upstream `client_model`)
|
|
85
|
+
joins the parity suite; `benchmark/messages.rb` measures encode, build
|
|
86
|
+
and decode against google-protobuf, published on the benchmarks page.
|
|
87
|
+
|
|
3
88
|
## 0.1.0
|
|
4
89
|
|
|
5
90
|
- Initial release: `Wire`, `Reader`, `Enum` and the `Message` DSL covering
|
data/README.md
CHANGED
|
@@ -50,6 +50,23 @@ and the output is byte-identical to what `protoc`-generated code and `google-pro
|
|
|
50
50
|
produce for the same values. The [tutorial](docs/tutorials/encode-a-message.md) walks
|
|
51
51
|
through a full schema and proves that.
|
|
52
52
|
|
|
53
|
+
## Performance
|
|
54
|
+
|
|
55
|
+
Measured against `google-protobuf` on the Prometheus client model, a family of 36,000
|
|
56
|
+
metrics with twelve labels each (Ruby 4.0.7; conditions and every table on the
|
|
57
|
+
[benchmarks page](docs/explanation/benchmarks.md)):
|
|
58
|
+
|
|
59
|
+
- Encoding allocates **one object, the output**, whatever the message's size or depth;
|
|
60
|
+
0.1.0 allocated 647,000 for this family. Decoding allocates only the messages,
|
|
61
|
+
containers and Strings it returns, 5x fewer than 0.1.0 and 1.3x faster.
|
|
62
|
+
- Built a message at a time, the way an exposition builds series, `google-protobuf`
|
|
63
|
+
leaves **504,001 native arenas and 225 MiB** behind for an 11.76 MB body and spends
|
|
64
|
+
1.77 s of every ten builds in GC; fast-protowire leaves 16.5 MiB, no arenas, and 0.27 s.
|
|
65
|
+
- `google-protobuf` is native, and 9 to 25x faster per operation on an existing tree
|
|
66
|
+
or one nested Hash. This gem trades that speed for memory that is roughly the size of
|
|
67
|
+
the output; fast-prometheus's scrape path goes further and writes series with `Wire`
|
|
68
|
+
directly, with no message per series at all.
|
|
69
|
+
|
|
53
70
|
## Documentation
|
|
54
71
|
|
|
55
72
|
### Tutorials
|
|
@@ -73,10 +90,12 @@ through a full schema and proves that.
|
|
|
73
90
|
|
|
74
91
|
- [Design: the wire format and nothing else](docs/explanation/design.md) — what google-protobuf costs per message, what this gem does instead, and what it leaves out.
|
|
75
92
|
- [How encoding works](docs/explanation/encoding.md) — compiled encoders, buffers, field order, presence, and decoding.
|
|
93
|
+
- [Benchmarks](docs/explanation/benchmarks.md) — encode, build and decode against google-protobuf, what changed since 0.1.0, and how to reproduce them.
|
|
76
94
|
|
|
77
95
|
## Development
|
|
78
96
|
|
|
79
97
|
```bash
|
|
80
98
|
bundle exec sus
|
|
81
99
|
bundle exec rubocop
|
|
100
|
+
BENCH_QUICK=1 bundle exec ruby benchmark/messages.rb # encode, build and decode against google-protobuf
|
|
82
101
|
```
|
data/lib/fast/protowire/field.rb
CHANGED
|
@@ -30,9 +30,15 @@ module Fast
|
|
|
30
30
|
|
|
31
31
|
MAP_KEY_TYPES = %i[int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string].freeze
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# The field numbers a tag can carry, less the range the specification
|
|
34
|
+
# reserves for the implementation.
|
|
35
|
+
NUMBERS = (1..(1 << 29) - 1)
|
|
36
|
+
RESERVED_NUMBERS = (19_000..19_999)
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
private_constant :SCALAR_WIRE_TYPES, :INTEGER_RANGES, :FIXED_FORMATS, :PACKABLE, :MAP_KEY_TYPES,
|
|
39
|
+
:NUMBERS, :RESERVED_NUMBERS
|
|
40
|
+
|
|
41
|
+
attr_reader :name, :number, :type, :rule, :oneof, :ivar, :enum, :wire_type
|
|
36
42
|
|
|
37
43
|
# +type+ is a scalar Symbol, an Enum module, a Message class, or a
|
|
38
44
|
# String / Proc naming a Message class resolved on first use (for
|
|
@@ -40,13 +46,17 @@ module Fast
|
|
|
40
46
|
# :repeated or :map.
|
|
41
47
|
def initialize(name, type, number, rule:, owner:, packed: nil, default: nil, oneof: nil, key_type: nil)
|
|
42
48
|
@name = name
|
|
43
|
-
@number = number
|
|
49
|
+
@number = validate_number(number)
|
|
44
50
|
@rule = rule
|
|
45
51
|
@owner = owner
|
|
46
52
|
@oneof = oneof
|
|
47
53
|
@ivar = :"@#{name}"
|
|
48
54
|
resolve_type(type)
|
|
55
|
+
@wire_type = rule == :map ? Wire::LENGTH_DELIMITED : SCALAR_WIRE_TYPES.fetch(@type)
|
|
56
|
+
raise ArgumentError, "#{name}: a #{@type} field cannot be packed" if packed && !packable?
|
|
57
|
+
|
|
49
58
|
@packed = rule == :repeated && (packed.nil? ? owner.syntax == :proto3 && packable? : packed)
|
|
59
|
+
@strict_utf8 = @type == :string && owner.syntax == :proto3
|
|
50
60
|
@default = default
|
|
51
61
|
@tag = Wire.tag(number, packed? ? Wire::LENGTH_DELIMITED : wire_type)
|
|
52
62
|
return unless map?
|
|
@@ -55,6 +65,9 @@ module Fast
|
|
|
55
65
|
|
|
56
66
|
@key_field = Field.new(:key, key_type, 1, rule: :optional, owner: owner)
|
|
57
67
|
@value_field = Field.new(:value, type, 2, rule: :optional, owner: owner)
|
|
68
|
+
# The only two tags an entry accepts: number and wire type together.
|
|
69
|
+
@key_tag = (1 << 3) | @key_field.wire_type
|
|
70
|
+
@value_tag = (2 << 3) | @value_field.wire_type
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
def message_class
|
|
@@ -65,10 +78,6 @@ module Fast
|
|
|
65
78
|
end
|
|
66
79
|
end
|
|
67
80
|
|
|
68
|
-
def wire_type
|
|
69
|
-
map? ? Wire::LENGTH_DELIMITED : SCALAR_WIRE_TYPES.fetch(type)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
81
|
def repeated?
|
|
73
82
|
rule == :repeated
|
|
74
83
|
end
|
|
@@ -85,6 +94,13 @@ module Fast
|
|
|
85
94
|
PACKABLE.include?(type)
|
|
86
95
|
end
|
|
87
96
|
|
|
97
|
+
# Whether a value of +wire_type+ can be read into this field: its own,
|
|
98
|
+
# plus the packed form of a packable repeated field however it was
|
|
99
|
+
# declared. Anything else is schema drift, kept as an unknown field.
|
|
100
|
+
def accepts?(wire_type)
|
|
101
|
+
wire_type == self.wire_type || (repeated? && packable? && wire_type == Wire::LENGTH_DELIMITED)
|
|
102
|
+
end
|
|
103
|
+
|
|
88
104
|
# Whether an unset field is distinguishable from one set to its default.
|
|
89
105
|
def explicit_presence?
|
|
90
106
|
rule != :implicit || !oneof.nil? || type == :message
|
|
@@ -141,23 +157,17 @@ module Fast
|
|
|
141
157
|
end
|
|
142
158
|
end
|
|
143
159
|
|
|
144
|
-
def encode(buffer, value)
|
|
145
|
-
case rule
|
|
146
|
-
when :repeated then encode_repeated(buffer, value)
|
|
147
|
-
when :map then encode_map(buffer, value)
|
|
148
|
-
else encode_one(buffer, value)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
160
|
# Reads one occurrence of this field into +current+ (the value already
|
|
153
|
-
# held) and returns the value to store.
|
|
154
|
-
|
|
161
|
+
# held) and returns the value to store. +message+ is the message being
|
|
162
|
+
# read into, for a map entry it cannot accept: that belongs among the
|
|
163
|
+
# message's unknown fields rather than in the map.
|
|
164
|
+
def decode(reader, wire_type, current, message)
|
|
155
165
|
case rule
|
|
156
166
|
when :repeated then decode_repeated(reader, wire_type, current)
|
|
157
|
-
when :map then decode_map_entry(reader, wire_type, current)
|
|
167
|
+
when :map then decode_map_entry(reader, wire_type, current, message)
|
|
158
168
|
else
|
|
159
169
|
if type == :message && current
|
|
160
|
-
|
|
170
|
+
expect(reader, wire_type).read_nested { |nested| current.merge_from(nested) }
|
|
161
171
|
else
|
|
162
172
|
read_one(reader, wire_type)
|
|
163
173
|
end
|
|
@@ -166,6 +176,15 @@ module Fast
|
|
|
166
176
|
|
|
167
177
|
protected
|
|
168
178
|
|
|
179
|
+
# A second occurrence of a message field merges into the first; of
|
|
180
|
+
# anything else, replaces it. (A map's value field is read this way by
|
|
181
|
+
# the map field, which is why this is protected rather than private.)
|
|
182
|
+
def decode_singular(reader, wire_type, current)
|
|
183
|
+
return read_one(reader, wire_type) unless type == :message && current
|
|
184
|
+
|
|
185
|
+
expect(reader, wire_type).read_nested { |nested| current.merge_from(nested) }
|
|
186
|
+
end
|
|
187
|
+
|
|
169
188
|
def coerce_one(value)
|
|
170
189
|
case type
|
|
171
190
|
when :string then coerce_string(value)
|
|
@@ -178,28 +197,55 @@ module Fast
|
|
|
178
197
|
end
|
|
179
198
|
end
|
|
180
199
|
|
|
181
|
-
def encode_one(buffer, value)
|
|
182
|
-
case type
|
|
183
|
-
when :message then Wire.append_length_delimited(buffer, @tag, value.encode)
|
|
184
|
-
when :string, :bytes then Wire.append_length_delimited(buffer, @tag, value)
|
|
185
|
-
else
|
|
186
|
-
buffer << @tag
|
|
187
|
-
append_scalar(buffer, value)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
200
|
def read_one(reader, wire_type)
|
|
192
201
|
expect(reader, wire_type)
|
|
193
202
|
case type
|
|
194
|
-
when :message then message_class.
|
|
195
|
-
when :string
|
|
203
|
+
when :message then reader.read_nested { |nested| message_class.new.merge_from(nested) }
|
|
204
|
+
when :string
|
|
205
|
+
@strict_utf8 ? read_string(reader) : reader.read_length_delimited.force_encoding(Encoding::UTF_8)
|
|
196
206
|
when :bytes then reader.read_length_delimited
|
|
197
207
|
else read_scalar(reader)
|
|
198
208
|
end
|
|
199
209
|
end
|
|
200
210
|
|
|
211
|
+
# What a map entry holds for this field when the entry omits it: the
|
|
212
|
+
# type's default, except that a message value is an empty instance, as
|
|
213
|
+
# the reference materialises one (and nil would not encode).
|
|
214
|
+
def entry_default
|
|
215
|
+
type == :message ? message_class.new : default_value
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Appends the tag and one value. A nested message is encoded straight
|
|
219
|
+
# into the buffer behind a length prefix filled in after it, with no
|
|
220
|
+
# buffer of its own; +width+ carries the prefix width from one value
|
|
221
|
+
# to the next, a hint shared by every encode of the field. (A map's
|
|
222
|
+
# key and value fields build the entry writer together, which is why
|
|
223
|
+
# this is protected rather than private.)
|
|
224
|
+
def writer
|
|
225
|
+
tag = @tag
|
|
226
|
+
case type
|
|
227
|
+
when :message
|
|
228
|
+
width = 1
|
|
229
|
+
->(buffer, value) { width = Wire.append_length_delimited_from(buffer, tag, width) { |b| value.encode(b) } }
|
|
230
|
+
when :string, :bytes then ->(buffer, value) { Wire.append_length_delimited(buffer, tag, value) }
|
|
231
|
+
else
|
|
232
|
+
scalar = scalar_writer
|
|
233
|
+
lambda do |buffer, value|
|
|
234
|
+
buffer << tag
|
|
235
|
+
scalar.call(buffer, value)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
201
240
|
private
|
|
202
241
|
|
|
242
|
+
def validate_number(number)
|
|
243
|
+
raise ArgumentError, "field number #{number} is reserved" if RESERVED_NUMBERS.cover?(number)
|
|
244
|
+
raise ArgumentError, "field number #{number} is outside #{NUMBERS}" unless NUMBERS.cover?(number)
|
|
245
|
+
|
|
246
|
+
number
|
|
247
|
+
end
|
|
248
|
+
|
|
203
249
|
def resolve_type(type)
|
|
204
250
|
case type
|
|
205
251
|
when Symbol
|
|
@@ -265,17 +311,19 @@ module Fast
|
|
|
265
311
|
end
|
|
266
312
|
end
|
|
267
313
|
|
|
314
|
+
# Packed: every value bare, behind one tag and length.
|
|
268
315
|
def repeated_step(ivar)
|
|
269
316
|
if packed?
|
|
270
317
|
tag = @tag
|
|
271
318
|
scalar = scalar_writer
|
|
319
|
+
width = 1
|
|
272
320
|
lambda do |message, buffer|
|
|
273
321
|
values = message.instance_variable_get(ivar)
|
|
274
322
|
next if values.empty?
|
|
275
323
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
324
|
+
width = Wire.append_length_delimited_from(buffer, tag, width) do |b|
|
|
325
|
+
values.each { |value| scalar.call(b, value) }
|
|
326
|
+
end
|
|
279
327
|
end
|
|
280
328
|
else
|
|
281
329
|
write = writer
|
|
@@ -283,36 +331,26 @@ module Fast
|
|
|
283
331
|
end
|
|
284
332
|
end
|
|
285
333
|
|
|
334
|
+
# Each entry is a message { key = 1; value = 2 }.
|
|
286
335
|
def map_step(ivar)
|
|
287
336
|
tag = @tag
|
|
288
|
-
key =
|
|
289
|
-
|
|
337
|
+
key, value = entry_writers
|
|
338
|
+
width = 1
|
|
290
339
|
lambda do |message, buffer|
|
|
291
340
|
message.instance_variable_get(ivar).each do |k, v|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
341
|
+
width = Wire.append_length_delimited_from(buffer, tag, width) do |b|
|
|
342
|
+
key.call(b, k)
|
|
343
|
+
value.call(b, v)
|
|
344
|
+
end
|
|
296
345
|
end
|
|
297
346
|
end
|
|
298
347
|
end
|
|
299
348
|
|
|
300
|
-
#
|
|
301
|
-
def writer
|
|
302
|
-
tag = @tag
|
|
303
|
-
case type
|
|
304
|
-
when :message then ->(buffer, value) { Wire.append_length_delimited(buffer, tag, value.encode) }
|
|
305
|
-
when :string, :bytes then ->(buffer, value) { Wire.append_length_delimited(buffer, tag, value) }
|
|
306
|
-
else
|
|
307
|
-
scalar = scalar_writer
|
|
308
|
-
lambda do |buffer, value|
|
|
309
|
-
buffer << tag
|
|
310
|
-
scalar.call(buffer, value)
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
end
|
|
349
|
+
# -- writers -----------------------------------------------------------
|
|
314
350
|
|
|
315
351
|
# Appends one scalar value without its tag, as packed fields need.
|
|
352
|
+
# Fixed-width types get a lambda each so the pack format is a literal:
|
|
353
|
+
# Ruby elides the Array in [value].pack(literal, buffer:), and only then.
|
|
316
354
|
def scalar_writer
|
|
317
355
|
case type
|
|
318
356
|
when :int32, :int64, :uint32, :uint64 then ->(buffer, value) { Wire.append_varint(buffer, value) }
|
|
@@ -322,9 +360,12 @@ module Fast
|
|
|
322
360
|
when :enum
|
|
323
361
|
enum = @enum
|
|
324
362
|
->(buffer, value) { Wire.append_varint(buffer, value.is_a?(Symbol) ? enum.resolve(value) : value) }
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
363
|
+
when :double then ->(buffer, value) { [value].pack("E", buffer: buffer) }
|
|
364
|
+
when :float then ->(buffer, value) { [value].pack("e", buffer: buffer) }
|
|
365
|
+
when :fixed32 then ->(buffer, value) { [value].pack("L<", buffer: buffer) }
|
|
366
|
+
when :sfixed32 then ->(buffer, value) { [value].pack("l<", buffer: buffer) }
|
|
367
|
+
when :fixed64 then ->(buffer, value) { [value].pack("Q<", buffer: buffer) }
|
|
368
|
+
when :sfixed64 then ->(buffer, value) { [value].pack("q<", buffer: buffer) }
|
|
328
369
|
end
|
|
329
370
|
end
|
|
330
371
|
|
|
@@ -365,10 +406,13 @@ module Fast
|
|
|
365
406
|
value.encoding == Encoding::BINARY ? value : value.b
|
|
366
407
|
end
|
|
367
408
|
|
|
409
|
+
# A +float+ holds what the wire holds: the value narrowed to single
|
|
410
|
+
# precision on assignment, as the reference narrows it, so what is
|
|
411
|
+
# read back is what a decode of the encoding reads.
|
|
368
412
|
def coerce_float(value)
|
|
369
413
|
raise ::TypeError, "#{name} expects a number, got #{value.class}" unless value.is_a?(Numeric)
|
|
370
414
|
|
|
371
|
-
value.to_f
|
|
415
|
+
type == :float ? [value.to_f].pack("e").unpack1("e") : value.to_f
|
|
372
416
|
end
|
|
373
417
|
|
|
374
418
|
def coerce_bool(value)
|
|
@@ -409,66 +453,68 @@ module Fast
|
|
|
409
453
|
integer
|
|
410
454
|
end
|
|
411
455
|
|
|
412
|
-
# -- encoding ----------------------------------------------------------
|
|
413
|
-
|
|
414
|
-
def encode_repeated(buffer, values)
|
|
415
|
-
return if values.empty?
|
|
416
|
-
|
|
417
|
-
if packed?
|
|
418
|
-
Wire.append_length_delimited_from(buffer, @tag) { |b| values.each { |v| append_scalar(b, v) } }
|
|
419
|
-
else
|
|
420
|
-
values.each { |v| encode_one(buffer, v) }
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
def encode_map(buffer, hash)
|
|
425
|
-
hash.each do |key, value|
|
|
426
|
-
Wire.append_length_delimited_from(buffer, @tag) do |entry|
|
|
427
|
-
@key_field.encode_one(entry, key)
|
|
428
|
-
@value_field.encode_one(entry, value)
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
def append_scalar(buffer, value)
|
|
434
|
-
case type
|
|
435
|
-
when :int32, :int64, :uint32, :uint64 then Wire.append_varint(buffer, value)
|
|
436
|
-
when :sint32 then Wire.append_varint(buffer, Wire.zigzag32(value))
|
|
437
|
-
when :sint64 then Wire.append_varint(buffer, Wire.zigzag64(value))
|
|
438
|
-
when :bool then buffer << (value ? 1 : 0)
|
|
439
|
-
when :enum then Wire.append_varint(buffer, enum_number(value))
|
|
440
|
-
else buffer << [value].pack(FIXED_FORMATS.fetch(type))
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
|
|
444
456
|
# -- decoding ----------------------------------------------------------
|
|
445
457
|
|
|
446
458
|
def decode_repeated(reader, wire_type, values)
|
|
447
459
|
if packable? && wire_type == Wire::LENGTH_DELIMITED
|
|
448
|
-
packed
|
|
449
|
-
values << read_scalar(packed) until packed.eof?
|
|
460
|
+
reader.read_packed { |packed| values << read_scalar(packed) until packed.eof? }
|
|
450
461
|
else
|
|
451
462
|
values << read_one(reader, wire_type)
|
|
452
463
|
end
|
|
453
464
|
values
|
|
454
465
|
end
|
|
455
466
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
467
|
+
# An entry the map accepts holds its key, its value, or both; one that
|
|
468
|
+
# carries anything else — an undeclared subfield, or the key or value
|
|
469
|
+
# with a wire type the entry does not accept — is not a map entry at
|
|
470
|
+
# all. The reference keeps such an entry among the parent message's
|
|
471
|
+
# unknown fields and leaves the map alone, so this does too.
|
|
472
|
+
def decode_map_entry(reader, wire_type, hash, message)
|
|
473
|
+
expect(reader, wire_type).read_nested do |entry|
|
|
474
|
+
key = nil
|
|
475
|
+
value = nil
|
|
476
|
+
unknown = nil
|
|
477
|
+
until entry.eof?
|
|
478
|
+
tag = entry.read_key
|
|
479
|
+
case tag
|
|
480
|
+
when @key_tag then key = @key_field.read_one(entry, tag & 0x7)
|
|
481
|
+
when @value_tag then value = @value_field.decode_singular(entry, tag & 0x7, value)
|
|
482
|
+
else
|
|
483
|
+
Wire.append_varint(unknown ||= String.new, tag)
|
|
484
|
+
unknown << entry.skip(tag & 0x7, tag >> 3)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
if unknown
|
|
488
|
+
keep_entry(message, key, value, unknown)
|
|
489
|
+
else
|
|
490
|
+
# An absent key or value reads as its type's default, an empty
|
|
491
|
+
# message for a message-typed value, as the reference
|
|
492
|
+
# materialises one. (Only nil is absent: false is a bool key.)
|
|
493
|
+
hash[key.nil? ? @key_field.default_value : key] = value.nil? ? @value_field.entry_default : value
|
|
466
494
|
end
|
|
467
495
|
end
|
|
468
|
-
hash[key] = value
|
|
469
496
|
hash
|
|
470
497
|
end
|
|
471
498
|
|
|
499
|
+
# A rejected entry, into the message's unknown fields (its own buffer,
|
|
500
|
+
# made here when the message has none yet) and written the way the
|
|
501
|
+
# reference writes one: the subfields it did carry, in number order and
|
|
502
|
+
# omitted when they are at their default — a message value is written
|
|
503
|
+
# whenever it was there — then the bytes it carried besides.
|
|
504
|
+
def keep_entry(message, key, value, unknown)
|
|
505
|
+
buffer = message.unknown_fields || message.instance_variable_set(:@unknown_fields, String.new)
|
|
506
|
+
write_key, write_value = entry_writers
|
|
507
|
+
Wire.append_length_delimited_from(buffer, @tag) do |b|
|
|
508
|
+
write_key.call(b, key) unless key.nil? || @key_field.omit?(key)
|
|
509
|
+
write_value.call(b, value) unless value.nil? || @value_field.omit?(value)
|
|
510
|
+
b << unknown
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def entry_writers
|
|
515
|
+
@entry_writers ||= [@key_field.writer, @value_field.writer]
|
|
516
|
+
end
|
|
517
|
+
|
|
472
518
|
def expect(reader, wire_type)
|
|
473
519
|
return reader if wire_type == self.wire_type
|
|
474
520
|
|
|
@@ -482,20 +528,27 @@ module Fast
|
|
|
482
528
|
when :int64 then signed(reader.read_varint, 64)
|
|
483
529
|
when :uint32 then reader.read_varint & 0xFFFF_FFFF
|
|
484
530
|
when :uint64 then reader.read_varint
|
|
485
|
-
when :sint32
|
|
531
|
+
when :sint32 then Wire.unzigzag(reader.read_varint & 0xFFFF_FFFF)
|
|
532
|
+
when :sint64 then Wire.unzigzag(reader.read_varint)
|
|
486
533
|
when :bool then reader.read_varint != 0
|
|
487
|
-
when :double, :fixed64, :sfixed64 then reader.
|
|
488
|
-
else reader.
|
|
534
|
+
when :double, :fixed64, :sfixed64 then reader.read_fixed(FIXED_FORMATS.fetch(type), 8)
|
|
535
|
+
else reader.read_fixed(FIXED_FORMATS.fetch(type), 4)
|
|
489
536
|
end
|
|
490
537
|
end
|
|
491
538
|
|
|
539
|
+
# proto3 requires a parser to reject a string field that is not valid
|
|
540
|
+
# UTF-8; proto2 does not, and the reference keeps it.
|
|
541
|
+
def read_string(reader)
|
|
542
|
+
string = reader.read_length_delimited.force_encoding(Encoding::UTF_8)
|
|
543
|
+
raise DecodeError, "#{name}: string is not valid UTF-8" if @strict_utf8 && !string.valid_encoding?
|
|
544
|
+
|
|
545
|
+
string
|
|
546
|
+
end
|
|
547
|
+
|
|
492
548
|
def signed(value, bits)
|
|
493
549
|
value &= (1 << bits) - 1
|
|
494
550
|
value >= (1 << (bits - 1)) ? value - (1 << bits) : value
|
|
495
551
|
end
|
|
496
|
-
|
|
497
|
-
# A map's key and value fields build the entry writer together.
|
|
498
|
-
protected :writer
|
|
499
552
|
end
|
|
500
553
|
end
|
|
501
554
|
end
|
|
@@ -97,6 +97,7 @@ module Fast
|
|
|
97
97
|
def compile_encoder
|
|
98
98
|
steps = sorted_fields.map(&:encoder_step)
|
|
99
99
|
define_method(:encode) do |buffer = String.new|
|
|
100
|
+
Wire.binary_buffer(buffer)
|
|
100
101
|
steps.each { |step| step.call(self, buffer) }
|
|
101
102
|
buffer << @unknown_fields if @unknown_fields
|
|
102
103
|
buffer
|
|
@@ -143,11 +144,12 @@ module Fast
|
|
|
143
144
|
|
|
144
145
|
# Scalar and message ivars stay unset until written (an unset ivar reads
|
|
145
146
|
# as nil); repeated and map fields get their container up front so it
|
|
146
|
-
# can be mutated in place.
|
|
147
|
-
|
|
147
|
+
# can be mutated in place. +attributes+ is a Hash, or the keywords a
|
|
148
|
+
# caller writes instead of one.
|
|
149
|
+
def initialize(attributes = nil)
|
|
148
150
|
self.class.container_fields.each { |field| instance_variable_set(field.ivar, field.default_value) }
|
|
149
151
|
@unknown_fields = nil
|
|
150
|
-
|
|
152
|
+
attributes&.each do |name, value|
|
|
151
153
|
field = self.class.fields[name.to_sym]
|
|
152
154
|
raise ArgumentError, "unknown field #{name.inspect} for #{self.class}" unless field
|
|
153
155
|
|
|
@@ -171,15 +173,23 @@ module Fast
|
|
|
171
173
|
|
|
172
174
|
# Reads fields from +reader+ into this message (protobuf merge
|
|
173
175
|
# semantics: later scalars win, repeated fields append, nested messages
|
|
174
|
-
# merge) and returns self.
|
|
176
|
+
# merge) and returns self. A declared field arriving with a wire type
|
|
177
|
+
# it does not accept is schema drift rather than corruption, so it is
|
|
178
|
+
# kept as an unknown field, as the reference does; so is a map entry
|
|
179
|
+
# carrying a subfield the entry does not accept, kept whole.
|
|
175
180
|
def merge_from(reader)
|
|
176
181
|
until reader.eof?
|
|
177
|
-
|
|
182
|
+
key = reader.read_varint
|
|
183
|
+
wire_type = key & 0x7
|
|
184
|
+
number = key >> 3
|
|
185
|
+
raise DecodeError, "field number 0" if number.zero? # as Reader#read_key, inlined for the loop
|
|
186
|
+
|
|
178
187
|
field = self.class.fields_by_number[number]
|
|
179
|
-
if field
|
|
180
|
-
write_field(field, field.decode(reader, wire_type, instance_variable_get(field.ivar)))
|
|
188
|
+
if field&.accepts?(wire_type)
|
|
189
|
+
write_field(field, field.decode(reader, wire_type, instance_variable_get(field.ivar), self))
|
|
181
190
|
else
|
|
182
|
-
(@unknown_fields ||= String.new
|
|
191
|
+
Wire.append_varint(@unknown_fields ||= String.new, key)
|
|
192
|
+
@unknown_fields << reader.skip(wire_type, number)
|
|
183
193
|
end
|
|
184
194
|
end
|
|
185
195
|
self
|
|
@@ -194,8 +204,12 @@ module Fast
|
|
|
194
204
|
end
|
|
195
205
|
end
|
|
196
206
|
|
|
197
|
-
# Two messages are equal when every field reads the same; an
|
|
198
|
-
# field set to its default is the same as one never set.
|
|
207
|
+
# Two messages are equal when every declared field reads the same; an
|
|
208
|
+
# implicit field set to its default is the same as one never set.
|
|
209
|
+
# Unknown fields are not compared, as the reference does not compare
|
|
210
|
+
# them: two messages that differ only in what they carry for fields
|
|
211
|
+
# this class never declared are the same message. Byte identity is
|
|
212
|
+
# +a.encode == b.encode+.
|
|
199
213
|
def ==(other)
|
|
200
214
|
other.class == self.class && comparable_values == other.comparable_values
|
|
201
215
|
end
|
|
@@ -225,11 +239,10 @@ module Fast
|
|
|
225
239
|
protected
|
|
226
240
|
|
|
227
241
|
def comparable_values
|
|
228
|
-
|
|
242
|
+
self.class.fields.each_value.map do |field|
|
|
229
243
|
value = instance_variable_get(field.ivar)
|
|
230
244
|
value.nil? && !field.explicit_presence? ? field.default_value : value
|
|
231
245
|
end
|
|
232
|
-
values << @unknown_fields
|
|
233
246
|
end
|
|
234
247
|
|
|
235
248
|
private
|
|
@@ -8,10 +8,18 @@ module Fast
|
|
|
8
8
|
# A cursor over an encoded message. Reads the primitive wire values and
|
|
9
9
|
# skips what it is not asked to interpret.
|
|
10
10
|
class Reader
|
|
11
|
+
# How deep nested messages and groups may go before the input is taken
|
|
12
|
+
# to be hostile rather than deep; the reference's limit.
|
|
13
|
+
MAX_DEPTH = 100
|
|
14
|
+
|
|
15
|
+
# Decoding is about bytes, so the input is read as bytes: a String
|
|
16
|
+
# tagged anything else is copied into a binary view once, here, rather
|
|
17
|
+
# than leaking its encoding into every slice handed out below.
|
|
11
18
|
def initialize(buffer, position = 0, limit = buffer.bytesize)
|
|
12
|
-
@buffer = buffer
|
|
19
|
+
@buffer = buffer.encoding == Encoding::BINARY ? buffer : buffer.b
|
|
13
20
|
@position = position
|
|
14
21
|
@limit = limit
|
|
22
|
+
@depth = 0
|
|
15
23
|
end
|
|
16
24
|
|
|
17
25
|
attr_reader :position
|
|
@@ -22,32 +30,56 @@ module Fast
|
|
|
22
30
|
|
|
23
31
|
# Returns [field number, wire type].
|
|
24
32
|
def read_tag
|
|
25
|
-
key =
|
|
33
|
+
key = read_key
|
|
26
34
|
[key >> 3, key & 0x7]
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# The next field's key, the varint holding its number and wire type
|
|
38
|
+
# together. Field number 0 exists on no wire: the reference rejects it
|
|
39
|
+
# wherever it appears, and so does this.
|
|
40
|
+
def read_key
|
|
41
|
+
key = read_varint
|
|
42
|
+
raise DecodeError, "field number 0" if key < 8
|
|
43
|
+
|
|
44
|
+
key
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Most varints (tags, small lengths) are one byte; the loop is only
|
|
48
|
+
# entered past it, and is a bare while because Kernel#loop costs an
|
|
49
|
+
# object per call.
|
|
29
50
|
def read_varint
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
loop do
|
|
33
|
-
raise DecodeError, "truncated varint" if @position >= @limit
|
|
51
|
+
byte = read_byte
|
|
52
|
+
return byte if byte < 0x80
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
result = byte & 0x7f
|
|
55
|
+
shift = 7
|
|
56
|
+
while byte >= 0x80
|
|
57
|
+
raise DecodeError, "varint too long" if shift > 63
|
|
39
58
|
|
|
59
|
+
byte = read_byte
|
|
60
|
+
result |= (byte & 0x7f) << shift
|
|
40
61
|
shift += 7
|
|
41
|
-
raise DecodeError, "varint too long" if shift > 63
|
|
42
62
|
end
|
|
63
|
+
# Only a tenth byte can carry bits above 64; every implementation
|
|
64
|
+
# truncates them rather than reading a wider value.
|
|
65
|
+
shift > 63 ? result & Wire::UINT64_MASK : result
|
|
43
66
|
end
|
|
44
67
|
|
|
45
68
|
def read_fixed32
|
|
46
|
-
|
|
69
|
+
read_fixed("L<", 4)
|
|
47
70
|
end
|
|
48
71
|
|
|
49
72
|
def read_fixed64
|
|
50
|
-
|
|
73
|
+
read_fixed("Q<", 8)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reads +width+ bytes as one value of the pack +format+, in place.
|
|
77
|
+
def read_fixed(format, width)
|
|
78
|
+
raise DecodeError, "truncated field" if @position + width > @limit
|
|
79
|
+
|
|
80
|
+
value = @buffer.unpack1(format, offset: @position)
|
|
81
|
+
@position += width
|
|
82
|
+
value
|
|
51
83
|
end
|
|
52
84
|
|
|
53
85
|
def read_bytes(length)
|
|
@@ -62,25 +94,53 @@ module Fast
|
|
|
62
94
|
read_bytes(read_varint)
|
|
63
95
|
end
|
|
64
96
|
|
|
65
|
-
#
|
|
97
|
+
# Bounds the reader to the next length-delimited value for the block
|
|
98
|
+
# and returns the block's result, so a packed repeated field is read
|
|
99
|
+
# in place, with no copy of its bytes.
|
|
66
100
|
def read_packed
|
|
67
101
|
length = read_varint
|
|
68
|
-
|
|
102
|
+
limit = @limit
|
|
103
|
+
raise DecodeError, "truncated field" if @position + length > limit
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
@limit = @position + length
|
|
106
|
+
result = yield self
|
|
107
|
+
@position = @limit
|
|
108
|
+
@limit = limit
|
|
109
|
+
result
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The same, for a value read by recursing into it — a nested message or
|
|
113
|
+
# a map entry — counting the nesting so deeply nested input raises
|
|
114
|
+
# rather than overflowing the VM stack. A packed field holds scalars
|
|
115
|
+
# and does not recurse, so it costs no level and reads through
|
|
116
|
+
# read_packed; the bounding is spelled out twice rather than delegated
|
|
117
|
+
# because every nested message pays for the call.
|
|
118
|
+
def read_nested
|
|
119
|
+
length = read_varint
|
|
120
|
+
limit = @limit
|
|
121
|
+
raise DecodeError, "truncated field" if @position + length > limit
|
|
122
|
+
raise DecodeError, "nested deeper than #{MAX_DEPTH}" if @depth >= MAX_DEPTH
|
|
123
|
+
|
|
124
|
+
@limit = @position + length
|
|
125
|
+
@depth += 1
|
|
126
|
+
result = yield self
|
|
127
|
+
@depth -= 1
|
|
128
|
+
@position = @limit
|
|
129
|
+
@limit = limit
|
|
130
|
+
result
|
|
73
131
|
end
|
|
74
132
|
|
|
75
133
|
# Skips one value of +wire_type+ and returns its raw bytes, so unknown
|
|
76
|
-
# fields survive a decode/encode round trip.
|
|
77
|
-
|
|
134
|
+
# fields survive a decode/encode round trip. +number+ is the field
|
|
135
|
+
# number the value arrived under: a group is closed by its own number
|
|
136
|
+
# and nothing else. Without one, any END_GROUP closes it.
|
|
137
|
+
def skip(wire_type, number = nil)
|
|
78
138
|
start = @position
|
|
79
139
|
case wire_type
|
|
80
140
|
when Wire::VARINT then read_varint
|
|
81
141
|
when Wire::FIXED64 then read_bytes(8)
|
|
82
142
|
when Wire::LENGTH_DELIMITED then read_length_delimited
|
|
83
|
-
when Wire::START_GROUP then skip_group
|
|
143
|
+
when Wire::START_GROUP then skip_group(number)
|
|
84
144
|
when Wire::FIXED32 then read_bytes(4)
|
|
85
145
|
else raise DecodeError, "unknown wire type #{wire_type}"
|
|
86
146
|
end
|
|
@@ -89,14 +149,28 @@ module Fast
|
|
|
89
149
|
|
|
90
150
|
private
|
|
91
151
|
|
|
92
|
-
def
|
|
152
|
+
def read_byte
|
|
153
|
+
raise DecodeError, "truncated varint" if @position >= @limit
|
|
154
|
+
|
|
155
|
+
byte = @buffer.getbyte(@position)
|
|
156
|
+
@position += 1
|
|
157
|
+
byte
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def skip_group(number)
|
|
161
|
+
raise DecodeError, "nested deeper than #{MAX_DEPTH}" if @depth >= MAX_DEPTH
|
|
162
|
+
|
|
163
|
+
@depth += 1
|
|
93
164
|
loop do
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
165
|
+
inner, wire_type = read_tag
|
|
166
|
+
if wire_type == Wire::END_GROUP
|
|
167
|
+
raise DecodeError, "group #{number} closed by #{inner}" unless number.nil? || inner == number
|
|
97
168
|
|
|
98
|
-
|
|
169
|
+
break
|
|
170
|
+
end
|
|
171
|
+
skip(wire_type, inner)
|
|
99
172
|
end
|
|
173
|
+
@depth -= 1
|
|
100
174
|
end
|
|
101
175
|
end
|
|
102
176
|
end
|
data/lib/fast/protowire/wire.rb
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module Fast
|
|
4
4
|
module Protowire
|
|
5
5
|
# The wire format itself: tags, varints, zigzag, fixed-width values and
|
|
6
|
-
# length prefixes, appended to a binary String buffer
|
|
7
|
-
#
|
|
6
|
+
# length prefixes, appended to a binary String buffer, and the size each
|
|
7
|
+
# takes so a length prefix can be written before its payload. Everything
|
|
8
|
+
# above this module is bookkeeping about which field gets which of these.
|
|
8
9
|
module Wire
|
|
9
10
|
VARINT = 0
|
|
10
11
|
FIXED64 = 1
|
|
@@ -14,10 +15,23 @@ module Fast
|
|
|
14
15
|
FIXED32 = 5
|
|
15
16
|
|
|
16
17
|
UINT64_MASK = (1 << 64) - 1
|
|
17
|
-
|
|
18
|
+
# Zero bytes a length prefix is widened to, by varint width.
|
|
19
|
+
PLACEHOLDERS = Array.new(11) { |width| ("\0" * width).b.freeze }.freeze
|
|
20
|
+
private_constant :PLACEHOLDERS
|
|
18
21
|
|
|
19
22
|
module_function
|
|
20
23
|
|
|
24
|
+
# A buffer written to here must be binary: appending a byte to a text
|
|
25
|
+
# String appends that encoding's character for it instead, silently
|
|
26
|
+
# writing something else. An empty buffer is simply retagged, so the
|
|
27
|
+
# usual ways of making one (+"", String.new("")) still work.
|
|
28
|
+
def binary_buffer(buffer)
|
|
29
|
+
return buffer if buffer.encoding == Encoding::BINARY
|
|
30
|
+
raise ArgumentError, "buffer must be a binary String, got #{buffer.encoding}" unless buffer.empty?
|
|
31
|
+
|
|
32
|
+
buffer.force_encoding(Encoding::BINARY)
|
|
33
|
+
end
|
|
34
|
+
|
|
21
35
|
# The key for +number+ / +wire_type+ as frozen bytes.
|
|
22
36
|
def tag(number, wire_type)
|
|
23
37
|
varint((number << 3) | wire_type).freeze
|
|
@@ -38,34 +52,100 @@ module Fast
|
|
|
38
52
|
buffer << value
|
|
39
53
|
end
|
|
40
54
|
|
|
55
|
+
# Bytes append_varint writes for +value+.
|
|
56
|
+
def varint_size(value)
|
|
57
|
+
return 10 if value.negative?
|
|
58
|
+
|
|
59
|
+
size = 1
|
|
60
|
+
while value > 0x7f
|
|
61
|
+
value >>= 7
|
|
62
|
+
size += 1
|
|
63
|
+
end
|
|
64
|
+
size
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# (* 2 rather than << 1: a left shift of a negative Fixnum allocates.)
|
|
41
68
|
def zigzag32(value)
|
|
42
|
-
(value
|
|
69
|
+
(value * 2) ^ (value >> 31)
|
|
43
70
|
end
|
|
44
71
|
|
|
45
72
|
def zigzag64(value)
|
|
46
|
-
(value
|
|
73
|
+
(value * 2) ^ (value >> 63)
|
|
47
74
|
end
|
|
48
75
|
|
|
49
76
|
def unzigzag(value)
|
|
50
77
|
(value >> 1) ^ -(value & 1)
|
|
51
78
|
end
|
|
52
79
|
|
|
53
|
-
# A length-delimited field
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
80
|
+
# A length-delimited field whose payload the block appends to +buffer+
|
|
81
|
+
# itself, behind a length prefix filled in once the payload is there,
|
|
82
|
+
# so a nested message, packed field or map entry costs no buffer of
|
|
83
|
+
# its own. Returns the width of the prefix it wrote: a caller writing
|
|
84
|
+
# many alike values passes it back as +width+ and the prefix is
|
|
85
|
+
# rarely resized.
|
|
86
|
+
def append_length_delimited_from(buffer, tag, width = 1)
|
|
57
87
|
buffer << tag
|
|
58
|
-
|
|
59
|
-
buffer
|
|
88
|
+
start = reserve_length(buffer, width)
|
|
89
|
+
yield buffer
|
|
90
|
+
close_length(buffer, start, width)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Appends +width+ placeholder bytes for a length prefix and returns the
|
|
94
|
+
# position after them, for close_length.
|
|
95
|
+
def reserve_length(buffer, width = 1)
|
|
96
|
+
buffer << PLACEHOLDERS[width]
|
|
97
|
+
buffer.bytesize
|
|
60
98
|
end
|
|
61
99
|
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
# Writes the varint size of everything appended since +start+ into the
|
|
101
|
+
# +width+ placeholder bytes before it, first resizing the placeholder
|
|
102
|
+
# when the size needs a different width, which moves only the payload.
|
|
103
|
+
# Returns the width written. (Growing a String in place reallocates it
|
|
104
|
+
# to exactly the new size, so on a large buffer a resize costs far more
|
|
105
|
+
# than the bytes moved; hence the width hint.)
|
|
106
|
+
def close_length(buffer, start, width = 1)
|
|
107
|
+
length = buffer.bytesize - start
|
|
108
|
+
needed = length < 0x80 ? 1 : varint_size(length)
|
|
109
|
+
buffer.bytesplice(start - width, width, PLACEHOLDERS[needed]) unless needed == width
|
|
110
|
+
position = start - width
|
|
111
|
+
while length > 0x7f
|
|
112
|
+
buffer.setbyte(position, (length & 0x7f) | 0x80)
|
|
113
|
+
position += 1
|
|
114
|
+
length >>= 7
|
|
115
|
+
end
|
|
116
|
+
buffer.setbyte(position, length)
|
|
117
|
+
needed
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Text of any encoding goes into the binary buffer as bytes. Ruby 3.4's
|
|
121
|
+
# String#append_as_bytes does that with no copy, and takes the tag and
|
|
122
|
+
# a one-byte size in the same call; before it, non-ASCII text is
|
|
123
|
+
# appended as a binary copy so the buffer never trips Ruby's encoding
|
|
124
|
+
# compatibility check.
|
|
125
|
+
if String.method_defined?(:append_as_bytes)
|
|
126
|
+
# A length-delimited field: +tag+, the varint size of +bytes+, +bytes+.
|
|
127
|
+
def append_length_delimited(buffer, tag, bytes)
|
|
128
|
+
size = bytes.bytesize
|
|
129
|
+
return buffer.append_as_bytes(tag, size, bytes) if size < 0x80
|
|
130
|
+
|
|
131
|
+
buffer << tag
|
|
132
|
+
append_varint(buffer, size)
|
|
133
|
+
buffer.append_as_bytes(bytes)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def append_bytes(buffer, bytes)
|
|
137
|
+
buffer.append_as_bytes(bytes)
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
def append_length_delimited(buffer, tag, bytes)
|
|
141
|
+
buffer << tag
|
|
142
|
+
append_varint(buffer, bytes.bytesize)
|
|
143
|
+
append_bytes(buffer, bytes)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def append_bytes(buffer, bytes)
|
|
147
|
+
buffer << (bytes.encoding == Encoding::BINARY || bytes.ascii_only? ? bytes : bytes.b)
|
|
148
|
+
end
|
|
69
149
|
end
|
|
70
150
|
end
|
|
71
151
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast-protowire
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -46,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
47
|
version: '0'
|
|
48
48
|
requirements: []
|
|
49
|
-
rubygems_version: 4.0.
|
|
49
|
+
rubygems_version: 4.0.20
|
|
50
50
|
specification_version: 4
|
|
51
51
|
summary: 'Protocol Buffers wire format for Ruby: declare messages, encode and decode
|
|
52
52
|
bytes, no runtime.'
|