omnizip 0.3.37 → 0.3.39
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 +78 -9
- data/README.adoc +108 -140
- data/config/formats/rar3_spec.yml +1 -0
- data/docs/guides/archive-formats/gzip-format.adoc +2 -4
- data/docs/guides/archive-formats/ole-format.adoc +54 -219
- data/docs/guides/archive-formats/rar5.adoc +15 -15
- data/docs/guides/archive-formats/rpm-format.adoc +48 -189
- data/docs/reference/api/overview.adoc +16 -17
- data/docs/troubleshooting/index.adoc +1 -4
- data/lib/omnizip/archive_handler.rb +2 -0
- data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
- data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
- data/lib/omnizip/archive_handlers.rb +2 -0
- data/lib/omnizip/cli.rb +25 -0
- data/lib/omnizip/convenience.rb +6 -4
- data/lib/omnizip/formats/iso/directory_builder.rb +24 -0
- data/lib/omnizip/formats/iso/reader.rb +2 -2
- data/lib/omnizip/formats/iso/writer.rb +20 -4
- data/lib/omnizip/formats/iso.rb +3 -1
- data/lib/omnizip/formats/rar/block_parser.rb +18 -8
- data/lib/omnizip/formats/rar/constants.rb +4 -0
- data/lib/omnizip/formats/rar/decompressor.rb +12 -8
- data/lib/omnizip/formats/rar/header.rb +7 -15
- data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +5 -3
- data/lib/omnizip/formats/rar/rar5/header.rb +50 -33
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +17 -34
- data/lib/omnizip/formats/rar/rar5/vint.rb +25 -37
- data/lib/omnizip/formats/rar/rar5/writer.rb +50 -14
- data/lib/omnizip/formats/rar/reader.rb +18 -14
- data/lib/omnizip/formats/rar/writer.rb +121 -136
- data/lib/omnizip/formats/rar.rb +5 -11
- data/lib/omnizip/formats/rar3/reader.rb +11 -50
- data/lib/omnizip/formats/rar3/writer.rb +13 -10
- data/lib/omnizip/formats/rar3.rb +15 -0
- data/lib/omnizip/formats/rar5/reader.rb +8 -10
- data/lib/omnizip/formats/rar5.rb +15 -0
- data/lib/omnizip/formats/seven_zip/parser.rb +22 -7
- data/lib/omnizip/formats/seven_zip/writer.rb +78 -19
- data/lib/omnizip/formats.rb +2 -0
- data/lib/omnizip/version.rb +1 -1
- data/readme-docs/api-usage.adoc +5 -4
- data/readme-docs/architecture.adoc +5 -4
- metadata +5 -1
|
@@ -89,9 +89,18 @@ header_data: "")
|
|
|
89
89
|
|
|
90
90
|
# Main archive header
|
|
91
91
|
class MainHeader < Header
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
# Archive flags: 0x0001 volume, 0x0002 volume number
|
|
93
|
+
# present, 0x0004 solid, 0x0008 recovery, 0x0010 locked
|
|
94
|
+
ARCHIVE_FLAG_VOLUME = 0x0001
|
|
95
|
+
ARCHIVE_FLAG_VOLUME_NUMBER = 0x0002
|
|
96
|
+
|
|
97
|
+
def initialize(archive_flags: 0, volume_number: nil)
|
|
98
|
+
data = VINT.encode(archive_flags)
|
|
99
|
+
if archive_flags.anybits?(ARCHIVE_FLAG_VOLUME_NUMBER)
|
|
100
|
+
data.concat(VINT.encode(volume_number.to_i))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
super(HEADER_TYPE_MAIN, header_data: data.pack("C*"))
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
|
|
@@ -102,69 +111,73 @@ header_data: "")
|
|
|
102
111
|
FILE_HAS_MTIME = 0x0002
|
|
103
112
|
FILE_HAS_CRC32 = 0x0004
|
|
104
113
|
|
|
114
|
+
# Compression information field layout (RAR 5.0 spec):
|
|
115
|
+
# bits 0-5 algorithm version (0), bit 7 solid,
|
|
116
|
+
# bits 8-10 method (0=store, 1-5=LZMA), bits 11-15
|
|
117
|
+
# dictionary size (128 KB * 2^N).
|
|
118
|
+
DICT_BASE = 128 * 1024
|
|
119
|
+
|
|
105
120
|
def initialize(filename:, file_size:, compressed_size:,
|
|
106
|
-
compression_method: 0,
|
|
121
|
+
compression_method: 0, dict_size: 0,
|
|
122
|
+
flags: 0, mtime: nil, crc32: nil, extra_area: nil)
|
|
107
123
|
# Build file flags based on what's provided
|
|
108
124
|
file_flags = 0
|
|
109
125
|
file_flags |= FILE_HAS_MTIME if mtime
|
|
110
126
|
file_flags |= FILE_HAS_CRC32 if crc32
|
|
111
127
|
|
|
112
128
|
# Build header data with file information
|
|
113
|
-
data = build_file_data(filename, file_size,
|
|
114
|
-
|
|
129
|
+
data = build_file_data(filename, file_size,
|
|
130
|
+
compression_method, dict_size,
|
|
131
|
+
file_flags, mtime, crc32)
|
|
115
132
|
super(HEADER_TYPE_FILE, flags: flags, data_area_size: compressed_size, header_data: data, extra_area: extra_area)
|
|
116
133
|
end
|
|
117
134
|
|
|
118
135
|
private
|
|
119
136
|
|
|
120
|
-
def build_file_data(filename, file_size,
|
|
121
|
-
|
|
137
|
+
def build_file_data(filename, file_size, compression_method,
|
|
138
|
+
dict_size, file_flags, mtime, crc32)
|
|
122
139
|
data = []
|
|
123
140
|
|
|
124
141
|
# File flags (VINT)
|
|
125
142
|
data.concat(VINT.encode(file_flags))
|
|
126
143
|
|
|
127
|
-
#
|
|
144
|
+
# Unpacked size (VINT)
|
|
128
145
|
data.concat(VINT.encode(file_size))
|
|
129
146
|
|
|
130
|
-
# Attributes (VINT) - ALWAYS present in RAR5
|
|
131
|
-
#
|
|
132
|
-
data.concat(VINT.encode(
|
|
133
|
-
|
|
134
|
-
# Mystery VINT with value 0x02 - observed in official RAR
|
|
135
|
-
# This appears after attributes in official archives - ALWAYS present
|
|
136
|
-
data.concat(VINT.encode(0x02))
|
|
147
|
+
# Attributes (VINT) - ALWAYS present in RAR5.
|
|
148
|
+
# Unix host: standard regular-file mode 0o100644.
|
|
149
|
+
data.concat(VINT.encode(0o100644))
|
|
137
150
|
|
|
138
151
|
# mtime (optional) - only if FILE_HAS_MTIME flag is set
|
|
139
|
-
# RAR5 stores mtime as Unix timestamp (seconds since epoch)
|
|
152
|
+
# RAR5 stores mtime as Unix timestamp (seconds since epoch)
|
|
140
153
|
# Format: 4 bytes little-endian (NOT a VINT)
|
|
141
154
|
if file_flags.anybits?(FILE_HAS_MTIME) && mtime
|
|
142
|
-
|
|
143
|
-
unix_time = mtime.to_i
|
|
144
|
-
# Pack as 32-bit unsigned little-endian
|
|
145
|
-
data.concat([unix_time].pack("V").bytes)
|
|
155
|
+
data.concat([mtime.to_i].pack("V").bytes)
|
|
146
156
|
end
|
|
147
157
|
|
|
148
158
|
# Data CRC32 (optional) - only if FILE_HAS_CRC32 flag is set
|
|
149
159
|
# Format: 4 bytes little-endian (NOT a VINT)
|
|
150
160
|
if file_flags.anybits?(FILE_HAS_CRC32) && crc32
|
|
151
|
-
# Pack as 32-bit unsigned little-endian
|
|
152
161
|
data.concat([crc32].pack("V").bytes)
|
|
153
162
|
end
|
|
154
163
|
|
|
155
|
-
# Compression
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
164
|
+
# Compression information (VINT): version 0 in bits 0-5,
|
|
165
|
+
# method in bits 8-10, dictionary size in bits 11-15
|
|
166
|
+
# (128 KB * 2^N).
|
|
167
|
+
dict_bits = if dict_size.positive?
|
|
168
|
+
Math.log2(dict_size / DICT_BASE).round.clamp(0, 15)
|
|
169
|
+
else
|
|
170
|
+
0
|
|
171
|
+
end
|
|
172
|
+
compression_info = (compression_method << 8) | (dict_bits << 11)
|
|
173
|
+
data.concat(VINT.encode(compression_info))
|
|
159
174
|
|
|
160
175
|
# Host OS (VINT) - 1 = Unix
|
|
161
|
-
data.concat(VINT.encode(1))
|
|
176
|
+
data.concat(VINT.encode(1))
|
|
162
177
|
|
|
163
|
-
# Name length (VINT)
|
|
178
|
+
# Name length (VINT) and name
|
|
164
179
|
name_bytes = filename.encode("UTF-8").bytes
|
|
165
180
|
data.concat(VINT.encode(name_bytes.size))
|
|
166
|
-
|
|
167
|
-
# Name
|
|
168
181
|
data.concat(name_bytes)
|
|
169
182
|
|
|
170
183
|
data.pack("C*")
|
|
@@ -173,9 +186,13 @@ file_flags, compression_method, mtime, crc32)
|
|
|
173
186
|
|
|
174
187
|
# End of archive header
|
|
175
188
|
class EndHeader < Header
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
# End of archive flags: 0x0001 volume and not the last
|
|
190
|
+
# volume in the set
|
|
191
|
+
END_FLAG_VOLUME_NEXT = 0x0001
|
|
192
|
+
|
|
193
|
+
def initialize(end_flags: 0)
|
|
194
|
+
super(HEADER_TYPE_END,
|
|
195
|
+
header_data: VINT.encode(end_flags).pack("C*"))
|
|
179
196
|
end
|
|
180
197
|
end
|
|
181
198
|
end
|
|
@@ -18,11 +18,10 @@ module Omnizip
|
|
|
18
18
|
# writer.write_end_header
|
|
19
19
|
# writer.close
|
|
20
20
|
class VolumeWriter
|
|
21
|
-
# Main
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
VOLUME_NUMBER_FLAG = 0x0008 # Bit 3: Volume number present in extra area
|
|
21
|
+
# Main ARCHIVE flags (spec): 0x0001 volume,
|
|
22
|
+
# 0x0002 volume number field present
|
|
23
|
+
VOLUME_ARCHIVE_FLAG = MainHeader::ARCHIVE_FLAG_VOLUME
|
|
24
|
+
VOLUME_NUMBER_FLAG = MainHeader::ARCHIVE_FLAG_VOLUME_NUMBER
|
|
26
25
|
|
|
27
26
|
# End header flags
|
|
28
27
|
VOLUME_END_FLAG = 0x0001 # Not the last volume (more volumes follow)
|
|
@@ -95,26 +94,18 @@ module Omnizip
|
|
|
95
94
|
def write_main_header
|
|
96
95
|
raise "Volume not open" unless @io
|
|
97
96
|
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
nil
|
|
97
|
+
# Archive flags: every volume declares 0x0001; volumes
|
|
98
|
+
# after the first also carry the volume number field
|
|
99
|
+
# (spec: number is N-1, absent on the first volume).
|
|
100
|
+
archive_flags = VOLUME_ARCHIVE_FLAG
|
|
101
|
+
volume_number = nil
|
|
103
102
|
if @volume_number > 1
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
VINT.encode(@volume_number).pack("C*")
|
|
103
|
+
archive_flags |= VOLUME_NUMBER_FLAG
|
|
104
|
+
volume_number = @volume_number - 1
|
|
107
105
|
end
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# Extra areas are not written: MainHeader does not
|
|
112
|
-
# model them and the omnizip-rs reference implements
|
|
113
|
-
# RAR5 reading only, so there is no writer-side spec
|
|
114
|
-
# to port. Volumes work without extras (the field is
|
|
115
|
-
# optional).
|
|
116
|
-
|
|
117
|
-
@io.write(header.encode)
|
|
107
|
+
@io.write(MainHeader.new(archive_flags: archive_flags,
|
|
108
|
+
volume_number: volume_number).encode)
|
|
118
109
|
end
|
|
119
110
|
|
|
120
111
|
# Write file data (header + compressed data)
|
|
@@ -135,18 +126,10 @@ module Omnizip
|
|
|
135
126
|
def write_end_header
|
|
136
127
|
raise "Volume not open" unless @io
|
|
137
128
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# EndHeader always encodes the plain END_OF_ARCHIVE
|
|
143
|
-
# marker: the RAR5 spec makes the volume flag optional,
|
|
144
|
-
# readers position the marker inside the volume
|
|
145
|
-
# sequence, and the omnizip-rs reference (read-side)
|
|
146
|
-
# carries no writer-side volume-flag encoding to port.
|
|
147
|
-
header = EndHeader.new
|
|
148
|
-
|
|
149
|
-
@io.write(header.encode)
|
|
129
|
+
# End-of-archive flags: 0x0001 marks a volume with more
|
|
130
|
+
# volumes following; the last volume ends with 0.
|
|
131
|
+
end_flags = @is_last ? 0 : VOLUME_END_FLAG
|
|
132
|
+
@io.write(EndHeader.new(end_flags: end_flags).encode)
|
|
150
133
|
end
|
|
151
134
|
|
|
152
135
|
# Generate volume filename from base name
|
|
@@ -6,57 +6,45 @@ module Omnizip
|
|
|
6
6
|
module Rar5
|
|
7
7
|
# Variable-length integer encoding/decoding for RAR5 format
|
|
8
8
|
module VINT
|
|
9
|
-
# Encode integer as VINT bytes
|
|
10
|
-
#
|
|
9
|
+
# Encode integer as VINT bytes per the RAR 5.0 spec: one or
|
|
10
|
+
# more bytes, each carrying 7 data bits starting with the
|
|
11
|
+
# least significant group; the high bit of every byte but
|
|
12
|
+
# the last is the continuation flag.
|
|
13
|
+
#
|
|
14
|
+
# @param value [Integer] Value to encode (0 to 2^63)
|
|
11
15
|
# @return [Array<Integer>] VINT bytes
|
|
12
16
|
def self.encode(value)
|
|
13
|
-
|
|
17
|
+
raise ArgumentError, "VINT cannot encode negative values" if value.negative?
|
|
14
18
|
|
|
15
19
|
bytes = []
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# First byte: continuation bits + high bits
|
|
24
|
-
first_byte = (0xFF << (9 - byte_count)) & 0xFF
|
|
25
|
-
first_byte |= (value >> (8 * (byte_count - 1))) & 0x7F
|
|
26
|
-
bytes << first_byte
|
|
27
|
-
|
|
28
|
-
# Remaining bytes
|
|
29
|
-
(byte_count - 1).downto(1) do |i|
|
|
30
|
-
bytes << ((value >> (8 * (i - 1))) & 0xFF)
|
|
20
|
+
loop do
|
|
21
|
+
byte = value & 0x7F
|
|
22
|
+
value >>= 7
|
|
23
|
+
byte |= 0x80 if value.positive?
|
|
24
|
+
bytes << byte
|
|
25
|
+
break if value.zero?
|
|
31
26
|
end
|
|
32
27
|
|
|
33
28
|
bytes
|
|
34
29
|
end
|
|
35
30
|
|
|
36
|
-
# Decode VINT from IO stream
|
|
31
|
+
# Decode VINT from IO stream (spec encoding, mirroring
|
|
32
|
+
# BlockParser#read_vint)
|
|
33
|
+
#
|
|
37
34
|
# @param io [IO] Input stream
|
|
38
35
|
# @return [Integer] Decoded value
|
|
39
36
|
def self.decode(io)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
mask >>= 1
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Extract value from first byte
|
|
52
|
-
value = first_byte & (0xFF >> (byte_count + 1))
|
|
53
|
-
|
|
54
|
-
# Read remaining bytes
|
|
55
|
-
byte_count.times do
|
|
56
|
-
value = (value << 8) | io.readbyte
|
|
37
|
+
result = 0
|
|
38
|
+
shift = 0
|
|
39
|
+
loop do
|
|
40
|
+
byte = io.readbyte
|
|
41
|
+
result |= (byte & 0x7F) << shift
|
|
42
|
+
break if byte.nobits?(0x80)
|
|
43
|
+
|
|
44
|
+
shift += 7
|
|
57
45
|
end
|
|
58
46
|
|
|
59
|
-
|
|
47
|
+
result
|
|
60
48
|
end
|
|
61
49
|
end
|
|
62
50
|
end
|
|
@@ -177,7 +177,10 @@ module Omnizip
|
|
|
177
177
|
write_signature(f)
|
|
178
178
|
write_main_header(f)
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
# Solid mode needs a RAR-compatible LZSS encoder; until
|
|
181
|
+
# one lands, fall back to independent STORE entries so
|
|
182
|
+
# the archive is actually extractable by unrar.
|
|
183
|
+
if @options[:solid] && Compression::Lzss.available?
|
|
181
184
|
write_solid_block(f, @files)
|
|
182
185
|
else
|
|
183
186
|
@files.each do |file|
|
|
@@ -251,6 +254,7 @@ module Omnizip
|
|
|
251
254
|
file_size: file_info[:size],
|
|
252
255
|
compressed_size: (idx.zero? ? result[:compressed_size] : 0), # Only first file has compressed size
|
|
253
256
|
compression_method: compression_method,
|
|
257
|
+
dict_size: dictionary_size_for(compression_method),
|
|
254
258
|
mtime: @options[:include_mtime] ? file[:mtime] : nil,
|
|
255
259
|
crc32: nil, # No CRC32 for solid/LZMA
|
|
256
260
|
)
|
|
@@ -307,12 +311,13 @@ module Omnizip
|
|
|
307
311
|
if @encryption_manager
|
|
308
312
|
encryption_result = @encryption_manager.encrypt_file_data(compressed_data)
|
|
309
313
|
final_data = encryption_result[:encrypted_data]
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
# Persist the salt/IV as the RAR5 file encryption extra
|
|
315
|
+
# record (spec type 0x01) — without it the archive
|
|
316
|
+
# cannot be decrypted by anything.
|
|
317
|
+
enc_record = build_encryption_extra_record(
|
|
318
|
+
encryption_result[:header],
|
|
319
|
+
)
|
|
320
|
+
extra_area = extra_area ? extra_area + enc_record : enc_record
|
|
316
321
|
else
|
|
317
322
|
final_data = compressed_data
|
|
318
323
|
end
|
|
@@ -332,6 +337,7 @@ module Omnizip
|
|
|
332
337
|
file_size: data.bytesize,
|
|
333
338
|
compressed_size: final_data.bytesize,
|
|
334
339
|
compression_method: compression_method,
|
|
340
|
+
dict_size: dictionary_size_for(compression_method),
|
|
335
341
|
mtime: @options[:include_mtime] ? file[:mtime] : nil,
|
|
336
342
|
crc32: file_crc32,
|
|
337
343
|
extra_area: extra_area,
|
|
@@ -356,6 +362,22 @@ module Omnizip
|
|
|
356
362
|
#
|
|
357
363
|
# @param data [String] File data
|
|
358
364
|
# @return [Integer] Compression method ID
|
|
365
|
+
# Dictionary size matching the compression modules' level
|
|
366
|
+
# table (128 KB << N), for the file header's dictionary
|
|
367
|
+
# bits. STORE needs no dictionary.
|
|
368
|
+
def dictionary_size_for(method)
|
|
369
|
+
return 0 if method == Compression::Store::METHOD
|
|
370
|
+
|
|
371
|
+
1 << case method
|
|
372
|
+
when 1 then 18 # 256 KB
|
|
373
|
+
when 2 then 20 # 1 MB
|
|
374
|
+
when 3 then 22 # 4 MB
|
|
375
|
+
when 4 then 23 # 8 MB
|
|
376
|
+
when 5 then 24 # 16 MB
|
|
377
|
+
else 22
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
359
381
|
def select_compression_method(data)
|
|
360
382
|
case @options[:compression]
|
|
361
383
|
when :store
|
|
@@ -367,7 +389,9 @@ module Omnizip
|
|
|
367
389
|
if Compression::Lzss.available?
|
|
368
390
|
Compression::Lzss.method_id(level)
|
|
369
391
|
else
|
|
370
|
-
#
|
|
392
|
+
warn "omnizip: RAR5 #{@options[:compression] == :lzma ? 'lzma' : 'lzss'} " \
|
|
393
|
+
"compression is not interoperable yet; storing data " \
|
|
394
|
+
"uncompressed instead"
|
|
371
395
|
Compression::Store::METHOD
|
|
372
396
|
end
|
|
373
397
|
when :auto
|
|
@@ -418,15 +442,27 @@ module Omnizip
|
|
|
418
442
|
def build_compression_extra_area(properties)
|
|
419
443
|
return nil if properties.nil?
|
|
420
444
|
|
|
421
|
-
|
|
445
|
+
# Extra record format per the RAR 5.0 spec: Size(vint),
|
|
446
|
+
# Type(vint), Data. Type 0x03 carries the compression
|
|
447
|
+
# parameters.
|
|
448
|
+
record = VINT.encode(0x03) + properties.bytes
|
|
449
|
+
VINT.encode(record.length) + record
|
|
450
|
+
end
|
|
422
451
|
|
|
423
|
-
|
|
424
|
-
|
|
452
|
+
# File encryption extra record (spec type 0x01): AES-256
|
|
453
|
+
# parameters the reader needs to derive the key and IV.
|
|
454
|
+
def build_encryption_extra_record(header)
|
|
455
|
+
require "base64"
|
|
425
456
|
|
|
426
|
-
|
|
427
|
-
|
|
457
|
+
kdf_count = Math.log2(header.kdf_iterations).round
|
|
458
|
+
record = [].concat(VINT.encode(0x01)) # record type
|
|
459
|
+
.concat(VINT.encode(header.version)) # 0 = AES-256
|
|
460
|
+
.concat(VINT.encode(0)) # flags: no check data
|
|
461
|
+
record << kdf_count # 1 byte, log2
|
|
462
|
+
record.concat(Base64.strict_decode64(header.salt).bytes)
|
|
463
|
+
record.concat(Base64.strict_decode64(header.iv).bytes)
|
|
428
464
|
|
|
429
|
-
|
|
465
|
+
(VINT.encode(record.length) + record).pack("C*")
|
|
430
466
|
end
|
|
431
467
|
|
|
432
468
|
# Generate PAR2 recovery files for archives
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "stringio"
|
|
5
|
+
require "zlib"
|
|
5
6
|
|
|
6
7
|
module Omnizip
|
|
7
8
|
module Formats
|
|
@@ -246,11 +247,7 @@ module Omnizip
|
|
|
246
247
|
# Native decompression only for RAR4 for now
|
|
247
248
|
return false unless @header.version == 4
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
return false if entry.nil? || entry.method.nil?
|
|
251
|
-
|
|
252
|
-
# All RAR4 methods are supported by our Dispatcher
|
|
253
|
-
true
|
|
250
|
+
!entry.nil? && !entry.method.nil?
|
|
254
251
|
end
|
|
255
252
|
|
|
256
253
|
# Extract entry using native decompression
|
|
@@ -258,17 +255,24 @@ module Omnizip
|
|
|
258
255
|
# @param entry [Models::RarEntry] File entry
|
|
259
256
|
# @param output_path [String] Destination path
|
|
260
257
|
def extract_entry_native(entry, output_path)
|
|
261
|
-
# Read compressed data from archive
|
|
262
258
|
compressed_data = read_compressed_data(entry)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
259
|
+
method = entry.method || 0x30
|
|
260
|
+
|
|
261
|
+
output = StringIO.new(+"")
|
|
262
|
+
Compression::Dispatcher.decompress(method, compressed_data, output)
|
|
263
|
+
|
|
264
|
+
# The native LZ/PPMd decoders understand Omnizip's own
|
|
265
|
+
# encoder output, not official RAR streams — without this
|
|
266
|
+
# check, real WinRAR archives would silently extract as
|
|
267
|
+
# garbage. The header CRC is authoritative: on mismatch,
|
|
268
|
+
# fall back to unrar.
|
|
269
|
+
data = output.string
|
|
270
|
+
if entry.crc && Zlib.crc32(data) != entry.crc
|
|
271
|
+
raise Compression::DecompressionError,
|
|
272
|
+
"CRC mismatch for #{entry.name}"
|
|
271
273
|
end
|
|
274
|
+
|
|
275
|
+
File.binwrite(output_path, data)
|
|
272
276
|
rescue StandardError => e
|
|
273
277
|
# Fall back to external decompressor on error
|
|
274
278
|
warn "Native decompression failed for #{entry.name}: #{e.message}"
|