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
@@ -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
@@ -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.local_header_offset, ::IO::SEEK_SET)
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
- class ConversionResult
7
- attr_reader :source_path, :target_path, :source_format, :target_format,
8
- :source_size, :target_size, :duration, :entry_count,
9
- :compression_ratio, :warnings
10
-
11
- # Initialize conversion result
12
- # @param source_path [String] Source file path
13
- # @param target_path [String] Target file path
14
- # @param source_format [Symbol] Source format
15
- # @param target_format [Symbol] Target format
16
- # @param source_size [Integer] Source file size in bytes
17
- # @param target_size [Integer] Target file size in bytes
18
- # @param duration [Float] Conversion duration in seconds
19
- # @param entry_count [Integer] Number of entries converted
20
- # @param warnings [Array<String>] Conversion warnings
21
- def initialize(
22
- source_path:,
23
- target_path:,
24
- source_format:,
25
- target_format:,
26
- source_size:,
27
- target_size:,
28
- duration:,
29
- entry_count:,
30
- warnings: []
31
- )
32
- @source_path = source_path
33
- @target_path = target_path
34
- @source_format = source_format
35
- @target_format = target_format
36
- @source_size = source_size
37
- @target_size = target_size
38
- @duration = duration
39
- @entry_count = entry_count
40
- @warnings = warnings
41
- @compression_ratio = calculate_compression_ratio
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
- # Convert to hash
90
- # @return [Hash] Result as hash
91
- def to_h
92
- {
93
- source_path: source_path,
94
- target_path: target_path,
95
- source_format: source_format,
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
- return "0 B" if bytes.zero?
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
- # @return [Symbol] Filter name (:bcj_x86, :delta, etc.)
36
- attr_reader :name_sym
37
-
38
- # @return [String] Binary properties data for filter configuration
39
- attr_accessor :properties
40
-
41
- # @return [Symbol] Target architecture for BCJ filters
42
- attr_accessor :architecture
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 (alternative key)
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
- @name_sym = attributes[:name] || attributes[:name_sym]
53
- @properties = attributes[:properties] || "".b
54
- @architecture = attributes[:architecture]
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
- # Set filter name.
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
- @name_sym
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(@name_sym)
78
+ filter = Omnizip::FilterRegistry.get(name)
78
79
 
79
80
  # Handle architecture parameter if needed
80
- if @architecture && requires_initialize_kwargs?(filter)
81
- filter.new(architecture: @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 #{@name_sym} doesn't support format-aware IDs. " \
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
- @name_sym.to_s.start_with?("bcj_")
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
- @name_sym == :delta
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 @name_sym.nil?
128
+ raise ArgumentError, "Filter name is required" if name.nil?
128
129
 
129
- unless Omnizip::FilterRegistry.registered?(@name_sym)
130
- raise ArgumentError, "Filter not registered: #{@name_sym}"
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.