herringbone 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +209 -0
- data/bin/herringbone +54 -0
- data/lib/herringbone/active_record.rb +131 -0
- data/lib/herringbone/byte_values.rb +144 -0
- data/lib/herringbone/codecs/lz4.rb +305 -0
- data/lib/herringbone/codecs/snappy.rb +375 -0
- data/lib/herringbone/compression.rb +75 -0
- data/lib/herringbone/encodings/delta.rb +153 -0
- data/lib/herringbone/encodings/plain.rb +87 -0
- data/lib/herringbone/encodings/rle.rb +203 -0
- data/lib/herringbone/format.rb +309 -0
- data/lib/herringbone/io_buffer_support.rb +25 -0
- data/lib/herringbone/reader.rb +469 -0
- data/lib/herringbone/schema.rb +551 -0
- data/lib/herringbone/thrift.rb +334 -0
- data/lib/herringbone/types.rb +454 -0
- data/lib/herringbone/version.rb +5 -0
- data/lib/herringbone/writer.rb +634 -0
- data/lib/herringbone.rb +45 -0
- metadata +104 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herringbone
|
|
4
|
+
# Reads Parquet files.
|
|
5
|
+
#
|
|
6
|
+
# Herringbone::Reader.open("data.parquet") do |reader|
|
|
7
|
+
# reader.each_row { |row| p row } # rows as Hashes with String keys
|
|
8
|
+
# reader.column("name") # all values of a top-level field
|
|
9
|
+
# reader.each_row(columns: ["id"]) { ... } # projection
|
|
10
|
+
# end
|
|
11
|
+
class Reader
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
MAGIC = "PAR1"
|
|
15
|
+
|
|
16
|
+
attr_reader :metadata, :schema
|
|
17
|
+
|
|
18
|
+
def self.open(path)
|
|
19
|
+
io = File.open(path, "rb")
|
|
20
|
+
reader = new(io)
|
|
21
|
+
return reader unless block_given?
|
|
22
|
+
begin
|
|
23
|
+
yield reader
|
|
24
|
+
ensure
|
|
25
|
+
io.close
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# +source+ is an IO (opened in binary mode), a path, or a String of Parquet bytes when
|
|
30
|
+
# +string: true+ is given.
|
|
31
|
+
def initialize(source)
|
|
32
|
+
@io = case source
|
|
33
|
+
when String then File.open(source, "rb")
|
|
34
|
+
when Pathname then File.open(source.to_s, "rb")
|
|
35
|
+
else source
|
|
36
|
+
end
|
|
37
|
+
@metadata = read_footer
|
|
38
|
+
@schema = Schema.from_elements(@metadata.schema)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.from_string(bytes)
|
|
42
|
+
new(StringIO.new(bytes.b))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def close
|
|
46
|
+
@io.close
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def num_rows = @metadata.num_rows
|
|
50
|
+
def row_groups = @metadata.row_groups
|
|
51
|
+
def num_row_groups = @metadata.row_groups.size
|
|
52
|
+
def created_by = @metadata.created_by
|
|
53
|
+
|
|
54
|
+
def key_value_metadata
|
|
55
|
+
(@metadata.key_value_metadata || []).to_h { |kv| [kv.key, kv.value] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Yields each row as a Hash of top-level field name => value
|
|
59
|
+
def each_row(columns: nil, &block)
|
|
60
|
+
return enum_for(:each_row, columns: columns) unless block
|
|
61
|
+
fields = select_fields(columns)
|
|
62
|
+
row_groups.each_index do |rg|
|
|
63
|
+
data = read_row_group_fields(rg, fields)
|
|
64
|
+
n = row_groups[rg].num_rows
|
|
65
|
+
names = fields.map(&:name)
|
|
66
|
+
n.times do |i|
|
|
67
|
+
row = {}
|
|
68
|
+
names.each_with_index { |name, j| row[name] = data[j][i] }
|
|
69
|
+
yield row
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
alias_method :each, :each_row
|
|
75
|
+
|
|
76
|
+
# Rows as an Array of Hashes
|
|
77
|
+
def rows(columns: nil) = each_row(columns: columns).to_a
|
|
78
|
+
|
|
79
|
+
# All values of a single top-level field, across all row groups
|
|
80
|
+
def column(name)
|
|
81
|
+
field = @schema.field(name) or raise ArgumentError, "No such column #{name.inspect}"
|
|
82
|
+
row_groups.each_index.flat_map { |rg| read_row_group_fields(rg, [field]).first }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Hash of field name => Array of values, for the given row group
|
|
86
|
+
def read_row_group(index, columns: nil)
|
|
87
|
+
fields = select_fields(columns)
|
|
88
|
+
fields.map(&:name).zip(read_row_group_fields(index, fields)).to_h
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Raw column data for a leaf column: [definition_levels, repetition_levels, values].
|
|
92
|
+
# Levels are nil when the column's max level is 0.
|
|
93
|
+
def read_column_chunk(row_group_index, column)
|
|
94
|
+
column = @schema.column(column) unless column.is_a?(Schema::Column)
|
|
95
|
+
raise ArgumentError, "No such leaf column" unless column
|
|
96
|
+
chunk = row_groups.fetch(row_group_index).columns.fetch(column.index)
|
|
97
|
+
ColumnChunkReader.new(@io, chunk, column).read
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def inspect
|
|
101
|
+
"#<#{self.class.name} rows=#{num_rows} row_groups=#{num_row_groups} created_by=#{created_by.inspect}>"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def select_fields(columns)
|
|
107
|
+
return @schema.fields unless columns
|
|
108
|
+
Array(columns).map { |c| @schema.field(c) or raise ArgumentError, "No such column #{c.inspect}" }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def read_row_group_fields(rg, fields)
|
|
112
|
+
rg_meta = row_groups.fetch(rg)
|
|
113
|
+
n = rg_meta.num_rows
|
|
114
|
+
fields.map do |field|
|
|
115
|
+
chunks = {}
|
|
116
|
+
field.leaves.each do |col|
|
|
117
|
+
chunks[col.index] = ColumnChunkReader.new(@io, rg_meta.columns.fetch(col.index), col).read
|
|
118
|
+
end
|
|
119
|
+
Assembler.new(field, chunks).read_rows(n)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def read_footer
|
|
124
|
+
@io.seek(0, IO::SEEK_END)
|
|
125
|
+
size = @io.pos
|
|
126
|
+
raise FormatError, "File too small to be Parquet (#{size} bytes)" if size < 12
|
|
127
|
+
@io.seek(size - 8)
|
|
128
|
+
tail = @io.read(8)
|
|
129
|
+
raise FormatError, "Missing PAR1 footer magic" unless tail.byteslice(4, 4) == MAGIC
|
|
130
|
+
raise UnsupportedError, "Encrypted Parquet files are not supported" if tail.byteslice(4, 4) == "PARE"
|
|
131
|
+
footer_len = tail.unpack1("V")
|
|
132
|
+
raise FormatError, "Footer length #{footer_len} exceeds file size" if footer_len + 12 > size
|
|
133
|
+
@io.seek(size - 8 - footer_len)
|
|
134
|
+
footer = @io.read(footer_len)
|
|
135
|
+
Format::FileMetaData.decode(footer).first
|
|
136
|
+
rescue Thrift::Error => e
|
|
137
|
+
raise FormatError, "Corrupt file metadata: #{e.message}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Decodes all pages of one column chunk into levels and values
|
|
141
|
+
class ColumnChunkReader
|
|
142
|
+
T = Format::Type
|
|
143
|
+
E = Format::Encoding
|
|
144
|
+
|
|
145
|
+
def initialize(io, chunk, column)
|
|
146
|
+
@io = io
|
|
147
|
+
@chunk = chunk
|
|
148
|
+
@column = column
|
|
149
|
+
@meta = chunk.meta_data or raise UnsupportedError, "Column chunk without metadata (encrypted?)"
|
|
150
|
+
raise UnsupportedError, "Column chunks in external files are not supported" if chunk.file_path
|
|
151
|
+
@max_def = column.max_definition_level
|
|
152
|
+
@max_rep = column.max_repetition_level
|
|
153
|
+
@converter = column.converter
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def read
|
|
157
|
+
buf = read_bytes
|
|
158
|
+
defs = @max_def.positive? ? [] : nil
|
|
159
|
+
reps = @max_rep.positive? ? [] : nil
|
|
160
|
+
values = []
|
|
161
|
+
pos = 0
|
|
162
|
+
total = @meta.num_values
|
|
163
|
+
seen = 0
|
|
164
|
+
while seen < total && pos < buf.bytesize
|
|
165
|
+
header, pos = decode_page_header(buf, pos)
|
|
166
|
+
size = header.compressed_page_size
|
|
167
|
+
# Some old writers under-report total_compressed_size; read on past the declared end
|
|
168
|
+
extend_buffer(buf, pos + size - buf.bytesize) if pos + size > buf.bytesize
|
|
169
|
+
raise FormatError, "Page overruns column chunk" if pos + size > buf.bytesize
|
|
170
|
+
body = buf.byteslice(pos, size)
|
|
171
|
+
pos += size
|
|
172
|
+
case header.type
|
|
173
|
+
when Format::PageType::DICTIONARY_PAGE
|
|
174
|
+
read_dictionary(header, body)
|
|
175
|
+
when Format::PageType::DATA_PAGE
|
|
176
|
+
seen += read_data_page_v1(header, body, defs, reps, values)
|
|
177
|
+
when Format::PageType::DATA_PAGE_V2
|
|
178
|
+
seen += read_data_page_v2(header, body, defs, reps, values)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
raise FormatError, "Column #{@column.dotted_path}: read #{seen} of #{total} values" if seen < total
|
|
182
|
+
[defs, reps, values]
|
|
183
|
+
rescue Thrift::Error => e
|
|
184
|
+
raise FormatError, "Corrupt page header in #{@column.dotted_path}: #{e.message}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def read_bytes
|
|
190
|
+
start = @meta.data_page_offset
|
|
191
|
+
dict = @meta.dictionary_page_offset
|
|
192
|
+
# Some writers store 0 when there is no dictionary page
|
|
193
|
+
start = dict if dict && dict.positive? && dict < start
|
|
194
|
+
@start = start
|
|
195
|
+
@io.seek(start)
|
|
196
|
+
len = @meta.total_compressed_size
|
|
197
|
+
(@io.read(len) || "".b).b
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def extend_buffer(buf, nbytes)
|
|
201
|
+
@io.seek(@start + buf.bytesize)
|
|
202
|
+
more = @io.read(nbytes)
|
|
203
|
+
buf << more.b if more
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def decode_page_header(buf, pos)
|
|
207
|
+
Format::PageHeader.decode(buf, pos)
|
|
208
|
+
rescue Thrift::Error
|
|
209
|
+
# The header may straddle the declared end of the chunk
|
|
210
|
+
before = buf.bytesize
|
|
211
|
+
extend_buffer(buf, 1024)
|
|
212
|
+
raise if buf.bytesize == before
|
|
213
|
+
Format::PageHeader.decode(buf, pos)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def decompress(body, size)
|
|
217
|
+
Compression.decompress(@meta.codec, body, size)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def read_dictionary(header, body)
|
|
221
|
+
dh = header.dictionary_page_header
|
|
222
|
+
data = decompress(body, header.uncompressed_page_size)
|
|
223
|
+
vals, = Encodings::Plain.decode(data, 0, dh.num_values, @column.type, @column.type_length)
|
|
224
|
+
vals.map!(&@converter) if @converter
|
|
225
|
+
@dictionary = vals
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def read_data_page_v1(header, body, defs, reps, values)
|
|
229
|
+
dh = header.data_page_header
|
|
230
|
+
n = dh.num_values
|
|
231
|
+
data = decompress(body, header.uncompressed_page_size)
|
|
232
|
+
pos = 0
|
|
233
|
+
if @max_rep.positive?
|
|
234
|
+
levels, pos = read_levels(data, pos, dh.repetition_level_encoding, @max_rep, n)
|
|
235
|
+
reps.concat(levels)
|
|
236
|
+
end
|
|
237
|
+
non_null = n
|
|
238
|
+
if @max_def.positive?
|
|
239
|
+
levels, pos = read_levels(data, pos, dh.definition_level_encoding, @max_def, n)
|
|
240
|
+
non_null = levels.count(@max_def)
|
|
241
|
+
defs.concat(levels)
|
|
242
|
+
end
|
|
243
|
+
values.concat(decode_values(data, pos, non_null, dh.encoding))
|
|
244
|
+
n
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def read_data_page_v2(header, body, defs, reps, values)
|
|
248
|
+
dh = header.data_page_header_v2
|
|
249
|
+
n = dh.num_values
|
|
250
|
+
rep_len = dh.repetition_levels_byte_length
|
|
251
|
+
def_len = dh.definition_levels_byte_length
|
|
252
|
+
if @max_rep.positive?
|
|
253
|
+
reps.concat(Encodings::RLE.decode_hybrid(body, 0, rep_len, RLE_WIDTH[@max_rep], n))
|
|
254
|
+
end
|
|
255
|
+
non_null = n
|
|
256
|
+
if @max_def.positive?
|
|
257
|
+
levels = Encodings::RLE.decode_hybrid(body, rep_len, rep_len + def_len, RLE_WIDTH[@max_def], n)
|
|
258
|
+
non_null = levels.count(@max_def)
|
|
259
|
+
defs.concat(levels)
|
|
260
|
+
end
|
|
261
|
+
data = body.byteslice(rep_len + def_len, body.bytesize - rep_len - def_len)
|
|
262
|
+
if dh.is_compressed != false
|
|
263
|
+
data = decompress(data, header.uncompressed_page_size - rep_len - def_len)
|
|
264
|
+
end
|
|
265
|
+
values.concat(decode_values(data, 0, non_null, dh.encoding))
|
|
266
|
+
n
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
RLE_WIDTH = Hash.new { |h, k| h[k] = k.bit_length }
|
|
270
|
+
|
|
271
|
+
def read_levels(data, pos, encoding, max, n)
|
|
272
|
+
width = RLE_WIDTH[max]
|
|
273
|
+
case encoding
|
|
274
|
+
when E::RLE
|
|
275
|
+
len = data.byteslice(pos, 4).unpack1("V")
|
|
276
|
+
start = pos + 4
|
|
277
|
+
[Encodings::RLE.decode_hybrid(data, start, start + len, width, n), start + len]
|
|
278
|
+
when E::BIT_PACKED
|
|
279
|
+
[Encodings::RLE.decode_legacy_bit_packed(data, pos, width, n), pos + (n * width + 7) / 8]
|
|
280
|
+
else
|
|
281
|
+
raise UnsupportedError, "Unsupported level encoding #{E::NAMES[encoding] || encoding}"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def decode_values(data, pos, count, encoding)
|
|
286
|
+
type = @column.type
|
|
287
|
+
vals = case encoding
|
|
288
|
+
when E::PLAIN
|
|
289
|
+
Encodings::Plain.decode(data, pos, count, type, @column.type_length).first
|
|
290
|
+
when E::PLAIN_DICTIONARY, E::RLE_DICTIONARY
|
|
291
|
+
raise FormatError, "Dictionary-encoded page without a dictionary in #{@column.dotted_path}" unless @dictionary
|
|
292
|
+
return [] if count.zero?
|
|
293
|
+
width = data.getbyte(pos)
|
|
294
|
+
indices = Encodings::RLE.decode_hybrid(data, pos + 1, data.bytesize, width, count)
|
|
295
|
+
dict = @dictionary
|
|
296
|
+
raise FormatError, "Dictionary index out of range in #{@column.dotted_path}" if indices.max >= dict.size
|
|
297
|
+
return indices.map! { |i| dict[i] }
|
|
298
|
+
when E::RLE
|
|
299
|
+
raise UnsupportedError, "RLE value encoding is only supported for BOOLEAN" unless type == T::BOOLEAN
|
|
300
|
+
len = data.byteslice(pos, 4).unpack1("V")
|
|
301
|
+
Encodings::RLE.decode_hybrid(data, pos + 4, pos + 4 + len, 1, count).map! { |v| v == 1 }
|
|
302
|
+
when E::DELTA_BINARY_PACKED
|
|
303
|
+
bits = type == T::INT32 ? 32 : 64
|
|
304
|
+
vals, = Encodings::Delta.decode_binary_packed(data, pos, bits, count)
|
|
305
|
+
raise FormatError, "DELTA_BINARY_PACKED page has #{vals.size} values, need #{count}" if vals.size < count
|
|
306
|
+
vals
|
|
307
|
+
when E::DELTA_LENGTH_BYTE_ARRAY
|
|
308
|
+
Encodings::Delta.decode_length_byte_array(data, pos, count).first
|
|
309
|
+
when E::DELTA_BYTE_ARRAY
|
|
310
|
+
Encodings::Delta.decode_byte_array(data, pos, count).first
|
|
311
|
+
when E::BYTE_STREAM_SPLIT
|
|
312
|
+
width = case type
|
|
313
|
+
when T::INT32, T::FLOAT then 4
|
|
314
|
+
when T::INT64, T::DOUBLE then 8
|
|
315
|
+
when T::FIXED_LEN_BYTE_ARRAY then @column.type_length
|
|
316
|
+
else raise UnsupportedError, "BYTE_STREAM_SPLIT is not valid for #{T::NAMES[type]}"
|
|
317
|
+
end
|
|
318
|
+
plain, = Encodings::ByteStreamSplit.decode(data, pos, count, width)
|
|
319
|
+
Encodings::Plain.decode(plain, 0, count, type, @column.type_length).first
|
|
320
|
+
else
|
|
321
|
+
raise UnsupportedError, "Unsupported encoding #{E::NAMES[encoding] || encoding}"
|
|
322
|
+
end
|
|
323
|
+
vals.map!(&@converter) if @converter
|
|
324
|
+
vals
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Rebuilds nested values of one top-level field from the levels of its leaf columns
|
|
329
|
+
# (the "record assembly" half of the Dremel algorithm).
|
|
330
|
+
class Assembler
|
|
331
|
+
def initialize(field, chunks)
|
|
332
|
+
@field = field
|
|
333
|
+
@defs = {}
|
|
334
|
+
@reps = {}
|
|
335
|
+
@vals = {}
|
|
336
|
+
chunks.each do |idx, (d, r, v)|
|
|
337
|
+
@defs[idx] = d
|
|
338
|
+
@reps[idx] = r
|
|
339
|
+
@vals[idx] = v
|
|
340
|
+
end
|
|
341
|
+
@ei = Hash.new(0) # entry cursor per leaf column
|
|
342
|
+
@vi = Hash.new(0) # value cursor per leaf column
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def read_rows(n)
|
|
346
|
+
f = @field
|
|
347
|
+
if f.leaf? && f.column.max_repetition_level.zero?
|
|
348
|
+
idx = f.column.index
|
|
349
|
+
defs = @defs[idx]
|
|
350
|
+
vals = @vals[idx]
|
|
351
|
+
return vals unless defs
|
|
352
|
+
max = f.column.max_definition_level
|
|
353
|
+
return vals if vals.size == defs.size
|
|
354
|
+
vi = -1
|
|
355
|
+
return defs.map { |d| d == max ? vals[vi += 1] : nil }
|
|
356
|
+
end
|
|
357
|
+
return read_simple_list(f) if f.kind == :list && f.element.leaf? && f.element.column.max_repetition_level == 1
|
|
358
|
+
|
|
359
|
+
out = Array.new(n) { read(f) }
|
|
360
|
+
f.leaves.each do |col|
|
|
361
|
+
if @ei[col.index] != (@defs[col.index] || @vals[col.index]).size
|
|
362
|
+
raise FormatError, "Column #{col.dotted_path} has leftover entries after assembling #{n} rows"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
out
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
private
|
|
369
|
+
|
|
370
|
+
# Fast path for a top-level list of primitives (the most common nested shape)
|
|
371
|
+
def read_simple_list(field)
|
|
372
|
+
col = field.element.column
|
|
373
|
+
defs = @defs[col.index]
|
|
374
|
+
reps = @reps[col.index]
|
|
375
|
+
vals = @vals[col.index]
|
|
376
|
+
max_def = col.max_definition_level
|
|
377
|
+
list_def = field.def_level
|
|
378
|
+
item_def = field.item_def
|
|
379
|
+
out = []
|
|
380
|
+
cur = nil
|
|
381
|
+
vi = 0
|
|
382
|
+
i = 0
|
|
383
|
+
n = defs.size
|
|
384
|
+
while i < n
|
|
385
|
+
d = defs[i]
|
|
386
|
+
if reps[i].zero?
|
|
387
|
+
if d < list_def
|
|
388
|
+
out << nil
|
|
389
|
+
i += 1
|
|
390
|
+
next
|
|
391
|
+
end
|
|
392
|
+
cur = []
|
|
393
|
+
out << cur
|
|
394
|
+
if d < item_def
|
|
395
|
+
i += 1
|
|
396
|
+
next
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
if d == max_def
|
|
400
|
+
cur << vals[vi]
|
|
401
|
+
vi += 1
|
|
402
|
+
else
|
|
403
|
+
cur << nil
|
|
404
|
+
end
|
|
405
|
+
i += 1
|
|
406
|
+
end
|
|
407
|
+
out
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def read(field)
|
|
411
|
+
c = field.first_leaf.index
|
|
412
|
+
kind = field.kind
|
|
413
|
+
if field.optional || kind == :list || kind == :map
|
|
414
|
+
d = @defs[c][@ei[c]]
|
|
415
|
+
raise FormatError, "Ran out of levels while assembling #{field.name}" if d.nil?
|
|
416
|
+
if d < field.def_level
|
|
417
|
+
skip(field)
|
|
418
|
+
return nil
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
case kind
|
|
423
|
+
when :leaf
|
|
424
|
+
@ei[c] += 1
|
|
425
|
+
v = @vi[c]
|
|
426
|
+
@vi[c] = v + 1
|
|
427
|
+
@vals[c][v]
|
|
428
|
+
when :struct
|
|
429
|
+
h = {}
|
|
430
|
+
field.children.each { |ch| h[ch.name] = read(ch) }
|
|
431
|
+
h
|
|
432
|
+
when :list
|
|
433
|
+
if d < field.item_def
|
|
434
|
+
skip(field)
|
|
435
|
+
return []
|
|
436
|
+
end
|
|
437
|
+
out = []
|
|
438
|
+
reps = @reps[c]
|
|
439
|
+
rl = field.rep_level
|
|
440
|
+
while true
|
|
441
|
+
out << read(field.element)
|
|
442
|
+
r = reps[@ei[c]]
|
|
443
|
+
break if r.nil? || r < rl
|
|
444
|
+
end
|
|
445
|
+
out
|
|
446
|
+
when :map
|
|
447
|
+
if d < field.item_def
|
|
448
|
+
skip(field)
|
|
449
|
+
return {}
|
|
450
|
+
end
|
|
451
|
+
out = {}
|
|
452
|
+
reps = @reps[c]
|
|
453
|
+
rl = field.rep_level
|
|
454
|
+
while true
|
|
455
|
+
k = read(field.key)
|
|
456
|
+
out[k] = field.value ? read(field.value) : nil
|
|
457
|
+
r = reps[@ei[c]]
|
|
458
|
+
break if r.nil? || r < rl
|
|
459
|
+
end
|
|
460
|
+
out
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def skip(field)
|
|
465
|
+
field.leaves.each { |col| @ei[col.index] += 1 }
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|