omnizip 0.3.38 → 0.3.40
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 +58 -0
- data/README.adoc +139 -140
- data/config/formats/rar3_spec.yml +1 -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 +7 -2
- 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/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 +18 -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/seven_zip/writer.rb +17 -0
- data/lib/omnizip/formats.rb +2 -0
- data/lib/omnizip/version.rb +1 -1
- data/readme-docs/api-usage.adoc +6 -5
- data/readme-docs/architecture.adoc +6 -5
- data/readme-docs/compression-algorithms.adoc +1 -1
- data/readme-docs/installation.adoc +1 -1
- data/readme-docs/performance-profiler.adoc +1 -1
- metadata +4 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Omnizip
|
|
7
|
+
module Buffer
|
|
8
|
+
# Bridges the in-memory Buffer API onto the file-based 7z
|
|
9
|
+
# reader/writer through a temporary file, since the 7z format has
|
|
10
|
+
# no in-memory stream implementation.
|
|
11
|
+
module SevenZipBridge
|
|
12
|
+
class << self
|
|
13
|
+
# Create a 7z archive in memory
|
|
14
|
+
#
|
|
15
|
+
# @param buffer [StringIO] Destination buffer
|
|
16
|
+
# @param options [Hash] Writer options
|
|
17
|
+
# @yield [WriteProxy] Block adding entries
|
|
18
|
+
def create(buffer, options = {}, &block)
|
|
19
|
+
Dir.mktmpdir("omnizip_buffer_7z") do |tmp|
|
|
20
|
+
archive_path = File.join(tmp, "buffer.7z")
|
|
21
|
+
writer = Formats::SevenZip::Writer.new(archive_path, options)
|
|
22
|
+
block&.call(WriteProxy.new(writer))
|
|
23
|
+
writer.write
|
|
24
|
+
|
|
25
|
+
buffer.write(File.binread(archive_path))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Read a 7z archive from memory
|
|
30
|
+
#
|
|
31
|
+
# @param buffer [StringIO] Archive bytes
|
|
32
|
+
# @yield [ReadProxy] Block reading entries
|
|
33
|
+
# @return [ReadProxy, Object] Proxy or block result
|
|
34
|
+
def open(buffer, &block)
|
|
35
|
+
Dir.mktmpdir("omnizip_buffer_7z") do |tmp|
|
|
36
|
+
archive_path = File.join(tmp, "buffer.7z")
|
|
37
|
+
File.binwrite(archive_path, buffer.string)
|
|
38
|
+
|
|
39
|
+
reader = Formats::SevenZip::Reader.new(archive_path)
|
|
40
|
+
reader.open
|
|
41
|
+
proxy = ReadProxy.new(reader, tmp)
|
|
42
|
+
|
|
43
|
+
block ? yield(proxy) : proxy
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Mirrors the MemoryArchive write interface onto a 7z writer
|
|
49
|
+
class WriteProxy
|
|
50
|
+
# @param writer [Formats::SevenZip::Writer]
|
|
51
|
+
def initialize(writer)
|
|
52
|
+
@writer = writer
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Add an entry; a trailing slash with empty content adds a
|
|
56
|
+
# directory entry
|
|
57
|
+
#
|
|
58
|
+
# @param name [String] Entry name
|
|
59
|
+
# @param data [String] Entry content
|
|
60
|
+
# @return [self]
|
|
61
|
+
def add(name, data, **_options)
|
|
62
|
+
if name.end_with?("/") && data.to_s.empty?
|
|
63
|
+
@writer.add_directory_entry(name)
|
|
64
|
+
else
|
|
65
|
+
@writer.add_data(name, data)
|
|
66
|
+
end
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Mirrors the MemoryArchive read interface onto a 7z reader
|
|
72
|
+
class ReadProxy
|
|
73
|
+
# @param reader [Formats::SevenZip::Reader] opened reader
|
|
74
|
+
# @param tmp [String] Scratch directory for extraction
|
|
75
|
+
def initialize(reader, tmp)
|
|
76
|
+
@reader = reader
|
|
77
|
+
@tmp = tmp
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Yield an Entry wrapper per archive entry
|
|
81
|
+
def each_entry
|
|
82
|
+
@reader.list_files.each do |entry|
|
|
83
|
+
yield Entry.new(entry, @reader, @tmp)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Filename => content for every file entry
|
|
88
|
+
#
|
|
89
|
+
# @return [Hash<String, String>]
|
|
90
|
+
def extract_all_to_memory
|
|
91
|
+
result = {}
|
|
92
|
+
each_entry do |entry|
|
|
93
|
+
result[entry.name] = entry.read unless entry.directory?
|
|
94
|
+
end
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Entry names; directories keep a trailing slash like the ZIP
|
|
99
|
+
# listing convention
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<String>]
|
|
102
|
+
def entry_names
|
|
103
|
+
@reader.list_files.map do |entry|
|
|
104
|
+
entry.is_dir ? "#{entry.name}/" : entry.name
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Read-only access to the underlying entries
|
|
109
|
+
#
|
|
110
|
+
# @return [Array<Formats::SevenZip::Models::FileEntry>]
|
|
111
|
+
def raw_entries
|
|
112
|
+
@reader.list_files
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Entry wrapper matching MemoryArchive::Entry's surface
|
|
116
|
+
class Entry
|
|
117
|
+
attr_reader :name, :size
|
|
118
|
+
|
|
119
|
+
# @param entry [Formats::SevenZip::Models::FileEntry]
|
|
120
|
+
# @param reader [Formats::SevenZip::Reader]
|
|
121
|
+
# @param tmp [String] Scratch directory
|
|
122
|
+
def initialize(entry, reader, tmp)
|
|
123
|
+
@entry = entry
|
|
124
|
+
@reader = reader
|
|
125
|
+
@tmp = tmp
|
|
126
|
+
@name = entry.is_dir ? "#{entry.name}/" : entry.name
|
|
127
|
+
@size = entry.size
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
def directory?
|
|
132
|
+
@entry.is_dir
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [Time, nil]
|
|
136
|
+
def time
|
|
137
|
+
@entry.mtime
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Extract and return this entry's content
|
|
141
|
+
#
|
|
142
|
+
# @return [String]
|
|
143
|
+
def read(_size = nil)
|
|
144
|
+
return "" if directory? || @entry.has_stream == false
|
|
145
|
+
|
|
146
|
+
dest = File.join(@tmp, "entry", @entry.name)
|
|
147
|
+
@reader.extract_entry(@entry.name, dest)
|
|
148
|
+
File.binread(dest)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/omnizip/buffer.rb
CHANGED
|
@@ -30,6 +30,7 @@ module Omnizip
|
|
|
30
30
|
# Nested classes - autoloaded
|
|
31
31
|
autoload :MemoryArchive, "omnizip/buffer/memory_archive"
|
|
32
32
|
autoload :MemoryExtractor, "omnizip/buffer/memory_extractor"
|
|
33
|
+
autoload :SevenZipBridge, "omnizip/buffer/seven_zip_bridge"
|
|
33
34
|
|
|
34
35
|
class << self
|
|
35
36
|
# Create archive in memory
|
|
@@ -54,7 +55,7 @@ module Omnizip
|
|
|
54
55
|
when :zip
|
|
55
56
|
create_zip(buffer, options, &block)
|
|
56
57
|
when :seven_zip, :"7z"
|
|
57
|
-
|
|
58
|
+
SevenZipBridge.create(buffer, options, &block)
|
|
58
59
|
else
|
|
59
60
|
raise ArgumentError, "Unsupported format: #{format}"
|
|
60
61
|
end
|
|
@@ -84,7 +85,7 @@ module Omnizip
|
|
|
84
85
|
when :zip
|
|
85
86
|
open_zip(buffer, &block)
|
|
86
87
|
when :seven_zip, :"7z"
|
|
87
|
-
|
|
88
|
+
SevenZipBridge.open(buffer, &block)
|
|
88
89
|
else
|
|
89
90
|
raise ArgumentError, "Unsupported format: #{format}"
|
|
90
91
|
end
|
|
@@ -132,14 +133,14 @@ module Omnizip
|
|
|
132
133
|
# @return [Symbol] Detected format
|
|
133
134
|
# @raise [Omnizip::FormatError] If format cannot be detected
|
|
134
135
|
def detect_format(buffer)
|
|
135
|
-
magic = buffer.read(4)
|
|
136
|
+
magic = buffer.read(4).to_s.b
|
|
136
137
|
buffer.rewind
|
|
137
138
|
|
|
138
139
|
case magic
|
|
139
|
-
when "PK\x03\x04", "PK\x05\x06", "PK\x07\x08"
|
|
140
|
+
when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
|
|
140
141
|
# ZIP signatures: local file header, EOCD, data descriptor
|
|
141
142
|
:zip
|
|
142
|
-
when "7z\xBC\xAF"
|
|
143
|
+
when "7z\xBC\xAF".b
|
|
143
144
|
:seven_zip
|
|
144
145
|
else
|
|
145
146
|
raise Omnizip::FormatError,
|
|
@@ -154,8 +154,13 @@ module Omnizip
|
|
|
154
154
|
writer_opts) do |rar|
|
|
155
155
|
inputs.each do |input|
|
|
156
156
|
if File.directory?(input)
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
CliOutputFormatter.verbose_puts(
|
|
158
|
+
"Adding directory: #{input}",
|
|
159
|
+
verbose,
|
|
160
|
+
)
|
|
161
|
+
# Strip the parent so the tree lands under its own
|
|
162
|
+
# name, matching the .7z creation behavior
|
|
163
|
+
rar.add_directory(input, File.dirname(input))
|
|
159
164
|
else
|
|
160
165
|
CliOutputFormatter.verbose_puts(
|
|
161
166
|
"Adding file: #{input}",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
#
|
|
4
6
|
# Copyright (C) 2024 Ribose Inc.
|
|
5
7
|
#
|
|
@@ -31,12 +33,20 @@ module Omnizip
|
|
|
31
33
|
|
|
32
34
|
# Execute the decompress command.
|
|
33
35
|
#
|
|
36
|
+
# Archives (.zip/.7z/.tar/...) extract into the output
|
|
37
|
+
# directory; single-file streams decompress to the output file.
|
|
38
|
+
#
|
|
34
39
|
# @param input_file [String] Path to input file
|
|
35
|
-
# @param output_file [String] Path to output file
|
|
40
|
+
# @param output_file [String] Path to output file or directory
|
|
36
41
|
# @return [void]
|
|
37
42
|
def run(input_file, output_file)
|
|
38
43
|
validate_inputs(input_file, output_file)
|
|
39
44
|
|
|
45
|
+
if archive_input?(input_file) || File.directory?(output_file)
|
|
46
|
+
run_archive_extract(input_file, output_file)
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
algorithm_name = options[:algorithm] || detect_algorithm
|
|
41
51
|
verbose = options[:verbose] || false
|
|
42
52
|
|
|
@@ -70,8 +80,26 @@ module Omnizip
|
|
|
70
80
|
end
|
|
71
81
|
end
|
|
72
82
|
|
|
83
|
+
ARCHIVE_EXTENSIONS = %w[.zip .7z .tar .rar .cpio .iso].freeze
|
|
84
|
+
|
|
73
85
|
private
|
|
74
86
|
|
|
87
|
+
def archive_input?(path)
|
|
88
|
+
ext = File.extname(path).downcase
|
|
89
|
+
ARCHIVE_EXTENSIONS.include?(ext)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run_archive_extract(input_file, output_dir)
|
|
93
|
+
FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
|
|
94
|
+
|
|
95
|
+
entries = Omnizip.list_archive(input_file)
|
|
96
|
+
Omnizip.extract_archive(input_file, output_dir)
|
|
97
|
+
|
|
98
|
+
puts "Extracted #{entries.size} file(s) to: #{output_dir}"
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
raise Omnizip::IOError, "Failed to extract archive: #{e.message}"
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
def validate_inputs(input_file, output_file)
|
|
76
104
|
unless File.exist?(input_file)
|
|
77
105
|
raise Omnizip::IOError, "Input file not found: #{input_file}"
|
|
@@ -18,19 +18,83 @@ module Omnizip
|
|
|
18
18
|
raise Errno::ENOENT, "Archive not found: #{archive_path}"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
if seven_zip?(archive_path)
|
|
22
|
+
show_seven_zip_metadata(archive_path, pattern)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless zip?(archive_path)
|
|
27
|
+
raise ArgumentError,
|
|
28
|
+
"Metadata is supported for .zip and .7z archives: " \
|
|
29
|
+
"#{archive_path}"
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
Omnizip::Zip::File.open(archive_path) do |archive|
|
|
22
|
-
if options[:show]
|
|
33
|
+
if options[:show] || editing_nothing?
|
|
23
34
|
show_metadata(archive, pattern)
|
|
24
35
|
else
|
|
25
36
|
edit_metadata(archive, pattern)
|
|
37
|
+
puts "Metadata updated successfully"
|
|
26
38
|
end
|
|
27
39
|
end
|
|
28
|
-
|
|
29
|
-
puts "Metadata updated successfully" unless options[:show]
|
|
30
40
|
end
|
|
31
41
|
|
|
32
42
|
private
|
|
33
43
|
|
|
44
|
+
def zip?(path)
|
|
45
|
+
path.end_with?(".zip")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def seven_zip?(path)
|
|
49
|
+
path.end_with?(".7z")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# True when no edit option was given — editing would be a no-op
|
|
53
|
+
def editing_nothing?
|
|
54
|
+
%i[comment set_mtime chmod set_attribute].none? { |key| options[key] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Read-only view for 7z archives; the edit path is zip-only
|
|
58
|
+
def show_seven_zip_metadata(archive_path, pattern)
|
|
59
|
+
if options[:comment] || options[:set_mtime] ||
|
|
60
|
+
options[:chmod] || options[:set_attribute]
|
|
61
|
+
raise ArgumentError,
|
|
62
|
+
"Metadata editing is only supported for ZIP archives"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
reader = Omnizip::Formats::SevenZip::Reader.new(archive_path)
|
|
66
|
+
reader.open
|
|
67
|
+
|
|
68
|
+
if pattern
|
|
69
|
+
entries = reader.list_files.select do |e|
|
|
70
|
+
File.fnmatch(pattern, e.name)
|
|
71
|
+
end
|
|
72
|
+
if entries.empty?
|
|
73
|
+
warn "No entries match pattern: #{pattern}"
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
entries.each do |entry|
|
|
78
|
+
puts "Entry: #{entry.name}"
|
|
79
|
+
puts " Type: #{entry.is_dir ? 'Directory' : 'File'}"
|
|
80
|
+
puts " Size: #{format_size(entry.size)}"
|
|
81
|
+
# Per-entry packed sizes are not tracked by the parser
|
|
82
|
+
if entry.compressed_size&.positive?
|
|
83
|
+
puts " Compressed: #{format_size(entry.compressed_size)}"
|
|
84
|
+
end
|
|
85
|
+
puts " Modified: #{entry.mtime}" if entry.mtime
|
|
86
|
+
puts " CRC32: 0x#{entry.crc.to_s(16).upcase}" if entry.crc
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
entries = reader.list_files
|
|
90
|
+
files = entries.reject(&:is_dir)
|
|
91
|
+
puts "Archive: #{archive_path}"
|
|
92
|
+
puts "Entries: #{entries.size} " \
|
|
93
|
+
"(#{files.size} files, #{entries.size - files.size} dirs)"
|
|
94
|
+
puts "Total size: #{format_size(files.sum(&:size))}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
34
98
|
def show_metadata(archive, pattern)
|
|
35
99
|
if pattern
|
|
36
100
|
# Show entry metadata
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
3
5
|
module Omnizip
|
|
4
6
|
module Converter
|
|
5
7
|
# Convert 7-Zip archives to ZIP format
|
|
@@ -8,18 +10,22 @@ module Omnizip
|
|
|
8
10
|
# @return [ConversionResult] Conversion result
|
|
9
11
|
def convert
|
|
10
12
|
start_time = Time.now
|
|
11
|
-
0
|
|
12
13
|
|
|
13
|
-
# Open source 7z archive
|
|
14
14
|
reader = Omnizip::Formats::SevenZip::Reader.new(source_path)
|
|
15
|
-
reader.
|
|
15
|
+
reader.open
|
|
16
|
+
entry_count = reader.list_files.size
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
entry_count = entries_data.size
|
|
18
|
+
Dir.mktmpdir("omnizip_convert") do |tmp|
|
|
19
|
+
reader.extract_all(tmp)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
writer = Omnizip::Formats::Zip::Writer.new(target_path)
|
|
22
|
+
Dir.glob(File.join(tmp, "**", "*")).each do |path|
|
|
23
|
+
next if File.directory?(path)
|
|
24
|
+
|
|
25
|
+
writer.add_file(path, path.delete_prefix("#{tmp}/"))
|
|
26
|
+
end
|
|
27
|
+
writer.write
|
|
28
|
+
end
|
|
23
29
|
|
|
24
30
|
create_result(start_time, entry_count)
|
|
25
31
|
end
|
|
@@ -43,50 +49,6 @@ module Omnizip
|
|
|
43
49
|
def self.can_convert?(source, target)
|
|
44
50
|
source.end_with?(".7z") && target.end_with?(".zip")
|
|
45
51
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def collect_seven_zip_entries(reader)
|
|
50
|
-
entries = []
|
|
51
|
-
|
|
52
|
-
reader.entries.each do |entry|
|
|
53
|
-
data = {
|
|
54
|
-
name: entry.name,
|
|
55
|
-
content: nil,
|
|
56
|
-
mtime: entry.mtime || Time.now,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# Extract entry data
|
|
60
|
-
unless entry.name.end_with?("/")
|
|
61
|
-
File.open(source_path, "rb") do |io|
|
|
62
|
-
data[:content] = reader.extract_entry_data(io, entry)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
entries << data
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
entries
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def create_zip(entries)
|
|
73
|
-
Omnizip::Zip::File.create(target_path) do |zip|
|
|
74
|
-
entries.each do |entry_data|
|
|
75
|
-
if entry_data[:name].end_with?("/")
|
|
76
|
-
# Directory entry
|
|
77
|
-
zip.add(entry_data[:name])
|
|
78
|
-
else
|
|
79
|
-
# File entry
|
|
80
|
-
zip.add(entry_data[:name]) { entry_data[:content] }
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Set archive comment if preserving metadata
|
|
85
|
-
if options.preserve_metadata
|
|
86
|
-
zip.comment = "Converted from 7z by Omnizip"
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
52
|
end
|
|
91
53
|
end
|
|
92
54
|
end
|
|
@@ -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
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "stringio"
|
|
5
|
+
require "zlib"
|
|
5
6
|
|
|
6
7
|
module Omnizip
|
|
7
8
|
module Formats
|
|
@@ -246,11 +247,7 @@ module Omnizip
|
|
|
246
247
|
# Native decompression only for RAR4 for now
|
|
247
248
|
return false unless @header.version == 4
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
return false if entry.nil? || entry.method.nil?
|
|
251
|
-
|
|
252
|
-
# All RAR4 methods are supported by our Dispatcher
|
|
253
|
-
true
|
|
250
|
+
!entry.nil? && !entry.method.nil?
|
|
254
251
|
end
|
|
255
252
|
|
|
256
253
|
# Extract entry using native decompression
|
|
@@ -258,17 +255,24 @@ module Omnizip
|
|
|
258
255
|
# @param entry [Models::RarEntry] File entry
|
|
259
256
|
# @param output_path [String] Destination path
|
|
260
257
|
def extract_entry_native(entry, output_path)
|
|
261
|
-
# Read compressed data from archive
|
|
262
258
|
compressed_data = read_compressed_data(entry)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
259
|
+
method = entry.method || 0x30
|
|
260
|
+
|
|
261
|
+
output = StringIO.new(+"")
|
|
262
|
+
Compression::Dispatcher.decompress(method, compressed_data, output)
|
|
263
|
+
|
|
264
|
+
# The native LZ/PPMd decoders understand Omnizip's own
|
|
265
|
+
# encoder output, not official RAR streams — without this
|
|
266
|
+
# check, real WinRAR archives would silently extract as
|
|
267
|
+
# garbage. The header CRC is authoritative: on mismatch,
|
|
268
|
+
# fall back to unrar.
|
|
269
|
+
data = output.string
|
|
270
|
+
if entry.crc && Zlib.crc32(data) != entry.crc
|
|
271
|
+
raise Compression::DecompressionError,
|
|
272
|
+
"CRC mismatch for #{entry.name}"
|
|
271
273
|
end
|
|
274
|
+
|
|
275
|
+
File.binwrite(output_path, data)
|
|
272
276
|
rescue StandardError => e
|
|
273
277
|
# Fall back to external decompressor on error
|
|
274
278
|
warn "Native decompression failed for #{entry.name}: #{e.message}"
|