omnizip 0.3.42 → 0.3.44
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 +34 -0
- data/CONTEXT.md +5 -1
- data/lib/omnizip/cli.rb +11 -0
- data/lib/omnizip/formats/rar/reader.rb +20 -2
- data/lib/omnizip/formats/rar.rb +2 -2
- data/lib/omnizip/formats/seven_zip/reader.rb +47 -3
- data/lib/omnizip/formats/seven_zip/writer.rb +27 -19
- data/lib/omnizip/version.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: 358c03758aa086cbb4f3a22845af9f309542df3c272fc640faaaf807d3b2adb0
|
|
4
|
+
data.tar.gz: 96c8735854c4413b2cb802611eb7acd2d4480d1983b5dd4e4e4a45ebf35d3725
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67ab99068276c2eef687be2d7497c911cdb96f920dea552b9392fe860cbc805345c8bf87f5f6640934b09d46b7fc3c598c8bb140a296f37395d2020ff9e0bbd6
|
|
7
|
+
data.tar.gz: 256599715a8f9aa94ee2e94376122a6571b8a7eb0e80548b74e57f14d2d2522a12fc6f3b5566f08c3b6a63692b06e3845a3d8948dfdf24fc53f22d79eebd434d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.43] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Reader invariant, library-wide: `Formats::SevenZip::Reader` and
|
|
14
|
+
`Formats::Rar::Reader` parse on first use, so `list_files`/
|
|
15
|
+
`extract_entry`/`extract_all` no longer silently operate on empty
|
|
16
|
+
entries when `#open` was not called first. "A reader is always
|
|
17
|
+
usable" is recorded in `CONTEXT.md`; `#open` remains for eager
|
|
18
|
+
loading. New specs pin the behavior for both readers.
|
|
19
|
+
- 7z entries in single-stream folders report their exact
|
|
20
|
+
`compressed_size` (folder pack sizes were parsed but never mapped
|
|
21
|
+
onto entries); solid folders share packed bytes across streams, so
|
|
22
|
+
those entries keep nil rather than a fabricated split.
|
|
23
|
+
|
|
24
|
+
## [0.3.42] - 2026-08-29
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `Formats::Zip::Reader` is usable immediately: entries parse on
|
|
28
|
+
first use, so `extract_all`/listing no longer silently do nothing
|
|
29
|
+
when `#read` was not called first. `#read` remains for eager
|
|
30
|
+
loading.
|
|
31
|
+
- `Temp::ArchiveHelper` writes zips through the native
|
|
32
|
+
`Formats::Zip::Writer` instead of the rubyzip-compat
|
|
33
|
+
`Omnizip::Zip::File` seam (the Metadata subsystem remains on
|
|
34
|
+
compat deliberately as an in-place central-directory editor).
|
|
35
|
+
- The RAR extraction spill cache installs an `at_exit` sweeper on
|
|
36
|
+
first use, reclaiming spill directories at normal process exit.
|
|
37
|
+
- Module docs tell the truth: `Formats::Rar` claimed read-only with
|
|
38
|
+
external tools required; `Formats::Iso` claimed read-only while
|
|
39
|
+
carrying the verified writer. New `CONTEXT.md` records the domain
|
|
40
|
+
glossary (primary parser vs adapters, native vs external decode,
|
|
41
|
+
interop-verified vs Omnizip-internal codecs, handler routes, the
|
|
42
|
+
compat seam, bridges).
|
|
43
|
+
|
|
10
44
|
## [0.3.41] - 2026-08-29
|
|
11
45
|
|
|
12
46
|
### Changed
|
data/CONTEXT.md
CHANGED
|
@@ -19,7 +19,11 @@ here.
|
|
|
19
19
|
extract/list/read. `.rar`, `.cpio`, `.iso` are read-only routes;
|
|
20
20
|
creating them raises truthfully.
|
|
21
21
|
|
|
22
|
-
## Reading and
|
|
22
|
+
## Reading and decode
|
|
23
|
+
|
|
24
|
+
- **Reader invariant — always usable** — every Reader
|
|
25
|
+
(`Formats::Zip`, `Formats::SevenZip`, `Formats::Rar`) parses on
|
|
26
|
+
first use; calling `#open` first is never required, only eager.
|
|
23
27
|
|
|
24
28
|
- **Native decode** — decompression implemented in Ruby inside
|
|
25
29
|
omnizip. RAR native decode is **CRC-gated**: the stored header CRC
|
data/lib/omnizip/cli.rb
CHANGED
|
@@ -344,6 +344,17 @@ module Omnizip
|
|
|
344
344
|
rescue StandardError => e
|
|
345
345
|
handle_error(e)
|
|
346
346
|
end
|
|
347
|
+
|
|
348
|
+
desc "verify ARCHIVE", "Verify archive integrity"
|
|
349
|
+
long_desc <<~DESC
|
|
350
|
+
Verify the integrity of an archive (currently RAR archives via
|
|
351
|
+
header and recovery-record checks).
|
|
352
|
+
DESC
|
|
353
|
+
def verify(archive)
|
|
354
|
+
Omnizip::Commands::ArchiveVerifyCommand.new(options).run(archive)
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
handle_error(e)
|
|
357
|
+
end
|
|
347
358
|
end
|
|
348
359
|
|
|
349
360
|
# Command-line interface for Omnizip.
|
|
@@ -12,7 +12,7 @@ module Omnizip
|
|
|
12
12
|
class Reader
|
|
13
13
|
include Omnizip::Formats::Rar::Constants
|
|
14
14
|
|
|
15
|
-
attr_reader :file_path, :header, :
|
|
15
|
+
attr_reader :file_path, :header, :archive_info,
|
|
16
16
|
:volume_manager
|
|
17
17
|
|
|
18
18
|
# Initialize reader with file path
|
|
@@ -25,6 +25,7 @@ module Omnizip
|
|
|
25
25
|
@archive_info = Models::RarArchive.new(file_path)
|
|
26
26
|
@volume_manager = VolumeManager.new(file_path)
|
|
27
27
|
@use_native = true # Prefer native decompression
|
|
28
|
+
@opened = false
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Open and parse RAR archive
|
|
@@ -34,14 +35,24 @@ module Omnizip
|
|
|
34
35
|
File.open(@file_path, "rb") do |io|
|
|
35
36
|
parse_archive(io)
|
|
36
37
|
end
|
|
38
|
+
@opened = true
|
|
37
39
|
self
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# All entries, parsing on first use — a Reader is always
|
|
43
|
+
# usable; #open remains for eager loading
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<Models::RarEntry>] File entries
|
|
46
|
+
def entries
|
|
47
|
+
ensure_open
|
|
48
|
+
@entries
|
|
49
|
+
end
|
|
50
|
+
|
|
40
51
|
# List all files in archive
|
|
41
52
|
#
|
|
42
53
|
# @return [Array<Models::RarEntry>] File entries
|
|
43
54
|
def list_files
|
|
44
|
-
|
|
55
|
+
entries
|
|
45
56
|
end
|
|
46
57
|
|
|
47
58
|
# Extract file to output path
|
|
@@ -51,6 +62,7 @@ module Omnizip
|
|
|
51
62
|
# @param password [String, nil] Optional password
|
|
52
63
|
# @raise [RuntimeError] if entry not found or extraction fails
|
|
53
64
|
def extract_entry(entry_name, output_path, password: nil)
|
|
65
|
+
ensure_open
|
|
54
66
|
entry = @entries.find { |e| e.name == entry_name }
|
|
55
67
|
raise "Entry not found: #{entry_name}" unless entry
|
|
56
68
|
|
|
@@ -79,6 +91,7 @@ module Omnizip
|
|
|
79
91
|
# @param password [String, nil] Optional password
|
|
80
92
|
# @raise [RuntimeError] on extraction error
|
|
81
93
|
def extract_all(output_dir, password: nil)
|
|
94
|
+
ensure_open
|
|
82
95
|
FileUtils.mkdir_p(output_dir)
|
|
83
96
|
|
|
84
97
|
# Use decompressor to extract all
|
|
@@ -126,6 +139,11 @@ module Omnizip
|
|
|
126
139
|
|
|
127
140
|
private
|
|
128
141
|
|
|
142
|
+
# Parse the archive once, on demand
|
|
143
|
+
def ensure_open
|
|
144
|
+
open unless @opened
|
|
145
|
+
end
|
|
146
|
+
|
|
129
147
|
# Parse RAR archive structure
|
|
130
148
|
#
|
|
131
149
|
# @param io [IO] Input stream
|
data/lib/omnizip/formats/rar.rb
CHANGED
|
@@ -165,8 +165,8 @@ module Omnizip
|
|
|
165
165
|
#
|
|
166
166
|
# @param archive_path [String] Path to RAR archive
|
|
167
167
|
# @return [ArchiveVerifier::VerificationResult] Verification result
|
|
168
|
-
def verify(archive_path)
|
|
169
|
-
ArchiveVerifier.new(archive_path).verify
|
|
168
|
+
def verify(archive_path, use_recovery: true)
|
|
169
|
+
ArchiveVerifier.new(archive_path).verify(use_recovery: use_recovery)
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
# Repair corrupted archive
|
|
@@ -28,6 +28,7 @@ module Omnizip
|
|
|
28
28
|
@offset = options[:offset] || 0
|
|
29
29
|
@solid_cache = {}
|
|
30
30
|
@solid_cache_extracted = {}
|
|
31
|
+
@opened = false
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# Open and parse .7z archive
|
|
@@ -79,10 +80,12 @@ module Omnizip
|
|
|
79
80
|
@split_reader&.volumes || [@file_path]
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
# List all files in archive
|
|
83
|
+
# List all files in archive, parsing on first use — a Reader
|
|
84
|
+
# is always usable; #open remains for eager loading
|
|
83
85
|
#
|
|
84
86
|
# @return [Array<Models::FileEntry>] File entries
|
|
85
87
|
def list_files
|
|
88
|
+
ensure_open
|
|
86
89
|
@entries.reject { |e| e.name.nil? || e.name.empty? }
|
|
87
90
|
end
|
|
88
91
|
|
|
@@ -92,6 +95,7 @@ module Omnizip
|
|
|
92
95
|
# @param output_path [String] Destination path
|
|
93
96
|
# @raise [RuntimeError] if entry not found or extraction fails
|
|
94
97
|
def extract_entry(entry_name, output_path)
|
|
98
|
+
ensure_open
|
|
95
99
|
# Delegate to split reader if available
|
|
96
100
|
if @split_reader
|
|
97
101
|
@split_reader.extract_entry(entry_name, output_path)
|
|
@@ -132,6 +136,7 @@ module Omnizip
|
|
|
132
136
|
# @param output_dir [String] Destination directory
|
|
133
137
|
# @raise [RuntimeError] on extraction error
|
|
134
138
|
def extract_all(output_dir)
|
|
139
|
+
ensure_open
|
|
135
140
|
FileUtils.mkdir_p(output_dir)
|
|
136
141
|
|
|
137
142
|
real_out = File.realpath(output_dir)
|
|
@@ -187,6 +192,11 @@ module Omnizip
|
|
|
187
192
|
|
|
188
193
|
private
|
|
189
194
|
|
|
195
|
+
# Parse the archive once, on demand
|
|
196
|
+
def ensure_open
|
|
197
|
+
open unless @opened
|
|
198
|
+
end
|
|
199
|
+
|
|
190
200
|
# Parse .7z archive structure
|
|
191
201
|
#
|
|
192
202
|
# @param io [io] Input stream
|
|
@@ -236,7 +246,16 @@ module Omnizip
|
|
|
236
246
|
|
|
237
247
|
# Parse metadata
|
|
238
248
|
parser = Parser.new(next_header_data)
|
|
239
|
-
|
|
249
|
+
begin
|
|
250
|
+
@stream_info, @entries = parse_metadata(parser)
|
|
251
|
+
rescue StandardError => e
|
|
252
|
+
if @headers_decrypted
|
|
253
|
+
raise "Failed to decrypt headers: incorrect password " \
|
|
254
|
+
"or corrupted data (#{e.message})"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
raise
|
|
258
|
+
end
|
|
240
259
|
|
|
241
260
|
# Map entries to their folders/streams
|
|
242
261
|
map_entries_to_streams
|
|
@@ -257,11 +276,16 @@ module Omnizip
|
|
|
257
276
|
|
|
258
277
|
# Decrypt using password
|
|
259
278
|
encryptor = HeaderEncryptor.new(@password)
|
|
260
|
-
encryptor.decrypt(
|
|
279
|
+
decrypted = encryptor.decrypt(
|
|
261
280
|
@encrypted_header.encrypted_data,
|
|
262
281
|
@encrypted_header.salt,
|
|
263
282
|
@encrypted_header.iv,
|
|
264
283
|
)
|
|
284
|
+
# AES-CBC 'succeeds' with any key and yields garbage; wrong
|
|
285
|
+
# passwords surface as later header-parse failures, so mark
|
|
286
|
+
# the archive and translate those into the password error
|
|
287
|
+
@headers_decrypted = true
|
|
288
|
+
decrypted
|
|
265
289
|
rescue OpenSSL::Cipher::CipherError => e
|
|
266
290
|
raise "Failed to decrypt headers: incorrect password (#{e.message})"
|
|
267
291
|
end
|
|
@@ -393,6 +417,20 @@ module Omnizip
|
|
|
393
417
|
entry.folder_index = folder_idx
|
|
394
418
|
entry.file_index = i
|
|
395
419
|
entry.size = @stream_info.unpack_sizes[stream_idx] if @stream_info.unpack_sizes[stream_idx]
|
|
420
|
+
|
|
421
|
+
# Single-stream folders own their packed bytes outright,
|
|
422
|
+
# so the entry's compressed size is exact. Solid folders
|
|
423
|
+
# share packed bytes across streams — leave nil rather
|
|
424
|
+
# than fabricate a split.
|
|
425
|
+
num_in_folder = @stream_info.num_unpack_streams_in_folders[folder_idx]
|
|
426
|
+
if num_in_folder == 1
|
|
427
|
+
folder = @stream_info.folders[folder_idx]
|
|
428
|
+
packed = folder.pack_stream_indices.sum do |pi|
|
|
429
|
+
@stream_info.pack_sizes[pi] || 0
|
|
430
|
+
end
|
|
431
|
+
entry.compressed_size = packed
|
|
432
|
+
end
|
|
433
|
+
|
|
396
434
|
stream_idx += 1
|
|
397
435
|
end
|
|
398
436
|
end
|
|
@@ -420,6 +458,12 @@ module Omnizip
|
|
|
420
458
|
pack_idx += num_streams
|
|
421
459
|
end
|
|
422
460
|
|
|
461
|
+
# Pack streams are laid out sequentially; skip past every
|
|
462
|
+
# stream owned by earlier folders to reach this folder's
|
|
463
|
+
# bytes (with stored archives pack sizes equal unpack
|
|
464
|
+
# sizes, which masked this offset in the past)
|
|
465
|
+
pack_idx.times { |i| pack_pos += @stream_info.pack_sizes[i] || 0 }
|
|
466
|
+
|
|
423
467
|
# Check if this is a BCJ2 multi-stream folder
|
|
424
468
|
if Bcj2StreamDecompressor.bcj2_folder?(folder)
|
|
425
469
|
extract_bcj2_entry(io, entry, folder, pack_pos, pack_idx)
|
|
@@ -169,15 +169,20 @@ module Omnizip
|
|
|
169
169
|
[lzma2_chunk, [compressed_size]]
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
# Build packed data for non-solid mode
|
|
172
|
+
# Build packed data for non-solid mode: every file is its own
|
|
173
|
+
# LZMA2 stream. Empty files keep a zero-size pack stream.
|
|
173
174
|
def build_non_solid_packed_data(file_data)
|
|
174
|
-
|
|
175
|
-
packed_data = String.new(encoding: "BINARY")
|
|
175
|
+
packed_data = String.new(encoding: Encoding::BINARY)
|
|
176
176
|
packed_sizes = []
|
|
177
177
|
|
|
178
178
|
file_data[:streams].each do |stream|
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
if stream[:size].positive?
|
|
180
|
+
compressed = compress_with_lzma2(stream[:data])
|
|
181
|
+
packed_data << compressed
|
|
182
|
+
packed_sizes << compressed.bytesize
|
|
183
|
+
else
|
|
184
|
+
packed_sizes << 0
|
|
185
|
+
end
|
|
181
186
|
end
|
|
182
187
|
|
|
183
188
|
[packed_data, packed_sizes]
|
|
@@ -255,7 +260,8 @@ module Omnizip
|
|
|
255
260
|
num_files)
|
|
256
261
|
else
|
|
257
262
|
# Non-solid mode: one pack stream per file, one folder per file
|
|
258
|
-
build_non_solid_streams_info(metadata, file_data[:streams]
|
|
263
|
+
build_non_solid_streams_info(metadata, file_data[:streams],
|
|
264
|
+
packed_sizes)
|
|
259
265
|
end
|
|
260
266
|
|
|
261
267
|
# kEnd for MainStreamsInfo
|
|
@@ -375,7 +381,7 @@ _num_files)
|
|
|
375
381
|
metadata << [PropertyId::K_END].pack("C")
|
|
376
382
|
end
|
|
377
383
|
|
|
378
|
-
def build_non_solid_streams_info(metadata, streams)
|
|
384
|
+
def build_non_solid_streams_info(metadata, streams, packed_sizes)
|
|
379
385
|
num_streams = streams.size
|
|
380
386
|
|
|
381
387
|
# kPackInfo property (0x06)
|
|
@@ -383,10 +389,10 @@ _num_files)
|
|
|
383
389
|
metadata << write_number(0) # Pack position
|
|
384
390
|
metadata << write_number(num_streams) # Number of pack streams
|
|
385
391
|
|
|
386
|
-
# kSize property - sizes of each pack stream
|
|
392
|
+
# kSize property - sizes of each pack stream (compressed)
|
|
387
393
|
metadata << [PropertyId::SIZE].pack("C")
|
|
388
|
-
|
|
389
|
-
metadata << write_number(
|
|
394
|
+
packed_sizes.each do |packed|
|
|
395
|
+
metadata << write_number(packed)
|
|
390
396
|
end
|
|
391
397
|
|
|
392
398
|
# kEnd for PackInfo
|
|
@@ -402,18 +408,20 @@ _num_files)
|
|
|
402
408
|
# External flag for all folders
|
|
403
409
|
metadata << [0].pack("C")
|
|
404
410
|
|
|
405
|
-
# Folder definitions (coders only, no sizes yet)
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
# Folder definitions (coders only, no sizes yet):
|
|
412
|
+
# one LZMA2 coder per non-empty stream, COPY for empties
|
|
413
|
+
streams.each do |stream|
|
|
414
|
+
if stream[:size].positive?
|
|
415
|
+
build_folder_coder(metadata)
|
|
416
|
+
else
|
|
417
|
+
metadata << write_number(1)
|
|
418
|
+
metadata << COPY_MAIN_BYTE
|
|
419
|
+
metadata << COPY_METHOD_ID
|
|
420
|
+
end
|
|
414
421
|
end
|
|
415
422
|
|
|
416
423
|
# kCodersUnpackSize - comes after ALL folder definitions
|
|
424
|
+
# (original sizes; kSize above carries the packed sizes)
|
|
417
425
|
metadata << [PropertyId::CODERS_UNPACK_SIZE].pack("C")
|
|
418
426
|
streams.each do |stream|
|
|
419
427
|
metadata << write_number(stream[:size])
|
data/lib/omnizip/version.rb
CHANGED
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.44
|
|
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-08-
|
|
11
|
+
date: 2026-08-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|