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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.adoc +139 -140
  4. data/config/formats/rar3_spec.yml +1 -0
  5. data/lib/omnizip/algorithm_registry.rb +7 -0
  6. data/lib/omnizip/algorithms/deflate64.rb +5 -8
  7. data/lib/omnizip/buffer/memory_extractor.rb +31 -6
  8. data/lib/omnizip/buffer/seven_zip_bridge.rb +154 -0
  9. data/lib/omnizip/buffer.rb +6 -5
  10. data/lib/omnizip/commands/archive_create_command.rb +7 -2
  11. data/lib/omnizip/commands/decompress_command.rb +29 -1
  12. data/lib/omnizip/commands/metadata_command.rb +67 -3
  13. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +14 -52
  14. data/lib/omnizip/formats/rar/block_parser.rb +18 -8
  15. data/lib/omnizip/formats/rar/constants.rb +4 -0
  16. data/lib/omnizip/formats/rar/decompressor.rb +12 -8
  17. data/lib/omnizip/formats/rar/header.rb +7 -15
  18. data/lib/omnizip/formats/rar/reader.rb +18 -14
  19. data/lib/omnizip/formats/rar/writer.rb +121 -136
  20. data/lib/omnizip/formats/rar.rb +18 -8
  21. data/lib/omnizip/formats/rar3/reader.rb +11 -50
  22. data/lib/omnizip/formats/rar3/writer.rb +13 -10
  23. data/lib/omnizip/formats/rar3.rb +15 -0
  24. data/lib/omnizip/formats/rar5/reader.rb +8 -10
  25. data/lib/omnizip/formats/rar5.rb +15 -0
  26. data/lib/omnizip/formats/seven_zip/writer.rb +17 -0
  27. data/lib/omnizip/formats.rb +2 -0
  28. data/lib/omnizip/version.rb +1 -1
  29. data/readme-docs/api-usage.adoc +6 -5
  30. data/readme-docs/architecture.adoc +6 -5
  31. data/readme-docs/compression-algorithms.adoc +1 -1
  32. data/readme-docs/installation.adoc +1 -1
  33. data/readme-docs/performance-profiler.adoc +1 -1
  34. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67142de350c2f3dd29c473268977799ed088cedb0340932cff6db7bc23ac009e
4
- data.tar.gz: c35fbc3353ca0c20f2ec4f86fa39fff67e6ed95822702a18c9b545d35406749c
3
+ metadata.gz: 5013ae71553867968422f57b01425820f17c5cd6e471ea0cb5b5d46d1cc6a1bd
4
+ data.tar.gz: 94698e90386f195cdd7f5fd7a8ebae3010f2f206e70eb96463ce8e4d2ece09b0
5
5
  SHA512:
6
- metadata.gz: 7f6b4e88bb0a227f7cff47b06e979891e6d5a57288ae141bd15e57a07abe2cef3c915600242bb05406c4b7a13ef9fdd0ab5f11870e41b65f0a95077191741ceb
7
- data.tar.gz: 3e8497c8eb222b4a139f965ec84f1672d8b12ae92e55986df95d8057c452bcace18a75c54a0464ca696776a33256d2e11ea53811c7d73e3ecb1c0eece06d04ff
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
- 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)
@@ -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
- writer = Omnizip::Formats::SevenZip::Writer.new('archive.7z')
683
- writer.add_file('document.pdf')
684
- writer.close
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
- Omnizip v0.3.0 provides complete RAR4 archive support with full read capabilities and write support for three compression methods:
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
- # 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)
718
+ reader = Omnizip::Formats::Rar::Reader.new('archive.rar')
719
+ reader.open
703
720
 
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
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
- * ✅ 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
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
- * ✅ Graceful error handling for truncated/malformed files
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
- # Create RAR4 archive with default compression (NORMAL)
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 |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)
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
- Full read/write support for RAR5 archives with STORE and LZMA compression, including optional fields (mtime, CRC32).
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/formats/rar5/reader'
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 " Method: #{entry.compression_method}"
783
- puts " CRC32: #{entry.crc32.to_s(16)}"
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
- * ✅ All compression methods: STORE, LZSS (methods 0-5)
792
- * ✅ Solid archive support
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 (51 test files verified)
813
+ * ✅ libarchive compatibility fixtures
800
814
 
801
815
  ====== Writing RAR5 Archives
802
816
 
803
817
  [source,ruby]
804
818
  ----
805
- require 'omnizip/formats/rar/rar5/writer'
819
+ require 'omnizip'
806
820
 
807
- # Create RAR5 archive with STORE compression (default)
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
- # LZMA compression with level selection
827
+ # AES-256 encryption
814
828
  writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
815
- compression: :lzma,
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
- # Auto-select compression based on file size
834
+ # Multi-volume archives split at 64 KB boundaries
822
835
  writer = Omnizip::Formats::Rar::Rar5::Writer.new('archive.rar',
823
- compression: :auto, # < 1KB → STORE, ≥ 1KB → LZMA
824
- level: 3
836
+ multi_volume: true,
837
+ volume_size: 65536
825
838
  )
826
- writer.add_file('small.txt')
827
- writer.add_file('large.dat')
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,3,3",options="header"]
846
+ [cols="2,2,4",options="header"]
834
847
  |===
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
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 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
854
+ ====== RAR5 Optional Fields
859
855
 
860
- **CRC32 Limitation:**
856
+ The writer can embed metadata that readers use without decompressing:
861
857
 
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
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
 
@@ -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
@@ -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: "Deflate64",
33
- type: :compression,
34
- streaming_supported: true,
35
- dictionary_size: DICTIONARY_SIZE,
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
- raise NotImplementedError, "7z format support coming in Phase 2"
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
- raise NotImplementedError, "7z format support coming in Phase 2"
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
- raise NotImplementedError, "7z format support coming in Phase 2"
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