omnizip 0.3.45 → 0.3.46
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 +25 -0
- data/lib/omnizip/archive_handlers/rar_handler.rb +51 -9
- data/lib/omnizip/cli.rb +12 -0
- data/lib/omnizip/formats/rar/rar5/header.rb +14 -7
- data/lib/omnizip/formats/rar/rar5/writer.rb +45 -2
- data/lib/omnizip/formats/rar/writer.rb +26 -4
- data/lib/omnizip/formats/rar.rb +2 -2
- data/lib/omnizip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb792b03b604528825ce30f9d2ef6c7b6ac077535f5a4bf027498a44138ab692
|
|
4
|
+
data.tar.gz: 777e4ed705e6aa51b53ca8bccf2b394ae24f6f9b4404325db398a24e13b5a811
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d512bfb76c29f2ca7a59bc6e46f558c2bc659b38dc1878b50ea879f91f93beff7c52a27a033b002f0a3e27ae2968484a8cb27b68aaa462a86aad9906f360a854
|
|
7
|
+
data.tar.gz: 25fdb5dacbe8dd8efb6835d916c8b455bcf07197845126dc8b832bbbf80418b80e904c5338a21e0300df6db83fd872fb6faa132f40e9b947ba19496a38eb44d2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.45] - 2026-08-31
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The ZIP ArchiveHandler — the seam the facade, the convenience
|
|
14
|
+
layer, and the CLI all route through — now sits on the native
|
|
15
|
+
`Formats::Zip` tree instead of the rubyzip-compat
|
|
16
|
+
`Omnizip::Zip::File` layer (which remains for in-place entry
|
|
17
|
+
editing and Metadata, by design). The native reader gains
|
|
18
|
+
`#read_entry` (decompress + CRC-verify, shared with extraction)
|
|
19
|
+
and the central-directory entry model gains the `Omnizip::Entry`
|
|
20
|
+
contract plus DOS-time decoding.
|
|
21
|
+
- CLI `archive list` and `archive extract` route through the
|
|
22
|
+
handler registry instead of hand-rolled format dispatch:
|
|
23
|
+
`archive list foo.tar` no longer falls into the 7z reader, zip
|
|
24
|
+
listing no longer opens the compat layer, and every handler's
|
|
25
|
+
`extract_to` returns the extracted file paths (7z/rar/tar
|
|
26
|
+
previously returned the reader or nil). Verbose listings show
|
|
27
|
+
real per-entry compressed sizes for the formats that track them.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Parity hardening: a nil PAR2 index crashed five frames deep as a
|
|
31
|
+
`TypeError`; the repairer boundary now raises a named
|
|
32
|
+
`ArgumentError` and the spec fixture can no longer select a nil
|
|
33
|
+
index (the order-coupled CI flake from 0.3.44).
|
|
34
|
+
|
|
10
35
|
## [0.3.44] - 2026-08-31
|
|
11
36
|
|
|
12
37
|
### Fixed
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tmpdir"
|
|
4
|
+
require "tempfile"
|
|
4
5
|
|
|
5
6
|
module Omnizip
|
|
6
7
|
module ArchiveHandlers
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# archives truthfully instead of raising on every operation.
|
|
8
|
+
# Adapter for the RAR format: extraction/listing/reading, plus
|
|
9
|
+
# creation through Formats::Rar (RAR5 STORE is unrar-verified;
|
|
10
|
+
# compressed methods fall back to STORE with a warning — see the
|
|
11
|
+
# README interop table).
|
|
12
12
|
class RarHandler
|
|
13
|
-
def create(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
def create(path, **options, &block)
|
|
14
|
+
proxy = nil
|
|
15
|
+
result = Omnizip::Formats::Rar.create(path, options) do |writer|
|
|
16
|
+
proxy = WriterProxy.new(writer)
|
|
17
|
+
block&.call(proxy)
|
|
18
|
+
end
|
|
19
|
+
proxy&.cleanup
|
|
20
|
+
result
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def extract_to(path, output_dir, password: nil, **_)
|
|
@@ -47,6 +51,44 @@ module Omnizip
|
|
|
47
51
|
File.binread(dest)
|
|
48
52
|
end
|
|
49
53
|
end
|
|
54
|
+
|
|
55
|
+
# Translates the generic +add(name, source_path)+ /
|
|
56
|
+
# +add_data(name, data)+ / +add_directory(name)+ interface onto
|
|
57
|
+
# the RAR writers. The writers take file paths, so in-memory
|
|
58
|
+
# content spills to a temporary file.
|
|
59
|
+
class WriterProxy
|
|
60
|
+
def initialize(writer)
|
|
61
|
+
@writer = writer
|
|
62
|
+
@spill = []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add(name, source_path = nil)
|
|
66
|
+
if source_path
|
|
67
|
+
@writer.add_file(source_path, name)
|
|
68
|
+
else
|
|
69
|
+
@writer.add_directory_entry(name)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def add_data(name, data)
|
|
74
|
+
file = Tempfile.new(["omnizip_rar_entry", File.extname(name)])
|
|
75
|
+
file.binmode
|
|
76
|
+
file.write(data)
|
|
77
|
+
file.close
|
|
78
|
+
@spill << file
|
|
79
|
+
@writer.add_file(file.path, name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_directory(name)
|
|
83
|
+
@writer.add_directory_entry(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Remove the temporary files add_data spilled
|
|
87
|
+
def cleanup
|
|
88
|
+
@spill.each(&:unlink)
|
|
89
|
+
@spill.clear
|
|
90
|
+
end
|
|
91
|
+
end
|
|
50
92
|
end
|
|
51
93
|
end
|
|
52
94
|
end
|
data/lib/omnizip/cli.rb
CHANGED
|
@@ -355,6 +355,18 @@ module Omnizip
|
|
|
355
355
|
rescue StandardError => e
|
|
356
356
|
handle_error(e)
|
|
357
357
|
end
|
|
358
|
+
|
|
359
|
+
desc "repair ARCHIVE OUTPUT", "Repair a damaged archive"
|
|
360
|
+
long_desc <<~DESC
|
|
361
|
+
Repair a damaged archive (currently RAR archives via recovery
|
|
362
|
+
records and .rev volumes). OUTPUT is the repaired archive path.
|
|
363
|
+
DESC
|
|
364
|
+
option :verbose, type: :boolean, default: false, aliases: "-v"
|
|
365
|
+
def repair(archive, output)
|
|
366
|
+
Omnizip::Commands::ArchiveRepairCommand.new(options).run(archive, output)
|
|
367
|
+
rescue StandardError => e
|
|
368
|
+
handle_error(e)
|
|
369
|
+
end
|
|
358
370
|
end
|
|
359
371
|
|
|
360
372
|
# Command-line interface for Omnizip.
|
|
@@ -106,8 +106,10 @@ header_data: "")
|
|
|
106
106
|
|
|
107
107
|
# File header
|
|
108
108
|
class FileHeader < Header
|
|
109
|
-
# File header flags
|
|
110
|
-
|
|
109
|
+
# File header flags. Per the RAR 5.0 spec, 0x0001 marks a
|
|
110
|
+
# directory entry (what this codebase once mislabeled as
|
|
111
|
+
# "has attributes" — attributes are an always-present field)
|
|
112
|
+
FILE_IS_DIRECTORY = 0x0001
|
|
111
113
|
FILE_HAS_MTIME = 0x0002
|
|
112
114
|
FILE_HAS_CRC32 = 0x0004
|
|
113
115
|
|
|
@@ -119,23 +121,27 @@ header_data: "")
|
|
|
119
121
|
|
|
120
122
|
def initialize(filename:, file_size:, compressed_size:,
|
|
121
123
|
compression_method: 0, dict_size: 0,
|
|
122
|
-
flags: 0, mtime: nil, crc32: nil,
|
|
124
|
+
flags: 0, mtime: nil, crc32: nil,
|
|
125
|
+
directory: false, extra_area: nil)
|
|
123
126
|
# Build file flags based on what's provided
|
|
124
127
|
file_flags = 0
|
|
128
|
+
file_flags |= FILE_IS_DIRECTORY if directory
|
|
125
129
|
file_flags |= FILE_HAS_MTIME if mtime
|
|
126
130
|
file_flags |= FILE_HAS_CRC32 if crc32
|
|
127
131
|
|
|
128
132
|
# Build header data with file information
|
|
129
133
|
data = build_file_data(filename, file_size,
|
|
130
134
|
compression_method, dict_size,
|
|
131
|
-
file_flags, mtime, crc32
|
|
135
|
+
file_flags, mtime, crc32,
|
|
136
|
+
directory: directory)
|
|
132
137
|
super(HEADER_TYPE_FILE, flags: flags, data_area_size: compressed_size, header_data: data, extra_area: extra_area)
|
|
133
138
|
end
|
|
134
139
|
|
|
135
140
|
private
|
|
136
141
|
|
|
137
142
|
def build_file_data(filename, file_size, compression_method,
|
|
138
|
-
dict_size, file_flags, mtime, crc32
|
|
143
|
+
dict_size, file_flags, mtime, crc32,
|
|
144
|
+
directory: false)
|
|
139
145
|
data = []
|
|
140
146
|
|
|
141
147
|
# File flags (VINT)
|
|
@@ -145,8 +151,9 @@ header_data: "")
|
|
|
145
151
|
data.concat(VINT.encode(file_size))
|
|
146
152
|
|
|
147
153
|
# Attributes (VINT) - ALWAYS present in RAR5.
|
|
148
|
-
# Unix host:
|
|
149
|
-
|
|
154
|
+
# Unix host: directory mode for directory entries,
|
|
155
|
+
# regular-file 0o100644 otherwise.
|
|
156
|
+
data.concat(VINT.encode(directory ? 0o040755 : 0o100644))
|
|
150
157
|
|
|
151
158
|
# mtime (optional) - only if FILE_HAS_MTIME flag is set
|
|
152
159
|
# RAR5 stores mtime as Unix timestamp (seconds since epoch)
|
|
@@ -121,6 +121,22 @@ module Omnizip
|
|
|
121
121
|
}
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
+
# Add a name-only directory entry (empty trees survive
|
|
125
|
+
# archive creation instead of vanishing)
|
|
126
|
+
#
|
|
127
|
+
# @param archive_path [String] Directory path inside the
|
|
128
|
+
# archive (trailing slash optional)
|
|
129
|
+
# @return [self]
|
|
130
|
+
def add_directory_entry(archive_path)
|
|
131
|
+
@files << {
|
|
132
|
+
input: nil,
|
|
133
|
+
archive: archive_path.chomp("/"),
|
|
134
|
+
mtime: Time.now,
|
|
135
|
+
directory: true,
|
|
136
|
+
}
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
|
|
124
140
|
# Add directory recursively
|
|
125
141
|
#
|
|
126
142
|
# @param dir_path [String] Directory path
|
|
@@ -227,11 +243,18 @@ module Omnizip
|
|
|
227
243
|
# @param files [Array<Hash>] Files to compress in solid mode
|
|
228
244
|
# @return [void]
|
|
229
245
|
def write_solid_block(io, files)
|
|
246
|
+
# Directory entries carry no data; write their headers
|
|
247
|
+
# separately so the solid stream indexes stay aligned
|
|
248
|
+
files.each do |file|
|
|
249
|
+
write_directory_file_entry(io, file) if file[:directory]
|
|
250
|
+
end
|
|
251
|
+
solid_files = files.reject { |f| f[:directory] }
|
|
252
|
+
|
|
230
253
|
# Create solid manager
|
|
231
254
|
manager = Solid::SolidManager.new(level: @options[:level])
|
|
232
255
|
|
|
233
256
|
# Add all files to solid stream
|
|
234
|
-
|
|
257
|
+
solid_files.each do |file|
|
|
235
258
|
data = File.binread(file[:input])
|
|
236
259
|
manager.add_file(file[:archive], data, mtime: file[:mtime],
|
|
237
260
|
stat: file[:stat])
|
|
@@ -242,7 +265,7 @@ module Omnizip
|
|
|
242
265
|
|
|
243
266
|
# Write each file header with references to the solid block
|
|
244
267
|
# In RAR5, all files share the same compressed data
|
|
245
|
-
|
|
268
|
+
solid_files.each_with_index do |file, idx|
|
|
246
269
|
file_info = result[:files][idx]
|
|
247
270
|
|
|
248
271
|
# Calculate CRC32 if needed (note: only for STORE in non-solid)
|
|
@@ -286,6 +309,8 @@ module Omnizip
|
|
|
286
309
|
# @param io [IO] Output stream
|
|
287
310
|
# @param file [Hash] File information
|
|
288
311
|
def write_file_entry(io, file)
|
|
312
|
+
return write_directory_file_entry(io, file) if file[:directory]
|
|
313
|
+
|
|
289
314
|
# Read file data
|
|
290
315
|
data = File.binread(file[:input])
|
|
291
316
|
|
|
@@ -350,6 +375,24 @@ module Omnizip
|
|
|
350
375
|
io.write(final_data)
|
|
351
376
|
end
|
|
352
377
|
|
|
378
|
+
# Write a directory entry: FileFlags directory bit, zero
|
|
379
|
+
# sizes, no data area
|
|
380
|
+
#
|
|
381
|
+
# @param io [IO] Output stream
|
|
382
|
+
# @param file [Hash] Entry with :archive name and :mtime
|
|
383
|
+
def write_directory_file_entry(io, file)
|
|
384
|
+
header = FileHeader.new(
|
|
385
|
+
filename: file[:archive],
|
|
386
|
+
file_size: 0,
|
|
387
|
+
compressed_size: 0,
|
|
388
|
+
compression_method: Compression::Store::METHOD,
|
|
389
|
+
dict_size: 0,
|
|
390
|
+
mtime: @options[:include_mtime] ? file[:mtime] : nil,
|
|
391
|
+
directory: true,
|
|
392
|
+
)
|
|
393
|
+
io.write(header.encode)
|
|
394
|
+
end
|
|
395
|
+
|
|
353
396
|
# Write End header
|
|
354
397
|
#
|
|
355
398
|
# @param io [IO] Output stream
|
|
@@ -93,6 +93,21 @@ module Omnizip
|
|
|
93
93
|
warn_unimplemented_options
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Add a name-only directory entry (empty trees survive
|
|
97
|
+
# archive creation instead of vanishing)
|
|
98
|
+
#
|
|
99
|
+
# @param archive_path [String] Directory path inside the
|
|
100
|
+
# archive (trailing slash optional)
|
|
101
|
+
# @return [self]
|
|
102
|
+
def add_directory_entry(archive_path)
|
|
103
|
+
@files << {
|
|
104
|
+
source: nil,
|
|
105
|
+
archive_path: archive_path.chomp("/"),
|
|
106
|
+
directory: true,
|
|
107
|
+
}
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
96
111
|
# Add file to archive
|
|
97
112
|
#
|
|
98
113
|
# @param file_path [String] Path to file
|
|
@@ -241,10 +256,17 @@ module Omnizip
|
|
|
241
256
|
(directory ? METHOD_STORE : select_compression_method(file_data))
|
|
242
257
|
compressed_data = compress_data(file_data, method)
|
|
243
258
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
if directory && file_path.nil?
|
|
260
|
+
# Name-only directory entry: no source to stat
|
|
261
|
+
file_attr = 0o040755
|
|
262
|
+
file_time = dos_time(Time.now)
|
|
263
|
+
data_crc = 0
|
|
264
|
+
else
|
|
265
|
+
stat = File.stat(file_path)
|
|
266
|
+
file_attr = directory ? 0o040755 : stat.mode
|
|
267
|
+
file_time = dos_time(stat.mtime)
|
|
268
|
+
data_crc = Zlib.crc32(file_data)
|
|
269
|
+
end
|
|
248
270
|
|
|
249
271
|
name_bytes = archive_path.encode("UTF-8").bytes
|
|
250
272
|
|
data/lib/omnizip/formats/rar.rb
CHANGED
|
@@ -174,8 +174,8 @@ module Omnizip
|
|
|
174
174
|
# @param archive_path [String] Path to corrupted RAR archive
|
|
175
175
|
# @param output_path [String] Path for repaired archive
|
|
176
176
|
# @return [ArchiveRepairer::RepairResult] Repair result
|
|
177
|
-
def repair(archive_path, output_path)
|
|
178
|
-
ArchiveRepairer.new.repair(archive_path, output_path)
|
|
177
|
+
def repair(archive_path, output_path, options = {})
|
|
178
|
+
ArchiveRepairer.new.repair(archive_path, output_path, options)
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
private
|
data/lib/omnizip/version.rb
CHANGED