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,18 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
rescue LoadError, ArgumentError
|
|
6
|
-
# lutaml-model not available, using simple classes
|
|
7
|
-
end
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "tmpdir"
|
|
8
5
|
|
|
9
6
|
module Omnizip
|
|
10
7
|
module Formats
|
|
11
8
|
module Rar5
|
|
12
9
|
# RAR v5 archive reader
|
|
13
10
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# Adapter over the primary `Formats::Rar::Reader` — the one RAR5
|
|
12
|
+
# parser verified against real archives and unrar — exposing the
|
|
13
|
+
# legacy entry shape (uncompressed_size, modified_time, ...).
|
|
16
14
|
#
|
|
17
15
|
# RAR 5 uses variable-length integers (vint) and improved header structure.
|
|
18
16
|
#
|
|
@@ -23,6 +21,17 @@ module Omnizip
|
|
|
23
21
|
# entries.each { |entry| puts entry.name }
|
|
24
22
|
# end
|
|
25
23
|
class Reader < Omnizip::Formats::Rar::RarFormatBase
|
|
24
|
+
# RAR5 compression-method byte (cinfo bits 8-10) to the legacy
|
|
25
|
+
# symbol vocabulary
|
|
26
|
+
METHOD_SYMBOLS = {
|
|
27
|
+
0x30 => :store,
|
|
28
|
+
0x31 => :fastest,
|
|
29
|
+
0x32 => :fast,
|
|
30
|
+
0x33 => :normal,
|
|
31
|
+
0x34 => :good,
|
|
32
|
+
0x35 => :best,
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
26
35
|
# Initialize a RAR v5 reader
|
|
27
36
|
def initialize
|
|
28
37
|
super("rar5")
|
|
@@ -32,314 +41,88 @@ module Omnizip
|
|
|
32
41
|
#
|
|
33
42
|
# @param io [IO] The input stream
|
|
34
43
|
# @return [Array<Entry>] The archive entries
|
|
35
|
-
# @raise [FormatError] If the archive
|
|
44
|
+
# @raise [FormatError] If the archive signature is invalid
|
|
36
45
|
def read_archive(io)
|
|
37
|
-
|
|
46
|
+
data = io.read
|
|
47
|
+
unless verify_magic_bytes(StringIO.new(data))
|
|
38
48
|
raise FormatError, "Invalid RAR v5 signature"
|
|
39
49
|
end
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
entries = []
|
|
45
|
-
|
|
46
|
-
# Read main header
|
|
47
|
-
main_header = read_header(io)
|
|
48
|
-
unless main_header.type == block_type_code(:main_header)
|
|
49
|
-
raise FormatError, "Expected main archive header"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
@archive_flags = main_header.flags
|
|
53
|
-
|
|
54
|
-
# Skip to end of main header
|
|
55
|
-
skip_to_header_end(io, main_header)
|
|
56
|
-
|
|
57
|
-
# Read blocks until end marker or EOF/error
|
|
58
|
-
loop do
|
|
59
|
-
header = read_header(io)
|
|
60
|
-
break if header.type == block_type_code(:end_marker)
|
|
51
|
+
Dir.mktmpdir("omnizip_rar5_read") do |tmp|
|
|
52
|
+
archive_path = File.join(tmp, "archive.rar")
|
|
53
|
+
File.binwrite(archive_path, data)
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
entries << entry if entry
|
|
66
|
-
when block_type_code(:service_header)
|
|
67
|
-
skip_header_data(io, header)
|
|
68
|
-
when block_type_code(:encryption_header)
|
|
69
|
-
read_encryption_header(io, header)
|
|
70
|
-
else
|
|
71
|
-
skip_header_data(io, header)
|
|
72
|
-
end
|
|
73
|
-
rescue EOFError, RangeError, FormatError
|
|
74
|
-
# Truncated or corrupted file - return what we have
|
|
75
|
-
break
|
|
55
|
+
primary = Rar::Reader.new(archive_path)
|
|
56
|
+
primary.open
|
|
57
|
+
primary.list_files.map { |entry| adapt_entry(entry) }
|
|
76
58
|
end
|
|
77
|
-
|
|
78
|
-
entries
|
|
79
|
-
rescue EOFError, RangeError, FormatError
|
|
80
|
-
# Handle truncated/invalid files gracefully
|
|
81
|
-
[]
|
|
82
59
|
end
|
|
83
60
|
|
|
84
61
|
private
|
|
85
62
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
# @param io [IO] The input stream
|
|
89
|
-
# @param header [HeaderBlock] The header
|
|
90
|
-
def skip_to_header_end(io, header)
|
|
91
|
-
# Calculate end of header: header_start + 4 (CRC) + header_size + vint_length
|
|
92
|
-
header_end = header.header_start + 4 + header.size.to_i + header.vint_length.to_i
|
|
93
|
-
|
|
94
|
-
# Validate the offset is reasonable (max 1GB)
|
|
95
|
-
max_offset = 1_073_741_824
|
|
96
|
-
raise RangeError, "Header offset too large" if header_end > max_offset
|
|
97
|
-
raise RangeError, "Header offset negative" if header_end.negative?
|
|
98
|
-
|
|
99
|
-
io.seek(header_end, ::IO::SEEK_SET)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Read a RAR v5 header
|
|
103
|
-
#
|
|
104
|
-
# @param io [IO] The input stream
|
|
105
|
-
# @return [HeaderBlock] The header block
|
|
106
|
-
def read_header(io)
|
|
107
|
-
# Record start position (before CRC)
|
|
108
|
-
header_start = io.pos
|
|
109
|
-
|
|
110
|
-
header_crc = io.read(4)&.unpack1("V")
|
|
111
|
-
|
|
112
|
-
# Read header size vint and track its length
|
|
113
|
-
header_size, vint_length = read_vint_with_length(io)
|
|
114
|
-
|
|
115
|
-
# Now we're at the start of header content
|
|
116
|
-
# header_size is the size of the content (from header_type to end)
|
|
117
|
-
content_start = io.pos
|
|
118
|
-
|
|
119
|
-
header_type = read_vint(io)
|
|
120
|
-
header_flags = read_vint(io)
|
|
121
|
-
|
|
122
|
-
# Read extra size if present
|
|
123
|
-
extra_size = 0
|
|
124
|
-
extra_size = read_vint(io) if header_flags & 0x0001 != 0
|
|
125
|
-
|
|
126
|
-
# Read data size if present
|
|
127
|
-
data_size = 0
|
|
128
|
-
data_size = read_vint(io) if header_flags & 0x0002 != 0
|
|
129
|
-
|
|
130
|
-
# Store positions for proper skipping
|
|
131
|
-
HeaderBlock.new(
|
|
132
|
-
crc: header_crc,
|
|
133
|
-
size: header_size,
|
|
134
|
-
vint_length: vint_length,
|
|
135
|
-
type: header_type,
|
|
136
|
-
flags: header_flags,
|
|
137
|
-
extra_size: extra_size,
|
|
138
|
-
data_size: data_size,
|
|
139
|
-
header_start: header_start,
|
|
140
|
-
content_start: content_start,
|
|
141
|
-
)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Read a vint and return both value and length
|
|
145
|
-
#
|
|
146
|
-
# @param io [IO] The input stream
|
|
147
|
-
# @return [Array<Integer, Integer>] Value and length in bytes
|
|
148
|
-
def read_vint_with_length(io)
|
|
149
|
-
result = 0
|
|
150
|
-
shift = 0
|
|
151
|
-
length = 0
|
|
152
|
-
|
|
153
|
-
loop do
|
|
154
|
-
byte = io.read(1)&.unpack1("C")
|
|
155
|
-
raise FormatError, "Unexpected EOF" unless byte
|
|
156
|
-
|
|
157
|
-
length += 1
|
|
158
|
-
result |= (byte & 0x7F) << shift
|
|
159
|
-
break if byte.nobits?(0x80)
|
|
160
|
-
|
|
161
|
-
shift += 7
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
[result, length]
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Read a file entry from archive
|
|
168
|
-
#
|
|
169
|
-
# @param io [IO] The input stream
|
|
170
|
-
# @param header [HeaderBlock] The file header
|
|
171
|
-
# @return [Entry] The file entry
|
|
172
|
-
def read_file_entry(io, header)
|
|
173
|
-
flags = read_vint(io)
|
|
174
|
-
unpacked_size = read_vint(io)
|
|
175
|
-
attributes = read_vint(io)
|
|
176
|
-
|
|
177
|
-
# Read modification time if present; nil when the header
|
|
178
|
-
# does not carry one
|
|
179
|
-
mtime = nil
|
|
180
|
-
if flags & 0x02 != 0 # time_present flag
|
|
181
|
-
mtime = read_file_time(io)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Read CRC32 if present
|
|
185
|
-
data_crc = 0
|
|
186
|
-
if flags & 0x04 != 0 # crc32_present flag
|
|
187
|
-
data_crc = io.read(4)&.unpack1("V")
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Read compression info
|
|
191
|
-
compression_flags = read_vint(io)
|
|
192
|
-
host_os = read_vint(io)
|
|
193
|
-
name_length = read_vint(io)
|
|
194
|
-
|
|
195
|
-
# Read filename (UTF-8 encoded)
|
|
196
|
-
name_bytes = io.read(name_length)
|
|
197
|
-
filename = name_bytes.force_encoding("UTF-8")
|
|
198
|
-
|
|
199
|
-
# Determine if directory
|
|
200
|
-
is_directory = flags.anybits?(0x01) # directory flag
|
|
201
|
-
|
|
202
|
-
# Skip to end of header
|
|
203
|
-
skip_to_header_end(io, header)
|
|
204
|
-
|
|
205
|
-
# Record data offset before skipping
|
|
206
|
-
data_offset = io.pos
|
|
207
|
-
|
|
208
|
-
# Skip file data (so we can read the next header)
|
|
209
|
-
if header.data_size.to_i.positive?
|
|
210
|
-
io.seek(header.data_size,
|
|
211
|
-
::IO::SEEK_CUR)
|
|
212
|
-
end
|
|
213
|
-
|
|
63
|
+
# Map a primary RarEntry onto the legacy Entry shape
|
|
64
|
+
def adapt_entry(entry)
|
|
214
65
|
Entry.new(
|
|
215
|
-
name:
|
|
216
|
-
compressed_size:
|
|
217
|
-
uncompressed_size:
|
|
218
|
-
crc32:
|
|
219
|
-
compression_method:
|
|
220
|
-
modified_time: mtime,
|
|
221
|
-
attributes: attributes,
|
|
222
|
-
encrypted:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
is_directory: is_directory,
|
|
66
|
+
name: entry.name,
|
|
67
|
+
compressed_size: entry.compressed_size,
|
|
68
|
+
uncompressed_size: entry.size,
|
|
69
|
+
crc32: entry.crc,
|
|
70
|
+
compression_method: METHOD_SYMBOLS[entry.method],
|
|
71
|
+
modified_time: entry.mtime,
|
|
72
|
+
attributes: entry.attributes,
|
|
73
|
+
encrypted: entry.encrypted,
|
|
74
|
+
host_os: entry.host_os,
|
|
75
|
+
is_directory: entry.is_dir,
|
|
226
76
|
)
|
|
227
77
|
end
|
|
228
78
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
79
|
+
class HeaderBlock
|
|
80
|
+
attr_accessor :crc, :size, :vint_length, :type, :flags, :extra_size,
|
|
81
|
+
:data_size, :header_start, :content_start
|
|
82
|
+
|
|
83
|
+
def initialize(crc: nil, size: nil, vint_length: 1, type: nil, flags: nil,
|
|
84
|
+
extra_size: nil, data_size: nil, header_start: nil, content_start: nil)
|
|
85
|
+
@crc = crc
|
|
86
|
+
@size = size
|
|
87
|
+
@vint_length = vint_length
|
|
88
|
+
@type = type
|
|
89
|
+
@flags = flags
|
|
90
|
+
@extra_size = extra_size
|
|
91
|
+
@data_size = data_size
|
|
92
|
+
@header_start = header_start
|
|
93
|
+
@content_start = content_start
|
|
244
94
|
end
|
|
245
95
|
end
|
|
246
96
|
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
header_end = header.header_start.to_i + 4 + header.size.to_i + header.vint_length.to_i
|
|
276
|
-
current_pos = io.pos
|
|
277
|
-
|
|
278
|
-
# Validate the offset is reasonable (max 1GB)
|
|
279
|
-
max_offset = 1_073_741_824
|
|
280
|
-
if header_end > max_offset || header_end.negative?
|
|
281
|
-
raise RangeError, "Invalid header offset"
|
|
97
|
+
# RAR v5 archive entry model
|
|
98
|
+
class Entry
|
|
99
|
+
include Omnizip::Entry
|
|
100
|
+
|
|
101
|
+
attr_accessor :name, :compressed_size, :uncompressed_size, :crc32,
|
|
102
|
+
:compression_method, :modified_time, :attributes,
|
|
103
|
+
:encrypted, :data_offset, :host_os, :is_directory
|
|
104
|
+
|
|
105
|
+
def entry_name = name
|
|
106
|
+
def entry_directory? = is_directory
|
|
107
|
+
def entry_size = uncompressed_size
|
|
108
|
+
def entry_mtime = modified_time
|
|
109
|
+
|
|
110
|
+
def initialize(name: nil, compressed_size: nil, uncompressed_size: nil,
|
|
111
|
+
crc32: nil, compression_method: nil, modified_time: nil,
|
|
112
|
+
attributes: nil, encrypted: nil, data_offset: nil,
|
|
113
|
+
host_os: nil, is_directory: nil)
|
|
114
|
+
@name = name
|
|
115
|
+
@compressed_size = compressed_size
|
|
116
|
+
@uncompressed_size = uncompressed_size
|
|
117
|
+
@crc32 = crc32
|
|
118
|
+
@compression_method = compression_method
|
|
119
|
+
@modified_time = modified_time
|
|
120
|
+
@attributes = attributes
|
|
121
|
+
@encrypted = encrypted
|
|
122
|
+
@data_offset = data_offset
|
|
123
|
+
@host_os = host_os
|
|
124
|
+
@is_directory = is_directory
|
|
282
125
|
end
|
|
283
|
-
|
|
284
|
-
if header_end > current_pos
|
|
285
|
-
io.seek(header_end - current_pos, ::IO::SEEK_CUR)
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# Skip data section if present (with bounds checking)
|
|
289
|
-
data_size = header.data_size.to_i
|
|
290
|
-
if data_size.positive? && data_size < max_offset
|
|
291
|
-
io.seek(data_size, ::IO::SEEK_CUR)
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# RAR v5 header block model
|
|
297
|
-
class HeaderBlock
|
|
298
|
-
attr_accessor :crc, :size, :vint_length, :type, :flags, :extra_size,
|
|
299
|
-
:data_size, :header_start, :content_start
|
|
300
|
-
|
|
301
|
-
def initialize(crc: nil, size: nil, vint_length: 1, type: nil, flags: nil,
|
|
302
|
-
extra_size: nil, data_size: nil, header_start: nil, content_start: nil)
|
|
303
|
-
@crc = crc
|
|
304
|
-
@size = size
|
|
305
|
-
@vint_length = vint_length
|
|
306
|
-
@type = type
|
|
307
|
-
@flags = flags
|
|
308
|
-
@extra_size = extra_size
|
|
309
|
-
@data_size = data_size
|
|
310
|
-
@header_start = header_start
|
|
311
|
-
@content_start = content_start
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# RAR v5 archive entry model
|
|
316
|
-
class Entry
|
|
317
|
-
include Omnizip::Entry
|
|
318
|
-
|
|
319
|
-
attr_accessor :name, :compressed_size, :uncompressed_size, :crc32,
|
|
320
|
-
:compression_method, :modified_time, :attributes,
|
|
321
|
-
:encrypted, :data_offset, :host_os, :is_directory
|
|
322
|
-
|
|
323
|
-
def entry_name = name
|
|
324
|
-
def entry_directory? = is_directory
|
|
325
|
-
def entry_size = uncompressed_size
|
|
326
|
-
def entry_mtime = modified_time
|
|
327
|
-
|
|
328
|
-
def initialize(name: nil, compressed_size: nil, uncompressed_size: nil,
|
|
329
|
-
crc32: nil, compression_method: nil, modified_time: nil,
|
|
330
|
-
attributes: nil, encrypted: nil, data_offset: nil,
|
|
331
|
-
host_os: nil, is_directory: nil)
|
|
332
|
-
@name = name
|
|
333
|
-
@compressed_size = compressed_size
|
|
334
|
-
@uncompressed_size = uncompressed_size
|
|
335
|
-
@crc32 = crc32
|
|
336
|
-
@compression_method = compression_method
|
|
337
|
-
@modified_time = modified_time
|
|
338
|
-
@attributes = attributes
|
|
339
|
-
@encrypted = encrypted
|
|
340
|
-
@data_offset = data_offset
|
|
341
|
-
@host_os = host_os
|
|
342
|
-
@is_directory = is_directory
|
|
343
126
|
end
|
|
344
127
|
end
|
|
345
128
|
end
|
|
@@ -1,23 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
rescue LoadError, ArgumentError
|
|
6
|
-
# lutaml-model not available, using simple classes
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
require "stringio"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
10
5
|
|
|
11
6
|
module Omnizip
|
|
12
7
|
module Formats
|
|
13
8
|
module Rar5
|
|
14
9
|
# RAR v5 archive writer
|
|
15
10
|
#
|
|
16
|
-
# Writes RAR 5.x format archives, creating headers and compressing file
|
|
17
|
-
# data according to the RAR v5 specification.
|
|
18
|
-
#
|
|
19
|
-
# RAR 5 uses variable-length integers and improved header structure.
|
|
20
|
-
#
|
|
21
11
|
# @example Writing a RAR5 archive
|
|
22
12
|
# writer = Rar5::Writer.new
|
|
23
13
|
# File.open("archive.rar", "wb") do |file|
|
|
@@ -26,185 +16,48 @@ module Omnizip
|
|
|
26
16
|
# ]
|
|
27
17
|
# writer.write_archive(file, entries)
|
|
28
18
|
# end
|
|
29
|
-
class Writer < Rar::RarFormatBase
|
|
19
|
+
class Writer < Omnizip::Formats::Rar::RarFormatBase
|
|
30
20
|
# Initialize a RAR v5 writer
|
|
31
21
|
def initialize
|
|
32
22
|
super("rar5")
|
|
33
|
-
@compressor = Compressor.new
|
|
34
23
|
end
|
|
35
24
|
|
|
36
25
|
# Write a RAR v5 archive
|
|
37
26
|
#
|
|
27
|
+
# Delegates to the primary `Formats::Rar::Rar5::Writer` so the
|
|
28
|
+
# output is spec-conformant and unrar-verified: entries spill
|
|
29
|
+
# to a temporary file, the primary writer produces the
|
|
30
|
+
# archive, and the bytes are copied into the given IO.
|
|
31
|
+
# Compression methods above :store are not official-RAR
|
|
32
|
+
# compatible, so the primary writer stores them (with a
|
|
33
|
+
# warning) rather than emitting an undecodable stream.
|
|
34
|
+
#
|
|
38
35
|
# @param io [IO] The output stream
|
|
39
36
|
# @param entries [Array<Hash>] The entries to write
|
|
40
37
|
# @return [void]
|
|
41
38
|
def write_archive(io, entries)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
Dir.mktmpdir("omnizip_rar5_write") do |tmp|
|
|
40
|
+
archive_path = File.join(tmp, "archive.rar")
|
|
41
|
+
primary = Rar::Rar5::Writer.new(archive_path,
|
|
42
|
+
include_mtime: true,
|
|
43
|
+
include_crc32: true)
|
|
44
|
+
|
|
45
|
+
entries.each do |entry|
|
|
46
|
+
name = entry[:name] || entry["name"]
|
|
47
|
+
data = entry[:data] || entry["data"]
|
|
48
|
+
mtime = entry[:time] || entry["time"] || Time.now
|
|
49
|
+
|
|
50
|
+
file_path = File.join(tmp, "entries", name)
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
52
|
+
File.binwrite(file_path, data.to_s)
|
|
53
|
+
File.utime(mtime, mtime, file_path)
|
|
54
|
+
|
|
55
|
+
primary.add_file(file_path, name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
primary.write
|
|
59
|
+
io.write(File.binread(archive_path))
|
|
51
60
|
end
|
|
52
|
-
|
|
53
|
-
# Write end marker
|
|
54
|
-
write_end_marker(io)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
# Write main archive header
|
|
60
|
-
#
|
|
61
|
-
# @param io [IO] The output stream
|
|
62
|
-
# @return [void]
|
|
63
|
-
def write_main_header(io)
|
|
64
|
-
flags = 0
|
|
65
|
-
|
|
66
|
-
header_data = StringIO.new
|
|
67
|
-
write_vint(header_data, flags)
|
|
68
|
-
|
|
69
|
-
write_header(
|
|
70
|
-
io,
|
|
71
|
-
type: block_type_code(:main_header),
|
|
72
|
-
flags: 0,
|
|
73
|
-
data: header_data.string,
|
|
74
|
-
)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Write a file entry
|
|
78
|
-
#
|
|
79
|
-
# @param io [IO] The output stream
|
|
80
|
-
# @param entry [Hash] The entry data
|
|
81
|
-
# @return [void]
|
|
82
|
-
def write_file_entry(io, entry)
|
|
83
|
-
name = entry[:name] || entry["name"]
|
|
84
|
-
data = entry[:data] || entry["data"]
|
|
85
|
-
mtime = entry[:time] || entry["time"] || Time.now
|
|
86
|
-
method = entry[:method] || entry["method"] || :normal
|
|
87
|
-
|
|
88
|
-
# Compress data
|
|
89
|
-
compressed_data = @compressor.compress(data, method: method)
|
|
90
|
-
|
|
91
|
-
# Calculate sizes and CRC
|
|
92
|
-
compressed_data.bytesize
|
|
93
|
-
unpacked_size = data.bytesize
|
|
94
|
-
file_crc = calculate_crc32(data)
|
|
95
|
-
|
|
96
|
-
# Encode filename to UTF-8
|
|
97
|
-
name_bytes = name.encode("UTF-8")
|
|
98
|
-
name_length = name_bytes.bytesize
|
|
99
|
-
|
|
100
|
-
# Build file header data
|
|
101
|
-
file_flags = spec.format.file_header_flags[:time_present] |
|
|
102
|
-
spec.format.file_header_flags[:crc32_present]
|
|
103
|
-
|
|
104
|
-
compression_info = compression_method_code(method) & 0x07
|
|
105
|
-
|
|
106
|
-
header_data = StringIO.new
|
|
107
|
-
write_vint(header_data, file_flags)
|
|
108
|
-
write_vint(header_data, unpacked_size)
|
|
109
|
-
write_vint(header_data, 0x20) # Attributes (normal file)
|
|
110
|
-
|
|
111
|
-
# Write modification time
|
|
112
|
-
write_file_time(header_data, mtime)
|
|
113
|
-
|
|
114
|
-
# Write CRC32
|
|
115
|
-
header_data.write([file_crc].pack("V"))
|
|
116
|
-
|
|
117
|
-
# Write compression info
|
|
118
|
-
write_vint(header_data, compression_info)
|
|
119
|
-
write_vint(header_data, spec.format.host_os[:windows])
|
|
120
|
-
write_vint(header_data, name_length)
|
|
121
|
-
header_data.write(name_bytes)
|
|
122
|
-
|
|
123
|
-
# Write header with data
|
|
124
|
-
write_header(
|
|
125
|
-
io,
|
|
126
|
-
type: block_type_code(:file_header),
|
|
127
|
-
flags: 0x0002, # Has data size
|
|
128
|
-
data: header_data.string,
|
|
129
|
-
data_section: compressed_data,
|
|
130
|
-
)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Write end marker
|
|
134
|
-
#
|
|
135
|
-
# @param io [IO] The output stream
|
|
136
|
-
# @return [void]
|
|
137
|
-
def write_end_marker(io)
|
|
138
|
-
write_header(
|
|
139
|
-
io,
|
|
140
|
-
type: block_type_code(:end_marker),
|
|
141
|
-
flags: 0x0004, # Archive end flag
|
|
142
|
-
data: "",
|
|
143
|
-
)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Write a RAR v5 header
|
|
147
|
-
#
|
|
148
|
-
# @param io [IO] The output stream
|
|
149
|
-
# @param type [Integer] The header type
|
|
150
|
-
# @param flags [Integer] The header flags
|
|
151
|
-
# @param data [String] The header data
|
|
152
|
-
# @param data_section [String, nil] Optional data section
|
|
153
|
-
# @return [void]
|
|
154
|
-
def write_header(io, type:, flags:, data:, data_section: nil)
|
|
155
|
-
header_io = StringIO.new
|
|
156
|
-
|
|
157
|
-
# Calculate header size (not including CRC)
|
|
158
|
-
size_bytes = StringIO.new
|
|
159
|
-
write_vint(size_bytes, type)
|
|
160
|
-
write_vint(size_bytes, flags)
|
|
161
|
-
|
|
162
|
-
if data_section
|
|
163
|
-
flags |= 0x0002 # Has data size flag
|
|
164
|
-
write_vint(size_bytes, data_section.bytesize)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
size_bytes.write(data)
|
|
168
|
-
|
|
169
|
-
header_size = size_bytes.string.bytesize
|
|
170
|
-
|
|
171
|
-
# Write size vint
|
|
172
|
-
write_vint(header_io, header_size)
|
|
173
|
-
|
|
174
|
-
# Write type and flags
|
|
175
|
-
write_vint(header_io, type)
|
|
176
|
-
write_vint(header_io, flags)
|
|
177
|
-
|
|
178
|
-
# Write data size if present
|
|
179
|
-
write_vint(header_io, data_section.bytesize) if data_section
|
|
180
|
-
|
|
181
|
-
# Write header data
|
|
182
|
-
header_io.write(data)
|
|
183
|
-
|
|
184
|
-
# Calculate CRC32 of header (excluding CRC field itself)
|
|
185
|
-
header_crc = calculate_crc32(header_io.string)
|
|
186
|
-
|
|
187
|
-
# Write CRC and header to output
|
|
188
|
-
io.write([header_crc].pack("V"))
|
|
189
|
-
io.write(header_io.string)
|
|
190
|
-
|
|
191
|
-
# Write data section if present
|
|
192
|
-
io.write(data_section) if data_section
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Write file time in RAR5 format
|
|
196
|
-
#
|
|
197
|
-
# @param io [IO] The output stream
|
|
198
|
-
# @param time [Time] The time to write
|
|
199
|
-
# @return [void]
|
|
200
|
-
def write_file_time(io, time)
|
|
201
|
-
# RAR5 uses Windows FILETIME format (100-nanosecond intervals since
|
|
202
|
-
# 1601-01-01). Simplified implementation.
|
|
203
|
-
unix_time = time.to_i
|
|
204
|
-
windows_time = (unix_time + 11_644_473_600) * 10_000_000
|
|
205
|
-
|
|
206
|
-
write_vint(io, 0x01) # Time flags (modification time present)
|
|
207
|
-
io.write([windows_time].pack("Q<"))
|
|
208
61
|
end
|
|
209
62
|
end
|
|
210
63
|
end
|
|
@@ -68,6 +68,23 @@ module Omnizip
|
|
|
68
68
|
@entries << entry
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Add an explicit directory entry with no data or stream
|
|
72
|
+
#
|
|
73
|
+
# @param archive_path [String] Directory path inside the
|
|
74
|
+
# archive (trailing slash optional)
|
|
75
|
+
# @return [self]
|
|
76
|
+
def add_directory_entry(archive_path)
|
|
77
|
+
entry = Models::FileEntry.new
|
|
78
|
+
entry.name = archive_path.chomp("/")
|
|
79
|
+
entry.source_path = nil
|
|
80
|
+
entry.size = 0
|
|
81
|
+
entry.is_dir = true
|
|
82
|
+
entry.has_stream = false
|
|
83
|
+
entry.mtime = Time.now
|
|
84
|
+
@entries << entry
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
71
88
|
def write
|
|
72
89
|
# Collect any files from the collector (if add_file/add_directory was used)
|
|
73
90
|
collected_entries = @collector.collect_files
|