omnizip 0.3.39 → 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 +56 -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 +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.rb +18 -0
- 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 +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5013ae71553867968422f57b01425820f17c5cd6e471ea0cb5b5d46d1cc6a1bd
|
|
4
|
+
data.tar.gz: 94698e90386f195cdd7f5fd7a8ebae3010f2f206e70eb96463ce8e4d2ece09b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 415d4cdcb48375d0ba0d1d57dc2592c18d47fcafb9817d956dbe5a3ec7e1fa1048063372759de1ff0c0ef194d06eaa7f5855cfead144681e1fb454ac4fc510b2
|
|
7
|
+
data.tar.gz: a901b525b49f60b94098d43a5c4517f5163e07b64ef08099c02f693fc50775461c584ee35f2a6227167bbd461cfcbd9e3985b2383c586610817363170f45a2ef
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.39] - 2026-08-28
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- RAR4 writing was structurally fictional: HEAD_CRC used CRC-16/CCITT
|
|
14
|
+
instead of the low 16 bits of CRC32, HEAD_SIZE values excluded the
|
|
15
|
+
2-byte CRC field, a duplicate marker block followed the signature
|
|
16
|
+
(the 7-byte signature IS the marker), and file headers lacked the
|
|
17
|
+
LONG_BLOCK flag. unrar rejected every archive with "Main archive
|
|
18
|
+
header is corrupt". STORE archives now round-trip through unrar
|
|
19
|
+
byte-identically, including explicit directory entries (marked with
|
|
20
|
+
the full 0xE0 dictionary mask — proven empirically: attribute-based
|
|
21
|
+
and trailing-backslash markers do not work), empty files, and
|
|
22
|
+
nested trees. `password:` now raises `NotImplementedError` instead
|
|
23
|
+
of writing the encrypted flag with no salt; `solid:`/`recovery:`/
|
|
24
|
+
`volume_size:` warn and are never advertised in header flags.
|
|
25
|
+
- RAR4 reading never parsed real WinRAR archives (the reader demanded
|
|
26
|
+
the same fictional layout the writer emitted). It now matches
|
|
27
|
+
`unrar l` exactly on libarchive fixtures — names (backslashes
|
|
28
|
+
normalized), sizes, directories, DOS timestamps — and native
|
|
29
|
+
decompression is CRC-verified before use: previously a real
|
|
30
|
+
WinRAR-compressed entry silently extracted garbage (e.g. 44308
|
|
31
|
+
bytes written for a 20111-byte file); now a CRC mismatch falls
|
|
32
|
+
back to unrar for byte-exact extraction while Omnizip-written
|
|
33
|
+
archives still decode natively.
|
|
34
|
+
- RAR3 writer/reader: 6 reserved bytes in the main header (was 4),
|
|
35
|
+
LONG_BLOCK flag, high size words before the filename (was after),
|
|
36
|
+
terminator flags 0. STORE output is `unrar t`-clean. The reader
|
|
37
|
+
dropped a fictional extended-time parser that produced year-2039
|
|
38
|
+
timestamps; base DOS time (minute precision) is used.
|
|
39
|
+
- RAR5 reader: mtime is a 32-bit Unix timestamp (the parser read a
|
|
40
|
+
vint plus FILETIME that do not exist in the format) and is nil
|
|
41
|
+
when the header carries none, instead of a fabricated `Time.now`.
|
|
42
|
+
Timestamps now match unrar exactly on fixtures.
|
|
43
|
+
- `Formats::Rar3`/`Formats::Rar5` never loaded: the autoload
|
|
44
|
+
registry in `formats/rar.rb` pointed at files defining a different
|
|
45
|
+
namespace. Both get proper parent autoload files
|
|
46
|
+
(`formats/rar3.rb`, `formats/rar5.rb`).
|
|
47
|
+
- Windows: the unrar program path was pre-wrapped in literal quotes
|
|
48
|
+
for a shell-string call form; shell-free array-form invocations
|
|
49
|
+
failed to start the program. Quotes now live only at the two
|
|
50
|
+
shell call sites, and unrar runs quietly (`-idq`).
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- unrar-gated RAR4 interop spec (STORE test + byte-identical
|
|
54
|
+
extraction with directories) and real WinRAR fixture parsing specs
|
|
55
|
+
(listing + unrar-fallback extraction with CRC assertions).
|
|
56
|
+
|
|
57
|
+
### Documentation
|
|
58
|
+
- README RAR4/RAR5 sections rewritten to verified reality: STORE is
|
|
59
|
+
unrar-verified while `:fastest`-`:best` are Omnizip-round-trip-only
|
|
60
|
+
(previously all methods claimed "fully working"); the real writer
|
|
61
|
+
API (no `close`, no block form, `compression:` key); RAR5 "LZMA SDK
|
|
62
|
+
byte-for-byte" claims removed (the option falls back to STORE with
|
|
63
|
+
a warning). Fixed fictional `SevenZip::Writer` `close`/`algorithm=`
|
|
64
|
+
examples in the API-usage and architecture guides.
|
|
65
|
+
|
|
10
66
|
## [0.3.38] - 2026-08-28
|
|
11
67
|
|
|
12
68
|
### Added
|
data/README.adoc
CHANGED
|
@@ -633,6 +633,37 @@ Omnizip.extract_archive('archive.zip', 'output/')
|
|
|
633
633
|
Omnizip.list_archive('archive.zip')
|
|
634
634
|
----
|
|
635
635
|
|
|
636
|
+
==== In-Memory Archives
|
|
637
|
+
|
|
638
|
+
`Omnizip::Buffer` creates and reads ZIP and 7z archives entirely in
|
|
639
|
+
memory (7z bridges through a temporary file internally):
|
|
640
|
+
|
|
641
|
+
[source,ruby]
|
|
642
|
+
----
|
|
643
|
+
require 'omnizip'
|
|
644
|
+
|
|
645
|
+
# Create in memory
|
|
646
|
+
buffer = Omnizip::Buffer.create(:zip) do |archive|
|
|
647
|
+
archive.add('readme.txt', 'Hello World')
|
|
648
|
+
archive.add('docs/', '') # directory entry
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Same API for 7z
|
|
652
|
+
buffer7z = Omnizip::Buffer.create(:seven_zip) do |archive|
|
|
653
|
+
archive.add('data.json', '{"key": "value"}')
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Read back (format auto-detected)
|
|
657
|
+
files = Omnizip::Buffer.extract_to_memory(buffer.string)
|
|
658
|
+
# => {"readme.txt" => "Hello World"}
|
|
659
|
+
|
|
660
|
+
Omnizip::Buffer.open(buffer.string) do |archive|
|
|
661
|
+
archive.each_entry do |entry|
|
|
662
|
+
puts "#{entry.name}: #{entry.size} bytes" unless entry.directory?
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
----
|
|
666
|
+
|
|
636
667
|
==== Rubyzip Compatibility Mode
|
|
637
668
|
|
|
638
669
|
[source,ruby]
|
|
@@ -38,6 +38,13 @@ module Omnizip
|
|
|
38
38
|
"Algorithm"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# All builtin algorithms register lazily, so the plain storage
|
|
42
|
+
# keys are empty until each algorithm is first used. Listing
|
|
43
|
+
# (e.g. the CLI's `omnizip list`) must show the builtins too.
|
|
44
|
+
def available
|
|
45
|
+
(super + BUILTIN_ALGORITHMS.keys).uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
41
48
|
def register(name, klass)
|
|
42
49
|
raise ArgumentError, "Algorithm name cannot be nil" if name.nil?
|
|
43
50
|
raise ArgumentError, "Algorithm class cannot be nil" if klass.nil?
|
|
@@ -28,14 +28,11 @@ module Omnizip
|
|
|
28
28
|
|
|
29
29
|
# Algorithm metadata
|
|
30
30
|
def self.metadata
|
|
31
|
-
|
|
32
|
-
name
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
compression_method: 9,
|
|
37
|
-
description: "Enhanced Deflate with 64KB window",
|
|
38
|
-
}
|
|
31
|
+
Models::AlgorithmMetadata.new.tap do |meta|
|
|
32
|
+
meta.name = "deflate64"
|
|
33
|
+
meta.description = "Enhanced Deflate with 64KB window"
|
|
34
|
+
meta.version = "1.0.0"
|
|
35
|
+
end
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
# Compress input stream to output stream
|
|
@@ -49,7 +49,7 @@ module Omnizip
|
|
|
49
49
|
when :zip
|
|
50
50
|
extract_all_zip(result)
|
|
51
51
|
when :seven_zip, :"7z"
|
|
52
|
-
|
|
52
|
+
extract_all_seven_zip(result)
|
|
53
53
|
else
|
|
54
54
|
raise ArgumentError, "Unsupported format: #{@format}"
|
|
55
55
|
end
|
|
@@ -76,7 +76,7 @@ module Omnizip
|
|
|
76
76
|
when :zip
|
|
77
77
|
content = extract_entry_zip(name)
|
|
78
78
|
when :seven_zip, :"7z"
|
|
79
|
-
|
|
79
|
+
content = extract_entry_seven_zip(name)
|
|
80
80
|
else
|
|
81
81
|
raise ArgumentError, "Unsupported format: #{@format}"
|
|
82
82
|
end
|
|
@@ -100,7 +100,7 @@ module Omnizip
|
|
|
100
100
|
when :zip
|
|
101
101
|
list_entries_zip(names)
|
|
102
102
|
when :seven_zip, :"7z"
|
|
103
|
-
|
|
103
|
+
SevenZipBridge.open(@buffer) { |archive| names.concat(archive.entry_names) }
|
|
104
104
|
else
|
|
105
105
|
raise ArgumentError, "Unsupported format: #{@format}"
|
|
106
106
|
end
|
|
@@ -156,14 +156,14 @@ module Omnizip
|
|
|
156
156
|
# @return [Symbol] Detected format
|
|
157
157
|
# @raise [Omnizip::FormatError] If format cannot be detected
|
|
158
158
|
def detect_format
|
|
159
|
-
magic = @buffer.read(4)
|
|
159
|
+
magic = @buffer.read(4).to_s.b
|
|
160
160
|
@buffer.rewind
|
|
161
161
|
|
|
162
162
|
case magic
|
|
163
|
-
when "PK\x03\x04", "PK\x05\x06", "PK\x07\x08"
|
|
163
|
+
when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
|
|
164
164
|
# ZIP signatures: local file header, EOCD, data descriptor
|
|
165
165
|
:zip
|
|
166
|
-
when "7z\xBC\xAF"
|
|
166
|
+
when "7z\xBC\xAF".b
|
|
167
167
|
:seven_zip
|
|
168
168
|
else
|
|
169
169
|
raise Omnizip::FormatError,
|
|
@@ -187,6 +187,31 @@ module Omnizip
|
|
|
187
187
|
end
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
+
# Extract every file entry from 7z
|
|
191
|
+
#
|
|
192
|
+
# @param result [Hash] Hash to populate with entries
|
|
193
|
+
def extract_all_seven_zip(result)
|
|
194
|
+
@buffer.rewind
|
|
195
|
+
SevenZipBridge.open(@buffer) do |archive|
|
|
196
|
+
archive.extract_all_to_memory.each do |name, content|
|
|
197
|
+
result[name] = content
|
|
198
|
+
@extracted_cache[name] = content
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Extract single entry from 7z
|
|
204
|
+
#
|
|
205
|
+
# @param name [String] Entry name
|
|
206
|
+
# @return [String, nil] Entry content or nil if not found
|
|
207
|
+
def extract_entry_seven_zip(name)
|
|
208
|
+
@buffer.rewind
|
|
209
|
+
SevenZipBridge.open(@buffer) do |archive|
|
|
210
|
+
entry = archive.raw_entries.find { |e| e.name == name }
|
|
211
|
+
archive.extract_all_to_memory[name] if entry
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
190
215
|
# Extract single entry from ZIP
|
|
191
216
|
#
|
|
192
217
|
# @param name [String] Entry name
|
|
@@ -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
|
data/lib/omnizip/formats/rar.rb
CHANGED
|
@@ -125,6 +125,24 @@ module Omnizip
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
class << self
|
|
128
|
+
# Create a RAR archive, yielding the writer for entries.
|
|
129
|
+
# RAR5 is the default; pass version: 4 for the RAR4 writer.
|
|
130
|
+
#
|
|
131
|
+
# @param path [String] Output archive path
|
|
132
|
+
# @param options [Hash] Writer options
|
|
133
|
+
# @return [String, Array<String>] Path(s) of the created
|
|
134
|
+
# archive (a volume set returns every volume)
|
|
135
|
+
def create(path, options = {})
|
|
136
|
+
version = options.fetch(:version, 5)
|
|
137
|
+
writer = if version == 4
|
|
138
|
+
Writer.new(path, options)
|
|
139
|
+
else
|
|
140
|
+
Rar5::Writer.new(path, options)
|
|
141
|
+
end
|
|
142
|
+
yield writer if block_given?
|
|
143
|
+
writer.write
|
|
144
|
+
end
|
|
145
|
+
|
|
128
146
|
# Check if RAR extraction is available
|
|
129
147
|
#
|
|
130
148
|
# @return [Boolean] true if unrar available
|
|
@@ -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
|
data/lib/omnizip/version.rb
CHANGED
data/readme-docs/api-usage.adoc
CHANGED
|
@@ -340,7 +340,7 @@ Zstandard offers fast compression with good ratios, using the native library.
|
|
|
340
340
|
|
|
341
341
|
[source,ruby]
|
|
342
342
|
----
|
|
343
|
-
algorithm = Omnizip::AlgorithmRegistry.get(:
|
|
343
|
+
algorithm = Omnizip::AlgorithmRegistry.get(:zstandard).new(level: 3)
|
|
344
344
|
algorithm.compress(input, output)
|
|
345
345
|
----
|
|
346
346
|
|
|
@@ -246,7 +246,7 @@ end
|
|
|
246
246
|
|
|
247
247
|
# List all available
|
|
248
248
|
algorithms = Omnizip::AlgorithmRegistry.available
|
|
249
|
-
# => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :
|
|
249
|
+
# => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :zstandard]
|
|
250
250
|
----
|
|
251
251
|
|
|
252
252
|
=== Adding New Algorithms
|
|
@@ -63,7 +63,7 @@ require 'omnizip'
|
|
|
63
63
|
|
|
64
64
|
# Check available algorithms
|
|
65
65
|
puts Omnizip::AlgorithmRegistry.available.inspect
|
|
66
|
-
# => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :
|
|
66
|
+
# => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :zstandard]
|
|
67
67
|
|
|
68
68
|
# Get version
|
|
69
69
|
puts Omnizip::VERSION
|
|
@@ -302,7 +302,7 @@ def compare_algorithms(data, algorithms)
|
|
|
302
302
|
end
|
|
303
303
|
|
|
304
304
|
data = File.read('test_file.dat')
|
|
305
|
-
compare_algorithms(data, [:deflate, :lzma, :bzip2, :
|
|
305
|
+
compare_algorithms(data, [:deflate, :lzma, :bzip2, :zstandard])
|
|
306
306
|
----
|
|
307
307
|
|
|
308
308
|
=== Example 3: Memory Leak Detection
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omnizip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.40
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -333,6 +333,7 @@ files:
|
|
|
333
333
|
- lib/omnizip/buffer.rb
|
|
334
334
|
- lib/omnizip/buffer/memory_archive.rb
|
|
335
335
|
- lib/omnizip/buffer/memory_extractor.rb
|
|
336
|
+
- lib/omnizip/buffer/seven_zip_bridge.rb
|
|
336
337
|
- lib/omnizip/checksum_registry.rb
|
|
337
338
|
- lib/omnizip/checksums.rb
|
|
338
339
|
- lib/omnizip/checksums/crc32.rb
|