omnizip 0.3.56 → 0.3.57

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a34ac63f49299330b590a1f9010940af988984f875f7ea101f44c126d90f5d6
4
- data.tar.gz: 5a7940049434d24a4f61d073db07133b6645133dd79783462e864856e3302d24
3
+ metadata.gz: 1ea5d1a7c799ac0f1b17c60e1c715fc0bf95a2d8dc3d3a02e0776bac6efbccf0
4
+ data.tar.gz: 198583bbdc571acdd26a8a1e25f4a6ce0e5aa6b33d613c247bf49556d164200c
5
5
  SHA512:
6
- metadata.gz: 8ad6baae78797134776df430810dc67b5fae438d702aa4613d02468fae496134d1e53d2d4c8c857ee51311fe1a72de463529ae0dc553bf9fc8a792be9a3a0718
7
- data.tar.gz: 385b3cec020df42f2532944a287467bd63cb155212278b0164c2e61e7db5c262e2858a306c4cccfaf949dbd0f61b5c2a013a72586166fbfca0a4323a5e46b65a
6
+ metadata.gz: edaf17357204a214beecdd32e18bd53139bb6a7b9797b29a181a44db80b531e847f6d3e48da97f6ce5f02e80feeb456c71e593df92b471b4d5875a8d13ae1e98
7
+ data.tar.gz: 6f82822274ad086eef57304f2386a43f979525dd6893f0eabf693b6c8c60a08a71729beb5c2a3328c8617871bbbc4b9d33a2b6801ae801eeb3614a8f4cbd6d4e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.56] - 2026-09-02
11
+
12
+ ### Fixed
13
+ - `FilterRegistry#entry_for` read its storage and lazy triggers
14
+ outside the mutex `register` writes under — a latent data race in
15
+ the 7z codec chain's filter resolution. Reads now hold the lock,
16
+ with the lazy trigger fired outside it (it re-enters through
17
+ register's own synchronize).
18
+ - The progress lifecycle is complete: `ProgressTracker` gains
19
+ `#start` and `#finish`, firing the reporter start/finish hooks
20
+ every reporter class overrides but nothing ever called —
21
+ "operation started/finished" output could never appear. `#finish`
22
+ also forces a final report and is idempotent.
23
+
10
24
  ## [0.3.55] - 2026-09-01
11
25
 
12
26
  ### Changed
@@ -120,8 +120,8 @@ module Omnizip
120
120
  # Open archive
121
121
  end
122
122
 
123
- def each_file
124
- # Yield archive entries
123
+ def entries
124
+ # Enumerate archive entries
125
125
  end
126
126
 
127
127
  def extract_all(output_dir)
@@ -103,11 +103,9 @@ omnizip archive list backup.7z
103
103
  require 'omnizip'
104
104
 
105
105
  reader = Omnizip::Formats::SevenZip::Reader.new('backup.7z')
106
- reader.open
107
- reader.each_file do |entry|
106
+ reader.entries.each do |entry|
108
107
  puts "#{entry.name}: #{entry.size} bytes"
109
108
  end
110
- reader.close
111
109
  ----
112
110
 
113
111
  === Using Different Compression Algorithms
@@ -180,12 +180,15 @@ writer.add_directory('/home/user/photos')
180
180
  writer.close
181
181
 
182
182
  # Get statistics
183
- info = Omnizip::Formats::SevenZip::Reader.info('backup.7z')
183
+ entries = Omnizip.list_archive('backup.7z', details: true)
184
+ files = entries.reject { |e| e[:directory] }
185
+ uncompressed = files.sum { |e| e[:size] }
186
+ compressed = File.size('backup.7z')
184
187
  puts "Archive created:"
185
- puts " Files: #{info.entry_count}"
186
- puts " Size: #{info.uncompressed_size} bytes"
187
- puts " Compressed: #{info.compressed_size} bytes"
188
- puts " Ratio: #{(info.compressed_size.to_f / info.uncompressed_size * 100).round(1)}%"
188
+ puts " Files: #{files.size}"
189
+ puts " Size: #{uncompressed} bytes"
190
+ puts " Compressed: #{compressed} bytes"
191
+ puts " Ratio: #{(compressed.to_f / uncompressed * 100).round(1)}%"
189
192
  ----
190
193
 
191
194
  === Verifying Your Archive
@@ -202,12 +205,10 @@ omnizip archive list backup.7z
202
205
  require 'omnizip'
203
206
 
204
207
  reader = Omnizip::Formats::SevenZip::Reader.new('backup.7z')
205
- reader.open
206
208
  puts "Archive contents:"
207
- reader.each_file do |entry|
209
+ reader.entries.each do |entry|
208
210
  puts " #{entry.name} (#{entry.size} bytes)"
209
211
  end
210
- reader.close
211
212
  ----
212
213
 
213
214
  === Next Steps
data/lib/omnizip/cli.rb CHANGED
@@ -182,8 +182,6 @@ module Omnizip
182
182
  desc: "Password for encryption (7z: header encryption, RAR5: AES-256-CBC)"
183
183
  option :encrypt_headers, type: :boolean, default: false,
184
184
  desc: "Encrypt archive headers (7z only, hides filenames)"
185
- option :preserve_ntfs_streams, type: :boolean, default: false,
186
- desc: "Preserve NTFS alternate data streams (Windows only, 7z only)"
187
185
  option :rar_version, type: :numeric, default: 5,
188
186
  desc: "RAR version (4 or 5, default: 5 for pure Ruby)"
189
187
  option :rar_compression, type: :string, default: "store",
@@ -18,7 +18,7 @@ module Omnizip
18
18
  # @param input_path [String] Path to corrupted archive
19
19
  # @param output_path [String] Path for repaired archive
20
20
  def run(input_path, output_path)
21
- raise "Archive not found: #{input_path}" unless File.exist?(input_path)
21
+ raise Omnizip::IOError, "Archive not found: #{input_path}" unless File.exist?(input_path)
22
22
 
23
23
  # Detect format
24
24
  format = detect_format(input_path)
@@ -18,7 +18,7 @@ module Omnizip
18
18
  # @param archive_path [String] Path to archive
19
19
  def run(archive_path)
20
20
  unless File.exist?(archive_path)
21
- raise "Archive not found: #{archive_path}"
21
+ raise Omnizip::IOError, "Archive not found: #{archive_path}"
22
22
  end
23
23
 
24
24
  # Detect format
data/lib/omnizip/entry.rb CHANGED
@@ -7,7 +7,7 @@ module Omnizip
7
7
  # Each format-specific entry class (Zip::Entry, Tar::Entry,
8
8
  # SevenZip::Models::FileEntry, Iso::DirectoryRecord, Ole::Dirent,
9
9
  # Xar::Entry, Rpm::Entry, Msi::Entry, Cpio::Entry, Rar3::Entry,
10
- # Rar5::Entry, Xz::Entry, Metadata::EntryMetadata) includes this
10
+ # Rar5::Entry, Metadata::EntryMetadata) includes this
11
11
  # module so callers can rely on a single narrow interface instead
12
12
  # of duck-typing through `respond_to?(:name) / :path / :filename`
13
13
  # cascades.
@@ -232,7 +232,7 @@ module Omnizip
232
232
  when :odc
233
233
  parse_odc(io)
234
234
  else
235
- raise "Unknown CPIO format"
235
+ raise Omnizip::InvalidArchiveError, "Unknown CPIO format"
236
236
  end
237
237
  end
238
238
 
@@ -258,7 +258,7 @@ module Omnizip
258
258
  # @return [Entry] Parsed entry
259
259
  def self.parse_newc(io)
260
260
  header = io.read(NEWC_HEADER_SIZE)
261
- raise "Truncated CPIO header" unless header && header.bytesize == NEWC_HEADER_SIZE
261
+ raise Omnizip::InvalidArchiveError, "Truncated CPIO header" unless header && header.bytesize == NEWC_HEADER_SIZE
262
262
 
263
263
  # Parse ASCII hex fields
264
264
  magic = header[0, 6]
@@ -318,7 +318,7 @@ module Omnizip
318
318
  # @return [Entry] Parsed entry
319
319
  def self.parse_odc(io)
320
320
  header = io.read(76)
321
- raise "Truncated CPIO header" unless header && header.bytesize == 76
321
+ raise Omnizip::InvalidArchiveError, "Truncated CPIO header" unless header && header.bytesize == 76
322
322
 
323
323
  # Parse octal fields
324
324
  magic = header[0, 6]
@@ -232,7 +232,7 @@ module Omnizip
232
232
  archive.extract_to_file(entry_name, output_path,
233
233
  password: password)
234
234
  rescue StandardError => e
235
- raise "Gem entry extraction failed: #{e.message}"
235
+ raise Omnizip::DecompressionError, "Gem entry extraction failed: #{e.message}"
236
236
  end
237
237
 
238
238
  # Extract entry with command
@@ -255,7 +255,7 @@ module Omnizip
255
255
  cmd = build_extract_command(archive_path, cached_dir, password)
256
256
  unless system(*cmd)
257
257
  FileUtils.rm_rf(cached_dir)
258
- raise "Command entry extraction failed: #{entry_name}"
258
+ raise Omnizip::DecompressionError, "Command entry extraction failed: #{entry_name}"
259
259
  end
260
260
  extract_cache[cache_key] = cached_dir
261
261
  install_exit_sweeper
@@ -138,7 +138,7 @@ module Omnizip
138
138
  stdout, stderr, status = Open3.capture3(*cmd)
139
139
 
140
140
  unless status.success?
141
- raise "RAR creation failed: #{stderr}\n#{stdout}"
141
+ raise Omnizip::CompressionError, "RAR creation failed: #{stderr}\n#{stdout}"
142
142
  end
143
143
 
144
144
  # Test archive if requested
@@ -257,7 +257,7 @@ module Omnizip
257
257
 
258
258
  return if status.success?
259
259
 
260
- raise "Archive test failed: #{stderr}\n#{stdout}"
260
+ raise Omnizip::CompressionError, "Archive test failed: #{stderr}\n#{stdout}"
261
261
  end
262
262
 
263
263
  # Get RAR executable path
@@ -52,10 +52,10 @@ module Omnizip
52
52
  @version = 5
53
53
  parse_rar5_header(io)
54
54
  else
55
- raise "Invalid RAR signature: #{(sig_bytes + (extra_byte&.bytes || [])).inspect}"
55
+ raise Omnizip::InvalidArchiveError, "Invalid RAR signature: #{(sig_bytes + (extra_byte&.bytes || [])).inspect}"
56
56
  end
57
57
  else
58
- raise "Invalid RAR signature: #{sig_bytes.inspect}"
58
+ raise Omnizip::InvalidArchiveError, "Invalid RAR signature: #{sig_bytes.inspect}"
59
59
  end
60
60
  end
61
61
 
@@ -103,7 +103,7 @@ module Omnizip
103
103
  end
104
104
 
105
105
  unless head_type == BLOCK_ARCHIVE
106
- raise "Expected archive block, got 0x#{head_type.to_s(16)}"
106
+ raise Omnizip::InvalidArchiveError, "Expected archive block, got 0x#{head_type.to_s(16)}"
107
107
  end
108
108
 
109
109
  @flags = head_flags
@@ -140,7 +140,7 @@ module Omnizip
140
140
  end
141
141
 
142
142
  unless header_type == RAR5_HEADER_MAIN
143
- raise "Expected main header, got #{header_type}"
143
+ raise Omnizip::InvalidArchiveError, "Expected main header, got #{header_type}"
144
144
  end
145
145
 
146
146
  @flags = header_flags
@@ -77,8 +77,8 @@ module Omnizip
77
77
  # @raise [RuntimeError] if no volume is active
78
78
  # @raise [RuntimeError] if data doesn't fit
79
79
  def write_to_current_volume(data)
80
- raise "No active volume" if @current_volume_number.zero?
81
- raise "Data doesn't fit in current volume" unless can_fit_in_current_volume?(data.bytesize)
80
+ raise Omnizip::CompressionError, "No active volume" if @current_volume_number.zero?
81
+ raise Omnizip::CompressionError, "Data doesn't fit in current volume" unless can_fit_in_current_volume?(data.bytesize)
82
82
 
83
83
  @current_volume_data << data
84
84
  @current_volume_bytes += data.bytesize
@@ -80,7 +80,7 @@ module Omnizip
80
80
  #
81
81
  # @return [void]
82
82
  def write_signature
83
- raise "Volume not open" unless @io
83
+ raise Omnizip::IOError, "Volume not open" unless @io
84
84
 
85
85
  # RAR5 signature: "Rar!\x1A\x07\x01\x00"
86
86
  signature = [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01,
@@ -92,7 +92,7 @@ module Omnizip
92
92
  #
93
93
  # @return [void]
94
94
  def write_main_header
95
- raise "Volume not open" unless @io
95
+ raise Omnizip::IOError, "Volume not open" unless @io
96
96
 
97
97
  # Archive flags: every volume declares 0x0001; volumes
98
98
  # after the first also carry the volume number field
@@ -114,7 +114,7 @@ module Omnizip
114
114
  # @param compressed_data [String] Compressed file data
115
115
  # @return [void]
116
116
  def write_file_data(file_header, compressed_data)
117
- raise "Volume not open" unless @io
117
+ raise Omnizip::IOError, "Volume not open" unless @io
118
118
 
119
119
  @io.write(file_header.encode)
120
120
  @io.write(compressed_data)
@@ -124,7 +124,7 @@ module Omnizip
124
124
  #
125
125
  # @return [void]
126
126
  def write_end_header
127
- raise "Volume not open" unless @io
127
+ raise Omnizip::IOError, "Volume not open" unless @io
128
128
 
129
129
  # End-of-archive flags: 0x0001 marks a volume with more
130
130
  # volumes following; the last volume ends with 0.
@@ -229,7 +229,7 @@ module Omnizip
229
229
 
230
230
  loop do
231
231
  byte = io.read(1)&.unpack1("C")
232
- raise "Unexpected EOF" unless byte
232
+ raise Omnizip::InvalidArchiveError, "Unexpected EOF" unless byte
233
233
 
234
234
  result |= (byte & 0x7F) << shift
235
235
  break if byte.nobits?(0x80)
@@ -47,6 +47,11 @@ module Omnizip
47
47
  entry.prefix = extract_string(
48
48
  header_data, PREFIX_OFFSET, PREFIX_SIZE
49
49
  )
50
+ # ustar stores long paths split across prefix + name;
51
+ # consumers see the joined path
52
+ unless entry.prefix.empty?
53
+ entry.name = "#{entry.prefix}/#{entry.name}"
54
+ end
50
55
  end
51
56
 
52
57
  # Verify checksum
@@ -69,7 +74,8 @@ module Omnizip
69
74
  header = "\0" * HEADER_SIZE
70
75
 
71
76
  # Write fields to header
72
- write_string(header, entry.name, NAME_OFFSET, NAME_SIZE)
77
+ name, prefix = split_long_name(entry.name, entry.prefix)
78
+ write_string(header, name, NAME_OFFSET, NAME_SIZE)
73
79
  write_octal(header, entry.mode, MODE_OFFSET, MODE_SIZE)
74
80
  write_octal(header, entry.uid, UID_OFFSET, UID_SIZE)
75
81
  write_octal(header, entry.gid, GID_OFFSET, GID_SIZE)
@@ -87,7 +93,7 @@ module Omnizip
87
93
  write_string(header, entry.gname, GNAME_OFFSET, GNAME_SIZE)
88
94
  write_octal(header, entry.devmajor, DEVMAJOR_OFFSET, DEVMAJOR_SIZE)
89
95
  write_octal(header, entry.devminor, DEVMINOR_OFFSET, DEVMINOR_SIZE)
90
- write_string(header, entry.prefix, PREFIX_OFFSET, PREFIX_SIZE)
96
+ write_string(header, prefix, PREFIX_OFFSET, PREFIX_SIZE)
91
97
 
92
98
  # Calculate and write checksum
93
99
  checksum = Entry.calculate_checksum(header)
@@ -130,6 +136,41 @@ module Omnizip
130
136
  field.strip.to_i(8)
131
137
  end
132
138
 
139
+ # Split a path for the ustar name (100) + prefix (155)
140
+ # fields. Paths under 100 bytes pass through; longer paths
141
+ # split at a "/" boundary, preferring the longest prefix
142
+ # that keeps both halves in bounds. Raises when no ustar
143
+ # representation exists — silent truncation corrupts
144
+ # archives.
145
+ #
146
+ # @param path [String] Full entry path
147
+ # @param prefix [String] Explicit prefix (wins when given)
148
+ # @return [Array<String>] [name, prefix] pair
149
+ def self.split_long_name(path, prefix = nil)
150
+ path = path.to_s
151
+ prefix = prefix.to_s
152
+ return [path, prefix] if !prefix.empty? || path.bytesize < NAME_SIZE
153
+
154
+ best = nil
155
+ offset = -1
156
+ while (slash = path.index("/", offset + 1))
157
+ candidate_prefix = path[0...slash]
158
+ candidate_name = path[(slash + 1)..]
159
+ offset = slash
160
+
161
+ next if candidate_prefix.bytesize >= PREFIX_SIZE
162
+ next if candidate_name.bytesize >= NAME_SIZE
163
+
164
+ best = [candidate_name, candidate_prefix]
165
+ end
166
+
167
+ return best if best
168
+
169
+ raise Omnizip::FormatError,
170
+ "Path too long for the ustar name+prefix fields " \
171
+ "(no PAX/GNU long-name support): #{path}"
172
+ end
173
+
133
174
  # Write a string to header
134
175
  #
135
176
  # @param header [String] Header data
@@ -151,7 +151,7 @@ progress: nil)
151
151
  #
152
152
  # @raise [StandardError] if no files added
153
153
  def validate_files!
154
- raise "No files added to PAR2 set" if @files.empty?
154
+ raise ArgumentError, "No files added to PAR2 set" if @files.empty?
155
155
  end
156
156
 
157
157
  # Generate unique set ID for this PAR2 set
@@ -244,7 +244,7 @@ module Omnizip
244
244
 
245
245
  # Ensure metadata is loaded
246
246
  if file_list.nil? || file_list.empty?
247
- raise "Internal error: file_list not populated in verifier"
247
+ raise Omnizip::Error, "Internal error: file_list not populated in verifier"
248
248
  end
249
249
 
250
250
  block_idx = 0
@@ -35,7 +35,7 @@ module Omnizip
35
35
  # Find next logbase where gcd(65535, logbase) == 1
36
36
  logbase += 1 while gcd(limit, logbase) != 1
37
37
 
38
- raise "Too many input blocks" if logbase >= limit
38
+ raise ArgumentError, "Too many input blocks" if logbase >= limit
39
39
 
40
40
  # Use antilog to convert logbase to base value
41
41
  # This is the key: base = antilog[logbase]
@@ -109,7 +109,7 @@ recovery_exponents, bases)
109
109
  num_rows.times do |pivot_row|
110
110
  # Get pivot value from right matrix diagonal
111
111
  pivot = right_matrix[pivot_row][pivot_row]
112
- raise "Singular matrix at row #{pivot_row}" if pivot.zero?
112
+ raise Omnizip::Error, "Singular matrix at row #{pivot_row}" if pivot.zero?
113
113
 
114
114
  # Scale pivot row to make pivot = 1
115
115
  unless pivot == 1
@@ -117,7 +117,7 @@ total_inputs, block_size)
117
117
  # @param recovery_idx [Integer] Recovery block index (0..recovery_count-1)
118
118
  # @return [Integer] Galois field coefficient
119
119
  def coefficient(output_idx, recovery_idx)
120
- raise "Matrix not computed - call compute! first" unless @matrix
120
+ raise Omnizip::Error, "Matrix not computed - call compute! first" unless @matrix
121
121
 
122
122
  # After transpose, indices are swapped: @matrix[recovery_idx][output_idx]
123
123
  @matrix[recovery_idx][output_idx]
@@ -200,7 +200,7 @@ output_offset: 0)
200
200
 
201
201
  num_rows.times do |pivot_row|
202
202
  pivot = left_matrix[pivot_row][pivot_row]
203
- raise "Singular matrix at row #{pivot_row}" if pivot.zero?
203
+ raise Omnizip::Error, "Singular matrix at row #{pivot_row}" if pivot.zero?
204
204
 
205
205
  # Scale pivot row to make pivot = 1
206
206
  unless pivot == 1
@@ -181,11 +181,11 @@ module Omnizip
181
181
 
182
182
  private
183
183
 
184
- # Depth-first search for the first regular file under +dir+.
184
+ # Depth-first search for the first regular file under +dir+,
185
+ # in sorted child order — :auto sampling must not depend on
186
+ # the OS's directory iteration order.
185
187
  def first_file_within(dir)
186
- ::Dir.foreach(dir) do |entry|
187
- next if [".", ".."].include?(entry)
188
-
188
+ ::Dir.children(dir).sort.each do |entry|
189
189
  full_path = ::File.join(dir, entry)
190
190
  return full_path if ::File.file?(full_path)
191
191
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.56"
4
+ VERSION = "0.3.57"
5
5
  end
@@ -130,7 +130,7 @@ module Omnizip
130
130
  action = yield(entry, dest_path)
131
131
  return if action == false
132
132
  else
133
- raise "Destination file already exists: #{dest_path}"
133
+ raise Errno::EEXIST, "Destination file already exists: #{dest_path}"
134
134
  end
135
135
  end
136
136
 
@@ -75,7 +75,7 @@ compression: :deflate, level: 6)
75
75
  # Write data to current entry
76
76
  # @param data [String] Data to write
77
77
  def write(data)
78
- raise "No entry started. Call put_next_entry first" unless @current_entry
78
+ raise Omnizip::IOError, "No entry started. Call put_next_entry first" unless @current_entry
79
79
 
80
80
  @current_entry_data << data.b
81
81
  self
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.56
4
+ version: 0.3.57
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.