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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/lib/omnizip/archive_handlers/seven_zip_handler.rb +1 -1
- data/lib/omnizip/archive_handlers/zip_handler.rb +1 -1
- data/lib/omnizip/buffer/memory_extractor.rb +3 -16
- data/lib/omnizip/buffer/seven_zip_bridge.rb +13 -0
- data/lib/omnizip/buffer.rb +23 -21
- data/lib/omnizip/checksums/verifier.rb +2 -2
- data/lib/omnizip/chunked.rb +14 -21
- data/lib/omnizip/cli/output_formatter.rb +16 -8
- data/lib/omnizip/cli.rb +1 -7
- data/lib/omnizip/commands/archive_create_command.rb +1 -10
- data/lib/omnizip/commands/archive_extract_command.rb +4 -13
- data/lib/omnizip/commands/archive_list_command.rb +1 -18
- data/lib/omnizip/commands/metadata_command.rb +21 -26
- data/lib/omnizip/commands/parity_create_command.rb +1 -7
- data/lib/omnizip/extraction/selective_extractor.rb +16 -4
- data/lib/omnizip/formats/cpio/reader.rb +1 -1
- data/lib/omnizip/formats/iso/reader.rb +3 -3
- data/lib/omnizip/formats/iso/volume_descriptor.rb +2 -2
- data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +0 -20
- data/lib/omnizip/formats/rar/decompressor.rb +7 -7
- data/lib/omnizip/formats/rar/reader.rb +2 -2
- data/lib/omnizip/formats/rpm.rb +4 -4
- data/lib/omnizip/formats/seven_zip/reader.rb +1 -1
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +1 -1
- data/lib/omnizip/formats/xar/reader.rb +2 -2
- data/lib/omnizip/formats/zip/reader.rb +96 -8
- data/lib/omnizip/models/conversion_result.rb +76 -70
- data/lib/omnizip/models/filter_config.rb +34 -44
- data/lib/omnizip/models/match_result.rb +35 -18
- data/lib/omnizip/models/optimization_suggestion.rb +45 -41
- data/lib/omnizip/models/performance_result.rb +53 -47
- data/lib/omnizip/models/profile_report.rb +70 -36
- data/lib/omnizip/parallel/parallel_extractor.rb +2 -2
- data/lib/omnizip/parallel/worker_pool.rb +3 -3
- data/lib/omnizip/parity/par2_creator.rb +0 -34
- data/lib/omnizip/parity/par2_verifier.rb +5 -0
- data/lib/omnizip/profiler/report_generator.rb +2 -13
- data/lib/omnizip/profiler.rb +1 -12
- data/lib/omnizip/temp/safe_extract.rb +3 -43
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.rb +1 -1
- metadata +1 -1
|
@@ -61,7 +61,7 @@ module Omnizip
|
|
|
61
61
|
# @param password [String, nil] Optional password
|
|
62
62
|
# @raise [RuntimeError] if extraction fails
|
|
63
63
|
def extract(archive_path, output_dir, password: nil)
|
|
64
|
-
raise "RAR extraction not available" unless available?
|
|
64
|
+
raise Omnizip::RarNotAvailableError, "RAR extraction not available" unless available?
|
|
65
65
|
|
|
66
66
|
if gem_available?
|
|
67
67
|
extract_with_gem(archive_path, output_dir, password)
|
|
@@ -78,7 +78,7 @@ module Omnizip
|
|
|
78
78
|
# @return [Array<Hash>] Entry information
|
|
79
79
|
# @raise [RuntimeError] if listing fails
|
|
80
80
|
def list(archive_path)
|
|
81
|
-
raise "RAR extraction not available" unless available?
|
|
81
|
+
raise Omnizip::RarNotAvailableError, "RAR extraction not available" unless available?
|
|
82
82
|
|
|
83
83
|
if gem_available?
|
|
84
84
|
list_with_gem(archive_path)
|
|
@@ -97,7 +97,7 @@ module Omnizip
|
|
|
97
97
|
# @param password [String, nil] Optional password
|
|
98
98
|
def extract_entry(archive_path, entry_name, output_path,
|
|
99
99
|
password: nil)
|
|
100
|
-
raise "RAR extraction not available" unless available?
|
|
100
|
+
raise Omnizip::RarNotAvailableError, "RAR extraction not available" unless available?
|
|
101
101
|
|
|
102
102
|
if gem_available?
|
|
103
103
|
extract_entry_with_gem(archive_path, entry_name,
|
|
@@ -185,7 +185,7 @@ module Omnizip
|
|
|
185
185
|
require "unrar"
|
|
186
186
|
Unrar.extract(archive_path, output_dir, password: password)
|
|
187
187
|
rescue StandardError => e
|
|
188
|
-
raise "Gem extraction failed: #{e.message}"
|
|
188
|
+
raise Omnizip::DecompressionError, "Gem extraction failed: #{e.message}"
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
# Extract with system command
|
|
@@ -193,7 +193,7 @@ module Omnizip
|
|
|
193
193
|
cmd = build_extract_command(archive_path, output_dir, password)
|
|
194
194
|
return if system(*cmd)
|
|
195
195
|
|
|
196
|
-
raise "Command extraction failed: #{archive_path}"
|
|
196
|
+
raise Omnizip::DecompressionError, "Command extraction failed: #{archive_path}"
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
# List with unrar gem
|
|
@@ -210,13 +210,13 @@ module Omnizip
|
|
|
210
210
|
}
|
|
211
211
|
end
|
|
212
212
|
rescue StandardError => e
|
|
213
|
-
raise "Gem listing failed: #{e.message}"
|
|
213
|
+
raise Omnizip::DecompressionError, "Gem listing failed: #{e.message}"
|
|
214
214
|
end
|
|
215
215
|
|
|
216
216
|
# List with system command
|
|
217
217
|
def list_with_command(archive_path)
|
|
218
218
|
output = `"#{command_path}" vb "#{archive_path}" 2>&1`
|
|
219
|
-
raise "Command listing failed" unless $CHILD_STATUS.success?
|
|
219
|
+
raise Omnizip::DecompressionError, "Command listing failed" unless $CHILD_STATUS.success?
|
|
220
220
|
|
|
221
221
|
output.split("\n").map do |line|
|
|
222
222
|
{ name: line.strip, size: 0, compressed_size: 0,
|
|
@@ -64,7 +64,7 @@ module Omnizip
|
|
|
64
64
|
def extract_entry(entry_name, output_path, password: nil)
|
|
65
65
|
ensure_open
|
|
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
|
# Create directory if needed
|
|
70
70
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
@@ -150,7 +150,7 @@ module Omnizip
|
|
|
150
150
|
def parse_archive(io)
|
|
151
151
|
# Read and validate header
|
|
152
152
|
@header = Header.read(io)
|
|
153
|
-
raise "Invalid RAR archive" unless @header.valid?
|
|
153
|
+
raise Omnizip::InvalidArchiveError, "Invalid RAR archive" unless @header.valid?
|
|
154
154
|
|
|
155
155
|
# Update archive info
|
|
156
156
|
@archive_info.version = @header.version
|
data/lib/omnizip/formats/rpm.rb
CHANGED
|
@@ -301,7 +301,7 @@ module Omnizip
|
|
|
301
301
|
#
|
|
302
302
|
# @param output_dir [String] Output directory
|
|
303
303
|
def extract(output_dir)
|
|
304
|
-
raise "RPM not opened" unless @file
|
|
304
|
+
raise Omnizip::IOError, "RPM not opened" unless @file
|
|
305
305
|
|
|
306
306
|
FileUtils.mkdir_p(output_dir)
|
|
307
307
|
|
|
@@ -322,7 +322,7 @@ module Omnizip
|
|
|
322
322
|
#
|
|
323
323
|
# @return [String] Raw compressed payload data
|
|
324
324
|
def raw_payload
|
|
325
|
-
raise "RPM not opened" unless @file
|
|
325
|
+
raise Omnizip::IOError, "RPM not opened" unless @file
|
|
326
326
|
|
|
327
327
|
payload_io = payload
|
|
328
328
|
payload_io.read
|
|
@@ -370,7 +370,7 @@ module Omnizip
|
|
|
370
370
|
end
|
|
371
371
|
|
|
372
372
|
def payload
|
|
373
|
-
raise "RPM not opened" unless @file
|
|
373
|
+
raise Omnizip::IOError, "RPM not opened" unless @file
|
|
374
374
|
|
|
375
375
|
# Calculate payload offset
|
|
376
376
|
offset = @lead.length
|
|
@@ -434,7 +434,7 @@ module Omnizip
|
|
|
434
434
|
|
|
435
435
|
output
|
|
436
436
|
rescue StandardError => e
|
|
437
|
-
raise "Failed to decompress with #{cmd}: #{e.message}"
|
|
437
|
+
raise Omnizip::DecompressionError, "Failed to decompress with #{cmd}: #{e.message}"
|
|
438
438
|
end
|
|
439
439
|
|
|
440
440
|
def extract_cpio(source, output_dir)
|
|
@@ -103,7 +103,7 @@ module Omnizip
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
entry = @entries.find { |e| e.name == entry_name }
|
|
106
|
-
raise "Entry not found: #{entry_name}" unless entry
|
|
106
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
|
|
107
107
|
|
|
108
108
|
# Create directory if needed
|
|
109
109
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
@@ -70,7 +70,7 @@ module Omnizip
|
|
|
70
70
|
# @raise [RuntimeError] if entry not found or extraction fails
|
|
71
71
|
def extract_entry(entry_name, output_path)
|
|
72
72
|
entry = @entries.find { |e| e.name == entry_name }
|
|
73
|
-
raise "Entry not found: #{entry_name}" unless entry
|
|
73
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
|
|
74
74
|
|
|
75
75
|
# Create directory if needed
|
|
76
76
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
@@ -203,7 +203,7 @@ module Omnizip
|
|
|
203
203
|
@file.seek(@header.header_size)
|
|
204
204
|
compressed_toc = @file.read(@header.toc_compressed_size)
|
|
205
205
|
|
|
206
|
-
raise "Failed to read TOC" unless compressed_toc
|
|
206
|
+
raise Omnizip::InvalidArchiveError, "Failed to read TOC" unless compressed_toc
|
|
207
207
|
|
|
208
208
|
# Parse TOC
|
|
209
209
|
@toc = Toc.parse(compressed_toc, @header.toc_uncompressed_size)
|
|
@@ -272,7 +272,7 @@ module Omnizip
|
|
|
272
272
|
inf.close
|
|
273
273
|
result
|
|
274
274
|
rescue Zlib::Error => e
|
|
275
|
-
raise "Failed to decompress data: #{e.message}"
|
|
275
|
+
raise Omnizip::DecompressionError, "Failed to decompress data: #{e.message}"
|
|
276
276
|
end
|
|
277
277
|
end
|
|
278
278
|
|
|
@@ -151,19 +151,94 @@ dereference_links: false)
|
|
|
151
151
|
read_entry_data(entry)
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
# Stream a single entry's decompressed content in +chunk_size+
|
|
155
|
+
# chunks, yielding each. STORE copies raw bytes and DEFLATE
|
|
156
|
+
# feeds an incremental inflater, so memory stays bounded by
|
|
157
|
+
# the chunk size; methods without an incremental decoder fall
|
|
158
|
+
# back to the whole-entry read (yielded as one chunk). The
|
|
159
|
+
# streamed path verifies the entry CRC after the last chunk.
|
|
160
|
+
#
|
|
161
|
+
# @param entry_name [String] Name of the entry to stream
|
|
162
|
+
# @param chunk_size [Integer] Read granularity in bytes
|
|
163
|
+
# @yield [chunk] Decompressed chunk
|
|
164
|
+
# @return [void]
|
|
165
|
+
def read_entry_stream(entry_name, chunk_size: 64 * 1024, &block)
|
|
166
|
+
ensure_read
|
|
167
|
+
entry = @entries.find { |e| e.filename == entry_name }
|
|
168
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
|
|
169
|
+
return if entry.directory? || entry.symlink?
|
|
170
|
+
|
|
171
|
+
crc = Omnizip::Checksums::Crc32.new
|
|
172
|
+
streamed =
|
|
173
|
+
File.open(file_path, "rb") do |io|
|
|
174
|
+
io.seek(entry_data_offset(io, entry), ::IO::SEEK_SET)
|
|
175
|
+
|
|
176
|
+
# Bound reads by the entry payload — what follows it in
|
|
177
|
+
# the file is the central directory, not entry data
|
|
178
|
+
remaining = entry.compressed_size
|
|
179
|
+
|
|
180
|
+
case entry.compression_method
|
|
181
|
+
when COMPRESSION_STORE
|
|
182
|
+
stream_raw(io, chunk_size, crc, remaining, &block)
|
|
183
|
+
when COMPRESSION_DEFLATE
|
|
184
|
+
stream_deflate(io, chunk_size, crc, remaining, &block)
|
|
185
|
+
else
|
|
186
|
+
yield(read_entry_data(entry))
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
return unless streamed
|
|
192
|
+
|
|
193
|
+
if crc.finalize != entry.crc32
|
|
194
|
+
raise Omnizip::ChecksumError,
|
|
195
|
+
"CRC mismatch for #{entry.filename}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
154
199
|
private
|
|
155
200
|
|
|
201
|
+
def stream_raw(io, chunk_size, crc, remaining)
|
|
202
|
+
while remaining.positive? &&
|
|
203
|
+
(chunk = io.read([chunk_size, remaining].min))
|
|
204
|
+
break if chunk.empty?
|
|
205
|
+
|
|
206
|
+
remaining -= chunk.bytesize
|
|
207
|
+
crc.update(chunk)
|
|
208
|
+
yield chunk
|
|
209
|
+
end
|
|
210
|
+
true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def stream_deflate(io, chunk_size, crc, remaining, &block)
|
|
214
|
+
require "zlib"
|
|
215
|
+
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
216
|
+
begin
|
|
217
|
+
while remaining.positive? &&
|
|
218
|
+
(chunk = io.read([chunk_size, remaining].min))
|
|
219
|
+
break if chunk.empty?
|
|
220
|
+
|
|
221
|
+
remaining -= chunk.bytesize
|
|
222
|
+
emit_inflated(inflater.inflate(chunk), crc, &block)
|
|
223
|
+
end
|
|
224
|
+
emit_inflated(inflater.finish, crc, &block)
|
|
225
|
+
ensure
|
|
226
|
+
inflater.close
|
|
227
|
+
end
|
|
228
|
+
true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def emit_inflated(out, crc)
|
|
232
|
+
return if out.nil? || out.empty?
|
|
233
|
+
|
|
234
|
+
crc.update(out)
|
|
235
|
+
yield out
|
|
236
|
+
end
|
|
237
|
+
|
|
156
238
|
# Decompress and CRC-verify a single entry's data
|
|
157
239
|
def read_entry_data(entry)
|
|
158
240
|
File.open(file_path, "rb") do |io|
|
|
159
|
-
io.seek(entry
|
|
160
|
-
|
|
161
|
-
fixed_header = io.read(30)
|
|
162
|
-
_signature, _version, _flags, _method, _time, _date, _crc32,
|
|
163
|
-
_comp_size, _uncomp_size, filename_length, extra_length = fixed_header.unpack("VvvvvvVVVvv")
|
|
164
|
-
|
|
165
|
-
variable_data = io.read(filename_length + extra_length)
|
|
166
|
-
LocalFileHeader.from_binary(fixed_header + variable_data)
|
|
241
|
+
io.seek(entry_data_offset(io, entry), ::IO::SEEK_SET)
|
|
167
242
|
|
|
168
243
|
compressed_data = io.read(entry.compressed_size)
|
|
169
244
|
decompressed_data = decompress_data(
|
|
@@ -184,6 +259,19 @@ dereference_links: false)
|
|
|
184
259
|
end
|
|
185
260
|
end
|
|
186
261
|
|
|
262
|
+
# File offset of an entry's compressed payload: the local
|
|
263
|
+
# header's fixed 30 bytes plus its filename/extra fields.
|
|
264
|
+
def entry_data_offset(io, entry)
|
|
265
|
+
io.seek(entry.local_header_offset, ::IO::SEEK_SET)
|
|
266
|
+
|
|
267
|
+
fixed_header = io.read(30)
|
|
268
|
+
_signature, _version, _flags, _method, _time, _date, _crc32,
|
|
269
|
+
_comp_size, _uncomp_size, filename_length, extra_length = fixed_header.unpack("VvvvvvVVVvv")
|
|
270
|
+
|
|
271
|
+
io.read(filename_length + extra_length)
|
|
272
|
+
io.pos
|
|
273
|
+
end
|
|
274
|
+
|
|
187
275
|
# Parse the central directory once, on demand
|
|
188
276
|
def ensure_read
|
|
189
277
|
read if @entries.empty? && @central_directory.empty?
|
|
@@ -1,84 +1,111 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
3
5
|
module Omnizip
|
|
4
6
|
module Models
|
|
5
|
-
# Model for format conversion results
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
7
|
+
# Model for format conversion results.
|
|
8
|
+
#
|
|
9
|
+
# Serialized via lutaml-model — no hand-rolled +to_h+ / +to_json+.
|
|
10
|
+
# The percentage/speed keys serialize the derived methods; the
|
|
11
|
+
# attribute names differ from the keys so the framework's
|
|
12
|
+
# generated readers do not shadow the methods.
|
|
13
|
+
class ConversionResult < Lutaml::Model::Serializable
|
|
14
|
+
attribute :source_path, :string
|
|
15
|
+
attribute :target_path, :string
|
|
16
|
+
attribute :source_format, :symbol
|
|
17
|
+
attribute :target_format, :symbol
|
|
18
|
+
attribute :source_size, :integer
|
|
19
|
+
attribute :target_size, :integer
|
|
20
|
+
attribute :duration, :double
|
|
21
|
+
attribute :entry_count, :integer
|
|
22
|
+
attribute :warnings, :string, collection: true, default: []
|
|
23
|
+
|
|
24
|
+
attribute :ratio, :double, method: :compression_ratio_value
|
|
25
|
+
attribute :reduction_pct, :double, method: :size_reduction
|
|
26
|
+
attribute :ratio_pct, :double, method: :size_ratio
|
|
27
|
+
attribute :speed, :double, method: :processing_speed
|
|
28
|
+
|
|
29
|
+
key_value do
|
|
30
|
+
map "source_path", to: :source_path,
|
|
31
|
+
render_default: true, render_nil: true
|
|
32
|
+
map "target_path", to: :target_path,
|
|
33
|
+
render_default: true, render_nil: true
|
|
34
|
+
map "source_format", to: :source_format,
|
|
35
|
+
render_default: true, render_nil: true
|
|
36
|
+
map "target_format", to: :target_format,
|
|
37
|
+
render_default: true, render_nil: true
|
|
38
|
+
map "source_size", to: :source_size,
|
|
39
|
+
render_default: true, render_nil: true
|
|
40
|
+
map "target_size", to: :target_size,
|
|
41
|
+
render_default: true, render_nil: true
|
|
42
|
+
map "size_reduction", to: :reduction_pct,
|
|
43
|
+
render_default: true, render_nil: true
|
|
44
|
+
map "size_ratio", to: :ratio_pct,
|
|
45
|
+
render_default: true, render_nil: true
|
|
46
|
+
map "duration", to: :duration,
|
|
47
|
+
render_default: true, render_nil: true
|
|
48
|
+
map "entry_count", to: :entry_count,
|
|
49
|
+
render_default: true, render_nil: true
|
|
50
|
+
map "processing_speed", to: :speed,
|
|
51
|
+
render_default: true, render_nil: true
|
|
52
|
+
map "warnings", to: :warnings,
|
|
53
|
+
render_default: true, render_nil: true
|
|
54
|
+
map "compression_ratio", to: :ratio,
|
|
55
|
+
render_default: true, render_nil: true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# lutaml-model 0.8.x does not render empty-string/empty-collection
|
|
59
|
+
# defaults, so they are assigned explicitly to keep every key
|
|
60
|
+
# present in the serialized shape.
|
|
61
|
+
def initialize(attrs = {})
|
|
62
|
+
super({ warnings: [] }.merge(attrs))
|
|
42
63
|
end
|
|
43
64
|
|
|
44
65
|
# Get size reduction percentage
|
|
66
|
+
#
|
|
45
67
|
# @return [Float] Size reduction as percentage
|
|
46
68
|
def size_reduction
|
|
47
|
-
return 0.0 if source_size.zero?
|
|
69
|
+
return 0.0 if source_size.to_i.zero?
|
|
48
70
|
|
|
49
71
|
((source_size - target_size).to_f / source_size * 100).round(2)
|
|
50
72
|
end
|
|
51
73
|
|
|
52
74
|
# Get size ratio
|
|
75
|
+
#
|
|
53
76
|
# @return [Float] Target size as percentage of source size
|
|
54
77
|
def size_ratio
|
|
55
|
-
return 0.0 if source_size.zero?
|
|
78
|
+
return 0.0 if source_size.to_i.zero?
|
|
56
79
|
|
|
57
80
|
(target_size.to_f / source_size * 100).round(2)
|
|
58
81
|
end
|
|
59
82
|
|
|
60
83
|
# Check if conversion resulted in smaller file
|
|
84
|
+
#
|
|
61
85
|
# @return [Boolean] True if target is smaller
|
|
62
86
|
def smaller?
|
|
63
87
|
target_size < source_size
|
|
64
88
|
end
|
|
65
89
|
|
|
66
90
|
# Check if conversion resulted in larger file
|
|
91
|
+
#
|
|
67
92
|
# @return [Boolean] True if target is larger
|
|
68
93
|
def larger?
|
|
69
94
|
target_size > source_size
|
|
70
95
|
end
|
|
71
96
|
|
|
72
97
|
# Check if there were warnings
|
|
98
|
+
#
|
|
73
99
|
# @return [Boolean] True if warnings exist
|
|
74
100
|
def warnings?
|
|
75
101
|
!warnings.empty?
|
|
76
102
|
end
|
|
77
103
|
|
|
78
104
|
# Get average processing speed
|
|
105
|
+
#
|
|
79
106
|
# @return [Float] MB/s processing speed
|
|
80
107
|
def processing_speed
|
|
81
|
-
return 0.0 if duration.zero? || source_size.zero?
|
|
108
|
+
return 0.0 if duration.to_f.zero? || source_size.to_i.zero?
|
|
82
109
|
|
|
83
110
|
speed = source_size / duration / 1_048_576.0
|
|
84
111
|
rounded = speed.round(2)
|
|
@@ -86,26 +113,17 @@ module Omnizip
|
|
|
86
113
|
rounded.zero? && speed.positive? ? speed : rounded
|
|
87
114
|
end
|
|
88
115
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
target_format: target_format,
|
|
97
|
-
source_size: source_size,
|
|
98
|
-
target_size: target_size,
|
|
99
|
-
size_reduction: size_reduction,
|
|
100
|
-
size_ratio: size_ratio,
|
|
101
|
-
duration: duration,
|
|
102
|
-
entry_count: entry_count,
|
|
103
|
-
processing_speed: processing_speed,
|
|
104
|
-
warnings: warnings,
|
|
105
|
-
}
|
|
116
|
+
# Compression ratio (0.0-1.0 fraction saved by compression)
|
|
117
|
+
#
|
|
118
|
+
# @return [Float]
|
|
119
|
+
def compression_ratio_value
|
|
120
|
+
return 0.0 if source_size.to_i.zero?
|
|
121
|
+
|
|
122
|
+
(1.0 - (target_size.to_f / source_size)).round(4)
|
|
106
123
|
end
|
|
107
124
|
|
|
108
125
|
# Format as human-readable string
|
|
126
|
+
#
|
|
109
127
|
# @return [String] Formatted result
|
|
110
128
|
def to_s
|
|
111
129
|
"Converted #{source_path} (#{format_size(source_size)}) to " \
|
|
@@ -115,20 +133,8 @@ module Omnizip
|
|
|
115
133
|
|
|
116
134
|
private
|
|
117
135
|
|
|
118
|
-
def calculate_compression_ratio
|
|
119
|
-
return 0.0 if source_size.zero?
|
|
120
|
-
|
|
121
|
-
(1.0 - (target_size.to_f / source_size)).round(4)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
136
|
def format_size(bytes)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
units = %w[B KB MB GB TB]
|
|
128
|
-
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
129
|
-
exp = [exp, units.size - 1].min
|
|
130
|
-
|
|
131
|
-
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
137
|
+
Omnizip::CliOutputFormatter.format_size(bytes)
|
|
132
138
|
end
|
|
133
139
|
end
|
|
134
140
|
end
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
# See the COPYING file for the complete text of the license.
|
|
17
17
|
#
|
|
18
18
|
|
|
19
|
+
require "lutaml/model"
|
|
20
|
+
|
|
19
21
|
module Omnizip
|
|
20
22
|
module Models
|
|
21
23
|
# Configuration model for a filter in a compression pipeline.
|
|
@@ -23,6 +25,8 @@ module Omnizip
|
|
|
23
25
|
# This class replaces hash-based filter configuration with a proper
|
|
24
26
|
# model class. It provides format-aware ID resolution and validation.
|
|
25
27
|
#
|
|
28
|
+
# Serialized via lutaml-model — no hand-rolled +to_h+ / +to_json+.
|
|
29
|
+
#
|
|
26
30
|
# @example Create a BCJ filter configuration
|
|
27
31
|
# config = FilterConfig.new(name: :bcj_x86, architecture: :x86)
|
|
28
32
|
# config.id_for_format(:xz) # => 0x04
|
|
@@ -31,42 +35,39 @@ module Omnizip
|
|
|
31
35
|
# @example Create a Delta filter configuration
|
|
32
36
|
# config = FilterConfig.new(name: :delta)
|
|
33
37
|
# config.delta? # => true
|
|
34
|
-
class FilterConfig
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
class FilterConfig < Lutaml::Model::Serializable
|
|
39
|
+
attribute :name, :symbol
|
|
40
|
+
attribute :properties, :string, default: ""
|
|
41
|
+
attribute :architecture, :symbol
|
|
42
|
+
|
|
43
|
+
key_value do
|
|
44
|
+
map "name", to: :name, render_default: true, render_nil: true
|
|
45
|
+
map "properties", to: :properties,
|
|
46
|
+
render_default: true, render_nil: true
|
|
47
|
+
map "architecture", to: :architecture,
|
|
48
|
+
render_default: true, render_nil: true
|
|
49
|
+
end
|
|
43
50
|
|
|
44
|
-
# Initialize filter configuration.
|
|
51
|
+
# Initialize filter configuration. The legacy +name_sym+ key is
|
|
52
|
+
# accepted as an alias for +name+.
|
|
45
53
|
#
|
|
46
54
|
# @param attributes [Hash] Initialization attributes
|
|
47
55
|
# @option attributes [Symbol] :name Filter name
|
|
48
|
-
# @option attributes [Symbol] :name_sym Filter name (
|
|
56
|
+
# @option attributes [Symbol] :name_sym Filter name (alias)
|
|
49
57
|
# @option attributes [String] :properties Binary properties data
|
|
50
58
|
# @option attributes [Symbol] :architecture Target architecture
|
|
51
59
|
def initialize(attributes = {})
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
attrs = attributes.dup
|
|
61
|
+
attrs[:name] ||= attrs.delete(:name_sym)
|
|
62
|
+
attrs[:properties] ||= "" # empty-string defaults do not render
|
|
63
|
+
super(attrs)
|
|
55
64
|
end
|
|
56
65
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# @param value [Symbol] Filter name
|
|
60
|
-
# @return [void]
|
|
61
|
-
def name=(value)
|
|
62
|
-
@name_sym = value
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Get filter name as symbol.
|
|
66
|
+
# Get filter name as symbol (legacy alias for #name).
|
|
66
67
|
#
|
|
67
68
|
# @return [Symbol] Filter name
|
|
68
69
|
def name_sym
|
|
69
|
-
|
|
70
|
+
name
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# Get filter instance from registry.
|
|
@@ -74,11 +75,11 @@ module Omnizip
|
|
|
74
75
|
# @return [Object] Filter instance from FilterRegistry
|
|
75
76
|
# @raise [KeyError] If filter not found in registry
|
|
76
77
|
def filter_instance
|
|
77
|
-
filter = Omnizip::FilterRegistry.get(
|
|
78
|
+
filter = Omnizip::FilterRegistry.get(name)
|
|
78
79
|
|
|
79
80
|
# Handle architecture parameter if needed
|
|
80
|
-
if
|
|
81
|
-
filter.new(architecture:
|
|
81
|
+
if architecture && requires_initialize_kwargs?(filter)
|
|
82
|
+
filter.new(architecture: architecture)
|
|
82
83
|
else
|
|
83
84
|
filter.new
|
|
84
85
|
end
|
|
@@ -100,7 +101,7 @@ module Omnizip
|
|
|
100
101
|
filter.id_for_format(format)
|
|
101
102
|
else
|
|
102
103
|
raise NotImplementedError,
|
|
103
|
-
"Filter #{
|
|
104
|
+
"Filter #{name} doesn't support format-aware IDs. " \
|
|
104
105
|
"Use the newer BCJ filter instead."
|
|
105
106
|
end
|
|
106
107
|
end
|
|
@@ -109,14 +110,14 @@ module Omnizip
|
|
|
109
110
|
#
|
|
110
111
|
# @return [Boolean] True if BCJ filter variant
|
|
111
112
|
def bcj?
|
|
112
|
-
|
|
113
|
+
name.to_s.start_with?("bcj_")
|
|
113
114
|
end
|
|
114
115
|
|
|
115
116
|
# Check if this is a Delta filter.
|
|
116
117
|
#
|
|
117
118
|
# @return [Boolean] True if Delta filter
|
|
118
119
|
def delta?
|
|
119
|
-
|
|
120
|
+
name == :delta
|
|
120
121
|
end
|
|
121
122
|
|
|
122
123
|
# Validate configuration.
|
|
@@ -124,26 +125,15 @@ module Omnizip
|
|
|
124
125
|
# @return [Boolean] True if valid
|
|
125
126
|
# @raise [ArgumentError] If filter name is nil or not registered
|
|
126
127
|
def validate!
|
|
127
|
-
raise ArgumentError, "Filter name is required" if
|
|
128
|
+
raise ArgumentError, "Filter name is required" if name.nil?
|
|
128
129
|
|
|
129
|
-
unless Omnizip::FilterRegistry.registered?(
|
|
130
|
-
raise ArgumentError, "Filter not registered: #{
|
|
130
|
+
unless Omnizip::FilterRegistry.registered?(name)
|
|
131
|
+
raise ArgumentError, "Filter not registered: #{name}"
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
true
|
|
134
135
|
end
|
|
135
136
|
|
|
136
|
-
# Convert to hash representation.
|
|
137
|
-
#
|
|
138
|
-
# @return [Hash] Hash representation with :name, :properties, :architecture
|
|
139
|
-
def to_h
|
|
140
|
-
{
|
|
141
|
-
name: @name_sym,
|
|
142
|
-
properties: @properties,
|
|
143
|
-
architecture: @architecture,
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
|
-
|
|
147
137
|
private
|
|
148
138
|
|
|
149
139
|
# Check if filter class requires keyword arguments for initialize.
|