omnizip 0.3.38 → 0.3.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/README.adoc +108 -140
- data/config/formats/rar3_spec.yml +1 -0
- 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 +0 -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.rb +2 -0
- data/lib/omnizip/version.rb +1 -1
- data/readme-docs/api-usage.adoc +5 -4
- data/readme-docs/architecture.adoc +5 -4
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f86f08dba1d67ec9a22053ee355dda5d461c42d29b45134ad75a1192584d0a5
|
|
4
|
+
data.tar.gz: 31d78c577804b43509bfea9a8bc9fd5edfad4ac149752cabb0eef87eafebbf56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6ee4bb68ede247277420ec455130140a8be8e1f417581f1c3f996cab48d34202ae26631862ef88ef301fb712c649f72100805c6d553afce51f2705747e68e28
|
|
7
|
+
data.tar.gz: b7f3567b7f8d3c7679bbd7563fd9fabf666aa3c1fbfdff38afd0e9e2dc9034e1f78428b710f2ff72518e45eef956eeb61a679c27df6b00e2a8e81cbbc0b5eed1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.38] - 2026-08-28
|
|
11
|
+
|
|
10
12
|
### Added
|
|
11
13
|
- `.cpio` and `.iso` read routing: `extract_archive`/`list_archive`/
|
|
12
14
|
`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)
|
|
@@ -679,16 +664,19 @@ File.open('input.txt', 'rb') do |input|
|
|
|
679
664
|
end
|
|
680
665
|
|
|
681
666
|
# .7z archive operations
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
667
|
+
Omnizip::Formats::SevenZip.create('archive.7z') do |sz|
|
|
668
|
+
sz.add_file('document.pdf')
|
|
669
|
+
end
|
|
685
670
|
----
|
|
686
671
|
|
|
687
672
|
==== RAR Archives
|
|
688
673
|
|
|
689
674
|
==== RAR4 Archives (v0.3.0)
|
|
690
675
|
|
|
691
|
-
|
|
676
|
+
RAR4 reading and writing against the on-disk format unrar accepts:
|
|
677
|
+
headers carry the low 16 bits of a CRC32, HEAD_SIZE counts the whole
|
|
678
|
+
block, and the dictionary mask 0xE0 marks directory entries. STORE
|
|
679
|
+
archives round-trip through official unrar byte-identically.
|
|
692
680
|
|
|
693
681
|
====== Reading RAR4 Archives
|
|
694
682
|
|
|
@@ -696,31 +684,29 @@ Omnizip v0.3.0 provides complete RAR4 archive support with full read capabilitie
|
|
|
696
684
|
----
|
|
697
685
|
require 'omnizip'
|
|
698
686
|
|
|
699
|
-
|
|
700
|
-
reader
|
|
701
|
-
File.open('archive.rar', 'rb') do |io|
|
|
702
|
-
entries = reader.read_archive(io)
|
|
687
|
+
reader = Omnizip::Formats::Rar::Reader.new('archive.rar')
|
|
688
|
+
reader.open
|
|
703
689
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
puts " Modified: #{entry.modified_time}"
|
|
709
|
-
puts " Directory: #{entry.is_directory}"
|
|
710
|
-
end
|
|
690
|
+
reader.list_files.each do |entry|
|
|
691
|
+
puts "#{entry.name}: #{entry.size} bytes (#{entry.compressed_size} compressed)"
|
|
692
|
+
puts " Directory: #{entry.is_dir}"
|
|
693
|
+
puts " Modified: #{entry.mtime}"
|
|
711
694
|
end
|
|
695
|
+
|
|
696
|
+
# STORE data decodes natively; WinRAR-compressed entries fall back to
|
|
697
|
+
# the unrar command after a CRC check rejects foreign streams
|
|
698
|
+
reader.extract_entry('test.txt', 'output/test.txt')
|
|
699
|
+
reader.extract_all('output/')
|
|
712
700
|
----
|
|
713
701
|
|
|
714
702
|
**RAR4 Reader Features:**
|
|
715
703
|
|
|
716
|
-
* ✅
|
|
717
|
-
* ✅
|
|
718
|
-
* ✅
|
|
719
|
-
* ✅
|
|
720
|
-
* ✅ Symlink detection and handling
|
|
704
|
+
* ✅ Block header parsing (MAIN/FILE/ENDARC), directories, empty files
|
|
705
|
+
* ✅ Native STORE decompression with stored-CRC32 verification
|
|
706
|
+
* ✅ unrar fallback for real WinRAR-compressed entries (byte-exact)
|
|
707
|
+
* ✅ DOS-style backslash name normalization to forward slashes
|
|
721
708
|
* ✅ Multi-volume archive detection
|
|
722
|
-
* ✅
|
|
723
|
-
* ✅ libarchive compatibility (52 test files verified)
|
|
709
|
+
* ✅ Parsing verified against libarchive WinRAR fixtures
|
|
724
710
|
|
|
725
711
|
====== Writing RAR4 Archives
|
|
726
712
|
|
|
@@ -728,141 +714,123 @@ end
|
|
|
728
714
|
----
|
|
729
715
|
require 'omnizip'
|
|
730
716
|
|
|
731
|
-
#
|
|
732
|
-
writer = Omnizip::Formats::Rar::Writer.new('archive.rar'
|
|
717
|
+
# STORE output is verified interoperable with unrar
|
|
718
|
+
writer = Omnizip::Formats::Rar::Writer.new('archive.rar',
|
|
719
|
+
compression: :store
|
|
720
|
+
)
|
|
733
721
|
writer.add_file('document.txt')
|
|
734
722
|
writer.add_file('image.png')
|
|
735
723
|
writer.add_directory('photos/')
|
|
736
724
|
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
725
|
----
|
|
752
726
|
|
|
727
|
+
Writer options: `compression:` (see below), `test_after_create: true`
|
|
728
|
+
(reads the archive back and verifies every file). RAR4 encryption is
|
|
729
|
+
not implemented — `password:` raises `NotImplementedError`. The
|
|
730
|
+
`solid:`/`recovery:`/`volume_size:` options are accepted but not
|
|
731
|
+
implemented; they are ignored with a warning and never advertised in
|
|
732
|
+
header flags.
|
|
733
|
+
|
|
753
734
|
====== RAR4 Compression Methods
|
|
754
735
|
|
|
755
736
|
[cols="2,2,2,4",options="header"]
|
|
756
737
|
|===
|
|
757
|
-
|Method |Speed |
|
|
758
|
-
|`:store` |Instant |
|
|
759
|
-
|`:fastest` |Very Fast
|
|
760
|
-
|`:normal` |Fast
|
|
761
|
-
|`:
|
|
738
|
+
|Method |Speed |unrar interop |Notes
|
|
739
|
+
|`:store` |Instant |✅ verified |Byte-identical round-trip incl. directories
|
|
740
|
+
|`:fastest` |Very Fast |❌ Omnizip-only |LZ77+Huffman, readable by Omnizip
|
|
741
|
+
|`:normal` (default) |Fast |❌ Omnizip-only |LZ77+Huffman, readable by Omnizip
|
|
742
|
+
|`:good` |Medium |❌ Omnizip-only |LZ77+Huffman, readable by Omnizip
|
|
743
|
+
|`:best` |Slow |❌ Omnizip-only |PPMd, readable by Omnizip
|
|
762
744
|
|===
|
|
763
745
|
|
|
746
|
+
Official RAR LZ/PPMd streams are proprietary; methods above `:store`
|
|
747
|
+
produce archives Omnizip can read back but unrar reports as checksum
|
|
748
|
+
errors. Use `:store` for interoperability.
|
|
749
|
+
|
|
764
750
|
===== RAR5 Archives (v0.3.0)
|
|
765
751
|
|
|
766
|
-
|
|
752
|
+
Read/write support for RAR5 archives with STORE compression,
|
|
753
|
+
AES-256 encryption, and multi-volume archives — all verified against
|
|
754
|
+
official unrar.
|
|
767
755
|
|
|
768
756
|
====== Reading RAR5 Archives
|
|
769
757
|
|
|
770
758
|
[source,ruby]
|
|
771
759
|
----
|
|
772
|
-
require 'omnizip
|
|
760
|
+
require 'omnizip'
|
|
773
761
|
|
|
774
|
-
# Read RAR5 archive
|
|
775
762
|
reader = Omnizip::Formats::Rar5::Reader.new
|
|
776
763
|
File.open('archive.rar', 'rb') do |io|
|
|
777
764
|
entries = reader.read_archive(io)
|
|
778
765
|
|
|
779
|
-
# List files with metadata
|
|
780
766
|
entries.each do |entry|
|
|
781
767
|
puts "#{entry.name}: #{entry.uncompressed_size} bytes"
|
|
782
|
-
puts "
|
|
783
|
-
puts "
|
|
784
|
-
puts " Modified: #{entry.modified_time}"
|
|
768
|
+
puts " CRC32: #{entry.crc32.to_s(16)}" if entry.crc32
|
|
769
|
+
puts " Modified: #{entry.modified_time}" if entry.modified_time
|
|
785
770
|
end
|
|
786
771
|
end
|
|
787
772
|
----
|
|
788
773
|
|
|
789
774
|
**RAR5 Reader Features:**
|
|
790
775
|
|
|
791
|
-
* ✅
|
|
792
|
-
* ✅
|
|
776
|
+
* ✅ STORE file data, header CRC32 verification
|
|
777
|
+
* ✅ 32-bit Unix mtime decoding (matches unrar; nil when absent)
|
|
793
778
|
* ✅ Unicode filenames (UTF-8)
|
|
794
|
-
* ✅ Symlink and hardlink support
|
|
795
779
|
* ✅ Multi-file archives
|
|
796
780
|
* ✅ VInt (variable-length integer) parsing
|
|
797
|
-
* ✅ Proper header tracking with bounds checking
|
|
798
781
|
* ✅ Graceful error handling for truncated/invalid files
|
|
799
|
-
* ✅ libarchive compatibility
|
|
782
|
+
* ✅ libarchive compatibility fixtures
|
|
800
783
|
|
|
801
784
|
====== Writing RAR5 Archives
|
|
802
785
|
|
|
803
786
|
[source,ruby]
|
|
804
787
|
----
|
|
805
|
-
require 'omnizip
|
|
788
|
+
require 'omnizip'
|
|
806
789
|
|
|
807
|
-
#
|
|
790
|
+
# STORE compression (default) — unrar-verified
|
|
808
791
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar')
|
|
809
792
|
writer.add_file('document.txt')
|
|
810
793
|
writer.add_file('image.png')
|
|
811
794
|
writer.write
|
|
812
795
|
|
|
813
|
-
#
|
|
796
|
+
# AES-256 encryption
|
|
814
797
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
815
|
-
|
|
816
|
-
level: 5 # 1=fastest, 3=normal, 5=best
|
|
798
|
+
password: 'secret'
|
|
817
799
|
)
|
|
818
800
|
writer.add_file('data.json')
|
|
819
801
|
writer.write
|
|
820
802
|
|
|
821
|
-
#
|
|
803
|
+
# Multi-volume archives split at 64 KB boundaries
|
|
822
804
|
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
823
|
-
|
|
824
|
-
|
|
805
|
+
multi_volume: true,
|
|
806
|
+
volume_size: 65536
|
|
825
807
|
)
|
|
826
|
-
writer.add_file('
|
|
827
|
-
writer.add_file('
|
|
828
|
-
writer.write
|
|
808
|
+
writer.add_file('big1.bin')
|
|
809
|
+
writer.add_file('big2.bin')
|
|
810
|
+
writer.write # writes archive.part1.rar, archive.part2.rar, ...
|
|
829
811
|
----
|
|
830
812
|
|
|
831
813
|
====== RAR5 Compression Methods
|
|
832
814
|
|
|
833
|
-
[cols="2,2,
|
|
815
|
+
[cols="2,2,4",options="header"]
|
|
834
816
|
|===
|
|
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
|
|
817
|
+
|Method |Interop |Notes
|
|
818
|
+
|`:store` (default) |✅ unrar-verified |Uncompressed passthrough
|
|
819
|
+
|`:lzss` / `:lzma` |falls back to STORE |Encoder is not official-RAR compatible; warns and stores
|
|
820
|
+
|`:auto` |STORE |Size-based selection
|
|
842
821
|
|===
|
|
843
822
|
|
|
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
|
|
823
|
+
====== RAR5 Optional Fields
|
|
859
824
|
|
|
860
|
-
|
|
825
|
+
The writer can embed metadata that readers use without decompressing:
|
|
861
826
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
827
|
+
[source,ruby]
|
|
828
|
+
----
|
|
829
|
+
writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
|
|
830
|
+
include_mtime: true, # 32-bit Unix mtime per file header
|
|
831
|
+
include_crc32: true # CRC32 per file header
|
|
832
|
+
)
|
|
833
|
+
----
|
|
866
834
|
|
|
867
835
|
====== RAR5 Optional Fields
|
|
868
836
|
|
|
@@ -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}"
|