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
|
@@ -99,15 +99,16 @@ writer.close
|
|
|
99
99
|
[source,ruby]
|
|
100
100
|
----
|
|
101
101
|
# 7z archive
|
|
102
|
-
|
|
103
|
-
writer.add_file('document.txt')
|
|
104
|
-
writer.add_directory('photos/')
|
|
105
|
-
|
|
102
|
+
Omnizip::Formats::SevenZip.create('archive.7z') do |writer|
|
|
103
|
+
writer.add_file('document.txt')
|
|
104
|
+
writer.add_directory('photos/')
|
|
105
|
+
end
|
|
106
106
|
|
|
107
107
|
# ZIP archive
|
|
108
108
|
writer = Omnizip::Formats::Zip::Writer.new('archive.zip')
|
|
109
109
|
writer.add_file('document.txt')
|
|
110
|
-
writer.
|
|
110
|
+
writer.add_directory('photos/')
|
|
111
|
+
writer.write
|
|
111
112
|
----
|
|
112
113
|
|
|
113
114
|
==== Extracting Archives
|
|
@@ -115,26 +116,24 @@ writer.close
|
|
|
115
116
|
[source,ruby]
|
|
116
117
|
----
|
|
117
118
|
# 7z archive
|
|
118
|
-
|
|
119
|
-
reader.extract_all('output/')
|
|
120
|
-
|
|
119
|
+
Omnizip::Formats::SevenZip.open('archive.7z') do |reader|
|
|
120
|
+
reader.extract_all('output/')
|
|
121
|
+
end
|
|
121
122
|
|
|
122
|
-
# ZIP archive
|
|
123
|
-
reader = Omnizip::Formats::Zip::Reader.new('archive.zip')
|
|
123
|
+
# ZIP archive (call .read to parse before extracting)
|
|
124
|
+
reader = Omnizip::Formats::Zip::Reader.new('archive.zip').read
|
|
124
125
|
reader.extract_all('output/')
|
|
125
|
-
reader.close
|
|
126
126
|
----
|
|
127
127
|
|
|
128
128
|
==== Listing Contents
|
|
129
129
|
|
|
130
130
|
[source,ruby]
|
|
131
131
|
----
|
|
132
|
-
|
|
133
|
-
reader.
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
Omnizip::Formats::SevenZip.open('archive.7z') do |reader|
|
|
133
|
+
reader.list_files.each do |entry|
|
|
134
|
+
puts "#{entry.name}: #{entry.size} bytes"
|
|
135
|
+
end
|
|
136
136
|
end
|
|
137
|
-
reader.close
|
|
138
137
|
----
|
|
139
138
|
|
|
140
139
|
=== Error Handling
|
|
@@ -145,7 +144,7 @@ begin
|
|
|
145
144
|
Omnizip.compress_file('input.txt', 'output.lzma')
|
|
146
145
|
rescue Omnizip::CompressionError => e
|
|
147
146
|
puts "Compression failed: #{e.message}"
|
|
148
|
-
rescue Omnizip::
|
|
147
|
+
rescue Omnizip::AlgorithmNotFoundError => e
|
|
149
148
|
puts "Algorithm not found: #{e.message}"
|
|
150
149
|
rescue IOError => e
|
|
151
150
|
puts "I/O error: #{e.message}"
|
|
@@ -188,11 +188,8 @@ omnizip archive create archive.zip files/ \
|
|
|
188
188
|
|
|
189
189
|
[source,ruby]
|
|
190
190
|
----
|
|
191
|
-
#
|
|
191
|
+
# One reader covers both RAR4 and RAR5 (version auto-detected)
|
|
192
192
|
reader = Omnizip::Formats::Rar::Reader.new('archive.rar')
|
|
193
|
-
|
|
194
|
-
# Or RAR5 reader
|
|
195
|
-
reader = Omnizip::Formats::Rar::Rar5::Reader.new('archive.rar')
|
|
196
193
|
----
|
|
197
194
|
|
|
198
195
|
### Getting Help
|
|
@@ -71,6 +71,8 @@ module Omnizip
|
|
|
71
71
|
tar: -> { Omnizip::ArchiveHandlers::TarHandler },
|
|
72
72
|
seven_zip: -> { Omnizip::ArchiveHandlers::SevenZipHandler },
|
|
73
73
|
rar: -> { Omnizip::ArchiveHandlers::RarHandler },
|
|
74
|
+
cpio: -> { Omnizip::ArchiveHandlers::CpioHandler },
|
|
75
|
+
iso: -> { Omnizip::ArchiveHandlers::IsoHandler },
|
|
74
76
|
}.freeze
|
|
75
77
|
private_constant :LAZY_LOAD_TRIGGERS
|
|
76
78
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module ArchiveHandlers
|
|
7
|
+
# Read-only adapter for the CPIO format (initramfs images, RPM
|
|
8
|
+
# payloads). Extraction and listing are supported; CPIO writing
|
|
9
|
+
# exists at the format level but is not exposed through the
|
|
10
|
+
# convenience archive API.
|
|
11
|
+
class CpioHandler
|
|
12
|
+
def create(_path, **_options)
|
|
13
|
+
raise Omnizip::UnsupportedFormatError,
|
|
14
|
+
"cpio archives cannot be created through the convenience " \
|
|
15
|
+
"API; use Omnizip::Formats::Cpio.create directly"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_to(path, output_dir, **_)
|
|
19
|
+
Omnizip::Formats::Cpio.extract(path, output_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list(path, details: false, **_)
|
|
23
|
+
entries = Omnizip::Formats::Cpio.list(path)
|
|
24
|
+
if details
|
|
25
|
+
entries.map do |e|
|
|
26
|
+
{ name: e.name, size: e.data.bytesize,
|
|
27
|
+
directory: e.name.end_with?("/"), mtime: nil }
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
entries.map(&:name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_entry(path, entry_name, **_)
|
|
35
|
+
entry = Omnizip::Formats::Cpio.list(path)
|
|
36
|
+
.find { |e| e.name == entry_name }
|
|
37
|
+
unless entry
|
|
38
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
entry.data
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Omnizip::ArchiveHandler.register(:cpio, Omnizip::ArchiveHandlers::CpioHandler.new)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module ArchiveHandlers
|
|
7
|
+
# Read-only adapter for the ISO 9660 format (disk images).
|
|
8
|
+
# Extraction and listing are supported; ISO writing exists at the
|
|
9
|
+
# format level but is not exposed through the convenience archive
|
|
10
|
+
# API.
|
|
11
|
+
class IsoHandler
|
|
12
|
+
def create(_path, **_options)
|
|
13
|
+
raise Omnizip::UnsupportedFormatError,
|
|
14
|
+
"iso images cannot be created through the convenience " \
|
|
15
|
+
"API; use Omnizip::Formats::Iso.create directly"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_to(path, output_dir, **_)
|
|
19
|
+
Omnizip::Formats::Iso.extract(path, output_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list(path, details: false, **_)
|
|
23
|
+
entries = Omnizip::Formats::Iso.list(path)
|
|
24
|
+
if details
|
|
25
|
+
entries.map do |e|
|
|
26
|
+
{ name: e.full_path, size: e.size, directory: e.directory?,
|
|
27
|
+
mtime: e.recording_date }
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
entries.map(&:full_path)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_entry(path, entry_name, **_)
|
|
35
|
+
entry = Omnizip::Formats::Iso.list(path)
|
|
36
|
+
.find { |e| e.full_path == entry_name }
|
|
37
|
+
unless entry
|
|
38
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}"
|
|
39
|
+
end
|
|
40
|
+
raise Errno::EISDIR, "Entry is a directory: #{entry_name}" if entry.directory?
|
|
41
|
+
|
|
42
|
+
Dir.mktmpdir("omnizip-iso-entry") do |dir|
|
|
43
|
+
dest = File.join(dir, "entry")
|
|
44
|
+
Omnizip::Formats::Iso.open(path) do |iso|
|
|
45
|
+
iso.extract_entry(entry_name, dest)
|
|
46
|
+
end
|
|
47
|
+
File.binread(dest)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Omnizip::ArchiveHandler.register(:iso, Omnizip::ArchiveHandlers::IsoHandler.new)
|
|
@@ -9,5 +9,7 @@ module Omnizip
|
|
|
9
9
|
autoload :TarHandler, "omnizip/archive_handlers/tar_handler"
|
|
10
10
|
autoload :SevenZipHandler, "omnizip/archive_handlers/seven_zip_handler"
|
|
11
11
|
autoload :RarHandler, "omnizip/archive_handlers/rar_handler"
|
|
12
|
+
autoload :CpioHandler, "omnizip/archive_handlers/cpio_handler"
|
|
13
|
+
autoload :IsoHandler, "omnizip/archive_handlers/iso_handler"
|
|
12
14
|
end
|
|
13
15
|
end
|
data/lib/omnizip/cli.rb
CHANGED
|
@@ -25,6 +25,28 @@ module Omnizip
|
|
|
25
25
|
# under Omnizip::Cli would re-trigger this file's autoload
|
|
26
26
|
# mid-load.
|
|
27
27
|
module Shared
|
|
28
|
+
# Thor 1.x does not treat --help/-h on subcommands as a request
|
|
29
|
+
# for help: the flag reaches the command as a positional argument
|
|
30
|
+
# and arity checking fails. Classes extending this module route
|
|
31
|
+
# those flags to the standard help output instead.
|
|
32
|
+
module HelpDispatch
|
|
33
|
+
def dispatch(meth, given_args, given_opts, config)
|
|
34
|
+
wants_help = [Array(given_args), Array(given_opts)].any? do |list|
|
|
35
|
+
list.include?("--help") || list.include?("-h")
|
|
36
|
+
end
|
|
37
|
+
if wants_help
|
|
38
|
+
name = meth || Array(given_args).grep_v(/\A-/).first
|
|
39
|
+
# Subcommand groups intercept their own --help (e.g.
|
|
40
|
+
# "archive create --help" must show the create usage).
|
|
41
|
+
return super if subcommands.include?(name)
|
|
42
|
+
|
|
43
|
+
return start(["help", name].compact, config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
# Print a formatted error message and exit nonzero.
|
|
29
51
|
#
|
|
30
52
|
# @param error [StandardError] the error to display
|
|
@@ -51,6 +73,7 @@ module Omnizip
|
|
|
51
73
|
# Profile commands subcommand group
|
|
52
74
|
class ProfileCommands < Thor
|
|
53
75
|
include Shared
|
|
76
|
+
extend Shared::HelpDispatch
|
|
54
77
|
|
|
55
78
|
class << self
|
|
56
79
|
def exit_on_failure?
|
|
@@ -99,6 +122,7 @@ module Omnizip
|
|
|
99
122
|
# Archive commands subcommand group
|
|
100
123
|
class ArchiveCommands < Thor
|
|
101
124
|
include Shared
|
|
125
|
+
extend Shared::HelpDispatch
|
|
102
126
|
|
|
103
127
|
class << self
|
|
104
128
|
def exit_on_failure?
|
|
@@ -328,6 +352,7 @@ module Omnizip
|
|
|
328
352
|
# files using various compression algorithms.
|
|
329
353
|
class Cli < Thor
|
|
330
354
|
include Shared
|
|
355
|
+
extend Shared::HelpDispatch
|
|
331
356
|
|
|
332
357
|
class << self
|
|
333
358
|
def exit_on_failure?
|
data/lib/omnizip/convenience.rb
CHANGED
|
@@ -230,16 +230,18 @@ module Omnizip
|
|
|
230
230
|
".7z" => :seven_zip,
|
|
231
231
|
}.freeze
|
|
232
232
|
|
|
233
|
-
# Extensions naming
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
233
|
+
# Extensions naming formats with read-only convenience routing.
|
|
234
|
+
# Writing them a ZIP under a foreign name was silent corruption;
|
|
235
|
+
# creation raises truthfully instead (the format-level writers
|
|
236
|
+
# exist for RAR/CPIO/ISO but are not part of the archive API).
|
|
237
237
|
READ_ONLY_FORMAT_EXTENSIONS = [".rar", ".iso", ".cpio"].freeze
|
|
238
238
|
|
|
239
239
|
# Extensions whose format has a READ-ONLY handler: extraction and
|
|
240
240
|
# listing route to it, while creation keeps raising.
|
|
241
241
|
READ_ARCHIVE_FORMAT_EXTENSIONS = {
|
|
242
242
|
".rar" => :rar,
|
|
243
|
+
".cpio" => :cpio,
|
|
244
|
+
".iso" => :iso,
|
|
243
245
|
}.freeze
|
|
244
246
|
|
|
245
247
|
# Extension -> single-file decompressor (stream interface).
|
|
@@ -43,6 +43,12 @@ module Omnizip
|
|
|
43
43
|
# Allocate sectors for directories and files
|
|
44
44
|
allocate_sectors(tree)
|
|
45
45
|
|
|
46
|
+
# Tree nodes are separate hashes from @files entries; copy
|
|
47
|
+
# the allocated file locations onto the tree so directory
|
|
48
|
+
# records reference the real extents (otherwise every
|
|
49
|
+
# record points at sector 0).
|
|
50
|
+
assign_file_locations(tree)
|
|
51
|
+
|
|
46
52
|
# Build path table
|
|
47
53
|
path_table = build_path_table(tree)
|
|
48
54
|
|
|
@@ -143,6 +149,24 @@ module Omnizip
|
|
|
143
149
|
allocate_file_sectors
|
|
144
150
|
end
|
|
145
151
|
|
|
152
|
+
# Copy allocated file locations onto the matching tree nodes
|
|
153
|
+
#
|
|
154
|
+
# @param tree [Hash] Directory tree root
|
|
155
|
+
def assign_file_locations(tree)
|
|
156
|
+
walk = lambda do |node|
|
|
157
|
+
(node[:children] || []).each do |child|
|
|
158
|
+
if child[:directory]
|
|
159
|
+
walk.call(child)
|
|
160
|
+
else
|
|
161
|
+
match = @files.find { |f| f[:iso_path] == child[:iso_path] }
|
|
162
|
+
child[:location] = match[:location] if match
|
|
163
|
+
child[:size] = match[:size] if match
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
walk.call(tree)
|
|
168
|
+
end
|
|
169
|
+
|
|
146
170
|
# Allocate sectors for a directory
|
|
147
171
|
#
|
|
148
172
|
# @param dir_node [Hash] Directory node
|
|
@@ -90,8 +90,8 @@ module Omnizip
|
|
|
90
90
|
@entries.each do |entry|
|
|
91
91
|
next if entry.current_directory? || entry.parent_directory?
|
|
92
92
|
|
|
93
|
-
output_path = File.join(output_dir, entry.
|
|
94
|
-
extract_entry(entry.
|
|
93
|
+
output_path = File.join(output_dir, entry.full_path)
|
|
94
|
+
extract_entry(entry.full_path, output_path)
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -178,8 +178,13 @@ module Omnizip
|
|
|
178
178
|
preparer: "OMNIZIP #{Omnizip::VERSION}",
|
|
179
179
|
application: "OMNIZIP",
|
|
180
180
|
level: 2,
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
# Rock Ridge System Use fields and Joliet UCS-2 directory
|
|
182
|
+
# trees are not implemented; advertising either extension
|
|
183
|
+
# makes readers misparse the image (e.g. 7-Zip reads the
|
|
184
|
+
# ASCII names as UCS-2 via the cloned SVD), so both stay
|
|
185
|
+
# off until real implementations land.
|
|
186
|
+
rock_ridge: false,
|
|
187
|
+
joliet: false,
|
|
183
188
|
}
|
|
184
189
|
end
|
|
185
190
|
|
|
@@ -193,6 +198,7 @@ module Omnizip
|
|
|
193
198
|
source: File.expand_path(dir_path),
|
|
194
199
|
iso_path: iso_path,
|
|
195
200
|
stat: File.stat(dir_path),
|
|
201
|
+
directory: true,
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
# Add all contents
|
|
@@ -219,6 +225,7 @@ module Omnizip
|
|
|
219
225
|
source: File.expand_path(dir_path),
|
|
220
226
|
iso_path: iso_path,
|
|
221
227
|
stat: File.stat(dir_path),
|
|
228
|
+
directory: true,
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
Dir.foreach(dir_path) do |entry|
|
|
@@ -283,13 +290,22 @@ module Omnizip
|
|
|
283
290
|
# @param builder [VolumeBuilder] Volume builder
|
|
284
291
|
# @param dir_structure [Hash] Directory structure
|
|
285
292
|
def write_volume_descriptors(io, builder, dir_structure)
|
|
293
|
+
# Fixed layout: 16 PVD, 17 terminator, 18 path table (L),
|
|
294
|
+
# 19 path table (BE), directory data from sector 22.
|
|
295
|
+
root = dir_structure[:root].merge(
|
|
296
|
+
total_sectors: dir_structure[:total_sectors],
|
|
297
|
+
path_table_size: dir_structure[:path_table_size],
|
|
298
|
+
path_table_location: 18,
|
|
299
|
+
path_table_location_be: 19,
|
|
300
|
+
)
|
|
301
|
+
|
|
286
302
|
# Write primary volume descriptor
|
|
287
|
-
pvd = builder.build_primary(
|
|
303
|
+
pvd = builder.build_primary(root)
|
|
288
304
|
io.write(pvd)
|
|
289
305
|
|
|
290
306
|
# Write Joliet supplementary descriptor if enabled
|
|
291
307
|
if @joliet
|
|
292
|
-
svd = builder.build_joliet(
|
|
308
|
+
svd = builder.build_joliet(root)
|
|
293
309
|
io.write(svd)
|
|
294
310
|
end
|
|
295
311
|
|
data/lib/omnizip/formats/iso.rb
CHANGED
|
@@ -69,7 +69,9 @@ module Omnizip
|
|
|
69
69
|
# @param path [String] Path to ISO file
|
|
70
70
|
# @return [Array<DirectoryRecord>] Directory entries
|
|
71
71
|
def self.list(path)
|
|
72
|
-
|
|
72
|
+
entries = nil
|
|
73
|
+
open(path) { |iso| entries = iso.entries }
|
|
74
|
+
entries
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
# Extract ISO contents
|
|
@@ -76,6 +76,9 @@ module Omnizip
|
|
|
76
76
|
# Read file name
|
|
77
77
|
name_bytes = io.read(name_size)
|
|
78
78
|
entry.name = decode_filename(name_bytes, head_flags)
|
|
79
|
+
# RAR4 archives store DOS-style backslash separators;
|
|
80
|
+
# normalize to forward slashes like unrar on Unix.
|
|
81
|
+
entry.name = entry.name.tr("\\", "/")
|
|
79
82
|
|
|
80
83
|
# Set entry properties
|
|
81
84
|
entry.size = unpack_size
|
|
@@ -88,18 +91,25 @@ module Omnizip
|
|
|
88
91
|
entry.attributes = attr
|
|
89
92
|
entry.mtime = dos_time_to_time(file_time)
|
|
90
93
|
|
|
91
|
-
# Set flags
|
|
92
|
-
|
|
94
|
+
# Set flags. RAR4 directory entries carry the full
|
|
95
|
+
# dictionary mask 0xE0 in HEAD_FLAGS (a directory has no
|
|
96
|
+
# dictionary); the exact match avoids misreading a large
|
|
97
|
+
# dictionary size on files as the marker.
|
|
98
|
+
entry.is_dir = head_flags.allbits?(FILE_DIRECTORY)
|
|
99
|
+
# WinRAR writes directory names with a trailing backslash;
|
|
100
|
+
# normalize it away for host paths.
|
|
101
|
+
entry.name = entry.name.chomp("\\") if entry.is_dir
|
|
93
102
|
entry.encrypted = head_flags.anybits?(FILE_ENCRYPTED)
|
|
94
103
|
entry.split_before = head_flags.anybits?(FILE_SPLIT_BEFORE)
|
|
95
104
|
entry.split_after = head_flags.anybits?(FILE_SPLIT_AFTER)
|
|
96
105
|
|
|
97
|
-
# Skip remaining header data and file data
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
remaining
|
|
106
|
+
# Skip remaining header data and file data. HEAD_SIZE
|
|
107
|
+
# counts the whole block including the 2-byte HEAD_CRC;
|
|
108
|
+
# consumed so far: 2 (CRC) + 30 (fixed fields) + name, plus
|
|
109
|
+
# 8 more when high-size words were present. Leftover bytes
|
|
110
|
+
# cover salt/extra time fields without interpreting them.
|
|
111
|
+
remaining = head_size - (name_size + 32)
|
|
112
|
+
remaining -= 8 if head_flags.anybits?(FILE_LARGE)
|
|
103
113
|
io.read(remaining) if remaining.positive?
|
|
104
114
|
io.read(pack_size) # Skip compressed data
|
|
105
115
|
|
|
@@ -21,6 +21,10 @@ module Omnizip
|
|
|
21
21
|
BLOCK_SUBBLOCK = 0x7A
|
|
22
22
|
BLOCK_ENDARC = 0x7B
|
|
23
23
|
|
|
24
|
+
# HEAD_FLAGS bit 0x8000 (LONG_BLOCK): the block carries a
|
|
25
|
+
# data area whose size is the PACK_SIZE field at offset 7
|
|
26
|
+
BLOCK_LONG = 0x8000
|
|
27
|
+
|
|
24
28
|
# Archive flags
|
|
25
29
|
ARCHIVE_VOLUME = 0x0001
|
|
26
30
|
ARCHIVE_COMMENT = 0x0002
|
|
@@ -152,7 +152,7 @@ module Omnizip
|
|
|
152
152
|
"WinRAR", "UnRAR.exe"
|
|
153
153
|
),
|
|
154
154
|
].each do |path|
|
|
155
|
-
return
|
|
155
|
+
return path if File.exist?(path)
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
nil
|
|
@@ -176,7 +176,7 @@ module Omnizip
|
|
|
176
176
|
def command_version
|
|
177
177
|
return nil unless command_available?
|
|
178
178
|
|
|
179
|
-
output =
|
|
179
|
+
output = `"#{command_path}" 2>&1`
|
|
180
180
|
output.match(/UNRAR\s+([\d.]+)/i)&.captures&.first || "unknown"
|
|
181
181
|
end
|
|
182
182
|
|
|
@@ -191,7 +191,7 @@ module Omnizip
|
|
|
191
191
|
# Extract with system command
|
|
192
192
|
def extract_with_command(archive_path, output_dir, password)
|
|
193
193
|
cmd = build_extract_command(archive_path, output_dir, password)
|
|
194
|
-
return if system(cmd)
|
|
194
|
+
return if system(*cmd)
|
|
195
195
|
|
|
196
196
|
raise "Command extraction failed: #{archive_path}"
|
|
197
197
|
end
|
|
@@ -215,7 +215,7 @@ module Omnizip
|
|
|
215
215
|
|
|
216
216
|
# List with system command
|
|
217
217
|
def list_with_command(archive_path)
|
|
218
|
-
output =
|
|
218
|
+
output = `"#{command_path}" vb "#{archive_path}" 2>&1`
|
|
219
219
|
raise "Command listing failed" unless $CHILD_STATUS.success?
|
|
220
220
|
|
|
221
221
|
output.split("\n").map do |line|
|
|
@@ -240,7 +240,7 @@ module Omnizip
|
|
|
240
240
|
output_path, password)
|
|
241
241
|
temp_dir = Dir.mktmpdir
|
|
242
242
|
cmd = build_extract_command(archive_path, temp_dir, password)
|
|
243
|
-
unless system(cmd)
|
|
243
|
+
unless system(*cmd)
|
|
244
244
|
raise "Command entry extraction failed: #{entry_name}"
|
|
245
245
|
end
|
|
246
246
|
|
|
@@ -252,9 +252,13 @@ module Omnizip
|
|
|
252
252
|
|
|
253
253
|
# Build extract command
|
|
254
254
|
def build_extract_command(archive_path, output_dir, password)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
# Array form: no shell involved, so paths and passwords
|
|
256
|
+
# with special characters stay literal. -idq silences the
|
|
257
|
+
# per-file progress banner.
|
|
258
|
+
cmd = [command_path, "x", "-idq", "-y"]
|
|
259
|
+
cmd << "-p#{password}" if password
|
|
260
|
+
cmd << archive_path
|
|
261
|
+
cmd << "#{output_dir}/"
|
|
258
262
|
cmd
|
|
259
263
|
end
|
|
260
264
|
|
|
@@ -84,20 +84,12 @@ module Omnizip
|
|
|
84
84
|
|
|
85
85
|
# Parse RAR4 header
|
|
86
86
|
#
|
|
87
|
+
# The 7-byte signature consumed by #parse IS the marker
|
|
88
|
+
# block, so the main header follows immediately.
|
|
89
|
+
#
|
|
87
90
|
# @param io [IO] Input stream
|
|
88
91
|
def parse_rar4_header(io)
|
|
89
|
-
#
|
|
90
|
-
read_uint16(io)
|
|
91
|
-
head_type = io.read(1)&.ord
|
|
92
|
-
read_uint16(io)
|
|
93
|
-
read_uint16(io)
|
|
94
|
-
|
|
95
|
-
unless head_type == BLOCK_MARKER
|
|
96
|
-
raise "Expected marker block, got 0x#{head_type.to_s(16)}"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Read archive header
|
|
100
|
-
read_uint16(io)
|
|
92
|
+
read_uint16(io) # HEAD_CRC
|
|
101
93
|
head_type = io.read(1)&.ord
|
|
102
94
|
head_flags = read_uint16(io)
|
|
103
95
|
head_size = read_uint16(io)
|
|
@@ -112,9 +104,9 @@ module Omnizip
|
|
|
112
104
|
@is_locked = head_flags.anybits?(ARCHIVE_LOCKED)
|
|
113
105
|
@comment_present = head_flags.anybits?(ARCHIVE_COMMENT)
|
|
114
106
|
|
|
115
|
-
# Skip rest of archive header
|
|
116
|
-
#
|
|
117
|
-
remaining = head_size -
|
|
107
|
+
# Skip rest of archive header. HEAD_SIZE counts the whole
|
|
108
|
+
# block including the 2 HEAD_CRC bytes; 7 bytes consumed.
|
|
109
|
+
remaining = head_size - 7
|
|
118
110
|
io.read(remaining) if remaining.positive?
|
|
119
111
|
end
|
|
120
112
|
|
|
@@ -42,9 +42,11 @@ module Omnizip
|
|
|
42
42
|
#
|
|
43
43
|
# @return [Boolean] true if implemented
|
|
44
44
|
def available?
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
45
|
+
# Decoder is implemented; the encoder is NOT compatible
|
|
46
|
+
# with official RAR tools (unrar "tests" its output as
|
|
47
|
+
# OK while extracting empty files). Callers fall back
|
|
48
|
+
# to STORE until a RAR-compatible encoder lands.
|
|
49
|
+
false
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# Compress data using RAR5 LZSS
|