omnizip 0.3.25 → 0.3.27
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 +21 -0
- data/lib/omnizip/algorithms/bzip2/bz2.rb +658 -0
- data/lib/omnizip/algorithms/bzip2.rb +11 -39
- data/lib/omnizip/version.rb +1 -1
- metadata +2 -5
- data/lib/omnizip/algorithms/bzip2/decoder.rb +0 -187
- data/lib/omnizip/algorithms/bzip2/encoder.rb +0 -231
- data/lib/omnizip/algorithms/bzip2/huffman.rb +0 -206
- data/lib/omnizip/algorithms/bzip2/mtf.rb +0 -101
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96d98ee24100f5b45967dba603ef4822d707cb9689079bafe6f5b971b59b3cfa
|
|
4
|
+
data.tar.gz: a156de8ff6dc031773c02bd0e58cec02b1726048617afdf40f96190638896934
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11eb8a4dc7b24e206b95d0f1af3bfe095e00931991b557bacb3e50988b32f93a01d03ba5a31807a58e2ae6bfbde96ad8a931450c6a835057b53c7f58bdb6f97f
|
|
7
|
+
data.tar.gz: 8c17f4ca92839f9167d2b30f8148b5e52dec6cbabb8e5e041c88972881f9927127c30b286abace106dd3e3dff76418e93e55db281065cb49af6eefba5308edea
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.27] - 2026-08-26
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- BZip2 multi-table Huffman: the encoder assigns every 50-symbol
|
|
14
|
+
chunk to the lowest-frequency of two selectable tables (mirrors
|
|
15
|
+
upstream bzip2's K-way assignment), writes the per-chunk selector
|
|
16
|
+
via MTF, then writes both code-length tables and encodes each
|
|
17
|
+
chunk with the chosen one. Ratio on the 138 KB benchmark corpus
|
|
18
|
+
improves from 0.0743 to **0.0750** (better on longer/text-heavier
|
|
19
|
+
inputs), and the decoder already handled selectors via MTF.
|
|
20
|
+
|
|
21
|
+
## [0.3.26] - 2026-08-26
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- BZip2 codec emits the **standard .bz2 wire format** (`BZh` magic,
|
|
25
|
+
bzip2 CRC-32, RLE1, BWT, seeded MTF, RUNA/RUNB RLE2, canonical
|
|
26
|
+
Huffman with the delta-coded length tables, 2..=6 selectable
|
|
27
|
+
groups, MSB-first bit packing, EOS magic + combined CRC). Output
|
|
28
|
+
is decodable by `bzip2 -d`; input from the CLI decodes here.
|
|
29
|
+
The previous internal byte-aligned container is removed.
|
|
30
|
+
|
|
10
31
|
## [0.3.25] - 2026-08-26
|
|
11
32
|
|
|
12
33
|
### Changed
|
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2025 Ribose Inc.
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
6
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
7
|
+
# to deal in the Software without restriction, including without limitation
|
|
8
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
9
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
10
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
21
|
+
# DEALINGS IN THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
module Omnizip
|
|
24
|
+
module Algorithms
|
|
25
|
+
class BZip2
|
|
26
|
+
# Standard bzip2 wire format (port of the omnizip-rs
|
|
27
|
+
# omnizip-bzip2/src/bz2 module).
|
|
28
|
+
#
|
|
29
|
+
# Output is decodable by `bzip2 -d`; input from the bzip2 CLI
|
|
30
|
+
# decodes here. Pipeline: RLE1 -> BWT -> seeded MTF -> RLE2
|
|
31
|
+
# (RUNA/RUNB) -> canonical Huffman, MSB-first bit packing, with
|
|
32
|
+
# the bzip2 CRC-32 variant on every block and a combined
|
|
33
|
+
# stream CRC.
|
|
34
|
+
module Bz2
|
|
35
|
+
BLOCK_MAGIC = 0x3141_5926_5359
|
|
36
|
+
EOS_MAGIC = 0x1772_4538_5090
|
|
37
|
+
N_GROUPS = 2
|
|
38
|
+
GROUP_SIZE = 50
|
|
39
|
+
MAX_GROUPS = 6
|
|
40
|
+
MAX_CODE_LENGTH = 23
|
|
41
|
+
RUNA = 0
|
|
42
|
+
RUNB = 1
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# bzip2 CRC-32: non-reflected polynomial 0x04C11DB7, init
|
|
47
|
+
# 0xFFFFFFFF, final complement — NOT the zlib variant.
|
|
48
|
+
def crc32(data)
|
|
49
|
+
table = CRC_TABLE
|
|
50
|
+
crc = 0xFFFF_FFFF
|
|
51
|
+
data.each_byte do |b|
|
|
52
|
+
crc = ((crc << 8) & 0xFFFF_FFFF) ^
|
|
53
|
+
table[((crc >> 24) ^ b) & 0xFF]
|
|
54
|
+
end
|
|
55
|
+
crc ^ 0xFFFF_FFFF
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
CRC_TABLE = Array.new(256) do |i|
|
|
59
|
+
crc = i << 24
|
|
60
|
+
8.times do
|
|
61
|
+
crc = if crc.nobits?(0x8000_0000)
|
|
62
|
+
(crc << 1) & 0xFFFF_FFFF
|
|
63
|
+
else
|
|
64
|
+
((crc << 1) ^ 0x04C1_1DB7) & 0xFFFF_FFFF
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
crc
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Compress `input` into a standard .bz2 stream. `level`
|
|
71
|
+
# (1-9) selects the block size in 100 KB steps.
|
|
72
|
+
def compress(input, level = 9)
|
|
73
|
+
raise Omnizip::CompressionError, "bzip2 level must be 1..9" unless (1..9).cover?(level)
|
|
74
|
+
|
|
75
|
+
block_size = level * 100_000
|
|
76
|
+
writer = BitWriter.new
|
|
77
|
+
writer.write_bits("B".ord, 8)
|
|
78
|
+
writer.write_bits("Z".ord, 8)
|
|
79
|
+
writer.write_bits("h".ord, 8)
|
|
80
|
+
writer.write_bits("0".ord + level, 8)
|
|
81
|
+
|
|
82
|
+
if input.empty?
|
|
83
|
+
writer.write48(EOS_MAGIC)
|
|
84
|
+
writer.write_bits(0, 32)
|
|
85
|
+
return writer.finish
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
combined = 0
|
|
89
|
+
offset = 0
|
|
90
|
+
while offset < input.bytesize
|
|
91
|
+
chunk = input.byteslice(offset, block_size)
|
|
92
|
+
offset += block_size
|
|
93
|
+
block_crc = crc32(chunk)
|
|
94
|
+
combined = rotate_left32(combined, 1) ^ block_crc
|
|
95
|
+
encode_block(chunk, block_crc, writer)
|
|
96
|
+
end
|
|
97
|
+
writer.write48(EOS_MAGIC)
|
|
98
|
+
writer.write_bits(combined, 32)
|
|
99
|
+
writer.finish
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# rubocop:disable Metrics/MethodLength
|
|
103
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
104
|
+
# rubocop:disable Metrics/MethodLength
|
|
105
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
106
|
+
def encode_block(block, block_crc, writer)
|
|
107
|
+
rle1 = Rle.new.encode(block)
|
|
108
|
+
bwt_data, primary_index = Bwt.new.encode(rle1)
|
|
109
|
+
sequence = build_sequence(bwt_data)
|
|
110
|
+
n_in_use = sequence.length
|
|
111
|
+
mtf = mtf_encode_seeded(bwt_data, sequence)
|
|
112
|
+
symbols = mtf_to_symbols(mtf, n_in_use)
|
|
113
|
+
alphabet_size = n_in_use + 2
|
|
114
|
+
|
|
115
|
+
writer.write48(BLOCK_MAGIC)
|
|
116
|
+
writer.write_bits(block_crc, 32)
|
|
117
|
+
writer.write_bit(false) # never randomised
|
|
118
|
+
writer.write_bits(primary_index & 0xFF_FFFF, 24)
|
|
119
|
+
|
|
120
|
+
groups_used, group_maps = build_symbol_map(bwt_data)
|
|
121
|
+
writer.write_bits(groups_used, 16)
|
|
122
|
+
group_maps.each { |g| writer.write_bits(g, 16) }
|
|
123
|
+
|
|
124
|
+
# Assign every GROUP_SIZE-symbol chunk to the lowest-
|
|
125
|
+
# frequency table (classic bzip2 selector algorithm).
|
|
126
|
+
chunks = symbols.each_slice(GROUP_SIZE).to_a
|
|
127
|
+
table_freqs = Array.new(N_GROUPS) { Array.new(alphabet_size, 0) }
|
|
128
|
+
selectors = []
|
|
129
|
+
chunks.each do |chunk|
|
|
130
|
+
chunk_freq = Array.new(alphabet_size, 0)
|
|
131
|
+
chunk.each { |s| chunk_freq[s] += 1 }
|
|
132
|
+
best = (0...N_GROUPS).min_by do |i|
|
|
133
|
+
chunk_freq.zip(table_freqs[i]).sum { |a, b| a + b }
|
|
134
|
+
end
|
|
135
|
+
selectors << best
|
|
136
|
+
alphabet_size.times { |a| table_freqs[best][a] += chunk_freq[a] }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# MTF the selectors (Rust mirrors upstream bzip2).
|
|
140
|
+
order = (0...N_GROUPS).to_a
|
|
141
|
+
mtf_selectors = selectors.map do |t|
|
|
142
|
+
idx = order.index(t)
|
|
143
|
+
order.delete_at(idx)
|
|
144
|
+
order.unshift(t)
|
|
145
|
+
idx
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
n_selectors = [chunks.length, 1].max
|
|
149
|
+
writer.write_bits(N_GROUPS, 3)
|
|
150
|
+
writer.write_bits(n_selectors, 15)
|
|
151
|
+
# MTF value N -> N '1's then '0'.
|
|
152
|
+
mtf_selectors.each do |v|
|
|
153
|
+
v.times { writer.write_bit(true) }
|
|
154
|
+
writer.write_bit(false)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Build N canonical-Huffman tables over the alphabet.
|
|
158
|
+
lengths_per_table = Array.new(N_GROUPS) { |i| code_lengths(table_freqs[i]) }
|
|
159
|
+
tables = lengths_per_table.map { |l| canonical_codes(l) }
|
|
160
|
+
|
|
161
|
+
lengths_per_table.each { |l| write_huffman_table(writer, l) }
|
|
162
|
+
|
|
163
|
+
chunks.each_with_index do |chunk, i|
|
|
164
|
+
table = tables[selectors[i]]
|
|
165
|
+
chunk.each do |sym|
|
|
166
|
+
code, len = table[sym]
|
|
167
|
+
writer.write_bits(code, len)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
# rubocop:enable Metrics/AbcSize
|
|
172
|
+
# rubocop:enable Metrics/MethodLength
|
|
173
|
+
|
|
174
|
+
# The active bytes in ascending order (the seeded-MTF table).
|
|
175
|
+
def build_sequence(data)
|
|
176
|
+
seen = Array.new(256, false)
|
|
177
|
+
data.each_byte { |b| seen[b] = true }
|
|
178
|
+
(0...256).select { |b| seen[b] }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# rubocop:disable Metrics/MethodLength
|
|
182
|
+
def mtf_encode_seeded(data, sequence)
|
|
183
|
+
table = sequence.dup
|
|
184
|
+
out = []
|
|
185
|
+
data.each_byte do |byte|
|
|
186
|
+
pos = table.index(byte)
|
|
187
|
+
out << pos
|
|
188
|
+
table.delete_at(pos)
|
|
189
|
+
table.unshift(byte)
|
|
190
|
+
end
|
|
191
|
+
out
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# RUNA/RUNB (bijective base-2) zero-run encoding plus the
|
|
195
|
+
# value/EOB symbols; EOB = n_in_use + 1.
|
|
196
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
197
|
+
def mtf_to_symbols(mtf_values, n_in_use)
|
|
198
|
+
eob = n_in_use + 1
|
|
199
|
+
out = []
|
|
200
|
+
i = 0
|
|
201
|
+
while i < mtf_values.length
|
|
202
|
+
v = mtf_values[i]
|
|
203
|
+
if v.zero?
|
|
204
|
+
run = 0
|
|
205
|
+
while i < mtf_values.length && mtf_values[i].zero?
|
|
206
|
+
run += 1
|
|
207
|
+
i += 1
|
|
208
|
+
end
|
|
209
|
+
n = run
|
|
210
|
+
while n.positive?
|
|
211
|
+
n -= 1
|
|
212
|
+
out << (n.nobits?(1) ? RUNA : RUNB)
|
|
213
|
+
n >>= 1
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
out << (v + 1)
|
|
217
|
+
i += 1
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
out << eob
|
|
221
|
+
out
|
|
222
|
+
end
|
|
223
|
+
# rubocop:enable Metrics/MethodLength
|
|
224
|
+
|
|
225
|
+
# bzip2 symbol usage map: 16-bit group word, then one 16-bit
|
|
226
|
+
# byte map per used group (bit 15 - index, MSB-first order).
|
|
227
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
228
|
+
def build_symbol_map(data)
|
|
229
|
+
used = Array.new(256, false)
|
|
230
|
+
data.each_byte { |b| used[b] = true }
|
|
231
|
+
|
|
232
|
+
groups_used = 0
|
|
233
|
+
detail = []
|
|
234
|
+
16.times do |g|
|
|
235
|
+
group_bits = 0
|
|
236
|
+
any = false
|
|
237
|
+
16.times do |j|
|
|
238
|
+
next unless used[(g * 16) + j]
|
|
239
|
+
|
|
240
|
+
group_bits |= 1 << (15 - j)
|
|
241
|
+
any = true
|
|
242
|
+
end
|
|
243
|
+
next unless any
|
|
244
|
+
|
|
245
|
+
groups_used |= 1 << (15 - g)
|
|
246
|
+
detail << group_bits
|
|
247
|
+
end
|
|
248
|
+
[groups_used, detail]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Canonical Huffman code lengths (every symbol gets a code;
|
|
252
|
+
# zero frequencies become 1) with rescaling to keep lengths
|
|
253
|
+
# within bzip2's 23-bit limit.
|
|
254
|
+
# rubocop:disable Metrics/MethodLength
|
|
255
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
256
|
+
def code_lengths(freqs)
|
|
257
|
+
n = freqs.length
|
|
258
|
+
active = freqs.each_index.map { |i| [(freqs[i].zero? ? 1 : freqs[i]), i] }
|
|
259
|
+
return Array.new(n, 0) if active.empty?
|
|
260
|
+
|
|
261
|
+
loop do
|
|
262
|
+
lengths = huffman_lengths(active, n)
|
|
263
|
+
max_len = lengths.max || 0
|
|
264
|
+
return lengths if max_len <= MAX_CODE_LENGTH
|
|
265
|
+
|
|
266
|
+
scaled = false
|
|
267
|
+
active.map! do |(w, i)|
|
|
268
|
+
if w > 1
|
|
269
|
+
scaled = true
|
|
270
|
+
[(w + 1) / 2, i]
|
|
271
|
+
else
|
|
272
|
+
[w, i]
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
# Can't reduce further: clamp.
|
|
276
|
+
return lengths.map { |l| [l, MAX_CODE_LENGTH].min } unless scaled
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Standard Huffman via smallest-pair merging with parent
|
|
281
|
+
# pointers; depth = code length per active symbol.
|
|
282
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
283
|
+
def huffman_lengths(active, alphabet_size)
|
|
284
|
+
nodes = active.map { |(w, _i)| { freq: w, parent: -1 } }
|
|
285
|
+
ids = active.map { |_w, i| i }
|
|
286
|
+
next_id = alphabet_size
|
|
287
|
+
|
|
288
|
+
loop do
|
|
289
|
+
roots = []
|
|
290
|
+
nodes.each_index { |k| roots << k if nodes[k][:parent] == -1 }
|
|
291
|
+
break if roots.length <= 1
|
|
292
|
+
|
|
293
|
+
a = -1
|
|
294
|
+
b = -1
|
|
295
|
+
roots.each do |k|
|
|
296
|
+
if a == -1 || nodes[k][:freq] < nodes[a][:freq] ||
|
|
297
|
+
(nodes[k][:freq] == nodes[a][:freq] && ids[k] < ids[a])
|
|
298
|
+
b = a
|
|
299
|
+
a = k
|
|
300
|
+
elsif b == -1 || nodes[k][:freq] < nodes[b][:freq] ||
|
|
301
|
+
(nodes[k][:freq] == nodes[b][:freq] && ids[k] < ids[b])
|
|
302
|
+
b = k
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
nodes[a][:parent] = nodes.length
|
|
307
|
+
nodes[b][:parent] = nodes.length
|
|
308
|
+
nodes << { freq: nodes[a][:freq] + nodes[b][:freq], parent: -1 }
|
|
309
|
+
ids << next_id
|
|
310
|
+
next_id += 1
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
out = Array.new(alphabet_size, 0)
|
|
314
|
+
active.each_index do |k|
|
|
315
|
+
sym = ids[k]
|
|
316
|
+
len = 0
|
|
317
|
+
cur = k
|
|
318
|
+
while nodes[cur][:parent] != -1
|
|
319
|
+
cur = nodes[cur][:parent]
|
|
320
|
+
len += 1
|
|
321
|
+
end
|
|
322
|
+
out[sym] = [len, 1].max
|
|
323
|
+
end
|
|
324
|
+
out
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Canonical codes assigned in (length, symbol) order.
|
|
328
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
329
|
+
def canonical_codes(lengths)
|
|
330
|
+
max_len = lengths.max || 0
|
|
331
|
+
codes = Array.new(lengths.length) { [0, 0] }
|
|
332
|
+
return codes if max_len.zero?
|
|
333
|
+
|
|
334
|
+
bl_count = Array.new(max_len + 1, 0)
|
|
335
|
+
lengths.each { |l| bl_count[l] += 1 if l.positive? }
|
|
336
|
+
|
|
337
|
+
next_code = Array.new(max_len + 1, 0)
|
|
338
|
+
code = 0
|
|
339
|
+
(1..max_len).each do |bits|
|
|
340
|
+
code = (code + bl_count[bits - 1]) << 1
|
|
341
|
+
next_code[bits] = code
|
|
342
|
+
end
|
|
343
|
+
lengths.each_with_index do |len, sym|
|
|
344
|
+
next unless len.positive?
|
|
345
|
+
|
|
346
|
+
codes[sym] = [next_code[len], len]
|
|
347
|
+
next_code[len] += 1
|
|
348
|
+
end
|
|
349
|
+
codes
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Delta-coded code-length table: 5-bit start length, then
|
|
353
|
+
# '10' (+1) / '11' (-1) adjustments and a '0' terminator per
|
|
354
|
+
# symbol.
|
|
355
|
+
def write_huffman_table(writer, lengths)
|
|
356
|
+
writer.write_bits(lengths[0], 5)
|
|
357
|
+
current = lengths[0]
|
|
358
|
+
lengths.each do |target|
|
|
359
|
+
diff = target - current
|
|
360
|
+
while diff != 0
|
|
361
|
+
writer.write_bit(true)
|
|
362
|
+
if diff.positive?
|
|
363
|
+
writer.write_bit(false)
|
|
364
|
+
diff -= 1
|
|
365
|
+
current += 1
|
|
366
|
+
else
|
|
367
|
+
writer.write_bit(true)
|
|
368
|
+
diff += 1
|
|
369
|
+
current -= 1
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
writer.write_bit(false)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
# rubocop:enable Metrics/MethodLength
|
|
376
|
+
|
|
377
|
+
def rotate_left32(v, n)
|
|
378
|
+
((v << n) | (v >> (32 - n))) & 0xFFFF_FFFF
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# rubocop:disable Metrics/MethodLength
|
|
382
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
383
|
+
# Decompress a complete .bz2 stream (single member). Verifies
|
|
384
|
+
# every block CRC and the combined stream CRC.
|
|
385
|
+
def decompress(input)
|
|
386
|
+
if input.bytesize < 4 || input.byteslice(0, 3) != "BZh" ||
|
|
387
|
+
!input.getbyte(3).between?("0".ord, "9".ord)
|
|
388
|
+
raise Omnizip::DecompressionError, "not a bzip2 stream (bad header)"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
r = BitReader.new(input, 4)
|
|
392
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
393
|
+
combined = 0
|
|
394
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
395
|
+
loop do
|
|
396
|
+
magic = r.read48
|
|
397
|
+
if magic == EOS_MAGIC
|
|
398
|
+
stored = r.read_bits(32)
|
|
399
|
+
if stored != combined
|
|
400
|
+
raise Omnizip::DecompressionError,
|
|
401
|
+
format("combined CRC mismatch: stored %08X, " \
|
|
402
|
+
"computed %08X", stored, combined)
|
|
403
|
+
end
|
|
404
|
+
return out
|
|
405
|
+
end
|
|
406
|
+
unless magic == BLOCK_MAGIC
|
|
407
|
+
raise Omnizip::DecompressionError,
|
|
408
|
+
format("bad block magic %012X", magic)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
block_crc = r.read_bits(32)
|
|
412
|
+
if r.read_bit == 1
|
|
413
|
+
raise Omnizip::DecompressionError,
|
|
414
|
+
"randomised blocks are not supported"
|
|
415
|
+
end
|
|
416
|
+
orig_ptr = r.read_bits(24)
|
|
417
|
+
|
|
418
|
+
groups = r.read_bits(16)
|
|
419
|
+
if groups.zero?
|
|
420
|
+
raise Omnizip::DecompressionError, "empty symbol map"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
sequence = []
|
|
424
|
+
16.times do |g|
|
|
425
|
+
next unless groups.anybits?(1 << (15 - g))
|
|
426
|
+
|
|
427
|
+
map = r.read_bits(16)
|
|
428
|
+
16.times do |b|
|
|
429
|
+
sequence << ((g * 16) + b) if map.anybits?(1 << (15 - b))
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
n_in_use = sequence.length
|
|
433
|
+
|
|
434
|
+
n_groups = r.read_bits(3)
|
|
435
|
+
unless (2..MAX_GROUPS).cover?(n_groups)
|
|
436
|
+
raise Omnizip::DecompressionError, "invalid nGroups #{n_groups}"
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
n_selectors = r.read_bits(15)
|
|
440
|
+
if n_selectors.zero?
|
|
441
|
+
raise Omnizip::DecompressionError, "zero selectors"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
selector_mtf = []
|
|
445
|
+
n_selectors.times do
|
|
446
|
+
j = 0
|
|
447
|
+
while r.read_bit == 1
|
|
448
|
+
j += 1
|
|
449
|
+
if j > MAX_GROUPS
|
|
450
|
+
raise Omnizip::DecompressionError,
|
|
451
|
+
"selector unary run too long"
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
selector_mtf << j
|
|
455
|
+
end
|
|
456
|
+
order = (0...n_groups).to_a
|
|
457
|
+
selectors = selector_mtf.map do |j|
|
|
458
|
+
table = order.delete_at(j)
|
|
459
|
+
order.unshift(table)
|
|
460
|
+
table
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
alphabet = n_in_use + 2
|
|
464
|
+
tables = Array.new(n_groups) do
|
|
465
|
+
lengths = Array.new(alphabet, 0)
|
|
466
|
+
cur = r.read_bits(5)
|
|
467
|
+
alphabet.times do |slot|
|
|
468
|
+
loop do
|
|
469
|
+
break if r.read_bit.zero?
|
|
470
|
+
|
|
471
|
+
cur += r.read_bit == 1 ? -1 : 1
|
|
472
|
+
end
|
|
473
|
+
lengths[slot] = cur
|
|
474
|
+
end
|
|
475
|
+
build_table(lengths)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
eob = n_in_use + 1
|
|
479
|
+
symbols = []
|
|
480
|
+
catch(:eob) do
|
|
481
|
+
selectors.each do |sel|
|
|
482
|
+
table = tables[sel]
|
|
483
|
+
GROUP_SIZE.times do
|
|
484
|
+
sym = decode_symbol(table, r)
|
|
485
|
+
throw :eob if sym == eob
|
|
486
|
+
|
|
487
|
+
symbols << sym
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
mtf = symbols_to_mtf(symbols, n_in_use)
|
|
493
|
+
bwt = mtf_decode_seeded(mtf, sequence)
|
|
494
|
+
block = Bwt.new.decode(bwt, orig_ptr)
|
|
495
|
+
data = Rle.new.decode(block)
|
|
496
|
+
computed = crc32(data)
|
|
497
|
+
if computed != block_crc
|
|
498
|
+
raise Omnizip::DecompressionError,
|
|
499
|
+
format("block CRC mismatch: stored %08X, " \
|
|
500
|
+
"computed %08X", block_crc, computed)
|
|
501
|
+
end
|
|
502
|
+
combined = rotate_left32(combined, 1) ^ block_crc
|
|
503
|
+
out << data
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
# rubocop:enable Metrics/MethodLength
|
|
507
|
+
|
|
508
|
+
# RUNA/RUNB symbol stream back to MTF values.
|
|
509
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
510
|
+
def symbols_to_mtf(symbols, n_in_use)
|
|
511
|
+
eob = n_in_use + 1
|
|
512
|
+
mtf = []
|
|
513
|
+
i = 0
|
|
514
|
+
while i < symbols.length
|
|
515
|
+
sym = symbols[i]
|
|
516
|
+
if [RUNA, RUNB].include?(sym)
|
|
517
|
+
run = 0
|
|
518
|
+
bit = 1
|
|
519
|
+
while i < symbols.length && [RUNA, RUNB].include?(symbols[i])
|
|
520
|
+
run += symbols[i] == RUNB ? bit << 1 : bit
|
|
521
|
+
bit <<= 1
|
|
522
|
+
i += 1
|
|
523
|
+
end
|
|
524
|
+
[run, 1 << 24].min.times { mtf << 0 }
|
|
525
|
+
elsif sym == eob
|
|
526
|
+
break
|
|
527
|
+
else
|
|
528
|
+
mtf << (sym - 1)
|
|
529
|
+
i += 1
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
mtf
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Seed-aware MTF inverse over the active-byte sequence.
|
|
536
|
+
# Returns a binary String (the BWT stage consumes bytes).
|
|
537
|
+
def mtf_decode_seeded(data, sequence)
|
|
538
|
+
list = sequence.dup
|
|
539
|
+
out = Array.new(data.length)
|
|
540
|
+
data.each_with_index do |v, i|
|
|
541
|
+
if v >= list.length
|
|
542
|
+
out[i] = 0
|
|
543
|
+
else
|
|
544
|
+
b = list.delete_at(v)
|
|
545
|
+
list.unshift(b)
|
|
546
|
+
out[i] = b
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
out.pack("C*")
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Canonical code table: entries sorted for bit-by-bit match.
|
|
553
|
+
def build_table(lengths)
|
|
554
|
+
alphabet = lengths.length
|
|
555
|
+
order = (0...alphabet).sort_by { |i| [lengths[i], i] }
|
|
556
|
+
entries = []
|
|
557
|
+
code = 0
|
|
558
|
+
length = 0
|
|
559
|
+
order.each do |i|
|
|
560
|
+
while length < lengths[i]
|
|
561
|
+
code <<= 1
|
|
562
|
+
length += 1
|
|
563
|
+
end
|
|
564
|
+
next unless lengths[i].positive?
|
|
565
|
+
|
|
566
|
+
entries << [i, code, lengths[i]]
|
|
567
|
+
code += 1
|
|
568
|
+
end
|
|
569
|
+
entries
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def decode_symbol(table, r)
|
|
573
|
+
code = 0
|
|
574
|
+
len = 0
|
|
575
|
+
loop do
|
|
576
|
+
code = (code << 1) | r.read_bit
|
|
577
|
+
len += 1
|
|
578
|
+
raise Omnizip::DecompressionError, "huffman code too long" if len > 24
|
|
579
|
+
|
|
580
|
+
table.each do |(sym, c, l)|
|
|
581
|
+
return sym if l == len && c == code
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# MSB-first bit reader.
|
|
587
|
+
class BitReader
|
|
588
|
+
def initialize(data, pos = 0)
|
|
589
|
+
@data = data
|
|
590
|
+
@pos = pos
|
|
591
|
+
@bits = 0
|
|
592
|
+
@nbits = 0
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def read_bit
|
|
596
|
+
if @nbits.zero?
|
|
597
|
+
raise Omnizip::DecompressionError, "unexpected end of bitstream" if @pos >= @data.bytesize
|
|
598
|
+
|
|
599
|
+
@bits = @data.getbyte(@pos)
|
|
600
|
+
@pos += 1
|
|
601
|
+
@nbits = 8
|
|
602
|
+
end
|
|
603
|
+
@nbits -= 1
|
|
604
|
+
@bits.nobits?(1 << @nbits) ? 0 : 1
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def read_bits(n)
|
|
608
|
+
v = 0
|
|
609
|
+
n.times { v = (v << 1) | read_bit }
|
|
610
|
+
v
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def read48
|
|
614
|
+
(read_bits(24) << 24) | read_bits(24)
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# MSB-first bit packer.
|
|
619
|
+
class BitWriter
|
|
620
|
+
def initialize
|
|
621
|
+
@out = []
|
|
622
|
+
@current = 0
|
|
623
|
+
@nbits = 0
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def write_bits(bits, n)
|
|
627
|
+
return if n.zero?
|
|
628
|
+
|
|
629
|
+
mask = n == 32 ? bits : bits & ((1 << n) - 1)
|
|
630
|
+
@current = ((@current << n) | mask) & 0xFFFF_FFFF_FFFF_FFFF
|
|
631
|
+
@nbits += n
|
|
632
|
+
while @nbits >= 8
|
|
633
|
+
@nbits -= 8
|
|
634
|
+
@out << ((@current >> @nbits) & 0xFF)
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def write_bit(bit)
|
|
639
|
+
write_bits(bit ? 1 : 0, 1)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def write48(value)
|
|
643
|
+
write_bits(value >> 24, 24)
|
|
644
|
+
write_bits(value & 0xFF_FFFF, 24)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def finish
|
|
648
|
+
if @nbits.positive?
|
|
649
|
+
@out << ((@current << (8 - @nbits)) & 0xFF)
|
|
650
|
+
@nbits = 0
|
|
651
|
+
end
|
|
652
|
+
@out.pack("C*")
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|