omnizip 0.3.38 → 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 +2 -0
- data/README.adoc +108 -140
- data/config/formats/rar3_spec.yml +1 -0
- 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/reader.rb +18 -14
- data/lib/omnizip/formats/rar/writer.rb +121 -136
- data/lib/omnizip/formats/rar.rb +0 -8
- 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.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 +3 -1
|
@@ -3,14 +3,22 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "zlib"
|
|
5
5
|
require "stringio"
|
|
6
|
+
require "tmpdir"
|
|
6
7
|
|
|
7
8
|
module Omnizip
|
|
8
9
|
module Formats
|
|
9
10
|
module Rar
|
|
10
11
|
# Pure Ruby RAR archive writer
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
13
|
+
# Writes RAR 1.5-4.x ("RAR4") archives per the on-disk layout
|
|
14
|
+
# unrar accepts: HEAD_CRC holds the low 16 bits of a CRC32 over
|
|
15
|
+
# the header bytes that follow it, HEAD_SIZE counts the whole
|
|
16
|
+
# block including the CRC field, and the 7-byte signature is
|
|
17
|
+
# itself the marker block (no second marker follows).
|
|
18
|
+
#
|
|
19
|
+
# Only METHOD_STORE output is interoperable with official RAR
|
|
20
|
+
# tools; the LZ/PPMd encoders produce Omnizip-internal streams
|
|
21
|
+
# that Omnizip can read back but unrar cannot.
|
|
14
22
|
#
|
|
15
23
|
# @example Create a RAR archive
|
|
16
24
|
# writer = Writer.new('archive.rar')
|
|
@@ -20,9 +28,7 @@ module Omnizip
|
|
|
20
28
|
#
|
|
21
29
|
# @example Create with options
|
|
22
30
|
# writer = Writer.new('archive.rar',
|
|
23
|
-
# compression: :best
|
|
24
|
-
# solid: true,
|
|
25
|
-
# recovery: 5
|
|
31
|
+
# compression: :best
|
|
26
32
|
# )
|
|
27
33
|
class Writer
|
|
28
34
|
include Omnizip::Formats::Rar::Constants
|
|
@@ -64,16 +70,27 @@ module Omnizip
|
|
|
64
70
|
# @option options [Symbol] :compression Compression level
|
|
65
71
|
# (:store, :fastest, :fast, :normal, :good, :best)
|
|
66
72
|
# @option options [Boolean] :solid Create solid archive
|
|
67
|
-
#
|
|
68
|
-
# @option options [
|
|
73
|
+
# (not implemented; ignored with a warning)
|
|
74
|
+
# @option options [Integer] :recovery Recovery record
|
|
75
|
+
# percentage (not implemented; ignored with a warning)
|
|
69
76
|
# @option options [String] :password Archive password
|
|
70
|
-
#
|
|
77
|
+
# (RAR4 encryption is not implemented)
|
|
78
|
+
# @option options [Integer] :volume_size Split into volumes
|
|
79
|
+
# (not implemented; ignored with a warning)
|
|
71
80
|
# @option options [Boolean] :test_after_create Test archive after creation
|
|
81
|
+
# @raise [NotImplementedError] if :password or :encrypt_headers is given
|
|
72
82
|
def initialize(output_path, options = {})
|
|
73
83
|
@output_path = output_path
|
|
74
84
|
@options = default_options.merge(options)
|
|
75
85
|
@files = []
|
|
76
86
|
@directories = []
|
|
87
|
+
|
|
88
|
+
if @options[:password] || @options[:encrypt_headers]
|
|
89
|
+
raise NotImplementedError,
|
|
90
|
+
"RAR4 encryption is not implemented"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
warn_unimplemented_options
|
|
77
94
|
end
|
|
78
95
|
|
|
79
96
|
# Add file to archive
|
|
@@ -114,13 +131,11 @@ module Omnizip
|
|
|
114
131
|
def write
|
|
115
132
|
File.open(@output_path, "wb") do |io|
|
|
116
133
|
write_signature(io)
|
|
117
|
-
write_marker_block(io)
|
|
118
134
|
write_archive_header(io)
|
|
119
135
|
write_file_entries(io)
|
|
120
136
|
write_end_block(io)
|
|
121
137
|
end
|
|
122
138
|
|
|
123
|
-
# Test archive if requested
|
|
124
139
|
test_archive if @options[:test_after_create]
|
|
125
140
|
|
|
126
141
|
@output_path
|
|
@@ -143,20 +158,30 @@ module Omnizip
|
|
|
143
158
|
}
|
|
144
159
|
end
|
|
145
160
|
|
|
146
|
-
#
|
|
161
|
+
# Warn about accepted-but-unimplemented options so the
|
|
162
|
+
# resulting archive never advertises features it lacks.
|
|
163
|
+
def warn_unimplemented_options
|
|
164
|
+
if @options[:solid]
|
|
165
|
+
warn "RAR4 writer ignores solid: not implemented " \
|
|
166
|
+
"(no corresponding flag is written)"
|
|
167
|
+
end
|
|
168
|
+
if @options[:volume_size]
|
|
169
|
+
warn "RAR4 writer ignores volume_size: not implemented " \
|
|
170
|
+
"(no corresponding flag is written)"
|
|
171
|
+
end
|
|
172
|
+
return unless @options[:recovery].to_i.positive?
|
|
173
|
+
|
|
174
|
+
warn "RAR4 writer ignores recovery: not implemented " \
|
|
175
|
+
"(no corresponding flag is written)"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# RAR4 HEAD_CRC: low 16 bits of the CRC32 over every header
|
|
179
|
+
# byte that follows the 2-byte CRC field (unrar archive.cpp).
|
|
147
180
|
#
|
|
148
|
-
#
|
|
149
|
-
# @param data [String] Header data to checksum
|
|
181
|
+
# @param header_data [String] Header bytes after the CRC field
|
|
150
182
|
# @return [Integer] 16-bit CRC value
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
data.bytes.each do |byte|
|
|
154
|
-
crc ^= (byte << 8)
|
|
155
|
-
8.times do
|
|
156
|
-
crc = crc.anybits?(0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1)
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
crc & 0xFFFF
|
|
183
|
+
def calculate_header_crc(header_data)
|
|
184
|
+
Zlib.crc32(header_data) & 0xFFFF
|
|
160
185
|
end
|
|
161
186
|
|
|
162
187
|
# Convert Ruby Time to DOS time format
|
|
@@ -169,44 +194,23 @@ module Omnizip
|
|
|
169
194
|
(dos_date << 16) | dos_time_part
|
|
170
195
|
end
|
|
171
196
|
|
|
172
|
-
# Write RAR signature
|
|
197
|
+
# Write RAR signature (the marker block itself)
|
|
173
198
|
#
|
|
174
199
|
# @param io [IO] Output stream
|
|
175
200
|
def write_signature(io)
|
|
176
201
|
io.write(RAR4_SIGNATURE.pack("C*"))
|
|
177
202
|
end
|
|
178
203
|
|
|
179
|
-
# Write
|
|
180
|
-
#
|
|
181
|
-
# @param io [IO] Output stream
|
|
182
|
-
def write_marker_block(io)
|
|
183
|
-
# Marker block has fixed CRC of 0x6152
|
|
184
|
-
io.write([0x6152].pack("v")) # HEAD_CRC
|
|
185
|
-
io.write([BLOCK_MARKER].pack("C")) # HEAD_TYPE
|
|
186
|
-
io.write([0x0000].pack("v")) # HEAD_FLAGS
|
|
187
|
-
io.write([0x0007].pack("v")) # HEAD_SIZE
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Write archive header
|
|
204
|
+
# Write archive header (MAIN_HEAD, 13 bytes total)
|
|
191
205
|
#
|
|
192
206
|
# @param io [IO] Output stream
|
|
193
207
|
def write_archive_header(io)
|
|
194
|
-
flags = 0
|
|
195
|
-
flags |= ARCHIVE_SOLID if @options[:solid]
|
|
196
|
-
flags |= ARCHIVE_RECOVERY if @options[:recovery].positive?
|
|
197
|
-
flags |= ARCHIVE_ENCRYPTED if @options[:password]
|
|
198
|
-
flags |= ARCHIVE_VOLUME if @options[:volume_size]
|
|
199
|
-
|
|
200
|
-
# Build header data (without CRC)
|
|
201
|
-
# SIZE includes: TYPE(1) + FLAGS(2) + SIZE(2) + Reserved(6) = 11 bytes
|
|
202
208
|
header_data = [BLOCK_ARCHIVE].pack("C") +
|
|
203
|
-
[
|
|
204
|
-
[
|
|
205
|
-
[0
|
|
209
|
+
[0x0000].pack("v") + # HEAD_FLAGS
|
|
210
|
+
[0x000D].pack("v") + # HEAD_SIZE
|
|
211
|
+
[0].pack("v") + [0].pack("V") # HighPosAV, PosAV
|
|
206
212
|
|
|
207
|
-
|
|
208
|
-
crc = calculate_header_crc16(header_data)
|
|
209
|
-
io.write([crc].pack("v"))
|
|
213
|
+
io.write([calculate_header_crc(header_data)].pack("v"))
|
|
210
214
|
io.write(header_data)
|
|
211
215
|
end
|
|
212
216
|
|
|
@@ -214,13 +218,12 @@ module Omnizip
|
|
|
214
218
|
#
|
|
215
219
|
# @param io [IO] Output stream
|
|
216
220
|
def write_file_entries(io)
|
|
217
|
-
@files.each do |file_info|
|
|
218
|
-
write_file_entry(io, file_info)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
221
|
@directories.each do |dir_info|
|
|
222
222
|
write_directory_entries(io, dir_info)
|
|
223
223
|
end
|
|
224
|
+
@files.each do |file_info|
|
|
225
|
+
write_file_entry(io, file_info)
|
|
226
|
+
end
|
|
224
227
|
end
|
|
225
228
|
|
|
226
229
|
# Write a single file entry
|
|
@@ -230,35 +233,36 @@ module Omnizip
|
|
|
230
233
|
def write_file_entry(io, file_info)
|
|
231
234
|
file_path = file_info[:source]
|
|
232
235
|
archive_path = file_info[:archive_path] || File.basename(file_path)
|
|
236
|
+
directory = file_info[:directory] || false
|
|
233
237
|
|
|
234
|
-
file_data = File.binread(file_path)
|
|
238
|
+
file_data = directory ? "" : File.binread(file_path)
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
method = file_info[:method] ||
|
|
241
|
+
(directory ? METHOD_STORE : select_compression_method(file_data))
|
|
238
242
|
compressed_data = compress_data(file_data, method)
|
|
239
243
|
|
|
240
|
-
# File metadata
|
|
241
244
|
stat = File.stat(file_path)
|
|
242
|
-
file_attr = stat.mode
|
|
245
|
+
file_attr = directory ? 0o040755 : stat.mode
|
|
243
246
|
file_time = dos_time(stat.mtime)
|
|
244
247
|
data_crc = Zlib.crc32(file_data)
|
|
245
248
|
|
|
246
|
-
# Name encoding
|
|
247
249
|
name_bytes = archive_path.encode("UTF-8").bytes
|
|
248
250
|
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
#
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
# HEAD_FLAGS bit 0x8000 (LONG_BLOCK) marks a data area of
|
|
252
|
+
# PACK_SIZE bytes; the full dictionary mask 0xE0 marks
|
|
253
|
+
# directory entries (a directory has no dictionary) —
|
|
254
|
+
# confirmed against unrar extraction behavior.
|
|
255
|
+
flags = BLOCK_LONG
|
|
256
|
+
flags |= FILE_DIRECTORY if directory
|
|
257
|
+
flags |= FILE_LARGE if file_data.bytesize > 0xFFFFFFFF
|
|
258
|
+
|
|
259
|
+
# Fixed fields after HEAD_SIZE: PACK(4) + UNP(4) + HOST_OS(1)
|
|
260
|
+
# + FILE_CRC(4) + FTIME(4) + UNP_VER(1) + METHOD(1) +
|
|
261
|
+
# NAME_SIZE(2) + ATTR(4) = 25 bytes; plus TYPE(1) +
|
|
262
|
+
# FLAGS(2) + SIZE(2) and the 2-byte CRC itself.
|
|
263
|
+
header_size = 32 + name_bytes.size
|
|
264
|
+
header_size += 8 if flags.anybits?(FILE_LARGE)
|
|
260
265
|
|
|
261
|
-
# Build file header (without CRC)
|
|
262
266
|
header_data = [BLOCK_FILE].pack("C") +
|
|
263
267
|
[flags].pack("v") +
|
|
264
268
|
[header_size].pack("v") +
|
|
@@ -267,61 +271,61 @@ module Omnizip
|
|
|
267
271
|
[OS_UNIX].pack("C") +
|
|
268
272
|
[data_crc].pack("V") +
|
|
269
273
|
[file_time].pack("V") +
|
|
270
|
-
[compression_version(method)].pack("C") +
|
|
271
|
-
[method].pack("C") +
|
|
274
|
+
[compression_version(method)].pack("C") +
|
|
275
|
+
[method].pack("C") +
|
|
272
276
|
[name_bytes.size].pack("v") +
|
|
273
277
|
[file_attr].pack("V")
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
if flags & FILE_LARGE != 0
|
|
279
|
+
if flags.anybits?(FILE_LARGE)
|
|
277
280
|
header_data += [(compressed_data.bytesize >> 32) & 0xFFFFFFFF].pack("V")
|
|
278
281
|
header_data += [(file_data.bytesize >> 32) & 0xFFFFFFFF].pack("V")
|
|
279
282
|
end
|
|
280
283
|
|
|
281
|
-
# Add filename
|
|
282
284
|
header_data += name_bytes.pack("C*")
|
|
283
285
|
|
|
284
|
-
|
|
285
|
-
crc = calculate_header_crc16(header_data)
|
|
286
|
-
io.write([crc].pack("v"))
|
|
286
|
+
io.write([calculate_header_crc(header_data)].pack("v"))
|
|
287
287
|
io.write(header_data)
|
|
288
288
|
io.write(compressed_data)
|
|
289
289
|
end
|
|
290
290
|
|
|
291
|
-
# Write directory entries
|
|
291
|
+
# Write directory tree as explicit directory entries plus the
|
|
292
|
+
# files inside them
|
|
292
293
|
#
|
|
293
294
|
# @param io [IO] Output stream
|
|
294
295
|
# @param dir_info [Hash] Directory information
|
|
295
296
|
def write_directory_entries(io, dir_info)
|
|
296
297
|
dir_path = dir_info[:source]
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
pattern = recursive ? "
|
|
300
|
-
Dir.glob(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
298
|
+
prefix = dir_info[:archive_path].to_s.gsub(%r{^/+|/+$}, "")
|
|
299
|
+
|
|
300
|
+
pattern = dir_info[:recursive] ? File.join(dir_path, "**", "*") : File.join(dir_path, "*")
|
|
301
|
+
Dir.glob(pattern).each do |path|
|
|
302
|
+
relative = path.delete_prefix("#{dir_path}/")
|
|
303
|
+
name = prefix.empty? ? relative : "#{prefix}/#{relative}"
|
|
304
|
+
|
|
305
|
+
if File.directory?(path)
|
|
306
|
+
write_file_entry(io, {
|
|
307
|
+
source: path,
|
|
308
|
+
archive_path: name,
|
|
309
|
+
directory: true,
|
|
310
|
+
})
|
|
311
|
+
else
|
|
312
|
+
write_file_entry(io, {
|
|
313
|
+
source: path,
|
|
314
|
+
archive_path: name,
|
|
315
|
+
})
|
|
316
|
+
end
|
|
308
317
|
end
|
|
309
318
|
end
|
|
310
319
|
|
|
311
|
-
# Write end block
|
|
320
|
+
# Write end block (ENDARC, 7 bytes total)
|
|
312
321
|
#
|
|
313
322
|
# @param io [IO] Output stream
|
|
314
323
|
def write_end_block(io)
|
|
315
|
-
flags = 0x4000 # ENDARC flag
|
|
316
|
-
|
|
317
|
-
# Build header data (without CRC)
|
|
318
324
|
header_data = [BLOCK_ENDARC].pack("C") +
|
|
319
|
-
[
|
|
325
|
+
[0x0000].pack("v") +
|
|
320
326
|
[0x0007].pack("v")
|
|
321
327
|
|
|
322
|
-
|
|
323
|
-
crc = calculate_header_crc16(header_data)
|
|
324
|
-
io.write([crc].pack("v"))
|
|
328
|
+
io.write([calculate_header_crc(header_data)].pack("v"))
|
|
325
329
|
io.write(header_data)
|
|
326
330
|
end
|
|
327
331
|
|
|
@@ -334,7 +338,6 @@ module Omnizip
|
|
|
334
338
|
input = StringIO.new(data)
|
|
335
339
|
output = StringIO.new
|
|
336
340
|
|
|
337
|
-
# Use native compression dispatcher
|
|
338
341
|
Compression::Dispatcher.compress(method, input, output, @options)
|
|
339
342
|
|
|
340
343
|
output.string
|
|
@@ -372,37 +375,10 @@ module Omnizip
|
|
|
372
375
|
end
|
|
373
376
|
end
|
|
374
377
|
|
|
375
|
-
# Get
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
case @options[:compression]
|
|
380
|
-
when :store then Zlib::NO_COMPRESSION
|
|
381
|
-
when :fastest then Zlib::BEST_SPEED
|
|
382
|
-
when :fast then 3
|
|
383
|
-
when :normal then Zlib::DEFAULT_COMPRESSION
|
|
384
|
-
when :good then 7
|
|
385
|
-
when :best then Zlib::BEST_COMPRESSION
|
|
386
|
-
else Zlib::DEFAULT_COMPRESSION
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Get compression method code (DEPRECATED - use select_compression_method)
|
|
391
|
-
#
|
|
392
|
-
# @return [Integer] Method code
|
|
393
|
-
def compression_method
|
|
394
|
-
case @options[:compression]
|
|
395
|
-
when :store then METHOD_STORE
|
|
396
|
-
when :fastest then METHOD_FASTEST
|
|
397
|
-
when :fast then METHOD_FAST
|
|
398
|
-
when :normal then METHOD_NORMAL
|
|
399
|
-
when :good then METHOD_GOOD
|
|
400
|
-
when :best then METHOD_BEST
|
|
401
|
-
else METHOD_NORMAL
|
|
402
|
-
end
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
# Get compression version based on method
|
|
378
|
+
# Get compression version needed to extract. WinRAR writes 20
|
|
379
|
+
# for stored files and 29 for every compressed method,
|
|
380
|
+
# including PPMd (method 0x35) — confirmed against libarchive
|
|
381
|
+
# RAR4 fixtures created by the official tool.
|
|
406
382
|
#
|
|
407
383
|
# @param method [Integer] Compression method
|
|
408
384
|
# @return [Integer] Version code
|
|
@@ -410,17 +386,26 @@ module Omnizip
|
|
|
410
386
|
method == METHOD_STORE ? 20 : 29
|
|
411
387
|
end
|
|
412
388
|
|
|
413
|
-
# Test archive integrity
|
|
389
|
+
# Test archive integrity by reading it back with the RAR
|
|
390
|
+
# reader and comparing every extracted entry to its source
|
|
414
391
|
#
|
|
415
392
|
# @return [Boolean] true if valid
|
|
416
393
|
def test_archive
|
|
417
|
-
# Basic validation: check if file exists and has RAR signature
|
|
418
394
|
return false unless File.exist?(@output_path)
|
|
419
395
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
396
|
+
reader = Reader.new(@output_path)
|
|
397
|
+
reader.open
|
|
398
|
+
|
|
399
|
+
Dir.mktmpdir("omnizip_rar_test") do |tmp|
|
|
400
|
+
@files.map { |f| f[:archive_path] || File.basename(f[:source]) }
|
|
401
|
+
.zip(@files.map { |f| f[:source] }).all? do |name, source|
|
|
402
|
+
dest = File.join(tmp, name)
|
|
403
|
+
reader.extract_entry(name, dest)
|
|
404
|
+
FileUtils.compare_file(source, dest)
|
|
405
|
+
end
|
|
423
406
|
end
|
|
407
|
+
rescue StandardError
|
|
408
|
+
false
|
|
424
409
|
end
|
|
425
410
|
end
|
|
426
411
|
end
|
data/lib/omnizip/formats/rar.rb
CHANGED
|
@@ -98,14 +98,6 @@ module Omnizip
|
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
# RAR3 support
|
|
102
|
-
module Rar3
|
|
103
|
-
autoload :Compressor, "omnizip/formats/rar3/compressor"
|
|
104
|
-
autoload :Decompressor, "omnizip/formats/rar3/decompressor"
|
|
105
|
-
autoload :Reader, "omnizip/formats/rar3/reader"
|
|
106
|
-
autoload :Writer, "omnizip/formats/rar3/writer"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
101
|
# Compression layer
|
|
110
102
|
module Compression
|
|
111
103
|
autoload :BitStream, "omnizip/formats/rar/compression/bit_stream"
|
|
@@ -178,12 +178,7 @@ module Omnizip
|
|
|
178
178
|
attr = header_data[pos, 4].unpack1("V")
|
|
179
179
|
pos += 4
|
|
180
180
|
|
|
181
|
-
#
|
|
182
|
-
name_bytes = header_data[pos, name_size]
|
|
183
|
-
pos += name_size
|
|
184
|
-
filename = decode_filename(name_bytes, block.flags)
|
|
185
|
-
|
|
186
|
-
# Handle large file sizes
|
|
181
|
+
# High size words precede the filename per the RAR4 layout
|
|
187
182
|
if block.flags & 0x0100 != 0 # large_file flag
|
|
188
183
|
high_packed = header_data[pos, 4].unpack1("V")
|
|
189
184
|
high_unpacked = header_data[pos + 4, 4].unpack1("V")
|
|
@@ -192,20 +187,23 @@ module Omnizip
|
|
|
192
187
|
pos += 8
|
|
193
188
|
end
|
|
194
189
|
|
|
190
|
+
# Read filename
|
|
191
|
+
name_bytes = header_data[pos, name_size]
|
|
192
|
+
pos += name_size
|
|
193
|
+
filename = decode_filename(name_bytes, block.flags)
|
|
194
|
+
|
|
195
195
|
# Read salt if encrypted
|
|
196
196
|
salt = nil
|
|
197
197
|
if block.flags & 0x0400 != 0 # salt flag
|
|
198
198
|
salt = header_data[pos, 8]
|
|
199
|
-
pos
|
|
199
|
+
pos + 8
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
-
#
|
|
202
|
+
# Base DOS time from the file header (minute precision).
|
|
203
|
+
# The RAR4 extended-time record (0x1000 flag) is a
|
|
204
|
+
# variable-length nibble format that is not parsed; the
|
|
205
|
+
# base time is authoritative for minute-granularity mtimes.
|
|
203
206
|
mtime = parse_dos_time(file_time)
|
|
204
|
-
if block.flags & 0x1000 != 0 # ext_time flag
|
|
205
|
-
# Extended time format: mtime[4] + ctime[4] + atime[4] + arctime[4] + flags
|
|
206
|
-
# Plus additional 3 bytes for each present time
|
|
207
|
-
mtime, = read_extended_time_data(header_data, pos)
|
|
208
|
-
end
|
|
209
207
|
|
|
210
208
|
# We've already read all header data, no need to seek
|
|
211
209
|
# Just skip to end of block and then past file data
|
|
@@ -246,33 +244,6 @@ module Omnizip
|
|
|
246
244
|
)
|
|
247
245
|
end
|
|
248
246
|
|
|
249
|
-
# Read extended time from header data
|
|
250
|
-
#
|
|
251
|
-
# @param data [String] The header data
|
|
252
|
-
# @param pos [Integer] Current position in data
|
|
253
|
-
# @return [Array<Time, Integer>] The modification time and new position
|
|
254
|
-
def read_extended_time_data(data, pos)
|
|
255
|
-
return [Time.now, pos] if pos + 4 > data.bytesize
|
|
256
|
-
|
|
257
|
-
mtime = parse_dos_time(data[pos, 4].unpack1("V"))
|
|
258
|
-
pos += 4
|
|
259
|
-
|
|
260
|
-
# Skip ctime, atime, arctime if present
|
|
261
|
-
begin
|
|
262
|
-
data[pos, 1].unpack1("C")
|
|
263
|
-
rescue StandardError
|
|
264
|
-
0
|
|
265
|
-
end
|
|
266
|
-
pos += 1
|
|
267
|
-
|
|
268
|
-
# For now, just skip the remaining extended time data
|
|
269
|
-
# The format is complex and varies
|
|
270
|
-
|
|
271
|
-
[mtime, pos]
|
|
272
|
-
rescue ArgumentError
|
|
273
|
-
[Time.now, pos]
|
|
274
|
-
end
|
|
275
|
-
|
|
276
247
|
# Decode filename based on encoding flags
|
|
277
248
|
#
|
|
278
249
|
# @param bytes [String] The filename bytes
|
|
@@ -305,16 +276,6 @@ module Omnizip
|
|
|
305
276
|
Time.now
|
|
306
277
|
end
|
|
307
278
|
|
|
308
|
-
# Read extended time information
|
|
309
|
-
#
|
|
310
|
-
# @param io [IO] The input stream
|
|
311
|
-
# @return [Time] The extended time
|
|
312
|
-
def read_extended_time(io)
|
|
313
|
-
io.read(2)&.unpack1("v")
|
|
314
|
-
# Extended time format varies, simplified implementation
|
|
315
|
-
Time.now
|
|
316
|
-
end
|
|
317
|
-
|
|
318
279
|
# Read comment block
|
|
319
280
|
#
|
|
320
281
|
# @param io [IO] The input stream
|
|
@@ -24,7 +24,7 @@ module Omnizip
|
|
|
24
24
|
# ]
|
|
25
25
|
# writer.write_archive(file, entries)
|
|
26
26
|
# end
|
|
27
|
-
class Writer < Rar::RarFormatBase
|
|
27
|
+
class Writer < Omnizip::Formats::Rar::RarFormatBase
|
|
28
28
|
# Initialize a RAR v3 writer
|
|
29
29
|
def initialize
|
|
30
30
|
super("rar3")
|
|
@@ -71,8 +71,9 @@ module Omnizip
|
|
|
71
71
|
# Set default flags if needed
|
|
72
72
|
|
|
73
73
|
header_data = StringIO.new
|
|
74
|
-
# Reserved fields
|
|
75
|
-
|
|
74
|
+
# Reserved fields: HighPosAV (2) + PosAV (4) = 6 bytes,
|
|
75
|
+
# making the main header 13 bytes total like WinRAR's
|
|
76
|
+
header_data.write([0].pack("v") + [0].pack("V"))
|
|
76
77
|
|
|
77
78
|
header = create_block_header(
|
|
78
79
|
type: block_type_code(:archive),
|
|
@@ -107,13 +108,17 @@ module Omnizip
|
|
|
107
108
|
name_bytes = name.encode("UTF-8")
|
|
108
109
|
name_size = name_bytes.bytesize
|
|
109
110
|
|
|
110
|
-
# Set flags
|
|
111
|
-
|
|
111
|
+
# Set flags: LONG_BLOCK marks the PACK_SIZE data area; names
|
|
112
|
+
# are written as plain bytes without the Unicode flag (the
|
|
113
|
+
# 0x0200 encoding appends a second encoded name we do not
|
|
114
|
+
# write)
|
|
115
|
+
flags = spec.format.file_flags[:long_block]
|
|
112
116
|
if unpacked_size > 0xFFFFFFFF
|
|
113
117
|
flags |= spec.format.file_flags[:large_file]
|
|
114
118
|
end
|
|
115
119
|
|
|
116
|
-
# Build file header data
|
|
120
|
+
# Build file header data. High size words precede the name
|
|
121
|
+
# per the RAR4 layout.
|
|
117
122
|
header_data = StringIO.new
|
|
118
123
|
header_data.write([packed_size & 0xFFFFFFFF].pack("V"))
|
|
119
124
|
header_data.write([unpacked_size & 0xFFFFFFFF].pack("V"))
|
|
@@ -124,13 +129,11 @@ module Omnizip
|
|
|
124
129
|
header_data.write([compression_method_code(method)].pack("C"))
|
|
125
130
|
header_data.write([name_size].pack("v"))
|
|
126
131
|
header_data.write([0x20].pack("V")) # File attributes
|
|
127
|
-
header_data.write(name_bytes)
|
|
128
|
-
|
|
129
|
-
# Add high size fields if needed
|
|
130
132
|
if flags & spec.format.file_flags[:large_file] != 0
|
|
131
133
|
header_data.write([(packed_size >> 32) & 0xFFFFFFFF].pack("V"))
|
|
132
134
|
header_data.write([(unpacked_size >> 32) & 0xFFFFFFFF].pack("V"))
|
|
133
135
|
end
|
|
136
|
+
header_data.write(name_bytes)
|
|
134
137
|
|
|
135
138
|
# Create and write block header
|
|
136
139
|
header = create_block_header(
|
|
@@ -151,7 +154,7 @@ module Omnizip
|
|
|
151
154
|
def write_terminator_block(io)
|
|
152
155
|
header = create_block_header(
|
|
153
156
|
type: block_type_code(:terminator),
|
|
154
|
-
flags:
|
|
157
|
+
flags: 0,
|
|
155
158
|
data: "",
|
|
156
159
|
)
|
|
157
160
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Formats
|
|
5
|
+
# Standalone RAR3 reader/writer tree (see Formats::Rar for the
|
|
6
|
+
# primary RAR support). Classes under this namespace load lazily;
|
|
7
|
+
# require "omnizip" first so sibling namespaces resolve.
|
|
8
|
+
module Rar3
|
|
9
|
+
autoload :Compressor, "omnizip/formats/rar3/compressor"
|
|
10
|
+
autoload :Decompressor, "omnizip/formats/rar3/decompressor"
|
|
11
|
+
autoload :Reader, "omnizip/formats/rar3/reader"
|
|
12
|
+
autoload :Writer, "omnizip/formats/rar3/writer"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -22,7 +22,7 @@ module Omnizip
|
|
|
22
22
|
# entries = reader.read_archive(file)
|
|
23
23
|
# entries.each { |entry| puts entry.name }
|
|
24
24
|
# end
|
|
25
|
-
class Reader < Rar::RarFormatBase
|
|
25
|
+
class Reader < Omnizip::Formats::Rar::RarFormatBase
|
|
26
26
|
# Initialize a RAR v5 reader
|
|
27
27
|
def initialize
|
|
28
28
|
super("rar5")
|
|
@@ -174,8 +174,9 @@ module Omnizip
|
|
|
174
174
|
unpacked_size = read_vint(io)
|
|
175
175
|
attributes = read_vint(io)
|
|
176
176
|
|
|
177
|
-
# Read modification time if present
|
|
178
|
-
|
|
177
|
+
# Read modification time if present; nil when the header
|
|
178
|
+
# does not carry one
|
|
179
|
+
mtime = nil
|
|
179
180
|
if flags & 0x02 != 0 # time_present flag
|
|
180
181
|
mtime = read_file_time(io)
|
|
181
182
|
end
|
|
@@ -247,14 +248,11 @@ module Omnizip
|
|
|
247
248
|
#
|
|
248
249
|
# @param io [IO] The input stream
|
|
249
250
|
# @return [Time] The file time
|
|
251
|
+
# RAR5 file headers store a 32-bit Unix timestamp when the
|
|
252
|
+
# mtime flag is set
|
|
250
253
|
def read_file_time(io)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
# Simplified time reading - actual format is more complex
|
|
254
|
-
unix_time = io.read(8)&.unpack1("Q<")
|
|
255
|
-
Time.at(unix_time / 10_000_000.0) if unix_time
|
|
256
|
-
rescue ArgumentError
|
|
257
|
-
Time.now
|
|
254
|
+
unix_time = io.read(4)&.unpack1("V")
|
|
255
|
+
Time.at(unix_time) if unix_time
|
|
258
256
|
end
|
|
259
257
|
|
|
260
258
|
# Read encryption header
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Formats
|
|
5
|
+
# Standalone RAR5 reader/writer tree (see Formats::Rar for the
|
|
6
|
+
# primary RAR support). Classes under this namespace load lazily;
|
|
7
|
+
# require "omnizip" first so sibling namespaces resolve.
|
|
8
|
+
module Rar5
|
|
9
|
+
autoload :Compressor, "omnizip/formats/rar5/compressor"
|
|
10
|
+
autoload :Decompressor, "omnizip/formats/rar5/decompressor"
|
|
11
|
+
autoload :Reader, "omnizip/formats/rar5/reader"
|
|
12
|
+
autoload :Writer, "omnizip/formats/rar5/writer"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/omnizip/formats.rb
CHANGED
|
@@ -7,6 +7,8 @@ module Omnizip
|
|
|
7
7
|
autoload :SevenZip, "omnizip/formats/seven_zip"
|
|
8
8
|
autoload :Zip, "omnizip/formats/zip"
|
|
9
9
|
autoload :Rar, "omnizip/formats/rar"
|
|
10
|
+
autoload :Rar3, "omnizip/formats/rar3"
|
|
11
|
+
autoload :Rar5, "omnizip/formats/rar5"
|
|
10
12
|
autoload :Tar, "omnizip/formats/tar"
|
|
11
13
|
autoload :Gzip, "omnizip/formats/gzip"
|
|
12
14
|
autoload :Bzip2File, "omnizip/formats/bzip2_file"
|