omnizip 0.3.39 → 0.3.41

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: 8f86f08dba1d67ec9a22053ee355dda5d461c42d29b45134ad75a1192584d0a5
4
- data.tar.gz: 31d78c577804b43509bfea9a8bc9fd5edfad4ac149752cabb0eef87eafebbf56
3
+ metadata.gz: 919a322870094f270bd574530af4c463b6446586fdf869026b79ad017834bc1e
4
+ data.tar.gz: 55507eb0b2da4109cb094e711cda2d39df10773c9774c4a01614edc0d8548d9a
5
5
  SHA512:
6
- metadata.gz: f6ee4bb68ede247277420ec455130140a8be8e1f417581f1c3f996cab48d34202ae26631862ef88ef301fb712c649f72100805c6d553afce51f2705747e68e28
7
- data.tar.gz: b7f3567b7f8d3c7679bbd7563fd9fabf666aa3c1fbfdff38afd0e9e2dc9034e1f78428b710f2ff72518e45eef956eeb61a679c27df6b00e2a8e81cbbc0b5eed1
6
+ metadata.gz: 73b03468e2bb51d227059f316ab4fce911ce19ee6e0c175559de7b1df58a69536d9284d2b5ee2a0a19366ef0c53d448d02e4b95f397042c69c569630aacea86f
7
+ data.tar.gz: dad012852021f331f77a36bf86842274f612d215e32829965bf4bbaf5ad8b9b17539eb7e531a55e73c93d206c73a66760ccde6dfcf234b49e371f17f79451df8
data/CHANGELOG.md CHANGED
@@ -7,6 +7,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.40] - 2026-08-29
11
+
12
+ ### Fixed
13
+ - Every CLI command now works end-to-end (each was exercised against
14
+ real archives and its failure fixed):
15
+ - `omnizip list` printed "No algorithms registered." — all builtin
16
+ algorithms register lazily, so the available list was empty until
17
+ each was first used. `AlgorithmRegistry.available` now includes
18
+ the builtins.
19
+ - `omnizip convert a.7z a.zip` crashed on a fictional reader/writer
20
+ API inside the conversion strategy; rewritten on the real APIs and
21
+ round-trips byte-identically through unzip.
22
+ - `omnizip archive create x.rar ...` raised NoMethodError (a
23
+ module-level `Formats::Rar.create` never existed). It exists now
24
+ (RAR5 default, `version: 4` for RAR4); directory inputs work —
25
+ the stale "RAR5 does not support directories" restriction is gone
26
+ and trees archive under their own name like `.7z`, verified with
27
+ unrar.
28
+ - `omnizip archive metadata` opened every input as a ZIP: `.7z`
29
+ crashed with a ZIP parse error, and runs with no edit option
30
+ still printed "Metadata updated successfully". 7z now shows real
31
+ read-only metadata, unknown formats raise truthful errors, and
32
+ ZIPs default to show mode when nothing is being edited.
33
+ - `omnizip decompress archive.zip out/` — the behavior the
34
+ command's own help documents — failed with "Is a directory".
35
+ Archive inputs (zip/7z/tar/rar/cpio/iso) and directory outputs
36
+ now extract through the convenience layer, content-verified with
37
+ diff for zip, 7z, rar, and tar.
38
+ - Deflate64.metadata returned a raw Hash, crashing the CLI algorithm
39
+ table; it returns `Models::AlgorithmMetadata` like every other
40
+ algorithm.
41
+ - Guides referenced a nonexistent `:zstd` algorithm-registry key;
42
+ corrected to `:zstandard` everywhere (the registry key differs from
43
+ RPM's `:zstd` compression type).
44
+
45
+ ### Added
46
+ - `Omnizip::Buffer` 7z support: `create(:seven_zip)` /
47
+ `create_from_hash(hash, :seven_zip)`, `open`, `extract_to_memory`,
48
+ and `MemoryExtractor` all handle 7z through a temporary-file bridge
49
+ (the format has no in-memory stream implementation), including
50
+ explicit directory entries via a new
51
+ `SevenZip::Writer#add_directory_entry`. Format auto-detection fixed
52
+ to compare binary magic literals — UTF-8 literals never matched
53
+ binary data, so 7z buffers raised "Unknown archive format".
54
+ - README gains an In-Memory Archives section (examples run verbatim).
55
+
56
+ ## [0.3.39] - 2026-08-28
57
+
58
+ ### Fixed
59
+ - RAR4 writing was structurally fictional: HEAD_CRC used CRC-16/CCITT
60
+ instead of the low 16 bits of CRC32, HEAD_SIZE values excluded the
61
+ 2-byte CRC field, a duplicate marker block followed the signature
62
+ (the 7-byte signature IS the marker), and file headers lacked the
63
+ LONG_BLOCK flag. unrar rejected every archive with "Main archive
64
+ header is corrupt". STORE archives now round-trip through unrar
65
+ byte-identically, including explicit directory entries (marked with
66
+ the full 0xE0 dictionary mask — proven empirically: attribute-based
67
+ and trailing-backslash markers do not work), empty files, and
68
+ nested trees. `password:` now raises `NotImplementedError` instead
69
+ of writing the encrypted flag with no salt; `solid:`/`recovery:`/
70
+ `volume_size:` warn and are never advertised in header flags.
71
+ - RAR4 reading never parsed real WinRAR archives (the reader demanded
72
+ the same fictional layout the writer emitted). It now matches
73
+ `unrar l` exactly on libarchive fixtures — names (backslashes
74
+ normalized), sizes, directories, DOS timestamps — and native
75
+ decompression is CRC-verified before use: previously a real
76
+ WinRAR-compressed entry silently extracted garbage (e.g. 44308
77
+ bytes written for a 20111-byte file); now a CRC mismatch falls
78
+ back to unrar for byte-exact extraction while Omnizip-written
79
+ archives still decode natively.
80
+ - RAR3 writer/reader: 6 reserved bytes in the main header (was 4),
81
+ LONG_BLOCK flag, high size words before the filename (was after),
82
+ terminator flags 0. STORE output is `unrar t`-clean. The reader
83
+ dropped a fictional extended-time parser that produced year-2039
84
+ timestamps; base DOS time (minute precision) is used.
85
+ - RAR5 reader: mtime is a 32-bit Unix timestamp (the parser read a
86
+ vint plus FILETIME that do not exist in the format) and is nil
87
+ when the header carries none, instead of a fabricated `Time.now`.
88
+ Timestamps now match unrar exactly on fixtures.
89
+ - `Formats::Rar3`/`Formats::Rar5` never loaded: the autoload
90
+ registry in `formats/rar.rb` pointed at files defining a different
91
+ namespace. Both get proper parent autoload files
92
+ (`formats/rar3.rb`, `formats/rar5.rb`).
93
+ - Windows: the unrar program path was pre-wrapped in literal quotes
94
+ for a shell-string call form; shell-free array-form invocations
95
+ failed to start the program. Quotes now live only at the two
96
+ shell call sites, and unrar runs quietly (`-idq`).
97
+
98
+ ### Added
99
+ - unrar-gated RAR4 interop spec (STORE test + byte-identical
100
+ extraction with directories) and real WinRAR fixture parsing specs
101
+ (listing + unrar-fallback extraction with CRC assertions).
102
+
103
+ ### Documentation
104
+ - README RAR4/RAR5 sections rewritten to verified reality: STORE is
105
+ unrar-verified while `:fastest`-`:best` are Omnizip-round-trip-only
106
+ (previously all methods claimed "fully working"); the real writer
107
+ API (no `close`, no block form, `compression:` key); RAR5 "LZMA SDK
108
+ byte-for-byte" claims removed (the option falls back to STORE with
109
+ a warning). Fixed fictional `SevenZip::Writer` `close`/`algorithm=`
110
+ examples in the API-usage and architecture guides.
111
+
10
112
  ## [0.3.38] - 2026-08-28
11
113
 
12
114
  ### Added
data/README.adoc CHANGED
@@ -633,6 +633,37 @@ Omnizip.extract_archive('archive.zip', 'output/')
633
633
  Omnizip.list_archive('archive.zip')
634
634
  ----
635
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
+
636
667
  ==== Rubyzip Compatibility Mode
637
668
 
638
669
  [source,ruby]
@@ -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
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "tmpdir"
5
+
6
+ module Omnizip
7
+ module Buffer
8
+ # Bridges the in-memory Buffer API onto the file-based 7z
9
+ # reader/writer through a temporary file, since the 7z format has
10
+ # no in-memory stream implementation.
11
+ module SevenZipBridge
12
+ class << self
13
+ # Create a 7z archive in memory
14
+ #
15
+ # @param buffer [StringIO] Destination buffer
16
+ # @param options [Hash] Writer options
17
+ # @yield [WriteProxy] Block adding entries
18
+ def create(buffer, options = {}, &block)
19
+ Dir.mktmpdir("omnizip_buffer_7z") do |tmp|
20
+ archive_path = File.join(tmp, "buffer.7z")
21
+ writer = Formats::SevenZip::Writer.new(archive_path, options)
22
+ block&.call(WriteProxy.new(writer))
23
+ writer.write
24
+
25
+ buffer.write(File.binread(archive_path))
26
+ end
27
+ end
28
+
29
+ # Read a 7z archive from memory
30
+ #
31
+ # @param buffer [StringIO] Archive bytes
32
+ # @yield [ReadProxy] Block reading entries
33
+ # @return [ReadProxy, Object] Proxy or block result
34
+ def open(buffer, &block)
35
+ Dir.mktmpdir("omnizip_buffer_7z") do |tmp|
36
+ archive_path = File.join(tmp, "buffer.7z")
37
+ File.binwrite(archive_path, buffer.string)
38
+
39
+ reader = Formats::SevenZip::Reader.new(archive_path)
40
+ reader.open
41
+ proxy = ReadProxy.new(reader, tmp)
42
+
43
+ block ? yield(proxy) : proxy
44
+ end
45
+ end
46
+ end
47
+
48
+ # Mirrors the MemoryArchive write interface onto a 7z writer
49
+ class WriteProxy
50
+ # @param writer [Formats::SevenZip::Writer]
51
+ def initialize(writer)
52
+ @writer = writer
53
+ end
54
+
55
+ # Add an entry; a trailing slash with empty content adds a
56
+ # directory entry
57
+ #
58
+ # @param name [String] Entry name
59
+ # @param data [String] Entry content
60
+ # @return [self]
61
+ def add(name, data, **_options)
62
+ if name.end_with?("/") && data.to_s.empty?
63
+ @writer.add_directory_entry(name)
64
+ else
65
+ @writer.add_data(name, data)
66
+ end
67
+ self
68
+ end
69
+ end
70
+
71
+ # Mirrors the MemoryArchive read interface onto a 7z reader
72
+ class ReadProxy
73
+ # @param reader [Formats::SevenZip::Reader] opened reader
74
+ # @param tmp [String] Scratch directory for extraction
75
+ def initialize(reader, tmp)
76
+ @reader = reader
77
+ @tmp = tmp
78
+ end
79
+
80
+ # Yield an Entry wrapper per archive entry
81
+ def each_entry
82
+ @reader.list_files.each do |entry|
83
+ yield Entry.new(entry, @reader, @tmp)
84
+ end
85
+ end
86
+
87
+ # Filename => content for every file entry
88
+ #
89
+ # @return [Hash<String, String>]
90
+ def extract_all_to_memory
91
+ result = {}
92
+ each_entry do |entry|
93
+ result[entry.name] = entry.read unless entry.directory?
94
+ end
95
+ result
96
+ end
97
+
98
+ # Entry names; directories keep a trailing slash like the ZIP
99
+ # listing convention
100
+ #
101
+ # @return [Array<String>]
102
+ def entry_names
103
+ @reader.list_files.map do |entry|
104
+ entry.is_dir ? "#{entry.name}/" : entry.name
105
+ end
106
+ end
107
+
108
+ # Read-only access to the underlying entries
109
+ #
110
+ # @return [Array<Formats::SevenZip::Models::FileEntry>]
111
+ def raw_entries
112
+ @reader.list_files
113
+ end
114
+
115
+ # Entry wrapper matching MemoryArchive::Entry's surface
116
+ class Entry
117
+ attr_reader :name, :size
118
+
119
+ # @param entry [Formats::SevenZip::Models::FileEntry]
120
+ # @param reader [Formats::SevenZip::Reader]
121
+ # @param tmp [String] Scratch directory
122
+ def initialize(entry, reader, tmp)
123
+ @entry = entry
124
+ @reader = reader
125
+ @tmp = tmp
126
+ @name = entry.is_dir ? "#{entry.name}/" : entry.name
127
+ @size = entry.size
128
+ end
129
+
130
+ # @return [Boolean]
131
+ def directory?
132
+ @entry.is_dir
133
+ end
134
+
135
+ # @return [Time, nil]
136
+ def time
137
+ @entry.mtime
138
+ end
139
+
140
+ # Extract and return this entry's content
141
+ #
142
+ # @return [String]
143
+ def read(_size = nil)
144
+ return "" if directory? || @entry.has_stream == false
145
+
146
+ dest = File.join(@tmp, "entry", @entry.name)
147
+ @reader.extract_entry(@entry.name, dest)
148
+ File.binread(dest)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -30,6 +30,7 @@ module Omnizip
30
30
  # Nested classes - autoloaded
31
31
  autoload :MemoryArchive, "omnizip/buffer/memory_archive"
32
32
  autoload :MemoryExtractor, "omnizip/buffer/memory_extractor"
33
+ autoload :SevenZipBridge, "omnizip/buffer/seven_zip_bridge"
33
34
 
34
35
  class << self
35
36
  # Create archive in memory
@@ -54,7 +55,7 @@ module Omnizip
54
55
  when :zip
55
56
  create_zip(buffer, options, &block)
56
57
  when :seven_zip, :"7z"
57
- raise NotImplementedError, "7z format support coming in Phase 2"
58
+ SevenZipBridge.create(buffer, options, &block)
58
59
  else
59
60
  raise ArgumentError, "Unsupported format: #{format}"
60
61
  end
@@ -84,7 +85,7 @@ module Omnizip
84
85
  when :zip
85
86
  open_zip(buffer, &block)
86
87
  when :seven_zip, :"7z"
87
- raise NotImplementedError, "7z format support coming in Phase 2"
88
+ SevenZipBridge.open(buffer, &block)
88
89
  else
89
90
  raise ArgumentError, "Unsupported format: #{format}"
90
91
  end
@@ -132,14 +133,14 @@ module Omnizip
132
133
  # @return [Symbol] Detected format
133
134
  # @raise [Omnizip::FormatError] If format cannot be detected
134
135
  def detect_format(buffer)
135
- magic = buffer.read(4)
136
+ magic = buffer.read(4).to_s.b
136
137
  buffer.rewind
137
138
 
138
139
  case magic
139
- when "PK\x03\x04", "PK\x05\x06", "PK\x07\x08"
140
+ when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
140
141
  # ZIP signatures: local file header, EOCD, data descriptor
141
142
  :zip
142
- when "7z\xBC\xAF"
143
+ when "7z\xBC\xAF".b
143
144
  :seven_zip
144
145
  else
145
146
  raise Omnizip::FormatError,
@@ -120,42 +120,35 @@ module Omnizip
120
120
 
121
121
  start_time = Time.now
122
122
 
123
+ # Format-agnostic options; Formats::Rar applies the
124
+ # version-specific semantics
123
125
  writer_opts = {
124
126
  version: version,
125
127
  compression: compression,
126
128
  level: level,
127
129
  include_mtime: include_mtime,
128
130
  include_crc32: include_crc32,
129
- }
130
-
131
- # Add solid compression for RAR5
132
- writer_opts[:solid] = solid if version == 5 && solid
133
-
134
- # Add multi-volume options for RAR5
135
- if version == 5 && multi_volume && volume_size
136
- writer_opts[:multi_volume] = true
137
- writer_opts[:volume_size] = volume_size
138
- writer_opts[:volume_naming] = volume_naming
139
- end
140
-
141
- # Add encryption options for RAR5
142
- if version == 5 && password
143
- writer_opts[:password] = password
144
- writer_opts[:kdf_iterations] = kdf_iterations
145
- end
146
-
147
- # Add recovery options for RAR5
148
- if version == 5 && recovery
149
- writer_opts[:recovery] = true
150
- writer_opts[:recovery_percent] = recovery_percent
151
- end
131
+ solid: solid,
132
+ multi_volume: multi_volume,
133
+ volume_size: volume_size,
134
+ volume_naming: volume_naming,
135
+ password: password,
136
+ kdf_iterations: kdf_iterations,
137
+ recovery: recovery,
138
+ recovery_percent: recovery_percent,
139
+ }.compact
152
140
 
153
141
  result_files = Omnizip::Formats::Rar.create(output_file,
154
142
  writer_opts) do |rar|
155
143
  inputs.each do |input|
156
144
  if File.directory?(input)
157
- raise ArgumentError,
158
- "RAR5 writer does not support directories yet. Add individual files."
145
+ CliOutputFormatter.verbose_puts(
146
+ "Adding directory: #{input}",
147
+ verbose,
148
+ )
149
+ # Strip the parent so the tree lands under its own
150
+ # name, matching the .7z creation behavior
151
+ rar.add_directory(input, File.dirname(input))
159
152
  else
160
153
  CliOutputFormatter.verbose_puts(
161
154
  "Adding file: #{input}",
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
3
5
  #
4
6
  # Copyright (C) 2024 Ribose Inc.
5
7
  #
@@ -31,12 +33,20 @@ module Omnizip
31
33
 
32
34
  # Execute the decompress command.
33
35
  #
36
+ # Archives (.zip/.7z/.tar/...) extract into the output
37
+ # directory; single-file streams decompress to the output file.
38
+ #
34
39
  # @param input_file [String] Path to input file
35
- # @param output_file [String] Path to output file
40
+ # @param output_file [String] Path to output file or directory
36
41
  # @return [void]
37
42
  def run(input_file, output_file)
38
43
  validate_inputs(input_file, output_file)
39
44
 
45
+ if archive_input?(input_file) || File.directory?(output_file)
46
+ run_archive_extract(input_file, output_file)
47
+ return
48
+ end
49
+
40
50
  algorithm_name = options[:algorithm] || detect_algorithm
41
51
  verbose = options[:verbose] || false
42
52
 
@@ -70,8 +80,26 @@ module Omnizip
70
80
  end
71
81
  end
72
82
 
83
+ ARCHIVE_EXTENSIONS = %w[.zip .7z .tar .rar .cpio .iso].freeze
84
+
73
85
  private
74
86
 
87
+ def archive_input?(path)
88
+ ext = File.extname(path).downcase
89
+ ARCHIVE_EXTENSIONS.include?(ext)
90
+ end
91
+
92
+ def run_archive_extract(input_file, output_dir)
93
+ FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
94
+
95
+ entries = Omnizip.list_archive(input_file)
96
+ Omnizip.extract_archive(input_file, output_dir)
97
+
98
+ puts "Extracted #{entries.size} file(s) to: #{output_dir}"
99
+ rescue StandardError => e
100
+ raise Omnizip::IOError, "Failed to extract archive: #{e.message}"
101
+ end
102
+
75
103
  def validate_inputs(input_file, output_file)
76
104
  unless File.exist?(input_file)
77
105
  raise Omnizip::IOError, "Input file not found: #{input_file}"
@@ -18,19 +18,83 @@ module Omnizip
18
18
  raise Errno::ENOENT, "Archive not found: #{archive_path}"
19
19
  end
20
20
 
21
+ if seven_zip?(archive_path)
22
+ show_seven_zip_metadata(archive_path, pattern)
23
+ return
24
+ end
25
+
26
+ unless zip?(archive_path)
27
+ raise ArgumentError,
28
+ "Metadata is supported for .zip and .7z archives: " \
29
+ "#{archive_path}"
30
+ end
31
+
21
32
  Omnizip::Zip::File.open(archive_path) do |archive|
22
- if options[:show]
33
+ if options[:show] || editing_nothing?
23
34
  show_metadata(archive, pattern)
24
35
  else
25
36
  edit_metadata(archive, pattern)
37
+ puts "Metadata updated successfully"
26
38
  end
27
39
  end
28
-
29
- puts "Metadata updated successfully" unless options[:show]
30
40
  end
31
41
 
32
42
  private
33
43
 
44
+ def zip?(path)
45
+ path.end_with?(".zip")
46
+ end
47
+
48
+ def seven_zip?(path)
49
+ path.end_with?(".7z")
50
+ end
51
+
52
+ # True when no edit option was given — editing would be a no-op
53
+ def editing_nothing?
54
+ %i[comment set_mtime chmod set_attribute].none? { |key| options[key] }
55
+ end
56
+
57
+ # Read-only view for 7z archives; the edit path is zip-only
58
+ def show_seven_zip_metadata(archive_path, pattern)
59
+ if options[:comment] || options[:set_mtime] ||
60
+ options[:chmod] || options[:set_attribute]
61
+ raise ArgumentError,
62
+ "Metadata editing is only supported for ZIP archives"
63
+ end
64
+
65
+ reader = Omnizip::Formats::SevenZip::Reader.new(archive_path)
66
+ reader.open
67
+
68
+ if pattern
69
+ entries = reader.list_files.select do |e|
70
+ File.fnmatch(pattern, e.name)
71
+ end
72
+ if entries.empty?
73
+ warn "No entries match pattern: #{pattern}"
74
+ return
75
+ end
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
88
+ else
89
+ entries = reader.list_files
90
+ files = entries.reject(&:is_dir)
91
+ puts "Archive: #{archive_path}"
92
+ puts "Entries: #{entries.size} " \
93
+ "(#{files.size} files, #{entries.size - files.size} dirs)"
94
+ puts "Total size: #{format_size(files.sum(&:size))}"
95
+ end
96
+ end
97
+
34
98
  def show_metadata(archive, pattern)
35
99
  if pattern
36
100
  # Show entry metadata