fast-protowire 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4332160b8041d794b7f19703e3c9ccfa0887bc1eb0d376b6e9d6e1947cf79c9
4
- data.tar.gz: 990b87f8ffcf4b7684dd45bfef79f2ec93d66d6ad60b491109c5ab65cdfa7ec2
3
+ metadata.gz: 9d6f10862b8163b7b4d5210479a6b27b62298e0951b24425db97cdec94356a10
4
+ data.tar.gz: 5a882709766e557ae6a65b3107ec3cc8de6fba07f90de3293607518cf880b4f8
5
5
  SHA512:
6
- metadata.gz: 69a56b231c6adb17d312ad7a90c2c223323d4283f564f5bc6b0853ee37136c5a3ffdc4ff81f86d70047f3fc10d5d1f510cc3152c5b3cd628f1bf965d5a50c8b9
7
- data.tar.gz: 8f9478ae25d88e9ca0d8ce55d64ffa248c7e480d2323436eceb3b6757b8a0668faf18d4b32b89fef7df4379461d1b50e7d652f5428ab67026c8df1c82abc3ee2
6
+ metadata.gz: 8a54a74df181260136e00d9227f050c1ef785096515b4a91197f939af58df1d865bea3eb5e660db97335111a1728407edccfce11afd8b60a4335076e95b1a258
7
+ data.tar.gz: 5048f2b557f2e3c4f44b4a2cb95110b11142c6b7c94975dbca68f7a3fc1550ea2e9b120460b4e36bb2ed7f316bdb476b60b2b334f449728c2a8ba869242a8036
data/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
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
+
3
61
  ## 0.2.0
4
62
 
5
63
  - Encoding allocates nothing per nested message, packed field or map
data/README.md CHANGED
@@ -58,11 +58,11 @@ metrics with twelve labels each (Ruby 4.0.7; conditions and every table on the
58
58
 
59
59
  - Encoding allocates **one object, the output**, whatever the message's size or depth;
60
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.7x faster.
61
+ containers and Strings it returns, 5x fewer than 0.1.0 and 1.3x faster.
62
62
  - Built a message at a time, the way an exposition builds series, `google-protobuf`
63
63
  leaves **504,001 native arenas and 225 MiB** behind for an 11.76 MB body and spends
64
- 1.66 s of every ten builds in GC; fast-protowire leaves 16.5 MiB, no arenas, and 0.26 s.
65
- - `google-protobuf` is native, and 10 to 20x faster per operation on an existing tree
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
66
  or one nested Hash. This gem trades that speed for memory that is roughly the size of
67
67
  the output; fast-prometheus's scrape path goes further and writes series with `Wire`
68
68
  directly, with no message per series at all.
@@ -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
- private_constant :SCALAR_WIRE_TYPES, :INTEGER_RANGES, :FIXED_FORMATS, :PACKABLE, :MAP_KEY_TYPES
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
- attr_reader :name, :number, :type, :rule, :oneof, :ivar, :enum
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
@@ -142,11 +158,13 @@ module Fast
142
158
  end
143
159
 
144
160
  # Reads one occurrence of this field into +current+ (the value already
145
- # held) and returns the value to store.
146
- def decode(reader, wire_type, current)
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)
147
165
  case rule
148
166
  when :repeated then decode_repeated(reader, wire_type, current)
149
- when :map then decode_map_entry(reader, wire_type, current)
167
+ when :map then decode_map_entry(reader, wire_type, current, message)
150
168
  else
151
169
  if type == :message && current
152
170
  expect(reader, wire_type).read_nested { |nested| current.merge_from(nested) }
@@ -158,6 +176,15 @@ module Fast
158
176
 
159
177
  protected
160
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
+
161
188
  def coerce_one(value)
162
189
  case type
163
190
  when :string then coerce_string(value)
@@ -174,12 +201,20 @@ module Fast
174
201
  expect(reader, wire_type)
175
202
  case type
176
203
  when :message then reader.read_nested { |nested| message_class.new.merge_from(nested) }
177
- when :string then reader.read_length_delimited.force_encoding(Encoding::UTF_8)
204
+ when :string
205
+ @strict_utf8 ? read_string(reader) : reader.read_length_delimited.force_encoding(Encoding::UTF_8)
178
206
  when :bytes then reader.read_length_delimited
179
207
  else read_scalar(reader)
180
208
  end
181
209
  end
182
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
+
183
218
  # Appends the tag and one value. A nested message is encoded straight
184
219
  # into the buffer behind a length prefix filled in after it, with no
185
220
  # buffer of its own; +width+ carries the prefix width from one value
@@ -204,6 +239,13 @@ module Fast
204
239
 
205
240
  private
206
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
+
207
249
  def resolve_type(type)
208
250
  case type
209
251
  when Symbol
@@ -292,8 +334,7 @@ module Fast
292
334
  # Each entry is a message { key = 1; value = 2 }.
293
335
  def map_step(ivar)
294
336
  tag = @tag
295
- key = @key_field.writer
296
- value = @value_field.writer
337
+ key, value = entry_writers
297
338
  width = 1
298
339
  lambda do |message, buffer|
299
340
  message.instance_variable_get(ivar).each do |k, v|
@@ -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)
@@ -413,30 +457,64 @@ module Fast
413
457
 
414
458
  def decode_repeated(reader, wire_type, values)
415
459
  if packable? && wire_type == Wire::LENGTH_DELIMITED
416
- reader.read_nested { |packed| values << read_scalar(packed) until packed.eof? }
460
+ reader.read_packed { |packed| values << read_scalar(packed) until packed.eof? }
417
461
  else
418
462
  values << read_one(reader, wire_type)
419
463
  end
420
464
  values
421
465
  end
422
466
 
423
- def decode_map_entry(reader, wire_type, hash)
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)
424
473
  expect(reader, wire_type).read_nested do |entry|
425
- key = @key_field.default_value
426
- value = @value_field.default_value
474
+ key = nil
475
+ value = nil
476
+ unknown = nil
427
477
  until entry.eof?
428
- tag = entry.read_varint
429
- case tag >> 3
430
- when 1 then key = @key_field.read_one(entry, tag & 0x7)
431
- when 2 then value = @value_field.decode(entry, tag & 0x7, value)
432
- else entry.skip(tag & 0x7)
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)
433
485
  end
434
486
  end
435
- hash[key] = value
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
494
+ end
436
495
  end
437
496
  hash
438
497
  end
439
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
+
440
518
  def expect(reader, wire_type)
441
519
  return reader if wire_type == self.wire_type
442
520
 
@@ -450,13 +528,23 @@ module Fast
450
528
  when :int64 then signed(reader.read_varint, 64)
451
529
  when :uint32 then reader.read_varint & 0xFFFF_FFFF
452
530
  when :uint64 then reader.read_varint
453
- when :sint32, :sint64 then Wire.unzigzag(reader.read_varint)
531
+ when :sint32 then Wire.unzigzag(reader.read_varint & 0xFFFF_FFFF)
532
+ when :sint64 then Wire.unzigzag(reader.read_varint)
454
533
  when :bool then reader.read_varint != 0
455
534
  when :double, :fixed64, :sfixed64 then reader.read_fixed(FIXED_FORMATS.fetch(type), 8)
456
535
  else reader.read_fixed(FIXED_FORMATS.fetch(type), 4)
457
536
  end
458
537
  end
459
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
+
460
548
  def signed(value, bits)
461
549
  value &= (1 << bits) - 1
462
550
  value >= (1 << (bits - 1)) ? value - (1 << bits) : value
@@ -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
@@ -172,17 +173,23 @@ module Fast
172
173
 
173
174
  # Reads fields from +reader+ into this message (protobuf merge
174
175
  # semantics: later scalars win, repeated fields append, nested messages
175
- # 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.
176
180
  def merge_from(reader)
177
181
  until reader.eof?
178
182
  key = reader.read_varint
179
183
  wire_type = key & 0x7
180
- field = self.class.fields_by_number[key >> 3]
181
- if field
182
- write_field(field, field.decode(reader, wire_type, instance_variable_get(field.ivar)))
184
+ number = key >> 3
185
+ raise DecodeError, "field number 0" if number.zero? # as Reader#read_key, inlined for the loop
186
+
187
+ field = self.class.fields_by_number[number]
188
+ if field&.accepts?(wire_type)
189
+ write_field(field, field.decode(reader, wire_type, instance_variable_get(field.ivar), self))
183
190
  else
184
191
  Wire.append_varint(@unknown_fields ||= String.new, key)
185
- @unknown_fields << reader.skip(wire_type)
192
+ @unknown_fields << reader.skip(wire_type, number)
186
193
  end
187
194
  end
188
195
  self
@@ -197,8 +204,12 @@ module Fast
197
204
  end
198
205
  end
199
206
 
200
- # Two messages are equal when every field reads the same; an implicit
201
- # 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+.
202
213
  def ==(other)
203
214
  other.class == self.class && comparable_values == other.comparable_values
204
215
  end
@@ -228,11 +239,10 @@ module Fast
228
239
  protected
229
240
 
230
241
  def comparable_values
231
- values = self.class.fields.each_value.map do |field|
242
+ self.class.fields.each_value.map do |field|
232
243
  value = instance_variable_get(field.ivar)
233
244
  value.nil? && !field.explicit_presence? ? field.default_value : value
234
245
  end
235
- values << @unknown_fields
236
246
  end
237
247
 
238
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,10 +30,20 @@ module Fast
22
30
 
23
31
  # Returns [field number, wire type].
24
32
  def read_tag
25
- key = read_varint
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
+
29
47
  # Most varints (tags, small lengths) are one byte; the loop is only
30
48
  # entered past it, and is a bare while because Kernel#loop costs an
31
49
  # object per call.
@@ -42,7 +60,9 @@ module Fast
42
60
  result |= (byte & 0x7f) << shift
43
61
  shift += 7
44
62
  end
45
- result
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
46
66
  end
47
67
 
48
68
  def read_fixed32
@@ -75,29 +95,52 @@ module Fast
75
95
  end
76
96
 
77
97
  # Bounds the reader to the next length-delimited value for the block
78
- # and returns the block's result: nested messages, packed fields and
79
- # map entries are read in place, with no copy of their bytes.
98
+ # and returns the block's result, so a packed repeated field is read
99
+ # in place, with no copy of its bytes.
100
+ def read_packed
101
+ length = read_varint
102
+ limit = @limit
103
+ raise DecodeError, "truncated field" if @position + length > limit
104
+
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.
80
118
  def read_nested
81
119
  length = read_varint
82
120
  limit = @limit
83
121
  raise DecodeError, "truncated field" if @position + length > limit
122
+ raise DecodeError, "nested deeper than #{MAX_DEPTH}" if @depth >= MAX_DEPTH
84
123
 
85
124
  @limit = @position + length
125
+ @depth += 1
86
126
  result = yield self
127
+ @depth -= 1
87
128
  @position = @limit
88
129
  @limit = limit
89
130
  result
90
131
  end
91
132
 
92
133
  # Skips one value of +wire_type+ and returns its raw bytes, so unknown
93
- # fields survive a decode/encode round trip.
94
- def skip(wire_type)
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)
95
138
  start = @position
96
139
  case wire_type
97
140
  when Wire::VARINT then read_varint
98
141
  when Wire::FIXED64 then read_bytes(8)
99
142
  when Wire::LENGTH_DELIMITED then read_length_delimited
100
- when Wire::START_GROUP then skip_group
143
+ when Wire::START_GROUP then skip_group(number)
101
144
  when Wire::FIXED32 then read_bytes(4)
102
145
  else raise DecodeError, "unknown wire type #{wire_type}"
103
146
  end
@@ -114,14 +157,20 @@ module Fast
114
157
  byte
115
158
  end
116
159
 
117
- def skip_group
160
+ def skip_group(number)
161
+ raise DecodeError, "nested deeper than #{MAX_DEPTH}" if @depth >= MAX_DEPTH
162
+
163
+ @depth += 1
118
164
  loop do
119
- number, wire_type = read_tag
120
- return if wire_type == Wire::END_GROUP
121
- raise DecodeError, "invalid group" if number.zero?
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
122
168
 
123
- skip(wire_type)
169
+ break
170
+ end
171
+ skip(wire_type, inner)
124
172
  end
173
+ @depth -= 1
125
174
  end
126
175
  end
127
176
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fast
4
4
  module Protowire
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -17,10 +17,21 @@ module Fast
17
17
  UINT64_MASK = (1 << 64) - 1
18
18
  # Zero bytes a length prefix is widened to, by varint width.
19
19
  PLACEHOLDERS = Array.new(11) { |width| ("\0" * width).b.freeze }.freeze
20
- private_constant :UINT64_MASK, :PLACEHOLDERS
20
+ private_constant :PLACEHOLDERS
21
21
 
22
22
  module_function
23
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
+
24
35
  # The key for +number+ / +wire_type+ as frozen bytes.
25
36
  def tag(number, wire_type)
26
37
  varint((number << 3) | wire_type).freeze
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs