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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fractor"
4
3
  require "fileutils"
5
4
 
6
5
  module Omnizip
@@ -21,101 +20,6 @@ module Omnizip
21
20
  # extractor = Omnizip::Parallel::ParallelExtractor.new(options)
22
21
  # extractor.extract('backup.zip', 'output/')
23
22
  class ParallelExtractor
24
- # Fractor Work class for extraction jobs
25
- class ExtractionWork < Fractor::Work
26
- def initialize(entry:, archive_path:, dest_dir:)
27
- super({
28
- entry: entry,
29
- archive_path: archive_path,
30
- dest_dir: dest_dir,
31
- })
32
- end
33
-
34
- def entry
35
- input[:entry]
36
- end
37
-
38
- def archive_path
39
- input[:archive_path]
40
- end
41
-
42
- def dest_dir
43
- input[:dest_dir]
44
- end
45
- end
46
-
47
- # Fractor Worker class for extraction
48
- class ExtractionWorker < Fractor::Worker
49
- def process(work)
50
- entry = work.entry
51
- archive_path = work.archive_path
52
- dest_dir = work.dest_dir
53
-
54
- # Read and decompress entry data
55
- data = read_entry_data(archive_path, entry)
56
-
57
- # Determine destination path
58
- dest_path = ::File.join(dest_dir, entry.name)
59
-
60
- # Return result
61
- Fractor::WorkResult.new(
62
- result: {
63
- entry_name: entry.name,
64
- dest_path: dest_path,
65
- data: data,
66
- directory: entry.directory?,
67
- unix_perms: safe_unix_perms(entry),
68
- },
69
- work: work,
70
- )
71
- rescue StandardError => e
72
- Fractor::WorkResult.new(
73
- error: e,
74
- work: work,
75
- )
76
- end
77
-
78
- private
79
-
80
- def read_entry_data(archive_path, entry)
81
- return "" if entry.directory?
82
-
83
- # Open archive and extract entry
84
- reader = Omnizip::Formats::Zip::Reader.new(archive_path)
85
- reader.read
86
-
87
- ::File.open(archive_path, "rb") do |io|
88
- # Find the entry in reader
89
- reader_entry = reader.entries.find { |e| e.filename == entry.name }
90
- raise Errno::ENOENT, "Entry not found in archive: #{entry.name}" unless reader_entry
91
-
92
- # Seek to entry data
93
- io.seek(reader_entry.local_header_offset, ::IO::SEEK_SET)
94
-
95
- # Read and parse local file header
96
- fixed_header = io.read(30)
97
- return "" unless fixed_header && fixed_header.size == 30
98
-
99
- _signature, _version, _flags, _method, _time, _date, _crc32,
100
- _comp_size, _uncomp_size, filename_length, extra_length = fixed_header.unpack("VvvvvvVVVvv")
101
-
102
- # Skip filename and extra field
103
- io.read(filename_length + extra_length)
104
-
105
- # Read compressed data
106
- compressed_data = io.read(reader_entry.compressed_size)
107
- return "" unless compressed_data
108
-
109
- # Decompress
110
- reader.decompress(
111
- compressed_data,
112
- reader_entry.compression_method,
113
- reader_entry.uncompressed_size,
114
- )
115
- end
116
- end
117
- end
118
-
119
23
  # @return [Omnizip::Models::ParallelOptions] parallel options
120
24
  attr_reader :options
121
25
 
@@ -179,41 +83,41 @@ module Omnizip
179
83
 
180
84
  # Schedule jobs
181
85
  entries.each do |entry|
182
- file_size = safe_entry_size(entry)
183
-
184
86
  job_queue.push_with_size(
185
- file: entry.name,
186
- size: file_size,
187
- data: { entry: entry },
87
+ file: entry.filename,
88
+ size: safe_entry_size(entry),
89
+ data: { entry_name: entry.filename },
188
90
  )
189
91
  end
190
92
 
191
- # Create work items from jobs
93
+ # Create work items from jobs, largest first
192
94
  work_items = []
193
95
  until job_queue.empty?
194
96
  job = job_queue.pop(timeout: 0.1)
195
97
  break unless job
196
98
 
197
- work_items << ExtractionWork.new(
198
- entry: job.data[:entry],
199
- archive_path: archive,
200
- dest_dir: dest,
201
- )
99
+ work_items << job.data[:entry_name]
100
+ end
101
+
102
+ # Threads, not Ractors: Omnizip's autoloads and Proc-holding
103
+ # codec constants cannot cross Ractor boundaries on Ruby <= 3.3
104
+ pool = ThreadPool.new(size: @options.threads)
105
+ raw_results, raw_errors = pool.map(work_items) do |entry_name|
106
+ process_entry(archive, dest, entry_name)
202
107
  end
203
108
 
204
- # Run the worker pool through the shared engine.
205
- engine = Engine.new(worker_class: ExtractionWorker,
206
- threads: @options.threads)
207
- results = []
208
- errors = engine.run(work_items) { |r| results << r }
209
-
210
- # Handle errors
211
- unless errors.empty?
212
- error_msgs = errors.map do |e|
213
- "#{e.work&.entry&.name}: #{e.error}"
214
- end.join("\n")
215
- raise Omnizip::ExtractionError, "Extraction errors:\n#{error_msgs}"
109
+ # Handle errors (raw_errors holds nil for every item that
110
+ # succeeded — zip+compact would keep those pairs)
111
+ failure_pairs = raw_errors.each_with_index.filter_map do |error, index|
112
+ [work_items[index], error] if error
216
113
  end
114
+ unless failure_pairs.empty?
115
+ error_msgs = failure_pairs.map { |name, e| "#{name}: #{e.message}" }
116
+ .join("\n")
117
+ raise Omnizip::DecompressionError, "Extraction errors:\n#{error_msgs}"
118
+ end
119
+
120
+ results = raw_results
217
121
 
218
122
  # Write files to disk (thread-safe)
219
123
  extracted_paths = write_extracted_files(results, overwrite: overwrite)
@@ -242,32 +146,37 @@ module Omnizip
242
146
 
243
147
  private
244
148
 
245
- def safe_unix_perms(entry)
246
- entry.unix_perms
247
- rescue NoMethodError
248
- 0
149
+ # Decompress one entry in a worker thread. Each worker opens
150
+ # the archive independently — its own file handle per thread.
151
+ def process_entry(archive_path, dest_dir, entry_name)
152
+ entry = Omnizip::Formats::Zip::Reader.new(archive_path)
153
+ .entries.find { |e| e.filename == entry_name }
154
+ raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
155
+
156
+ {
157
+ entry_name: entry_name,
158
+ dest_path: ::File.join(dest_dir, entry_name),
159
+ data: entry.directory? ? "" : read_entry_data(archive_path, entry_name),
160
+ directory: entry.directory?,
161
+ unix_perms: entry.unix_permissions.to_i,
162
+ }
163
+ end
164
+
165
+ def read_entry_data(archive_path, entry_name)
166
+ Omnizip::Formats::Zip::Reader.new(archive_path)
167
+ .read_entry(entry_name)
249
168
  end
250
169
 
251
170
  def safe_entry_size(entry)
252
- entry.size
253
- rescue NoMethodError
254
- 0
171
+ entry.uncompressed_size.to_i
255
172
  end
256
173
 
257
- # Read archive entries
174
+ # Read archive entries through the native reader
258
175
  #
259
176
  # @param archive_path [String] archive path
260
- # @return [Array<Entry>] array of entries
177
+ # @return [Array<CentralDirectoryHeader>] array of entries
261
178
  def read_archive_entries(archive_path)
262
- entries = []
263
-
264
- Omnizip::Zip::File.open(archive_path) do |zip|
265
- zip.each do |entry|
266
- entries << entry
267
- end
268
- end
269
-
270
- entries
179
+ Omnizip::Formats::Zip::Reader.new(archive_path).entries
271
180
  end
272
181
 
273
182
  # Write extracted files to disk
@@ -278,34 +187,36 @@ module Omnizip
278
187
  def write_extracted_files(results, overwrite: false)
279
188
  extracted_paths = []
280
189
 
281
- results.each do |work_result|
282
- result = work_result.result
190
+ results.each do |result|
283
191
  next unless result
284
192
 
285
193
  dest_path = result[:dest_path]
286
194
 
287
195
  # Thread-safe file writing
288
196
  @write_mutex.synchronize do
197
+ if result[:directory]
198
+ # mkdir_p is idempotent — a file entry may already have
199
+ # created this directory as its parent
200
+ FileUtils.mkdir_p(dest_path)
201
+ extracted_paths << dest_path
202
+ next
203
+ end
204
+
289
205
  # Check if file exists
290
206
  if ::File.exist?(dest_path) && !overwrite
291
207
  raise Errno::EEXIST, "File exists: #{dest_path}"
292
208
  end
293
209
 
294
- # Write file or create directory
295
- if result[:directory]
296
- FileUtils.mkdir_p(dest_path)
297
- else
298
- FileUtils.mkdir_p(::File.dirname(dest_path))
299
- ::File.binwrite(dest_path, result[:data])
210
+ FileUtils.mkdir_p(::File.dirname(dest_path))
211
+ ::File.binwrite(dest_path, result[:data])
300
212
 
301
- # Set permissions if Unix
302
- if result[:unix_perms].positive?
303
- ::File.chmod(result[:unix_perms] & 0o777, dest_path)
304
- end
305
-
306
- @stats[:bytes_extracted] += result[:data].bytesize
213
+ # Set permissions if Unix
214
+ if result[:unix_perms].positive?
215
+ ::File.chmod(result[:unix_perms] & 0o777, dest_path)
307
216
  end
308
217
 
218
+ @stats[:bytes_extracted] += result[:data].bytesize
219
+
309
220
  extracted_paths << dest_path
310
221
  end
311
222
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string: true
2
+
3
+ module Omnizip
4
+ module Parallel
5
+ # Bounded thread pool for library work.
6
+ #
7
+ # Unlike the Fractor pool, threads share the VM state — Ractor
8
+ # workers cannot trigger Omnizip's autoloads or touch Proc-holding
9
+ # codec constants on Ruby <= 3.3 ("require by autoload on
10
+ # non-main Ractor is not supported"). Decompression itself still
11
+ # parallelizes: the zlib/bzip2 C extensions release the GVL.
12
+ class ThreadPool
13
+ # @return [Integer] number of worker threads
14
+ attr_reader :size
15
+
16
+ # Initialize the pool
17
+ #
18
+ # @param size [Integer] worker thread count (at least 1)
19
+ def initialize(size:)
20
+ @size = [size.to_i, 1].max
21
+ end
22
+
23
+ # Run +block+ for every item, at most +size+ at a time, and
24
+ # return a pair of parallel arrays: results (nil where an item
25
+ # raised) and errors (nil where it succeeded). Item order is
26
+ # preserved in both arrays.
27
+ #
28
+ # @param items [Array] work items
29
+ # @yieldparam item [Object] one work item
30
+ # @return [Array<Array, Array>]
31
+ def map(items)
32
+ results = Array.new(items.size)
33
+ errors = Array.new(items.size)
34
+ queue = Queue.new
35
+ items.each_with_index { |item, index| queue << [item, index] }
36
+
37
+ threads = Array.new(@size) do
38
+ Thread.new do
39
+ loop do
40
+ begin
41
+ item, index = queue.pop(true)
42
+ rescue ThreadError
43
+ break
44
+ end
45
+
46
+ begin
47
+ results[index] = yield(item)
48
+ rescue StandardError => e
49
+ errors[index] = e
50
+ end
51
+ end
52
+ end
53
+ end
54
+ threads.each(&:join)
55
+
56
+ [results, errors]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -132,8 +132,12 @@ module Omnizip
132
132
  def shutdown(timeout: 30)
133
133
  return unless @running
134
134
 
135
+ # Stop in BOTH modes: batch worker Ractors otherwise linger
136
+ # in Ractor.receive for the life of the process, burning CPU
137
+ # after the batch completes
138
+ @supervisor&.stop
139
+
135
140
  if @continuous
136
- @supervisor.stop
137
141
  @supervisor_thread&.join(timeout)
138
142
  end
139
143
 
@@ -29,6 +29,7 @@ module Omnizip
29
29
  autoload :Engine, "omnizip/parallel/engine"
30
30
  autoload :ParallelCompressor, "omnizip/parallel/parallel_compressor"
31
31
  autoload :ParallelExtractor, "omnizip/parallel/parallel_extractor"
32
+ autoload :ThreadPool, "omnizip/parallel/thread_pool"
32
33
 
33
34
  class << self
34
35
  # Global configuration
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.53"
4
+ VERSION = "0.3.55"
5
5
  end
data/lib/omnizip.rb CHANGED
@@ -85,6 +85,9 @@ module Omnizip
85
85
  autoload :ArchiveHandlers, "omnizip/archive_handlers"
86
86
  autoload :Archive, "omnizip/archive"
87
87
  autoload :Password, "omnizip/password"
88
+ # The class lives at the top level of omnizip/password.rb; without
89
+ # this entry, referencing it does not trigger the Password autoload
90
+ autoload :PasswordError, "omnizip/password"
88
91
  autoload :Convenience, "omnizip/convenience"
89
92
  end
90
93
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.53
4
+ version: 0.3.55
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -650,6 +650,7 @@ files:
650
650
  - lib/omnizip/parallel/job_scheduler.rb
651
651
  - lib/omnizip/parallel/parallel_compressor.rb
652
652
  - lib/omnizip/parallel/parallel_extractor.rb
653
+ - lib/omnizip/parallel/thread_pool.rb
653
654
  - lib/omnizip/parallel/worker_pool.rb
654
655
  - lib/omnizip/parity.rb
655
656
  - lib/omnizip/parity/chunked_block_processor.rb