omnizip 0.3.47 → 0.3.48

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: 22e232b5a4b1ddbbece5ce1b6052ea90a9e76a120e8d246cf03ae0b568d4ba73
4
- data.tar.gz: af66a96e666d25e75196b68b317e2f34a37f2fa3e0da2d79d8f2d67d2a6d86a2
3
+ metadata.gz: 8ff2a922c26dc80694f6167b0d46e16d28fe34fa9a0cdbcea12f6f5dc57085b6
4
+ data.tar.gz: f260edbb81b37ba0824fb9a896fc1668a60d3e74ffeba44821670d4304dd065d
5
5
  SHA512:
6
- metadata.gz: ed6a8cd7c2613912ed4c50e914cd1a4184253e6b66325741649d2790c9b0650546f6066354a28956b98e2f0a7c17853a56a0723308877a93043b55979270a2c1
7
- data.tar.gz: f6ad381f14bfd3a621584747bd71f544adacafc6d8241261f6bf7c553a92763ca0fef2165d4a65895f0ff54231b071e6f98903d87caf7125bfe229c372afc489
6
+ metadata.gz: 0b47b5b5702166234e3c5e5cc132f3f944837904a9f4ce91a560e6c4207de329ec8d1e5ae01028e1b872c4dfbf5fda8b0ffee981ac9698cf500bc114d0a0ae65
7
+ data.tar.gz: e788bec8df533ea0417eb8db6e762a89ac17908e676558132eb127e1aa18af4c64bdc6da2151d50d772a4754c4a81ced191fa4b1b750fb6df27683d031ad237b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.47] - 2026-08-31
11
+
12
+ ### Fixed
13
+ - `.rar` is a writable extension route: the RAR handler gained
14
+ create in 0.3.46, but the route table still classified `.rar` as
15
+ read-only — `Archive.create('a.rar')` without an explicit format
16
+ kept raising "cannot be written by the convenience API" while
17
+ `Archive.create(fmt: :rar)` worked. The route table matches the
18
+ handler now; `.cpio`/`.iso` stay read-only.
19
+
10
20
  ## [0.3.46] - 2026-08-31
11
21
 
12
22
  ### Added
@@ -36,7 +36,7 @@ module Omnizip
36
36
  end
37
37
  end
38
38
 
39
- PASSWORD_FORMATS = [:seven_zip].freeze
39
+ PASSWORD_FORMATS = %i[seven_zip rar].freeze
40
40
  private_constant :PASSWORD_FORMATS
41
41
 
42
42
  class << self
@@ -69,14 +69,12 @@ module Omnizip
69
69
  @writer = writer
70
70
  end
71
71
 
72
- # Directory entries are a no-op: 7z archives derive the
73
- # directory tree from the files' archive paths ("sub/f.txt"
74
- # implies "sub/"), and the writer/reader handle slashed
75
- # paths (verified against 7zz).
76
72
  def add(name, source_path = nil)
77
- return if source_path.nil?
78
-
79
- @writer.add_file(source_path, name)
73
+ if source_path
74
+ @writer.add_file(source_path, name)
75
+ else
76
+ @writer.add_directory_entry(name)
77
+ end
80
78
  end
81
79
 
82
80
  def add_data(name, data)
@@ -123,7 +123,11 @@ module Omnizip
123
123
  @entry = entry
124
124
  @reader = reader
125
125
  @tmp = tmp
126
- @name = entry.is_dir ? "#{entry.name}/" : entry.name
126
+ @name = if entry.is_dir
127
+ entry.name.end_with?("/") ? entry.name : "#{entry.name}/"
128
+ else
129
+ entry.name
130
+ end
127
131
  @size = entry.size
128
132
  end
129
133
 
@@ -34,6 +34,15 @@ module Omnizip
34
34
  # @param output_file [String] Path to output archive
35
35
  # @param inputs [Array<String>] Paths to files/directories to archive
36
36
  # @return [void]
37
+ # Execute the archive create command.
38
+ #
39
+ # Routes through the Archive facade: the handler registry
40
+ # picks each format's writer and applies its option semantics,
41
+ # so this command only translates CLI vocabulary and reports.
42
+ #
43
+ # @param output_file [String] Path to output archive
44
+ # @param inputs [Array<String>] Paths to files/directories to archive
45
+ # @return [void]
37
46
  def run(output_file, *inputs)
38
47
  validate_inputs(output_file, inputs)
39
48
 
@@ -44,220 +53,126 @@ module Omnizip
44
53
  opts = apply_profile(first_file, opts)
45
54
  end
46
55
 
47
- # Determine format from extension or --format option
48
- format = opts[:format] || detect_format(output_file)
49
-
50
- if format == "rar"
51
- create_rar_archive(output_file, inputs, opts)
52
- else
53
- create_7z_archive(output_file, inputs, opts)
54
- end
55
- end
56
-
57
- private
58
-
59
- def detect_format(filename)
60
- case File.extname(filename).downcase
61
- when ".rar" then "rar"
62
- else "7z"
63
- end
64
- end
65
-
66
- def create_rar_archive(output_file, inputs, opts)
67
- version = opts[:rar_version] || 5
68
- compression = (opts[:rar_compression] || "store").to_sym
69
- level = opts[:level] || 3
70
- include_mtime = opts[:include_mtime] || false
71
- include_crc32 = opts[:include_crc32] || false
72
- solid = opts[:solid] || false
73
- multi_volume = opts[:multi_volume] || false
74
- volume_size = opts[:volume_size]
75
- volume_naming = opts[:volume_naming] || "part"
76
- password = opts[:password]
77
- kdf_iterations = opts[:kdf_iterations] || 262_144
78
- recovery = opts[:recovery] || false
79
- recovery_percent = opts[:recovery_percent] || 5
56
+ format = (opts[:format] || detect_format(output_file)).to_sym
57
+ handler_opts = handler_options(format, opts)
80
58
  verbose = opts[:verbose] || false
81
59
 
82
- if verbose
83
- CliOutputFormatter.verbose_puts(
84
- "Creating RAR#{version} archive: #{output_file}",
85
- true,
86
- )
87
- CliOutputFormatter.verbose_puts(
88
- "Compression: #{compression}, Level: #{level}",
89
- true,
90
- )
91
- CliOutputFormatter.verbose_puts(
92
- "Include mtime: #{include_mtime}, Include CRC32: #{include_crc32}",
93
- true,
94
- )
95
- if solid
96
- CliOutputFormatter.verbose_puts(
97
- "Solid compression: enabled",
98
- true,
99
- )
100
- end
101
- if multi_volume && volume_size
102
- CliOutputFormatter.verbose_puts(
103
- "Multi-volume: enabled (size: #{volume_size}, naming: #{volume_naming})",
104
- true,
105
- )
106
- end
107
- if password
108
- CliOutputFormatter.verbose_puts(
109
- "Encryption: enabled (AES-256-CBC, KDF iterations: #{kdf_iterations})",
110
- true,
111
- )
112
- end
113
- if recovery
114
- CliOutputFormatter.verbose_puts(
115
- "PAR2 recovery: enabled (redundancy: #{recovery_percent}%)",
116
- true,
117
- )
118
- end
119
- end
60
+ announce(output_file, format, handler_opts, opts, verbose)
120
61
 
121
62
  start_time = Time.now
122
63
 
123
- # Format-agnostic options; Formats::Rar applies the
124
- # version-specific semantics
125
- writer_opts = {
126
- version: version,
127
- compression: compression,
128
- level: level,
129
- include_mtime: include_mtime,
130
- include_crc32: include_crc32,
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
140
-
141
- result_files = Omnizip::Formats::Rar.create(output_file,
142
- writer_opts) do |rar|
64
+ Omnizip::Archive.create(output_file, format: format,
65
+ **handler_opts) do |archive|
143
66
  inputs.each do |input|
144
67
  if File.directory?(input)
145
68
  CliOutputFormatter.verbose_puts(
146
69
  "Adding directory: #{input}",
147
70
  verbose,
148
71
  )
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))
72
+ archive.add_directory(input)
152
73
  else
153
74
  CliOutputFormatter.verbose_puts(
154
75
  "Adding file: #{input}",
155
76
  verbose,
156
77
  )
157
- rar.add_file(input)
78
+ archive.add_file(input)
158
79
  end
159
80
  end
160
81
  end
161
82
 
162
83
  elapsed = Time.now - start_time
163
-
164
- # Handle result based on whether recovery or multi-volume is enabled
165
- files = result_files.is_a?(Array) ? result_files : [result_files]
166
- files.find { |f| f.end_with?(".rar") } || files.first
167
- archive_size = files.sum { |f| File.size(f) }
84
+ archive_size = calculate_archive_size(output_file,
85
+ handler_opts[:volume_size])
168
86
 
169
87
  if verbose
170
88
  puts ""
171
89
  puts "Archive created successfully"
172
- if files.size > 1
173
- puts "Files created: #{files.size}"
174
- puts " Archive volumes: #{files.count { |f| f.end_with?('.rar') }}"
175
- if recovery
176
- puts " PAR2 files: #{files.count { |f| f.include?('.par2') }}"
177
- end
90
+ if handler_opts[:volume_size]
178
91
  puts "Total size: #{format_bytes(archive_size)}"
92
+ puts "Volumes: #{count_volumes(output_file)}"
179
93
  else
180
94
  puts "Archive size: #{format_bytes(archive_size)}"
181
95
  end
182
96
  puts "Time elapsed: #{elapsed.round(2)}s"
183
- elsif files.size == 1
184
- puts "Created: #{files.first}"
185
97
  else
186
- puts "Created: #{files.size} files (#{files.count do |f|
187
- f.end_with?('.rar')
188
- end} volumes)"
98
+ puts "Created: #{output_file}"
189
99
  end
190
100
  end
191
101
 
192
- def create_7z_archive(output_file, inputs, opts)
193
- algorithm = (opts[:algorithm] || "lzma2").to_sym
194
- level = opts[:level] || 5
195
- solid = opts.fetch(:solid, true)
196
- verbose = opts[:verbose] || false
197
- filters = parse_filters(opts[:filters])
198
- volume_size = parse_volume_size(opts[:volume_size])
199
- password = opts[:password]
200
- encrypt_headers = opts[:encrypt_headers] || false
201
- preserve_ntfs_streams = opts[:preserve_ntfs_streams] || false
102
+ private
202
103
 
203
- if verbose
104
+ def detect_format(filename)
105
+ case File.extname(filename).downcase
106
+ when ".rar" then :rar
107
+ else :seven_zip
108
+ end
109
+ end
110
+
111
+ # Translate the format-agnostic CLI vocabulary into the option
112
+ # keywords each format's writer understands; nil values drop
113
+ def handler_options(format, opts)
114
+ case format
115
+ when :rar
116
+ rar_options(opts)
117
+ else
118
+ seven_zip_options(opts)
119
+ end
120
+ end
121
+
122
+ def rar_options(opts)
123
+ {
124
+ version: opts[:rar_version],
125
+ compression: opts[:rar_compression]&.to_sym,
126
+ level: opts[:level],
127
+ include_mtime: opts[:include_mtime],
128
+ include_crc32: opts[:include_crc32],
129
+ solid: opts[:solid],
130
+ multi_volume: opts[:multi_volume],
131
+ volume_size: opts[:volume_size],
132
+ volume_naming: opts[:volume_naming],
133
+ password: opts[:password],
134
+ kdf_iterations: opts[:kdf_iterations],
135
+ recovery: opts[:recovery],
136
+ recovery_percent: opts[:recovery_percent],
137
+ }.compact
138
+ end
139
+
140
+ def seven_zip_options(opts)
141
+ {
142
+ algorithm: (opts[:algorithm] || "lzma2").to_sym,
143
+ level: opts[:level] || 5,
144
+ solid: opts.fetch(:solid, true),
145
+ filters: parse_filters(opts[:filters]),
146
+ volume_size: parse_volume_size(opts[:volume_size]),
147
+ password: opts[:password],
148
+ encrypt_headers: opts[:encrypt_headers] || false,
149
+ }.compact
150
+ end
151
+
152
+ def announce(output_file, format, handler_opts, _opts, verbose)
153
+ return unless verbose
154
+
155
+ CliOutputFormatter.verbose_puts(
156
+ "Creating archive: #{output_file}",
157
+ true,
158
+ )
159
+ CliOutputFormatter.verbose_puts(
160
+ "Format: #{format == :rar ? "RAR#{handler_opts[:version] || 5}" : '7z'}, " \
161
+ "Algorithm: #{handler_opts[:algorithm] || handler_opts[:compression]}, " \
162
+ "Level: #{handler_opts[:level]}, Solid: #{handler_opts[:solid]}",
163
+ true,
164
+ )
165
+ if handler_opts[:volume_size]
204
166
  CliOutputFormatter.verbose_puts(
205
- "Creating archive: #{output_file}",
167
+ "Volume size: #{format_bytes(handler_opts[:volume_size])}",
206
168
  true,
207
169
  )
170
+ end
171
+ if handler_opts[:password]
208
172
  CliOutputFormatter.verbose_puts(
209
- "Algorithm: #{algorithm}, Level: #{level}, " \
210
- "Solid: #{solid}",
173
+ "Encryption: enabled",
211
174
  true,
212
175
  )
213
- if volume_size
214
- CliOutputFormatter.verbose_puts(
215
- "Volume size: #{format_bytes(volume_size)}",
216
- true,
217
- )
218
- end
219
- unless filters.empty?
220
- CliOutputFormatter.verbose_puts(
221
- "Filters: #{filters.join(', ')}",
222
- true,
223
- )
224
- end
225
- if encrypt_headers
226
- CliOutputFormatter.verbose_puts(
227
- "Header encryption: enabled",
228
- true,
229
- )
230
- end
231
- if preserve_ntfs_streams && Omnizip::Platform.supports_ntfs_streams?
232
- CliOutputFormatter.verbose_puts(
233
- "NTFS streams: preserving",
234
- true,
235
- )
236
- end
237
- end
238
-
239
- start_time = Time.now
240
-
241
- create_7z_archive_writer(output_file, inputs, algorithm, level, solid,
242
- filters, volume_size, password, encrypt_headers,
243
- preserve_ntfs_streams, verbose)
244
-
245
- elapsed = Time.now - start_time
246
-
247
- archive_size = calculate_archive_size(output_file, volume_size)
248
-
249
- if verbose
250
- puts ""
251
- puts "Archive created successfully"
252
- if volume_size
253
- puts "Total size: #{format_bytes(archive_size)}"
254
- puts "Volumes: #{count_volumes(output_file)}"
255
- else
256
- puts "Archive size: #{format_bytes(archive_size)}"
257
- end
258
- puts "Time elapsed: #{elapsed.round(2)}s"
259
- else
260
- puts "Created: #{output_file}"
261
176
  end
262
177
  end
263
178
 
@@ -289,44 +204,6 @@ module Omnizip
289
204
  filter_str.split(",").map(&:strip).map(&:to_sym)
290
205
  end
291
206
 
292
- def create_7z_archive_writer(output_file, inputs, algorithm, level, solid,
293
- filters, volume_size, password, encrypt_headers,
294
- _preserve_ntfs_streams, verbose)
295
- writer_opts = {
296
- algorithm: algorithm,
297
- level: level,
298
- solid: solid,
299
- filters: filters,
300
- }
301
- writer_opts[:volume_size] = volume_size if volume_size
302
- writer_opts[:password] = password if password
303
- writer_opts[:encrypt_headers] = encrypt_headers if encrypt_headers
304
-
305
- writer = Formats::SevenZip::Writer.new(output_file, writer_opts)
306
-
307
- inputs.each do |input|
308
- if File.directory?(input)
309
- CliOutputFormatter.verbose_puts(
310
- "Adding directory: #{input}",
311
- verbose,
312
- )
313
- writer.add_directory(input)
314
- else
315
- CliOutputFormatter.verbose_puts(
316
- "Adding file: #{input}",
317
- verbose,
318
- )
319
- writer.add_file(input)
320
- end
321
- end
322
-
323
- CliOutputFormatter.verbose_puts("Writing archive...", verbose)
324
- writer.write
325
- rescue StandardError => e
326
- raise Omnizip::CompressionError,
327
- "Failed to create archive: #{e.message}"
328
- end
329
-
330
207
  def format_bytes(bytes)
331
208
  units = %w[B KB MB GB]
332
209
  size = bytes.to_f
@@ -184,7 +184,9 @@ module Omnizip
184
184
  warn "RAR4 writer ignores volume_size: not implemented " \
185
185
  "(no corresponding flag is written)"
186
186
  end
187
- return unless @options[:recovery]&.to_i&.positive?
187
+ recovery = @options[:recovery]
188
+ return unless recovery.is_a?(Numeric) && recovery.positive?
189
+ return if recovery == true
188
190
 
189
191
  warn "RAR4 writer ignores recovery: not implemented " \
190
192
  "(no corresponding flag is written)"
@@ -75,7 +75,9 @@ module Omnizip
75
75
  # @return [self]
76
76
  def add_directory_entry(archive_path)
77
77
  entry = Models::FileEntry.new
78
- entry.name = archive_path.chomp("/")
78
+ # Trailing slash is preserved: explicit directory entries
79
+ # carry it ("testdir/"), matching the collector's tree walk
80
+ entry.name = archive_path
79
81
  entry.source_path = nil
80
82
  entry.size = 0
81
83
  entry.is_dir = true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.47"
4
+ VERSION = "0.3.48"
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.47
4
+ version: 0.3.48
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.