herringbone 0.8.0 → 0.9.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: b0226448164e89104b937f6d2009b6b8de9893fbabf06b20ab7379cdfd306ccd
4
- data.tar.gz: a90c4223cf2f849fc839e3389cf40bba2d17145525bc99e57412f53cbb83f866
3
+ metadata.gz: c59941b3e4ed776a03d3596bf8b9288dd852b89f49934dfb1110f6b29d527ab8
4
+ data.tar.gz: 13ef32e91a12a11d249be06834f0c33dfb70c9ebb7746d29d28844f9af180fa5
5
5
  SHA512:
6
- metadata.gz: fe6316662f7ed600969dfd0ed7c297faf26c9694e9143b277bffca342b5593eb346989a99cce41e52ca27b3b655d069e53f8e279863b869bdb3703fcb0e7bb20
7
- data.tar.gz: 9c441b9927dbd48f589bf42309e3468e55e47f056c363fc2edce0692e9be745de1db164bb313c04181e64b88b0262770843a983bff4a7b235a437deddd5e7b9e
6
+ metadata.gz: '083624f293ce5d1825c9f6998e46b6e58767e9bbec15d6adbbd1cb556ef3771d2a0426a41ba8f097a43b2b937bef55ce018a01925a7df2d9f9d8d36a1b4f8da9'
7
+ data.tar.gz: 38b917be5c728a3837d3eafae3e9f060708574f5c6249792219d17a410a4fe14683df46dfd4b922d3bf4503a327a2384105c950e06ca1523a7482c042629f367
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0
6
+
7
+ - ZSTD files are read without the `zstd-ruby` gem, by a pure-Ruby decompressor; `zstd-ruby` is
8
+ still used when loaded, and still needed to write ZSTD.
9
+
5
10
  ## 0.8.0
6
11
 
7
12
  - `Herringbone::MissingCodecError` explains over several lines which gem to install and that it has
data/MANUAL.md CHANGED
@@ -55,7 +55,7 @@ Add these libraries to speed up and enable certain functionality:
55
55
 
56
56
  ```ruby
57
57
  gem "snappy" # native Snappy, 2-3x faster reads and writes of typical files
58
- gem "zstd-ruby" # ZSTD
58
+ gem "zstd-ruby" # writing ZSTD; reading falls back to pure Ruby, 30-100x slower
59
59
  gem "brotli" # Brotli
60
60
  gem "xxhash" # faster bloom filters
61
61
  gem "numo-narray-alt" # read(as: :numo)
data/README.md CHANGED
@@ -4,7 +4,7 @@ A pure-Ruby reader and writer for [Apache Parquet](https://parquet.apache.org/)
4
4
 
5
5
  - No Thrift gem and no native extensions of its own: the Thrift compact protocol, all encodings
6
6
  and the Snappy and LZ4 codecs are implemented in Ruby (ZSTD and Brotli use optional gems).
7
- LZO, from Hadoop-era files, can be read but not written
7
+ ZSTD and LZO (from Hadoop-era files) are also read in pure Ruby; writing ZSTD needs `zstd-ruby`
8
8
  - Full nesting support (structs, lists, maps, any depth) via Dremel record shredding/assembly
9
9
  - Reads files from parquet-mr, Arrow, Spark, Impala, DuckDB, Rust writers etc.
10
10
  - Optimized reads with batches and pages
@@ -21,9 +21,9 @@ rules, combining files, type mapping and the inspector.
21
21
  gem "herringbone"
22
22
  ```
23
23
 
24
- Optionally add `snappy` (faster), `zstd-ruby` and `brotli` (more codecs), `xxhash` (faster bloom
25
- filters) or `numo-narray-alt` (`read(as: :numo)`). Herringbone uses whichever of them are loaded,
26
- see [Optional gems](MANUAL.md#optional-gems).
24
+ Optionally add `snappy` (faster), `zstd-ruby` (writing ZSTD, much faster reading of it), `brotli`,
25
+ `xxhash` (faster bloom filters) or `numo-narray-alt` (`read(as: :numo)`). Herringbone uses
26
+ whichever of them are loaded, see [Optional gems](MANUAL.md#optional-gems).
27
27
 
28
28
  ## Exporting from Rails
29
29
 
@@ -0,0 +1,774 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herringbone
4
+ module Codecs
5
+ # Pure-Ruby Zstandard decompressor, so files written with ZSTD can be read without the
6
+ # zstd-ruby gem. When zstd-ruby is loaded, Compression uses it instead; writing ZSTD still
7
+ # needs it, as there is no compressor here.
8
+ #
9
+ # Written from RFC 8878. Dictionaries are not supported (Parquet does not use them).
10
+ #
11
+ # Every input is distrusted: output may never grow past the size the page header declares,
12
+ # every loop either consumes input or produces bounded output, and malformed tables, offsets
13
+ # and bitstreams raise Error instead of decoding garbage.
14
+ #
15
+ # @api private
16
+ module Zstd
17
+ # Raised for corrupt, truncated or unsupported Zstandard input, and for input that
18
+ # decompresses to more than the expected size.
19
+ class Error < StandardError; end
20
+
21
+ # Little-endian magic number of a Zstandard frame
22
+ MAGIC = 0xFD2FB528
23
+ # Magic numbers 0x184D2A50..0x184D2A5F start a skippable frame
24
+ SKIPPABLE_MAGIC = 0x184D2A50
25
+ # No block may decompress to more than this, nor have a larger compressed body
26
+ BLOCK_MAX = 128 * 1024
27
+ # The output grows past this as it is decoded, so a page header claiming a huge size
28
+ # cannot make us allocate it upfront
29
+ PREALLOCATE_MAX = 1 << 22
30
+ # Shorthand for Encoding::BINARY.
31
+ BINARY = Encoding::BINARY
32
+
33
+ # Literal length codes: baseline by code
34
+ LL_BASE = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 28, 32, 40,
35
+ 48, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16_384, 32_768, 65_536].freeze
36
+ # Extra bits of each literal length code
37
+ LL_BITS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3,
38
+ 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].freeze
39
+ # Match length codes: baseline by code
40
+ ML_BASE = ((3..34).to_a + [35, 37, 39, 41, 43, 47, 51, 59, 67, 83, 99, 131, 259, 515, 1027, 2051,
41
+ 4099, 8195, 16_387, 32_771, 65_539]).freeze
42
+ # Extra bits of each match length code
43
+ ML_BITS = ([0] * 32 + [1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]).freeze
44
+ # Largest offset code; its value takes this many extra bits
45
+ MAX_OFFSET_CODE = 31
46
+
47
+ # Predefined FSE distributions (RFC 8878 section 3.1.1.3.2.2), with their accuracy logs
48
+ LL_DEFAULT = [[4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2,
49
+ 1, 1, 1, 1, 1, -1, -1, -1, -1], 6].freeze
50
+ # Predefined match length distribution and accuracy log
51
+ ML_DEFAULT = [[1, 4, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
52
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1], 6].freeze
53
+ # Predefined offset code distribution and accuracy log
54
+ OF_DEFAULT = [[1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1,
55
+ -1, -1, -1], 5].freeze
56
+
57
+ module_function
58
+
59
+ # Decompresses one or more concatenated Zstandard frames (skippable frames are ignored)
60
+ #
61
+ # @param input [String] compressed bytes
62
+ # @param limit [Integer] the decompressed size expected, which the output may not exceed
63
+ # @return [String] decompressed bytes in ASCII-8BIT
64
+ # @raise [Error] if the input is corrupt, truncated, uses a dictionary or decompresses to
65
+ # more than +limit+ bytes
66
+ def decompress(input, limit)
67
+ src = (input.encoding == BINARY) ? input : input.b
68
+ raise Error, "Zstandard input is empty, expected at least one frame" if src.empty?
69
+ out = String.new(capacity: [limit, PREALLOCATE_MAX].min, encoding: BINARY)
70
+ pos = 0
71
+ while pos < src.bytesize
72
+ raise Error, "Zstandard input ends inside a frame magic number at offset #{pos}" if src.bytesize - pos < 4
73
+ magic = u32(src, pos)
74
+ if magic & 0xFFFFFFF0 == SKIPPABLE_MAGIC
75
+ raise Error, "Zstandard skippable frame at offset #{pos} is truncated" if src.bytesize - pos < 8
76
+ pos += 8 + u32(src, pos + 4)
77
+ raise Error, "Zstandard skippable frame runs past the end of the input" if pos > src.bytesize
78
+ elsif magic == MAGIC
79
+ pos = FrameDecoder.new(src, pos + 4, out, limit).decode
80
+ else
81
+ raise Error, format("Not Zstandard data: expected the frame magic number at offset %d, found 0x%08X", pos, magic)
82
+ end
83
+ end
84
+ out
85
+ end
86
+
87
+ # @param src [String] binary string
88
+ # @param pos [Integer] offset of the value
89
+ # @return [Integer] the little-endian 32-bit value at +pos+
90
+ def u32(src, pos)
91
+ src.getbyte(pos) | (src.getbyte(pos + 1) << 8) | (src.getbyte(pos + 2) << 16) | (src.getbyte(pos + 3) << 24)
92
+ end
93
+
94
+ # Reads a stream backwards, from its last byte to its first, as FSE and Huffman streams are
95
+ # written. The last byte holds a 1 bit marking where the stream starts. Reading past the
96
+ # first byte yields zeros, and is remembered so the caller can tell the stream was too short.
97
+ #
98
+ # @api private
99
+ class BackwardBits
100
+ # @param src [String] binary input
101
+ # @param start [Integer] offset of the stream's first byte
102
+ # @param stop [Integer] offset just past its last byte
103
+ # @raise [Error] if the stream is empty or its last byte has no start marker
104
+ def initialize(src, start, stop)
105
+ raise Error, "Zstandard bitstream is empty" if stop <= start
106
+ last = src.getbyte(stop - 1)
107
+ raise Error, "Zstandard bitstream does not end with a marker bit (last byte is 0)" if last == 0
108
+ @src = src
109
+ @start = start
110
+ @pos = stop - 1
111
+ @nbits = last.bit_length - 1
112
+ @bits = last & ((1 << @nbits) - 1)
113
+ @overflow = 0
114
+ end
115
+
116
+ # @param n [Integer] number of bits, at most 56
117
+ # @return [Integer] the next +n+ bits, most significant first
118
+ def read(n)
119
+ return 0 if n == 0
120
+ refill if @nbits < n
121
+ if @nbits < n
122
+ @overflow += n - @nbits
123
+ @bits <<= n - @nbits
124
+ @nbits = n
125
+ end
126
+ @nbits -= n
127
+ value = @bits >> @nbits
128
+ @bits &= (1 << @nbits) - 1
129
+ value
130
+ end
131
+
132
+ # @param n [Integer] number of bits, at most 56
133
+ # @return [Integer] the next +n+ bits, without consuming them
134
+ def peek(n)
135
+ refill if @nbits < n
136
+ (@nbits < n) ? @bits << (n - @nbits) : @bits >> (@nbits - n)
137
+ end
138
+
139
+ # @param n [Integer] number of bits to consume, after a peek of at least +n+
140
+ # @return [void]
141
+ def skip(n)
142
+ if @nbits < n
143
+ @overflow += n - @nbits
144
+ @bits = 0
145
+ @nbits = 0
146
+ else
147
+ @nbits -= n
148
+ @bits &= (1 << @nbits) - 1
149
+ end
150
+ end
151
+
152
+ # @return [Boolean] whether more bits were read than the stream has
153
+ def overflowed?
154
+ @overflow > 0
155
+ end
156
+
157
+ # @return [Boolean] whether every bit was read, and no more
158
+ def finished?
159
+ @nbits == 0 && @pos == @start && @overflow == 0
160
+ end
161
+
162
+ private
163
+
164
+ # @return [void]
165
+ def refill
166
+ while @nbits <= 48 && @pos > @start
167
+ @pos -= 1
168
+ @bits = (@bits << 8) | @src.getbyte(@pos)
169
+ @nbits += 8
170
+ end
171
+ end
172
+ end
173
+
174
+ # An FSE decoding table: for each state, the symbol it decodes to and how to reach the
175
+ # next state (read +nbits+ bits, add them to +base+)
176
+ #
177
+ # @api private
178
+ FSETable = Struct.new(:log, :symbols, :nbits, :base)
179
+
180
+ # A Huffman decoding table indexed by the next +max_bits+ bits of the stream
181
+ #
182
+ # @api private
183
+ HuffmanTable = Struct.new(:max_bits, :symbols, :lengths)
184
+
185
+ # Decodes one frame, appending its content to the output. Keeps what blocks of a frame
186
+ # share: the repeat offsets and the tables later blocks may reuse.
187
+ #
188
+ # @api private
189
+ class FrameDecoder
190
+ # @param src [String] binary input
191
+ # @param pos [Integer] offset just past the frame's magic number
192
+ # @param out [String] binary output, appended to
193
+ # @param limit [Integer] size +out+ may not exceed
194
+ def initialize(src, pos, out, limit)
195
+ @src = src
196
+ @pos = pos
197
+ @out = out
198
+ @limit = limit
199
+ @base = out.bytesize
200
+ @reps = [1, 4, 8]
201
+ @huffman = nil
202
+ @ll_table = @of_table = @ml_table = nil
203
+ end
204
+
205
+ # @return [Integer] offset just past the frame
206
+ # @raise [Error] for a corrupt or truncated frame, or one that would exceed the limit
207
+ def decode
208
+ content_size, checksum = read_frame_header
209
+ if content_size && content_size > @limit - @out.bytesize
210
+ raise Error, "Zstandard frame declares #{content_size} bytes of content, more than the " \
211
+ "#{@limit - @out.bytesize} bytes left of the #{@limit} expected"
212
+ end
213
+
214
+ last = false
215
+ until last
216
+ need(3, "block header")
217
+ header = @src.getbyte(@pos) | (@src.getbyte(@pos + 1) << 8) | (@src.getbyte(@pos + 2) << 16)
218
+ @pos += 3
219
+ last = header & 1 == 1
220
+ size = header >> 3
221
+ case (header >> 1) & 3
222
+ when 0
223
+ raise Error, "Zstandard raw block of #{size} bytes is larger than the #{BLOCK_MAX} allowed" if size > BLOCK_MAX
224
+ need(size, "raw block")
225
+ emit(size)
226
+ @out << @src.byteslice(@pos, size)
227
+ @pos += size
228
+ when 1
229
+ need(1, "RLE block")
230
+ raise Error, "Zstandard RLE block of #{size} bytes is larger than the #{BLOCK_MAX} allowed" if size > BLOCK_MAX
231
+ emit(size)
232
+ @out << (@src.byteslice(@pos, 1) * size)
233
+ @pos += 1
234
+ when 2
235
+ raise Error, "Zstandard compressed block of #{size} bytes is larger than the #{BLOCK_MAX} allowed" if size > BLOCK_MAX
236
+ need(size, "compressed block")
237
+ before = @out.bytesize
238
+ decode_compressed_block(@pos, @pos + size)
239
+ if @out.bytesize - before > BLOCK_MAX
240
+ raise Error, "Zstandard block decompressed to #{@out.bytesize - before} bytes, more than the #{BLOCK_MAX} allowed"
241
+ end
242
+ @pos += size
243
+ else
244
+ raise Error, "Zstandard block at offset #{@pos - 3} has the reserved block type 3"
245
+ end
246
+ end
247
+
248
+ produced = @out.bytesize - @base
249
+ if content_size && produced != content_size
250
+ raise Error, "Zstandard frame decompressed to #{produced} bytes, but its header declares #{content_size}"
251
+ end
252
+ if checksum
253
+ need(4, "content checksum")
254
+ expected = Zstd.u32(@src, @pos)
255
+ actual = XXHash.xxh64(@out.byteslice(@base, produced)) & 0xFFFFFFFF
256
+ raise Error, format("Zstandard content checksum mismatch: frame says %08x, content hashes to %08x", expected, actual) if expected != actual
257
+ @pos += 4
258
+ end
259
+ @pos
260
+ end
261
+
262
+ private
263
+
264
+ # @return [Array(Integer, Boolean)] the declared content size (nil when absent) and
265
+ # whether a content checksum follows the blocks
266
+ def read_frame_header
267
+ need(1, "frame header")
268
+ descriptor = @src.getbyte(@pos)
269
+ @pos += 1
270
+ fcs_flag = descriptor >> 6
271
+ single_segment = descriptor[5] == 1
272
+ raise Error, "Zstandard frame header has the reserved bit set" if descriptor[3] == 1
273
+ dict_id_size = [0, 1, 2, 4][descriptor & 3]
274
+ fcs_size = [single_segment ? 1 : 0, 2, 4, 8][fcs_flag]
275
+
276
+ header_size = (single_segment ? 0 : 1) + dict_id_size + fcs_size
277
+ need(header_size, "frame header")
278
+ @pos += 1 unless single_segment # window descriptor: we keep the whole output anyway
279
+ if dict_id_size > 0
280
+ dict_id = read_le(@pos, dict_id_size)
281
+ if dict_id != 0
282
+ raise Error, "Zstandard frame needs dictionary #{dict_id}, but dictionaries are not supported"
283
+ end
284
+ @pos += dict_id_size
285
+ end
286
+ content_size = (fcs_size > 0) ? read_le(@pos, fcs_size) : nil
287
+ content_size += 256 if fcs_size == 2
288
+ @pos += fcs_size
289
+ [content_size, descriptor[2] == 1]
290
+ end
291
+
292
+ # @param pos [Integer] offset of the value
293
+ # @param size [Integer] its size in bytes
294
+ # @return [Integer] the little-endian unsigned value
295
+ def read_le(pos, size)
296
+ value = 0
297
+ (size - 1).downto(0) { |i| value = (value << 8) | @src.getbyte(pos + i) }
298
+ value
299
+ end
300
+
301
+ # @param n [Integer] bytes needed at the current position
302
+ # @param what [String] what they are, for the message
303
+ # @return [void]
304
+ # @raise [Error] if the input has fewer
305
+ def need(n, what)
306
+ return if @src.bytesize - @pos >= n
307
+ raise Error, "Zstandard #{what} at offset #{@pos} needs #{n} bytes, but only #{@src.bytesize - @pos} are left"
308
+ end
309
+
310
+ # @param n [Integer] bytes about to be appended to the output
311
+ # @return [void]
312
+ # @raise [Error] if they would take it past the limit
313
+ def emit(n)
314
+ return if @out.bytesize + n <= @limit
315
+ raise Error, "Zstandard data decompresses to more than the #{@limit} bytes expected"
316
+ end
317
+
318
+ # @param pos [Integer] offset of the block body
319
+ # @param stop [Integer] offset just past it
320
+ # @return [void]
321
+ def decode_compressed_block(pos, stop)
322
+ literals, pos = read_literals(pos, stop)
323
+ raise Error, "Zstandard block ends before its sequences section" if pos >= stop
324
+ count = @src.getbyte(pos)
325
+ pos += 1
326
+ if count >= 128
327
+ raise Error, "Zstandard sequence count is truncated" if pos >= stop
328
+ if count == 255
329
+ raise Error, "Zstandard sequence count is truncated" if pos + 2 > stop
330
+ count = @src.getbyte(pos) + (@src.getbyte(pos + 1) << 8) + 0x7F00
331
+ pos += 2
332
+ else
333
+ count = ((count - 128) << 8) + @src.getbyte(pos)
334
+ pos += 1
335
+ end
336
+ end
337
+
338
+ if count == 0
339
+ raise Error, "Zstandard block has #{stop - pos} bytes after an empty sequences section" if pos != stop
340
+ emit(literals.bytesize)
341
+ @out << literals
342
+ return
343
+ end
344
+
345
+ raise Error, "Zstandard sequences section has no compression modes byte" if pos >= stop
346
+ modes = @src.getbyte(pos)
347
+ pos += 1
348
+ raise Error, "Zstandard sequences section sets reserved mode bits" if modes & 3 != 0
349
+ @ll_table, pos = read_sequence_table(modes >> 6, pos, stop, @ll_table, LL_PREDEFINED, LL_BASE.size - 1, 9, "literal lengths")
350
+ @of_table, pos = read_sequence_table((modes >> 4) & 3, pos, stop, @of_table, OF_PREDEFINED, MAX_OFFSET_CODE, 8, "offsets")
351
+ @ml_table, pos = read_sequence_table((modes >> 2) & 3, pos, stop, @ml_table, ML_PREDEFINED, ML_BASE.size - 1, 9, "match lengths")
352
+ execute_sequences(BackwardBits.new(@src, pos, stop), count, literals)
353
+ end
354
+
355
+ # @param mode [Integer] 0 predefined, 1 RLE, 2 FSE-compressed, 3 repeat
356
+ # @param pos [Integer] offset of the table description
357
+ # @param stop [Integer] end of the block
358
+ # @param previous [FSETable, nil] the table of the previous block, for mode 3
359
+ # @param default [FSETable] the predefined table, for mode 0
360
+ # @param max_symbol [Integer] largest valid code
361
+ # @param max_log [Integer] largest valid accuracy log
362
+ # @param name [String] what the codes are, for messages
363
+ # @return [Array(FSETable, Integer)] the table, and the offset after its description
364
+ def read_sequence_table(mode, pos, stop, previous, default, max_symbol, max_log, name)
365
+ case mode
366
+ when 0
367
+ [default, pos]
368
+ when 1
369
+ raise Error, "Zstandard RLE table for #{name} is truncated" if pos >= stop
370
+ symbol = @src.getbyte(pos)
371
+ raise Error, "Zstandard RLE table for #{name} uses code #{symbol}, the largest is #{max_symbol}" if symbol > max_symbol
372
+ [FSETable.new(0, [symbol], [0], [0]), pos + 1]
373
+ when 2
374
+ counts, log, pos = Zstd.read_fse_counts(@src, pos, stop, max_symbol, max_log)
375
+ [Zstd.build_fse_table(counts, log), pos]
376
+ else
377
+ raise Error, "Zstandard block repeats the #{name} table, but no earlier block has one" unless previous
378
+ [previous, pos]
379
+ end
380
+ end
381
+
382
+ # Decodes the sequences and copies their literals and matches to the output
383
+ #
384
+ # @param bits [BackwardBits] the sequences bitstream
385
+ # @param count [Integer] number of sequences
386
+ # @param literals [String] decoded literals, which the sequences copy from in order
387
+ # @return [void]
388
+ def execute_sequences(bits, count, literals)
389
+ ll_t = @ll_table
390
+ of_t = @of_table
391
+ ml_t = @ml_table
392
+ ll_state = bits.read(ll_t.log)
393
+ of_state = bits.read(of_t.log)
394
+ ml_state = bits.read(ml_t.log)
395
+ r1, r2, r3 = @reps
396
+ out = @out
397
+ base = @base
398
+ limit = @limit
399
+ lit_pos = 0
400
+ lit_size = literals.bytesize
401
+ i = 0
402
+ while i < count
403
+ of_code = of_t.symbols[of_state]
404
+ ll_code = ll_t.symbols[ll_state]
405
+ ml_code = ml_t.symbols[ml_state]
406
+ offset_value = (1 << of_code) + bits.read(of_code)
407
+ match_len = ML_BASE[ml_code] + bits.read(ML_BITS[ml_code])
408
+ lit_len = LL_BASE[ll_code] + bits.read(LL_BITS[ll_code])
409
+
410
+ if offset_value > 3
411
+ offset = offset_value - 3
412
+ r3 = r2
413
+ r2 = r1
414
+ r1 = offset
415
+ else
416
+ case offset_value - ((lit_len == 0) ? 0 : 1)
417
+ when 0
418
+ offset = r1
419
+ when 1
420
+ offset = r2
421
+ r2 = r1
422
+ r1 = offset
423
+ when 2
424
+ offset = r3
425
+ r3 = r2
426
+ r2 = r1
427
+ r1 = offset
428
+ else
429
+ offset = r1 - 1
430
+ raise Error, "Zstandard sequence #{i} repeats an offset of 0" if offset == 0
431
+ r3 = r2
432
+ r2 = r1
433
+ r1 = offset
434
+ end
435
+ end
436
+
437
+ i += 1
438
+ if i < count
439
+ ll_state = ll_t.base[ll_state] + bits.read(ll_t.nbits[ll_state])
440
+ ml_state = ml_t.base[ml_state] + bits.read(ml_t.nbits[ml_state])
441
+ of_state = of_t.base[of_state] + bits.read(of_t.nbits[of_state])
442
+ end
443
+ raise Error, "Zstandard sequences bitstream is too short for #{count} sequences" if bits.overflowed?
444
+
445
+ if lit_pos + lit_len > lit_size
446
+ raise Error, "Zstandard sequence #{i - 1} needs #{lit_len} literals, but only #{lit_size - lit_pos} are left"
447
+ end
448
+ at = out.bytesize
449
+ if at + lit_len + match_len > limit
450
+ raise Error, "Zstandard data decompresses to more than the #{limit} bytes expected"
451
+ end
452
+ out << literals.byteslice(lit_pos, lit_len) if lit_len > 0
453
+ lit_pos += lit_len
454
+ at += lit_len
455
+ if offset > at - base
456
+ raise Error, "Zstandard sequence #{i - 1} copies from #{offset} bytes back, but the frame has only #{at - base} bytes so far"
457
+ end
458
+ if offset >= match_len
459
+ out << out.byteslice(at - offset, match_len)
460
+ else
461
+ pattern = out.byteslice(at - offset, offset)
462
+ out << (pattern * (match_len / offset)) << pattern.byteslice(0, match_len % offset)
463
+ end
464
+ end
465
+
466
+ raise Error, "Zstandard sequences bitstream has bits left after #{count} sequences" unless bits.finished?
467
+ @reps = [r1, r2, r3]
468
+ rest = lit_size - lit_pos
469
+ emit(rest)
470
+ out << literals.byteslice(lit_pos, rest) if rest > 0
471
+ end
472
+
473
+ # @param pos [Integer] offset of the literals section
474
+ # @param stop [Integer] end of the block
475
+ # @return [Array(String, Integer)] the literals, and the offset just past the section
476
+ def read_literals(pos, stop)
477
+ raise Error, "Zstandard block is empty, expected a literals section" if pos >= stop
478
+ b0 = @src.getbyte(pos)
479
+ type = b0 & 3
480
+ size_format = (b0 >> 2) & 3
481
+
482
+ if type < 2
483
+ header_size = [1, 2, 1, 3][size_format]
484
+ raise Error, "Zstandard literals header is truncated" if pos + header_size > stop
485
+ size = case header_size
486
+ when 1 then b0 >> 3
487
+ when 2 then (b0 >> 4) + (@src.getbyte(pos + 1) << 4)
488
+ else (b0 >> 4) + (@src.getbyte(pos + 1) << 4) + (@src.getbyte(pos + 2) << 12)
489
+ end
490
+ raise Error, "Zstandard block has #{size} literals, more than the #{BLOCK_MAX} allowed" if size > BLOCK_MAX
491
+ pos += header_size
492
+ if type == 0
493
+ raise Error, "Zstandard raw literals run past the end of the block" if pos + size > stop
494
+ return [@src.byteslice(pos, size), pos + size]
495
+ end
496
+ raise Error, "Zstandard RLE literals are truncated" if pos >= stop
497
+ return [@src.byteslice(pos, 1) * size, pos + 1]
498
+ end
499
+
500
+ header_size, size_bits, streams = [[3, 10, 1], [3, 10, 4], [4, 14, 4], [5, 18, 4]][size_format]
501
+ raise Error, "Zstandard literals header is truncated" if pos + header_size > stop
502
+ header = read_le(pos, header_size)
503
+ mask = (1 << size_bits) - 1
504
+ size = (header >> 4) & mask
505
+ compressed_size = (header >> (4 + size_bits)) & mask
506
+ raise Error, "Zstandard block has #{size} literals, more than the #{BLOCK_MAX} allowed" if size > BLOCK_MAX
507
+ pos += header_size
508
+ stop_literals = pos + compressed_size
509
+ raise Error, "Zstandard compressed literals run past the end of the block" if stop_literals > stop
510
+
511
+ if type == 2
512
+ @huffman, pos = Zstd.read_huffman_table(@src, pos, stop_literals)
513
+ elsif @huffman.nil?
514
+ raise Error, "Zstandard block reuses the Huffman table, but no earlier block has one"
515
+ end
516
+ [Zstd.decode_huffman_literals(@huffman, @src, pos, stop_literals, size, streams), stop_literals]
517
+ end
518
+ end
519
+
520
+ # Reads an FSE table description (RFC 8878 section 4.1.1)
521
+ #
522
+ # @param src [String] binary input
523
+ # @param pos [Integer] offset of the description
524
+ # @param stop [Integer] offset it may not reach past
525
+ # @param max_symbol [Integer] largest valid symbol
526
+ # @param max_log [Integer] largest valid accuracy log
527
+ # @return [Array(Array<Integer>, Integer, Integer)] the normalized counts (-1 for "less than
528
+ # one"), the accuracy log, and the offset just past the description
529
+ # @raise [Error] if the description is truncated or does not add up
530
+ def read_fse_counts(src, pos, stop, max_symbol, max_log)
531
+ bitpos = 0
532
+ read_bits = lambda do |n|
533
+ byte = pos + (bitpos >> 3)
534
+ window = 0
535
+ 4.times { |i| window |= (src.getbyte(byte + i) || 0) << (8 * i) if byte + i < stop }
536
+ (window >> (bitpos & 7)) & ((1 << n) - 1)
537
+ end
538
+
539
+ log = read_bits.call(4) + 5
540
+ bitpos = 4
541
+ raise Error, "Zstandard FSE table has accuracy log #{log}, the largest allowed here is #{max_log}" if log > max_log
542
+ remaining = (1 << log) + 1
543
+ threshold = 1 << log
544
+ nbits = log + 1
545
+ counts = []
546
+ previous_zero = false
547
+
548
+ while remaining > 1
549
+ if previous_zero
550
+ repeat = read_bits.call(2)
551
+ bitpos += 2
552
+ zeros = repeat
553
+ while repeat == 3
554
+ raise Error, "Zstandard FSE table has more zero counts than symbols" if counts.size + zeros > max_symbol
555
+ repeat = read_bits.call(2)
556
+ bitpos += 2
557
+ zeros += repeat
558
+ end
559
+ raise Error, "Zstandard FSE table has more zero counts than symbols" if counts.size + zeros > max_symbol
560
+ zeros.times { counts << 0 }
561
+ end
562
+ raise Error, "Zstandard FSE table has counts for more than #{max_symbol + 1} symbols" if counts.size > max_symbol
563
+
564
+ max = 2 * threshold - 1 - remaining
565
+ low = read_bits.call(nbits - 1)
566
+ if low < max
567
+ count = low
568
+ bitpos += nbits - 1
569
+ else
570
+ count = read_bits.call(nbits)
571
+ count -= max if count >= threshold
572
+ bitpos += nbits
573
+ end
574
+ count -= 1
575
+ remaining -= count.abs
576
+ # Otherwise the loop below halves threshold down to 0 and spins forever
577
+ raise Error, "Zstandard FSE table counts add up to more than #{1 << log}" if remaining < 1
578
+ counts << count
579
+ previous_zero = count == 0
580
+ while remaining < threshold
581
+ nbits -= 1
582
+ threshold >>= 1
583
+ end
584
+ end
585
+
586
+ used = (bitpos + 7) >> 3
587
+ raise Error, "Zstandard FSE table description runs past its end" if pos + used > stop
588
+ [counts, log, pos + used]
589
+ end
590
+
591
+ # Spreads normalized counts over a decoding table (RFC 8878 section 4.1.1)
592
+ #
593
+ # @param counts [Array<Integer>] normalized count per symbol, -1 for "less than one"
594
+ # @param log [Integer] accuracy log; the counts' magnitudes add up to 2**log
595
+ # @return [FSETable]
596
+ def build_fse_table(counts, log)
597
+ size = 1 << log
598
+ symbols = Array.new(size)
599
+ high = size - 1
600
+ counts.each_with_index do |count, symbol|
601
+ next unless count == -1
602
+ symbols[high] = symbol
603
+ high -= 1
604
+ end
605
+
606
+ step = (size >> 1) + (size >> 3) + 3
607
+ mask = size - 1
608
+ position = 0
609
+ counts.each_with_index do |count, symbol|
610
+ count.times do
611
+ symbols[position] = symbol
612
+ position = (position + step) & mask
613
+ position = (position + step) & mask while position > high
614
+ end
615
+ end
616
+ raise Error, "Zstandard FSE table counts do not fill the table" unless position == 0
617
+
618
+ next_state = counts.map { |count| (count == -1) ? 1 : count }
619
+ nbits = Array.new(size)
620
+ base = Array.new(size)
621
+ size.times do |state|
622
+ symbol = symbols[state]
623
+ n = next_state[symbol]
624
+ next_state[symbol] += 1
625
+ nbits[state] = log - (n.bit_length - 1)
626
+ base[state] = (n << nbits[state]) - size
627
+ end
628
+ FSETable.new(log, symbols, nbits, base)
629
+ end
630
+
631
+ # Reads a Huffman tree description (RFC 8878 section 4.2.1)
632
+ #
633
+ # @param src [String] binary input
634
+ # @param pos [Integer] offset of the description
635
+ # @param stop [Integer] offset it may not reach past
636
+ # @return [Array(HuffmanTable, Integer)] the table, and the offset just past the description
637
+ def read_huffman_table(src, pos, stop)
638
+ raise Error, "Zstandard Huffman tree description is missing" if pos >= stop
639
+ header = src.getbyte(pos)
640
+ pos += 1
641
+ if header >= 128
642
+ count = header - 127
643
+ size = (count + 1) / 2
644
+ raise Error, "Zstandard Huffman weights run past the end of the literals" if pos + size > stop
645
+ weights = Array.new(count) do |i|
646
+ byte = src.getbyte(pos + i / 2)
647
+ i.even? ? byte >> 4 : byte & 15
648
+ end
649
+ pos += size
650
+ else
651
+ raise Error, "Zstandard Huffman weights have a compressed size of 0" if header == 0
652
+ raise Error, "Zstandard Huffman weights run past the end of the literals" if pos + header > stop
653
+ weights = decode_huffman_weights(src, pos, pos + header)
654
+ pos += header
655
+ end
656
+ [build_huffman_table(weights), pos]
657
+ end
658
+
659
+ # Decodes FSE-compressed Huffman weights: two states take turns over one bitstream until
660
+ # it runs out
661
+ #
662
+ # @param src [String] binary input
663
+ # @param pos [Integer] offset of the compressed weights
664
+ # @param stop [Integer] offset just past them
665
+ # @return [Array<Integer>] the weights
666
+ def decode_huffman_weights(src, pos, stop)
667
+ counts, log, pos = read_fse_counts(src, pos, stop, 15, 6)
668
+ table = build_fse_table(counts, log)
669
+ bits = BackwardBits.new(src, pos, stop)
670
+ states = [bits.read(log), bits.read(log)]
671
+ weights = []
672
+ turn = 0
673
+ while true
674
+ raise Error, "Zstandard Huffman weights describe more than 255 symbols" if weights.size >= 255
675
+ state = states[turn]
676
+ weights << table.symbols[state]
677
+ states[turn] = table.base[state] + bits.read(table.nbits[state])
678
+ if bits.overflowed?
679
+ raise Error, "Zstandard Huffman weights describe more than 255 symbols" if weights.size >= 255
680
+ weights << table.symbols[states[1 - turn]]
681
+ break
682
+ end
683
+ turn = 1 - turn
684
+ end
685
+ weights
686
+ end
687
+
688
+ # Builds the decoding table from the weights of all symbols but the last, whose weight is
689
+ # implied by the others
690
+ #
691
+ # @param weights [Array<Integer>] weight per symbol, 0 for unused symbols
692
+ # @return [HuffmanTable]
693
+ def build_huffman_table(weights)
694
+ total = weights.sum { |w| (w > 0) ? 1 << (w - 1) : 0 }
695
+ raise Error, "Zstandard Huffman tree has no weights" if total == 0
696
+ max_bits = total.bit_length
697
+ raise Error, "Zstandard Huffman codes would be #{max_bits} bits long, the most allowed is 11" if max_bits > 11
698
+ rest = (1 << max_bits) - total
699
+ raise Error, "Zstandard Huffman weights do not form a complete tree" unless rest & (rest - 1) == 0
700
+ weights += [rest.bit_length]
701
+
702
+ size = 1 << max_bits
703
+ symbols = Array.new(size)
704
+ lengths = Array.new(size)
705
+ position = 0
706
+ 1.upto(max_bits) do |w|
707
+ span = 1 << (w - 1)
708
+ length = max_bits + 1 - w
709
+ weights.each_with_index do |weight, symbol|
710
+ next unless weight == w
711
+ symbols.fill(symbol, position, span)
712
+ lengths.fill(length, position, span)
713
+ position += span
714
+ end
715
+ end
716
+ HuffmanTable.new(max_bits, symbols, lengths)
717
+ end
718
+
719
+ # @param table [HuffmanTable]
720
+ # @param src [String] binary input
721
+ # @param pos [Integer] offset of the stream(s), after the tree description
722
+ # @param stop [Integer] offset just past the last stream
723
+ # @param size [Integer] number of literals
724
+ # @param streams [Integer] 1, or 4 with a jump table in front
725
+ # @return [String] the literals
726
+ def decode_huffman_literals(table, src, pos, stop, size, streams)
727
+ return decode_huffman_stream(table, src, pos, stop, size) if streams == 1
728
+
729
+ raise Error, "Zstandard literals jump table is truncated" if pos + 6 > stop
730
+ sizes = Array.new(3) { |i| src.getbyte(pos + 2 * i) | (src.getbyte(pos + 2 * i + 1) << 8) }
731
+ pos += 6
732
+ sizes << stop - pos - sizes.sum
733
+ raise Error, "Zstandard literals jump table points past the end of the literals" if sizes.last < 1
734
+ segment = (size + 3) / 4
735
+ raise Error, "Zstandard block has too few literals (#{size}) for 4 streams" if size < 3 * segment
736
+ out = String.new(capacity: size, encoding: BINARY)
737
+ sizes.each_with_index do |stream_size, i|
738
+ count = (i < 3) ? segment : size - 3 * segment
739
+ out << decode_huffman_stream(table, src, pos, pos + stream_size, count)
740
+ pos += stream_size
741
+ end
742
+ out
743
+ end
744
+
745
+ # @param table [HuffmanTable]
746
+ # @param src [String] binary input
747
+ # @param pos [Integer] offset of the stream
748
+ # @param stop [Integer] offset just past it
749
+ # @param count [Integer] number of literals it holds
750
+ # @return [String] the literals
751
+ def decode_huffman_stream(table, src, pos, stop, count)
752
+ bits = BackwardBits.new(src, pos, stop)
753
+ max_bits = table.max_bits
754
+ symbols = table.symbols
755
+ lengths = table.lengths
756
+ decoded = Array.new(count) do
757
+ index = bits.peek(max_bits)
758
+ bits.skip(lengths[index])
759
+ symbols[index]
760
+ end
761
+ raise Error, "Zstandard Huffman stream is too short for #{count} literals" if bits.overflowed?
762
+ raise Error, "Zstandard Huffman stream has bits left after #{count} literals" unless bits.finished?
763
+ decoded.pack("C*")
764
+ end
765
+
766
+ # Tables for the predefined distributions, built once
767
+ LL_PREDEFINED = Ractor.make_shareable(build_fse_table(*LL_DEFAULT))
768
+ # Predefined match length table
769
+ ML_PREDEFINED = Ractor.make_shareable(build_fse_table(*ML_DEFAULT))
770
+ # Predefined offset code table
771
+ OF_PREDEFINED = Ractor.make_shareable(build_fse_table(*OF_DEFAULT))
772
+ end
773
+ end
774
+ end
@@ -42,7 +42,8 @@ module Herringbone
42
42
  # Dispatches page (de)compression by Parquet codec id. Snappy and LZ4 are pure Ruby and GZIP
43
43
  # uses zlib, so those always work. LZO is pure Ruby too, but can only be read. ZSTD and Brotli
44
44
  # come from optional gems (zstd-ruby, brotli), which the application requires; if they are
45
- # not loaded, MissingCodecError says what to add.
45
+ # not loaded, MissingCodecError says what to add. ZSTD is the exception when reading: without
46
+ # zstd-ruby, pages are decompressed by Codecs::Zstd, in pure Ruby.
46
47
  # (Snappy also uses the optional snappy gem when it is loaded, see Codecs::Snappy.)
47
48
  #
48
49
  # @api private
@@ -156,7 +157,7 @@ module Herringbone
156
157
  when Format::Codec::LZ4_RAW then Codecs::LZ4.decompress_block(data, uncompressed_size)
157
158
  when Format::Codec::LZ4 then Codecs::LZ4.decompress_hadoop(data, uncompressed_size)
158
159
  when Format::Codec::LZO then Codecs::LZO.decompress_hadoop(data, uncompressed_size)
159
- when Format::Codec::ZSTD then library(codec).decompress(data)
160
+ when Format::Codec::ZSTD then unzstd(data, uncompressed_size)
160
161
  when Format::Codec::BROTLI then library(codec).inflate(data)
161
162
  else
162
163
  raise UnsupportedError, "Unsupported compression codec #{Format::Codec::NAMES[codec] || codec}"
@@ -223,6 +224,15 @@ module Herringbone
223
224
  stream.finish
224
225
  end
225
226
 
227
+ # @param data [String] one or more Zstandard frames
228
+ # @param uncompressed_size [Integer] expected decompressed size in bytes
229
+ # @return [String] decompressed bytes, from zstd-ruby when it is loaded, else pure Ruby
230
+ # @raise [Codecs::Zstd::Error] on corrupt data, when decompressed in pure Ruby
231
+ def unzstd(data, uncompressed_size)
232
+ zstd = loaded_library(:Zstd)
233
+ zstd ? zstd.decompress(data) : Codecs::Zstd.decompress(data, uncompressed_size)
234
+ end
235
+
226
236
  # Codec output relabelled as binary without copying it, unless it is frozen
227
237
  #
228
238
  # @param str [String] bytes
@@ -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.8.0"
5
+ VERSION = "0.9.0"
6
6
  end
data/lib/herringbone.rb CHANGED
@@ -110,6 +110,7 @@ module Herringbone
110
110
  autoload :LZ4, "#{LIB}/codecs/lz4"
111
111
  autoload :LZO, "#{LIB}/codecs/lzo"
112
112
  autoload :Snappy, "#{LIB}/codecs/snappy"
113
+ autoload :Zstd, "#{LIB}/codecs/zstd"
113
114
  end
114
115
 
115
116
  module Encodings
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: herringbone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
@@ -45,6 +45,7 @@ files:
45
45
  - lib/herringbone/codecs/lz4.rb
46
46
  - lib/herringbone/codecs/lzo.rb
47
47
  - lib/herringbone/codecs/snappy.rb
48
+ - lib/herringbone/codecs/zstd.rb
48
49
  - lib/herringbone/combiner.rb
49
50
  - lib/herringbone/compression.rb
50
51
  - lib/herringbone/encodings/delta.rb