omnizip 0.3.39 → 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 +102 -0
- data/README.adoc +31 -0
- data/lib/omnizip/algorithm_registry.rb +7 -0
- data/lib/omnizip/algorithms/deflate64.rb +5 -8
- data/lib/omnizip/buffer/memory_extractor.rb +31 -6
- data/lib/omnizip/buffer/seven_zip_bridge.rb +154 -0
- data/lib/omnizip/buffer.rb +6 -5
- data/lib/omnizip/commands/archive_create_command.rb +18 -25
- data/lib/omnizip/commands/decompress_command.rb +29 -1
- data/lib/omnizip/commands/metadata_command.rb +67 -3
- data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +14 -52
- 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 +31 -0
- 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/formats/seven_zip/writer.rb +17 -0
- data/lib/omnizip/version.rb +1 -1
- data/readme-docs/api-usage.adoc +1 -1
- data/readme-docs/architecture.adoc +1 -1
- data/readme-docs/compression-algorithms.adoc +1 -1
- data/readme-docs/installation.adoc +1 -1
- data/readme-docs/performance-profiler.adoc +1 -1
- metadata +3 -2
|
@@ -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 7-Zip archives to ZIP format
|
|
@@ -8,18 +10,22 @@ module Omnizip
|
|
|
8
10
|
# @return [ConversionResult] Conversion result
|
|
9
11
|
def convert
|
|
10
12
|
start_time = Time.now
|
|
11
|
-
0
|
|
12
13
|
|
|
13
|
-
# Open source 7z archive
|
|
14
14
|
reader = Omnizip::Formats::SevenZip::Reader.new(source_path)
|
|
15
|
-
reader.
|
|
15
|
+
reader.open
|
|
16
|
+
entry_count = reader.list_files.size
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
entry_count = entries_data.size
|
|
18
|
+
Dir.mktmpdir("omnizip_convert") do |tmp|
|
|
19
|
+
reader.extract_all(tmp)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
writer = Omnizip::Formats::Zip::Writer.new(target_path)
|
|
22
|
+
Dir.glob(File.join(tmp, "**", "*")).each do |path|
|
|
23
|
+
next if File.directory?(path)
|
|
24
|
+
|
|
25
|
+
writer.add_file(path, path.delete_prefix("#{tmp}/"))
|
|
26
|
+
end
|
|
27
|
+
writer.write
|
|
28
|
+
end
|
|
23
29
|
|
|
24
30
|
create_result(start_time, entry_count)
|
|
25
31
|
end
|
|
@@ -43,50 +49,6 @@ module Omnizip
|
|
|
43
49
|
def self.can_convert?(source, target)
|
|
44
50
|
source.end_with?(".7z") && target.end_with?(".zip")
|
|
45
51
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def collect_seven_zip_entries(reader)
|
|
50
|
-
entries = []
|
|
51
|
-
|
|
52
|
-
reader.entries.each do |entry|
|
|
53
|
-
data = {
|
|
54
|
-
name: entry.name,
|
|
55
|
-
content: nil,
|
|
56
|
-
mtime: entry.mtime || Time.now,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# Extract entry data
|
|
60
|
-
unless entry.name.end_with?("/")
|
|
61
|
-
File.open(source_path, "rb") do |io|
|
|
62
|
-
data[:content] = reader.extract_entry_data(io, entry)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
entries << data
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
entries
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def create_zip(entries)
|
|
73
|
-
Omnizip::Zip::File.create(target_path) do |zip|
|
|
74
|
-
entries.each do |entry_data|
|
|
75
|
-
if entry_data[:name].end_with?("/")
|
|
76
|
-
# Directory entry
|
|
77
|
-
zip.add(entry_data[:name])
|
|
78
|
-
else
|
|
79
|
-
# File entry
|
|
80
|
-
zip.add(entry_data[:name]) { entry_data[:content] }
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Set archive comment if preserving metadata
|
|
85
|
-
if options.preserve_metadata
|
|
86
|
-
zip.comment = "Converted from 7z by Omnizip"
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
52
|
end
|
|
91
53
|
end
|
|
92
54
|
end
|
|
@@ -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
|
@@ -125,6 +125,27 @@ module Omnizip
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
class << self
|
|
128
|
+
# Create a RAR archive, yielding the writer for entries.
|
|
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.
|
|
133
|
+
#
|
|
134
|
+
# @param path [String] Output archive path
|
|
135
|
+
# @param options [Hash] Writer options
|
|
136
|
+
# @return [String, Array<String>] Path(s) of the created
|
|
137
|
+
# archive (a volume set returns every volume)
|
|
138
|
+
def create(path, options = {})
|
|
139
|
+
version = options.fetch(:version, 5)
|
|
140
|
+
writer = if version == 4
|
|
141
|
+
Writer.new(path, options)
|
|
142
|
+
else
|
|
143
|
+
Rar5::Writer.new(path, rar5_options(options))
|
|
144
|
+
end
|
|
145
|
+
yield writer if block_given?
|
|
146
|
+
writer.write
|
|
147
|
+
end
|
|
148
|
+
|
|
128
149
|
# Check if RAR extraction is available
|
|
129
150
|
#
|
|
130
151
|
# @return [Boolean] true if unrar available
|
|
@@ -155,6 +176,16 @@ module Omnizip
|
|
|
155
176
|
def repair(archive_path, output_path)
|
|
156
177
|
ArchiveRepairer.new.repair(archive_path, output_path)
|
|
157
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
|
|
158
189
|
end
|
|
159
190
|
end
|
|
160
191
|
end
|