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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67142de350c2f3dd29c473268977799ed088cedb0340932cff6db7bc23ac009e
4
- data.tar.gz: c35fbc3353ca0c20f2ec4f86fa39fff67e6ed95822702a18c9b545d35406749c
3
+ metadata.gz: 8f86f08dba1d67ec9a22053ee355dda5d461c42d29b45134ad75a1192584d0a5
4
+ data.tar.gz: 31d78c577804b43509bfea9a8bc9fd5edfad4ac149752cabb0eef87eafebbf56
5
5
  SHA512:
6
- metadata.gz: 7f6b4e88bb0a227f7cff47b06e979891e6d5a57288ae141bd15e57a07abe2cef3c915600242bb05406c4b7a13ef9fdd0ab5f11870e41b65f0a95077191741ceb
7
- data.tar.gz: 3e8497c8eb222b4a139f965ec84f1672d8b12ae92e55986df95d8057c452bcace18a75c54a0464ca696776a33256d2e11ea53811c7d73e3ecb1c0eece06d04ff
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
- compressed = Omnizip::Formats::Xz.compress("Hello, World!")
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
- # Decompress from XZ format
123
- compressed_data = File.read('output.xz')
124
- decompressed = Omnizip::Formats::Xz.decompress(compressed_data)
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.compress(data, check_type: :crc64)
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::Writer.create('archive.7z') do |sz|
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::Reader.open('archive.7z') do |sz|
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::Reader.open('archive.7z') do |sz|
231
- sz.entries.each do |entry|
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::Writer.create('archive.7z',
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::Writer.create('secure.7z',
252
+ Omnizip::Formats::SevenZip.create('secure.7z',
252
253
  password: 'secret123',
253
- encrypt_header: true
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 compression (default)
421
- Omnizip::Formats::Rpm.write('mypackage-1.0-1.noarch.rpm') do |rpm|
422
- rpm.name = 'mypackage'
423
- rpm.version = '1.0'
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
- # Create RPM with different compression
443
- Omnizip::Formats::Rpm.write('mypackage-1.0-1.x86_64.rpm',
444
- compression: :xz) do |rpm|
445
- rpm.name = 'mypackage'
446
- rpm.version = '1.0'
447
- rpm.release = '1'
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** - Full read support with all compression methods, write support with STORE, FASTEST, NORMAL (v0.3.0)
548
- * **RAR5** - Full read/write support with STORE and LZMA compression, multi-volume, solid archives (v0.3.0)
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
- writer = Omnizip::Formats::SevenZip::Writer.new('archive.7z')
683
- writer.add_file('document.pdf')
684
- writer.close
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
- Omnizip v0.3.0 provides complete RAR4 archive support with full read capabilities and write support for three compression methods:
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
- # Read RAR4 archive with native decompression
700
- reader = Omnizip::Formats::Rar3::Reader.new
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
- # List files with metadata
705
- entries.each do |entry|
706
- puts "#{entry.name}: #{entry.uncompressed_size} bytes (#{entry.compressed_size} compressed)"
707
- puts " Method: #{entry.compression_method}"
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
- * ✅ All compression methods: STORE, FASTEST, FAST, NORMAL, GOOD, BEST
717
- * ✅ Proper block header parsing (FILE blocks, archive headers)
718
- * ✅ Minimal archive support (archives without archive header)
719
- * ✅ Unicode filename support
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
- * ✅ Graceful error handling for truncated/malformed files
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
- # Create RAR4 archive with default compression (NORMAL)
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 |Ratio |Status
758
- |`:store` |Instant |1.0x |✅ Fully working
759
- |`:fastest` |Very Fast |2-3x |✅ Fully working
760
- |`:normal` |Fast |3-5x |✅ Fully working (default)
761
- |`:best` |Slow |5-10x |⚠️ Known issues (v0.3.1)
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
- Full read/write support for RAR5 archives with STORE and LZMA compression, including optional fields (mtime, CRC32).
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/formats/rar5/reader'
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 " Method: #{entry.compression_method}"
783
- puts " CRC32: #{entry.crc32.to_s(16)}"
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
- * ✅ All compression methods: STORE, LZSS (methods 0-5)
792
- * ✅ Solid archive support
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 (51 test files verified)
782
+ * ✅ libarchive compatibility fixtures
800
783
 
801
784
  ====== Writing RAR5 Archives
802
785
 
803
786
  [source,ruby]
804
787
  ----
805
- require 'omnizip/formats/rar/rar5/writer'
788
+ require 'omnizip'
806
789
 
807
- # Create RAR5 archive with STORE compression (default)
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
- # LZMA compression with level selection
796
+ # AES-256 encryption
814
797
  writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
815
- compression: :lzma,
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
- # Auto-select compression based on file size
803
+ # Multi-volume archives split at 64 KB boundaries
822
804
  writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
823
- compression: :auto, # < 1KB → STORE, ≥ 1KB → LZMA
824
- level: 3
805
+ multi_volume: true,
806
+ volume_size: 65536
825
807
  )
826
- writer.add_file('small.txt')
827
- writer.add_file('large.dat')
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,3,3",options="header"]
815
+ [cols="2,2,4",options="header"]
834
816
  |===
835
- |Method |Level |Dictionary |Status
836
- |`:store` |0 |None |✅ Uncompressed passthrough
837
- |`:lzma` |1 |256 KB |✅ LZMA fastest
838
- |`:lzma` |2 |1 MB |✅ LZMA fast
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 Features
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
- **CRC32 Limitation:**
825
+ The writer can embed metadata that readers use without decompressing:
861
826
 
862
- * ⚠️ **CRC32 checksums** - Only compatible with STORE compression
863
- - When LZMA compression is used, CRC32 is automatically disabled
864
- - This is a RAR5 format limitation, not an implementation issue
865
- - Use BLAKE2sp (always enabled) for compressed file integrity
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
 
@@ -45,6 +45,7 @@ format:
45
45
  version: 0x0800 # Version flag
46
46
  ext_time: 0x1000 # Extended time field present
47
47
  ext_flags: 0x2000 # Extended flags field
48
+ long_block: 0x8000 # Block carries a data area (PACK_SIZE)
48
49
 
49
50
  compression_methods:
50
51
  store: 0x30 # No compression
@@ -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
- entry.is_dir = head_flags.anybits?(FILE_DIRECTORY)
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
- # Fixed fields: TYPE(1) + FLAGS(2) + SIZE(2) + PACK_SIZE(4) + UNPACK_SIZE(4) +
99
- # HOST_OS(1) + FILE_CRC(4) + FILE_TIME(4) + VERSION(1) + METHOD(1) +
100
- # NAME_SIZE(2) + ATTR(4) = 30 bytes
101
- remaining = head_size - (name_size + 30)
102
- remaining += 8 if head_flags.anybits?(FILE_LARGE)
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 "\"#{path}\"" if File.exist?(path)
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 = `#{command_path} 2>&1`
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 = `#{command_path} vb "#{archive_path}" 2>&1`
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
- cmd = "#{command_path} x -y"
256
- cmd += " -p#{password}" if password
257
- cmd += " \"#{archive_path}\" \"#{output_dir}/\""
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
- # Read marker block
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
- # head_size includes TYPE(1) + FLAGS(2) + SIZE(2) = 5 bytes already read
117
- remaining = head_size - 5
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
- # Check if we have the compression method
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
- # Decompress using Dispatcher
265
- File.open(output_path, "wb") do |output|
266
- # For now, assume METHOD_STORE (0x30)
267
- # Real implementation would get method from entry
268
- method = entry.method || 0x30
269
-
270
- Compression::Dispatcher.decompress(method, compressed_data, output)
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}"