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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7282589689a627d93346959e47ca2c5dcd7f37c496e61ab0e58e5bd794082010
4
- data.tar.gz: 3b9b78d8fbb79c59b2603268955b9a92ee9d5046495b778ae1b1ec9f1b60fe51
3
+ metadata.gz: 96d98ee24100f5b45967dba603ef4822d707cb9689079bafe6f5b971b59b3cfa
4
+ data.tar.gz: a156de8ff6dc031773c02bd0e58cec02b1726048617afdf40f96190638896934
5
5
  SHA512:
6
- metadata.gz: 5c9ccbbc2d410b760251c708499720b4c0ee598fc1b9307d637d42b5cd88f1744f4123fd12b18c05561b7b2eb396f22a8054435e6a19af7ef9cbd181a7e23ca7
7
- data.tar.gz: 419850a99a4fccbedc21cba3df34b89ebfaf0a1949bcfe7cf93544b55162ff118dcfa71053a64833d608f95a514ec8054a6b23f2df7d464426302ebea1994b04
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