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
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,64 @@ 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
|
+
|
|
66
|
+
## [0.3.38] - 2026-08-28
|
|
67
|
+
|
|
10
68
|
### Added
|
|
11
69
|
- `.cpio` and `.iso` read routing: `extract_archive`/`list_archive`/
|
|
12
70
|
`read_from_archive`/`Archive.open` operate on CPIO archives and ISO
|
data/README.adoc
CHANGED
|
@@ -115,13 +115,14 @@ Omnizip provides complete XZ container format (`.xz`) support with LZMA2 compres
|
|
|
115
115
|
----
|
|
116
116
|
require 'omnizip'
|
|
117
117
|
|
|
118
|
-
# Compress to XZ format
|
|
119
|
-
|
|
120
|
-
File.write('output.xz', compressed)
|
|
118
|
+
# Compress a file to XZ format
|
|
119
|
+
Omnizip::Formats::Xz.create_file('input.txt', level: 6)
|
|
121
120
|
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
# Compress in-memory data (returns the .xz bytes)
|
|
122
|
+
compressed = Omnizip::Formats::Xz.create("Hello, World!")
|
|
123
|
+
|
|
124
|
+
# Decompress (accepts a path or the .xz bytes)
|
|
125
|
+
decompressed = Omnizip::Formats::Xz.decompress(compressed)
|
|
125
126
|
|
|
126
127
|
# Or use the Reader API
|
|
127
128
|
reader = Omnizip::Formats::Xz::Reader.new('file.xz')
|
|
@@ -133,7 +134,7 @@ data = reader.read
|
|
|
133
134
|
[source,ruby]
|
|
134
135
|
----
|
|
135
136
|
# Configure checksum type
|
|
136
|
-
compressed = Omnizip::Formats::Xz.
|
|
137
|
+
compressed = Omnizip::Formats::Xz.create(data, check_type: :crc64)
|
|
137
138
|
# Options: :crc32 (default), :crc64, :sha256, :none
|
|
138
139
|
|
|
139
140
|
# Using Builder API for multi-part data
|
|
@@ -216,19 +217,19 @@ Omnizip provides complete 7-Zip container format (`.7z`) support with multiple c
|
|
|
216
217
|
require 'omnizip'
|
|
217
218
|
|
|
218
219
|
# Create 7z archive
|
|
219
|
-
Omnizip::Formats::SevenZip
|
|
220
|
+
Omnizip::Formats::SevenZip.create('archive.7z') do |sz|
|
|
220
221
|
sz.add_file('document.pdf')
|
|
221
222
|
sz.add_directory('photos/')
|
|
222
223
|
end
|
|
223
224
|
|
|
224
225
|
# Extract 7z archive
|
|
225
|
-
Omnizip::Formats::SevenZip
|
|
226
|
+
Omnizip::Formats::SevenZip.open('archive.7z') do |sz|
|
|
226
227
|
sz.extract_all('output/')
|
|
227
228
|
end
|
|
228
229
|
|
|
229
230
|
# List contents
|
|
230
|
-
Omnizip::Formats::SevenZip
|
|
231
|
-
sz.
|
|
231
|
+
Omnizip::Formats::SevenZip.open('archive.7z') do |sz|
|
|
232
|
+
sz.list_files.each do |entry|
|
|
232
233
|
puts "#{entry.name}: #{entry.size} bytes"
|
|
233
234
|
end
|
|
234
235
|
end
|
|
@@ -239,7 +240,7 @@ end
|
|
|
239
240
|
[source,ruby]
|
|
240
241
|
----
|
|
241
242
|
# With compression options
|
|
242
|
-
Omnizip::Formats::SevenZip
|
|
243
|
+
Omnizip::Formats::SevenZip.create('archive.7z',
|
|
243
244
|
algorithm: :lzma2,
|
|
244
245
|
level: 9,
|
|
245
246
|
solid: true
|
|
@@ -248,9 +249,9 @@ Omnizip::Formats::SevenZip::Writer.create('archive.7z',
|
|
|
248
249
|
end
|
|
249
250
|
|
|
250
251
|
# With password encryption
|
|
251
|
-
Omnizip::Formats::SevenZip
|
|
252
|
+
Omnizip::Formats::SevenZip.create('secure.7z',
|
|
252
253
|
password: 'secret123',
|
|
253
|
-
|
|
254
|
+
encrypt_headers: true
|
|
254
255
|
) do |sz|
|
|
255
256
|
sz.add_file('confidential.doc')
|
|
256
257
|
end
|
|
@@ -417,39 +418,23 @@ puts "#{info[:name]}-#{info[:version]}-#{info[:release]}"
|
|
|
417
418
|
----
|
|
418
419
|
require 'omnizip'
|
|
419
420
|
|
|
420
|
-
# Create RPM package with gzip
|
|
421
|
-
Omnizip::Formats::Rpm.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
rpm.release = '1'
|
|
425
|
-
rpm.arch = 'noarch'
|
|
426
|
-
rpm.summary = 'My awesome package'
|
|
427
|
-
rpm.description = 'A longer description of the package'
|
|
428
|
-
rpm.license = 'MIT'
|
|
429
|
-
rpm.vendor = 'My Company'
|
|
430
|
-
rpm.url = 'https://example.com/mypackage'
|
|
431
|
-
|
|
432
|
-
# Add files from filesystem
|
|
433
|
-
rpm.add_file('/usr/bin/myapp', 'path/to/myapp')
|
|
434
|
-
rpm.add_file('/etc/myapp.conf', 'path/to/config')
|
|
435
|
-
rpm.add_directory('/var/lib/myapp')
|
|
436
|
-
|
|
437
|
-
# Add dependencies
|
|
438
|
-
rpm.add_dependency('glibc', '>= 2.17')
|
|
439
|
-
rpm.add_provides('myapp')
|
|
440
|
-
end
|
|
421
|
+
# Create RPM package with gzip-compressed payload (default)
|
|
422
|
+
rpm = Omnizip::Formats::Rpm::Writer.new(
|
|
423
|
+
name: 'mypackage', version: '1.0', release: '1', arch: 'noarch'
|
|
424
|
+
)
|
|
441
425
|
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
rpm.arch = 'x86_64'
|
|
449
|
-
# ... add files
|
|
450
|
-
end
|
|
426
|
+
# Add payload entries (content is passed as a string)
|
|
427
|
+
rpm.add_file('/usr/bin/myapp', File.binread('path/to/myapp'), mode: 0o755)
|
|
428
|
+
rpm.add_file('/etc/myapp.conf', File.binread('path/to/config'))
|
|
429
|
+
rpm.add_directory('/var/lib/myapp')
|
|
430
|
+
|
|
431
|
+
rpm.write('mypackage-1.0-1.noarch.rpm')
|
|
451
432
|
|
|
452
433
|
# Supported compression types: :gzip (default), :bzip2, :xz, :zstd, :none
|
|
434
|
+
Omnizip::Formats::Rpm::Writer.new(
|
|
435
|
+
name: 'mypackage', version: '1.0', release: '1',
|
|
436
|
+
arch: 'x86_64', compression: :xz
|
|
437
|
+
)
|
|
453
438
|
----
|
|
454
439
|
|
|
455
440
|
**Architecture**:
|
|
@@ -544,8 +529,8 @@ See link:readme-docs/preprocessing-filters.adoc[Preprocessing Filters Guide] for
|
|
|
544
529
|
|
|
545
530
|
* **.7z** - Full read/write with solid compression, multi-volume support
|
|
546
531
|
* **ZIP** - Full read/write with ZIP64, WinZip AES encryption
|
|
547
|
-
* **RAR4** -
|
|
548
|
-
* **RAR5** -
|
|
532
|
+
* **RAR4** - Read support for real WinRAR archives (STORE natively; compressed methods via unrar), write with STORE verified against unrar, FASTEST through BEST for Omnizip round-trip (v0.3.0)
|
|
533
|
+
* **RAR5** - Read/write with STORE (unrar-verified), AES-256 encryption, multi-volume; compressed-method writing falls back to STORE (v0.3.0)
|
|
549
534
|
* **TAR** - Full read/write with POSIX extensions
|
|
550
535
|
* **ISO 9660** - Full read/write with Rock Ridge/Joliet
|
|
551
536
|
* **CPIO** - Full read/write (newc, CRC formats) with RPM payload support (v0.4.0)
|
|
@@ -648,6 +633,37 @@ Omnizip.extract_archive('archive.zip', 'output/')
|
|
|
648
633
|
Omnizip.list_archive('archive.zip')
|
|
649
634
|
----
|
|
650
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
|
+
|
|
651
667
|
==== Rubyzip Compatibility Mode
|
|
652
668
|
|
|
653
669
|
[source,ruby]
|
|
@@ -679,16 +695,19 @@ File.open('input.txt', 'rb') do |input|
|
|
|
679
695
|
end
|
|
680
696
|
|
|
681
697
|
# .7z archive operations
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
698
|
+
Omnizip::Formats::SevenZip.create('archive.7z') do |sz|
|
|
699
|
+
sz.add_file('document.pdf')
|
|
700
|
+
end
|
|
685
701
|
----
|
|
686
702
|
|
|
687
703
|
==== RAR Archives
|
|
688
704
|
|
|
689
705
|
==== RAR4 Archives (v0.3.0)
|
|
690
706
|
|
|
691
|
-
|
|
707
|
+
RAR4 reading and writing against the on-disk format unrar accepts:
|
|
708
|
+
headers carry the low 16 bits of a CRC32, HEAD_SIZE counts the whole
|
|
709
|
+
block, and the dictionary mask 0xE0 marks directory entries. STORE
|
|
710
|
+
archives round-trip through official unrar byte-identically.
|
|
692
711
|
|
|
693
712
|
====== Reading RAR4 Archives
|
|
694
713
|
|
|
@@ -696,31 +715,29 @@ Omnizip v0.3.0 provides complete RAR4 archive support with full read capabilitie
|
|
|
696
715
|
----
|
|
697
716
|
require 'omnizip'
|
|
698
717
|
|
|
699
|
-
|
|
700
|
-
reader
|
|
701
|
-
File.open('archive.rar', 'rb') do |io|
|
|
702
|
-
entries = reader.read_archive(io)
|
|
718
|
+
reader = Omnizip::Formats::Rar::Reader.new('archive.rar')
|
|
719
|
+
reader.open
|
|
703
720
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
puts " Modified: #{entry.modified_time}"
|
|
709
|
-
puts " Directory: #{entry.is_directory}"
|
|
710
|
-
end
|
|
721
|
+
reader.list_files.each do |entry|
|
|
722
|
+
puts "#{entry.name}: #{entry.size} bytes (#{entry.compressed_size} compressed)"
|
|
723
|
+
puts " Directory: #{entry.is_dir}"
|
|
724
|
+
puts " Modified: #{entry.mtime}"
|
|
711
725
|
end
|
|
726
|
+
|
|
727
|
+
# STORE data decodes natively; WinRAR-compressed entries fall back to
|
|
728
|
+
# the unrar command after a CRC check rejects foreign streams
|
|
729
|
+
reader.extract_entry('test.txt', 'output/test.txt')
|
|
730
|
+
reader.extract_all('output/')
|
|
712
731
|
----
|
|
713
732
|
|
|
714
733
|
**RAR4 Reader Features:**
|
|
715
734
|
|
|
716
|
-
* ✅
|
|
717
|
-
* ✅
|
|
718
|
-
* ✅
|
|
719
|
-
* ✅
|
|
720
|
-
* ✅ Symlink detection and handling
|
|
735
|
+
* ✅ Block header parsing (MAIN/FILE/ENDARC), directories, empty files
|
|
736
|
+
* ✅ Native STORE decompression with stored-CRC32 verification
|
|
737
|
+
* ✅ unrar fallback for real WinRAR-compressed entries (byte-exact)
|
|
738
|
+
* ✅ DOS-style backslash name normalization to forward slashes
|
|
721
739
|
* ✅ Multi-volume archive detection
|
|
722
|
-
* ✅
|
|
723
|
-
* ✅ libarchive compatibility (52 test files verified)
|
|
740
|
+
* ✅ Parsing verified against libarchive WinRAR fixtures
|
|
724
741
|
|
|
725
742
|
====== Writing RAR4 Archives
|
|
726
743
|
|
|
@@ -728,141 +745,123 @@ end
|
|
|
728
745
|
----
|
|
729
746
|
require 'omnizip'
|
|
730
747
|
|
|
731
|
-
#
|
|
732
|
-
writer = Omnizip::Formats::Rar::Writer.new('archive.rar'
|
|
748
|
+
# STORE output is verified interoperable with unrar
|
|
749
|
+
writer = Omnizip::Formats::Rar::Writer.new('archive.rar',
|
|
750
|
+
compression: :store
|
|
751
|
+
)
|
|
733
752
|
writer.add_file('document.txt')
|
|
734
753
|
writer.add_file('image.png')
|
|
735
754
|
writer.add_directory('photos/')
|
|
736
755
|
writer.write
|
|
737
|
-
writer.close
|
|
738
|
-
|
|
739
|
-
# Or use block syntax
|
|
740
|
-
Omnizip::Formats::Rar::Writer.new('archive.rar') do |rar|
|
|
741
|
-
rar.add_file('document.txt')
|
|
742
|
-
rar.add_directory('photos/')
|
|
743
|
-
end
|
|
744
|
-
|
|
745
|
-
# Select compression method
|
|
746
|
-
writer = Omnizip::Formats::Rar::Writer.new('archive.rar',
|
|
747
|
-
compression_method: :normal # or :store, :fastest
|
|
748
|
-
)
|
|
749
|
-
writer.add_file('large_file.bin')
|
|
750
|
-
writer.write
|
|
751
756
|
----
|
|
752
757
|
|
|
758
|
+
Writer options: `compression:` (see below), `test_after_create: true`
|
|
759
|
+
(reads the archive back and verifies every file). RAR4 encryption is
|
|
760
|
+
not implemented — `password:` raises `NotImplementedError`. The
|
|
761
|
+
`solid:`/`recovery:`/`volume_size:` options are accepted but not
|
|
762
|
+
implemented; they are ignored with a warning and never advertised in
|
|
763
|
+
header flags.
|
|
764
|
+
|
|
753
765
|
====== RAR4 Compression Methods
|
|
754
766
|
|
|
755
767
|
[cols="2,2,2,4",options="header"]
|
|
756
768
|
|===
|
|
757
|
-
|Method |Speed |
|
|
758
|
-
|`:store` |Instant |
|
|
759
|
-
|`:fastest` |Very Fast
|
|
760
|
-
|`:normal` |Fast
|
|
761
|
-
|`:
|
|
769
|
+
|Method |Speed |unrar interop |Notes
|
|
770
|
+
|`:store` |Instant |✅ verified |Byte-identical round-trip incl. directories
|
|
771
|
+
|`:fastest` |Very Fast |❌ Omnizip-only |LZ77+Huffman, readable by Omnizip
|
|
772
|
+
|`:normal` (default) |Fast |❌ Omnizip-only |LZ77+Huffman, readable by Omnizip
|
|
773
|
+
|`:good` |Medium |❌ Omnizip-only |LZ77+Huffman, readable by Omnizip
|
|
774
|
+
|`:best` |Slow |❌ Omnizip-only |PPMd, readable by Omnizip
|
|
762
775
|
|===
|
|
763
776
|
|
|
777
|
+
Official RAR LZ/PPMd streams are proprietary; methods above `:store`
|
|
778
|
+
produce archives Omnizip can read back but unrar reports as checksum
|
|
779
|
+
errors. Use `:store` for interoperability.
|
|
780
|
+
|
|
764
781
|
===== RAR5 Archives (v0.3.0)
|
|
765
782
|
|
|
766
|
-
|
|
783
|
+
Read/write support for RAR5 archives with STORE compression,
|
|
784
|
+
AES-256 encryption, and multi-volume archives — all verified against
|
|
785
|
+
official unrar.
|
|
767
786
|
|
|
768
787
|
====== Reading RAR5 Archives
|
|
769
788
|
|
|
770
789
|
[source,ruby]
|
|
771
790
|
----
|
|
772
|
-
require 'omnizip
|
|
791
|
+
require 'omnizip'
|
|
773
792
|
|
|
774
|
-
# Read RAR5 archive
|
|
775
793
|
reader = Omnizip::Formats::Rar5::Reader.new
|
|
776
794
|
File.open('archive.rar', 'rb') do |io|
|
|
777
795
|
entries = reader.read_archive(io)
|
|
778
796
|
|
|
779
|
-
# List files with metadata
|
|
780
797
|
entries.each do |entry|
|
|
781
798
|
puts "#{entry.name}: #{entry.uncompressed_size} bytes"
|
|
782
|
-
puts "
|
|
783
|
-
puts "
|
|
784
|
-
puts " Modified: #{entry.modified_time}"
|
|
799
|
+
puts " CRC32: #{entry.crc32.to_s(16)}" if entry.crc32
|
|
800
|
+
puts " Modified: #{entry.modified_time}" if entry.modified_time
|
|
785
801
|
end
|
|
786
802
|
end
|
|
787
803
|
----
|
|
788
804
|
|
|
789
805
|
**RAR5 Reader Features:**
|
|
790
806
|
|
|
791
|
-
* ✅
|
|
792
|
-
* ✅
|
|
807
|
+
* ✅ STORE file data, header CRC32 verification
|
|
808
|
+
* ✅ 32-bit Unix mtime decoding (matches unrar; nil when absent)
|
|
793
809
|
* ✅ Unicode filenames (UTF-8)
|
|
794
|
-
* ✅ Symlink and hardlink support
|
|
795
810
|
* ✅ Multi-file archives
|
|
796
811
|
* ✅ VInt (variable-length integer) parsing
|
|
797
|
-
* ✅ Proper header tracking with bounds checking
|
|
798
812
|
* ✅ Graceful error handling for truncated/invalid files
|
|
799
|
-
* ✅ libarchive compatibility
|
|
813
|
+
* ✅ libarchive compatibility fixtures
|
|
800
814
|
|
|
801
815
|
====== Writing RAR5 Archives
|
|
802
816
|
|
|
803
817
|
[source,ruby]
|
|
804
818
|
----
|
|
805
|
-
require 'omnizip
|
|
819
|
+
require 'omnizip'
|
|
806
820
|
|
|
807
|
-
#
|
|
821
|
+
# STORE compression (default) — unrar-verified
|
|
808
822
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar')
|
|
809
823
|
writer.add_file('document.txt')
|
|
810
824
|
writer.add_file('image.png')
|
|
811
825
|
writer.write
|
|
812
826
|
|
|
813
|
-
#
|
|
827
|
+
# AES-256 encryption
|
|
814
828
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
815
|
-
|
|
816
|
-
level: 5 # 1=fastest, 3=normal, 5=best
|
|
829
|
+
password: 'secret'
|
|
817
830
|
)
|
|
818
831
|
writer.add_file('data.json')
|
|
819
832
|
writer.write
|
|
820
833
|
|
|
821
|
-
#
|
|
834
|
+
# Multi-volume archives split at 64 KB boundaries
|
|
822
835
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
823
|
-
|
|
824
|
-
|
|
836
|
+
multi_volume: true,
|
|
837
|
+
volume_size: 65536
|
|
825
838
|
)
|
|
826
|
-
writer.add_file('
|
|
827
|
-
writer.add_file('
|
|
828
|
-
writer.write
|
|
839
|
+
writer.add_file('big1.bin')
|
|
840
|
+
writer.add_file('big2.bin')
|
|
841
|
+
writer.write # writes archive.part1.rar, archive.part2.rar, ...
|
|
829
842
|
----
|
|
830
843
|
|
|
831
844
|
====== RAR5 Compression Methods
|
|
832
845
|
|
|
833
|
-
[cols="2,2,
|
|
846
|
+
[cols="2,2,4",options="header"]
|
|
834
847
|
|===
|
|
835
|
-
|Method |
|
|
836
|
-
|`:store`
|
|
837
|
-
|`:lzma` |
|
|
838
|
-
|`:
|
|
839
|
-
|`:lzma` |3 |4 MB |✅ LZMA normal (default)
|
|
840
|
-
|`:lzma` |4 |8 MB |✅ LZMA good
|
|
841
|
-
|`:lzma` |5 |16 MB |✅ LZMA best
|
|
848
|
+
|Method |Interop |Notes
|
|
849
|
+
|`:store` (default) |✅ unrar-verified |Uncompressed passthrough
|
|
850
|
+
|`:lzss` / `:lzma` |falls back to STORE |Encoder is not official-RAR compatible; warns and stores
|
|
851
|
+
|`:auto` |STORE |Size-based selection
|
|
842
852
|
|===
|
|
843
853
|
|
|
844
|
-
====== RAR5
|
|
845
|
-
|
|
846
|
-
**Implemented (v0.3.0):**
|
|
847
|
-
|
|
848
|
-
* ✅ **STORE compression** - Uncompressed storage (method 0)
|
|
849
|
-
* ✅ **LZMA compression** - 5 compression levels (methods 1-5) - *SDK-compatible encoder*
|
|
850
|
-
* ✅ **Auto compression** - Smart selection based on file size
|
|
851
|
-
* ✅ **Multi-volume archives** - Split archives across multiple volumes
|
|
852
|
-
* ✅ **Solid compression** - 10-30% better compression for similar files
|
|
853
|
-
* ✅ **AES-256 encryption** - Password protection with PBKDF2-HMAC-SHA256
|
|
854
|
-
* ✅ **PAR2 recovery records** - Error correction with Reed-Solomon codes
|
|
855
|
-
* ✅ **Optional fields** - Modification time (mtime), CRC32 checksums
|
|
856
|
-
* ✅ **Pure Ruby** - Zero external dependencies
|
|
857
|
-
* ✅ **LZMA SDK compatibility** - Encoder produces byte-for-byte identical output to reference implementation
|
|
858
|
-
* ✅ **Full reader support** - All compression methods, solid archives, unicode, symlinks
|
|
854
|
+
====== RAR5 Optional Fields
|
|
859
855
|
|
|
860
|
-
|
|
856
|
+
The writer can embed metadata that readers use without decompressing:
|
|
861
857
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
858
|
+
[source,ruby]
|
|
859
|
+
----
|
|
860
|
+
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
861
|
+
include_mtime: true, # 32-bit Unix mtime per file header
|
|
862
|
+
include_crc32: true # CRC32 per file header
|
|
863
|
+
)
|
|
864
|
+
----
|
|
866
865
|
|
|
867
866
|
====== RAR5 Optional Fields
|
|
868
867
|
|
|
@@ -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
|