omnizip 0.3.13 → 0.3.15

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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/lib/omnizip/algorithm.rb +36 -1
  3. data/lib/omnizip/algorithms/bzip2.rb +1 -3
  4. data/lib/omnizip/algorithms/deflate.rb +1 -3
  5. data/lib/omnizip/algorithms/deflate64.rb +2 -2
  6. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +7 -8
  7. data/lib/omnizip/algorithms/lzma.rb +1 -1
  8. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +5 -1
  9. data/lib/omnizip/algorithms/lzma2.rb +4 -12
  10. data/lib/omnizip/algorithms/zstandard/constants.rb +38 -25
  11. data/lib/omnizip/algorithms/zstandard/decoder.rb +101 -136
  12. data/lib/omnizip/algorithms/zstandard/encoder.rb +81 -138
  13. data/lib/omnizip/algorithms/zstandard/frame/block.rb +17 -0
  14. data/lib/omnizip/algorithms/zstandard/frame/header.rb +42 -29
  15. data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +114 -117
  16. data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +434 -222
  17. data/lib/omnizip/algorithms/zstandard/fse/interleaved.rb +92 -0
  18. data/lib/omnizip/algorithms/zstandard/fse/table.rb +98 -176
  19. data/lib/omnizip/algorithms/zstandard/fse/table_description.rb +203 -0
  20. data/lib/omnizip/algorithms/zstandard/fse.rb +21 -2
  21. data/lib/omnizip/algorithms/zstandard/huffman.rb +195 -188
  22. data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +232 -255
  23. data/lib/omnizip/algorithms/zstandard/literals.rb +170 -103
  24. data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +47 -199
  25. data/lib/omnizip/algorithms/zstandard/sequences.rb +260 -252
  26. data/lib/omnizip/algorithms/zstandard/xxhash.rb +159 -0
  27. data/lib/omnizip/algorithms/zstandard.rb +20 -24
  28. data/lib/omnizip/algorithms.rb +14 -1
  29. data/lib/omnizip/buffer.rb +1 -2
  30. data/lib/omnizip/commands/archive_list_command.rb +9 -7
  31. data/lib/omnizip/convenience.rb +1 -2
  32. data/lib/omnizip/entry.rb +44 -0
  33. data/lib/omnizip/extraction/selective_extractor.rb +2 -10
  34. data/lib/omnizip/filter_pipeline.rb +1 -1
  35. data/lib/omnizip/filter_registry.rb +18 -16
  36. data/lib/omnizip/filters/bcj2.rb +1 -1
  37. data/lib/omnizip/filters/bcj_arm.rb +1 -1
  38. data/lib/omnizip/filters/bcj_arm64.rb +1 -1
  39. data/lib/omnizip/filters/bcj_ia64.rb +1 -1
  40. data/lib/omnizip/filters/bcj_ppc.rb +1 -1
  41. data/lib/omnizip/filters/bcj_sparc.rb +1 -1
  42. data/lib/omnizip/filters/bcj_x86.rb +1 -1
  43. data/lib/omnizip/filters.rb +0 -2
  44. data/lib/omnizip/formats/bzip2_file.rb +0 -7
  45. data/lib/omnizip/formats/cpio/entry.rb +6 -0
  46. data/lib/omnizip/formats/cpio.rb +0 -6
  47. data/lib/omnizip/formats/gzip.rb +0 -7
  48. data/lib/omnizip/formats/iso/directory_record.rb +7 -0
  49. data/lib/omnizip/formats/iso.rb +0 -5
  50. data/lib/omnizip/formats/lzip.rb +0 -7
  51. data/lib/omnizip/formats/lzma_alone.rb +0 -6
  52. data/lib/omnizip/formats/msi/entry.rb +6 -0
  53. data/lib/omnizip/formats/msi.rb +0 -9
  54. data/lib/omnizip/formats/ole/dirent.rb +6 -0
  55. data/lib/omnizip/formats/ole.rb +0 -9
  56. data/lib/omnizip/formats/rar.rb +0 -6
  57. data/lib/omnizip/formats/rar3/reader.rb +7 -0
  58. data/lib/omnizip/formats/rar5/reader.rb +7 -0
  59. data/lib/omnizip/formats/rpm/entry.rb +6 -0
  60. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +7 -0
  61. data/lib/omnizip/formats/seven_zip.rb +0 -6
  62. data/lib/omnizip/formats/tar/entry.rb +6 -0
  63. data/lib/omnizip/formats/tar.rb +2 -10
  64. data/lib/omnizip/formats/xar/entry.rb +6 -0
  65. data/lib/omnizip/formats/xar.rb +0 -6
  66. data/lib/omnizip/formats/zip.rb +0 -6
  67. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +33 -31
  68. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +177 -163
  69. data/lib/omnizip/implementations.rb +18 -4
  70. data/lib/omnizip/io/source.rb +3 -0
  71. data/lib/omnizip/metadata/entry_metadata.rb +7 -0
  72. data/lib/omnizip/parallel/engine.rb +48 -0
  73. data/lib/omnizip/parallel/parallel_compressor.rb +5 -16
  74. data/lib/omnizip/parallel/parallel_extractor.rb +5 -16
  75. data/lib/omnizip/parallel.rb +1 -0
  76. data/lib/omnizip/profile/profile_registry.rb +1 -2
  77. data/lib/omnizip/registry.rb +0 -1
  78. data/lib/omnizip/rubyzip_compat.rb +0 -1
  79. data/lib/omnizip/temp/temp_file.rb +1 -2
  80. data/lib/omnizip/version.rb +1 -1
  81. data/lib/omnizip/zip/entry.rb +7 -0
  82. data/lib/omnizip/zip/file.rb +4 -8
  83. data/lib/omnizip.rb +1 -1
  84. metadata +7 -6
  85. data/lib/omnizip/filters/filter_base.rb +0 -6
  86. data/lib/omnizip/filters/registration.rb +0 -22
  87. data/lib/omnizip/filters/registry.rb +0 -111
  88. data/lib/omnizip/format_registry.rb +0 -100
@@ -23,312 +23,289 @@
23
23
  module Omnizip
24
24
  module Algorithms
25
25
  class Zstandard
26
- # Huffman Encoder for Zstandard (RFC 8878 Section 4.2)
26
+ # Huffman encoder for Zstandard literals (RFC 8878 §4.2).
27
27
  #
28
- # Encodes literals using Huffman coding with FSE-compressed weights.
29
- class HuffmanEncoder
28
+ # Builds a length-limited Huffman code from per-byte frequencies,
29
+ # emits the weight table (direct or FSE-compressed), and codes
30
+ # the literals into 1 or 4 reverse bitstreams.
31
+ module HuffmanEncoder
30
32
  include Constants
31
33
 
32
- # @return [Array<Integer>] Code lengths for each symbol
33
- attr_reader :code_lengths
34
+ module_function
34
35
 
35
- # @return [Hash<Integer, Integer>] Symbol to code mapping
36
- attr_reader :codes
37
-
38
- # @return [Integer] Maximum code length
39
- attr_reader :max_bits
40
-
41
- # Build Huffman encoder from symbol frequencies
36
+ # Build 256 weights (0 for absent symbols) from the literal
37
+ # bytes, with code lengths capped at HUFFMAN_MAX_BITS.
42
38
  #
43
- # @param frequencies [Array<Integer>] Symbol frequencies
44
- # @param max_bits [Integer] Maximum code length (default 11)
45
- # @return [HuffmanEncoder] Huffman encoder
46
- def self.build_from_frequencies(frequencies,
47
- max_bits = HUFFMAN_MAX_BITS)
48
- return nil if frequencies.nil? || frequencies.empty?
49
-
50
- # Build Huffman tree and get code lengths
51
- code_lengths = build_huffman_lengths(frequencies, max_bits)
52
-
53
- # Limit code lengths to max_bits
54
- code_lengths = limit_code_lengths(code_lengths, max_bits)
39
+ # @param literals [String]
40
+ # @return [Array<Integer>]
41
+ def build_weights(literals)
42
+ counts = Array.new(256, 0)
43
+ literals.each_byte { |b| counts[b] += 1 }
44
+
45
+ present = (0..255).select { |b| counts[b].positive? }
46
+ if present.length < 2
47
+ # A Huffman table needs at least 2 symbols; pad with
48
+ # symbol 0 (or 1) so the real symbol keeps a real code.
49
+ weights = Array.new(256, 0)
50
+ sym = present.first || 0
51
+ weights[sym] = 1
52
+ other = sym.zero? ? 1 : 0
53
+ weights[other] = 1
54
+ return weights
55
+ end
55
56
 
56
- # Build canonical codes
57
- codes = build_canonical_codes(code_lengths)
57
+ freqs = present.map { |b| counts[b] }
58
+ lengths = huffman_lengths(freqs)
59
+ lengths = limit_lengths(lengths, HUFFMAN_MAX_BITS, freqs)
58
60
 
59
- new(code_lengths, codes, max_bits)
61
+ max_len = lengths.max
62
+ weights = Array.new(256, 0)
63
+ present.each_with_index do |byte, i|
64
+ weights[byte] = max_len - lengths[i] + 1
65
+ end
66
+ weights
60
67
  end
61
68
 
62
- # Build Huffman code lengths using package-merge algorithm
69
+ # Standard Huffman code lengths via smallest-pair merging.
63
70
  #
64
- # @param frequencies [Array<Integer>] Symbol frequencies
65
- # @param max_bits [Integer] Maximum code length
66
- # @return [Array<Integer>] Code lengths
67
- def self.build_huffman_lengths(frequencies, max_bits)
68
- return [] if frequencies.nil? || frequencies.empty?
69
-
70
- # Create list of (frequency, symbol) pairs
71
- symbols_with_freq = frequencies.each_with_index
72
- .select { |freq, _| freq&.positive? }
73
- .map { |freq, sym| [freq, sym] }
74
-
75
- return Array.new(frequencies.length, 0) if symbols_with_freq.empty?
71
+ # @param freqs [Array<Integer>]
72
+ # @return [Array<Integer>] code length per symbol
73
+ def huffman_lengths(freqs)
74
+ nodes = freqs.map { |f| { freq: f, parent: -1 } }
75
+
76
+ while nodes.count { |n| n[:parent] == -1 } > 1
77
+ a = -1
78
+ b = -1
79
+ nodes.each_with_index do |n, i|
80
+ next unless n[:parent] == -1
81
+
82
+ if a == -1 || n[:freq] < nodes[a][:freq]
83
+ b = a
84
+ a = i
85
+ elsif b == -1 || n[:freq] < nodes[b][:freq]
86
+ b = i
87
+ end
88
+ end
76
89
 
77
- # Sort by frequency
78
- symbols_with_freq.sort_by! { |freq, _| freq }
90
+ nodes[a][:parent] = nodes.length
91
+ nodes[b][:parent] = nodes.length
92
+ nodes << { freq: nodes[a][:freq] + nodes[b][:freq], parent: -1 }
93
+ end
79
94
 
80
- # Build Huffman tree
81
- code_lengths = Array.new(frequencies.length, 0)
95
+ lengths = Array.new(freqs.length, 0)
96
+ freqs.each_index do |i|
97
+ len = 0
98
+ cur = i
99
+ while nodes[cur][:parent] != -1
100
+ cur = nodes[cur][:parent]
101
+ len += 1
102
+ end
103
+ lengths[i] = [len, 255].min
104
+ end
105
+ lengths
106
+ end
82
107
 
83
- # Simple Huffman tree building
84
- # Using a priority queue approach
85
- build_tree_lengths(symbols_with_freq, code_lengths, max_bits)
108
+ # Cap code lengths at max_len while preserving the Kraft
109
+ # inequality: shorten the longest codes and lengthen the
110
+ # shortest until the sum of 2^-len is <= 1.
111
+ #
112
+ # @param lengths [Array<Integer>] mutated in place
113
+ # @param max_len [Integer]
114
+ # @param freqs [Array<Integer>]
115
+ # @return [Array<Integer>]
116
+ def limit_lengths(lengths, max_len, freqs)
117
+ loop do
118
+ cur_max = lengths.max || 0
119
+ break if cur_max <= max_len
120
+
121
+ longest = nil
122
+ lengths.each_with_index do |l, i|
123
+ next unless l == cur_max
124
+ next if longest && freqs[i] >= freqs[longest]
125
+
126
+ longest = i
127
+ end
86
128
 
87
- code_lengths
88
- end
129
+ shortest = nil
130
+ lengths.each_with_index do |l, i|
131
+ next unless l.positive? && l < max_len
132
+ next if shortest && freqs[i] <= freqs[shortest]
89
133
 
90
- # Build code lengths using tree approach
91
- def self.build_tree_lengths(symbols_with_freq, code_lengths, max_bits)
92
- return if symbols_with_freq.empty?
134
+ shortest = i
135
+ end
93
136
 
94
- # Create leaf nodes
95
- nodes = symbols_with_freq.map do |freq, sym|
96
- { freq: freq, symbol: sym, left: nil, right: nil, depth: 0 }
137
+ if longest && shortest
138
+ lengths[longest] -= 1
139
+ lengths[shortest] += 1
140
+ else
141
+ lengths.map! { |l| [l, max_len].min }
142
+ break
143
+ end
97
144
  end
98
145
 
99
- # Build tree by combining nodes
100
- while nodes.length > 1
101
- # Sort by frequency
102
- nodes.sort_by! { |n| n[:freq] }
146
+ loop do
147
+ kraft = lengths.sum { |l| l.positive? ? 2.0**-l : 0.0 }
148
+ break if kraft <= 1.0 + 1e-10
103
149
 
104
- # Combine two smallest
105
- left = nodes.shift
106
- right = nodes.shift
150
+ min_idx = nil
151
+ lengths.each_with_index do |l, i|
152
+ next unless l.positive? && l < max_len
153
+ next if min_idx && lengths[i] >= lengths[min_idx]
107
154
 
108
- combined = {
109
- freq: left[:freq] + right[:freq],
110
- symbol: nil,
111
- left: left,
112
- right: right,
113
- depth: [left[:depth], right[:depth]].max + 1,
114
- }
155
+ min_idx = i
156
+ end
157
+ break if min_idx.nil?
115
158
 
116
- nodes << combined
159
+ lengths[min_idx] += 1
117
160
  end
118
161
 
119
- # Extract code lengths from tree
120
- if nodes.length == 1
121
- assign_lengths(nodes[0], 0, code_lengths, max_bits)
122
- elsif symbols_with_freq.length == 1
123
- # Single symbol
124
- code_lengths[symbols_with_freq[0][1]] = 1
125
- end
162
+ lengths
126
163
  end
127
164
 
128
- # Recursively assign code lengths to symbols
129
- def self.assign_lengths(node, depth, code_lengths, max_bits)
130
- return unless node
131
-
132
- depth = [depth, max_bits].min
133
-
134
- if node[:symbol]
135
- # Leaf node
136
- code_lengths[node[:symbol]] = depth.positive? ? depth : 1
165
+ # Serialize the weight table. Direct 4-bit weights when the
166
+ # alphabet fits, FSE-compressed otherwise.
167
+ #
168
+ # @param weights [Array<Integer>]
169
+ # @return [String]
170
+ def encode_weights(weights)
171
+ max_symbol = weights.rindex(&:positive?)
172
+ raise Omnizip::CompressionError, "no present Huffman weights" if max_symbol.nil?
173
+
174
+ if max_symbol <= 128
175
+ encode_weights_direct(weights, max_symbol)
137
176
  else
138
- # Internal node
139
- assign_lengths(node[:left], depth + 1, code_lengths, max_bits)
140
- assign_lengths(node[:right], depth + 1, code_lengths, max_bits)
177
+ encode_weights_fse(weights, max_symbol)
141
178
  end
142
179
  end
143
180
 
144
- # Limit code lengths to maximum
145
- #
146
- # Uses the package-merge algorithm concept to limit lengths.
147
- #
148
- # @param code_lengths [Array<Integer>] Original code lengths
149
- # @param max_bits [Integer] Maximum code length
150
- # @return [Array<Integer>] Limited code lengths
151
- def self.limit_code_lengths(code_lengths, max_bits)
152
- return code_lengths if code_lengths.nil? || code_lengths.empty?
153
-
154
- # Check if any length exceeds max
155
- max_length = code_lengths.max || 0
156
- return code_lengths if max_length <= max_bits
157
-
158
- # Limit using a simple approach: cap at max_bits and adjust
159
- # This is a simplified implementation
160
- lengths = code_lengths.map { |l| [l, max_bits].min }
161
-
162
- # Ensure Kraft inequality is satisfied
163
- # Sum of 2^(-length) must be <= 1
164
- kraft_sum = lengths.sum { |l| l.positive? ? 1 << (max_bits - l) : 0 }
165
- max_kraft = 1 << max_bits
166
-
167
- if kraft_sum > max_kraft
168
- # Need to increase some lengths
169
- # This is simplified - a proper implementation would use package-merge
170
- lengths = redistribute_lengths(lengths, max_bits)
181
+ # Direct encoding: header byte 127 + o_size, then two 4-bit
182
+ # weights per byte. The last present weight is implied on
183
+ # decode and is dropped here.
184
+ def encode_weights_direct(weights, max_symbol)
185
+ o_size = max_symbol
186
+ i_size = 127 + o_size
187
+ out = [i_size].pack("C")
188
+
189
+ (0...o_size).step(2) do |n|
190
+ high = weights[n] & 0x0F
191
+ low = n + 1 < o_size ? weights[n + 1] & 0x0F : 0
192
+ out << ((high << 4) | low).chr
171
193
  end
172
194
 
173
- lengths
195
+ out
174
196
  end
175
197
 
176
- # Redistribute lengths to satisfy Kraft inequality
177
- def self.redistribute_lengths(lengths, max_bits)
178
- # Simplified: just cap at max_bits
179
- lengths.map { |l| [l, max_bits].min }
180
- end
198
+ # FSE-compressed weights (RFC 8878 §4.2.1.2): payload-size
199
+ # header byte, NCount, then the 2-state bitstream.
200
+ def encode_weights_fse(weights, max_symbol)
201
+ o_size = max_symbol
202
+ symbols = weights.first(o_size)
181
203
 
182
- # Build canonical Huffman codes from lengths
183
- #
184
- # @param code_lengths [Array<Integer>] Code lengths for each symbol
185
- # @return [Hash<Integer, Integer>] Symbol to code mapping
186
- def self.build_canonical_codes(code_lengths)
187
- codes = {}
188
- return codes if code_lengths.nil? || code_lengths.empty?
189
-
190
- max_length = code_lengths.compact.max || 0
191
- return codes if max_length.zero?
192
-
193
- # Count symbols at each length
194
- bl_count = Array.new(max_length + 1, 0)
195
- code_lengths.each do |length|
196
- bl_count[length] += 1 if length&.positive?
204
+ distinct = symbols.uniq.length
205
+ if distinct <= 1
206
+ raise Omnizip::CompressionError,
207
+ "uniform Huffman weights give no compression"
197
208
  end
198
209
 
199
- # Calculate starting code for each length
200
- code = 0
201
- next_code = Array.new(max_length + 1, 0)
202
- (1..max_length).each do |bits|
203
- code = ((code + bl_count[bits - 1]) << 1)
204
- next_code[bits] = code
210
+ encoder = FSE::Encoder.build_from_symbols(symbols, 11, 6)
211
+ if encoder.nil?
212
+ raise Omnizip::CompressionError,
213
+ "uniform Huffman weights give no compression"
205
214
  end
206
215
 
207
- # Assign codes to symbols
208
- code_lengths.each_with_index do |length, symbol|
209
- next if length.nil? || length.zero?
210
-
211
- codes[symbol] = next_code[length]
212
- next_code[length] += 1
216
+ payload = encoder.compress(symbols)
217
+ if payload.bytesize >= 128
218
+ raise Omnizip::CompressionError,
219
+ "FSE weight payload exceeds the 127-byte header limit"
213
220
  end
214
221
 
215
- codes
216
- end
217
-
218
- # Initialize Huffman encoder
219
- #
220
- # @param code_lengths [Array<Integer>] Code lengths
221
- # @param codes [Hash<Integer, Integer>] Symbol to code mapping
222
- # @param max_bits [Integer] Maximum code length
223
- def initialize(code_lengths, codes, max_bits)
224
- @code_lengths = code_lengths
225
- @codes = codes
226
- @max_bits = max_bits
227
-
228
- # Build reverse lookup for encoding
229
- @symbol_code = {}
230
- @symbol_length = {}
231
-
232
- codes.each do |symbol, code|
233
- @symbol_code[symbol] = code
234
- @symbol_length[symbol] = code_lengths[symbol]
235
- end
222
+ [payload.bytesize].pack("C") + payload
236
223
  end
237
224
 
238
- # Encode data using Huffman codes
225
+ # Encode literals as a Compressed_Literals_Block section:
226
+ # header + weights + coded stream(s).
239
227
  #
240
- # @param data [String] Data to encode
241
- # @return [String] Encoded bitstream
242
- def encode(data)
243
- return "" if data.nil? || data.empty?
244
-
245
- bits = []
246
-
247
- data.each_byte do |byte|
248
- code = @symbol_code[byte]
249
- length = @symbol_length[byte]
250
-
251
- next unless code && length
252
-
253
- # Write bits MSB first
254
- length.times do |i|
255
- bit = (code >> (length - 1 - i)) & 1
256
- bits << bit
228
+ # @param literals [String]
229
+ # @return [String]
230
+ # rubocop:disable Metrics/MethodLength
231
+ # rubocop:disable-next Metrics/AbcSize
232
+ def encode_literals(literals)
233
+ weights = build_weights(literals)
234
+ # The wire format drops the last present weight (it is implied
235
+ # by the Kraft inequality on decode), so the coding table must
236
+ # be rebuilt exactly the way the decoder rebuilds it.
237
+ max_symbol = weights.rindex(&:positive?)
238
+ wire_weights = weights.first(max_symbol)
239
+ full_weights = wire_weights +
240
+ [HuffmanTableReader.implied_last_weight(wire_weights)]
241
+ table = Huffman.from_weights(full_weights)
242
+ encode_table = table.encode_table
243
+
244
+ weights_wire = encode_weights(weights)
245
+ lit_size = literals.bytesize
246
+
247
+ # Single stream (3-byte header) when both sizes fit 10 bits.
248
+ if lit_size < 1024
249
+ coded = encode_huffman_stream(encode_table, literals)
250
+ lit_c_size = weights_wire.bytesize + coded.bytesize
251
+ if lit_c_size < 1024
252
+ header = LITERALS_BLOCK_COMPRESSED |
253
+ (lit_size << 4) | (lit_c_size << 14)
254
+ return [header].pack("V")[0, 3] + weights_wire + coded
257
255
  end
258
256
  end
259
257
 
260
- # Convert bit array to bytes
261
- bits_to_bytes(bits)
262
- end
263
-
264
- # Encode Huffman table description for Zstandard
265
- #
266
- # Zstandard compresses Huffman weights using FSE.
267
- #
268
- # @return [String] Encoded Huffman table description
269
- def encode_table_description
270
- # Convert code lengths to weights
271
- # Weight = max_bits - code_length + 1 (for non-zero lengths)
272
- weights = @code_lengths.map do |length|
273
- next 0 if length.nil? || length.zero?
274
-
275
- @max_bits - length + 1
276
- end
277
-
278
- encode_weights_fse(weights)
279
- end
280
-
281
- private
282
-
283
- # Encode weights using FSE compression
284
- def encode_weights_fse(weights)
285
- # Count non-zero weights
286
- num_weights = weights.count(&:positive?)
287
-
288
- if num_weights.zero?
289
- # No symbols - empty table
290
- return "\x00"
258
+ # 4 streams with a jump table.
259
+ segment_size = (lit_size + 3) / 4
260
+ segments = Array.new(4) do |i|
261
+ encode_huffman_stream(
262
+ encode_table,
263
+ literals.byteslice(segment_size * i, segment_size),
264
+ )
291
265
  end
292
266
 
293
- # Build header byte
294
- # Bit 7: FSE compressed (1)
295
- # Bits 0-6: depends on format
296
-
297
- if num_weights <= 127
298
- # Simple format: just the count
299
- header = 0x80 | num_weights
300
- header_bytes = [header].pack("C")
301
-
302
- # Encode weights as FSE (simplified: just raw bytes for now)
303
-
304
- else
305
- # Extended format
306
- header = 0x80 | 127
307
- header_bytes = [header, num_weights].pack("CC")
308
-
309
- end
310
- weight_bytes = weights.select(&:positive?).pack("C*")
311
- header_bytes + weight_bytes
267
+ lit_c_size = weights_wire.bytesize + 6 + segments.sum(&:bytesize)
268
+
269
+ out = if lit_size < 1024 && lit_c_size < 1024
270
+ header = LITERALS_BLOCK_COMPRESSED | (0b01 << 2) |
271
+ (lit_size << 4) | (lit_c_size << 14)
272
+ [header].pack("V")[0, 3]
273
+ elsif lit_size < 16_384 && lit_c_size < 16_384
274
+ header = LITERALS_BLOCK_COMPRESSED | (0b10 << 2) |
275
+ (lit_size << 4) | (lit_c_size << 18)
276
+ [header].pack("V")[0, 4]
277
+ elsif lit_size < 262_144 && lit_c_size < 262_144
278
+ low = LITERALS_BLOCK_COMPRESSED | (0b11 << 2) |
279
+ (lit_size << 4) | ((lit_c_size & 0x3FF) << 22)
280
+ [low].pack("V") + [(lit_c_size >> 10) & 0xFF].pack("C")
281
+ else
282
+ raise Omnizip::CompressionError,
283
+ "literals section exceeds the 18-bit header limits"
284
+ end
285
+
286
+ out + weights_wire +
287
+ segments.first(3).map { |s| [s.bytesize].pack("v") }.join +
288
+ segments.join
312
289
  end
290
+ # rubocop:enable Metrics/MethodLength
313
291
 
314
- # Convert bit array to bytes
315
- def bits_to_bytes(bits)
316
- # Pad to byte boundary
317
- bits = bits.dup
318
- while bits.length % 8 != 0
319
- bits << 0
320
- end
321
-
322
- bytes = []
323
- bits.each_slice(8) do |byte_bits|
324
- byte = 0
325
- byte_bits.each_with_index do |bit, i|
326
- byte |= (bit << (7 - i)) # MSB first for Huffman
327
- end
328
- bytes << byte
292
+ # Code literals into one reverse bitstream (C BIT_CStream
293
+ # direction): the encoder writes the last symbol first so the
294
+ # reverse reader recovers symbols in order.
295
+ #
296
+ # @param encode_table [Array<Array(Integer, Integer)>]
297
+ # @param literals [String]
298
+ # @return [String]
299
+ def encode_huffman_stream(encode_table, literals)
300
+ bitc = FSE::Encoder::BitCStream.new
301
+ (literals.bytesize - 1).downto(0) do |i|
302
+ code, len = encode_table[literals.getbyte(i)]
303
+ next if len.zero?
304
+
305
+ bitc.add_bits(code, len)
306
+ bitc.flush
329
307
  end
330
-
331
- bytes.pack("C*")
308
+ bitc.close
332
309
  end
333
310
  end
334
311
  end