omnizip 0.3.37 → 0.3.38

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: dbb80e9d06123c538d5823fd0263b38c8a1a386ce0ba107f1aefd6320b2f3618
4
- data.tar.gz: 53a147f76a0290e7b8dc211c2040cb090799f196fbc57c24cd4ff44290ef53f1
3
+ metadata.gz: 67142de350c2f3dd29c473268977799ed088cedb0340932cff6db7bc23ac009e
4
+ data.tar.gz: c35fbc3353ca0c20f2ec4f86fa39fff67e6ed95822702a18c9b545d35406749c
5
5
  SHA512:
6
- metadata.gz: 8f1fae0da5256104bc0ce6871ebf6f85ec4e60865d513004a55c30389eca1dd3ba47bc3d94c6c0f419717930933920428eb3755e2a3f237014094012638953b7
7
- data.tar.gz: 27eb53ffd94c880bfa429202cd8b9bdc9094e83ccc382a96479a6dd0c8f090997081f41d5b3463c6f63bb7e9d06fff0dc7b44c45425c0bdd85cc503998559763
6
+ metadata.gz: 7f6b4e88bb0a227f7cff47b06e979891e6d5a57288ae141bd15e57a07abe2cef3c915600242bb05406c4b7a13ef9fdd0ab5f11870e41b65f0a95077191741ceb
7
+ data.tar.gz: 3e8497c8eb222b4a139f965ec84f1672d8b12ae92e55986df95d8057c452bcace18a75c54a0464ca696776a33256d2e11ea53811c7d73e3ecb1c0eece06d04ff
data/CHANGELOG.md CHANGED
@@ -7,6 +7,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+ - `.cpio` and `.iso` read routing: `extract_archive`/`list_archive`/
12
+ `read_from_archive`/`Archive.open` operate on CPIO archives and ISO
13
+ 9660 images through new read-only handlers, mirroring the RAR
14
+ routing (creation keeps raising truthfully).
15
+
16
+ ### Fixed
17
+ - 7z archives with explicit directory entries (or zero-byte
18
+ entries) were structurally invalid: the writer never emitted the
19
+ kEmptyStream/kEmptyFile properties, hardcoded 0x20 attributes
20
+ over each entry's real attributes, and its kSubStreamsInfo
21
+ digest/size arrays counted directory entries as substreams. 7-Zip
22
+ rejects such archives with "Headers Error" (and our own reader
23
+ crashed extracting directory entries). Verified against 7zz
24
+ ground truth (`-mhc=off` dumps): kEmptyStream/kEmptyFile carry
25
+ RAW bit vectors with no all-defined marker — the parser expected
26
+ that marker everywhere, so it now reads both layouts correctly
27
+ (which also fixes reading 7-Zip-created archives containing empty
28
+ files).
29
+ - RAR5 vint encoding was not the spec encoding: multi-byte values
30
+ (any size >= 128, extra-area sizes, dictionary sizes) were written
31
+ in a byte-swapped form no reader decodes — archives with more
32
+ than ~127 bytes of content were corrupt for unrar AND for our own
33
+ spec-conformant parser. All round-trips now verified against
34
+ unrar, including encrypted STORE, multi-file, and multi-volume
35
+ archives (volume flags were the previously documented "no
36
+ write-side reference" gap: Main archive flags 0x0001/0x0002 and
37
+ end-of-archive 0x0001 are written per the RAR 5.0 specification).
38
+ - RAR5 file headers: a "mystery vint" not in the spec shifted every
39
+ field; the compression method was written into the version bits;
40
+ the file-attribute value chmods extracted files unreadable. Now:
41
+ spec field order, method in bits 8-10 with dictionary bits
42
+ 11-15, Unix attribute 0o100644.
43
+ - RAR5 encrypted writing now persists the salt/IV as the file
44
+ encryption extra record (spec type 0x01) — archives it wrote
45
+ before could not be decrypted by anything (unrar reported "All
46
+ OK" only because no CRC was stored).
47
+ - RAR5 `:lzma`/`:lzss` compression advertised method bits for a
48
+ stream that nothing can decode (the encoder is not
49
+ official-RAR-compatible; `unrar` extracts empty files silently).
50
+ `Lzss.available?` is now honest, so writers fall back to STORE
51
+ with a warning; solid mode falls back to independent STORE
52
+ entries.
53
+ - ISO writing: directory records pointed every file at sector 0
54
+ (allocated extents were never copied onto the tree nodes); the
55
+ volume descriptor declared 100 sectors regardless of actual
56
+ size; `add_directory` entries were not marked as directories;
57
+ Rock Ridge and Joliet were advertised by default without being
58
+ implemented (the cloned SVD made 7-Zip read ASCII names as
59
+ UCS-2); `Reader#extract_all` looked entries up by bare name
60
+ instead of full path; `Formats::Iso.list` returned the Reader
61
+ instead of entries. Images now extract byte-identically through
62
+ 7zz, including nested directories.
63
+ - CLI: `omnizip <command> --help` failed with an arity error on
64
+ every command (Thor does not route `--help` on subcommands by
65
+ itself). A dispatch hook now prints per-command help.
66
+ - `Omnizip::Formats::Rar::Rar5::MainHeader/FileHeader` were
67
+ unreferenceable unless another file happened to load header.rb
68
+ first (missing autoload entries); three Rar5 autoloads pointed at
69
+ files that never existed.
70
+
71
+ ### Changed
72
+ - Docs: the OLE, RPM, GZIP, RAR5 and API-overview guides now use
73
+ the real APIs (`Formats::Ole.list/read/info`, `Formats::Rpm.*`,
74
+ `Gzip.compress_stream`, `Formats::Zip::Reader#read`, correct
75
+ error class names); fictional methods (`open_stream`, `root`,
76
+ `extract_to`, `extract_files`, `extract_payload`,
77
+ `Rar::Rar5::Reader`, scriptlets/signature accessors) removed.
78
+
79
+ ## [0.3.37] - 2026-08-28
80
+
10
81
  ### Added
11
82
  - `Omnizip::Archive` — the block-style facade the guides have
12
83
  documented since the docs were written (it never existed; ~50
@@ -143,15 +214,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
143
214
  been pinned to :copy) and replaced the match-encoding TODO with a
144
215
  wire-level round-trip verification of the distance-8/length
145
216
  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
217
  - The remaining lib/ TODO markers are resolved as documented design
156
218
  decisions rather than dangling work: RAR5 volume extras and
157
219
  EndHeader volume flags (no write-side reference exists;
@@ -165,6 +227,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
165
227
  the salt/IV header, so archives it writes cannot be decrypted;
166
228
  encryption stays read-verified only.
167
229
 
230
+ ### Verified
231
+ - LZIP interop with the real `lzip` CLI (1.26): our members pass
232
+ `lzip -t`/`-dc` and the CLI's members decode through
233
+ `Formats::Lzip` (spec skips where the CLI is absent).
234
+
168
235
  ## [0.3.28] - 2026-08-26
169
236
 
170
237
  ### Added
@@ -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
  ----
@@ -8,37 +8,13 @@ grand_parent: Guides
8
8
  [[ole-format]]
9
9
  == Purpose
10
10
 
11
- OLE (Object Linking and Embedding) Compound Document format is used by Microsoft for storing structured data in a single file. Common OLE files include:
12
- - MSI (Windows Installer packages)
13
- - DOC (legacy Word documents)
14
- - XLS (legacy Excel spreadsheets)
15
- - PPT (legacy PowerPoint presentations)
16
- - THM (Microsoft Theme files)
17
- - MSG (Outlook messages)
11
+ OLE (Object Linking and Embedding) Compound Document format is used
12
+ by Microsoft for storing structured data in a single file. Common OLE
13
+ files include MSI (Windows Installer packages), DOC (legacy Word),
14
+ XLS (legacy Excel), PPT (legacy PowerPoint) and THM (theme) files.
18
15
 
19
- OLE uses a FAT-like file system within a single file, supporting up to 4GB of structured data.
20
-
21
- == Key Characteristics
22
-
23
- [cols="1,3"]
24
- |===
25
- |Property |Value
26
-
27
- |Max Size
28
- |2GB (standard), 4GB (with 4KB sectors)
29
-
30
- |Sector Sizes
31
- |512 bytes (standard), 4KB (large)
32
-
33
- |Streams
34
- |Multiple streams and sub-storages
35
-
36
- |Encryption
37
- |Optional (document-specific)
38
-
39
- |Best For
40
- |Windows compound files, legacy Office documents, MSI installers
41
- |===
16
+ The API is stream-oriented: open the compound document, list
17
+ streams and storages by path, and read the streams you need.
42
18
 
43
19
  == Basic Usage
44
20
 
@@ -46,189 +22,104 @@ OLE uses a FAT-like file system within a single file, supporting up to 4GB of st
46
22
 
47
23
  [source,ruby]
48
24
  ----
49
- # Open OLE compound document
50
- ole = Omnizip::Ole.open('document.doc')
51
-
52
- # Access root storage
53
- root = ole.root
54
- puts "Root storage: #{root.name}"
55
-
56
- # List streams and storages
57
- root.each do |entry|
58
- puts "#{entry.name} (#{entry.size} bytes)" if entry.file?
59
- puts "#{entry.name}/" if entry.directory?
25
+ # Open OLE compound document (block form closes automatically)
26
+ Omnizip::Formats::Ole.open('document.doc') do |ole|
27
+ puts ole.list.inspect
60
28
  end
61
29
 
30
+ # Non-block form: close explicitly
31
+ ole = Omnizip::Formats::Ole.open('document.doc')
62
32
  ole.close
63
33
  ----
64
34
 
35
+ === List Entries
36
+
37
+ [source,ruby]
38
+ ----
39
+ # Top-level streams and storages
40
+ Omnizip::Formats::Ole.list('document.doc')
41
+
42
+ # Inside a nested storage
43
+ Omnizip::Formats::Ole.list('document.doc', '/ObjectPool')
44
+ ----
45
+
65
46
  === Read Stream Data
66
47
 
67
48
  [source,ruby]
68
49
  ----
69
- # Read a specific stream
70
- ole = Omnizip::Ole.open('document.doc')
71
-
72
- # Access \x01SummaryInformation stream (document metadata)
73
- summary = ole.open_stream("\x01SummaryInformation")
74
- if summary
75
- data = summary.read
76
- puts "Summary size: #{data.size} bytes"
77
- end
50
+ # \x01SummaryInformation holds document metadata
51
+ summary = Omnizip::Formats::Ole.read('document.doc',
52
+ "\x01SummaryInformation")
53
+ puts "Summary size: #{summary.bytesize} bytes"
78
54
 
79
- # Access WordDocument stream (main content)
80
- word_stream = ole.open_stream("WordDocument")
81
- if word_stream
82
- content = word_stream.read
83
- puts "Document content size: #{content.size} bytes"
84
- end
55
+ # WordDocument stream (main content)
56
+ content = Omnizip::Formats::Ole.read('document.doc', 'WordDocument')
57
+ puts "Document content size: #{content.bytesize} bytes"
85
58
 
86
- ole.close
59
+ # Write a stream to disk
60
+ File.binwrite('workbook.bin',
61
+ Omnizip::Formats::Ole.read('spreadsheet.xls', 'Workbook'))
87
62
  ----
88
63
 
89
- === Extract All Files
64
+ === Entry Introspection
90
65
 
91
66
  [source,ruby]
92
67
  ----
93
- # Extract all streams from OLE to files
94
- Omnizip::Ole.open('document.doc') do |ole|
95
- ole.extract_to('/output/directory/')
96
- end
68
+ info = Omnizip::Formats::Ole.info('document.doc', 'WordDocument')
69
+ puts info.inspect
97
70
 
98
- # Extract specific stream
99
- Omnizip::Ole.open('spreadsheet.xls') do |ole|
100
- workbook = ole.open_stream('Workbook')
101
- File.binwrite('workbook.bin', workbook.read)
102
- end
71
+ Omnizip::Formats::Ole.exist?('document.doc', 'WordDocument') # => true
72
+ Omnizip::Formats::Ole.file?('document.doc', 'WordDocument') # => true
73
+ Omnizip::Formats::Ole.directory?('document.doc', 'ObjectPool') # => true
103
74
  ----
104
75
 
105
76
  == MSI Package Inspection
106
77
 
107
- MSI files are OLE compound documents containing Windows Installer data:
78
+ MSI files are OLE compound documents; the installer tables live in
79
+ storages and streams you can walk with +list+:
108
80
 
109
81
  [source,ruby]
110
82
  ----
111
- # Open MSI package
112
- msi = Omnizip::Ole.open('installer.msi')
113
-
114
- # List MSI tables (storages)
115
- tables = msi.root.select { |e| e.directory? }
116
- puts "MSI Tables:"
117
- tables.each { |t| puts " #{t.name}" }
118
-
119
- # Access \x05SummaryInformation
120
- summary = msi.open_stream("\x05SummaryInformation")
121
- if summary
122
- puts "Summary information found"
83
+ Omnizip::Formats::Ole.open('installer.msi') do |msi|
84
+ msi.list('/').each { |entry| puts entry }
85
+ summary = msi.read("\x05SummaryInformation") rescue nil
86
+ tables = msi.read('_Tables') rescue nil
87
+ puts "Tables stream size: #{tables&.bytesize}" if tables
123
88
  end
124
-
125
- # Access _Tables stream (list of all tables)
126
- tables_stream = msi.open_stream("_Tables")
127
- if tables_stream
128
- puts "Tables stream size: #{tables_stream.size}"
129
- end
130
-
131
- msi.close
132
89
  ----
133
90
 
134
91
  == Storage Structure
135
92
 
136
93
  OLE files have a hierarchical structure like a file system:
137
94
 
138
- [source]
139
95
  ----
140
96
  Root Storage/
141
97
  ├── \x01SummaryInformation (document metadata)
142
- ├── \x01DocumentSummaryInformation
143
98
  ├── WordDocument (main content)
144
- ├── 1Table (data tables)
145
99
  ├── Data/
146
- ├── Mso1Table
147
- │ └── Mso0Table
100
+ └── Mso1Table
148
101
  └── ObjectPool/
149
- └── _1000000000/
150
- └── Ole10Native
151
- ----
152
-
153
- [source,ruby]
154
- ----
155
- # Navigate storage hierarchy
156
- ole = Omnizip::Ole.open('document.doc')
157
-
158
- # Access nested storage
159
- pool = ole.root.find("ObjectPool")
160
- if pool
161
- pool.each do |entry|
162
- puts "ObjectPool/#{entry.name}"
163
- end
164
- end
165
-
166
- ole.close
167
- ----
168
-
169
- == Summary Information
170
-
171
- OLE documents contain standard metadata streams:
172
-
173
- [source,ruby]
102
+ └── Ole10Native
174
103
  ----
175
- # Read summary information
176
- ole = Omnizip::Ole.open('document.doc')
177
-
178
- # Access summary stream
179
- summary_stream = ole.open_stream("\x01SummaryInformation")
180
- if summary_stream
181
- # Summary information is in a specific binary format
182
- # Omnizip provides helpers to decode it
183
- summary = ole.summary_information
184
- if summary
185
- puts "Title: #{summary.title}"
186
- puts "Author: #{summary.author}"
187
- puts "Subject: #{summary.subject}"
188
- puts "Keywords: #{summary.keywords}"
189
- puts "Created: #{summary.create_time}"
190
- puts "Modified: #{summary.last_save_time}"
191
- end
192
- end
193
104
 
194
- ole.close
195
- ----
105
+ Stream paths use `/` separators (e.g. +"ObjectPool/Ole10Native"+).
196
106
 
197
107
  == File Format Detection
198
108
 
199
- OLE files can be identified by their signature:
109
+ OLE files carry the signature +D0 CF 11 E0 A1 B1 1A E1+ in the first
110
+ 8 bytes:
200
111
 
201
112
  [source,ruby]
202
113
  ----
203
- # Check if file is OLE
204
- File.open('document.doc', 'rb') do |f|
205
- header = f.read(8)
206
- if Omnizip::Ole.ole_signature?(header)
207
- puts "This is an OLE compound document"
208
- end
209
- end
210
-
211
- # Get OLE file type
212
- ole = Omnizip::Ole.open('document.doc')
213
- case ole.file_type
214
- when :word_document
215
- puts "Microsoft Word document"
216
- when :excel_spreadsheet
217
- puts "Microsoft Excel spreadsheet"
218
- when :powerpoint
219
- puts "Microsoft PowerPoint presentation"
220
- when :msi_package
221
- puts "Windows Installer package"
222
- else
223
- puts "Unknown OLE type: #{ole.file_type}"
114
+ File.open('document.doc', "rb") do |f|
115
+ magic = f.read(8)
116
+ ole = magic == "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1".b
117
+ puts(ole ? "OLE compound document" : "not OLE")
224
118
  end
225
- ole.close
226
119
  ----
227
120
 
228
121
  == Supported File Types
229
122
 
230
- Omnizip supports reading the following OLE-based formats:
231
-
232
123
  [cols="2,1,1"]
233
124
  |===
234
125
  |Format |Extension |Read Support
@@ -249,67 +140,11 @@ Omnizip supports reading the following OLE-based formats:
249
140
  |.msi
250
141
  |Yes
251
142
 
252
- |Microsoft Theme
253
- |.thm
254
- |Yes
255
-
256
- |Outlook Message
257
- |.msg
258
- |Yes
259
-
260
143
  |Generic OLE
261
144
  |.ole
262
145
  |Yes
263
146
  |===
264
147
 
265
- == Low-Level Access
266
-
267
- For advanced use cases, you can access the OLE internals directly:
268
-
269
- [source,ruby]
270
- ----
271
- # Access BAT (Block Allocation Table)
272
- ole = Omnizip::Ole.open('document.doc')
273
-
274
- # Get sector size
275
- puts "Sector size: #{ole.sector_size}"
276
-
277
- # Access BAT entries
278
- bat = ole.bat
279
- puts "BAT entries: #{bat.size}"
280
-
281
- # Access SBAT (Small Block Allocation Table)
282
- sbat = ole.sbat
283
- puts "SBAT entries: #{sbat.size}" if sbat
284
-
285
- # Access directory entries
286
- ole.root.each do |entry|
287
- puts "#{entry.name}: starting_sector=#{entry.starting_sector}, size=#{entry.size}"
288
- end
289
-
290
- ole.close
291
- ----
292
-
293
- == Performance Considerations
294
-
295
- OLE files can be large. For efficient reading:
296
-
297
- [source,ruby]
298
- ----
299
- # Stream read large OLE files efficiently
300
- Omnizip::Ole.open('large.msi') do |ole|
301
- # Only read streams you need
302
- summary = ole.open_stream("\x05SummaryInformation")
303
- if summary
304
- # Read in chunks for large streams
305
- chunk_size = 65536
306
- while chunk = summary.read(chunk_size)
307
- process(chunk)
308
- end
309
- end
310
- end
311
- ----
312
-
313
148
  == See Also
314
149
 
315
150
  * link:seven-zip-format.html[7z Format] - Can contain OLE files
@@ -493,24 +493,24 @@ volumes.each { |v| puts " - #{File.basename(v)}" }
493
493
 
494
494
  [source,ruby]
495
495
  ----
496
- # Convert ZIP to RAR5
497
- def zip_to_rar5(zip_file, rar_file)
498
- zip = Omnizip::Formats::Zip::Reader.new(zip_file)
499
-
500
- Omnizip::Formats::Rar::Rar5::Writer.new(rar_file,
501
- compression: :lzma,
502
- level: 5
503
- ) do |writer|
504
- zip.each_entry do |entry|
505
- content = zip.read_entry(entry)
506
- writer.add_data(entry.name, content)
507
- end
496
+ # Convert ZIP to RAR5 (extract first: the RAR5 writer adds files
497
+ # from disk)
498
+ def zip_to_rar5(zip_file, rar_file, workdir)
499
+ require "fileutils"
500
+
501
+ # Reader#read parses the central directory and returns self
502
+ zip = Omnizip::Formats::Zip::Reader.new(zip_file).read
503
+ zip.extract_all(workdir)
504
+
505
+ writer = Omnizip::Formats::Rar::Rar5::Writer.new(rar_file,
506
+ compression: :store)
507
+ Dir.glob(File.join(workdir, "**", "*")).each do |path|
508
+ writer.add_file(path, path.delete_prefix("#{workdir}/")) if File.file?(path)
508
509
  end
509
-
510
- zip.close
510
+ writer.write
511
511
  end
512
512
 
513
- zip_to_rar5('archive.zip', 'archive.rar')
513
+ zip_to_rar5('archive.zip', 'archive.rar', 'workdir')
514
514
  ----
515
515
 
516
516
  === See Also