omnizip 0.3.42 → 0.3.43

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: 060755f6c69bca46bcb999d0d36471045ae3d13e260cfa3c73601a8d440b1634
4
- data.tar.gz: b524b77c579c824009821ca5d1bdb47327cfc55f3ebd1dab4ea8574d1ec3c631
3
+ metadata.gz: 436437db6697f7363305eda4e6a3e2b78b922aa12b3c43e29756efe187310b60
4
+ data.tar.gz: 6e0b17e143c50beae378cdb5b7902e773d888bca14183fcf83e61dfeabe56a5e
5
5
  SHA512:
6
- metadata.gz: 237f14e0c479844a473d1830e956882a0d250f6c733522bcd9f3d36b7ba10b474b4564902791aecd52948ad330b82bb0bb7e61dd97db607bf3dcd85ab3dc847b
7
- data.tar.gz: cfe4637c8ad1223ccb8b9735fc1494d61937b636f3d79dd7fbd2c1f59f6f35eb38686152ee56732f0cb29cfbf3c12f7ddfb6dae5bd6c146b8d833fca5315a00a
6
+ metadata.gz: f1e20ae301093cacb8c582e1b554a678c72cefa6bf21095d129bebc14b8c4095a68a87da653ef5ea16cb65c00de9edd6ae7b0d92191e49c25e05438ceba264ab
7
+ data.tar.gz: a6ad938ab6bdfd75586bcef013ba056cc523681ef34449d03bc44daf18a591e6485b292b7c62bd17c72c13c20c7ff8c45caf4faf6735d5cff79a0c2f4a5a010b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.42] - 2026-08-29
11
+
12
+ ### Changed
13
+ - `Formats::Zip::Reader` is usable immediately: entries parse on
14
+ first use, so `extract_all`/listing no longer silently do nothing
15
+ when `#read` was not called first. `#read` remains for eager
16
+ loading.
17
+ - `Temp::ArchiveHelper` writes zips through the native
18
+ `Formats::Zip::Writer` instead of the rubyzip-compat
19
+ `Omnizip::Zip::File` seam (the Metadata subsystem remains on
20
+ compat deliberately as an in-place central-directory editor).
21
+ - The RAR extraction spill cache installs an `at_exit` sweeper on
22
+ first use, reclaiming spill directories at normal process exit.
23
+ - Module docs tell the truth: `Formats::Rar` claimed read-only with
24
+ external tools required; `Formats::Iso` claimed read-only while
25
+ carrying the verified writer. New `CONTEXT.md` records the domain
26
+ glossary (primary parser vs adapters, native vs external decode,
27
+ interop-verified vs Omnizip-internal codecs, handler routes, the
28
+ compat seam, bridges).
29
+
10
30
  ## [0.3.41] - 2026-08-29
11
31
 
12
32
  ### Changed
data/CONTEXT.md CHANGED
@@ -19,7 +19,11 @@ here.
19
19
  extract/list/read. `.rar`, `.cpio`, `.iso` are read-only routes;
20
20
  creating them raises truthfully.
21
21
 
22
- ## Reading and decoding
22
+ ## Reading and decode
23
+
24
+ - **Reader invariant — always usable** — every Reader
25
+ (`Formats::Zip`, `Formats::SevenZip`, `Formats::Rar`) parses on
26
+ first use; calling `#open` first is never required, only eager.
23
27
 
24
28
  - **Native decode** — decompression implemented in Ruby inside
25
29
  omnizip. RAR native decode is **CRC-gated**: the stored header CRC
@@ -12,7 +12,7 @@ module Omnizip
12
12
  class Reader
13
13
  include Omnizip::Formats::Rar::Constants
14
14
 
15
- attr_reader :file_path, :header, :entries, :archive_info,
15
+ attr_reader :file_path, :header, :archive_info,
16
16
  :volume_manager
17
17
 
18
18
  # Initialize reader with file path
@@ -25,6 +25,7 @@ module Omnizip
25
25
  @archive_info = Models::RarArchive.new(file_path)
26
26
  @volume_manager = VolumeManager.new(file_path)
27
27
  @use_native = true # Prefer native decompression
28
+ @opened = false
28
29
  end
29
30
 
30
31
  # Open and parse RAR archive
@@ -34,14 +35,24 @@ module Omnizip
34
35
  File.open(@file_path, "rb") do |io|
35
36
  parse_archive(io)
36
37
  end
38
+ @opened = true
37
39
  self
38
40
  end
39
41
 
42
+ # All entries, parsing on first use — a Reader is always
43
+ # usable; #open remains for eager loading
44
+ #
45
+ # @return [Array<Models::RarEntry>] File entries
46
+ def entries
47
+ ensure_open
48
+ @entries
49
+ end
50
+
40
51
  # List all files in archive
41
52
  #
42
53
  # @return [Array<Models::RarEntry>] File entries
43
54
  def list_files
44
- @entries
55
+ entries
45
56
  end
46
57
 
47
58
  # Extract file to output path
@@ -51,6 +62,7 @@ module Omnizip
51
62
  # @param password [String, nil] Optional password
52
63
  # @raise [RuntimeError] if entry not found or extraction fails
53
64
  def extract_entry(entry_name, output_path, password: nil)
65
+ ensure_open
54
66
  entry = @entries.find { |e| e.name == entry_name }
55
67
  raise "Entry not found: #{entry_name}" unless entry
56
68
 
@@ -79,6 +91,7 @@ module Omnizip
79
91
  # @param password [String, nil] Optional password
80
92
  # @raise [RuntimeError] on extraction error
81
93
  def extract_all(output_dir, password: nil)
94
+ ensure_open
82
95
  FileUtils.mkdir_p(output_dir)
83
96
 
84
97
  # Use decompressor to extract all
@@ -126,6 +139,11 @@ module Omnizip
126
139
 
127
140
  private
128
141
 
142
+ # Parse the archive once, on demand
143
+ def ensure_open
144
+ open unless @opened
145
+ end
146
+
129
147
  # Parse RAR archive structure
130
148
  #
131
149
  # @param io [IO] Input stream
@@ -28,6 +28,7 @@ module Omnizip
28
28
  @offset = options[:offset] || 0
29
29
  @solid_cache = {}
30
30
  @solid_cache_extracted = {}
31
+ @opened = false
31
32
  end
32
33
 
33
34
  # Open and parse .7z archive
@@ -79,10 +80,12 @@ module Omnizip
79
80
  @split_reader&.volumes || [@file_path]
80
81
  end
81
82
 
82
- # List all files in archive
83
+ # List all files in archive, parsing on first use — a Reader
84
+ # is always usable; #open remains for eager loading
83
85
  #
84
86
  # @return [Array<Models::FileEntry>] File entries
85
87
  def list_files
88
+ ensure_open
86
89
  @entries.reject { |e| e.name.nil? || e.name.empty? }
87
90
  end
88
91
 
@@ -92,6 +95,7 @@ module Omnizip
92
95
  # @param output_path [String] Destination path
93
96
  # @raise [RuntimeError] if entry not found or extraction fails
94
97
  def extract_entry(entry_name, output_path)
98
+ ensure_open
95
99
  # Delegate to split reader if available
96
100
  if @split_reader
97
101
  @split_reader.extract_entry(entry_name, output_path)
@@ -132,6 +136,7 @@ module Omnizip
132
136
  # @param output_dir [String] Destination directory
133
137
  # @raise [RuntimeError] on extraction error
134
138
  def extract_all(output_dir)
139
+ ensure_open
135
140
  FileUtils.mkdir_p(output_dir)
136
141
 
137
142
  real_out = File.realpath(output_dir)
@@ -187,6 +192,11 @@ module Omnizip
187
192
 
188
193
  private
189
194
 
195
+ # Parse the archive once, on demand
196
+ def ensure_open
197
+ open unless @opened
198
+ end
199
+
190
200
  # Parse .7z archive structure
191
201
  #
192
202
  # @param io [io] Input stream
@@ -236,7 +246,16 @@ module Omnizip
236
246
 
237
247
  # Parse metadata
238
248
  parser = Parser.new(next_header_data)
239
- @stream_info, @entries = parse_metadata(parser)
249
+ begin
250
+ @stream_info, @entries = parse_metadata(parser)
251
+ rescue StandardError => e
252
+ if @headers_decrypted
253
+ raise "Failed to decrypt headers: incorrect password " \
254
+ "or corrupted data (#{e.message})"
255
+ end
256
+
257
+ raise
258
+ end
240
259
 
241
260
  # Map entries to their folders/streams
242
261
  map_entries_to_streams
@@ -257,11 +276,16 @@ module Omnizip
257
276
 
258
277
  # Decrypt using password
259
278
  encryptor = HeaderEncryptor.new(@password)
260
- encryptor.decrypt(
279
+ decrypted = encryptor.decrypt(
261
280
  @encrypted_header.encrypted_data,
262
281
  @encrypted_header.salt,
263
282
  @encrypted_header.iv,
264
283
  )
284
+ # AES-CBC 'succeeds' with any key and yields garbage; wrong
285
+ # passwords surface as later header-parse failures, so mark
286
+ # the archive and translate those into the password error
287
+ @headers_decrypted = true
288
+ decrypted
265
289
  rescue OpenSSL::Cipher::CipherError => e
266
290
  raise "Failed to decrypt headers: incorrect password (#{e.message})"
267
291
  end
@@ -393,6 +417,20 @@ module Omnizip
393
417
  entry.folder_index = folder_idx
394
418
  entry.file_index = i
395
419
  entry.size = @stream_info.unpack_sizes[stream_idx] if @stream_info.unpack_sizes[stream_idx]
420
+
421
+ # Single-stream folders own their packed bytes outright,
422
+ # so the entry's compressed size is exact. Solid folders
423
+ # share packed bytes across streams — leave nil rather
424
+ # than fabricate a split.
425
+ num_in_folder = @stream_info.num_unpack_streams_in_folders[folder_idx]
426
+ if num_in_folder == 1
427
+ folder = @stream_info.folders[folder_idx]
428
+ packed = folder.pack_stream_indices.sum do |pi|
429
+ @stream_info.pack_sizes[pi] || 0
430
+ end
431
+ entry.compressed_size = packed
432
+ end
433
+
396
434
  stream_idx += 1
397
435
  end
398
436
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.42"
4
+ VERSION = "0.3.43"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.42
4
+ version: 0.3.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.