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,245 +23,252 @@
23
23
  module Omnizip
24
24
  module Algorithms
25
25
  class Zstandard
26
- # Huffman decoding for Zstandard (RFC 8878 Section 4.2)
26
+ # Huffman coding for Zstandard literals (RFC 8878 §4.2).
27
27
  #
28
- # Zstandard uses FSE-compressed Huffman weights followed by
29
- # canonical Huffman decoding.
28
+ # Zstandard does NOT use symbol-ordered canonical Huffman. The
29
+ # decode table groups symbols by weight: all weight-1 symbols
30
+ # occupy the first DTable entries, then weight-2, and so on, with
31
+ # ascending symbol order inside each group. A symbol with weight
32
+ # w has code length (tableLog + 1 - w) and occupies (1 << w) >> 1
33
+ # consecutive DTable entries.
30
34
  class Huffman
31
35
  include Constants
32
36
 
33
- # @return [Hash<Integer, Array<Integer>>] Code to symbol mapping
34
- attr_reader :decode_table
37
+ DecodeEntry = Struct.new(:symbol, :nb_bits)
35
38
 
36
- # @return [Integer] Maximum code length
37
- attr_reader :max_bits
39
+ # @return [Array<Integer>] per-symbol weights (0 = absent)
40
+ attr_reader :weights
38
41
 
39
- # Build Huffman table from weights
40
- #
41
- # @param weights [Array<Integer>] Symbol weights (0 means not present)
42
- # @param max_bits [Integer] Maximum code length
43
- # @return [Huffman] Built Huffman decoder
44
- def self.build_from_weights(weights, max_bits = HUFFMAN_MAX_BITS)
45
- # Convert weights to code lengths
46
- code_lengths = calculate_code_lengths(weights, max_bits)
47
-
48
- # Build canonical Huffman codes
49
- codes = build_canonical_codes(code_lengths)
50
-
51
- # Build decode table: code -> [symbol, length]
52
- decode_table = {}
53
- code_lengths.each_with_index do |length, symbol|
54
- next if length.nil? || length.zero?
55
-
56
- code = codes[symbol]
57
- decode_table[code] = [symbol, length]
58
- end
42
+ # @return [Integer] table log (max code length in this tree)
43
+ attr_reader :table_log
59
44
 
60
- new(decode_table, max_bits)
45
+ # Build a decode table from per-symbol weights.
46
+ #
47
+ # @param weights [Array<Integer>] one entry per symbol
48
+ # @return [Huffman]
49
+ def self.from_weights(weights)
50
+ table_log = compute_table_log(weights)
51
+ new(weights, table_log)
61
52
  end
62
53
 
63
- # Calculate code lengths from weights
64
- #
65
- # Weight 0 means symbol is not present.
66
- # Higher weights mean shorter codes.
67
- #
68
- # @param weights [Array<Integer>] Symbol weights
69
- # @param max_bits [Integer] Maximum code length
70
- # @return [Array<Integer>] Code lengths
71
- def self.calculate_code_lengths(weights, max_bits)
72
- return [] if weights.nil? || weights.empty?
73
-
74
- # Find max weight
75
- max_weight = weights.max || 0
76
- return Array.new(weights.length, 0) if max_weight.zero?
77
-
78
- # Convert weights to code lengths
79
- # Higher weight = shorter code length
80
- weights.map do |weight|
81
- next 0 if weight.nil? || weight.zero?
82
-
83
- # Code length = max_weight - weight + 1
84
- [max_weight - weight + 1, max_bits].min
85
- end
54
+ def initialize(weights, table_log)
55
+ @weights = weights
56
+ @table_log = table_log
57
+ @lookup = nil
86
58
  end
87
59
 
88
- # Build canonical Huffman codes from lengths
60
+ # Decode one symbol from a reverse bitstream (peek max_bits,
61
+ # look up, consume the code's length).
89
62
  #
90
- # @param code_lengths [Array<Integer>] Code lengths for each symbol
91
- # @return [Hash<Integer, Integer>] Symbol to code mapping
92
- def self.build_canonical_codes(code_lengths)
93
- codes = {}
94
- return codes if code_lengths.nil? || code_lengths.empty?
95
-
96
- max_length = code_lengths.compact.max || 0
97
-
98
- # Count symbols at each length
99
- bl_count = Array.new(max_length + 1, 0)
100
- code_lengths.each do |length|
101
- bl_count[length] += 1 if length&.positive?
102
- end
103
-
104
- # Calculate starting code for each length
105
- code = 0
106
- next_code = Array.new(max_length + 1, 0)
107
- (1..max_length).each do |bits|
108
- code = ((code + bl_count[bits - 1]) << 1)
109
- next_code[bits] = code
110
- end
111
-
112
- # Assign codes to symbols
113
- code_lengths.each_with_index do |length, symbol|
114
- next if length.nil? || length.zero?
115
-
116
- codes[symbol] = next_code[length]
117
- next_code[length] += 1
63
+ # @param bitstream [FSE::BitStream]
64
+ # @return [Integer] decoded symbol byte
65
+ def decode(bitstream)
66
+ entry = lookup[bitstream.peek_bits(HUFFMAN_MAX_BITS)]
67
+ if entry.nil? || entry.nb_bits.zero?
68
+ raise Omnizip::DecompressionError,
69
+ "Huffman lookup miss: no code for the next bits"
118
70
  end
119
71
 
120
- codes
72
+ bitstream.read_bits(entry.nb_bits)
73
+ entry.symbol
121
74
  end
122
75
 
123
- # Initialize Huffman decoder
76
+ # Build the flat 1 << HUFFMAN_MAX_BITS lookup table.
124
77
  #
125
- # @param decode_table [Hash] Code to [symbol, length] mapping
126
- # @param max_bits [Integer] Maximum code length
127
- def initialize(decode_table, max_bits)
128
- @decode_table = decode_table
129
- @max_bits = max_bits
130
-
131
- # Build lookup table for faster decoding
132
- build_lookup_table
78
+ # @return [Array<DecodeEntry>]
79
+ def lookup
80
+ @lookup ||= begin
81
+ table_size = 1 << @table_log
82
+ dtable = Array.new(table_size) { DecodeEntry.new(0, 0) }
83
+ max_weight = @weights.max || 0
84
+
85
+ pos = 0
86
+ (1..max_weight).each do |w|
87
+ length = @table_log + 1 - w
88
+ entries_per_symbol = (1 << w) >> 1
89
+ @weights.each_with_index do |sw, sym|
90
+ next unless sw == w
91
+
92
+ entry = DecodeEntry.new(sym, length)
93
+ entries_per_symbol.times do
94
+ if pos < table_size
95
+ dtable[pos] = entry
96
+ pos += 1
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ expand = 1 << (HUFFMAN_MAX_BITS - @table_log)
103
+ lookup = []
104
+ table_size.times do |i|
105
+ expand.times { lookup << dtable[i] }
106
+ end
107
+ while lookup.length < (1 << HUFFMAN_MAX_BITS)
108
+ lookup << DecodeEntry.new(0, 0)
109
+ end
110
+ lookup
111
+ end
133
112
  end
134
113
 
135
- # Decode a symbol from bitstream
114
+ # Per-symbol (code, length) table for encoding, indexed by
115
+ # symbol value. Absent symbols map to (0, 0).
136
116
  #
137
- # @param bitstream [FSE::ForwardBitStream] The bitstream to read from
138
- # @return [Integer] Decoded symbol
139
- def decode(bitstream)
140
- return 0 if @lookup_table.nil? || @lookup_table.empty?
141
-
142
- # Peek max_bits bits
143
- code = 0
144
- bits_read = 0
145
-
146
- (@max_bits || 1).times do
147
- bit = read_single_bit_forward(bitstream)
148
- code = (code << 1) | bit
149
- bits_read += 1
150
-
151
- # Check if this code exists in our table
152
- if @decode_table.key?(code)
153
- expected_length = @decode_table[code][1]
154
- if bits_read == expected_length
155
- return @decode_table[code][0]
117
+ # @return [Array<Array(Integer, Integer)>]
118
+ def encode_table
119
+ @encode_table ||= begin
120
+ codes = Array.new(@weights.length) { [0, 0] }
121
+ max_weight = @weights.max || 0
122
+ dtable_pos = 0
123
+ (1..max_weight).each do |w|
124
+ length = @table_log + 1 - w
125
+ entries_per_symbol = (1 << w) >> 1
126
+ @weights.each_with_index do |sw, sym|
127
+ next unless sw == w
128
+
129
+ code = dtable_pos >> (@table_log - length)
130
+ codes[sym] = [code, length]
131
+ dtable_pos += entries_per_symbol
156
132
  end
157
133
  end
134
+ codes.fill([0, 0], codes.length..255)
135
+ codes
158
136
  end
159
-
160
- # Fallback: try lookup table
161
- symbol = @lookup_table[code]
162
- return symbol if symbol
163
-
164
- 0
165
137
  end
166
138
 
167
- private
168
-
169
- # Build lookup table for fast decoding
170
- def build_lookup_table
171
- @lookup_table = {}
172
-
173
- return if @decode_table.nil? || @decode_table.empty?
139
+ # Derive tableLog from the Kraft sum of the weights
140
+ # (C reference HUF_readStats).
141
+ #
142
+ # @return [Integer]
143
+ def self.compute_table_log(weights)
144
+ if weights.empty? || weights.all?(0)
145
+ raise Omnizip::DecompressionError,
146
+ "Huffman weights: no present symbols"
147
+ end
174
148
 
175
- @decode_table.each do |code, (symbol, length)|
176
- # For codes shorter than max_bits, fill all variations
177
- padding_bits = (@max_bits || 1) - length
178
- next if padding_bits.negative?
149
+ weight_total = weights.sum { |w| w.positive? ? (1 << w) >> 1 : 0 }
150
+ if weight_total.zero?
151
+ raise Omnizip::DecompressionError, "Huffman weight total is 0"
152
+ end
179
153
 
180
- (1 << padding_bits).times do |padding|
181
- full_code = (code << padding_bits) | padding
182
- @lookup_table[full_code] = symbol
183
- end
154
+ table_log = if weight_total.nobits?(weight_total - 1)
155
+ Constants.highbit32(weight_total)
156
+ else
157
+ Constants.highbit32(weight_total) + 1
158
+ end
159
+ if table_log > HUFFMAN_MAX_BITS
160
+ raise Omnizip::DecompressionError,
161
+ "Huffman tableLog #{table_log} exceeds max #{HUFFMAN_MAX_BITS}"
184
162
  end
185
- end
186
163
 
187
- # Read a single bit in forward order (MSB first)
188
- def read_single_bit_forward(bitstream)
189
- bitstream.read_bits(1)
164
+ table_log
190
165
  end
191
166
  end
192
167
 
193
- # Huffman table reader (RFC 8878 Section 4.2.1)
194
- #
195
- # Reads compressed Huffman table description from input.
168
+ # Reads a Huffman tree description from the wire (RFC 8878
169
+ # §4.2.1) and builds a Huffman decode table.
196
170
  class HuffmanTableReader
197
171
  include Constants
198
172
 
199
- # Read Huffman table from input
173
+ # Read the table from the head of `src`.
200
174
  #
201
- # @param input [IO] Input stream positioned at Huffman description
202
- # @return [Huffman] Huffman decoder
203
- def self.read(input)
204
- reader = new(input)
205
- reader.read_table
206
- end
175
+ # @param src [String]
176
+ # @return [Array(Huffman, Integer)] table and bytes consumed
177
+ def self.read(src)
178
+ raise Omnizip::DecompressionError, "empty Huffman header" if src.empty?
179
+
180
+ i_size = src.getbyte(0)
181
+ weights, consumed =
182
+ if i_size >= 128
183
+ read_direct_weights(src)
184
+ else
185
+ read_fse_compressed_weights(src)
186
+ end
207
187
 
208
- def initialize(input)
209
- @input = input
188
+ [Huffman.from_weights(weights), consumed]
210
189
  end
211
190
 
212
- # Read and build Huffman table
213
- #
214
- # @return [Huffman] Huffman decoder
215
- def read_table
216
- # Read header
217
- header = @input.read(1).ord
218
-
219
- # FSE compressed or raw weights?
220
- fse_compressed = header.anybits?(0x80)
221
-
222
- if fse_compressed
223
- read_fse_compressed_weights(header)
224
- else
225
- read_raw_weights(header)
191
+ # Direct 4-bit weights: byte 0 is 127 + o_size, then
192
+ # (o_size + 1) / 2 bytes holding two nibbles each.
193
+ def self.read_direct_weights(src)
194
+ i_size = src.getbyte(0)
195
+ o_size = i_size - 127
196
+ packed_bytes = (o_size + 1) / 2
197
+ needed = 1 + packed_bytes
198
+ if src.bytesize < needed
199
+ raise Omnizip::DecompressionError,
200
+ "truncated direct Huffman weights: need #{needed} bytes"
226
201
  end
227
- end
228
202
 
229
- private
203
+ weights = []
204
+ (0...o_size).step(2) do |n|
205
+ byte = src.getbyte(1 + (n / 2))
206
+ weights << (byte >> 4)
207
+ weights << (byte & 0x0F) if n + 1 < o_size
208
+ end
230
209
 
231
- # Read FSE-compressed weights
232
- def read_fse_compressed_weights(header)
233
- # Read accuracy log (4 bits)
234
- (header & 0x1F) + 5
210
+ weights << implied_last_weight(weights)
211
+ [weights, needed]
212
+ end
235
213
 
236
- # Read number of symbols (if header bit 6 is set)
237
- # For simplicity, assume 256 symbols
238
- num_symbols = 256
214
+ # FSE-compressed weights: byte 0 is the payload size, followed
215
+ # by an FSE table description and a 2-state interleaved
216
+ # bitstream (RFC 8878 §4.2.1.2).
217
+ def self.read_fse_compressed_weights(src)
218
+ i_size = src.getbyte(0)
219
+ if 1 + i_size > src.bytesize
220
+ raise Omnizip::DecompressionError,
221
+ "truncated FSE Huffman weights: need #{1 + i_size} bytes"
222
+ end
239
223
 
240
- # Read compressed weights using FSE
241
- # This is a simplified implementation
242
- weights = Array.new(num_symbols, 0)
224
+ payload = src.byteslice(1, i_size)
225
+ table, consumed = FSE.read_table(payload)
226
+ bitstream = payload.byteslice(consumed..)
243
227
 
244
- # For now, use uniform weights as fallback
245
- Huffman.build_from_weights(weights, HUFFMAN_MAX_BITS)
228
+ weights = FSE.decode_stream(table, bitstream, HUF_SYMBOLVALUE_MAX)
229
+ weights << implied_last_weight(weights)
230
+ [weights, 1 + i_size]
246
231
  end
247
232
 
248
- # Read raw (uncompressed) weights
249
- def read_raw_weights(header)
250
- # Header byte: 0b0RHHHHH
251
- # R = repeat flag (not used in basic implementation)
252
- # HHHHH = header byte
233
+ # Compute the implied last weight via the Kraft inequality:
234
+ # sum(2^(w-1)) over all symbols equals 1 << tableLog.
235
+ def self.implied_last_weight(weights)
236
+ weight_total = 0
237
+ weights.each do |w|
238
+ if w > HUFFMAN_MAX_LOG
239
+ raise Omnizip::DecompressionError,
240
+ "Huffman weight #{w} exceeds tableLog max"
241
+ end
253
242
 
254
- # Read number of weights
255
- num_weights = header & 0x3F
256
- num_weights = 256 if num_weights.zero?
243
+ weight_total += (1 << w) >> 1
244
+ end
245
+ if weight_total.zero?
246
+ raise Omnizip::DecompressionError, "Huffman weights sum to 0"
247
+ end
257
248
 
258
- weights = []
259
- num_weights.times do
260
- byte = @input.read(1)&.ord || 0
261
- weights << byte
249
+ table_log = Constants.highbit32(weight_total) + 1
250
+ if table_log > HUFFMAN_MAX_LOG
251
+ raise Omnizip::DecompressionError,
252
+ "Huffman tableLog #{table_log} exceeds max"
253
+ end
254
+
255
+ rest = (1 << table_log) - weight_total
256
+ if rest.zero?
257
+ raise Omnizip::DecompressionError,
258
+ "Huffman weights already complete; no implied weight"
259
+ end
260
+ unless rest.nobits?(rest - 1)
261
+ raise Omnizip::DecompressionError,
262
+ "Huffman implied weight remainder #{rest} is not a power of 2"
263
+ end
264
+
265
+ last_weight = Constants.highbit32(rest) + 1
266
+ if last_weight > HUFFMAN_MAX_LOG
267
+ raise Omnizip::DecompressionError,
268
+ "Huffman implied weight #{last_weight} exceeds max"
262
269
  end
263
270
 
264
- Huffman.build_from_weights(weights, HUFFMAN_MAX_BITS)
271
+ last_weight
265
272
  end
266
273
  end
267
274
  end