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,10 +23,12 @@
23
23
  module Omnizip
24
24
  module Algorithms
25
25
  class Zstandard
26
- # Pure Ruby Zstandard encoder (RFC 8878)
26
+ # Pure Ruby Zstandard encoder (RFC 8878).
27
27
  #
28
- # Encodes data using Zstandard format.
29
- # Supports raw blocks and Huffman-compressed literals.
28
+ # Emits a single-segment frame. Each 128 KiB chunk of input
29
+ # becomes one block, choosing whichever is smallest: RLE, a
30
+ # Compressed_Block (Huffman-coded literals + an empty sequences
31
+ # section), or a Raw_Block fallback for incompressible data.
30
32
  class Encoder
31
33
  include Constants
32
34
 
@@ -34,177 +36,125 @@ module Omnizip
34
36
 
35
37
  # Initialize encoder
36
38
  #
37
- # @param output_stream [IO] Output stream for compressed data
38
- # @param options [Hash] Encoder options
39
- # @option options [Integer] :level Compression level (1-22)
40
- # @option options [Boolean] :use_compression Use Huffman compression (default: true)
39
+ # @param output_stream [IO] output for compressed data
40
+ # @param options [Hash]
41
+ # @option options [Integer] :level compression level (1-22)
42
+ # @option options [Boolean] :checksum write the frame checksum
41
43
  def initialize(output_stream, options = {})
42
44
  @output_stream = output_stream
43
45
  @options = options
44
46
  @level = options[:level] || DEFAULT_LEVEL
45
- @use_compression = options.fetch(:use_compression, true)
47
+ @checksum = options.fetch(:checksum, false)
46
48
  end
47
49
 
48
- # Encode data stream
50
+ # Encode a data stream
49
51
  #
50
- # @param data [String] Data to compress
51
- # @return [void]
52
+ # @param data [String]
52
53
  def encode_stream(data)
53
- # Write Zstandard frame
54
+ data = data.dup.force_encoding(Encoding::BINARY)
54
55
  write_frame(data)
55
56
  end
56
57
 
57
58
  private
58
59
 
59
- # Write a complete Zstandard frame
60
60
  def write_frame(data)
61
- # Write magic number
62
61
  write_u32le(MAGIC_NUMBER)
62
+ write_frame_header(data)
63
+ write_blocks(data)
64
+ write_u32le(XXHash64.frame_checksum(data)) if @checksum
65
+ end
63
66
 
64
- # Write frame header descriptor
65
- # Single segment, no checksum, no dictionary
66
- if data.bytesize < 256
67
- # Single segment, 1-byte FCS (FCS flag = 0)
68
- descriptor = 0x20 # Single segment flag (bit 5)
69
- @output_stream.putc(descriptor)
70
- @output_stream.putc(data.bytesize)
67
+ # Single-segment frame header: no window descriptor, no
68
+ # dictionary; Frame_Content_Size in 1, 2, 4 or 8 bytes.
69
+ def write_frame_header(data)
70
+ size = data.bytesize
71
+ if size < 256
72
+ @output_stream.putc(0x20) # single segment, FCS 1 byte
73
+ @output_stream.putc(size)
74
+ elsif size <= 65_791
75
+ @output_stream.putc(0x60) # single segment, FCS 2 bytes (+256)
76
+ write_u16le(size - 256)
77
+ elsif size <= 0xFFFFFFFF
78
+ @output_stream.putc(0xA0) # single segment, FCS 4 bytes
79
+ write_u32le(size)
71
80
  else
72
- # Single segment, 4-byte FCS (FCS flag = 2)
73
- # Bits 6-7 = 10 binary = 0x80
74
- # Bit 5 = 1 (single segment) = 0x20
75
- descriptor = 0x80 | 0x20 # 0xA0
76
- @output_stream.putc(descriptor)
77
- write_u32le(data.bytesize)
81
+ @output_stream.putc(0xE0) # single segment, FCS 8 bytes
82
+ write_u64le(size)
78
83
  end
79
-
80
- # Write blocks
81
- write_blocks(data)
82
-
83
- # Write content checksum (optional, disabled for now)
84
- # write_u32le(xxhash32(data))
85
84
  end
86
85
 
87
- # Write blocks containing the data
88
86
  def write_blocks(data)
89
- return if data.empty?
87
+ return write_empty_last_block if data.empty?
90
88
 
91
89
  offset = 0
92
- max_block_size = BLOCK_MAX_SIZE
93
-
94
90
  while offset < data.bytesize
95
- chunk = data.byteslice(offset, max_block_size)
91
+ chunk = data.byteslice(offset, BLOCK_MAX_SIZE)
96
92
  offset += chunk.bytesize
97
-
98
93
  is_last = offset >= data.bytesize
99
-
100
- # Use RLE for repetitive data, otherwise raw blocks
101
- # Compressed blocks are deferred until decoder fully supports them
102
- if rle_efficient?(chunk)
103
- write_rle_block(chunk, is_last)
104
- else
105
- write_raw_block(chunk, is_last)
106
- end
94
+ write_chunk(chunk, is_last)
107
95
  end
108
96
  end
109
97
 
110
- # Check if RLE encoding would be efficient for a chunk
111
- def rle_efficient?(chunk)
112
- return false if chunk.bytesize < 3
113
-
114
- first_byte = chunk.getbyte(0)
115
- chunk.bytes.all?(first_byte)
98
+ def write_empty_last_block
99
+ write_block_header(1, BLOCK_TYPE_RAW, 0)
116
100
  end
117
101
 
118
- # Write an RLE (run-length encoded) block
119
- def write_rle_block(data, is_last)
120
- byte = data.getbyte(0)
121
- size = data.bytesize
122
-
123
- # Block header (3 bytes, little-endian)
124
- # Bit 0: Last_Block (1 = last)
125
- # Bits 1-2: Block_Type (1 = RLE)
126
- # Bits 3-23: Block_Size
127
-
128
- header = size << 3 # Block size in bits 3-23
129
- header |= BLOCK_TYPE_RLE << 1 # Block type = 1 (RLE)
130
- header |= 1 if is_last # Last block flag in bit 0
131
-
132
- # Write 3 bytes little-endian
133
- @output_stream.putc(header & 0xFF)
134
- @output_stream.putc((header >> 8) & 0xFF)
135
- @output_stream.putc((header >> 16) & 0xFF)
102
+ def write_chunk(chunk, is_last)
103
+ if chunk.bytesize >= 4 && rle_chunk?(chunk)
104
+ write_block_header(is_last ? 1 : 0, BLOCK_TYPE_RLE, chunk.bytesize)
105
+ @output_stream.putc(chunk.getbyte(0))
106
+ return
107
+ end
136
108
 
137
- # Write single byte to repeat
138
- @output_stream.putc(byte)
109
+ content = try_compressed(chunk)
110
+ if content.nil?
111
+ write_block_header(is_last ? 1 : 0, BLOCK_TYPE_RAW,
112
+ chunk.bytesize)
113
+ @output_stream.write(chunk)
114
+ else
115
+ write_block_header(is_last ? 1 : 0, BLOCK_TYPE_COMPRESSED,
116
+ content.bytesize)
117
+ @output_stream.write(content)
118
+ end
139
119
  end
140
120
 
141
- # Write a raw (uncompressed) block
142
- def write_raw_block(data, is_last)
143
- # Block header (3 bytes, little-endian)
144
- # Bit 0: Last_Block (1 = last)
145
- # Bits 1-2: Block_Type (0 = raw)
146
- # Bits 3-23: Block_Size
147
-
148
- header = data.bytesize << 3 # Block size in bits 3-23
149
- header |= BLOCK_TYPE_RAW << 1 # Block type in bits 1-2
150
- header |= 1 if is_last # Last block flag in bit 0
151
-
152
- # Write 3 bytes little-endian
153
- @output_stream.putc(header & 0xFF)
154
- @output_stream.putc((header >> 8) & 0xFF)
155
- @output_stream.putc((header >> 16) & 0xFF)
156
-
157
- # Write block content
158
- @output_stream.write(data)
121
+ # Build a Compressed_Block content: literals section plus an
122
+ # empty sequences section (nbSeq = 0). Returns nil when the
123
+ # result is not smaller than the raw chunk.
124
+ def try_compressed(chunk)
125
+ literals_section = LiteralsEncoder.encode(chunk)
126
+ # A single 0x00 byte: Number_of_Sequences = 0.
127
+ content = literals_section + "\x00".b
128
+ return nil if content.bytesize >= chunk.bytesize
129
+
130
+ content
131
+ rescue Omnizip::CompressionError
132
+ nil
159
133
  end
160
134
 
161
- # Write a compressed block with Huffman literals
162
- #
163
- # @param data [String] Block data
164
- # @param is_last [Boolean] Whether this is the last block
165
- # @return [Boolean] True if compression succeeded, false otherwise
166
- def write_compressed_block(data, is_last)
167
- # Encode literals section
168
- literals_section = LiteralsEncoder.encode(data, use_compression: true)
169
-
170
- # Check if compression is beneficial
171
- # Compressed block has overhead: block header (3) + literals header + sequences
172
- # For now, we need sequences section too (even if empty)
173
- sequences_section = encode_empty_sequences
174
-
175
- block_content = literals_section + sequences_section
176
- compressed_size = block_content.bytesize
177
-
178
- # Only use compressed if it's smaller
179
- if compressed_size >= data.bytesize
180
- return false
181
- end
135
+ def rle_chunk?(chunk)
136
+ first = chunk.getbyte(0)
137
+ idx = 1
138
+ while idx < chunk.bytesize
139
+ return false if chunk.getbyte(idx) != first
182
140
 
183
- # Write block header for compressed block
184
- header = compressed_size << 3 # Block size in bits 3-23
185
- header |= BLOCK_TYPE_COMPRESSED << 1 # Block type = 2 (compressed)
186
- header |= 1 if is_last # Last block flag in bit 0
141
+ idx += 1
142
+ end
143
+ true
144
+ end
187
145
 
188
- # Write 3 bytes little-endian
146
+ def write_block_header(last, type, size)
147
+ header = (last & 1) | (type << 1) | (size << 3)
189
148
  @output_stream.putc(header & 0xFF)
190
149
  @output_stream.putc((header >> 8) & 0xFF)
191
150
  @output_stream.putc((header >> 16) & 0xFF)
192
-
193
- # Write block content
194
- @output_stream.write(block_content)
195
-
196
- true
197
151
  end
198
152
 
199
- # Encode empty sequences section
200
- #
201
- # For blocks with only literals (no matches), we need an empty sequences section.
202
- def encode_empty_sequences
203
- # Number of sequences = 0 (single byte 0x00)
204
- "\x00"
153
+ def write_u16le(value)
154
+ @output_stream.putc(value & 0xFF)
155
+ @output_stream.putc((value >> 8) & 0xFF)
205
156
  end
206
157
 
207
- # Write unsigned 32-bit little-endian
208
158
  def write_u32le(value)
209
159
  @output_stream.putc(value & 0xFF)
210
160
  @output_stream.putc((value >> 8) & 0xFF)
@@ -212,15 +162,8 @@ module Omnizip
212
162
  @output_stream.putc((value >> 24) & 0xFF)
213
163
  end
214
164
 
215
- # Calculate XXHash32 checksum (simplified)
216
- def xxhash32(data, seed = 0)
217
- hash = seed
218
-
219
- data.each_byte do |byte|
220
- hash = ((hash << 5) + hash + byte) & 0xFFFFFFFF
221
- end
222
-
223
- hash
165
+ def write_u64le(value)
166
+ 8.times { |i| @output_stream.putc((value >> (8 * i)) & 0xFF) }
224
167
  end
225
168
  end
226
169
  end
@@ -60,6 +60,23 @@ module Omnizip
60
60
  new(last_block, block_type, block_size, raw)
61
61
  end
62
62
 
63
+ # Parse block header from a byte string.
64
+ #
65
+ # @param data [String]
66
+ # @param pos [Integer] offset of the 3-byte header
67
+ # @return [Array(Block, Integer)] block and position after the
68
+ # header
69
+ def self.parse_from(data, pos)
70
+ raw = data.getbyte(pos) |
71
+ (data.getbyte(pos + 1) << 8) |
72
+ (data.getbyte(pos + 2) << 16)
73
+ last_block = raw.allbits?(0x01)
74
+ block_type = (raw >> 1) & 0x03
75
+ block_size = (raw >> 3) & 0x1FFFFF
76
+
77
+ [new(last_block, block_type, block_size, raw), pos + 3]
78
+ end
79
+
63
80
  # Initialize with parsed values
64
81
  #
65
82
  # @param last_block [Boolean]
@@ -63,24 +63,41 @@ module Omnizip
63
63
  # @param input [IO] Input stream positioned at frame header
64
64
  # @return [Header] Parsed header
65
65
  def self.parse(input)
66
- descriptor = input.read(1).ord
66
+ parse_from(input.read, 0).first
67
+ end
68
+
69
+ # Parse frame header from a byte string.
70
+ #
71
+ # @param data [String]
72
+ # @param pos [Integer] offset of the descriptor byte
73
+ # @return [Array(Header, Integer)] header and position after
74
+ # the header
75
+ def self.parse_from(data, pos)
76
+ descriptor = data.getbyte(pos)
67
77
 
68
78
  header = new(descriptor)
79
+ offset = pos + 1
69
80
 
70
- # Parse optional fields based on descriptor bits
71
81
  if header.window_descriptor?
72
- header.parse_window_descriptor(input)
82
+ header.parse_window_descriptor_byte(data.getbyte(offset))
83
+ offset += 1
73
84
  end
74
85
 
75
86
  if header.dictionary_id?
76
- header.parse_dictionary_id(input)
87
+ size = header.dictionary_id_size
88
+ bytes = data.byteslice(offset, size)
89
+ offset += size
90
+ header.assign_dictionary_id(bytes)
77
91
  end
78
92
 
79
93
  if header.content_size?
80
- header.parse_content_size(input)
94
+ size = header.content_size_size
95
+ bytes = data.byteslice(offset, size)
96
+ offset += size
97
+ header.assign_content_size(bytes)
81
98
  end
82
99
 
83
- header
100
+ [header, offset]
84
101
  end
85
102
 
86
103
  # Initialize with descriptor byte
@@ -96,6 +113,7 @@ module Omnizip
96
113
  @dictionary_id_flag = descriptor & 0x03
97
114
 
98
115
  @window_log = nil
116
+ @window_mantissa = 0
99
117
  @dictionary_id = nil
100
118
  @content_size = nil
101
119
  @header_size = 1
@@ -165,53 +183,48 @@ module Omnizip
165
183
 
166
184
  # Get window size
167
185
  #
186
+ # windowAdd = (windowBase / 8) * Mantissa (RFC 8878
187
+ # §3.1.1.1.2); the mantissa occupies descriptor bits 0-2 and
188
+ # the exponent bits 3-7.
189
+ #
168
190
  # @return [Integer, nil] Window size or nil if not applicable
169
191
  def window_size
170
192
  return nil unless @window_log
171
193
 
172
- @window_log - 10
173
- mantissa = @window_log < 22 ? (@window_log - 10) : (@window_log - 11)
174
- (1 << @window_log) + (mantissa << (@window_log - 4))
194
+ window_base = 1 << @window_log
195
+ window_base + ((window_base / 8) * @window_mantissa)
175
196
  end
176
197
 
177
198
  # Parse window descriptor byte
178
- def parse_window_descriptor(input)
179
- byte = input.read(1).ord
180
- exponent = (byte >> 3) & 0x1F
181
- byte & 0x07
182
- @window_log = 10 + exponent
199
+ def parse_window_descriptor_byte(byte)
200
+ @window_mantissa = byte & 0x07
201
+ @window_log = 10 + ((byte >> 3) & 0x1F)
183
202
  @header_size += 1
184
203
  end
185
204
 
186
- # Parse dictionary ID (variable size)
187
- def parse_dictionary_id(input)
188
- size = dictionary_id_size
189
- bytes = input.read(size)
190
-
191
- @dictionary_id = case size
205
+ # Assign the dictionary ID from its wire bytes.
206
+ def assign_dictionary_id(bytes)
207
+ @dictionary_id = case bytes.bytesize
192
208
  when 1 then bytes.ord
193
209
  when 2 then bytes.unpack1("v")
194
210
  when 4 then bytes.unpack1("V")
195
211
  end
196
212
 
197
- @header_size += size
213
+ @header_size += bytes.bytesize
198
214
  end
199
215
 
200
- # Parse content size (variable size)
201
- def parse_content_size(input)
202
- size = content_size_size
203
- bytes = input.read(size)
204
-
205
- @content_size = case size
216
+ # Assign the content size from its wire bytes.
217
+ def assign_content_size(bytes)
218
+ @content_size = case bytes.bytesize
206
219
  when 1 then bytes.ord
207
- when 2 then bytes.unpack1("v")
220
+ when 2 then bytes.unpack1("v") + 256
208
221
  when 4 then bytes.unpack1("V")
209
222
  when 8
210
223
  low, high = bytes.unpack("VV")
211
224
  low + (high << 32)
212
225
  end
213
226
 
214
- @header_size += size
227
+ @header_size += bytes.bytesize
215
228
  end
216
229
  end
217
230
  end