omnizip 0.3.51 → 0.3.53
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 +56 -0
- data/lib/omnizip/archive_handlers/seven_zip_handler.rb +1 -1
- data/lib/omnizip/archive_handlers/zip_handler.rb +1 -1
- data/lib/omnizip/buffer/memory_extractor.rb +3 -16
- data/lib/omnizip/buffer/seven_zip_bridge.rb +13 -0
- data/lib/omnizip/buffer.rb +23 -21
- data/lib/omnizip/checksums/verifier.rb +2 -2
- data/lib/omnizip/chunked.rb +14 -21
- data/lib/omnizip/cli/output_formatter.rb +16 -8
- data/lib/omnizip/cli.rb +1 -7
- data/lib/omnizip/commands/archive_create_command.rb +1 -10
- data/lib/omnizip/commands/archive_extract_command.rb +4 -13
- data/lib/omnizip/commands/archive_list_command.rb +1 -18
- data/lib/omnizip/commands/metadata_command.rb +21 -26
- data/lib/omnizip/commands/parity_create_command.rb +1 -7
- data/lib/omnizip/extraction/selective_extractor.rb +16 -4
- data/lib/omnizip/formats/cpio/reader.rb +1 -1
- data/lib/omnizip/formats/iso/reader.rb +3 -3
- data/lib/omnizip/formats/iso/volume_descriptor.rb +2 -2
- data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +0 -20
- data/lib/omnizip/formats/rar/decompressor.rb +7 -7
- data/lib/omnizip/formats/rar/reader.rb +2 -2
- data/lib/omnizip/formats/rpm.rb +4 -4
- data/lib/omnizip/formats/seven_zip/reader.rb +1 -1
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +1 -1
- data/lib/omnizip/formats/xar/reader.rb +2 -2
- data/lib/omnizip/formats/zip/reader.rb +96 -8
- data/lib/omnizip/models/conversion_result.rb +76 -70
- data/lib/omnizip/models/filter_config.rb +34 -44
- data/lib/omnizip/models/match_result.rb +35 -18
- data/lib/omnizip/models/optimization_suggestion.rb +45 -41
- data/lib/omnizip/models/performance_result.rb +53 -47
- data/lib/omnizip/models/profile_report.rb +70 -36
- data/lib/omnizip/parallel/parallel_extractor.rb +2 -2
- data/lib/omnizip/parallel/worker_pool.rb +3 -3
- data/lib/omnizip/parity/par2_creator.rb +0 -34
- data/lib/omnizip/parity/par2_verifier.rb +5 -0
- data/lib/omnizip/profiler/report_generator.rb +2 -13
- data/lib/omnizip/profiler.rb +1 -12
- data/lib/omnizip/temp/safe_extract.rb +3 -43
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.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: a7e72a57eaeb6e597e40020de67aa0242da65c6e6e8ceaa933d779d926e55666
|
|
4
|
+
data.tar.gz: 105cf4f238610f518462c115d398403fef5daca9b2994788361b027c7c5ec648
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5301b14047d1150885624527d03176c61a979464b7a16329a47449e13fe05679c1e5ce11a650bfd29101aaaf502b5ab81513558fc080dca85554266ae30f8dc
|
|
7
|
+
data.tar.gz: a44ffd04cc02fa03e2b23a330da4f6612d268bfd350d9b9672e42d7a107dcad65201ed9707651b3533fdd790a5841a5ff9297d6f3a8e937b51e0f1614d756c55
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.52] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The extract command's pattern path opens archives through the
|
|
14
|
+
Archive facade instead of a hand-rolled extension `case` that
|
|
15
|
+
misrouted `.cpio`/`.iso`/`.rpm` files into the 7z reader — pattern
|
|
16
|
+
extraction now works for every routed format. The metadata
|
|
17
|
+
command's 7z view lists through the handler registry (the detail
|
|
18
|
+
contract gains `:crc`), and SafeExtract extracts through
|
|
19
|
+
`extract_archive`, so every routed format gets rollback and
|
|
20
|
+
verification instead of ZIP-only with a `NotImplementedError`
|
|
21
|
+
for 7z. SelectiveExtractor understands both entry contracts
|
|
22
|
+
(legacy `entry_name` and reader-session `name`) and skips
|
|
23
|
+
directory entries when extracting.
|
|
24
|
+
- `CliOutputFormatter.format_size` is now the single byte formatter;
|
|
25
|
+
the nine private `format_bytes`/`format_size` copies (four
|
|
26
|
+
different shapes, two unit ceilings) become delegators.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- The long-deferred parity spec flake: `Par2Verifier` crashed five
|
|
30
|
+
frames deep on a nil PAR2 path (`File.exist?(nil)` → TypeError)
|
|
31
|
+
where the repairer already raised a clean ArgumentError; the
|
|
32
|
+
verifier now mirrors it, and the spec's `.verify` group gains the
|
|
33
|
+
nil-index fallback `.repair` already had. The dead shadowed
|
|
34
|
+
`analyze_file` definition in `Par2Creator` (silently overridden
|
|
35
|
+
by a second definition with a divergent file_id format) is
|
|
36
|
+
deleted. The tautological "LZMA2 is implicit" xz assertion became
|
|
37
|
+
a real block-header parse asserting the explicit 0x21 filter ID.
|
|
38
|
+
|
|
39
|
+
## [0.3.51] - 2026-09-01
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Profile resolution lives in one place: `Omnizip::Profile` gains
|
|
43
|
+
`resolve`, `apply`, and `first_file_among`, and the convenience
|
|
44
|
+
methods plus the `archive create` command collapse onto them. The
|
|
45
|
+
two prior copies had drifted — the command only accepted `"auto"`
|
|
46
|
+
strings and could not pass a `CompressionProfile` through.
|
|
47
|
+
- Both direct converter strategies (ZIP↔7z) now extract through
|
|
48
|
+
`Omnizip.extract_archive` and write through a shared
|
|
49
|
+
`ConversionStrategy#repack_tree` on the Archive facade — they were
|
|
50
|
+
the last library code instantiating format readers/writers
|
|
51
|
+
directly, and the 7z→ZIP path required `#open` against the
|
|
52
|
+
reader-always-usable invariant.
|
|
53
|
+
- `create_rar` routes through the RAR handler (unrar-verified) with
|
|
54
|
+
the generic writer block interface; its stale "requires RAR
|
|
55
|
+
license" docstring is gone. `decompress_file`'s three-arm case
|
|
56
|
+
with magic `:xz`/`:zstandard` markers becomes a
|
|
57
|
+
`SINGLE_FILE_DECOMPRESSORS` table mirroring the compressor table.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- macOS CI failed at setup from 2026-09-01: Homebrew disabled the
|
|
61
|
+
`rar` cask (Gatekeeper check), breaking `brew install rar`. The
|
|
62
|
+
workflow now installs it separately and non-fatally, like the
|
|
63
|
+
Windows branch already did for winrar; RAR interop specs skip
|
|
64
|
+
when unrar is absent.
|
|
65
|
+
|
|
10
66
|
## [0.3.50] - 2026-08-31
|
|
11
67
|
|
|
12
68
|
### Added
|
|
@@ -38,7 +38,7 @@ module Omnizip
|
|
|
38
38
|
extracted = []
|
|
39
39
|
reader.entries.each do |entry|
|
|
40
40
|
dest_path = ::File.join(output_dir, entry.filename)
|
|
41
|
-
raise "File exists: #{dest_path}" if ::File.exist?(dest_path)
|
|
41
|
+
raise Errno::EEXIST, "File exists: #{dest_path}" if ::File.exist?(dest_path)
|
|
42
42
|
|
|
43
43
|
reader.extract_entry(entry, output_dir)
|
|
44
44
|
extracted << dest_path
|
|
@@ -151,24 +151,12 @@ module Omnizip
|
|
|
151
151
|
|
|
152
152
|
private
|
|
153
153
|
|
|
154
|
-
#
|
|
154
|
+
# Delegate to the shared buffer-side sniffer
|
|
155
155
|
#
|
|
156
156
|
# @return [Symbol] Detected format
|
|
157
157
|
# @raise [Omnizip::FormatError] If format cannot be detected
|
|
158
158
|
def detect_format
|
|
159
|
-
|
|
160
|
-
@buffer.rewind
|
|
161
|
-
|
|
162
|
-
case magic
|
|
163
|
-
when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
|
|
164
|
-
# ZIP signatures: local file header, EOCD, data descriptor
|
|
165
|
-
:zip
|
|
166
|
-
when "7z\xBC\xAF".b
|
|
167
|
-
:seven_zip
|
|
168
|
-
else
|
|
169
|
-
raise Omnizip::FormatError,
|
|
170
|
-
"Unknown archive format (magic: #{magic.inspect})"
|
|
171
|
-
end
|
|
159
|
+
Omnizip::Buffer.detect_format(@buffer)
|
|
172
160
|
end
|
|
173
161
|
|
|
174
162
|
# Extract all entries from ZIP
|
|
@@ -207,8 +195,7 @@ module Omnizip
|
|
|
207
195
|
def extract_entry_seven_zip(name)
|
|
208
196
|
@buffer.rewind
|
|
209
197
|
SevenZipBridge.open(@buffer) do |archive|
|
|
210
|
-
|
|
211
|
-
archive.extract_all_to_memory[name] if entry
|
|
198
|
+
archive.read_entry(name)
|
|
212
199
|
end
|
|
213
200
|
end
|
|
214
201
|
|
|
@@ -112,6 +112,19 @@ module Omnizip
|
|
|
112
112
|
@reader.list_files
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# Content of a single named file entry, or nil when absent
|
|
116
|
+
# (directories carry no content). Extracts just that entry —
|
|
117
|
+
# not the whole archive.
|
|
118
|
+
#
|
|
119
|
+
# @param name [String] Entry name
|
|
120
|
+
# @return [String, nil]
|
|
121
|
+
def read_entry(name)
|
|
122
|
+
entry = raw_entries.find { |e| e.name == name && !e.is_dir }
|
|
123
|
+
return nil unless entry
|
|
124
|
+
|
|
125
|
+
Entry.new(entry, @reader, @tmp).read
|
|
126
|
+
end
|
|
127
|
+
|
|
115
128
|
# Entry wrapper matching MemoryArchive::Entry's surface
|
|
116
129
|
class Entry
|
|
117
130
|
attr_reader :name, :size
|
data/lib/omnizip/buffer.rb
CHANGED
|
@@ -107,27 +107,9 @@ module Omnizip
|
|
|
107
107
|
extractor.extract_all
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# @param format [Symbol] Archive format
|
|
114
|
-
# @param options [Hash] Format-specific options
|
|
115
|
-
# @return [StringIO] Complete archive in memory
|
|
116
|
-
#
|
|
117
|
-
# @example Create from Hash
|
|
118
|
-
# data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
|
|
119
|
-
# zip = Omnizip::Buffer.create_from_hash(data, :zip)
|
|
120
|
-
def create_from_hash(hash, format = :zip, **options)
|
|
121
|
-
create(format, **options) do |archive|
|
|
122
|
-
hash.each do |name, content|
|
|
123
|
-
archive.add(name, content)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
# Detect archive format from magic bytes
|
|
110
|
+
# Detect archive format from magic bytes. THE buffer-side
|
|
111
|
+
# sniffer — in-memory data has no extension to route by, so
|
|
112
|
+
# every consumer delegates here.
|
|
131
113
|
#
|
|
132
114
|
# @param buffer [StringIO] Buffer containing archive data
|
|
133
115
|
# @return [Symbol] Detected format
|
|
@@ -148,6 +130,26 @@ module Omnizip
|
|
|
148
130
|
end
|
|
149
131
|
end
|
|
150
132
|
|
|
133
|
+
# Create archive from Hash of filename => content
|
|
134
|
+
#
|
|
135
|
+
# @param hash [Hash<String, String>] Filename => content mapping
|
|
136
|
+
# @param format [Symbol] Archive format
|
|
137
|
+
# @param options [Hash] Format-specific options
|
|
138
|
+
# @return [StringIO] Complete archive in memory
|
|
139
|
+
#
|
|
140
|
+
# @example Create from Hash
|
|
141
|
+
# data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
|
|
142
|
+
# zip = Omnizip::Buffer.create_from_hash(data, :zip)
|
|
143
|
+
def create_from_hash(hash, format = :zip, **options)
|
|
144
|
+
create(format, **options) do |archive|
|
|
145
|
+
hash.each do |name, content|
|
|
146
|
+
archive.add(name, content)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
151
153
|
# Create ZIP archive in buffer
|
|
152
154
|
#
|
|
153
155
|
# @param buffer [StringIO] Buffer to write to
|
|
@@ -94,7 +94,7 @@ module Omnizip
|
|
|
94
94
|
when CHECK_SHA256
|
|
95
95
|
verify_sha256(data, expected)
|
|
96
96
|
else
|
|
97
|
-
raise "Unknown check type: #{check_type}"
|
|
97
|
+
raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -122,7 +122,7 @@ module Omnizip
|
|
|
122
122
|
when CHECK_SHA256
|
|
123
123
|
Digest::SHA256.digest(data)
|
|
124
124
|
else
|
|
125
|
-
raise "Unknown check type: #{check_type}"
|
|
125
|
+
raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
end
|
data/lib/omnizip/chunked.rb
CHANGED
|
@@ -95,30 +95,23 @@ module Omnizip
|
|
|
95
95
|
"Input archive not found: #{input}"
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Omnizip::Zip::File.open(input) do |zip|
|
|
102
|
-
entry = zip.entries.first
|
|
103
|
-
total_size = entry.size
|
|
104
|
-
|
|
105
|
-
# Read the full entry content
|
|
106
|
-
content = zip.get_input_stream(entry)
|
|
98
|
+
reader = Omnizip::Formats::Zip::Reader.new(input)
|
|
99
|
+
entry = reader.entries.first
|
|
100
|
+
raise Omnizip::Error, "No entries to decompress in #{input}" unless entry
|
|
107
101
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
chunk = content.byteslice(offset, chunk_size) || ""
|
|
112
|
-
break if chunk.empty?
|
|
102
|
+
total_size = entry.uncompressed_size.to_i
|
|
103
|
+
processed = 0
|
|
104
|
+
writer = Writer.new(output, chunk_size: chunk_size)
|
|
113
105
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
# Streams entry-by-entry in bounded memory — the whole entry
|
|
107
|
+
# is never held at once
|
|
108
|
+
reader.read_entry_stream(entry.filename, chunk_size: chunk_size) do |chunk|
|
|
109
|
+
writer.write_chunk(chunk)
|
|
110
|
+
processed += chunk.bytesize
|
|
117
111
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
end
|
|
112
|
+
if progress
|
|
113
|
+
percentage = (processed.to_f / total_size * 100).round(2)
|
|
114
|
+
progress.call(processed, total_size, percentage)
|
|
122
115
|
end
|
|
123
116
|
end
|
|
124
117
|
|
|
@@ -44,19 +44,27 @@ module Omnizip
|
|
|
44
44
|
].join("\n")
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Format file size in human-readable format.
|
|
47
|
+
# Format file size in human-readable format. This is THE byte
|
|
48
|
+
# formatter — commands, the CLI, and display-oriented models
|
|
49
|
+
# delegate here instead of carrying private copies.
|
|
48
50
|
#
|
|
49
|
-
# @param bytes [Integer] Size in bytes
|
|
50
|
-
# @return [String] Formatted size
|
|
51
|
+
# @param bytes [Integer, nil] Size in bytes
|
|
52
|
+
# @return [String] Formatted size ("512 B", "1.5 KB", "2.0 MB")
|
|
51
53
|
def format_size(bytes)
|
|
54
|
+
return "0 B" if bytes.nil? || bytes.zero?
|
|
55
|
+
|
|
52
56
|
units = %w[B KB MB GB TB]
|
|
53
|
-
|
|
57
|
+
size = bytes.to_f
|
|
58
|
+
unit_index = 0
|
|
59
|
+
|
|
60
|
+
while size >= 1024.0 && unit_index < units.size - 1
|
|
61
|
+
size /= 1024.0
|
|
62
|
+
unit_index += 1
|
|
63
|
+
end
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
exp = [exp, units.length - 1].min
|
|
65
|
+
return format("%d B", bytes) if unit_index.zero?
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
"#{size} #{units[exp]}"
|
|
67
|
+
format("%.1f %s", size, units[unit_index])
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
# Format error message for display.
|
data/lib/omnizip/cli.rb
CHANGED
|
@@ -60,13 +60,7 @@ module Omnizip
|
|
|
60
60
|
# @param bytes [Integer] the byte count
|
|
61
61
|
# @return [String] human-readable size (e.g. "1.5 MB")
|
|
62
62
|
def format_bytes(bytes)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
units = %w[B KB MB GB TB]
|
|
66
|
-
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
67
|
-
exp = [exp, units.size - 1].min
|
|
68
|
-
|
|
69
|
-
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
63
|
+
Omnizip::CliOutputFormatter.format_size(bytes)
|
|
70
64
|
end
|
|
71
65
|
end
|
|
72
66
|
|
|
@@ -201,16 +201,7 @@ module Omnizip
|
|
|
201
201
|
end
|
|
202
202
|
|
|
203
203
|
def format_bytes(bytes)
|
|
204
|
-
|
|
205
|
-
size = bytes.to_f
|
|
206
|
-
unit_idx = 0
|
|
207
|
-
|
|
208
|
-
while size >= 1024 && unit_idx < units.length - 1
|
|
209
|
-
size /= 1024.0
|
|
210
|
-
unit_idx += 1
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
format("%.2f %s", size, units[unit_idx])
|
|
204
|
+
Omnizip::CliOutputFormatter.format_size(bytes)
|
|
214
205
|
end
|
|
215
206
|
|
|
216
207
|
def parse_volume_size(size_str)
|
|
@@ -135,17 +135,10 @@ module Omnizip
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def extract_with_patterns(archive_file, output_dir, verbose)
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
when ".rar"
|
|
143
|
-
Formats::Rar::Reader.new(archive_file).open
|
|
144
|
-
when ".tar"
|
|
145
|
-
Formats::Tar::Reader.new(archive_file).read
|
|
146
|
-
else
|
|
147
|
-
Formats::SevenZip::Reader.new(archive_file).open
|
|
148
|
-
end
|
|
138
|
+
# Route through the facade: the extension routes pick each
|
|
139
|
+
# format's reader, and the reader session holds no open
|
|
140
|
+
# resources to close
|
|
141
|
+
archive = Omnizip::Archive.open(archive_file)
|
|
149
142
|
|
|
150
143
|
# Build filter chain
|
|
151
144
|
filter = Extraction::FilterChain.new
|
|
@@ -193,8 +186,6 @@ module Omnizip
|
|
|
193
186
|
end
|
|
194
187
|
|
|
195
188
|
extracted.size
|
|
196
|
-
ensure
|
|
197
|
-
archive.close if archive.is_a?(Omnizip::Zip::File)
|
|
198
189
|
end
|
|
199
190
|
|
|
200
191
|
def extract_gzip(archive_file, output_dir, verbose)
|
|
@@ -157,24 +157,7 @@ module Omnizip
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
def format_bytes(bytes)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
units = %w[B KB MB GB]
|
|
163
|
-
size = bytes.to_f
|
|
164
|
-
unit_idx = 0
|
|
165
|
-
|
|
166
|
-
while size >= 1024 && unit_idx < units.length - 1
|
|
167
|
-
size /= 1024.0
|
|
168
|
-
unit_idx += 1
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
if size < 10
|
|
172
|
-
format("%.2f %s", size, units[unit_idx])
|
|
173
|
-
elsif size < 100
|
|
174
|
-
format("%.1f %s", size, units[unit_idx])
|
|
175
|
-
else
|
|
176
|
-
format("%.0f %s", size, units[unit_idx])
|
|
177
|
-
end
|
|
160
|
+
Omnizip::CliOutputFormatter.format_size(bytes)
|
|
178
161
|
end
|
|
179
162
|
|
|
180
163
|
# Helper methods to handle different entry types. Each delegates
|
|
@@ -54,7 +54,8 @@ module Omnizip
|
|
|
54
54
|
%i[comment set_mtime chmod set_attribute].none? { |key| options[key] }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# Read-only view for 7z archives
|
|
57
|
+
# Read-only view for 7z archives through the handler registry;
|
|
58
|
+
# the edit path is zip-only
|
|
58
59
|
def show_seven_zip_metadata(archive_path, pattern)
|
|
59
60
|
if options[:comment] || options[:set_mtime] ||
|
|
60
61
|
options[:chmod] || options[:set_attribute]
|
|
@@ -62,37 +63,37 @@ module Omnizip
|
|
|
62
63
|
"Metadata editing is only supported for ZIP archives"
|
|
63
64
|
end
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
reader.open
|
|
66
|
+
entries = Omnizip.list_archive(archive_path, details: true)
|
|
67
67
|
|
|
68
68
|
if pattern
|
|
69
|
-
entries =
|
|
70
|
-
File.fnmatch(pattern,
|
|
69
|
+
entries = entries.select do |entry|
|
|
70
|
+
File.fnmatch(pattern, entry[:name])
|
|
71
71
|
end
|
|
72
72
|
if entries.empty?
|
|
73
73
|
warn "No entries match pattern: #{pattern}"
|
|
74
74
|
return
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
entries.each
|
|
78
|
-
puts "Entry: #{entry.name}"
|
|
79
|
-
puts " Type: #{entry.is_dir ? 'Directory' : 'File'}"
|
|
80
|
-
puts " Size: #{format_size(entry.size)}"
|
|
81
|
-
# Per-entry packed sizes are not tracked by the parser
|
|
82
|
-
if entry.compressed_size&.positive?
|
|
83
|
-
puts " Compressed: #{format_size(entry.compressed_size)}"
|
|
84
|
-
end
|
|
85
|
-
puts " Modified: #{entry.mtime}" if entry.mtime
|
|
86
|
-
puts " CRC32: 0x#{entry.crc.to_s(16).upcase}" if entry.crc
|
|
87
|
-
end
|
|
77
|
+
entries.each { |entry| show_seven_zip_entry(entry) }
|
|
88
78
|
else
|
|
89
|
-
|
|
90
|
-
files = entries.reject(&:is_dir)
|
|
79
|
+
files = entries.reject { |entry| entry[:directory] }
|
|
91
80
|
puts "Archive: #{archive_path}"
|
|
92
81
|
puts "Entries: #{entries.size} " \
|
|
93
82
|
"(#{files.size} files, #{entries.size - files.size} dirs)"
|
|
94
|
-
puts "Total size: #{format_size(files.sum
|
|
83
|
+
puts "Total size: #{format_size(files.sum { |entry| entry[:size] })}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def show_seven_zip_entry(entry)
|
|
88
|
+
puts "Entry: #{entry[:name]}"
|
|
89
|
+
puts " Type: #{entry[:directory] ? 'Directory' : 'File'}"
|
|
90
|
+
puts " Size: #{format_size(entry[:size])}"
|
|
91
|
+
# Per-entry packed sizes are not tracked by the parser
|
|
92
|
+
if entry[:compressed_size]&.positive?
|
|
93
|
+
puts " Compressed: #{format_size(entry[:compressed_size])}"
|
|
95
94
|
end
|
|
95
|
+
puts " Modified: #{entry[:mtime]}" if entry[:mtime]
|
|
96
|
+
puts " CRC32: 0x#{entry[:crc].to_s(16).upcase}" if entry[:crc]
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
def show_metadata(archive, pattern)
|
|
@@ -199,13 +200,7 @@ module Omnizip
|
|
|
199
200
|
end
|
|
200
201
|
|
|
201
202
|
def format_size(bytes)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
units = %w[B KB MB GB TB]
|
|
205
|
-
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
206
|
-
exp = [exp, units.size - 1].min
|
|
207
|
-
|
|
208
|
-
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
203
|
+
Omnizip::CliOutputFormatter.format_size(bytes)
|
|
209
204
|
end
|
|
210
205
|
end
|
|
211
206
|
end
|
|
@@ -107,13 +107,7 @@ module Omnizip
|
|
|
107
107
|
# @param bytes [Integer] Size in bytes
|
|
108
108
|
# @return [String] Formatted size
|
|
109
109
|
def format_size(bytes)
|
|
110
|
-
|
|
111
|
-
"#{bytes} B"
|
|
112
|
-
elsif bytes < 1024 * 1024
|
|
113
|
-
"#{(bytes / 1024.0).round(1)} KB"
|
|
114
|
-
else
|
|
115
|
-
"#{(bytes / (1024.0 * 1024)).round(1)} MB"
|
|
116
|
-
end
|
|
110
|
+
Omnizip::CliOutputFormatter.format_size(bytes)
|
|
117
111
|
end
|
|
118
112
|
end
|
|
119
113
|
end
|
|
@@ -33,7 +33,10 @@ module Omnizip
|
|
|
33
33
|
FileUtils.mkdir_p(dest)
|
|
34
34
|
extracted = []
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
# Directory entries carry no content to extract
|
|
37
|
+
entries_to_extract = list_matches.reject do |entry|
|
|
38
|
+
entry_filename(entry).end_with?("/")
|
|
39
|
+
end
|
|
37
40
|
total = entries_to_extract.size
|
|
38
41
|
current = 0
|
|
39
42
|
|
|
@@ -162,7 +165,7 @@ module Omnizip
|
|
|
162
165
|
entry.get_input_stream.read
|
|
163
166
|
# allowed: some archives read entry content from the archive
|
|
164
167
|
elsif @archive.respond_to?(:read)
|
|
165
|
-
@archive.read(entry)
|
|
168
|
+
@archive.read(entry_filename(entry))
|
|
166
169
|
else
|
|
167
170
|
raise Error, "Cannot read entry content"
|
|
168
171
|
end
|
|
@@ -191,10 +194,19 @@ module Omnizip
|
|
|
191
194
|
|
|
192
195
|
# Get filename from entry
|
|
193
196
|
#
|
|
194
|
-
# @param entry [#entry_name] Entry (Omnizip::Entry
|
|
197
|
+
# @param entry [#entry_name, #name] Entry (legacy Omnizip::Entry,
|
|
198
|
+
# Archive::Entry from a reader session, or compatible)
|
|
195
199
|
# @return [String] Filename
|
|
196
200
|
def entry_filename(entry)
|
|
197
|
-
|
|
201
|
+
# allowed: entries come from a caller-supplied archive
|
|
202
|
+
if entry.respond_to?(:entry_name)
|
|
203
|
+
entry.entry_name
|
|
204
|
+
# allowed: reader-session entries expose name, not entry_name
|
|
205
|
+
elsif entry.respond_to?(:name)
|
|
206
|
+
entry.name
|
|
207
|
+
else
|
|
208
|
+
entry.to_s
|
|
209
|
+
end
|
|
198
210
|
end
|
|
199
211
|
|
|
200
212
|
# Update progress tracker
|
|
@@ -64,7 +64,7 @@ module Omnizip
|
|
|
64
64
|
# @raise [RuntimeError] if entry not found
|
|
65
65
|
def extract_entry(entry_name, output_path)
|
|
66
66
|
entry = @entries.find { |e| e.name == entry_name }
|
|
67
|
-
raise "Entry not found: #{entry_name}" unless entry
|
|
67
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
|
|
68
68
|
|
|
69
69
|
extract_single_entry(entry, output_path)
|
|
70
70
|
end
|
|
@@ -67,7 +67,7 @@ module Omnizip
|
|
|
67
67
|
# @param output_path [String] Destination path
|
|
68
68
|
def extract_entry(entry_path, output_path)
|
|
69
69
|
entry = find_entry(entry_path)
|
|
70
|
-
raise "Entry not found: #{entry_path}" unless entry
|
|
70
|
+
raise Errno::ENOENT, "Entry not found: #{entry_path}" unless entry
|
|
71
71
|
|
|
72
72
|
if entry.directory?
|
|
73
73
|
FileUtils.mkdir_p(output_path)
|
|
@@ -111,7 +111,7 @@ module Omnizip
|
|
|
111
111
|
loop do
|
|
112
112
|
@io.seek(sector * Iso::SECTOR_SIZE)
|
|
113
113
|
data = @io.read(Iso::SECTOR_SIZE)
|
|
114
|
-
raise "Failed to read volume descriptor" unless data
|
|
114
|
+
raise Omnizip::InvalidArchiveError, "Failed to read volume descriptor" unless data
|
|
115
115
|
|
|
116
116
|
vd = VolumeDescriptor.parse(data)
|
|
117
117
|
|
|
@@ -127,7 +127,7 @@ module Omnizip
|
|
|
127
127
|
|
|
128
128
|
return if @primary_volume_descriptor
|
|
129
129
|
|
|
130
|
-
raise "No primary volume descriptor found"
|
|
130
|
+
raise Omnizip::InvalidArchiveError, "No primary volume descriptor found"
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
# Parse directory structure
|
|
@@ -29,7 +29,7 @@ module Omnizip
|
|
|
29
29
|
#
|
|
30
30
|
# @param data [String] Binary data
|
|
31
31
|
def parse(data)
|
|
32
|
-
raise "Invalid volume descriptor size" unless data.bytesize >= 2048
|
|
32
|
+
raise Omnizip::InvalidArchiveError, "Invalid volume descriptor size" unless data.bytesize >= 2048
|
|
33
33
|
|
|
34
34
|
# Byte 0: Volume descriptor type
|
|
35
35
|
@type = data.getbyte(0)
|
|
@@ -37,7 +37,7 @@ module Omnizip
|
|
|
37
37
|
# Bytes 1-5: Standard identifier "CD001"
|
|
38
38
|
@identifier = data[1, 5]
|
|
39
39
|
unless @identifier == ISO_IDENTIFIER
|
|
40
|
-
raise "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
|
|
40
|
+
raise Omnizip::InvalidArchiveError, "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Byte 6: Version (should be 1)
|
|
@@ -230,26 +230,6 @@ module Omnizip
|
|
|
230
230
|
# Decode as 8 direct bits
|
|
231
231
|
@range_decoder.decode_direct_bits(8)
|
|
232
232
|
end
|
|
233
|
-
|
|
234
|
-
# Decode RAR-specific escape code
|
|
235
|
-
#
|
|
236
|
-
# RAR variant H uses different escape code values
|
|
237
|
-
# and handling compared to standard PPMd7.
|
|
238
|
-
#
|
|
239
|
-
# Escape codes in RAR:
|
|
240
|
-
# - 0: New symbol follows
|
|
241
|
-
# - 1: Same as last symbol (run-length)
|
|
242
|
-
# - 2-255: Reserved for future use
|
|
243
|
-
#
|
|
244
|
-
# @return [Integer, nil] Escape code or nil
|
|
245
|
-
def decode_escape_code
|
|
246
|
-
# RAR escape codes differ from PPMd7
|
|
247
|
-
# This is a placeholder for the proper implementation
|
|
248
|
-
|
|
249
|
-
# For now, return 0 (new symbol follows)
|
|
250
|
-
# Real implementation would decode from range coder
|
|
251
|
-
0
|
|
252
|
-
end
|
|
253
233
|
end
|
|
254
234
|
end
|
|
255
235
|
end
|