omnizip 0.3.40 → 0.3.41
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 +46 -0
- data/lib/omnizip/commands/archive_create_command.rb +11 -23
- data/lib/omnizip/converter/zip_to_seven_zip_strategy.rb +21 -63
- data/lib/omnizip/formats/rar/block_parser.rb +42 -6
- data/lib/omnizip/formats/rar/decompressor.rb +35 -8
- data/lib/omnizip/formats/rar/header.rb +18 -1
- data/lib/omnizip/formats/rar/models/rar_entry.rb +1 -1
- data/lib/omnizip/formats/rar/reader.rb +17 -22
- data/lib/omnizip/formats/rar/writer.rb +1 -1
- data/lib/omnizip/formats/rar.rb +14 -1
- data/lib/omnizip/formats/rar3/reader.rb +66 -314
- data/lib/omnizip/formats/rar5/reader.rb +80 -297
- data/lib/omnizip/formats/rar5/writer.rb +33 -180
- 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: 919a322870094f270bd574530af4c463b6446586fdf869026b79ad017834bc1e
|
|
4
|
+
data.tar.gz: 55507eb0b2da4109cb094e711cda2d39df10773c9774c4a01614edc0d8548d9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73b03468e2bb51d227059f316ab4fce911ce19ee6e0c175559de7b1df58a69536d9284d2b5ee2a0a19366ef0c53d448d02e4b95f397042c69c569630aacea86f
|
|
7
|
+
data.tar.gz: dad012852021f331f77a36bf86842274f612d215e32829965bf4bbaf5ad8b9b17539eb7e531a55e73c93d206c73a66760ccde6dfcf234b49e371f17f79451df8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.40] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Every CLI command now works end-to-end (each was exercised against
|
|
14
|
+
real archives and its failure fixed):
|
|
15
|
+
- `omnizip list` printed "No algorithms registered." — all builtin
|
|
16
|
+
algorithms register lazily, so the available list was empty until
|
|
17
|
+
each was first used. `AlgorithmRegistry.available` now includes
|
|
18
|
+
the builtins.
|
|
19
|
+
- `omnizip convert a.7z a.zip` crashed on a fictional reader/writer
|
|
20
|
+
API inside the conversion strategy; rewritten on the real APIs and
|
|
21
|
+
round-trips byte-identically through unzip.
|
|
22
|
+
- `omnizip archive create x.rar ...` raised NoMethodError (a
|
|
23
|
+
module-level `Formats::Rar.create` never existed). It exists now
|
|
24
|
+
(RAR5 default, `version: 4` for RAR4); directory inputs work —
|
|
25
|
+
the stale "RAR5 does not support directories" restriction is gone
|
|
26
|
+
and trees archive under their own name like `.7z`, verified with
|
|
27
|
+
unrar.
|
|
28
|
+
- `omnizip archive metadata` opened every input as a ZIP: `.7z`
|
|
29
|
+
crashed with a ZIP parse error, and runs with no edit option
|
|
30
|
+
still printed "Metadata updated successfully". 7z now shows real
|
|
31
|
+
read-only metadata, unknown formats raise truthful errors, and
|
|
32
|
+
ZIPs default to show mode when nothing is being edited.
|
|
33
|
+
- `omnizip decompress archive.zip out/` — the behavior the
|
|
34
|
+
command's own help documents — failed with "Is a directory".
|
|
35
|
+
Archive inputs (zip/7z/tar/rar/cpio/iso) and directory outputs
|
|
36
|
+
now extract through the convenience layer, content-verified with
|
|
37
|
+
diff for zip, 7z, rar, and tar.
|
|
38
|
+
- Deflate64.metadata returned a raw Hash, crashing the CLI algorithm
|
|
39
|
+
table; it returns `Models::AlgorithmMetadata` like every other
|
|
40
|
+
algorithm.
|
|
41
|
+
- Guides referenced a nonexistent `:zstd` algorithm-registry key;
|
|
42
|
+
corrected to `:zstandard` everywhere (the registry key differs from
|
|
43
|
+
RPM's `:zstd` compression type).
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- `Omnizip::Buffer` 7z support: `create(:seven_zip)` /
|
|
47
|
+
`create_from_hash(hash, :seven_zip)`, `open`, `extract_to_memory`,
|
|
48
|
+
and `MemoryExtractor` all handle 7z through a temporary-file bridge
|
|
49
|
+
(the format has no in-memory stream implementation), including
|
|
50
|
+
explicit directory entries via a new
|
|
51
|
+
`SevenZip::Writer#add_directory_entry`. Format auto-detection fixed
|
|
52
|
+
to compare binary magic literals — UTF-8 literals never matched
|
|
53
|
+
binary data, so 7z buffers raised "Unknown archive format".
|
|
54
|
+
- README gains an In-Memory Archives section (examples run verbatim).
|
|
55
|
+
|
|
10
56
|
## [0.3.39] - 2026-08-28
|
|
11
57
|
|
|
12
58
|
### Fixed
|
|
@@ -120,35 +120,23 @@ module Omnizip
|
|
|
120
120
|
|
|
121
121
|
start_time = Time.now
|
|
122
122
|
|
|
123
|
+
# Format-agnostic options; Formats::Rar applies the
|
|
124
|
+
# version-specific semantics
|
|
123
125
|
writer_opts = {
|
|
124
126
|
version: version,
|
|
125
127
|
compression: compression,
|
|
126
128
|
level: level,
|
|
127
129
|
include_mtime: include_mtime,
|
|
128
130
|
include_crc32: include_crc32,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
writer_opts[:volume_naming] = volume_naming
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Add encryption options for RAR5
|
|
142
|
-
if version == 5 && password
|
|
143
|
-
writer_opts[:password] = password
|
|
144
|
-
writer_opts[:kdf_iterations] = kdf_iterations
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Add recovery options for RAR5
|
|
148
|
-
if version == 5 && recovery
|
|
149
|
-
writer_opts[:recovery] = true
|
|
150
|
-
writer_opts[:recovery_percent] = recovery_percent
|
|
151
|
-
end
|
|
131
|
+
solid: solid,
|
|
132
|
+
multi_volume: multi_volume,
|
|
133
|
+
volume_size: volume_size,
|
|
134
|
+
volume_naming: volume_naming,
|
|
135
|
+
password: password,
|
|
136
|
+
kdf_iterations: kdf_iterations,
|
|
137
|
+
recovery: recovery,
|
|
138
|
+
recovery_percent: recovery_percent,
|
|
139
|
+
}.compact
|
|
152
140
|
|
|
153
141
|
result_files = Omnizip::Formats::Rar.create(output_file,
|
|
154
142
|
writer_opts) do |rar|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
3
5
|
module Omnizip
|
|
4
6
|
module Converter
|
|
5
7
|
# Convert ZIP archives to 7-Zip format
|
|
@@ -10,14 +12,20 @@ module Omnizip
|
|
|
10
12
|
start_time = Time.now
|
|
11
13
|
entry_count = 0
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Dir.mktmpdir("omnizip_convert_zip") do |tmp|
|
|
16
|
+
# Extract the ZIP through the native reader; directories are
|
|
17
|
+
# implied by extracted file paths
|
|
18
|
+
Omnizip::Formats::Zip.extract(source_path, tmp)
|
|
19
|
+
|
|
20
|
+
writer = Omnizip::Formats::SevenZip::Writer.new(target_path,
|
|
21
|
+
writer_options)
|
|
22
|
+
Dir.glob(File.join(tmp, "**", "*")).each do |path|
|
|
23
|
+
next if File.directory?(path)
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
entry_count += 1
|
|
26
|
+
writer.add_file(path, path.delete_prefix("#{tmp}/"))
|
|
27
|
+
end
|
|
28
|
+
writer.write
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
create_result(start_time, entry_count)
|
|
@@ -45,62 +53,12 @@ module Omnizip
|
|
|
45
53
|
|
|
46
54
|
private
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
directory: entry.directory?,
|
|
55
|
-
mtime: entry.time,
|
|
56
|
-
content: nil,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
unless entry.directory?
|
|
60
|
-
data[:content] = zip.get_input_stream(entry)
|
|
61
|
-
|
|
62
|
-
if options.preserve_metadata && entry.unix_perms.positive?
|
|
63
|
-
data[:unix_perms] = entry.unix_perms
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
entries << data
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
entries
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def create_seven_zip(entries)
|
|
74
|
-
writer = Omnizip::Formats::SevenZip::Writer.new(target_path)
|
|
75
|
-
|
|
76
|
-
# Set compression options
|
|
77
|
-
compression = options.compression || :lzma2
|
|
78
|
-
level = options.compression_level || 5
|
|
79
|
-
options.solid.nil? || options.solid
|
|
80
|
-
|
|
81
|
-
# Add each entry
|
|
82
|
-
entries.each do |entry_data|
|
|
83
|
-
if entry_data[:directory]
|
|
84
|
-
# 7z doesn't have explicit directory entries
|
|
85
|
-
# Directories are implied by file paths
|
|
86
|
-
next
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
writer.add_data(
|
|
90
|
-
entry_data[:name],
|
|
91
|
-
entry_data[:content],
|
|
92
|
-
algorithm: compression,
|
|
93
|
-
level: level,
|
|
94
|
-
)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Apply filter if specified
|
|
98
|
-
if options.filter
|
|
99
|
-
add_warning("Filters not yet supported in 7z conversion")
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Write the archive
|
|
103
|
-
writer.write
|
|
56
|
+
# SevenZip::Writer takes algorithm/level/solid as keywords
|
|
57
|
+
def writer_options
|
|
58
|
+
{
|
|
59
|
+
algorithm: options.compression || :lzma2,
|
|
60
|
+
level: options.compression_level || 5,
|
|
61
|
+
}
|
|
104
62
|
end
|
|
105
63
|
end
|
|
106
64
|
end
|
|
@@ -45,6 +45,7 @@ module Omnizip
|
|
|
45
45
|
# @return [Models::RarEntry, nil] Parsed entry or nil
|
|
46
46
|
def parse_rar4_file_block(io)
|
|
47
47
|
entry = Models::RarEntry.new
|
|
48
|
+
block_start = io.pos
|
|
48
49
|
|
|
49
50
|
# Read block header
|
|
50
51
|
read_uint16(io)
|
|
@@ -73,12 +74,23 @@ module Omnizip
|
|
|
73
74
|
unpack_size |= (high_unpack_size << 32)
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
# Sanity bounds: corrupt headers must not drive huge reads.
|
|
78
|
+
# head_size can never exceed the bytes left from the block
|
|
79
|
+
# start, and the name must fit inside the header.
|
|
80
|
+
if head_size > io.size - block_start ||
|
|
81
|
+
name_size + 32 > head_size
|
|
82
|
+
return nil
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# Read file name
|
|
77
86
|
name_bytes = io.read(name_size)
|
|
78
87
|
entry.name = decode_filename(name_bytes, head_flags)
|
|
79
88
|
# RAR4 archives store DOS-style backslash separators;
|
|
80
|
-
# normalize to forward slashes like unrar on Unix.
|
|
81
|
-
|
|
89
|
+
# normalize to forward slashes like unrar on Unix. Scrub
|
|
90
|
+
# first: unicode-flagged names may carry bytes that are not
|
|
91
|
+
# valid UTF-8, and String#tr raises on them under any
|
|
92
|
+
# locale.
|
|
93
|
+
entry.name = entry.name.scrub.tr("\\", "/")
|
|
82
94
|
|
|
83
95
|
# Set entry properties
|
|
84
96
|
entry.size = unpack_size
|
|
@@ -111,7 +123,14 @@ module Omnizip
|
|
|
111
123
|
remaining = head_size - (name_size + 32)
|
|
112
124
|
remaining -= 8 if head_flags.anybits?(FILE_LARGE)
|
|
113
125
|
io.read(remaining) if remaining.positive?
|
|
114
|
-
|
|
126
|
+
# Record where the packed data starts so extraction can seek
|
|
127
|
+
# straight to it instead of re-parsing the archive
|
|
128
|
+
entry.data_offset = io.pos
|
|
129
|
+
# Seek past the data: never allocate the packed bytes just
|
|
130
|
+
# to skip them. A corrupt pack_size (bigger than the file)
|
|
131
|
+
# would overflow the seek offset on Linux — clamp to EOF so
|
|
132
|
+
# the entry still lists.
|
|
133
|
+
skip_past(io, pack_size)
|
|
115
134
|
|
|
116
135
|
entry
|
|
117
136
|
end
|
|
@@ -135,6 +154,10 @@ module Omnizip
|
|
|
135
154
|
extra_size = header_flags.anybits?(RAR5_FLAG_EXTRA_AREA) ? read_vint(io) : 0
|
|
136
155
|
data_size = header_flags.anybits?(RAR5_FLAG_DATA_AREA) ? read_vint(io) : 0
|
|
137
156
|
|
|
157
|
+
# Sanity bounds: corrupt headers must not drive huge reads
|
|
158
|
+
remaining_bytes = io.size - io.pos
|
|
159
|
+
return nil if extra_size > remaining_bytes || data_size > remaining_bytes
|
|
160
|
+
|
|
138
161
|
# Read file header
|
|
139
162
|
file_flags = read_vint(io)
|
|
140
163
|
unpack_size = read_vint(io)
|
|
@@ -155,7 +178,7 @@ module Omnizip
|
|
|
155
178
|
|
|
156
179
|
# Read file name
|
|
157
180
|
name_bytes = io.read(name_length)
|
|
158
|
-
entry.name = name_bytes.force_encoding("UTF-8")
|
|
181
|
+
entry.name = name_bytes.to_s.scrub.force_encoding("UTF-8")
|
|
159
182
|
|
|
160
183
|
# Set entry properties
|
|
161
184
|
entry.size = unpack_size
|
|
@@ -168,13 +191,26 @@ module Omnizip
|
|
|
168
191
|
entry.is_dir = file_flags.anybits?(RAR5_FLAG_IS_DIR)
|
|
169
192
|
entry.version = 5
|
|
170
193
|
|
|
171
|
-
# Skip extra area, then compressed data
|
|
194
|
+
# Skip extra area, then compressed data; record the data
|
|
195
|
+
# start for direct-seek extraction. The data is seeked past,
|
|
196
|
+
# never read: corrupt sizes must not drive huge allocations.
|
|
172
197
|
io.read(extra_size) if extra_size.positive?
|
|
173
|
-
|
|
198
|
+
entry.data_offset = io.pos
|
|
199
|
+
skip_past(io, data_size)
|
|
174
200
|
|
|
175
201
|
entry
|
|
176
202
|
end
|
|
177
203
|
|
|
204
|
+
# Skip forward without allocating: clamp corrupt sizes to EOF
|
|
205
|
+
# so an overflowing seek (EINVAL on Linux) cannot abort the
|
|
206
|
+
# block walk
|
|
207
|
+
def skip_past(io, bytes)
|
|
208
|
+
return if bytes <= 0
|
|
209
|
+
|
|
210
|
+
remaining = io.size - io.pos
|
|
211
|
+
io.seek([bytes, remaining].min, ::IO::SEEK_CUR)
|
|
212
|
+
end
|
|
213
|
+
|
|
178
214
|
# Decode filename from bytes
|
|
179
215
|
#
|
|
180
216
|
# @param bytes [String] Raw filename bytes
|
|
@@ -236,18 +236,45 @@ module Omnizip
|
|
|
236
236
|
end
|
|
237
237
|
|
|
238
238
|
# Extract entry with command
|
|
239
|
+
#
|
|
240
|
+
# unrar cannot extract a single entry without walking the
|
|
241
|
+
# archive, so the archive spills to a cache directory ONCE
|
|
242
|
+
# per archive path; subsequent extractions copy out of the
|
|
243
|
+
# cache instead of re-running unrar over the whole file.
|
|
239
244
|
def extract_entry_with_command(archive_path, entry_name,
|
|
240
245
|
output_path, password)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
cache_key = begin
|
|
247
|
+
File.realpath(archive_path)
|
|
248
|
+
rescue StandardError
|
|
249
|
+
archive_path
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
cached_dir = extract_cache[cache_key]
|
|
253
|
+
unless cached_dir
|
|
254
|
+
cached_dir = Dir.mktmpdir("omnizip_unrar")
|
|
255
|
+
cmd = build_extract_command(archive_path, cached_dir, password)
|
|
256
|
+
unless system(*cmd)
|
|
257
|
+
FileUtils.rm_rf(cached_dir)
|
|
258
|
+
raise "Command entry extraction failed: #{entry_name}"
|
|
259
|
+
end
|
|
260
|
+
extract_cache[cache_key] = cached_dir
|
|
245
261
|
end
|
|
246
262
|
|
|
247
|
-
source = File.join(
|
|
248
|
-
FileUtils.
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
source = File.join(cached_dir, entry_name)
|
|
264
|
+
FileUtils.cp(source, output_path) if File.exist?(source)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Extracted-archive cache: archive path => spill directory.
|
|
268
|
+
# Cleared by .clear_extract_cache! (tests); directories live
|
|
269
|
+
# in the system temp dir until process exit.
|
|
270
|
+
def extract_cache
|
|
271
|
+
@extract_cache ||= {}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Drop the extraction cache and remove its directories
|
|
275
|
+
def clear_extract_cache!
|
|
276
|
+
extract_cache.each_value { |dir| FileUtils.rm_rf(dir) }
|
|
277
|
+
extract_cache.clear
|
|
251
278
|
end
|
|
252
279
|
|
|
253
280
|
# Build extract command
|
|
@@ -85,7 +85,9 @@ module Omnizip
|
|
|
85
85
|
# Parse RAR4 header
|
|
86
86
|
#
|
|
87
87
|
# The 7-byte signature consumed by #parse IS the marker
|
|
88
|
-
# block, so the main header follows immediately.
|
|
88
|
+
# block, so the main header follows immediately. Minimal
|
|
89
|
+
# archives skip the main header and start with a file block —
|
|
90
|
+
# accept those by rewinding to the block.
|
|
89
91
|
#
|
|
90
92
|
# @param io [IO] Input stream
|
|
91
93
|
def parse_rar4_header(io)
|
|
@@ -94,6 +96,12 @@ module Omnizip
|
|
|
94
96
|
head_flags = read_uint16(io)
|
|
95
97
|
head_size = read_uint16(io)
|
|
96
98
|
|
|
99
|
+
if head_type == BLOCK_FILE
|
|
100
|
+
io.seek(-7, ::IO::SEEK_CUR)
|
|
101
|
+
@flags = 0
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
97
105
|
unless head_type == BLOCK_ARCHIVE
|
|
98
106
|
raise "Expected archive block, got 0x#{head_type.to_s(16)}"
|
|
99
107
|
end
|
|
@@ -117,11 +125,20 @@ module Omnizip
|
|
|
117
125
|
# RAR5 uses variable-length integer encoding. Layout per the
|
|
118
126
|
# RAR 5.0 format: HeaderCRC32(4), Header size(vint), Header
|
|
119
127
|
# type(vint), Header flags(vint), then type-specific fields.
|
|
128
|
+
header_start = io.pos
|
|
120
129
|
read_uint32(io)
|
|
121
130
|
read_vint(io) # header size (redundant: areas carry their own)
|
|
122
131
|
header_type = read_vint(io)
|
|
123
132
|
header_flags = read_vint(io)
|
|
124
133
|
|
|
134
|
+
# Minimal archives skip the main header and start with a
|
|
135
|
+
# file header (type 2) — rewind so block iteration reads it
|
|
136
|
+
if header_type == RAR5_HEADER_FILE
|
|
137
|
+
io.seek(header_start, ::IO::SEEK_SET)
|
|
138
|
+
@flags = 0
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
125
142
|
unless header_type == RAR5_HEADER_MAIN
|
|
126
143
|
raise "Expected main header, got #{header_type}"
|
|
127
144
|
end
|
|
@@ -10,7 +10,7 @@ module Omnizip
|
|
|
10
10
|
attr_accessor :name, :size, :compressed_size, :crc, :is_dir,
|
|
11
11
|
:host_os, :mtime, :attributes, :method,
|
|
12
12
|
:version, :flags, :volume_index, :split_before,
|
|
13
|
-
:split_after, :encrypted
|
|
13
|
+
:split_after, :encrypted, :data_offset
|
|
14
14
|
|
|
15
15
|
# Initialize RAR entry
|
|
16
16
|
def initialize
|
|
@@ -268,7 +268,7 @@ module Omnizip
|
|
|
268
268
|
# fall back to unrar.
|
|
269
269
|
data = output.string
|
|
270
270
|
if entry.crc && Zlib.crc32(data) != entry.crc
|
|
271
|
-
raise Compression::DecompressionError,
|
|
271
|
+
raise Compression::Dispatcher::DecompressionError,
|
|
272
272
|
"CRC mismatch for #{entry.name}"
|
|
273
273
|
end
|
|
274
274
|
|
|
@@ -296,19 +296,26 @@ module Omnizip
|
|
|
296
296
|
# @param entry [Models::RarEntry] File entry
|
|
297
297
|
# @return [StringIO] Compressed data stream
|
|
298
298
|
def read_compressed_data(entry)
|
|
299
|
-
#
|
|
299
|
+
# The parser records each entry's data offset, so native
|
|
300
|
+
# extraction is a single seek + read
|
|
301
|
+
if entry.data_offset && entry.compressed_size
|
|
302
|
+
File.open(@file_path, "rb") do |io|
|
|
303
|
+
io.seek(entry.data_offset)
|
|
304
|
+
return StringIO.new(io.read(entry.compressed_size) || "")
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Fallback for entries parsed without offsets (e.g. by older
|
|
309
|
+
# parsers): walk the blocks to locate the entry
|
|
300
310
|
File.open(@file_path, "rb") do |io|
|
|
301
|
-
# Skip signature and headers
|
|
302
311
|
Header.read(io)
|
|
303
312
|
|
|
304
|
-
# Parse file blocks to find our entry
|
|
305
313
|
parser = BlockParser.new(@header.version)
|
|
306
314
|
|
|
307
315
|
loop do
|
|
308
316
|
block_start = io.pos
|
|
309
317
|
break if io.eof?
|
|
310
318
|
|
|
311
|
-
# Peek at block type
|
|
312
319
|
crc_bytes = io.read(2)
|
|
313
320
|
break unless crc_bytes
|
|
314
321
|
|
|
@@ -317,45 +324,33 @@ module Omnizip
|
|
|
317
324
|
|
|
318
325
|
head_type = type_byte.ord
|
|
319
326
|
|
|
320
|
-
# If end block, stop
|
|
321
327
|
break if head_type == BLOCK_ENDARC
|
|
322
328
|
|
|
323
|
-
# Reset to block start
|
|
324
329
|
io.seek(block_start)
|
|
325
330
|
|
|
326
|
-
# If not a file block, read and skip it
|
|
327
331
|
unless head_type == BLOCK_FILE
|
|
328
|
-
# Read header
|
|
329
332
|
io.read(2) # CRC
|
|
330
333
|
io.read(1) # TYPE
|
|
331
|
-
io.read(2)
|
|
334
|
+
io.read(2) # FLAGS
|
|
332
335
|
size = io.read(2)&.unpack1("v") || 0
|
|
333
336
|
|
|
334
|
-
#
|
|
335
|
-
remaining = size -
|
|
337
|
+
# 7 bytes consumed (CRC/TYPE/FLAGS/SIZE); skip the rest
|
|
338
|
+
remaining = size - 7
|
|
336
339
|
io.read(remaining) if remaining.positive?
|
|
337
340
|
next
|
|
338
341
|
end
|
|
339
342
|
|
|
340
|
-
# Parse this file block
|
|
341
343
|
test_entry = parser.parse_file_block(io)
|
|
342
344
|
|
|
343
|
-
# Check if this is our entry
|
|
344
345
|
if test_entry && test_entry.name == entry.name
|
|
345
|
-
|
|
346
|
-
# So we need to back up and read it
|
|
347
|
-
data_end = io.pos
|
|
348
|
-
data_start = data_end - entry.compressed_size
|
|
346
|
+
data_start = io.pos - entry.compressed_size
|
|
349
347
|
|
|
350
348
|
io.seek(data_start)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return StringIO.new(compressed)
|
|
349
|
+
return StringIO.new(io.read(entry.compressed_size) || "")
|
|
354
350
|
end
|
|
355
351
|
end
|
|
356
352
|
end
|
|
357
353
|
|
|
358
|
-
# If we didn't find it, return empty
|
|
359
354
|
StringIO.new("")
|
|
360
355
|
end
|
|
361
356
|
end
|
|
@@ -169,7 +169,7 @@ module Omnizip
|
|
|
169
169
|
warn "RAR4 writer ignores volume_size: not implemented " \
|
|
170
170
|
"(no corresponding flag is written)"
|
|
171
171
|
end
|
|
172
|
-
return unless @options[:recovery]
|
|
172
|
+
return unless @options[:recovery]&.to_i&.positive?
|
|
173
173
|
|
|
174
174
|
warn "RAR4 writer ignores recovery: not implemented " \
|
|
175
175
|
"(no corresponding flag is written)"
|
data/lib/omnizip/formats/rar.rb
CHANGED
|
@@ -127,6 +127,9 @@ module Omnizip
|
|
|
127
127
|
class << self
|
|
128
128
|
# Create a RAR archive, yielding the writer for entries.
|
|
129
129
|
# RAR5 is the default; pass version: 4 for the RAR4 writer.
|
|
130
|
+
# RAR5-only options (solid, multi-volume, password, recovery)
|
|
131
|
+
# are ignored for RAR4 — its writer warns about the
|
|
132
|
+
# unimplemented ones and raises for passwords.
|
|
130
133
|
#
|
|
131
134
|
# @param path [String] Output archive path
|
|
132
135
|
# @param options [Hash] Writer options
|
|
@@ -137,7 +140,7 @@ module Omnizip
|
|
|
137
140
|
writer = if version == 4
|
|
138
141
|
Writer.new(path, options)
|
|
139
142
|
else
|
|
140
|
-
Rar5::Writer.new(path, options)
|
|
143
|
+
Rar5::Writer.new(path, rar5_options(options))
|
|
141
144
|
end
|
|
142
145
|
yield writer if block_given?
|
|
143
146
|
writer.write
|
|
@@ -173,6 +176,16 @@ module Omnizip
|
|
|
173
176
|
def repair(archive_path, output_path)
|
|
174
177
|
ArchiveRepairer.new.repair(archive_path, output_path)
|
|
175
178
|
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Writer keywords the RAR5 writer understands
|
|
183
|
+
def rar5_options(options)
|
|
184
|
+
options.slice(:compression, :level, :solid, :multi_volume,
|
|
185
|
+
:volume_size, :volume_naming, :password,
|
|
186
|
+
:kdf_iterations, :recovery, :recovery_percent,
|
|
187
|
+
:include_mtime, :include_crc32, :dict_size)
|
|
188
|
+
end
|
|
176
189
|
end
|
|
177
190
|
end
|
|
178
191
|
end
|