omnizip 0.3.51 → 0.3.53

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 +56 -0
  3. data/lib/omnizip/archive_handlers/seven_zip_handler.rb +1 -1
  4. data/lib/omnizip/archive_handlers/zip_handler.rb +1 -1
  5. data/lib/omnizip/buffer/memory_extractor.rb +3 -16
  6. data/lib/omnizip/buffer/seven_zip_bridge.rb +13 -0
  7. data/lib/omnizip/buffer.rb +23 -21
  8. data/lib/omnizip/checksums/verifier.rb +2 -2
  9. data/lib/omnizip/chunked.rb +14 -21
  10. data/lib/omnizip/cli/output_formatter.rb +16 -8
  11. data/lib/omnizip/cli.rb +1 -7
  12. data/lib/omnizip/commands/archive_create_command.rb +1 -10
  13. data/lib/omnizip/commands/archive_extract_command.rb +4 -13
  14. data/lib/omnizip/commands/archive_list_command.rb +1 -18
  15. data/lib/omnizip/commands/metadata_command.rb +21 -26
  16. data/lib/omnizip/commands/parity_create_command.rb +1 -7
  17. data/lib/omnizip/extraction/selective_extractor.rb +16 -4
  18. data/lib/omnizip/formats/cpio/reader.rb +1 -1
  19. data/lib/omnizip/formats/iso/reader.rb +3 -3
  20. data/lib/omnizip/formats/iso/volume_descriptor.rb +2 -2
  21. data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +0 -20
  22. data/lib/omnizip/formats/rar/decompressor.rb +7 -7
  23. data/lib/omnizip/formats/rar/reader.rb +2 -2
  24. data/lib/omnizip/formats/rpm.rb +4 -4
  25. data/lib/omnizip/formats/seven_zip/reader.rb +1 -1
  26. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +1 -1
  27. data/lib/omnizip/formats/xar/reader.rb +2 -2
  28. data/lib/omnizip/formats/zip/reader.rb +96 -8
  29. data/lib/omnizip/models/conversion_result.rb +76 -70
  30. data/lib/omnizip/models/filter_config.rb +34 -44
  31. data/lib/omnizip/models/match_result.rb +35 -18
  32. data/lib/omnizip/models/optimization_suggestion.rb +45 -41
  33. data/lib/omnizip/models/performance_result.rb +53 -47
  34. data/lib/omnizip/models/profile_report.rb +70 -36
  35. data/lib/omnizip/parallel/parallel_extractor.rb +2 -2
  36. data/lib/omnizip/parallel/worker_pool.rb +3 -3
  37. data/lib/omnizip/parity/par2_creator.rb +0 -34
  38. data/lib/omnizip/parity/par2_verifier.rb +5 -0
  39. data/lib/omnizip/profiler/report_generator.rb +2 -13
  40. data/lib/omnizip/profiler.rb +1 -12
  41. data/lib/omnizip/temp/safe_extract.rb +3 -43
  42. data/lib/omnizip/version.rb +1 -1
  43. data/lib/omnizip/zip/file.rb +1 -1
  44. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b268c5b315ddcfff3347f6843e30e7ca180b1b3bae14b21a7a721bd2ccf3f6e
4
- data.tar.gz: 2d150420264453af6e1ba5114b32a31b1a67c12a020f41435a037747dec2bfde
3
+ metadata.gz: a7e72a57eaeb6e597e40020de67aa0242da65c6e6e8ceaa933d779d926e55666
4
+ data.tar.gz: 105cf4f238610f518462c115d398403fef5daca9b2994788361b027c7c5ec648
5
5
  SHA512:
6
- metadata.gz: '053812e04bf42b6fe5335d57a5183c90b19a3ed8b414ee57ac9a83831a04f15e7225ecc79d29bc42e78e692bf7fe6f60b88ba234a3fc92eb0e781e7a357acd14'
7
- data.tar.gz: dd2e32bd6ea8732be35a2f6c7325ed23b922860c051caf7666df93edd7f7da1342bdc1f6c9928346a404a7a1c707c509bd74d1b281ddd07cdc4a591c67e8a05d
6
+ metadata.gz: a5301b14047d1150885624527d03176c61a979464b7a16329a47449e13fe05679c1e5ce11a650bfd29101aaaf502b5ab81513558fc080dca85554266ae30f8dc
7
+ data.tar.gz: a44ffd04cc02fa03e2b23a330da4f6612d268bfd350d9b9672e42d7a107dcad65201ed9707651b3533fdd790a5841a5ff9297d6f3a8e937b51e0f1614d756c55
data/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.52] - 2026-09-01
11
+
12
+ ### Changed
13
+ - The extract command's pattern path opens archives through the
14
+ Archive facade instead of a hand-rolled extension `case` that
15
+ misrouted `.cpio`/`.iso`/`.rpm` files into the 7z reader — pattern
16
+ extraction now works for every routed format. The metadata
17
+ command's 7z view lists through the handler registry (the detail
18
+ contract gains `:crc`), and SafeExtract extracts through
19
+ `extract_archive`, so every routed format gets rollback and
20
+ verification instead of ZIP-only with a `NotImplementedError`
21
+ for 7z. SelectiveExtractor understands both entry contracts
22
+ (legacy `entry_name` and reader-session `name`) and skips
23
+ directory entries when extracting.
24
+ - `CliOutputFormatter.format_size` is now the single byte formatter;
25
+ the nine private `format_bytes`/`format_size` copies (four
26
+ different shapes, two unit ceilings) become delegators.
27
+
28
+ ### Fixed
29
+ - The long-deferred parity spec flake: `Par2Verifier` crashed five
30
+ frames deep on a nil PAR2 path (`File.exist?(nil)` → TypeError)
31
+ where the repairer already raised a clean ArgumentError; the
32
+ verifier now mirrors it, and the spec's `.verify` group gains the
33
+ nil-index fallback `.repair` already had. The dead shadowed
34
+ `analyze_file` definition in `Par2Creator` (silently overridden
35
+ by a second definition with a divergent file_id format) is
36
+ deleted. The tautological "LZMA2 is implicit" xz assertion became
37
+ a real block-header parse asserting the explicit 0x21 filter ID.
38
+
39
+ ## [0.3.51] - 2026-09-01
40
+
41
+ ### Changed
42
+ - Profile resolution lives in one place: `Omnizip::Profile` gains
43
+ `resolve`, `apply`, and `first_file_among`, and the convenience
44
+ methods plus the `archive create` command collapse onto them. The
45
+ two prior copies had drifted — the command only accepted `"auto"`
46
+ strings and could not pass a `CompressionProfile` through.
47
+ - Both direct converter strategies (ZIP↔7z) now extract through
48
+ `Omnizip.extract_archive` and write through a shared
49
+ `ConversionStrategy#repack_tree` on the Archive facade — they were
50
+ the last library code instantiating format readers/writers
51
+ directly, and the 7z→ZIP path required `#open` against the
52
+ reader-always-usable invariant.
53
+ - `create_rar` routes through the RAR handler (unrar-verified) with
54
+ the generic writer block interface; its stale "requires RAR
55
+ license" docstring is gone. `decompress_file`'s three-arm case
56
+ with magic `:xz`/`:zstandard` markers becomes a
57
+ `SINGLE_FILE_DECOMPRESSORS` table mirroring the compressor table.
58
+
59
+ ### Fixed
60
+ - macOS CI failed at setup from 2026-09-01: Homebrew disabled the
61
+ `rar` cask (Gatekeeper check), breaking `brew install rar`. The
62
+ workflow now installs it separately and non-fatally, like the
63
+ Windows branch already did for winrar; RAR interop specs skip
64
+ when unrar is absent.
65
+
10
66
  ## [0.3.50] - 2026-08-31
11
67
 
12
68
  ### Added
@@ -31,7 +31,7 @@ module Omnizip
31
31
  entries.map do |e|
32
32
  { name: e.name, size: e.size, directory: e.is_dir,
33
33
  compressed_size: (e.compressed_size if e.compressed_size&.positive?),
34
- mtime: e.mtime }
34
+ mtime: e.mtime, crc: e.crc }
35
35
  end
36
36
  else
37
37
  entries.map(&:name)
@@ -38,7 +38,7 @@ module Omnizip
38
38
  extracted = []
39
39
  reader.entries.each do |entry|
40
40
  dest_path = ::File.join(output_dir, entry.filename)
41
- raise "File exists: #{dest_path}" if ::File.exist?(dest_path)
41
+ raise Errno::EEXIST, "File exists: #{dest_path}" if ::File.exist?(dest_path)
42
42
 
43
43
  reader.extract_entry(entry, output_dir)
44
44
  extracted << dest_path
@@ -151,24 +151,12 @@ module Omnizip
151
151
 
152
152
  private
153
153
 
154
- # Detect archive format from magic bytes
154
+ # Delegate to the shared buffer-side sniffer
155
155
  #
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).to_s.b
160
- @buffer.rewind
161
-
162
- case magic
163
- when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
164
- # ZIP signatures: local file header, EOCD, data descriptor
165
- :zip
166
- when "7z\xBC\xAF".b
167
- :seven_zip
168
- else
169
- raise Omnizip::FormatError,
170
- "Unknown archive format (magic: #{magic.inspect})"
171
- end
159
+ Omnizip::Buffer.detect_format(@buffer)
172
160
  end
173
161
 
174
162
  # Extract all entries from ZIP
@@ -207,8 +195,7 @@ module Omnizip
207
195
  def extract_entry_seven_zip(name)
208
196
  @buffer.rewind
209
197
  SevenZipBridge.open(@buffer) do |archive|
210
- entry = archive.raw_entries.find { |e| e.name == name }
211
- archive.extract_all_to_memory[name] if entry
198
+ archive.read_entry(name)
212
199
  end
213
200
  end
214
201
 
@@ -112,6 +112,19 @@ module Omnizip
112
112
  @reader.list_files
113
113
  end
114
114
 
115
+ # Content of a single named file entry, or nil when absent
116
+ # (directories carry no content). Extracts just that entry —
117
+ # not the whole archive.
118
+ #
119
+ # @param name [String] Entry name
120
+ # @return [String, nil]
121
+ def read_entry(name)
122
+ entry = raw_entries.find { |e| e.name == name && !e.is_dir }
123
+ return nil unless entry
124
+
125
+ Entry.new(entry, @reader, @tmp).read
126
+ end
127
+
115
128
  # Entry wrapper matching MemoryArchive::Entry's surface
116
129
  class Entry
117
130
  attr_reader :name, :size
@@ -107,27 +107,9 @@ module Omnizip
107
107
  extractor.extract_all
108
108
  end
109
109
 
110
- # Create archive from Hash of filename => content
111
- #
112
- # @param hash [Hash<String, String>] Filename => content mapping
113
- # @param format [Symbol] Archive format
114
- # @param options [Hash] Format-specific options
115
- # @return [StringIO] Complete archive in memory
116
- #
117
- # @example Create from Hash
118
- # data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
119
- # zip = Omnizip::Buffer.create_from_hash(data, :zip)
120
- def create_from_hash(hash, format = :zip, **options)
121
- create(format, **options) do |archive|
122
- hash.each do |name, content|
123
- archive.add(name, content)
124
- end
125
- end
126
- end
127
-
128
- private
129
-
130
- # Detect archive format from magic bytes
110
+ # Detect archive format from magic bytes. THE buffer-side
111
+ # sniffer — in-memory data has no extension to route by, so
112
+ # every consumer delegates here.
131
113
  #
132
114
  # @param buffer [StringIO] Buffer containing archive data
133
115
  # @return [Symbol] Detected format
@@ -148,6 +130,26 @@ module Omnizip
148
130
  end
149
131
  end
150
132
 
133
+ # Create archive from Hash of filename => content
134
+ #
135
+ # @param hash [Hash<String, String>] Filename => content mapping
136
+ # @param format [Symbol] Archive format
137
+ # @param options [Hash] Format-specific options
138
+ # @return [StringIO] Complete archive in memory
139
+ #
140
+ # @example Create from Hash
141
+ # data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
142
+ # zip = Omnizip::Buffer.create_from_hash(data, :zip)
143
+ def create_from_hash(hash, format = :zip, **options)
144
+ create(format, **options) do |archive|
145
+ hash.each do |name, content|
146
+ archive.add(name, content)
147
+ end
148
+ end
149
+ end
150
+
151
+ private
152
+
151
153
  # Create ZIP archive in buffer
152
154
  #
153
155
  # @param buffer [StringIO] Buffer to write to
@@ -94,7 +94,7 @@ module Omnizip
94
94
  when CHECK_SHA256
95
95
  verify_sha256(data, expected)
96
96
  else
97
- raise "Unknown check type: #{check_type}"
97
+ raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
98
98
  end
99
99
  end
100
100
 
@@ -122,7 +122,7 @@ module Omnizip
122
122
  when CHECK_SHA256
123
123
  Digest::SHA256.digest(data)
124
124
  else
125
- raise "Unknown check type: #{check_type}"
125
+ raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
126
126
  end
127
127
  end
128
128
  end
@@ -95,30 +95,23 @@ module Omnizip
95
95
  "Input archive not found: #{input}"
96
96
  end
97
97
 
98
- writer = Writer.new(output, chunk_size: chunk_size)
99
- processed = 0
100
-
101
- Omnizip::Zip::File.open(input) do |zip|
102
- entry = zip.entries.first
103
- total_size = entry.size
104
-
105
- # Read the full entry content
106
- content = zip.get_input_stream(entry)
98
+ reader = Omnizip::Formats::Zip::Reader.new(input)
99
+ entry = reader.entries.first
100
+ raise Omnizip::Error, "No entries to decompress in #{input}" unless entry
107
101
 
108
- # Write in chunks
109
- offset = 0
110
- while offset < content.bytesize
111
- chunk = content.byteslice(offset, chunk_size) || ""
112
- break if chunk.empty?
102
+ total_size = entry.uncompressed_size.to_i
103
+ processed = 0
104
+ writer = Writer.new(output, chunk_size: chunk_size)
113
105
 
114
- writer.write_chunk(chunk)
115
- processed += chunk.bytesize
116
- offset += chunk.bytesize
106
+ # Streams entry-by-entry in bounded memory — the whole entry
107
+ # is never held at once
108
+ reader.read_entry_stream(entry.filename, chunk_size: chunk_size) do |chunk|
109
+ writer.write_chunk(chunk)
110
+ processed += chunk.bytesize
117
111
 
118
- if progress
119
- percentage = (processed.to_f / total_size * 100).round(2)
120
- progress.call(processed, total_size, percentage)
121
- end
112
+ if progress
113
+ percentage = (processed.to_f / total_size * 100).round(2)
114
+ progress.call(processed, total_size, percentage)
122
115
  end
123
116
  end
124
117
 
@@ -44,19 +44,27 @@ module Omnizip
44
44
  ].join("\n")
45
45
  end
46
46
 
47
- # Format file size in human-readable format.
47
+ # Format file size in human-readable format. This is THE byte
48
+ # formatter — commands, the CLI, and display-oriented models
49
+ # delegate here instead of carrying private copies.
48
50
  #
49
- # @param bytes [Integer] Size in bytes
50
- # @return [String] Formatted size
51
+ # @param bytes [Integer, nil] Size in bytes
52
+ # @return [String] Formatted size ("512 B", "1.5 KB", "2.0 MB")
51
53
  def format_size(bytes)
54
+ return "0 B" if bytes.nil? || bytes.zero?
55
+
52
56
  units = %w[B KB MB GB TB]
53
- return "0 B" if bytes.zero?
57
+ size = bytes.to_f
58
+ unit_index = 0
59
+
60
+ while size >= 1024.0 && unit_index < units.size - 1
61
+ size /= 1024.0
62
+ unit_index += 1
63
+ end
54
64
 
55
- exp = (Math.log(bytes) / Math.log(1024)).floor
56
- exp = [exp, units.length - 1].min
65
+ return format("%d B", bytes) if unit_index.zero?
57
66
 
58
- size = (bytes.to_f / (1024**exp)).round(2)
59
- "#{size} #{units[exp]}"
67
+ format("%.1f %s", size, units[unit_index])
60
68
  end
61
69
 
62
70
  # Format error message for display.
data/lib/omnizip/cli.rb CHANGED
@@ -60,13 +60,7 @@ module Omnizip
60
60
  # @param bytes [Integer] the byte count
61
61
  # @return [String] human-readable size (e.g. "1.5 MB")
62
62
  def format_bytes(bytes)
63
- return "0 B" if bytes.zero?
64
-
65
- units = %w[B KB MB GB TB]
66
- exp = (Math.log(bytes) / Math.log(1024)).to_i
67
- exp = [exp, units.size - 1].min
68
-
69
- "%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
63
+ Omnizip::CliOutputFormatter.format_size(bytes)
70
64
  end
71
65
  end
72
66
 
@@ -201,16 +201,7 @@ module Omnizip
201
201
  end
202
202
 
203
203
  def format_bytes(bytes)
204
- units = %w[B KB MB GB]
205
- size = bytes.to_f
206
- unit_idx = 0
207
-
208
- while size >= 1024 && unit_idx < units.length - 1
209
- size /= 1024.0
210
- unit_idx += 1
211
- end
212
-
213
- format("%.2f %s", size, units[unit_idx])
204
+ Omnizip::CliOutputFormatter.format_size(bytes)
214
205
  end
215
206
 
216
207
  def parse_volume_size(size_str)
@@ -135,17 +135,10 @@ module Omnizip
135
135
  end
136
136
 
137
137
  def extract_with_patterns(archive_file, output_dir, verbose)
138
- # Determine archive type and open appropriately
139
- archive = case File.extname(archive_file).downcase
140
- when ".zip"
141
- Omnizip::Zip::File.open(archive_file)
142
- when ".rar"
143
- Formats::Rar::Reader.new(archive_file).open
144
- when ".tar"
145
- Formats::Tar::Reader.new(archive_file).read
146
- else
147
- Formats::SevenZip::Reader.new(archive_file).open
148
- end
138
+ # Route through the facade: the extension routes pick each
139
+ # format's reader, and the reader session holds no open
140
+ # resources to close
141
+ archive = Omnizip::Archive.open(archive_file)
149
142
 
150
143
  # Build filter chain
151
144
  filter = Extraction::FilterChain.new
@@ -193,8 +186,6 @@ module Omnizip
193
186
  end
194
187
 
195
188
  extracted.size
196
- ensure
197
- archive.close if archive.is_a?(Omnizip::Zip::File)
198
189
  end
199
190
 
200
191
  def extract_gzip(archive_file, output_dir, verbose)
@@ -157,24 +157,7 @@ module Omnizip
157
157
  end
158
158
 
159
159
  def format_bytes(bytes)
160
- return "0 B" if bytes.zero?
161
-
162
- units = %w[B KB MB GB]
163
- size = bytes.to_f
164
- unit_idx = 0
165
-
166
- while size >= 1024 && unit_idx < units.length - 1
167
- size /= 1024.0
168
- unit_idx += 1
169
- end
170
-
171
- if size < 10
172
- format("%.2f %s", size, units[unit_idx])
173
- elsif size < 100
174
- format("%.1f %s", size, units[unit_idx])
175
- else
176
- format("%.0f %s", size, units[unit_idx])
177
- end
160
+ Omnizip::CliOutputFormatter.format_size(bytes)
178
161
  end
179
162
 
180
163
  # Helper methods to handle different entry types. Each delegates
@@ -54,7 +54,8 @@ module Omnizip
54
54
  %i[comment set_mtime chmod set_attribute].none? { |key| options[key] }
55
55
  end
56
56
 
57
- # Read-only view for 7z archives; the edit path is zip-only
57
+ # Read-only view for 7z archives through the handler registry;
58
+ # the edit path is zip-only
58
59
  def show_seven_zip_metadata(archive_path, pattern)
59
60
  if options[:comment] || options[:set_mtime] ||
60
61
  options[:chmod] || options[:set_attribute]
@@ -62,37 +63,37 @@ module Omnizip
62
63
  "Metadata editing is only supported for ZIP archives"
63
64
  end
64
65
 
65
- reader = Omnizip::Formats::SevenZip::Reader.new(archive_path)
66
- reader.open
66
+ entries = Omnizip.list_archive(archive_path, details: true)
67
67
 
68
68
  if pattern
69
- entries = reader.list_files.select do |e|
70
- File.fnmatch(pattern, e.name)
69
+ entries = entries.select do |entry|
70
+ File.fnmatch(pattern, entry[:name])
71
71
  end
72
72
  if entries.empty?
73
73
  warn "No entries match pattern: #{pattern}"
74
74
  return
75
75
  end
76
76
 
77
- entries.each do |entry|
78
- puts "Entry: #{entry.name}"
79
- puts " Type: #{entry.is_dir ? 'Directory' : 'File'}"
80
- puts " Size: #{format_size(entry.size)}"
81
- # Per-entry packed sizes are not tracked by the parser
82
- if entry.compressed_size&.positive?
83
- puts " Compressed: #{format_size(entry.compressed_size)}"
84
- end
85
- puts " Modified: #{entry.mtime}" if entry.mtime
86
- puts " CRC32: 0x#{entry.crc.to_s(16).upcase}" if entry.crc
87
- end
77
+ entries.each { |entry| show_seven_zip_entry(entry) }
88
78
  else
89
- entries = reader.list_files
90
- files = entries.reject(&:is_dir)
79
+ files = entries.reject { |entry| entry[:directory] }
91
80
  puts "Archive: #{archive_path}"
92
81
  puts "Entries: #{entries.size} " \
93
82
  "(#{files.size} files, #{entries.size - files.size} dirs)"
94
- puts "Total size: #{format_size(files.sum(&:size))}"
83
+ puts "Total size: #{format_size(files.sum { |entry| entry[:size] })}"
84
+ end
85
+ end
86
+
87
+ def show_seven_zip_entry(entry)
88
+ puts "Entry: #{entry[:name]}"
89
+ puts " Type: #{entry[:directory] ? 'Directory' : 'File'}"
90
+ puts " Size: #{format_size(entry[:size])}"
91
+ # Per-entry packed sizes are not tracked by the parser
92
+ if entry[:compressed_size]&.positive?
93
+ puts " Compressed: #{format_size(entry[:compressed_size])}"
95
94
  end
95
+ puts " Modified: #{entry[:mtime]}" if entry[:mtime]
96
+ puts " CRC32: 0x#{entry[:crc].to_s(16).upcase}" if entry[:crc]
96
97
  end
97
98
 
98
99
  def show_metadata(archive, pattern)
@@ -199,13 +200,7 @@ module Omnizip
199
200
  end
200
201
 
201
202
  def format_size(bytes)
202
- return "0 B" if bytes.zero?
203
-
204
- units = %w[B KB MB GB TB]
205
- exp = (Math.log(bytes) / Math.log(1024)).to_i
206
- exp = [exp, units.size - 1].min
207
-
208
- "%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
203
+ Omnizip::CliOutputFormatter.format_size(bytes)
209
204
  end
210
205
  end
211
206
  end
@@ -107,13 +107,7 @@ module Omnizip
107
107
  # @param bytes [Integer] Size in bytes
108
108
  # @return [String] Formatted size
109
109
  def format_size(bytes)
110
- if bytes < 1024
111
- "#{bytes} B"
112
- elsif bytes < 1024 * 1024
113
- "#{(bytes / 1024.0).round(1)} KB"
114
- else
115
- "#{(bytes / (1024.0 * 1024)).round(1)} MB"
116
- end
110
+ Omnizip::CliOutputFormatter.format_size(bytes)
117
111
  end
118
112
  end
119
113
  end
@@ -33,7 +33,10 @@ module Omnizip
33
33
  FileUtils.mkdir_p(dest)
34
34
  extracted = []
35
35
 
36
- entries_to_extract = list_matches
36
+ # Directory entries carry no content to extract
37
+ entries_to_extract = list_matches.reject do |entry|
38
+ entry_filename(entry).end_with?("/")
39
+ end
37
40
  total = entries_to_extract.size
38
41
  current = 0
39
42
 
@@ -162,7 +165,7 @@ module Omnizip
162
165
  entry.get_input_stream.read
163
166
  # allowed: some archives read entry content from the archive
164
167
  elsif @archive.respond_to?(:read)
165
- @archive.read(entry)
168
+ @archive.read(entry_filename(entry))
166
169
  else
167
170
  raise Error, "Cannot read entry content"
168
171
  end
@@ -191,10 +194,19 @@ module Omnizip
191
194
 
192
195
  # Get filename from entry
193
196
  #
194
- # @param entry [#entry_name] Entry (Omnizip::Entry or compatible)
197
+ # @param entry [#entry_name, #name] Entry (legacy Omnizip::Entry,
198
+ # Archive::Entry from a reader session, or compatible)
195
199
  # @return [String] Filename
196
200
  def entry_filename(entry)
197
- entry.entry_name || entry.to_s
201
+ # allowed: entries come from a caller-supplied archive
202
+ if entry.respond_to?(:entry_name)
203
+ entry.entry_name
204
+ # allowed: reader-session entries expose name, not entry_name
205
+ elsif entry.respond_to?(:name)
206
+ entry.name
207
+ else
208
+ entry.to_s
209
+ end
198
210
  end
199
211
 
200
212
  # Update progress tracker
@@ -64,7 +64,7 @@ module Omnizip
64
64
  # @raise [RuntimeError] if entry not found
65
65
  def extract_entry(entry_name, output_path)
66
66
  entry = @entries.find { |e| e.name == entry_name }
67
- raise "Entry not found: #{entry_name}" unless entry
67
+ raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
68
68
 
69
69
  extract_single_entry(entry, output_path)
70
70
  end
@@ -67,7 +67,7 @@ module Omnizip
67
67
  # @param output_path [String] Destination path
68
68
  def extract_entry(entry_path, output_path)
69
69
  entry = find_entry(entry_path)
70
- raise "Entry not found: #{entry_path}" unless entry
70
+ raise Errno::ENOENT, "Entry not found: #{entry_path}" unless entry
71
71
 
72
72
  if entry.directory?
73
73
  FileUtils.mkdir_p(output_path)
@@ -111,7 +111,7 @@ module Omnizip
111
111
  loop do
112
112
  @io.seek(sector * Iso::SECTOR_SIZE)
113
113
  data = @io.read(Iso::SECTOR_SIZE)
114
- raise "Failed to read volume descriptor" unless data
114
+ raise Omnizip::InvalidArchiveError, "Failed to read volume descriptor" unless data
115
115
 
116
116
  vd = VolumeDescriptor.parse(data)
117
117
 
@@ -127,7 +127,7 @@ module Omnizip
127
127
 
128
128
  return if @primary_volume_descriptor
129
129
 
130
- raise "No primary volume descriptor found"
130
+ raise Omnizip::InvalidArchiveError, "No primary volume descriptor found"
131
131
  end
132
132
 
133
133
  # Parse directory structure
@@ -29,7 +29,7 @@ module Omnizip
29
29
  #
30
30
  # @param data [String] Binary data
31
31
  def parse(data)
32
- raise "Invalid volume descriptor size" unless data.bytesize >= 2048
32
+ raise Omnizip::InvalidArchiveError, "Invalid volume descriptor size" unless data.bytesize >= 2048
33
33
 
34
34
  # Byte 0: Volume descriptor type
35
35
  @type = data.getbyte(0)
@@ -37,7 +37,7 @@ module Omnizip
37
37
  # Bytes 1-5: Standard identifier "CD001"
38
38
  @identifier = data[1, 5]
39
39
  unless @identifier == ISO_IDENTIFIER
40
- raise "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
40
+ raise Omnizip::InvalidArchiveError, "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
41
41
  end
42
42
 
43
43
  # Byte 6: Version (should be 1)
@@ -230,26 +230,6 @@ module Omnizip
230
230
  # Decode as 8 direct bits
231
231
  @range_decoder.decode_direct_bits(8)
232
232
  end
233
-
234
- # Decode RAR-specific escape code
235
- #
236
- # RAR variant H uses different escape code values
237
- # and handling compared to standard PPMd7.
238
- #
239
- # Escape codes in RAR:
240
- # - 0: New symbol follows
241
- # - 1: Same as last symbol (run-length)
242
- # - 2-255: Reserved for future use
243
- #
244
- # @return [Integer, nil] Escape code or nil
245
- def decode_escape_code
246
- # RAR escape codes differ from PPMd7
247
- # This is a placeholder for the proper implementation
248
-
249
- # For now, return 0 (new symbol follows)
250
- # Real implementation would decode from range coder
251
- 0
252
- end
253
233
  end
254
234
  end
255
235
  end