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 +4 -4
- data/CHANGELOG.md +5 -0
- data/MANUAL.md +1 -1
- data/README.md +4 -4
- data/lib/herringbone/codecs/zstd.rb +774 -0
- data/lib/herringbone/compression.rb +12 -2
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c59941b3e4ed776a03d3596bf8b9288dd852b89f49934dfb1110f6b29d527ab8
|
|
4
|
+
data.tar.gz: 13ef32e91a12a11d249be06834f0c33dfb70c9ebb7746d29d28844f9af180fa5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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`
|
|
25
|
-
filters) or `numo-narray-alt` (`read(as: :numo)`). Herringbone uses
|
|
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
|
|
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
|
data/lib/herringbone/version.rb
CHANGED
data/lib/herringbone.rb
CHANGED
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.
|
|
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
|