omnizip 0.3.52 → 0.3.54

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/lib/omnizip/archive_handlers/zip_handler.rb +1 -1
  4. data/lib/omnizip/buffer/memory_archive.rb +55 -60
  5. data/lib/omnizip/buffer/memory_extractor.rb +26 -41
  6. data/lib/omnizip/buffer/seven_zip_bridge.rb +13 -0
  7. data/lib/omnizip/buffer.rb +38 -29
  8. data/lib/omnizip/checksums/verifier.rb +2 -2
  9. data/lib/omnizip/chunked.rb +14 -21
  10. data/lib/omnizip/formats/cpio/reader.rb +1 -1
  11. data/lib/omnizip/formats/iso/reader.rb +3 -3
  12. data/lib/omnizip/formats/iso/volume_descriptor.rb +2 -2
  13. data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +0 -20
  14. data/lib/omnizip/formats/rar/decompressor.rb +7 -7
  15. data/lib/omnizip/formats/rar/reader.rb +2 -2
  16. data/lib/omnizip/formats/rpm.rb +4 -4
  17. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +2 -2
  18. data/lib/omnizip/formats/seven_zip/coder_chain.rb +5 -5
  19. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +2 -2
  20. data/lib/omnizip/formats/seven_zip/file_collector.rb +1 -1
  21. data/lib/omnizip/formats/seven_zip/header.rb +4 -4
  22. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +2 -2
  23. data/lib/omnizip/formats/seven_zip/parser.rb +8 -8
  24. data/lib/omnizip/formats/seven_zip/reader.rb +15 -20
  25. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +6 -6
  26. data/lib/omnizip/formats/seven_zip/stream_compressor.rb +1 -1
  27. data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +5 -5
  28. data/lib/omnizip/formats/seven_zip/writer.rb +1 -1
  29. data/lib/omnizip/formats/xar/reader.rb +2 -2
  30. data/lib/omnizip/formats/zip/reader.rb +96 -8
  31. data/lib/omnizip/formats/zip/writer.rb +13 -6
  32. data/lib/omnizip/models/conversion_result.rb +75 -63
  33. data/lib/omnizip/models/filter_config.rb +34 -44
  34. data/lib/omnizip/models/match_result.rb +35 -18
  35. data/lib/omnizip/models/optimization_suggestion.rb +45 -41
  36. data/lib/omnizip/models/performance_result.rb +53 -47
  37. data/lib/omnizip/models/profile_report.rb +70 -36
  38. data/lib/omnizip/parallel/engine.rb +9 -5
  39. data/lib/omnizip/parallel/job_scheduler.rb +5 -1
  40. data/lib/omnizip/parallel/parallel_extractor.rb +63 -152
  41. data/lib/omnizip/parallel/thread_pool.rb +60 -0
  42. data/lib/omnizip/parallel/worker_pool.rb +8 -4
  43. data/lib/omnizip/parallel.rb +1 -0
  44. data/lib/omnizip/profiler/report_generator.rb +1 -1
  45. data/lib/omnizip/version.rb +1 -1
  46. data/lib/omnizip/zip/file.rb +1 -1
  47. data/lib/omnizip.rb +3 -0
  48. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8002bf03f99715929f352dfb92a91a10d02f6dac7152006a6cb87ca20b3647ae
4
- data.tar.gz: b095258b0b0af358ae9c0a21482a6f8b29de1620bb2162d64dd8794f29eb8f59
3
+ metadata.gz: 87089cc22eb4f5edd1cb37e7a0de297527fb2f7c0a2815f29d1ff271c73f7461
4
+ data.tar.gz: e6b7848399a98ae4f38f80cbc71833ada099e071d4f4126437ebbfcca820dc48
5
5
  SHA512:
6
- metadata.gz: 527be0108ee0fa63b6e7ebafdeaafd05430a17c08e0d59b0f731db7a3b76401da96daec205947c7c3b5baa07aba886515959513ef3d4b31438889761219e958d
7
- data.tar.gz: e1f8665f545fc81a4459cbdffbb9e5d5aca219a177a5b2d2ac3bd47c589a2ceb11b02d8cfdc748ac39381417f12682bf53cf14105dcbcd3d04bd59b77d7f9e87
6
+ metadata.gz: cfd421337992988889d3e5e8d4e85177248c5586238c2c79a5d87e5fc2229de3620f53e93d582ac72992c90b213421b1832cd44ecb9e1ff94e94622a48fa6b88
7
+ data.tar.gz: 33ca53cfe50c72a1d31afbbf0848ed9559824867edf34f1a4fdc37484300c029fcbf36bdb3135d92f607e31c517d6a519e945065a6c17f817cfbdc0af9ee6de9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.53] - 2026-09-01
11
+
12
+ ### Changed
13
+ - The last six hand-rolled `to_h` serializers migrate onto
14
+ lutaml-model like their siblings: `ConversionResult`,
15
+ `PerformanceResult`, `ProfileReport` (with a nested Summary model
16
+ built from aggregates), `OptimizationSuggestion`, `MatchResult`,
17
+ and `FilterConfig`. Computed report fields (size_reduction,
18
+ throughput, priority_score, summary aggregates) serialize through
19
+ lutaml derived attributes keyed under their documented names.
20
+ Serialized hashes now use string keys (`to_hash`, matching the
21
+ other migrated models).
22
+ - `Formats::Zip::Reader` gains `read_entry_stream`, yielding bounded
23
+ chunks (raw copy for STORE, incremental inflater for DEFLATE) with
24
+ a CRC check after the last chunk. `Chunked.decompress_file`
25
+ streams through it — the memory-efficient path no longer holds the
26
+ whole decompressed entry in memory to slice it. Extracting a
27
+ single 7z entry in memory (`MemoryExtractor`) extracts just that
28
+ entry instead of decompressing the entire archive.
29
+
30
+ ### Fixed
31
+ - Boundary errors stop masquerading as bare RuntimeErrors:
32
+ `Checksums::Verifier` raises `UnknownChecksumError` (the class
33
+ existed unused), and thirty more `raise "string"` sites gained
34
+ their matching classes (`Errno::ENOENT` for missing entries
35
+ — matching the handler-registry convention — `Errno::EEXIST`,
36
+ `IOError`, `InvalidArchiveError`, `DecompressionError`,
37
+ `RarNotAvailableError`). The never-called placeholder
38
+ `decode_escape_code` in the RAR PPMd decoder is deleted.
39
+
40
+ ## [0.3.52] - 2026-09-01
41
+
42
+ ### Changed
43
+ - The extract command's pattern path opens archives through the
44
+ Archive facade instead of a hand-rolled extension `case` that
45
+ misrouted `.cpio`/`.iso`/`.rpm` files into the 7z reader — pattern
46
+ extraction now works for every routed format. The metadata
47
+ command's 7z view lists through the handler registry (the detail
48
+ contract gains `:crc`), and SafeExtract extracts through
49
+ `extract_archive`, so every routed format gets rollback and
50
+ verification instead of ZIP-only with a `NotImplementedError`
51
+ for 7z. SelectiveExtractor understands both entry contracts
52
+ (legacy `entry_name` and reader-session `name`) and skips
53
+ directory entries when extracting.
54
+ - `CliOutputFormatter.format_size` is now the single byte formatter;
55
+ the nine private `format_bytes`/`format_size` copies (four
56
+ different shapes, two unit ceilings) become delegators.
57
+
58
+ ### Fixed
59
+ - The long-deferred parity spec flake: `Par2Verifier` crashed five
60
+ frames deep on a nil PAR2 path (`File.exist?(nil)` → TypeError)
61
+ where the repairer already raised a clean ArgumentError; the
62
+ verifier now mirrors it, and the spec's `.verify` group gains the
63
+ nil-index fallback `.repair` already had. The dead shadowed
64
+ `analyze_file` definition in `Par2Creator` (silently overridden
65
+ by a second definition with a divergent file_id format) is
66
+ deleted. The tautological "LZMA2 is implicit" xz assertion became
67
+ a real block-header parse asserting the explicit 0x21 filter ID.
68
+
10
69
  ## [0.3.51] - 2026-09-01
11
70
 
12
71
  ### Changed
@@ -38,7 +38,7 @@ module Omnizip
38
38
  extracted = []
39
39
  reader.entries.each do |entry|
40
40
  dest_path = ::File.join(output_dir, entry.filename)
41
- raise "File exists: #{dest_path}" if ::File.exist?(dest_path)
41
+ raise Errno::EEXIST, "File exists: #{dest_path}" if ::File.exist?(dest_path)
42
42
 
43
43
  reader.extract_entry(entry, output_dir)
44
44
  extracted << dest_path
@@ -9,27 +9,24 @@ module Omnizip
9
9
  # (for creating) and InputStream (for reading).
10
10
  #
11
11
  # @example Creating an archive
12
- # buffer = StringIO.new
13
- # Omnizip::Zip::OutputStream.open(buffer) do |zos|
14
- # archive = MemoryArchive.new(zos, :zip)
15
- # archive.add('file.txt', 'content')
16
- # end
12
+ # writer = Formats::Zip::Writer.new(nil)
13
+ # archive = MemoryArchive.new(writer, :zip)
14
+ # archive.add('file.txt', 'content')
15
+ # writer.write_to_io(buffer)
17
16
  #
18
17
  # @example Reading an archive
19
- # Omnizip::Zip::InputStream.open(buffer) do |zis|
20
- # archive = MemoryArchive.new(zis, :zip)
21
- # archive.each_entry do |entry|
22
- # puts entry.name
23
- # end
18
+ # archive = MemoryArchive.new(Formats::Zip::Reader.new(path), :zip)
19
+ # archive.each_entry do |entry|
20
+ # puts entry.name
24
21
  # end
25
22
  class MemoryArchive
26
23
  attr_reader :format, :stream
27
24
 
28
25
  # Initialize memory archive wrapper
29
26
  #
30
- # @param stream [Omnizip::Zip::OutputStream, Omnizip::Zip::InputStream]
31
- # Underlying stream
32
- # @param format [Symbol] Archive format (:zip, :seven_zip)
27
+ # @param stream [Formats::Zip::Writer, Formats::Zip::Reader]
28
+ # Native writer (write mode) or reader (read mode)
29
+ # @param format [Symbol] Archive format (:zip)
33
30
  def initialize(stream, format)
34
31
  @stream = stream
35
32
  @format = format
@@ -57,13 +54,13 @@ module Omnizip
57
54
  def add(name, data, **options)
58
55
  ensure_write_mode!
59
56
 
60
- case stream
61
- when Omnizip::Zip::OutputStream
62
- stream.put_next_entry(name, **options)
63
- stream.write(data) unless name.end_with?("/")
57
+ if name.end_with?("/") && data.to_s.empty?
58
+ stream.add_directory(name)
64
59
  else
65
- raise NotImplementedError,
66
- "Unsupported stream type: #{stream.class}"
60
+ method = if options[:compression] == :store
61
+ Formats::Zip::Constants::COMPRESSION_STORE
62
+ end
63
+ stream.add_data(name, data, nil, compression_method: method)
67
64
  end
68
65
 
69
66
  self
@@ -101,15 +98,8 @@ module Omnizip
101
98
  def each_entry
102
99
  ensure_read_mode!
103
100
 
104
- case stream
105
- when Omnizip::Zip::InputStream
106
- while (zip_entry = stream.get_next_entry)
107
- entry = Entry.new(zip_entry, stream)
108
- yield(entry)
109
- end
110
- else
111
- raise NotImplementedError,
112
- "Unsupported stream type: #{stream.class}"
101
+ stream.entries.each do |zip_entry|
102
+ yield(Entry.new(zip_entry, stream))
113
103
  end
114
104
  end
115
105
 
@@ -142,22 +132,10 @@ module Omnizip
142
132
  def to_s
143
133
  ensure_write_mode!
144
134
 
145
- case stream
146
- when Omnizip::Zip::OutputStream
147
- # OutputStream wraps the IO, we need to get the underlying buffer
148
- # This is only safe after close
149
- unless stream.closed?
150
- raise "Archive must be closed before accessing data"
151
- end
152
-
153
- # The buffer was passed in during creation, but we don't have
154
- # direct access. This method should be called on the StringIO
155
- # returned by Buffer.create instead.
156
- raise NotImplementedError,
157
- "Use Buffer.create return value instead"
158
- else
159
- raise "Cannot get string from read mode archive"
160
- end
135
+ # The buffer is owned by Buffer.create; this method should be
136
+ # called on the StringIO it returns instead.
137
+ raise NotImplementedError,
138
+ "Use Buffer.create return value instead"
161
139
  end
162
140
 
163
141
  # Entry wrapper with read capability
@@ -169,17 +147,19 @@ module Omnizip
169
147
 
170
148
  # Initialize entry wrapper
171
149
  #
172
- # @param entry [Omnizip::Zip::Entry] Underlying entry
173
- # @param stream [Omnizip::Zip::InputStream] Stream to read from
174
- def initialize(entry, stream)
150
+ # @param entry [Formats::Zip::CentralDirectoryHeader] entry
151
+ # @param reader [Formats::Zip::Reader] Native archive reader
152
+ def initialize(entry, reader)
175
153
  @entry = entry
176
- @stream = stream
177
- @name = entry.name
178
- @size = entry.size
154
+ @reader = reader
155
+ @name = entry.filename
156
+ @size = entry.uncompressed_size
179
157
  @compressed_size = entry.compressed_size
180
158
  @time = entry.time
181
- @comment = entry.comment
159
+ @comment = entry.comment.to_s
182
160
  @directory = entry.directory?
161
+ @content = nil
162
+ @pos = 0
183
163
  end
184
164
 
185
165
  # Read entry content
@@ -195,7 +175,20 @@ module Omnizip
195
175
  # process_chunk(chunk)
196
176
  # end
197
177
  def read(size = nil)
198
- @stream.read(size)
178
+ # IO-like streaming semantics: successive reads return
179
+ # successive chunks, nil at end-of-content. Callers like
180
+ # Pipe::StreamDecompressor loop on this — a read that
181
+ # always returns data would loop forever.
182
+ @content ||= @reader.read_entry(@name)
183
+ return nil if @pos >= @content.bytesize
184
+
185
+ chunk = if size
186
+ @content.byteslice(@pos, size)
187
+ else
188
+ @content.byteslice(@pos, @content.bytesize - @pos)
189
+ end
190
+ @pos += chunk.bytesize
191
+ chunk
199
192
  end
200
193
 
201
194
  # Check if entry is a directory
@@ -229,22 +222,24 @@ module Omnizip
229
222
 
230
223
  private
231
224
 
232
- # Ensure stream is in write mode (OutputStream)
225
+ # Ensure stream is in write mode (native Writer)
233
226
  #
234
- # @raise [RuntimeError] If not in write mode
227
+ # @raise [Omnizip::IOError] If not in write mode
235
228
  def ensure_write_mode!
236
- return if stream.is_a?(Omnizip::Zip::OutputStream)
229
+ return if stream.is_a?(Omnizip::Formats::Zip::Writer)
237
230
 
238
- raise "Operation requires write mode (OutputStream)"
231
+ raise Omnizip::IOError,
232
+ "Operation requires write mode (Formats::Zip::Writer)"
239
233
  end
240
234
 
241
- # Ensure stream is in read mode (InputStream)
235
+ # Ensure stream is in read mode (native Reader)
242
236
  #
243
- # @raise [RuntimeError] If not in read mode
237
+ # @raise [Omnizip::IOError] If not in read mode
244
238
  def ensure_read_mode!
245
- return if stream.is_a?(Omnizip::Zip::InputStream)
239
+ return if stream.is_a?(Omnizip::Formats::Zip::Reader)
246
240
 
247
- raise "Operation requires read mode (InputStream)"
241
+ raise Omnizip::IOError,
242
+ "Operation requires read mode (Formats::Zip::Reader)"
248
243
  end
249
244
  end
250
245
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
+ require "tmpdir"
4
5
 
5
6
  module Omnizip
6
7
  module Buffer
@@ -151,38 +152,36 @@ module Omnizip
151
152
 
152
153
  private
153
154
 
154
- # Detect archive format from magic bytes
155
+ # Delegate to the shared buffer-side sniffer
155
156
  #
156
157
  # @return [Symbol] Detected format
157
158
  # @raise [Omnizip::FormatError] If format cannot be detected
158
159
  def detect_format
159
- magic = @buffer.read(4).to_s.b
160
- @buffer.rewind
160
+ Omnizip::Buffer.detect_format(@buffer)
161
+ end
161
162
 
162
- case magic
163
- when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
164
- # ZIP signatures: local file header, EOCD, data descriptor
165
- :zip
166
- when "7z\xBC\xAF".b
167
- :seven_zip
168
- else
169
- raise Omnizip::FormatError,
170
- "Unknown archive format (magic: #{magic.inspect})"
163
+ # The native ZIP reader parses from a path; spill the buffer to
164
+ # a temporary file and yield a reader over it.
165
+ def with_zip_reader
166
+ @buffer.rewind
167
+ Dir.mktmpdir("omnizip_extractor_zip") do |tmp|
168
+ path = File.join(tmp, "buffer.zip")
169
+ File.binwrite(path, @buffer.read)
170
+ yield Omnizip::Formats::Zip::Reader.new(path)
171
171
  end
172
172
  end
173
173
 
174
- # Extract all entries from ZIP
174
+ # Extract all entries from ZIP through the native reader
175
175
  #
176
176
  # @param result [Hash] Hash to populate with entries
177
177
  def extract_all_zip(result)
178
- @buffer.rewind
179
- Omnizip::Zip::InputStream.open(@buffer) do |zis|
180
- while (entry = zis.get_next_entry)
178
+ with_zip_reader do |reader|
179
+ reader.entries.each do |entry|
181
180
  next if entry.directory?
182
181
 
183
- content = zis.read
184
- result[entry.name] = content
185
- @extracted_cache[entry.name] = content
182
+ content = reader.read_entry(entry.filename)
183
+ result[entry.filename] = content
184
+ @extracted_cache[entry.filename] = content
186
185
  end
187
186
  end
188
187
  end
@@ -207,41 +206,27 @@ module Omnizip
207
206
  def extract_entry_seven_zip(name)
208
207
  @buffer.rewind
209
208
  SevenZipBridge.open(@buffer) do |archive|
210
- entry = archive.raw_entries.find { |e| e.name == name }
211
- archive.extract_all_to_memory[name] if entry
209
+ archive.read_entry(name)
212
210
  end
213
211
  end
214
212
 
215
- # Extract single entry from ZIP
213
+ # Extract single entry from ZIP through the native reader
216
214
  #
217
215
  # @param name [String] Entry name
218
216
  # @return [String, nil] Entry content or nil if not found
219
217
  def extract_entry_zip(name)
220
- @buffer.rewind
221
- content = nil
222
-
223
- Omnizip::Zip::InputStream.open(@buffer) do |zis|
224
- while (entry = zis.get_next_entry)
225
- if entry.name == name
226
- content = zis.read unless entry.directory?
227
- break
228
- end
229
- end
218
+ with_zip_reader do |reader|
219
+ entry = reader.entries.find { |e| e.filename == name }
220
+ reader.read_entry(name) if entry && !entry.directory?
230
221
  end
231
-
232
- content
233
222
  end
234
223
 
235
- # List all entry names from ZIP
224
+ # List all entry names from ZIP through the native reader
236
225
  #
237
226
  # @param names [Array] Array to populate with names
238
227
  def list_entries_zip(names)
239
- @buffer.rewind
240
-
241
- Omnizip::Zip::InputStream.open(@buffer) do |zis|
242
- while (entry = zis.get_next_entry)
243
- names << entry.name
244
- end
228
+ with_zip_reader do |reader|
229
+ reader.entries.each { |entry| names << entry.filename }
245
230
  end
246
231
  end
247
232
  end
@@ -112,6 +112,19 @@ module Omnizip
112
112
  @reader.list_files
113
113
  end
114
114
 
115
+ # Content of a single named file entry, or nil when absent
116
+ # (directories carry no content). Extracts just that entry —
117
+ # not the whole archive.
118
+ #
119
+ # @param name [String] Entry name
120
+ # @return [String, nil]
121
+ def read_entry(name)
122
+ entry = raw_entries.find { |e| e.name == name && !e.is_dir }
123
+ return nil unless entry
124
+
125
+ Entry.new(entry, @reader, @tmp).read
126
+ end
127
+
115
128
  # Entry wrapper matching MemoryArchive::Entry's surface
116
129
  class Entry
117
130
  attr_reader :name, :size
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
+ require "tmpdir"
4
5
 
5
6
  module Omnizip
6
7
  # In-memory archive operations without filesystem I/O
@@ -107,27 +108,9 @@ module Omnizip
107
108
  extractor.extract_all
108
109
  end
109
110
 
110
- # Create archive from Hash of filename => content
111
- #
112
- # @param hash [Hash<String, String>] Filename => content mapping
113
- # @param format [Symbol] Archive format
114
- # @param options [Hash] Format-specific options
115
- # @return [StringIO] Complete archive in memory
116
- #
117
- # @example Create from Hash
118
- # data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
119
- # zip = Omnizip::Buffer.create_from_hash(data, :zip)
120
- def create_from_hash(hash, format = :zip, **options)
121
- create(format, **options) do |archive|
122
- hash.each do |name, content|
123
- archive.add(name, content)
124
- end
125
- end
126
- end
127
-
128
- private
129
-
130
- # Detect archive format from magic bytes
111
+ # Detect archive format from magic bytes. THE buffer-side
112
+ # sniffer — in-memory data has no extension to route by, so
113
+ # every consumer delegates here.
131
114
  #
132
115
  # @param buffer [StringIO] Buffer containing archive data
133
116
  # @return [Symbol] Detected format
@@ -148,27 +131,53 @@ module Omnizip
148
131
  end
149
132
  end
150
133
 
151
- # Create ZIP archive in buffer
134
+ # Create archive from Hash of filename => content
135
+ #
136
+ # @param hash [Hash<String, String>] Filename => content mapping
137
+ # @param format [Symbol] Archive format
138
+ # @param options [Hash] Format-specific options
139
+ # @return [StringIO] Complete archive in memory
140
+ #
141
+ # @example Create from Hash
142
+ # data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
143
+ # zip = Omnizip::Buffer.create_from_hash(data, :zip)
144
+ def create_from_hash(hash, format = :zip, **options)
145
+ create(format, **options) do |archive|
146
+ hash.each do |name, content|
147
+ archive.add(name, content)
148
+ end
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ # Create ZIP archive in buffer through the native writer
152
155
  #
153
156
  # @param buffer [StringIO] Buffer to write to
154
157
  # @param options [Hash] ZIP-specific options
155
158
  # @yield [archive] Block to populate archive
156
159
  def create_zip(buffer, _options, &block)
157
- Omnizip::Zip::OutputStream.open(buffer) do |zos|
158
- archive = Buffer::MemoryArchive.new(zos, :zip)
159
- block&.call(archive)
160
- end
160
+ writer = Formats::Zip::Writer.new(nil)
161
+ archive = Buffer::MemoryArchive.new(writer, :zip)
162
+ block&.call(archive)
163
+ writer.write_to_io(buffer)
161
164
  end
162
165
 
163
- # Open ZIP archive from buffer
166
+ # Open ZIP archive from buffer through the native reader.
167
+ # The reader parses from a path, so the buffer spills to a
168
+ # temporary file (same pattern as the 7z bridge).
164
169
  #
165
170
  # @param buffer [StringIO] Buffer containing ZIP data
166
171
  # @yield [archive] Block to read from archive
167
172
  # @return [MemoryArchive, Object] Archive or block return value
168
173
  def open_zip(buffer, &block)
169
174
  result = nil
170
- Omnizip::Zip::InputStream.open(buffer) do |zis|
171
- archive = Buffer::MemoryArchive.new(zis, :zip)
175
+ Dir.mktmpdir("omnizip_buffer_zip") do |tmp|
176
+ path = File.join(tmp, "buffer.zip")
177
+ File.binwrite(path, buffer.string)
178
+
179
+ archive = Buffer::MemoryArchive.new(Formats::Zip::Reader.new(path),
180
+ :zip)
172
181
  result = block ? yield(archive) : archive
173
182
  end
174
183
  result
@@ -94,7 +94,7 @@ module Omnizip
94
94
  when CHECK_SHA256
95
95
  verify_sha256(data, expected)
96
96
  else
97
- raise "Unknown check type: #{check_type}"
97
+ raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
98
98
  end
99
99
  end
100
100
 
@@ -122,7 +122,7 @@ module Omnizip
122
122
  when CHECK_SHA256
123
123
  Digest::SHA256.digest(data)
124
124
  else
125
- raise "Unknown check type: #{check_type}"
125
+ raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
126
126
  end
127
127
  end
128
128
  end
@@ -95,30 +95,23 @@ module Omnizip
95
95
  "Input archive not found: #{input}"
96
96
  end
97
97
 
98
- writer = Writer.new(output, chunk_size: chunk_size)
99
- processed = 0
100
-
101
- Omnizip::Zip::File.open(input) do |zip|
102
- entry = zip.entries.first
103
- total_size = entry.size
104
-
105
- # Read the full entry content
106
- content = zip.get_input_stream(entry)
98
+ reader = Omnizip::Formats::Zip::Reader.new(input)
99
+ entry = reader.entries.first
100
+ raise Omnizip::Error, "No entries to decompress in #{input}" unless entry
107
101
 
108
- # Write in chunks
109
- offset = 0
110
- while offset < content.bytesize
111
- chunk = content.byteslice(offset, chunk_size) || ""
112
- break if chunk.empty?
102
+ total_size = entry.uncompressed_size.to_i
103
+ processed = 0
104
+ writer = Writer.new(output, chunk_size: chunk_size)
113
105
 
114
- writer.write_chunk(chunk)
115
- processed += chunk.bytesize
116
- offset += chunk.bytesize
106
+ # Streams entry-by-entry in bounded memory — the whole entry
107
+ # is never held at once
108
+ reader.read_entry_stream(entry.filename, chunk_size: chunk_size) do |chunk|
109
+ writer.write_chunk(chunk)
110
+ processed += chunk.bytesize
117
111
 
118
- if progress
119
- percentage = (processed.to_f / total_size * 100).round(2)
120
- progress.call(processed, total_size, percentage)
121
- end
112
+ if progress
113
+ percentage = (processed.to_f / total_size * 100).round(2)
114
+ progress.call(processed, total_size, percentage)
122
115
  end
123
116
  end
124
117
 
@@ -64,7 +64,7 @@ module Omnizip
64
64
  # @raise [RuntimeError] if entry not found
65
65
  def extract_entry(entry_name, output_path)
66
66
  entry = @entries.find { |e| e.name == entry_name }
67
- raise "Entry not found: #{entry_name}" unless entry
67
+ raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
68
68
 
69
69
  extract_single_entry(entry, output_path)
70
70
  end
@@ -67,7 +67,7 @@ module Omnizip
67
67
  # @param output_path [String] Destination path
68
68
  def extract_entry(entry_path, output_path)
69
69
  entry = find_entry(entry_path)
70
- raise "Entry not found: #{entry_path}" unless entry
70
+ raise Errno::ENOENT, "Entry not found: #{entry_path}" unless entry
71
71
 
72
72
  if entry.directory?
73
73
  FileUtils.mkdir_p(output_path)
@@ -111,7 +111,7 @@ module Omnizip
111
111
  loop do
112
112
  @io.seek(sector * Iso::SECTOR_SIZE)
113
113
  data = @io.read(Iso::SECTOR_SIZE)
114
- raise "Failed to read volume descriptor" unless data
114
+ raise Omnizip::InvalidArchiveError, "Failed to read volume descriptor" unless data
115
115
 
116
116
  vd = VolumeDescriptor.parse(data)
117
117
 
@@ -127,7 +127,7 @@ module Omnizip
127
127
 
128
128
  return if @primary_volume_descriptor
129
129
 
130
- raise "No primary volume descriptor found"
130
+ raise Omnizip::InvalidArchiveError, "No primary volume descriptor found"
131
131
  end
132
132
 
133
133
  # Parse directory structure
@@ -29,7 +29,7 @@ module Omnizip
29
29
  #
30
30
  # @param data [String] Binary data
31
31
  def parse(data)
32
- raise "Invalid volume descriptor size" unless data.bytesize >= 2048
32
+ raise Omnizip::InvalidArchiveError, "Invalid volume descriptor size" unless data.bytesize >= 2048
33
33
 
34
34
  # Byte 0: Volume descriptor type
35
35
  @type = data.getbyte(0)
@@ -37,7 +37,7 @@ module Omnizip
37
37
  # Bytes 1-5: Standard identifier "CD001"
38
38
  @identifier = data[1, 5]
39
39
  unless @identifier == ISO_IDENTIFIER
40
- raise "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
40
+ raise Omnizip::InvalidArchiveError, "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
41
41
  end
42
42
 
43
43
  # Byte 6: Version (should be 1)
@@ -230,26 +230,6 @@ module Omnizip
230
230
  # Decode as 8 direct bits
231
231
  @range_decoder.decode_direct_bits(8)
232
232
  end
233
-
234
- # Decode RAR-specific escape code
235
- #
236
- # RAR variant H uses different escape code values
237
- # and handling compared to standard PPMd7.
238
- #
239
- # Escape codes in RAR:
240
- # - 0: New symbol follows
241
- # - 1: Same as last symbol (run-length)
242
- # - 2-255: Reserved for future use
243
- #
244
- # @return [Integer, nil] Escape code or nil
245
- def decode_escape_code
246
- # RAR escape codes differ from PPMd7
247
- # This is a placeholder for the proper implementation
248
-
249
- # For now, return 0 (new symbol follows)
250
- # Real implementation would decode from range coder
251
- 0
252
- end
253
233
  end
254
234
  end
255
235
  end