herringbone 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 +4 -4
- data/README.md +56 -4
- data/lib/herringbone/active_record.rb +44 -12
- data/lib/herringbone/bloom_filter.rb +112 -12
- data/lib/herringbone/byte_values.rb +29 -0
- data/lib/herringbone/codecs/lz4.rb +66 -3
- data/lib/herringbone/codecs/snappy.rb +132 -3
- data/lib/herringbone/compression.rb +48 -0
- data/lib/herringbone/encodings/delta.rb +70 -13
- data/lib/herringbone/encodings/plain.rb +22 -0
- data/lib/herringbone/encodings/rle.rb +53 -1
- data/lib/herringbone/format.rb +150 -0
- data/lib/herringbone/inspector.rb +477 -81
- data/lib/herringbone/io_buffer_support.rb +3 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +98 -6
- data/lib/herringbone/reader/column_cursor.rb +46 -2
- data/lib/herringbone/reader/numo.rb +365 -0
- data/lib/herringbone/reader/page_stream.rb +280 -4
- data/lib/herringbone/reader/scan.rb +103 -9
- data/lib/herringbone/reader.rb +308 -17
- data/lib/herringbone/schema.rb +306 -15
- data/lib/herringbone/thrift.rb +155 -4
- data/lib/herringbone/types.rb +176 -41
- data/lib/herringbone/version.rb +2 -1
- data/lib/herringbone/visualizer.rb +51 -11
- data/lib/herringbone/writer.rb +308 -31
- data/lib/herringbone/xxhash.rb +108 -2
- data/lib/herringbone.rb +28 -0
- metadata +3 -2
|
@@ -6,20 +6,38 @@ module Herringbone
|
|
|
6
6
|
module Delta
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
+
# Values per block written by the encoder (the size parquet-mr and Arrow use).
|
|
9
10
|
BLOCK_SIZE = 128
|
|
11
|
+
# Miniblocks per block written by the encoder, so 32 values per miniblock.
|
|
10
12
|
MINIBLOCKS = 4
|
|
11
13
|
|
|
14
|
+
# @param n [Integer] zigzag-encoded unsigned integer
|
|
15
|
+
# @return [Integer] the signed integer it represents
|
|
12
16
|
def zigzag_decode(n) = (n >> 1) ^ -(n & 1)
|
|
17
|
+
|
|
18
|
+
# @param n [Integer] signed integer
|
|
19
|
+
# @return [Integer] zigzag-encoded form (0, -1, 1, -2 map to 0, 1, 2, 3)
|
|
13
20
|
def zigzag_encode(n) = n.negative? ? ((-n) << 1) - 1 : n << 1
|
|
14
21
|
|
|
15
22
|
# Wraps an integer into the signed range of +bits+ bits
|
|
23
|
+
# @param v [Integer] integer to wrap
|
|
24
|
+
# @param bits [Integer] target width, 32 or 64
|
|
25
|
+
# @return [Integer] +v+ modulo 2^bits, as a two's complement signed integer
|
|
16
26
|
def wrap(v, bits)
|
|
17
27
|
half = 1 << (bits - 1)
|
|
18
28
|
((v + half) & ((1 << bits) - 1)) - half
|
|
19
29
|
end
|
|
20
30
|
|
|
21
31
|
# Decodes DELTA_BINARY_PACKED integers. +bits+ is 32 or 64 (for wraparound).
|
|
22
|
-
# Returns [values, new_pos]. If +count+ is nil, the total from the header is used.
|
|
32
|
+
# Returns [values, new_pos]. If +count+ is nil, the total from the header is used. A smaller
|
|
33
|
+
# +count+ decodes only that many values, but the returned offset is still the end of the
|
|
34
|
+
# whole encoded block, so data following it can be read from there.
|
|
35
|
+
# @param data [String] binary page data
|
|
36
|
+
# @param pos [Integer] byte offset of the block header
|
|
37
|
+
# @param bits [Integer] integer width that deltas wrap around in, 32 or 64
|
|
38
|
+
# @param count [Integer, nil] maximum number of values to decode
|
|
39
|
+
# @return [Array(Array<Integer>, Integer)] the decoded values and the offset just past the encoded block
|
|
40
|
+
# @raise [FormatError] on an invalid header or miniblock bit width, or truncated data
|
|
23
41
|
def decode_binary_packed(data, pos, bits = 64, count = nil)
|
|
24
42
|
block_size, pos = RLE.read_uleb(data, pos)
|
|
25
43
|
miniblocks, pos = RLE.read_uleb(data, pos)
|
|
@@ -28,34 +46,44 @@ module Herringbone
|
|
|
28
46
|
raise FormatError, "Invalid DELTA_BINARY_PACKED header" if miniblocks.zero? || block_size % miniblocks != 0
|
|
29
47
|
per_mini = block_size / miniblocks
|
|
30
48
|
raise FormatError, "Invalid miniblock size #{per_mini}" if per_mini % 8 != 0
|
|
31
|
-
|
|
49
|
+
want = (count && count < total) ? count : total
|
|
32
50
|
values = []
|
|
33
51
|
return [values, pos] if total.zero?
|
|
34
52
|
last = zigzag_decode(first)
|
|
35
|
-
values << last
|
|
53
|
+
values << last if want.positive?
|
|
36
54
|
half = 1 << (bits - 1)
|
|
37
55
|
mask = (1 << bits) - 1
|
|
38
|
-
|
|
56
|
+
left = total - 1 # deltas still encoded, decoded or skipped
|
|
57
|
+
while left.positive?
|
|
39
58
|
min_delta, pos = RLE.read_uleb(data, pos)
|
|
40
59
|
min_delta = zigzag_decode(min_delta)
|
|
41
60
|
widths = data.byteslice(pos, miniblocks).unpack("C*")
|
|
42
61
|
pos += miniblocks
|
|
43
62
|
widths.each do |w|
|
|
44
|
-
|
|
63
|
+
# Miniblocks past the last value have a width byte but no body
|
|
64
|
+
break unless left.positive?
|
|
45
65
|
raise FormatError, "Invalid delta bit width #{w}" if w > bits
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
if values.size < want
|
|
67
|
+
deltas = RLE.unpack_bits(data, pos, per_mini, w)
|
|
68
|
+
take = want - values.size
|
|
69
|
+
deltas = deltas.first(take) if take < per_mini
|
|
70
|
+
deltas.each do |d|
|
|
71
|
+
last = ((last + min_delta + d + half) & mask) - half
|
|
72
|
+
values << last
|
|
73
|
+
end
|
|
53
74
|
end
|
|
75
|
+
pos += per_mini * w / 8
|
|
76
|
+
left -= per_mini
|
|
54
77
|
end
|
|
55
78
|
end
|
|
56
79
|
[values, pos]
|
|
57
80
|
end
|
|
58
81
|
|
|
82
|
+
# Encodes integers with DELTA_BINARY_PACKED, using BLOCK_SIZE values per block in MINIBLOCKS
|
|
83
|
+
# miniblocks. Deltas wrap at +bits+ so INT32 columns never need more than 32-bit widths.
|
|
84
|
+
# @param values [Array<Integer>] signed integers that fit in +bits+ bits
|
|
85
|
+
# @param bits [Integer] physical integer width, 32 or 64
|
|
86
|
+
# @return [String] encoded bytes in ASCII-8BIT
|
|
59
87
|
def encode_binary_packed(values, bits = 64)
|
|
60
88
|
out = String.new(encoding: Encoding::BINARY)
|
|
61
89
|
per_mini = BLOCK_SIZE / MINIBLOCKS
|
|
@@ -82,6 +110,12 @@ module Herringbone
|
|
|
82
110
|
out
|
|
83
111
|
end
|
|
84
112
|
|
|
113
|
+
# Decodes DELTA_LENGTH_BYTE_ARRAY: DELTA_BINARY_PACKED lengths followed by the concatenated bytes.
|
|
114
|
+
# @param data [String] binary page data
|
|
115
|
+
# @param pos [Integer] byte offset of the lengths block
|
|
116
|
+
# @param count [Integer] number of values to decode
|
|
117
|
+
# @return [Array(Array<String>, Integer)] binary slices of +data+ and the offset just past them
|
|
118
|
+
# @raise [FormatError] if there are too few lengths or a value runs past the end of +data+
|
|
85
119
|
def decode_length_byte_array(data, pos, count)
|
|
86
120
|
lengths, pos = decode_binary_packed(data, pos, 32, count)
|
|
87
121
|
raise FormatError, "DELTA_LENGTH_BYTE_ARRAY has #{lengths.size} lengths, need #{count}" if lengths.size < count
|
|
@@ -95,22 +129,35 @@ module Herringbone
|
|
|
95
129
|
[out, pos]
|
|
96
130
|
end
|
|
97
131
|
|
|
132
|
+
# Encodes byte strings with DELTA_LENGTH_BYTE_ARRAY.
|
|
133
|
+
# @param values [Array<String>] byte strings, in any encoding
|
|
134
|
+
# @return [String] encoded bytes in ASCII-8BIT
|
|
98
135
|
def encode_length_byte_array(values)
|
|
99
136
|
encode_binary_packed(values.map(&:bytesize), 32) << values.pack("a*" * values.size)
|
|
100
137
|
end
|
|
101
138
|
|
|
139
|
+
# Decodes DELTA_BYTE_ARRAY (incremental encoding): DELTA_BINARY_PACKED prefix lengths, then
|
|
140
|
+
# the suffixes as DELTA_LENGTH_BYTE_ARRAY. Each value is the previous value's prefix plus its suffix.
|
|
141
|
+
# @param data [String] binary page data
|
|
142
|
+
# @param pos [Integer] byte offset of the prefix lengths block
|
|
143
|
+
# @param count [Integer] number of values to decode
|
|
144
|
+
# @return [Array(Array<String>, Integer)] binary values and the offset just past them
|
|
145
|
+
# @raise [FormatError] if a prefix is negative or longer than the previous value, or the data is malformed
|
|
102
146
|
def decode_byte_array(data, pos, count)
|
|
103
147
|
prefixes, pos = decode_binary_packed(data, pos, 32, count)
|
|
104
148
|
suffixes, pos = decode_length_byte_array(data, pos, count)
|
|
105
149
|
prev = "".b
|
|
106
150
|
out = Array.new(count) do |i|
|
|
107
151
|
prefix = prefixes[i]
|
|
108
|
-
raise FormatError, "DELTA_BYTE_ARRAY prefix
|
|
152
|
+
raise FormatError, "DELTA_BYTE_ARRAY prefix length #{prefix} out of range" if prefix > prev.bytesize || prefix.negative?
|
|
109
153
|
prev = prefix.zero? ? suffixes[i] : prev.byteslice(0, prefix) + suffixes[i]
|
|
110
154
|
end
|
|
111
155
|
[out, pos]
|
|
112
156
|
end
|
|
113
157
|
|
|
158
|
+
# Encodes byte strings with DELTA_BYTE_ARRAY, sharing the common byte prefix with the previous value.
|
|
159
|
+
# @param values [Array<String>] byte strings, in any encoding
|
|
160
|
+
# @return [String] encoded bytes in ASCII-8BIT
|
|
114
161
|
def encode_byte_array(values)
|
|
115
162
|
prev = "".b
|
|
116
163
|
prefixes = []
|
|
@@ -132,6 +179,12 @@ module Herringbone
|
|
|
132
179
|
module_function
|
|
133
180
|
|
|
134
181
|
# Returns the value bytes re-interleaved into PLAIN layout
|
|
182
|
+
# @param data [String] binary page data
|
|
183
|
+
# @param pos [Integer] byte offset of the first stream
|
|
184
|
+
# @param count [Integer] number of values
|
|
185
|
+
# @param width [Integer] byte width of one value
|
|
186
|
+
# @return [Array(String, Integer)] PLAIN-layout bytes and the offset just past the streams
|
|
187
|
+
# @raise [FormatError] if fewer than count * width bytes remain
|
|
135
188
|
def decode(data, pos, count, width)
|
|
136
189
|
nbytes = count * width
|
|
137
190
|
raise FormatError, "Truncated BYTE_STREAM_SPLIT data" if pos + nbytes > data.bytesize
|
|
@@ -139,6 +192,10 @@ module Herringbone
|
|
|
139
192
|
[streams[0].zip(*streams[1..]).flatten.pack("C*"), pos + nbytes]
|
|
140
193
|
end
|
|
141
194
|
|
|
195
|
+
# Splits PLAIN-layout fixed-width values into +width+ byte streams. Inverse of decode.
|
|
196
|
+
# @param plain [String] PLAIN-encoded values, a multiple of +width+ bytes
|
|
197
|
+
# @param width [Integer] byte width of one value
|
|
198
|
+
# @return [String] the concatenated streams in ASCII-8BIT
|
|
142
199
|
def encode(plain, width)
|
|
143
200
|
count = plain.bytesize / width
|
|
144
201
|
bytes = plain.unpack("C*")
|
|
@@ -7,6 +7,7 @@ module Herringbone
|
|
|
7
7
|
module Plain
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
# Fixed-width numeric types: pack/unpack directive and byte width (all little-endian).
|
|
10
11
|
FORMATS = {
|
|
11
12
|
Format::Type::INT32 => ["l<", 4],
|
|
12
13
|
Format::Type::INT64 => ["q<", 8],
|
|
@@ -16,6 +17,14 @@ module Herringbone
|
|
|
16
17
|
|
|
17
18
|
# Decodes +count+ values of +type+ from +data+ starting at +pos+.
|
|
18
19
|
# Returns [values, new_pos].
|
|
20
|
+
# INT96 values come back as [nanoseconds_of_day, julian_day] pairs.
|
|
21
|
+
# @param data [String] binary page data
|
|
22
|
+
# @param pos [Integer] byte offset of the first value
|
|
23
|
+
# @param count [Integer] number of values to decode
|
|
24
|
+
# @param type [Integer] physical type, a Format::Type constant
|
|
25
|
+
# @param type_length [Integer, nil] byte width, required for FIXED_LEN_BYTE_ARRAY
|
|
26
|
+
# @return [Array(Array, Integer)] the decoded values and the offset just past them
|
|
27
|
+
# @raise [FormatError] if the data is truncated or the type is unknown
|
|
19
28
|
def decode(data, pos, count, type, type_length = nil)
|
|
20
29
|
case type
|
|
21
30
|
when Format::Type::BOOLEAN
|
|
@@ -44,6 +53,12 @@ module Herringbone
|
|
|
44
53
|
end
|
|
45
54
|
end
|
|
46
55
|
|
|
56
|
+
# Decodes PLAIN BYTE_ARRAY values: each is a 4-byte little-endian length followed by the bytes.
|
|
57
|
+
# @param data [String] binary page data
|
|
58
|
+
# @param pos [Integer] byte offset of the first length prefix
|
|
59
|
+
# @param count [Integer] number of values to decode
|
|
60
|
+
# @return [Array(Array<String>, Integer)] binary slices of +data+ and the offset just past them
|
|
61
|
+
# @raise [FormatError] if a length prefix or value runs past the end of +data+
|
|
47
62
|
def decode_byte_arrays(data, pos, count)
|
|
48
63
|
out = Array.new(count)
|
|
49
64
|
size = data.bytesize
|
|
@@ -60,6 +75,13 @@ module Herringbone
|
|
|
60
75
|
[out, pos]
|
|
61
76
|
end
|
|
62
77
|
|
|
78
|
+
# Encodes values of a physical type with PLAIN. Inverse of decode; INT96 values are
|
|
79
|
+
# [nanoseconds_of_day, julian_day] pairs and booleans are packed LSB-first, 8 per byte.
|
|
80
|
+
# @param values [Array] values in their physical Ruby form, without nulls
|
|
81
|
+
# @param type [Integer] physical type, a Format::Type constant
|
|
82
|
+
# @param type_length [Integer, nil] byte width, required for FIXED_LEN_BYTE_ARRAY
|
|
83
|
+
# @return [String] encoded bytes in ASCII-8BIT
|
|
84
|
+
# @raise [EncodeError] if a FIXED_LEN_BYTE_ARRAY value has the wrong size or the type is unknown
|
|
63
85
|
def encode(values, type, type_length = nil)
|
|
64
86
|
case type
|
|
65
87
|
when Format::Type::BOOLEAN
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Herringbone
|
|
4
|
+
# Decoders and encoders for the Parquet value and level encodings: PLAIN, the RLE / bit-packed
|
|
5
|
+
# hybrid, the DELTA_* family and BYTE_STREAM_SPLIT. They work on binary Strings and byte offsets,
|
|
6
|
+
# and know nothing about pages or columns.
|
|
4
7
|
module Encodings
|
|
5
8
|
# Bit packing (LSB-first, as used by Parquet) and the RLE / bit-packed hybrid encoding
|
|
6
9
|
# used for repetition/definition levels, dictionary indices and RLE booleans.
|
|
@@ -9,6 +12,11 @@ module Herringbone
|
|
|
9
12
|
|
|
10
13
|
# Unpacks +count+ values of +width+ bits each, starting at byte +offset+ of +data+.
|
|
11
14
|
# Missing trailing bytes are treated as zeroes.
|
|
15
|
+
# @param data [String] binary input
|
|
16
|
+
# @param offset [Integer] byte offset of the first packed value
|
|
17
|
+
# @param count [Integer] number of values to unpack
|
|
18
|
+
# @param width [Integer] bits per value, 0..64
|
|
19
|
+
# @return [Array<Integer>] +count+ unsigned values
|
|
12
20
|
def unpack_bits(data, offset, count, width)
|
|
13
21
|
return Array.new(count, 0) if width.zero?
|
|
14
22
|
nbytes = (count * width + 7) / 8
|
|
@@ -22,7 +30,7 @@ module Herringbone
|
|
|
22
30
|
|
|
23
31
|
# Pad to a whole number of 32-bit words plus one spare word, so reads never go out of range
|
|
24
32
|
pad = (-chunk.bytesize % 4) + 4
|
|
25
|
-
words = (
|
|
33
|
+
words = (chunk + ("\0" * pad)).unpack("V*")
|
|
26
34
|
mask = (1 << width) - 1
|
|
27
35
|
out = Array.new(count)
|
|
28
36
|
bitpos = 0
|
|
@@ -41,6 +49,10 @@ module Herringbone
|
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
# Slow path for widths above 32 bits (used by DELTA_BINARY_PACKED with 64-bit values)
|
|
52
|
+
# @param chunk [String] packed bytes, starting at the first value
|
|
53
|
+
# @param count [Integer] number of values to unpack
|
|
54
|
+
# @param width [Integer] bits per value, 33..64
|
|
55
|
+
# @return [Array<Integer>] +count+ unsigned values
|
|
44
56
|
def unpack_wide(chunk, count, width)
|
|
45
57
|
mask = (1 << width) - 1
|
|
46
58
|
out = Array.new(count)
|
|
@@ -61,6 +73,9 @@ module Herringbone
|
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
# Packs +values+ with +width+ bits each; the value count is padded up to a multiple of 8.
|
|
76
|
+
# @param values [Array<Integer>] non-negative integers that fit in +width+ bits
|
|
77
|
+
# @param width [Integer] bits per value
|
|
78
|
+
# @return [String] packed bytes in ASCII-8BIT, +width+ bytes per group of 8 values
|
|
64
79
|
def pack_bits(values, width)
|
|
65
80
|
return "".b if width.zero? || values.empty?
|
|
66
81
|
n = values.size
|
|
@@ -93,6 +108,11 @@ module Herringbone
|
|
|
93
108
|
out
|
|
94
109
|
end
|
|
95
110
|
|
|
111
|
+
# Reads an unsigned LEB128 varint (hybrid run headers, DELTA_BINARY_PACKED headers).
|
|
112
|
+
# @param data [String] binary input
|
|
113
|
+
# @param pos [Integer] byte offset of the varint
|
|
114
|
+
# @return [Array(Integer, Integer)] the decoded value and the offset just past it
|
|
115
|
+
# @raise [FormatError] if the input ends inside the varint
|
|
96
116
|
def read_uleb(data, pos)
|
|
97
117
|
result = 0
|
|
98
118
|
shift = 0
|
|
@@ -106,6 +126,10 @@ module Herringbone
|
|
|
106
126
|
end
|
|
107
127
|
end
|
|
108
128
|
|
|
129
|
+
# Appends +n+ as an unsigned LEB128 varint.
|
|
130
|
+
# @param out [String] binary output buffer, appended to
|
|
131
|
+
# @param n [Integer] non-negative integer to encode
|
|
132
|
+
# @return [String] +out+
|
|
109
133
|
def write_uleb(out, n)
|
|
110
134
|
while n >= 0x80
|
|
111
135
|
out << ((n & 0x7F) | 0x80)
|
|
@@ -116,6 +140,13 @@ module Herringbone
|
|
|
116
140
|
|
|
117
141
|
# Decodes the RLE/bit-packed hybrid from +data+ between +pos+ and +limit+,
|
|
118
142
|
# returning exactly +count+ values (missing values are an error).
|
|
143
|
+
# @param data [String] binary input
|
|
144
|
+
# @param pos [Integer] byte offset of the first run header
|
|
145
|
+
# @param limit [Integer] byte offset just past the encoded data
|
|
146
|
+
# @param width [Integer] bits per value
|
|
147
|
+
# @param count [Integer] number of values to decode
|
|
148
|
+
# @return [Array<Integer>] exactly +count+ values
|
|
149
|
+
# @raise [FormatError] if the runs end before +count+ values were produced
|
|
119
150
|
def decode_hybrid(data, pos, limit, width, count)
|
|
120
151
|
out = []
|
|
121
152
|
value_bytes = (width + 7) / 8
|
|
@@ -145,6 +176,10 @@ module Herringbone
|
|
|
145
176
|
|
|
146
177
|
# Encodes +values+ with the RLE/bit-packed hybrid. Repeated runs of 8+ equal values
|
|
147
178
|
# become RLE runs, everything else goes into bit-packed groups of 8.
|
|
179
|
+
# Output has no length prefix; callers that need one (levels in data page v1) add it.
|
|
180
|
+
# @param values [Array<Integer>] non-negative integers that fit in +width+ bits
|
|
181
|
+
# @param width [Integer] bits per value
|
|
182
|
+
# @return [String] encoded runs in ASCII-8BIT
|
|
148
183
|
def encode_hybrid(values, width)
|
|
149
184
|
out = String.new(encoding: Encoding::BINARY)
|
|
150
185
|
n = values.size
|
|
@@ -181,6 +216,13 @@ module Herringbone
|
|
|
181
216
|
out
|
|
182
217
|
end
|
|
183
218
|
|
|
219
|
+
# Appends values[from...to] as one bit-packed run, zero-padded to whole groups of 8.
|
|
220
|
+
# @param out [String] binary output buffer, appended to
|
|
221
|
+
# @param values [Array<Integer>] all values being encoded
|
|
222
|
+
# @param from [Integer] index of the first literal value
|
|
223
|
+
# @param to [Integer] index just past the last literal value
|
|
224
|
+
# @param width [Integer] bits per value
|
|
225
|
+
# @return [String] +out+
|
|
184
226
|
def flush_literals(out, values, from, to, width)
|
|
185
227
|
count = to - from
|
|
186
228
|
groups = (count + 7) / 8
|
|
@@ -188,13 +230,23 @@ module Herringbone
|
|
|
188
230
|
out << pack_bits(values[from, count], width)
|
|
189
231
|
end
|
|
190
232
|
|
|
233
|
+
# Number of bits needed to store values up to +max_value+ (0 for 0).
|
|
234
|
+
# @param max_value [Integer, nil] largest value to encode; nil counts as 0
|
|
235
|
+
# @return [Integer] bit width
|
|
191
236
|
def bit_width(max_value)
|
|
192
237
|
max_value.to_i.bit_length
|
|
193
238
|
end
|
|
194
239
|
|
|
195
240
|
# Legacy BIT_PACKED level encoding (deprecated): MSB-first bit order, no header.
|
|
241
|
+
# @param data [String] binary input
|
|
242
|
+
# @param pos [Integer] byte offset of the packed levels
|
|
243
|
+
# @param width [Integer] bits per value
|
|
244
|
+
# @param count [Integer] number of values to decode
|
|
245
|
+
# @return [Array<Integer>] +count+ levels
|
|
246
|
+
# @raise [FormatError] if +data+ ends before +count+ levels
|
|
196
247
|
def decode_legacy_bit_packed(data, pos, width, count)
|
|
197
248
|
nbytes = (count * width + 7) / 8
|
|
249
|
+
raise FormatError, "Truncated BIT_PACKED levels" if pos + nbytes > data.bytesize
|
|
198
250
|
bits = data.byteslice(pos, nbytes).unpack1("B*")
|
|
199
251
|
Array.new(count) { |i| bits[i * width, width].to_i(2) }
|
|
200
252
|
end
|