omnizip 0.3.25 → 0.3.26

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: 86505c60d041512f5f031bf20d19c4db6654ffe112bd0e885a7eb76129eaf37a
4
+ data.tar.gz: 8ceca40e4225c272bc527f508d231629b3357959e25e5f3b4ad50ba91116171f
5
5
  SHA512:
6
- metadata.gz: 5c9ccbbc2d410b760251c708499720b4c0ee598fc1b9307d637d42b5cd88f1744f4123fd12b18c05561b7b2eb396f22a8054435e6a19af7ef9cbd181a7e23ca7
7
- data.tar.gz: 419850a99a4fccbedc21cba3df34b89ebfaf0a1949bcfe7cf93544b55162ff118dcfa71053a64833d608f95a514ec8054a6b23f2df7d464426302ebea1994b04
6
+ metadata.gz: 51953cf6f14aa679b99fe0abfc9fcc171addd943787ca7dde5b4f70945bf45ecad4dc2d7e72029f89495ba7e5c73ee9f8b72bcd8dcbe681812ef4038b7228328
7
+ data.tar.gz: 73407d59fc5dc1e4c6a8b997724101181c1650b8fff434b73cfa2a8835d643292fb192dcfe8a85baea49114cb5ae5cedd7c32f1e40d5e653b18b9d8f9ebd5a48
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.26] - 2026-08-26
11
+
12
+ ### Changed
13
+ - BZip2 codec emits the **standard .bz2 wire format** (`BZh` magic,
14
+ bzip2 CRC-32, RLE1, BWT, seeded MTF, RUNA/RUNB RLE2, canonical
15
+ Huffman with the delta-coded length tables, 2..=6 selectable
16
+ groups, MSB-first bit packing, EOS magic + combined CRC). Output
17
+ is decodable by `bzip2 -d`; input from the CLI decodes here.
18
+ The previous internal byte-aligned container is removed.
19
+
10
20
  ## [0.3.25] - 2026-08-26
11
21
 
12
22
  ### Changed
@@ -0,0 +1,625 @@
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
+ def encode_block(block, block_crc, writer)
105
+ rle1 = Rle.new.encode(block)
106
+ bwt_data, primary_index = Bwt.new.encode(rle1)
107
+ sequence = build_sequence(bwt_data)
108
+ n_in_use = sequence.length
109
+ mtf = mtf_encode_seeded(bwt_data, sequence)
110
+ symbols = mtf_to_symbols(mtf, n_in_use)
111
+
112
+ writer.write48(BLOCK_MAGIC)
113
+ writer.write_bits(block_crc, 32)
114
+ writer.write_bit(false) # never randomised
115
+ writer.write_bits(primary_index & 0xFF_FFFF, 24)
116
+
117
+ groups_used, group_maps = build_symbol_map(bwt_data)
118
+ writer.write_bits(groups_used, 16)
119
+ group_maps.each { |g| writer.write_bits(g, 16) }
120
+
121
+ n_selectors = [(symbols.length.to_f / GROUP_SIZE).ceil, 1].max
122
+ writer.write_bits(N_GROUPS, 3)
123
+ writer.write_bits(n_selectors, 15)
124
+ # MTF-coded selectors; all are 0 (table 0) — unary '0'.
125
+ n_selectors.times { writer.write_bit(false) }
126
+
127
+ alphabet_size = n_in_use + 2
128
+ freqs = Array.new(alphabet_size, 0)
129
+ symbols.each { |sym| freqs[sym] += 1 }
130
+ lengths = code_lengths(freqs)
131
+ codes = canonical_codes(lengths)
132
+
133
+ N_GROUPS.times { write_huffman_table(writer, lengths) }
134
+ symbols.each do |sym|
135
+ code, len = codes[sym]
136
+ writer.write_bits(code, len)
137
+ end
138
+ end
139
+ # rubocop:enable Metrics/AbcSize
140
+
141
+ # The active bytes in ascending order (the seeded-MTF table).
142
+ def build_sequence(data)
143
+ seen = Array.new(256, false)
144
+ data.each_byte { |b| seen[b] = true }
145
+ (0...256).select { |b| seen[b] }
146
+ end
147
+
148
+ # rubocop:disable Metrics/MethodLength
149
+ def mtf_encode_seeded(data, sequence)
150
+ table = sequence.dup
151
+ out = []
152
+ data.each_byte do |byte|
153
+ pos = table.index(byte)
154
+ out << pos
155
+ table.delete_at(pos)
156
+ table.unshift(byte)
157
+ end
158
+ out
159
+ end
160
+
161
+ # RUNA/RUNB (bijective base-2) zero-run encoding plus the
162
+ # value/EOB symbols; EOB = n_in_use + 1.
163
+ # rubocop:disable-next Metrics/AbcSize
164
+ def mtf_to_symbols(mtf_values, n_in_use)
165
+ eob = n_in_use + 1
166
+ out = []
167
+ i = 0
168
+ while i < mtf_values.length
169
+ v = mtf_values[i]
170
+ if v.zero?
171
+ run = 0
172
+ while i < mtf_values.length && mtf_values[i].zero?
173
+ run += 1
174
+ i += 1
175
+ end
176
+ n = run
177
+ while n.positive?
178
+ n -= 1
179
+ out << (n.nobits?(1) ? RUNA : RUNB)
180
+ n >>= 1
181
+ end
182
+ else
183
+ out << (v + 1)
184
+ i += 1
185
+ end
186
+ end
187
+ out << eob
188
+ out
189
+ end
190
+ # rubocop:enable Metrics/MethodLength
191
+
192
+ # bzip2 symbol usage map: 16-bit group word, then one 16-bit
193
+ # byte map per used group (bit 15 - index, MSB-first order).
194
+ # rubocop:disable-next Metrics/AbcSize
195
+ def build_symbol_map(data)
196
+ used = Array.new(256, false)
197
+ data.each_byte { |b| used[b] = true }
198
+
199
+ groups_used = 0
200
+ detail = []
201
+ 16.times do |g|
202
+ group_bits = 0
203
+ any = false
204
+ 16.times do |j|
205
+ next unless used[(g * 16) + j]
206
+
207
+ group_bits |= 1 << (15 - j)
208
+ any = true
209
+ end
210
+ next unless any
211
+
212
+ groups_used |= 1 << (15 - g)
213
+ detail << group_bits
214
+ end
215
+ [groups_used, detail]
216
+ end
217
+
218
+ # Canonical Huffman code lengths (every symbol gets a code;
219
+ # zero frequencies become 1) with rescaling to keep lengths
220
+ # within bzip2's 23-bit limit.
221
+ # rubocop:disable Metrics/MethodLength
222
+ # rubocop:disable-next Metrics/AbcSize
223
+ def code_lengths(freqs)
224
+ n = freqs.length
225
+ active = freqs.each_index.map { |i| [(freqs[i].zero? ? 1 : freqs[i]), i] }
226
+ return Array.new(n, 0) if active.empty?
227
+
228
+ loop do
229
+ lengths = huffman_lengths(active, n)
230
+ max_len = lengths.max || 0
231
+ return lengths if max_len <= MAX_CODE_LENGTH
232
+
233
+ scaled = false
234
+ active.map! do |(w, i)|
235
+ if w > 1
236
+ scaled = true
237
+ [(w + 1) / 2, i]
238
+ else
239
+ [w, i]
240
+ end
241
+ end
242
+ # Can't reduce further: clamp.
243
+ return lengths.map { |l| [l, MAX_CODE_LENGTH].min } unless scaled
244
+ end
245
+ end
246
+
247
+ # Standard Huffman via smallest-pair merging with parent
248
+ # pointers; depth = code length per active symbol.
249
+ # rubocop:disable-next Metrics/AbcSize
250
+ def huffman_lengths(active, alphabet_size)
251
+ nodes = active.map { |(w, _i)| { freq: w, parent: -1 } }
252
+ ids = active.map { |_w, i| i }
253
+ next_id = alphabet_size
254
+
255
+ loop do
256
+ roots = []
257
+ nodes.each_index { |k| roots << k if nodes[k][:parent] == -1 }
258
+ break if roots.length <= 1
259
+
260
+ a = -1
261
+ b = -1
262
+ roots.each do |k|
263
+ if a == -1 || nodes[k][:freq] < nodes[a][:freq] ||
264
+ (nodes[k][:freq] == nodes[a][:freq] && ids[k] < ids[a])
265
+ b = a
266
+ a = k
267
+ elsif b == -1 || nodes[k][:freq] < nodes[b][:freq] ||
268
+ (nodes[k][:freq] == nodes[b][:freq] && ids[k] < ids[b])
269
+ b = k
270
+ end
271
+ end
272
+
273
+ nodes[a][:parent] = nodes.length
274
+ nodes[b][:parent] = nodes.length
275
+ nodes << { freq: nodes[a][:freq] + nodes[b][:freq], parent: -1 }
276
+ ids << next_id
277
+ next_id += 1
278
+ end
279
+
280
+ out = Array.new(alphabet_size, 0)
281
+ active.each_index do |k|
282
+ sym = ids[k]
283
+ len = 0
284
+ cur = k
285
+ while nodes[cur][:parent] != -1
286
+ cur = nodes[cur][:parent]
287
+ len += 1
288
+ end
289
+ out[sym] = [len, 1].max
290
+ end
291
+ out
292
+ end
293
+
294
+ # Canonical codes assigned in (length, symbol) order.
295
+ # rubocop:disable-next Metrics/AbcSize
296
+ def canonical_codes(lengths)
297
+ max_len = lengths.max || 0
298
+ codes = Array.new(lengths.length) { [0, 0] }
299
+ return codes if max_len.zero?
300
+
301
+ bl_count = Array.new(max_len + 1, 0)
302
+ lengths.each { |l| bl_count[l] += 1 if l.positive? }
303
+
304
+ next_code = Array.new(max_len + 1, 0)
305
+ code = 0
306
+ (1..max_len).each do |bits|
307
+ code = (code + bl_count[bits - 1]) << 1
308
+ next_code[bits] = code
309
+ end
310
+ lengths.each_with_index do |len, sym|
311
+ next unless len.positive?
312
+
313
+ codes[sym] = [next_code[len], len]
314
+ next_code[len] += 1
315
+ end
316
+ codes
317
+ end
318
+
319
+ # Delta-coded code-length table: 5-bit start length, then
320
+ # '10' (+1) / '11' (-1) adjustments and a '0' terminator per
321
+ # symbol.
322
+ def write_huffman_table(writer, lengths)
323
+ writer.write_bits(lengths[0], 5)
324
+ current = lengths[0]
325
+ lengths.each do |target|
326
+ diff = target - current
327
+ while diff != 0
328
+ writer.write_bit(true)
329
+ if diff.positive?
330
+ writer.write_bit(false)
331
+ diff -= 1
332
+ current += 1
333
+ else
334
+ writer.write_bit(true)
335
+ diff += 1
336
+ current -= 1
337
+ end
338
+ end
339
+ writer.write_bit(false)
340
+ end
341
+ end
342
+ # rubocop:enable Metrics/MethodLength
343
+
344
+ def rotate_left32(v, n)
345
+ ((v << n) | (v >> (32 - n))) & 0xFFFF_FFFF
346
+ end
347
+
348
+ # rubocop:disable Metrics/MethodLength
349
+ # rubocop:disable-next Metrics/AbcSize
350
+ # Decompress a complete .bz2 stream (single member). Verifies
351
+ # every block CRC and the combined stream CRC.
352
+ def decompress(input)
353
+ if input.bytesize < 4 || input.byteslice(0, 3) != "BZh" ||
354
+ !input.getbyte(3).between?("0".ord, "9".ord)
355
+ raise Omnizip::DecompressionError, "not a bzip2 stream (bad header)"
356
+ end
357
+
358
+ r = BitReader.new(input, 4)
359
+ out = String.new(encoding: Encoding::BINARY)
360
+ combined = 0
361
+ # rubocop:disable-next Metrics/BlockLength
362
+ loop do
363
+ magic = r.read48
364
+ if magic == EOS_MAGIC
365
+ stored = r.read_bits(32)
366
+ if stored != combined
367
+ raise Omnizip::DecompressionError,
368
+ format("combined CRC mismatch: stored %08X, " \
369
+ "computed %08X", stored, combined)
370
+ end
371
+ return out
372
+ end
373
+ unless magic == BLOCK_MAGIC
374
+ raise Omnizip::DecompressionError,
375
+ format("bad block magic %012X", magic)
376
+ end
377
+
378
+ block_crc = r.read_bits(32)
379
+ if r.read_bit == 1
380
+ raise Omnizip::DecompressionError,
381
+ "randomised blocks are not supported"
382
+ end
383
+ orig_ptr = r.read_bits(24)
384
+
385
+ groups = r.read_bits(16)
386
+ if groups.zero?
387
+ raise Omnizip::DecompressionError, "empty symbol map"
388
+ end
389
+
390
+ sequence = []
391
+ 16.times do |g|
392
+ next unless groups.anybits?(1 << (15 - g))
393
+
394
+ map = r.read_bits(16)
395
+ 16.times do |b|
396
+ sequence << ((g * 16) + b) if map.anybits?(1 << (15 - b))
397
+ end
398
+ end
399
+ n_in_use = sequence.length
400
+
401
+ n_groups = r.read_bits(3)
402
+ unless (2..MAX_GROUPS).cover?(n_groups)
403
+ raise Omnizip::DecompressionError, "invalid nGroups #{n_groups}"
404
+ end
405
+
406
+ n_selectors = r.read_bits(15)
407
+ if n_selectors.zero?
408
+ raise Omnizip::DecompressionError, "zero selectors"
409
+ end
410
+
411
+ selector_mtf = []
412
+ n_selectors.times do
413
+ j = 0
414
+ while r.read_bit == 1
415
+ j += 1
416
+ if j > MAX_GROUPS
417
+ raise Omnizip::DecompressionError,
418
+ "selector unary run too long"
419
+ end
420
+ end
421
+ selector_mtf << j
422
+ end
423
+ order = (0...n_groups).to_a
424
+ selectors = selector_mtf.map do |j|
425
+ table = order.delete_at(j)
426
+ order.unshift(table)
427
+ table
428
+ end
429
+
430
+ alphabet = n_in_use + 2
431
+ tables = Array.new(n_groups) do
432
+ lengths = Array.new(alphabet, 0)
433
+ cur = r.read_bits(5)
434
+ alphabet.times do |slot|
435
+ loop do
436
+ break if r.read_bit.zero?
437
+
438
+ cur += r.read_bit == 1 ? -1 : 1
439
+ end
440
+ lengths[slot] = cur
441
+ end
442
+ build_table(lengths)
443
+ end
444
+
445
+ eob = n_in_use + 1
446
+ symbols = []
447
+ catch(:eob) do
448
+ selectors.each do |sel|
449
+ table = tables[sel]
450
+ GROUP_SIZE.times do
451
+ sym = decode_symbol(table, r)
452
+ throw :eob if sym == eob
453
+
454
+ symbols << sym
455
+ end
456
+ end
457
+ end
458
+
459
+ mtf = symbols_to_mtf(symbols, n_in_use)
460
+ bwt = mtf_decode_seeded(mtf, sequence)
461
+ block = Bwt.new.decode(bwt, orig_ptr)
462
+ data = Rle.new.decode(block)
463
+ computed = crc32(data)
464
+ if computed != block_crc
465
+ raise Omnizip::DecompressionError,
466
+ format("block CRC mismatch: stored %08X, " \
467
+ "computed %08X", block_crc, computed)
468
+ end
469
+ combined = rotate_left32(combined, 1) ^ block_crc
470
+ out << data
471
+ end
472
+ end
473
+ # rubocop:enable Metrics/MethodLength
474
+
475
+ # RUNA/RUNB symbol stream back to MTF values.
476
+ # rubocop:disable-next Metrics/AbcSize
477
+ def symbols_to_mtf(symbols, n_in_use)
478
+ eob = n_in_use + 1
479
+ mtf = []
480
+ i = 0
481
+ while i < symbols.length
482
+ sym = symbols[i]
483
+ if [RUNA, RUNB].include?(sym)
484
+ run = 0
485
+ bit = 1
486
+ while i < symbols.length && [RUNA, RUNB].include?(symbols[i])
487
+ run += symbols[i] == RUNB ? bit << 1 : bit
488
+ bit <<= 1
489
+ i += 1
490
+ end
491
+ [run, 1 << 24].min.times { mtf << 0 }
492
+ elsif sym == eob
493
+ break
494
+ else
495
+ mtf << (sym - 1)
496
+ i += 1
497
+ end
498
+ end
499
+ mtf
500
+ end
501
+
502
+ # Seed-aware MTF inverse over the active-byte sequence.
503
+ # Returns a binary String (the BWT stage consumes bytes).
504
+ def mtf_decode_seeded(data, sequence)
505
+ list = sequence.dup
506
+ out = Array.new(data.length)
507
+ data.each_with_index do |v, i|
508
+ if v >= list.length
509
+ out[i] = 0
510
+ else
511
+ b = list.delete_at(v)
512
+ list.unshift(b)
513
+ out[i] = b
514
+ end
515
+ end
516
+ out.pack("C*")
517
+ end
518
+
519
+ # Canonical code table: entries sorted for bit-by-bit match.
520
+ def build_table(lengths)
521
+ alphabet = lengths.length
522
+ order = (0...alphabet).sort_by { |i| [lengths[i], i] }
523
+ entries = []
524
+ code = 0
525
+ length = 0
526
+ order.each do |i|
527
+ while length < lengths[i]
528
+ code <<= 1
529
+ length += 1
530
+ end
531
+ next unless lengths[i].positive?
532
+
533
+ entries << [i, code, lengths[i]]
534
+ code += 1
535
+ end
536
+ entries
537
+ end
538
+
539
+ def decode_symbol(table, r)
540
+ code = 0
541
+ len = 0
542
+ loop do
543
+ code = (code << 1) | r.read_bit
544
+ len += 1
545
+ raise Omnizip::DecompressionError, "huffman code too long" if len > 24
546
+
547
+ table.each do |(sym, c, l)|
548
+ return sym if l == len && c == code
549
+ end
550
+ end
551
+ end
552
+
553
+ # MSB-first bit reader.
554
+ class BitReader
555
+ def initialize(data, pos = 0)
556
+ @data = data
557
+ @pos = pos
558
+ @bits = 0
559
+ @nbits = 0
560
+ end
561
+
562
+ def read_bit
563
+ if @nbits.zero?
564
+ raise Omnizip::DecompressionError, "unexpected end of bitstream" if @pos >= @data.bytesize
565
+
566
+ @bits = @data.getbyte(@pos)
567
+ @pos += 1
568
+ @nbits = 8
569
+ end
570
+ @nbits -= 1
571
+ @bits.nobits?(1 << @nbits) ? 0 : 1
572
+ end
573
+
574
+ def read_bits(n)
575
+ v = 0
576
+ n.times { v = (v << 1) | read_bit }
577
+ v
578
+ end
579
+
580
+ def read48
581
+ (read_bits(24) << 24) | read_bits(24)
582
+ end
583
+ end
584
+
585
+ # MSB-first bit packer.
586
+ class BitWriter
587
+ def initialize
588
+ @out = []
589
+ @current = 0
590
+ @nbits = 0
591
+ end
592
+
593
+ def write_bits(bits, n)
594
+ return if n.zero?
595
+
596
+ mask = n == 32 ? bits : bits & ((1 << n) - 1)
597
+ @current = ((@current << n) | mask) & 0xFFFF_FFFF_FFFF_FFFF
598
+ @nbits += n
599
+ while @nbits >= 8
600
+ @nbits -= 8
601
+ @out << ((@current >> @nbits) & 0xFF)
602
+ end
603
+ end
604
+
605
+ def write_bit(bit)
606
+ write_bits(bit ? 1 : 0, 1)
607
+ end
608
+
609
+ def write48(value)
610
+ write_bits(value >> 24, 24)
611
+ write_bits(value & 0xFF_FFFF, 24)
612
+ end
613
+
614
+ def finish
615
+ if @nbits.positive?
616
+ @out << ((@current << (8 - @nbits)) & 0xFF)
617
+ @nbits = 0
618
+ end
619
+ @out.pack("C*")
620
+ end
621
+ end
622
+ end
623
+ end
624
+ end
625
+ end