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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/herringbone/thrift.rb +118 -59
- data/lib/herringbone/version.rb +1 -1
- 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: cad7828c89f6cdda317d574829316e6f96cc967e2a19d3c7589ab54324ae0967
|
|
4
|
+
data.tar.gz: f889fe0c5ac568435597a48d6cbdb779a0e3b5d1b6d66e5965e2a7a5f38168aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/herringbone/thrift.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 >
|
|
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
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
size.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
|
269
|
+
when T_DOUBLE then read_double
|
|
246
270
|
when T_BINARY then read_binary
|
|
247
271
|
when T_LIST, T_SET
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
data/lib/herringbone/version.rb
CHANGED
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.
|
|
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-
|
|
10
|
+
date: 2026-10-04 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|