omnizip 0.3.37 → 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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -9
  3. data/README.adoc +108 -140
  4. data/config/formats/rar3_spec.yml +1 -0
  5. data/docs/guides/archive-formats/gzip-format.adoc +2 -4
  6. data/docs/guides/archive-formats/ole-format.adoc +54 -219
  7. data/docs/guides/archive-formats/rar5.adoc +15 -15
  8. data/docs/guides/archive-formats/rpm-format.adoc +48 -189
  9. data/docs/reference/api/overview.adoc +16 -17
  10. data/docs/troubleshooting/index.adoc +1 -4
  11. data/lib/omnizip/archive_handler.rb +2 -0
  12. data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
  13. data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
  14. data/lib/omnizip/archive_handlers.rb +2 -0
  15. data/lib/omnizip/cli.rb +25 -0
  16. data/lib/omnizip/convenience.rb +6 -4
  17. data/lib/omnizip/formats/iso/directory_builder.rb +24 -0
  18. data/lib/omnizip/formats/iso/reader.rb +2 -2
  19. data/lib/omnizip/formats/iso/writer.rb +20 -4
  20. data/lib/omnizip/formats/iso.rb +3 -1
  21. data/lib/omnizip/formats/rar/block_parser.rb +18 -8
  22. data/lib/omnizip/formats/rar/constants.rb +4 -0
  23. data/lib/omnizip/formats/rar/decompressor.rb +12 -8
  24. data/lib/omnizip/formats/rar/header.rb +7 -15
  25. data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +5 -3
  26. data/lib/omnizip/formats/rar/rar5/header.rb +50 -33
  27. data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +17 -34
  28. data/lib/omnizip/formats/rar/rar5/vint.rb +25 -37
  29. data/lib/omnizip/formats/rar/rar5/writer.rb +50 -14
  30. data/lib/omnizip/formats/rar/reader.rb +18 -14
  31. data/lib/omnizip/formats/rar/writer.rb +121 -136
  32. data/lib/omnizip/formats/rar.rb +5 -11
  33. data/lib/omnizip/formats/rar3/reader.rb +11 -50
  34. data/lib/omnizip/formats/rar3/writer.rb +13 -10
  35. data/lib/omnizip/formats/rar3.rb +15 -0
  36. data/lib/omnizip/formats/rar5/reader.rb +8 -10
  37. data/lib/omnizip/formats/rar5.rb +15 -0
  38. data/lib/omnizip/formats/seven_zip/parser.rb +22 -7
  39. data/lib/omnizip/formats/seven_zip/writer.rb +78 -19
  40. data/lib/omnizip/formats.rb +2 -0
  41. data/lib/omnizip/version.rb +1 -1
  42. data/readme-docs/api-usage.adoc +5 -4
  43. data/readme-docs/architecture.adoc +5 -4
  44. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbb80e9d06123c538d5823fd0263b38c8a1a386ce0ba107f1aefd6320b2f3618
4
- data.tar.gz: 53a147f76a0290e7b8dc211c2040cb090799f196fbc57c24cd4ff44290ef53f1
3
+ metadata.gz: 8f86f08dba1d67ec9a22053ee355dda5d461c42d29b45134ad75a1192584d0a5
4
+ data.tar.gz: 31d78c577804b43509bfea9a8bc9fd5edfad4ac149752cabb0eef87eafebbf56
5
5
  SHA512:
6
- metadata.gz: 8f1fae0da5256104bc0ce6871ebf6f85ec4e60865d513004a55c30389eca1dd3ba47bc3d94c6c0f419717930933920428eb3755e2a3f237014094012638953b7
7
- data.tar.gz: 27eb53ffd94c880bfa429202cd8b9bdc9094e83ccc382a96479a6dd0c8f090997081f41d5b3463c6f63bb7e9d06fff0dc7b44c45425c0bdd85cc503998559763
6
+ metadata.gz: f6ee4bb68ede247277420ec455130140a8be8e1f417581f1c3f996cab48d34202ae26631862ef88ef301fb712c649f72100805c6d553afce51f2705747e68e28
7
+ data.tar.gz: b7f3567b7f8d3c7679bbd7563fd9fabf666aa3c1fbfdff38afd0e9e2dc9034e1f78428b710f2ff72518e45eef956eeb61a679c27df6b00e2a8e81cbbc0b5eed1
data/CHANGELOG.md CHANGED
@@ -7,6 +7,79 @@ 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
+
12
+ ### Added
13
+ - `.cpio` and `.iso` read routing: `extract_archive`/`list_archive`/
14
+ `read_from_archive`/`Archive.open` operate on CPIO archives and ISO
15
+ 9660 images through new read-only handlers, mirroring the RAR
16
+ routing (creation keeps raising truthfully).
17
+
18
+ ### Fixed
19
+ - 7z archives with explicit directory entries (or zero-byte
20
+ entries) were structurally invalid: the writer never emitted the
21
+ kEmptyStream/kEmptyFile properties, hardcoded 0x20 attributes
22
+ over each entry's real attributes, and its kSubStreamsInfo
23
+ digest/size arrays counted directory entries as substreams. 7-Zip
24
+ rejects such archives with "Headers Error" (and our own reader
25
+ crashed extracting directory entries). Verified against 7zz
26
+ ground truth (`-mhc=off` dumps): kEmptyStream/kEmptyFile carry
27
+ RAW bit vectors with no all-defined marker — the parser expected
28
+ that marker everywhere, so it now reads both layouts correctly
29
+ (which also fixes reading 7-Zip-created archives containing empty
30
+ files).
31
+ - RAR5 vint encoding was not the spec encoding: multi-byte values
32
+ (any size >= 128, extra-area sizes, dictionary sizes) were written
33
+ in a byte-swapped form no reader decodes — archives with more
34
+ than ~127 bytes of content were corrupt for unrar AND for our own
35
+ spec-conformant parser. All round-trips now verified against
36
+ unrar, including encrypted STORE, multi-file, and multi-volume
37
+ archives (volume flags were the previously documented "no
38
+ write-side reference" gap: Main archive flags 0x0001/0x0002 and
39
+ end-of-archive 0x0001 are written per the RAR 5.0 specification).
40
+ - RAR5 file headers: a "mystery vint" not in the spec shifted every
41
+ field; the compression method was written into the version bits;
42
+ the file-attribute value chmods extracted files unreadable. Now:
43
+ spec field order, method in bits 8-10 with dictionary bits
44
+ 11-15, Unix attribute 0o100644.
45
+ - RAR5 encrypted writing now persists the salt/IV as the file
46
+ encryption extra record (spec type 0x01) — archives it wrote
47
+ before could not be decrypted by anything (unrar reported "All
48
+ OK" only because no CRC was stored).
49
+ - RAR5 `:lzma`/`:lzss` compression advertised method bits for a
50
+ stream that nothing can decode (the encoder is not
51
+ official-RAR-compatible; `unrar` extracts empty files silently).
52
+ `Lzss.available?` is now honest, so writers fall back to STORE
53
+ with a warning; solid mode falls back to independent STORE
54
+ entries.
55
+ - ISO writing: directory records pointed every file at sector 0
56
+ (allocated extents were never copied onto the tree nodes); the
57
+ volume descriptor declared 100 sectors regardless of actual
58
+ size; `add_directory` entries were not marked as directories;
59
+ Rock Ridge and Joliet were advertised by default without being
60
+ implemented (the cloned SVD made 7-Zip read ASCII names as
61
+ UCS-2); `Reader#extract_all` looked entries up by bare name
62
+ instead of full path; `Formats::Iso.list` returned the Reader
63
+ instead of entries. Images now extract byte-identically through
64
+ 7zz, including nested directories.
65
+ - CLI: `omnizip <command> --help` failed with an arity error on
66
+ every command (Thor does not route `--help` on subcommands by
67
+ itself). A dispatch hook now prints per-command help.
68
+ - `Omnizip::Formats::Rar::Rar5::MainHeader/FileHeader` were
69
+ unreferenceable unless another file happened to load header.rb
70
+ first (missing autoload entries); three Rar5 autoloads pointed at
71
+ files that never existed.
72
+
73
+ ### Changed
74
+ - Docs: the OLE, RPM, GZIP, RAR5 and API-overview guides now use
75
+ the real APIs (`Formats::Ole.list/read/info`, `Formats::Rpm.*`,
76
+ `Gzip.compress_stream`, `Formats::Zip::Reader#read`, correct
77
+ error class names); fictional methods (`open_stream`, `root`,
78
+ `extract_to`, `extract_files`, `extract_payload`,
79
+ `Rar::Rar5::Reader`, scriptlets/signature accessors) removed.
80
+
81
+ ## [0.3.37] - 2026-08-28
82
+
10
83
  ### Added
11
84
  - `Omnizip::Archive` — the block-style facade the guides have
12
85
  documented since the docs were written (it never existed; ~50
@@ -143,15 +216,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
143
216
  been pinned to :copy) and replaced the match-encoding TODO with a
144
217
  wire-level round-trip verification of the distance-8/length
145
218
  pattern through the LZMA2 decoder. No code under lib/ changes.
146
-
147
- ## [0.3.30] - 2026-08-27
148
-
149
- ### Verified
150
- - LZIP interop with the real `lzip` CLI (1.26): our members pass
151
- `lzip -t`/`-dc` and the CLI's members decode through
152
- `Formats::Lzip` (spec skips where the CLI is absent).
153
-
154
- ### Changed
155
219
  - The remaining lib/ TODO markers are resolved as documented design
156
220
  decisions rather than dangling work: RAR5 volume extras and
157
221
  EndHeader volume flags (no write-side reference exists;
@@ -165,6 +229,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
165
229
  the salt/IV header, so archives it writes cannot be decrypted;
166
230
  encryption stays read-verified only.
167
231
 
232
+ ### Verified
233
+ - LZIP interop with the real `lzip` CLI (1.26): our members pass
234
+ `lzip -t`/`-dc` and the CLI's members decode through
235
+ `Formats::Lzip` (spec skips where the CLI is absent).
236
+
168
237
  ## [0.3.28] - 2026-08-26
169
238
 
170
239
  ### Added
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
@@ -92,10 +92,8 @@ GZIP supports streaming for large files:
92
92
  ----
93
93
  # Stream compress large file
94
94
  File.open('large.bin', 'rb') do |input|
95
- Omnizip::Gzip::Writer.open('large.bin.gz') do |output|
96
- while chunk = input.read(65536)
97
- output.write(chunk)
98
- end
95
+ File.open('large.bin.gz', 'wb') do |output|
96
+ Omnizip::Formats::Gzip.compress_stream(input, output)
99
97
  end
100
98
  end
101
99
  ----