herringbone 0.6.0 → 0.6.1

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: 36e3b2444245fc90b36b577284aab2a81e4991ff04e30dd781f9ba94df43ae90
4
- data.tar.gz: 9e2e4dbe2ab635a03aa743206cd5e6d74d6039cc5e790cefae8b04a7afd8304b
3
+ metadata.gz: cad7828c89f6cdda317d574829316e6f96cc967e2a19d3c7589ab54324ae0967
4
+ data.tar.gz: f889fe0c5ac568435597a48d6cbdb779a0e3b5d1b6d66e5965e2a7a5f38168aa
5
5
  SHA512:
6
- metadata.gz: 162e38811c7e6371ed61a777fddcfa8e1e811de5225842e13dad1cf67da20490c7b7eb2347eaeb6515322a6ace971d453fa4ce1556cc75f4e70acfdcda7a9179
7
- data.tar.gz: 0e5798748be214eb32a7939799ecb7a885283099908c05003065a98569c0572dc17a25fd4e6d017c5635256cefdd3b0db5072e28c1536fe4e26867c1d019eca0
6
+ metadata.gz: 7095d8deb5d3232381ca860a1e58251fb8dea505c4ec911e96cdb28dba970575510fec7ab5f96f7b640f9f3fcbb1e557d424975c5e4bdb6f31040b38186093f8
7
+ data.tar.gz: fd061d539396bb88713c2c0daedcde0d6a70e6b797f7ce57728fa23f4076d9ca62c127e5c9d6b392300d55536ad898ee9ebf1d36082896b7c1aef5e086ff65f7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.1
6
+
7
+ - `Herringbone::Reader` raises `Herringbone::FormatError` for forged metadata or page headers that
8
+ made it loop endlessly, overflow the stack or raise `ArgumentError`, `RangeError` or
9
+ `NoMemoryError`.
10
+
5
11
  ## 0.6.0
6
12
 
7
13
  - `require "herringbone"` loads the rest of the gem on first use: under 1 ms instead of about
@@ -45,6 +45,14 @@ module Herringbone
45
45
  double: T_DOUBLE, binary: T_BINARY, string: T_BINARY
46
46
  }.freeze
47
47
 
48
+ # Values each declared integer type can hold, whatever width it came in on the wire
49
+ INT_RANGES = {
50
+ i16: -2**15...2**15, i32: -2**31...2**31, i64: -2**63...2**63
51
+ }.freeze
52
+
53
+ # Deepest nesting of structs, lists, sets and maps read or skipped, as in Apache Thrift
54
+ MAX_DEPTH = 64
55
+
48
56
  # Wire type written for a declared field type
49
57
  #
50
58
  # @param type [Symbol, Array, Class] a scalar type from WIRE_TYPES, a +[:list, elem]+ Array,
@@ -89,6 +97,7 @@ module Herringbone
89
97
  def initialize(buf, pos = 0)
90
98
  @buf = buf
91
99
  @pos = pos
100
+ @depth = 0
92
101
  end
93
102
 
94
103
  # Reads one raw byte
@@ -104,17 +113,21 @@ module Herringbone
104
113
 
105
114
  # Reads an unsigned LEB128 varint
106
115
  #
107
- # @return [Integer] the decoded non-negative value
108
- # @raise [Error] when the varint runs past the buffer or is longer than 64 bits allow
116
+ # @return [Integer] the decoded non-negative value, below 2**64
117
+ # @raise [Error] when the varint runs past the buffer, is longer than 10 bytes or does not
118
+ # fit in 64 bits
109
119
  def read_varint
110
120
  result = 0
111
121
  shift = 0
112
122
  while true
113
123
  b = read_byte
114
124
  result |= (b & 0x7F) << shift
115
- return result if b < 0x80
125
+ if b < 0x80
126
+ raise Error, "Varint exceeds 64 bits" if result >= 2**64
127
+ return result
128
+ end
116
129
  shift += 7
117
- raise Error, "Varint too long" if shift > 70
130
+ raise Error, "Varint too long" if shift > 63
118
131
  end
119
132
  end
120
133
 
@@ -141,8 +154,10 @@ module Herringbone
141
154
 
142
155
  # Reads an 8-byte little-endian double
143
156
  #
144
- # @return [Float, nil] the value (nil when fewer than 8 bytes are left: this is not checked)
157
+ # @return [Float] the value
158
+ # @raise [Error] when fewer than 8 bytes are left
145
159
  def read_double
160
+ raise Error, "Unexpected end of Thrift data at #{@pos}" if @pos + 8 > @buf.bytesize
146
161
  v = @buf.byteslice(@pos, 8).unpack1("E")
147
162
  @pos += 8
148
163
  v
@@ -155,26 +170,28 @@ module Herringbone
155
170
  #
156
171
  # @param klass [Class] a Thrift::Struct subclass
157
172
  # @return [Struct] an instance of +klass+ with the fields that were present set
158
- # @raise [Error] on truncated or malformed data
173
+ # @raise [Error] on truncated or malformed data, or nesting deeper than MAX_DEPTH
159
174
  def read_struct(klass)
160
- obj = klass.new
161
- fields = klass.fields_by_id
162
- last_id = 0
163
- while true
164
- header = read_byte
165
- wire = header & 0x0F
166
- break if wire == T_STOP
167
- delta = header >> 4
168
- fid = delta.zero? ? read_zigzag : last_id + delta
169
- last_id = fid
170
- field = fields[fid]
171
- if field && Thrift.compatible?(wire, field.type)
172
- obj.instance_variable_set(field.ivar, read_value(wire, field.type))
173
- else
174
- skip(wire)
175
+ nested do
176
+ obj = klass.new
177
+ fields = klass.fields_by_id
178
+ last_id = 0
179
+ while true
180
+ header = read_byte
181
+ wire = header & 0x0F
182
+ break if wire == T_STOP
183
+ delta = header >> 4
184
+ fid = delta.zero? ? read_zigzag : last_id + delta
185
+ last_id = fid
186
+ field = fields[fid]
187
+ if field && Thrift.compatible?(wire, field.type)
188
+ obj.instance_variable_set(field.ivar, read_value(wire, field.type))
189
+ else
190
+ skip(wire)
191
+ end
175
192
  end
193
+ obj
176
194
  end
177
- obj
178
195
  end
179
196
 
180
197
  # Reads one value of wire type +wire+ as declared +type+
@@ -184,7 +201,8 @@ module Herringbone
184
201
  # an Array or Struct subclass gives the element type or struct to read
185
202
  # @return [Object] true/false, an Integer (bytes are signed), a Float, a String, an Array
186
203
  # (or nil, see #read_list) or a Struct
187
- # @raise [Error] on an unsupported wire type or truncated data
204
+ # @raise [Error] on an unsupported wire type, truncated data or an integer out of the range
205
+ # of +type+
188
206
  def read_value(wire, type)
189
207
  case wire
190
208
  when T_TRUE then true
@@ -192,7 +210,10 @@ module Herringbone
192
210
  when T_BYTE
193
211
  b = read_byte
194
212
  (b >= 0x80) ? b - 0x100 : b
195
- when T_I16, T_I32, T_I64 then read_zigzag
213
+ when T_I16, T_I32, T_I64
214
+ n = read_zigzag
215
+ raise Error, "Integer #{n} out of range for #{type}" unless INT_RANGES.fetch(type).cover?(n)
216
+ n
196
217
  when T_DOUBLE then read_double
197
218
  when T_BINARY
198
219
  s = read_binary
@@ -210,24 +231,26 @@ module Herringbone
210
231
  # @param type [Array] declared list type, +[:list, elem_type]+
211
232
  # @return [Array, nil] the elements, or nil (with the list skipped) when the element wire
212
233
  # type does not match +elem_type+
213
- # @raise [Error] on truncated or malformed data
234
+ # @raise [Error] on truncated or malformed data, or nesting deeper than MAX_DEPTH
214
235
  def read_list(type)
215
- header = read_byte
216
- size = header >> 4
217
- size = read_varint if size == 15
218
- elem_wire = header & 0x0F
219
- elem_type = type[1]
220
- bool_elems = elem_wire == T_TRUE || elem_wire == T_FALSE
221
- if size.positive? && !(bool_elems && elem_type == :bool) && !Thrift.compatible?(elem_wire, elem_type)
222
- size.times { bool_elems ? read_byte : skip(elem_wire) }
223
- return nil
224
- end
225
- Array.new(size) do
226
- if elem_wire == T_TRUE || elem_wire == T_FALSE
227
- # Booleans inside lists are encoded as full bytes
228
- read_byte == T_TRUE
229
- else
230
- read_value(elem_wire, elem_type)
236
+ nested do
237
+ header = read_byte
238
+ size = header >> 4
239
+ size = read_size if size == 15
240
+ elem_wire = header & 0x0F
241
+ elem_type = type[1]
242
+ bool_elems = elem_wire == T_TRUE || elem_wire == T_FALSE
243
+ if size.positive? && !(bool_elems && elem_type == :bool) && !Thrift.compatible?(elem_wire, elem_type)
244
+ size.times { bool_elems ? read_byte : skip(elem_wire) }
245
+ return nil
246
+ end
247
+ Array.new(size) do
248
+ if elem_wire == T_TRUE || elem_wire == T_FALSE
249
+ # Booleans inside lists are encoded as full bytes
250
+ read_byte == T_TRUE
251
+ else
252
+ read_value(elem_wire, elem_type)
253
+ end
231
254
  end
232
255
  end
233
256
  end
@@ -236,40 +259,76 @@ module Herringbone
236
259
  #
237
260
  # @param wire [Integer] wire type of the value to skip
238
261
  # @return [void]
239
- # @raise [Error] on an unknown wire type (including T_STOP) or truncated data
262
+ # @raise [Error] on an unknown wire type (including T_STOP), truncated data or nesting deeper
263
+ # than MAX_DEPTH
240
264
  def skip(wire)
241
265
  case wire
242
266
  when T_TRUE, T_FALSE then nil
243
267
  when T_BYTE then read_byte
244
268
  when T_I16, T_I32, T_I64 then read_varint
245
- when T_DOUBLE then @pos += 8
269
+ when T_DOUBLE then read_double
246
270
  when T_BINARY then read_binary
247
271
  when T_LIST, T_SET
248
- header = read_byte
249
- size = header >> 4
250
- size = read_varint if size == 15
251
- elem = header & 0x0F
252
- size.times { (elem == T_TRUE || elem == T_FALSE) ? read_byte : skip(elem) }
272
+ nested do
273
+ header = read_byte
274
+ size = header >> 4
275
+ size = read_size if size == 15
276
+ elem = header & 0x0F
277
+ size.times { (elem == T_TRUE || elem == T_FALSE) ? read_byte : skip(elem) }
278
+ end
253
279
  when T_MAP
254
- size = read_varint
255
- unless size.zero?
256
- kv = read_byte
257
- size.times do
258
- [kv >> 4, kv & 0x0F].each { |w| (w == T_TRUE || w == T_FALSE) ? read_byte : skip(w) }
280
+ nested do
281
+ size = read_size
282
+ unless size.zero?
283
+ kv = read_byte
284
+ size.times do
285
+ [kv >> 4, kv & 0x0F].each { |w| (w == T_TRUE || w == T_FALSE) ? read_byte : skip(w) }
286
+ end
259
287
  end
260
288
  end
261
289
  when T_STRUCT
262
- while true
263
- header = read_byte
264
- w = header & 0x0F
265
- break if w == T_STOP
266
- read_zigzag if (header >> 4).zero?
267
- skip(w)
290
+ nested do
291
+ while true
292
+ header = read_byte
293
+ w = header & 0x0F
294
+ break if w == T_STOP
295
+ read_zigzag if (header >> 4).zero?
296
+ skip(w)
297
+ end
268
298
  end
269
299
  else
270
300
  raise Error, "Cannot skip wire type #{wire}"
271
301
  end
272
302
  end
303
+
304
+ private
305
+
306
+ # Reads the element count of a list, set or map
307
+ #
308
+ # Every element takes at least one byte, so a count above the bytes left is corrupt. Checking
309
+ # that up front keeps a forged count from preallocating a huge Array or skipping for ever.
310
+ #
311
+ # @return [Integer] the count, at most the number of bytes left in the buffer
312
+ # @raise [Error] when the count exceeds the bytes left
313
+ def read_size
314
+ size = read_varint
315
+ left = @buf.bytesize - @pos
316
+ raise Error, "Collection of #{size} elements exceeds the #{left} bytes left" if size > left
317
+ size
318
+ end
319
+
320
+ # Runs the block one nesting level deeper
321
+ #
322
+ # @yieldreturn [Object] the value read inside the nested container
323
+ # @return [Object] the block's value
324
+ # @raise [Error] when the nesting gets deeper than MAX_DEPTH
325
+ def nested
326
+ @depth += 1
327
+ raise Error, "Thrift data nested deeper than #{MAX_DEPTH} levels" if @depth > MAX_DEPTH
328
+ yield
329
+ ensure
330
+ @depth -= 1
331
+ end
273
332
  end
274
333
 
275
334
  # Encodes compact protocol data by appending to a binary String
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Herringbone
4
4
  # Gem version, also written to the +created_by+ field of every file's footer
5
- VERSION = "0.6.0"
5
+ VERSION = "0.6.1"
6
6
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: herringbone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-03 00:00:00.000000000 Z
10
+ date: 2026-10-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal