omnizip 0.3.53 → 0.3.55

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/lib/omnizip/algorithms/lzma/dictionary.rb +2 -2
  4. data/lib/omnizip/algorithms/lzma/lzma_alone_decoder.rb +7 -7
  5. data/lib/omnizip/algorithms/lzma/xz_buffered_range_encoder.rb +4 -4
  6. data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +7 -7
  7. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +8 -8
  8. data/lib/omnizip/buffer/memory_archive.rb +55 -60
  9. data/lib/omnizip/buffer/memory_extractor.rb +25 -27
  10. data/lib/omnizip/buffer.rb +15 -8
  11. data/lib/omnizip/commands/metadata_command.rb +14 -6
  12. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +2 -2
  13. data/lib/omnizip/formats/seven_zip/coder_chain.rb +5 -5
  14. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +2 -2
  15. data/lib/omnizip/formats/seven_zip/file_collector.rb +1 -1
  16. data/lib/omnizip/formats/seven_zip/header.rb +4 -4
  17. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +2 -2
  18. data/lib/omnizip/formats/seven_zip/parser.rb +8 -8
  19. data/lib/omnizip/formats/seven_zip/reader.rb +14 -19
  20. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +5 -5
  21. data/lib/omnizip/formats/seven_zip/stream_compressor.rb +1 -1
  22. data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +5 -5
  23. data/lib/omnizip/formats/seven_zip/writer.rb +1 -1
  24. data/lib/omnizip/formats/zip/writer.rb +13 -6
  25. data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +5 -5
  26. data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +1 -1
  27. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +1 -1
  28. data/lib/omnizip/implementations/seven_zip/lzma/state_machine.rb +1 -1
  29. data/lib/omnizip/metadata/metadata_registry.rb +9 -2
  30. data/lib/omnizip/parallel/engine.rb +9 -5
  31. data/lib/omnizip/parallel/job_scheduler.rb +5 -1
  32. data/lib/omnizip/parallel/parallel_extractor.rb +62 -151
  33. data/lib/omnizip/parallel/thread_pool.rb +60 -0
  34. data/lib/omnizip/parallel/worker_pool.rb +5 -1
  35. data/lib/omnizip/parallel.rb +1 -0
  36. data/lib/omnizip/version.rb +1 -1
  37. data/lib/omnizip.rb +3 -0
  38. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7e72a57eaeb6e597e40020de67aa0242da65c6e6e8ceaa933d779d926e55666
4
- data.tar.gz: 105cf4f238610f518462c115d398403fef5daca9b2994788361b027c7c5ec648
3
+ metadata.gz: 7de280d85fcea7072c88798c925b5aa3e03bcff124c1294e72abeb6f976cc5e8
4
+ data.tar.gz: 579759d99f27af57c070e5e1b6a6d6b121651932b10681da88e96331adb36bee
5
5
  SHA512:
6
- metadata.gz: a5301b14047d1150885624527d03176c61a979464b7a16329a47449e13fe05679c1e5ce11a650bfd29101aaaf502b5ab81513558fc080dca85554266ae30f8dc
7
- data.tar.gz: a44ffd04cc02fa03e2b23a330da4f6612d268bfd350d9b9672e42d7a107dcad65201ed9707651b3533fdd790a5841a5ff9297d6f3a8e937b51e0f1614d756c55
6
+ metadata.gz: a08a53e33284555a3fe7493834f522e0bf00a8609705e06d6c05578bb342bb3db4e4ba1c3c0793f18a001ef55305fd3396a7300b6c7f1c82196a9702931fc374
7
+ data.tar.gz: 7b0550e981ff0cb317beffdcb7eff7b233a7fff0bc4812eae01a566f1a0d255de8896e217d1d1ce62618b56bb5ea284dc414c98b53c171fa4af0b54a1a048b86
data/CHANGELOG.md CHANGED
@@ -7,6 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.54] - 2026-09-01
11
+
12
+ ### Changed
13
+ - The 7z tree's 38 bare `raise "string"` sites gain their matching
14
+ classes: `PasswordError` for encrypted-header problems (with a
15
+ new autoload entry — the class lives at the top of password.rb,
16
+ so referencing it never loaded the file),
17
+ `InvalidArchiveError`/`UnsupportedFormatError` for parse and
18
+ structure failures, `ChecksumError` for CRC mismatches,
19
+ `AlgorithmNotFoundError`/`UnknownFilterError` in CoderChain, and
20
+ `Errno::ENOENT` for missing entries and volumes. The reader's
21
+ encrypted-header probe re-raises password errors by class instead
22
+ of matching message text.
23
+ - In-memory ZIP leaves the rubyzip compat layer: `Buffer.create`
24
+ writes through the native Writer (which gains a per-entry
25
+ compression-method override so `:store` entries work beside
26
+ deflated ones), and `Buffer.open`/`MemoryExtractor` read through
27
+ the native Reader via a temporary-file spill.
28
+ `MemoryArchive::Entry#read` now has true IO streaming semantics —
29
+ successive chunks and nil at EOF (an always-returning read looped
30
+ forever in `Pipe::StreamDecompressor`).
31
+ - The parallel subsystem is revived: Engine drives our own
32
+ WorkerPool instead of a phantom `Fractor::WorkerPool`,
33
+ ParallelExtractor reads through the native ZIP reader and runs on
34
+ a bounded ThreadPool — Ractor workers cannot trigger Omnizip's
35
+ autoloads or cross into Proc-holding codec constants on
36
+ Ruby <= 3.3 — JobScheduler's implemented round_robin/bin_packing
37
+ strategies are reachable, directory entries extract idempotently,
38
+ and the subsystem gains specs.
39
+
40
+ ### Fixed
41
+ - par2_compatibility_spec was missing `require "tmpdir"` and failed
42
+ 9 of 15 examples whenever no earlier spec happened to load tmpdir
43
+ first.
44
+
45
+ ## [0.3.53] - 2026-09-01
46
+
47
+ ### Changed
48
+ - The last six hand-rolled `to_h` serializers migrate onto
49
+ lutaml-model like their siblings: `ConversionResult`,
50
+ `PerformanceResult`, `ProfileReport` (with a nested Summary model
51
+ built from aggregates), `OptimizationSuggestion`, `MatchResult`,
52
+ and `FilterConfig`. Computed report fields (size_reduction,
53
+ throughput, priority_score, summary aggregates) serialize through
54
+ lutaml derived attributes keyed under their documented names.
55
+ Serialized hashes now use string keys (`to_hash`, matching the
56
+ other migrated models).
57
+ - `Formats::Zip::Reader` gains `read_entry_stream`, yielding bounded
58
+ chunks (raw copy for STORE, incremental inflater for DEFLATE) with
59
+ a CRC check after the last chunk. `Chunked.decompress_file`
60
+ streams through it — the memory-efficient path no longer holds the
61
+ whole decompressed entry in memory to slice it. Extracting a
62
+ single 7z entry in memory (`MemoryExtractor`) extracts just that
63
+ entry instead of decompressing the entire archive.
64
+
65
+ ### Fixed
66
+ - Boundary errors stop masquerading as bare RuntimeErrors:
67
+ `Checksums::Verifier` raises `UnknownChecksumError` (the class
68
+ existed unused), and thirty more `raise "string"` sites gained
69
+ their matching classes (`Errno::ENOENT` for missing entries
70
+ — matching the handler-registry convention — `Errno::EEXIST`,
71
+ `IOError`, `InvalidArchiveError`, `DecompressionError`,
72
+ `RarNotAvailableError`). The never-called placeholder
73
+ `decode_escape_code` in the RAR PPMd decoder is deleted.
74
+
10
75
  ## [0.3.52] - 2026-09-01
11
76
 
12
77
  ### Changed
@@ -31,7 +31,7 @@ module Omnizip
31
31
 
32
32
  # Read bytes from dictionary at a distance back
33
33
  def read_bytes(distance, length)
34
- raise "Invalid distance: #{distance}" if distance > @buffer.bytesize
34
+ raise Omnizip::DecompressionError, "Invalid distance: #{distance}" if distance > @buffer.bytesize
35
35
 
36
36
  result = String.new(encoding: Encoding::BINARY)
37
37
  src_pos = @buffer.bytesize - distance
@@ -46,7 +46,7 @@ module Omnizip
46
46
 
47
47
  # Get byte at distance back
48
48
  def get_byte(distance)
49
- raise "Invalid distance: #{distance}" if distance > @buffer.bytesize
49
+ raise Omnizip::DecompressionError, "Invalid distance: #{distance}" if distance > @buffer.bytesize
50
50
 
51
51
  @buffer.getbyte(@buffer.bytesize - distance)
52
52
  end
@@ -118,12 +118,12 @@ module Omnizip
118
118
  # Step 1: Parse properties byte (SEQ_PROPERTIES)
119
119
  # Reference: alone_decoder.c:64-68
120
120
  props = @input.getbyte
121
- raise "Invalid .lzma header: missing properties byte" if props.nil?
121
+ raise Omnizip::DecompressionError, "Invalid .lzma header: missing properties byte" if props.nil?
122
122
 
123
123
  # Use XZ Utils property byte parsing
124
124
  # Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c:1216-1228
125
125
  if props > MAX_PROPERTY_BYTE
126
- raise "Invalid .lzma header: properties byte #{props} exceeds maximum #{MAX_PROPERTY_BYTE}"
126
+ raise Omnizip::DecompressionError, "Invalid .lzma header: properties byte #{props} exceeds maximum #{MAX_PROPERTY_BYTE}"
127
127
  end
128
128
 
129
129
  # Parse lc, lp, pb from properties byte
@@ -136,7 +136,7 @@ module Omnizip
136
136
  # Validate lc + lp <= 4 (LZMA_LCLP_MAX)
137
137
  # Reference: lzma_decoder.c:1227
138
138
  if @lc + @lp > 4
139
- raise "Invalid .lzma header: lc (#{@lc}) + lp (#{@lp}) exceeds maximum 4"
139
+ raise Omnizip::DecompressionError, "Invalid .lzma header: lc (#{@lc}) + lp (#{@lp}) exceeds maximum 4"
140
140
  end
141
141
 
142
142
  # Step 2: Parse dictionary size (SEQ_DICTIONARY_SIZE)
@@ -144,7 +144,7 @@ module Omnizip
144
144
  @dict_size = 0
145
145
  4.times do |i|
146
146
  byte = @input.getbyte
147
- raise "Incomplete .lzma header: missing dictionary size byte" if byte.nil?
147
+ raise Omnizip::DecompressionError, "Incomplete .lzma header: missing dictionary size byte" if byte.nil?
148
148
 
149
149
  @dict_size |= (byte << (i * 8))
150
150
  end
@@ -163,7 +163,7 @@ module Omnizip
163
163
  d += 1
164
164
 
165
165
  if d != @dict_size
166
- raise "Invalid .lzma header: dictionary size #{@dict_size} is not 2^n or 2^n + 2^(n-1)"
166
+ raise Omnizip::DecompressionError, "Invalid .lzma header: dictionary size #{@dict_size} is not 2^n or 2^n + 2^(n-1)"
167
167
  end
168
168
  end
169
169
 
@@ -172,7 +172,7 @@ module Omnizip
172
172
  @uncompressed_size = 0
173
173
  8.times do |i|
174
174
  byte = @input.getbyte
175
- raise "Incomplete .lzma header: missing uncompressed size byte" if byte.nil?
175
+ raise Omnizip::DecompressionError, "Incomplete .lzma header: missing uncompressed size byte" if byte.nil?
176
176
 
177
177
  @uncompressed_size |= (byte << (i * 8))
178
178
  end
@@ -182,7 +182,7 @@ module Omnizip
182
182
  # Reference: alone_decoder.c:116-120
183
183
  if @picky && @uncompressed_size != 0xFFFFFFFFFFFFFFFF &&
184
184
  @uncompressed_size >= MAX_UNCOMPRESSED_SIZE
185
- raise "Invalid .lzma header: uncompressed size #{@uncompressed_size} exceeds maximum #{MAX_UNCOMPRESSED_SIZE}"
185
+ raise Omnizip::DecompressionError, "Invalid .lzma header: uncompressed size #{@uncompressed_size} exceeds maximum #{MAX_UNCOMPRESSED_SIZE}"
186
186
  end
187
187
 
188
188
  # Note: XZ Utils uses UINT64_MAX (0xFFFFFFFFFFFFFFFF) for unknown size
@@ -97,7 +97,7 @@ module Omnizip
97
97
  # @param prob [BitModel] Probability model
98
98
  # @param bit [Integer] Bit value (0 or 1)
99
99
  def queue_bit(prob, bit)
100
- raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
100
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
101
101
 
102
102
  @symbols[@count] = bit.zero? ? RC_BIT_0 : RC_BIT_1
103
103
  @probs[@count] = prob
@@ -111,7 +111,7 @@ module Omnizip
111
111
  def queue_direct_bits(value, num_bits)
112
112
  num_bits.downto(1) do |i|
113
113
  bit = (value >> (i - 1)) & 1
114
- raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
114
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
115
115
 
116
116
  @symbols[@count] = bit.zero? ? RC_DIRECT_0 : RC_DIRECT_1
117
117
  @probs[@count] = nil
@@ -122,7 +122,7 @@ module Omnizip
122
122
  # Queue flush operation
123
123
  def queue_flush
124
124
  5.times do
125
- raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
125
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
126
126
 
127
127
  @symbols[@count] = RC_FLUSH
128
128
  @probs[@count] = nil
@@ -237,7 +237,7 @@ module Omnizip
237
237
 
238
238
  # Forget queued symbols (e.g., when output limit reached)
239
239
  def forget
240
- raise "Cannot forget with partial encoding" unless @pos.zero?
240
+ raise Omnizip::CompressionError, "Cannot forget with partial encoding" unless @pos.zero?
241
241
 
242
242
  @count = 0
243
243
  end
@@ -63,7 +63,7 @@ module Omnizip
63
63
 
64
64
  # Forget pending symbols (matches XZ Utils rc_forget)
65
65
  def forget
66
- raise "Cannot forget while encoding" if @pos != 0
66
+ raise Omnizip::CompressionError, "Cannot forget while encoding" if @pos != 0
67
67
 
68
68
  @count = 0
69
69
  end
@@ -73,7 +73,7 @@ module Omnizip
73
73
  # @param prob [Probability] Probability model
74
74
  # @param bit [Integer] Bit value (0 or 1)
75
75
  def bit(prob, bit)
76
- raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
76
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
77
77
 
78
78
  @symbols[@count] = bit
79
79
  @probs[@count] = prob
@@ -118,7 +118,7 @@ module Omnizip
118
118
  # @param bit_count [Integer] Number of bits
119
119
  def direct(value, bit_count)
120
120
  bit_count.times do
121
- raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
121
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
122
122
 
123
123
  @symbols[@count] = RC_DIRECT_0 | ((value >> (bit_count -= 1)) & 1)
124
124
  @probs[@count] = nil
@@ -129,7 +129,7 @@ module Omnizip
129
129
  # Queue flush operation (matches XZ Utils rc_flush)
130
130
  def flush
131
131
  5.times do
132
- raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
132
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
133
133
 
134
134
  @symbols[@count] = RC_FLUSH
135
135
  @probs[@count] = nil
@@ -159,7 +159,7 @@ module Omnizip
159
159
  # @param out_size [Integer] Output buffer size
160
160
  # @return [Boolean] True if output buffer filled before encoding complete
161
161
  def encode(out, out_pos, out_size)
162
- raise "Symbol buffer overflow" if @count > RC_SYMBOLS_MAX
162
+ raise Omnizip::CompressionError, "Symbol buffer overflow" if @count > RC_SYMBOLS_MAX
163
163
 
164
164
  skip_increment = false
165
165
 
@@ -214,7 +214,7 @@ module Omnizip
214
214
  break
215
215
 
216
216
  else
217
- raise "Unknown symbol type: #{@symbols[@pos]}"
217
+ raise Omnizip::CompressionError, "Unknown symbol type: #{@symbols[@pos]}"
218
218
  end
219
219
 
220
220
  @pos += 1 unless skip_increment
@@ -288,7 +288,7 @@ module Omnizip
288
288
 
289
289
  # Forget pending symbols (matches XZ Utils rc_forget)
290
290
  def forget
291
- raise "Cannot forget while encoding" if @pos != 0
291
+ raise Omnizip::CompressionError, "Cannot forget while encoding" if @pos != 0
292
292
 
293
293
  @count = 0
294
294
  end
@@ -318,7 +318,7 @@ check_rc_finished: true)
318
318
 
319
319
  # Handle nil @pos or limit gracefully
320
320
  if limit && (@pos.nil? || limit.nil?)
321
- raise "Invalid state: @pos=#{@pos.inspect}, limit=#{limit.inspect}"
321
+ raise Omnizip::DecompressionError, "Invalid state: @pos=#{@pos.inspect}, limit=#{limit.inspect}"
322
322
  end
323
323
 
324
324
  # Circular buffer: flush output before it can be overwritten.
@@ -757,7 +757,7 @@ check_rc_finished: true)
757
757
  def read_header
758
758
  # Property byte
759
759
  props = @input.getbyte
760
- raise "Invalid LZMA header" if props.nil?
760
+ raise Omnizip::DecompressionError, "Invalid LZMA header" if props.nil?
761
761
 
762
762
  @lc = props % 9
763
763
  remainder = props / 9
@@ -768,7 +768,7 @@ check_rc_finished: true)
768
768
  @dict_size = 0
769
769
  4.times do |i|
770
770
  byte = @input.getbyte
771
- raise "Incomplete header" if byte.nil?
771
+ raise Omnizip::DecompressionError, "Incomplete header" if byte.nil?
772
772
 
773
773
  @dict_size |= (byte << (i * 8))
774
774
  end
@@ -780,7 +780,7 @@ check_rc_finished: true)
780
780
  @uncompressed_size = 0
781
781
  8.times do |i|
782
782
  byte = @input.getbyte
783
- raise "Incomplete header" if byte.nil?
783
+ raise Omnizip::DecompressionError, "Incomplete header" if byte.nil?
784
784
 
785
785
  @uncompressed_size |= (byte << (i * 8))
786
786
  end
@@ -791,9 +791,9 @@ check_rc_finished: true)
791
791
  # @return [void]
792
792
  # @raise [RuntimeError] If parameters are invalid
793
793
  def validate_parameters
794
- raise "Invalid lc (#{@lc})" unless @lc.between?(0, 8)
795
- raise "Invalid lp (#{@lp})" unless @lp.between?(0, 4)
796
- raise "Invalid pb (#{@pb})" unless @pb.between?(0, 4)
794
+ raise Omnizip::DecompressionError, "Invalid lc (#{@lc})" unless @lc.between?(0, 8)
795
+ raise Omnizip::DecompressionError, "Invalid lp (#{@lp})" unless @lp.between?(0, 4)
796
+ raise Omnizip::DecompressionError, "Invalid pb (#{@pb})" unless @pb.between?(0, 4)
797
797
  end
798
798
 
799
799
  # Initialize probability models
@@ -1201,7 +1201,7 @@ check_rc_finished: true)
1201
1201
  # @dict_full is clamped at @dict_size. Use actual bytes written for validation.
1202
1202
  actual_bytes_written = @pos - LZ_DICT_INIT_POS
1203
1203
  unless actual_bytes_written > distance
1204
- raise "Invalid rep distance: #{distance} (bytes_written: #{actual_bytes_written})"
1204
+ raise Omnizip::DecompressionError, "Invalid rep distance: #{distance} (bytes_written: #{actual_bytes_written})"
1205
1205
  end
1206
1206
 
1207
1207
  # IMPORTANT: Limit match length to not exceed uncompressed_size
@@ -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
@@ -159,18 +160,28 @@ module Omnizip
159
160
  Omnizip::Buffer.detect_format(@buffer)
160
161
  end
161
162
 
162
- # Extract all entries from ZIP
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
+ end
172
+ end
173
+
174
+ # Extract all entries from ZIP through the native reader
163
175
  #
164
176
  # @param result [Hash] Hash to populate with entries
165
177
  def extract_all_zip(result)
166
- @buffer.rewind
167
- Omnizip::Zip::InputStream.open(@buffer) do |zis|
168
- while (entry = zis.get_next_entry)
178
+ with_zip_reader do |reader|
179
+ reader.entries.each do |entry|
169
180
  next if entry.directory?
170
181
 
171
- content = zis.read
172
- result[entry.name] = content
173
- @extracted_cache[entry.name] = content
182
+ content = reader.read_entry(entry.filename)
183
+ result[entry.filename] = content
184
+ @extracted_cache[entry.filename] = content
174
185
  end
175
186
  end
176
187
  end
@@ -199,36 +210,23 @@ module Omnizip
199
210
  end
200
211
  end
201
212
 
202
- # Extract single entry from ZIP
213
+ # Extract single entry from ZIP through the native reader
203
214
  #
204
215
  # @param name [String] Entry name
205
216
  # @return [String, nil] Entry content or nil if not found
206
217
  def extract_entry_zip(name)
207
- @buffer.rewind
208
- content = nil
209
-
210
- Omnizip::Zip::InputStream.open(@buffer) do |zis|
211
- while (entry = zis.get_next_entry)
212
- if entry.name == name
213
- content = zis.read unless entry.directory?
214
- break
215
- end
216
- 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?
217
221
  end
218
-
219
- content
220
222
  end
221
223
 
222
- # List all entry names from ZIP
224
+ # List all entry names from ZIP through the native reader
223
225
  #
224
226
  # @param names [Array] Array to populate with names
225
227
  def list_entries_zip(names)
226
- @buffer.rewind
227
-
228
- Omnizip::Zip::InputStream.open(@buffer) do |zis|
229
- while (entry = zis.get_next_entry)
230
- names << entry.name
231
- end
228
+ with_zip_reader do |reader|
229
+ reader.entries.each { |entry| names << entry.filename }
232
230
  end
233
231
  end
234
232
  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
  # In-memory archive operations without filesystem I/O
@@ -150,27 +151,33 @@ module Omnizip
150
151
 
151
152
  private
152
153
 
153
- # Create ZIP archive in buffer
154
+ # Create ZIP archive in buffer through the native writer
154
155
  #
155
156
  # @param buffer [StringIO] Buffer to write to
156
157
  # @param options [Hash] ZIP-specific options
157
158
  # @yield [archive] Block to populate archive
158
159
  def create_zip(buffer, _options, &block)
159
- Omnizip::Zip::OutputStream.open(buffer) do |zos|
160
- archive = Buffer::MemoryArchive.new(zos, :zip)
161
- block&.call(archive)
162
- 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)
163
164
  end
164
165
 
165
- # 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).
166
169
  #
167
170
  # @param buffer [StringIO] Buffer containing ZIP data
168
171
  # @yield [archive] Block to read from archive
169
172
  # @return [MemoryArchive, Object] Archive or block return value
170
173
  def open_zip(buffer, &block)
171
174
  result = nil
172
- Omnizip::Zip::InputStream.open(buffer) do |zis|
173
- 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)
174
181
  result = block ? yield(archive) : archive
175
182
  end
176
183
  result