omnizip 0.3.56 → 0.3.58
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 +4 -4
- data/CHANGELOG.md +37 -0
- data/docs/developer/architecture.adoc +2 -2
- data/docs/getting-started/quick-start.adoc +1 -3
- data/docs/getting-started/your-first-archive.adoc +9 -8
- data/docs/guides/creating-archives.adoc +17 -23
- data/lib/omnizip/cli.rb +0 -2
- data/lib/omnizip/commands/archive_repair_command.rb +1 -1
- data/lib/omnizip/commands/archive_verify_command.rb +1 -1
- data/lib/omnizip/commands/decompress_command.rb +4 -1
- data/lib/omnizip/entry.rb +1 -1
- data/lib/omnizip/formats/cpio/entry.rb +3 -3
- data/lib/omnizip/formats/rar/decompressor.rb +2 -2
- data/lib/omnizip/formats/rar/external_writer.rb +2 -2
- data/lib/omnizip/formats/rar/header.rb +4 -4
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_splitter.rb +2 -2
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +4 -4
- data/lib/omnizip/formats/rar/rar5/writer.rb +5 -0
- data/lib/omnizip/formats/rar/recovery_record.rb +1 -1
- data/lib/omnizip/formats/tar/header.rb +43 -2
- data/lib/omnizip/formats/zip/writer.rb +40 -0
- data/lib/omnizip/parity/par2_creator.rb +1 -1
- data/lib/omnizip/parity/par2_repairer.rb +1 -1
- data/lib/omnizip/parity/par2cmdline_algorithm.rb +1 -1
- data/lib/omnizip/parity/reed_solomon_decoder.rb +1 -1
- data/lib/omnizip/parity/reed_solomon_matrix.rb +2 -2
- data/lib/omnizip/profile.rb +4 -4
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.rb +1 -1
- data/lib/omnizip/zip/output_stream.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04bb0d1c81ef7f4571014bb6679ad0d4750d4c078729daea82c88f643e246818
|
|
4
|
+
data.tar.gz: 7523e6a77a43db5eb02ec7c16ef4adf0751150129f329af82eb11297f93a68f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da1c027f6e44246e6421760d3197501db38957aeb1541c0db6e3553ab73b5e03a221a362b0165d4196bf1185f0022b341529321cdce9c2a339c3cf2f31b4f733
|
|
7
|
+
data.tar.gz: 7732ed6d8268d006195f7648980e2f8619ea89d45767932afd587f508592f627697a9cc436f6f3e1a01b41a8a579034f654c1bc33cc3331fced28af5bd68b8ff
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.57] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- TAR archives no longer corrupt silently on long paths: the writer
|
|
14
|
+
now splits entry names over 99 bytes across the ustar prefix +
|
|
15
|
+
name fields at a "/" boundary, the reader joins them back for
|
|
16
|
+
consumers, and paths no ustar fields can represent raise
|
|
17
|
+
`FormatError` instead of truncating. Verified against system tar.
|
|
18
|
+
- `:auto` profile sampling walked directories in the OS's
|
|
19
|
+
iteration order, resolving different files (and compression
|
|
20
|
+
settings) per platform; the walk is sorted and deterministic now.
|
|
21
|
+
- The getting-started docs' 7z examples used a Reader API that
|
|
22
|
+
never shipped (`each_file`, `close`, `Reader.info`) — every
|
|
23
|
+
snippet raised NoMethodError verbatim. Rewritten against the real
|
|
24
|
+
surface and executed to verify.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- The last 28 bare `raise "string"` sites (RAR tree, cpio entries,
|
|
28
|
+
parity internals, zip compat streams, two commands) gained their
|
|
29
|
+
matching error classes — `lib/` now contains zero bare raises.
|
|
30
|
+
- The dead `--preserve-ntfs-streams` CLI flag is removed; nothing
|
|
31
|
+
read it and no code path implements NTFS stream preservation.
|
|
32
|
+
|
|
33
|
+
## [0.3.56] - 2026-09-02
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- `FilterRegistry#entry_for` read its storage and lazy triggers
|
|
37
|
+
outside the mutex `register` writes under — a latent data race in
|
|
38
|
+
the 7z codec chain's filter resolution. Reads now hold the lock,
|
|
39
|
+
with the lazy trigger fired outside it (it re-enters through
|
|
40
|
+
register's own synchronize).
|
|
41
|
+
- The progress lifecycle is complete: `ProgressTracker` gains
|
|
42
|
+
`#start` and `#finish`, firing the reporter start/finish hooks
|
|
43
|
+
every reporter class overrides but nothing ever called —
|
|
44
|
+
"operation started/finished" output could never appear. `#finish`
|
|
45
|
+
also forces a final report and is idempotent.
|
|
46
|
+
|
|
10
47
|
## [0.3.55] - 2026-09-01
|
|
11
48
|
|
|
12
49
|
### Changed
|
|
@@ -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.
|
|
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
|
-
|
|
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: #{
|
|
186
|
-
puts " Size: #{
|
|
187
|
-
puts " Compressed: #{
|
|
188
|
-
puts " Ratio: #{(
|
|
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.
|
|
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
|
|
@@ -76,7 +76,7 @@ writer.write
|
|
|
76
76
|
|
|
77
77
|
IMPORTANT: RAR5 CRC32 checksums only work with STORE compression. When LZMA is used, CRC32 is automatically disabled. Use BLAKE2sp (always present) for compressed file integrity.
|
|
78
78
|
|
|
79
|
-
=== RAR5 Multi-Volume Archives
|
|
79
|
+
=== RAR5 Multi-Volume Archives
|
|
80
80
|
|
|
81
81
|
Split large archives across multiple volumes:
|
|
82
82
|
|
|
@@ -110,36 +110,30 @@ volumes = writer.write
|
|
|
110
110
|
# Minimum volume size: 64 KB
|
|
111
111
|
----
|
|
112
112
|
|
|
113
|
-
=== RAR5 Solid
|
|
113
|
+
=== RAR5 Solid Archives
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
`solid: true` requests a shared solid block, but the RAR-compatible
|
|
116
|
+
LZSS encoder is not implemented yet — the writer warns and writes
|
|
117
|
+
independent STORE entries instead, so the archive stays extractable
|
|
118
|
+
by official unrar:
|
|
116
119
|
|
|
117
120
|
[source,ruby]
|
|
118
121
|
----
|
|
119
122
|
require 'omnizip/formats/rar/rar5/writer'
|
|
120
123
|
|
|
121
|
-
# Enable solid compression
|
|
122
124
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('project.rar',
|
|
123
|
-
|
|
124
|
-
level: 5,
|
|
125
|
-
solid: true # Shared dictionary across files
|
|
125
|
+
solid: true # warns: falls back to independent STORE entries
|
|
126
126
|
)
|
|
127
127
|
writer.add_directory('source_code/')
|
|
128
128
|
writer.write
|
|
129
|
+
----
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# - Text documents
|
|
134
|
-
# - Similar structured data
|
|
135
|
-
|
|
136
|
-
# Trade-offs:
|
|
137
|
-
# - Cannot extract individual files without decompressing entire block
|
|
138
|
-
# - Corruption may affect multiple files
|
|
139
|
-
# - Slightly longer extraction time for single files
|
|
131
|
+
Solid compression (shared dictionary across files, better ratios
|
|
132
|
+
for similar content) arrives when the RAR-compatible LZSS encoder
|
|
133
|
+
lands.
|
|
140
134
|
----
|
|
141
135
|
|
|
142
|
-
=== RAR5 AES-256 Encryption
|
|
136
|
+
=== RAR5 AES-256 Encryption
|
|
143
137
|
|
|
144
138
|
Password-protect archives with industry-standard encryption:
|
|
145
139
|
|
|
@@ -177,7 +171,7 @@ writer.write
|
|
|
177
171
|
# - Password verification before decryption
|
|
178
172
|
----
|
|
179
173
|
|
|
180
|
-
=== RAR5 PAR2 Recovery Records
|
|
174
|
+
=== RAR5 PAR2 Recovery Records
|
|
181
175
|
|
|
182
176
|
Add error correction for archive recovery:
|
|
183
177
|
|
|
@@ -216,7 +210,7 @@ files = writer.write
|
|
|
216
210
|
# - Works with multi-volume, solid, and encrypted archives
|
|
217
211
|
----
|
|
218
212
|
|
|
219
|
-
=== RAR5 Combined Features
|
|
213
|
+
=== RAR5 Combined Features
|
|
220
214
|
|
|
221
215
|
Use all features together for comprehensive protection:
|
|
222
216
|
|
|
@@ -262,13 +256,13 @@ puts " Total size: #{files.sum { |f| File.size(f) } / 1024 / 1024}MB"
|
|
|
262
256
|
|
|
263
257
|
=== RAR5 Best Practices
|
|
264
258
|
|
|
265
|
-
**For Maximum
|
|
259
|
+
**For Maximum Compatibility:**
|
|
266
260
|
[source,ruby]
|
|
267
261
|
----
|
|
268
262
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
269
263
|
compression: :lzma,
|
|
270
|
-
level: 5, #
|
|
271
|
-
|
|
264
|
+
level: 5, # Honored by the 7z-style writers; RAR5 entries
|
|
265
|
+
# are STORE until the LZSS encoder lands
|
|
272
266
|
include_mtime: true
|
|
273
267
|
)
|
|
274
268
|
----
|
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
|
|
@@ -80,7 +80,10 @@ module Omnizip
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
# Archive-vs-stream routing derives from the registry's
|
|
84
|
+
# extension routes so new formats route here automatically
|
|
85
|
+
ARCHIVE_EXTENSIONS = (Convenience::ARCHIVE_FORMAT_EXTENSIONS.keys +
|
|
86
|
+
Convenience::READ_ARCHIVE_FORMAT_EXTENSIONS.keys).freeze
|
|
84
87
|
|
|
85
88
|
private
|
|
86
89
|
|
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,
|
|
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.
|
|
@@ -199,6 +199,11 @@ module Omnizip
|
|
|
199
199
|
if @options[:solid] && Compression::Lzss.available?
|
|
200
200
|
write_solid_block(f, @files)
|
|
201
201
|
else
|
|
202
|
+
if @options[:solid]
|
|
203
|
+
warn "omnizip: solid: true requested but the " \
|
|
204
|
+
"RAR-compatible LZSS encoder is not available " \
|
|
205
|
+
"— writing independent STORE entries instead"
|
|
206
|
+
end
|
|
202
207
|
@files.each do |file|
|
|
203
208
|
write_file_entry(f, file)
|
|
204
209
|
end
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
@@ -152,6 +152,8 @@ module Omnizip
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
def write_precompressed_to_io(io, compression_method: COMPRESSION_DEFLATE)
|
|
155
|
+
check_classic_entry_count!
|
|
156
|
+
entries.each { |entry| check_classic_size_limit!(entry) }
|
|
155
157
|
local_header_offsets = []
|
|
156
158
|
|
|
157
159
|
entries.each do |entry|
|
|
@@ -189,6 +191,7 @@ module Omnizip
|
|
|
189
191
|
|
|
190
192
|
# Write to an IO object
|
|
191
193
|
def write_to_io(io, compression_method: COMPRESSION_DEFLATE, level: 6)
|
|
194
|
+
check_classic_entry_count!
|
|
192
195
|
local_header_offsets = []
|
|
193
196
|
|
|
194
197
|
# Write local file headers and data. An entry-level
|
|
@@ -220,6 +223,8 @@ module Omnizip
|
|
|
220
223
|
entry[:crc32] = calculate_crc32(entry[:uncompressed_data])
|
|
221
224
|
end
|
|
222
225
|
|
|
226
|
+
check_classic_size_limit!(entry)
|
|
227
|
+
|
|
223
228
|
# Update local header with compressed sizes
|
|
224
229
|
local_header.compressed_size = entry[:compressed_size]
|
|
225
230
|
local_header.uncompressed_size = entry[:uncompressed_size]
|
|
@@ -234,6 +239,7 @@ module Omnizip
|
|
|
234
239
|
|
|
235
240
|
# Record start of central directory
|
|
236
241
|
central_directory_offset = io.pos
|
|
242
|
+
check_classic_offset_limit!(central_directory_offset)
|
|
237
243
|
|
|
238
244
|
# Write central directory headers
|
|
239
245
|
entries.each_with_index do |entry, index|
|
|
@@ -257,8 +263,42 @@ module Omnizip
|
|
|
257
263
|
io.write(eocd.to_binary)
|
|
258
264
|
end
|
|
259
265
|
|
|
266
|
+
# Classic (non-ZIP64) ZIP headers pack sizes and offsets as
|
|
267
|
+
# 32-bit values and entry counts as 16-bit; exceeding them
|
|
268
|
+
# raises instead of letting Array#pack fail with a bare
|
|
269
|
+
# RangeError. ZIP64 writing is not supported.
|
|
270
|
+
MAX_CLASSIC_SIZE = 0xFFFFFFFF
|
|
271
|
+
MAX_CLASSIC_ENTRIES = 0xFFFF
|
|
272
|
+
private_constant :MAX_CLASSIC_SIZE, :MAX_CLASSIC_ENTRIES
|
|
273
|
+
|
|
260
274
|
private
|
|
261
275
|
|
|
276
|
+
def check_classic_entry_count!
|
|
277
|
+
return if entries.size <= MAX_CLASSIC_ENTRIES
|
|
278
|
+
|
|
279
|
+
raise Omnizip::UnsupportedFormatError,
|
|
280
|
+
"#{entries.size} entries exceed the classic ZIP " \
|
|
281
|
+
"limit of #{MAX_CLASSIC_ENTRIES} (ZIP64 writing is " \
|
|
282
|
+
"not supported)"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def check_classic_size_limit!(entry)
|
|
286
|
+
return if entry[:compressed_size].to_i <= MAX_CLASSIC_SIZE &&
|
|
287
|
+
entry[:uncompressed_size].to_i <= MAX_CLASSIC_SIZE
|
|
288
|
+
|
|
289
|
+
raise Omnizip::UnsupportedFormatError,
|
|
290
|
+
"entry #{entry[:filename].inspect} exceeds the 4 GiB " \
|
|
291
|
+
"classic ZIP size limit (ZIP64 writing is not supported)"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def check_classic_offset_limit!(offset)
|
|
295
|
+
return if offset <= MAX_CLASSIC_SIZE
|
|
296
|
+
|
|
297
|
+
raise Omnizip::UnsupportedFormatError,
|
|
298
|
+
"archive layout exceeds the 4 GiB classic ZIP " \
|
|
299
|
+
"offset limit (ZIP64 writing is not supported)"
|
|
300
|
+
end
|
|
301
|
+
|
|
262
302
|
# Create an entry hash
|
|
263
303
|
def create_entry(
|
|
264
304
|
filename:,
|
|
@@ -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
|
data/lib/omnizip/profile.rb
CHANGED
|
@@ -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.
|
|
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
|
|
data/lib/omnizip/version.rb
CHANGED
data/lib/omnizip/zip/file.rb
CHANGED
|
@@ -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,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omnizip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.58
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|