omnizip 0.3.42 → 0.3.43
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 +20 -0
- data/CONTEXT.md +5 -1
- data/lib/omnizip/formats/rar/reader.rb +20 -2
- data/lib/omnizip/formats/seven_zip/reader.rb +41 -3
- data/lib/omnizip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 436437db6697f7363305eda4e6a3e2b78b922aa12b3c43e29756efe187310b60
|
|
4
|
+
data.tar.gz: 6e0b17e143c50beae378cdb5b7902e773d888bca14183fcf83e61dfeabe56a5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1e20ae301093cacb8c582e1b554a678c72cefa6bf21095d129bebc14b8c4095a68a87da653ef5ea16cb65c00de9edd6ae7b0d92191e49c25e05438ceba264ab
|
|
7
|
+
data.tar.gz: a6ad938ab6bdfd75586bcef013ba056cc523681ef34449d03bc44daf18a591e6485b292b7c62bd17c72c13c20c7ff8c45caf4faf6735d5cff79a0c2f4a5a010b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.42] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `Formats::Zip::Reader` is usable immediately: entries parse on
|
|
14
|
+
first use, so `extract_all`/listing no longer silently do nothing
|
|
15
|
+
when `#read` was not called first. `#read` remains for eager
|
|
16
|
+
loading.
|
|
17
|
+
- `Temp::ArchiveHelper` writes zips through the native
|
|
18
|
+
`Formats::Zip::Writer` instead of the rubyzip-compat
|
|
19
|
+
`Omnizip::Zip::File` seam (the Metadata subsystem remains on
|
|
20
|
+
compat deliberately as an in-place central-directory editor).
|
|
21
|
+
- The RAR extraction spill cache installs an `at_exit` sweeper on
|
|
22
|
+
first use, reclaiming spill directories at normal process exit.
|
|
23
|
+
- Module docs tell the truth: `Formats::Rar` claimed read-only with
|
|
24
|
+
external tools required; `Formats::Iso` claimed read-only while
|
|
25
|
+
carrying the verified writer. New `CONTEXT.md` records the domain
|
|
26
|
+
glossary (primary parser vs adapters, native vs external decode,
|
|
27
|
+
interop-verified vs Omnizip-internal codecs, handler routes, the
|
|
28
|
+
compat seam, bridges).
|
|
29
|
+
|
|
10
30
|
## [0.3.41] - 2026-08-29
|
|
11
31
|
|
|
12
32
|
### 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
|
|
@@ -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
|
|
@@ -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
|
data/lib/omnizip/version.rb
CHANGED