omnizip 0.3.39 → 0.3.41
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 +102 -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 +18 -25
- 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/converter/zip_to_seven_zip_strategy.rb +21 -63
- data/lib/omnizip/formats/rar/block_parser.rb +42 -6
- data/lib/omnizip/formats/rar/decompressor.rb +35 -8
- data/lib/omnizip/formats/rar/header.rb +18 -1
- data/lib/omnizip/formats/rar/models/rar_entry.rb +1 -1
- data/lib/omnizip/formats/rar/reader.rb +17 -22
- data/lib/omnizip/formats/rar/writer.rb +1 -1
- data/lib/omnizip/formats/rar.rb +31 -0
- data/lib/omnizip/formats/rar3/reader.rb +66 -314
- data/lib/omnizip/formats/rar5/reader.rb +80 -297
- data/lib/omnizip/formats/rar5/writer.rb +33 -180
- 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 +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 919a322870094f270bd574530af4c463b6446586fdf869026b79ad017834bc1e
|
|
4
|
+
data.tar.gz: 55507eb0b2da4109cb094e711cda2d39df10773c9774c4a01614edc0d8548d9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73b03468e2bb51d227059f316ab4fce911ce19ee6e0c175559de7b1df58a69536d9284d2b5ee2a0a19366ef0c53d448d02e4b95f397042c69c569630aacea86f
|
|
7
|
+
data.tar.gz: dad012852021f331f77a36bf86842274f612d215e32829965bf4bbaf5ad8b9b17539eb7e531a55e73c93d206c73a66760ccde6dfcf234b49e371f17f79451df8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.40] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Every CLI command now works end-to-end (each was exercised against
|
|
14
|
+
real archives and its failure fixed):
|
|
15
|
+
- `omnizip list` printed "No algorithms registered." — all builtin
|
|
16
|
+
algorithms register lazily, so the available list was empty until
|
|
17
|
+
each was first used. `AlgorithmRegistry.available` now includes
|
|
18
|
+
the builtins.
|
|
19
|
+
- `omnizip convert a.7z a.zip` crashed on a fictional reader/writer
|
|
20
|
+
API inside the conversion strategy; rewritten on the real APIs and
|
|
21
|
+
round-trips byte-identically through unzip.
|
|
22
|
+
- `omnizip archive create x.rar ...` raised NoMethodError (a
|
|
23
|
+
module-level `Formats::Rar.create` never existed). It exists now
|
|
24
|
+
(RAR5 default, `version: 4` for RAR4); directory inputs work —
|
|
25
|
+
the stale "RAR5 does not support directories" restriction is gone
|
|
26
|
+
and trees archive under their own name like `.7z`, verified with
|
|
27
|
+
unrar.
|
|
28
|
+
- `omnizip archive metadata` opened every input as a ZIP: `.7z`
|
|
29
|
+
crashed with a ZIP parse error, and runs with no edit option
|
|
30
|
+
still printed "Metadata updated successfully". 7z now shows real
|
|
31
|
+
read-only metadata, unknown formats raise truthful errors, and
|
|
32
|
+
ZIPs default to show mode when nothing is being edited.
|
|
33
|
+
- `omnizip decompress archive.zip out/` — the behavior the
|
|
34
|
+
command's own help documents — failed with "Is a directory".
|
|
35
|
+
Archive inputs (zip/7z/tar/rar/cpio/iso) and directory outputs
|
|
36
|
+
now extract through the convenience layer, content-verified with
|
|
37
|
+
diff for zip, 7z, rar, and tar.
|
|
38
|
+
- Deflate64.metadata returned a raw Hash, crashing the CLI algorithm
|
|
39
|
+
table; it returns `Models::AlgorithmMetadata` like every other
|
|
40
|
+
algorithm.
|
|
41
|
+
- Guides referenced a nonexistent `:zstd` algorithm-registry key;
|
|
42
|
+
corrected to `:zstandard` everywhere (the registry key differs from
|
|
43
|
+
RPM's `:zstd` compression type).
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- `Omnizip::Buffer` 7z support: `create(:seven_zip)` /
|
|
47
|
+
`create_from_hash(hash, :seven_zip)`, `open`, `extract_to_memory`,
|
|
48
|
+
and `MemoryExtractor` all handle 7z through a temporary-file bridge
|
|
49
|
+
(the format has no in-memory stream implementation), including
|
|
50
|
+
explicit directory entries via a new
|
|
51
|
+
`SevenZip::Writer#add_directory_entry`. Format auto-detection fixed
|
|
52
|
+
to compare binary magic literals — UTF-8 literals never matched
|
|
53
|
+
binary data, so 7z buffers raised "Unknown archive format".
|
|
54
|
+
- README gains an In-Memory Archives section (examples run verbatim).
|
|
55
|
+
|
|
56
|
+
## [0.3.39] - 2026-08-28
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- RAR4 writing was structurally fictional: HEAD_CRC used CRC-16/CCITT
|
|
60
|
+
instead of the low 16 bits of CRC32, HEAD_SIZE values excluded the
|
|
61
|
+
2-byte CRC field, a duplicate marker block followed the signature
|
|
62
|
+
(the 7-byte signature IS the marker), and file headers lacked the
|
|
63
|
+
LONG_BLOCK flag. unrar rejected every archive with "Main archive
|
|
64
|
+
header is corrupt". STORE archives now round-trip through unrar
|
|
65
|
+
byte-identically, including explicit directory entries (marked with
|
|
66
|
+
the full 0xE0 dictionary mask — proven empirically: attribute-based
|
|
67
|
+
and trailing-backslash markers do not work), empty files, and
|
|
68
|
+
nested trees. `password:` now raises `NotImplementedError` instead
|
|
69
|
+
of writing the encrypted flag with no salt; `solid:`/`recovery:`/
|
|
70
|
+
`volume_size:` warn and are never advertised in header flags.
|
|
71
|
+
- RAR4 reading never parsed real WinRAR archives (the reader demanded
|
|
72
|
+
the same fictional layout the writer emitted). It now matches
|
|
73
|
+
`unrar l` exactly on libarchive fixtures — names (backslashes
|
|
74
|
+
normalized), sizes, directories, DOS timestamps — and native
|
|
75
|
+
decompression is CRC-verified before use: previously a real
|
|
76
|
+
WinRAR-compressed entry silently extracted garbage (e.g. 44308
|
|
77
|
+
bytes written for a 20111-byte file); now a CRC mismatch falls
|
|
78
|
+
back to unrar for byte-exact extraction while Omnizip-written
|
|
79
|
+
archives still decode natively.
|
|
80
|
+
- RAR3 writer/reader: 6 reserved bytes in the main header (was 4),
|
|
81
|
+
LONG_BLOCK flag, high size words before the filename (was after),
|
|
82
|
+
terminator flags 0. STORE output is `unrar t`-clean. The reader
|
|
83
|
+
dropped a fictional extended-time parser that produced year-2039
|
|
84
|
+
timestamps; base DOS time (minute precision) is used.
|
|
85
|
+
- RAR5 reader: mtime is a 32-bit Unix timestamp (the parser read a
|
|
86
|
+
vint plus FILETIME that do not exist in the format) and is nil
|
|
87
|
+
when the header carries none, instead of a fabricated `Time.now`.
|
|
88
|
+
Timestamps now match unrar exactly on fixtures.
|
|
89
|
+
- `Formats::Rar3`/`Formats::Rar5` never loaded: the autoload
|
|
90
|
+
registry in `formats/rar.rb` pointed at files defining a different
|
|
91
|
+
namespace. Both get proper parent autoload files
|
|
92
|
+
(`formats/rar3.rb`, `formats/rar5.rb`).
|
|
93
|
+
- Windows: the unrar program path was pre-wrapped in literal quotes
|
|
94
|
+
for a shell-string call form; shell-free array-form invocations
|
|
95
|
+
failed to start the program. Quotes now live only at the two
|
|
96
|
+
shell call sites, and unrar runs quietly (`-idq`).
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
- unrar-gated RAR4 interop spec (STORE test + byte-identical
|
|
100
|
+
extraction with directories) and real WinRAR fixture parsing specs
|
|
101
|
+
(listing + unrar-fallback extraction with CRC assertions).
|
|
102
|
+
|
|
103
|
+
### Documentation
|
|
104
|
+
- README RAR4/RAR5 sections rewritten to verified reality: STORE is
|
|
105
|
+
unrar-verified while `:fastest`-`:best` are Omnizip-round-trip-only
|
|
106
|
+
(previously all methods claimed "fully working"); the real writer
|
|
107
|
+
API (no `close`, no block form, `compression:` key); RAR5 "LZMA SDK
|
|
108
|
+
byte-for-byte" claims removed (the option falls back to STORE with
|
|
109
|
+
a warning). Fixed fictional `SevenZip::Writer` `close`/`algorithm=`
|
|
110
|
+
examples in the API-usage and architecture guides.
|
|
111
|
+
|
|
10
112
|
## [0.3.38] - 2026-08-28
|
|
11
113
|
|
|
12
114
|
### 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,
|
|
@@ -120,42 +120,35 @@ module Omnizip
|
|
|
120
120
|
|
|
121
121
|
start_time = Time.now
|
|
122
122
|
|
|
123
|
+
# Format-agnostic options; Formats::Rar applies the
|
|
124
|
+
# version-specific semantics
|
|
123
125
|
writer_opts = {
|
|
124
126
|
version: version,
|
|
125
127
|
compression: compression,
|
|
126
128
|
level: level,
|
|
127
129
|
include_mtime: include_mtime,
|
|
128
130
|
include_crc32: include_crc32,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
writer_opts[:volume_naming] = volume_naming
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Add encryption options for RAR5
|
|
142
|
-
if version == 5 && password
|
|
143
|
-
writer_opts[:password] = password
|
|
144
|
-
writer_opts[:kdf_iterations] = kdf_iterations
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Add recovery options for RAR5
|
|
148
|
-
if version == 5 && recovery
|
|
149
|
-
writer_opts[:recovery] = true
|
|
150
|
-
writer_opts[:recovery_percent] = recovery_percent
|
|
151
|
-
end
|
|
131
|
+
solid: solid,
|
|
132
|
+
multi_volume: multi_volume,
|
|
133
|
+
volume_size: volume_size,
|
|
134
|
+
volume_naming: volume_naming,
|
|
135
|
+
password: password,
|
|
136
|
+
kdf_iterations: kdf_iterations,
|
|
137
|
+
recovery: recovery,
|
|
138
|
+
recovery_percent: recovery_percent,
|
|
139
|
+
}.compact
|
|
152
140
|
|
|
153
141
|
result_files = Omnizip::Formats::Rar.create(output_file,
|
|
154
142
|
writer_opts) do |rar|
|
|
155
143
|
inputs.each do |input|
|
|
156
144
|
if File.directory?(input)
|
|
157
|
-
|
|
158
|
-
|
|
145
|
+
CliOutputFormatter.verbose_puts(
|
|
146
|
+
"Adding directory: #{input}",
|
|
147
|
+
verbose,
|
|
148
|
+
)
|
|
149
|
+
# Strip the parent so the tree lands under its own
|
|
150
|
+
# name, matching the .7z creation behavior
|
|
151
|
+
rar.add_directory(input, File.dirname(input))
|
|
159
152
|
else
|
|
160
153
|
CliOutputFormatter.verbose_puts(
|
|
161
154
|
"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
|