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.
@@ -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
- # This class provides basic RAR archive creation in pure Ruby.
13
- # It writes RAR4-compatible archives with basic compression support.
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
- # @option options [Integer] :recovery Recovery record percentage (0-10)
68
- # @option options [Boolean] :encrypt_headers Encrypt file names
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
- # @option options [Integer] :volume_size Split into volumes (bytes)
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
- # Calculate CRC16-CCITT for block headers
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
- # RAR uses CRC16-CCITT with polynomial 0x1021
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 calculate_header_crc16(data)
152
- crc = 0
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 marker block
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
- [flags].pack("v") +
204
- [0x000B].pack("v") +
205
- [0, 0, 0].pack("vvv") # 6 bytes reserved (3 x uint16)
209
+ [0x0000].pack("v") + # HEAD_FLAGS
210
+ [0x000D].pack("v") + # HEAD_SIZE
211
+ [0].pack("v") + [0].pack("V") # HighPosAV, PosAV
206
212
 
207
- # Calculate and write CRC
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
- # Select appropriate compression method based on data
237
- method = file_info[:method] || select_compression_method(file_data)
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
- # Flags
250
- flags = FILE_UNICODE
251
- flags |= FILE_ENCRYPTED if @options[:password]
252
- flags |= FILE_LARGE if file_data.size > 0xFFFFFFFF
253
-
254
- # Calculate header size
255
- # Fixed fields: TYPE(1) + FLAGS(2) + SIZE(2) + PACK_SIZE(4) + UNPACK_SIZE(4) +
256
- # HOST_OS(1) + FILE_CRC(4) + FILE_TIME(4) + VERSION(1) + METHOD(1) +
257
- # NAME_SIZE(2) + ATTR(4) = 30 bytes
258
- header_size = 30 + name_bytes.size
259
- header_size += 8 if flags & FILE_LARGE != 0
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") + # VERSION first
271
- [method].pack("C") + # METHOD second
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
- # Add high 32 bits for large files
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
- # Calculate and write CRC
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
- recursive = dir_info[:recursive]
298
-
299
- pattern = recursive ? "**/*" : "*"
300
- Dir.glob(File.join(dir_path, pattern)).each do |path|
301
- next unless File.file?(path)
302
-
303
- relative_path = path.sub("#{dir_path}/", "")
304
- write_file_entry(io, {
305
- source: path,
306
- archive_path: relative_path,
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
- [flags].pack("v") +
325
+ [0x0000].pack("v") +
320
326
  [0x0007].pack("v")
321
327
 
322
- # Calculate and write CRC
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 Zlib compression level (DEPRECATED - kept for compatibility)
376
- #
377
- # @return [Integer] Zlib compression level
378
- def compression_zlib_level
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
- File.open(@output_path, "rb") do |io|
421
- signature = io.read(7)
422
- signature&.bytes == RAR4_SIGNATURE[0..6]
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
@@ -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
- # Read filename
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 += 8
199
+ pos + 8
200
200
  end
201
201
 
202
- # Read extended time if present
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
- header_data.write([0, 0].pack("vv"))
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
- flags = spec.format.file_flags[:unicode]
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: 0x4000, # Archive end flag
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
- mtime = Time.now
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
- read_vint(io)
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
@@ -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"