cabriolet 0.2.3 → 0.2.4

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: 59d85958b00fa7eb684912e7ec77bfd9ce261a01035ed7ef42c2d6db5b1405a7
4
- data.tar.gz: e9fa2123fe7c48778a01018c68f47cdb66f68449fe4fee48074e2b7591d5b9e1
3
+ metadata.gz: cb8a4d045d1bc49ab6448b9d1890524386e743d27249ad5dd5c5e630140e8a20
4
+ data.tar.gz: c63202e8fe947a0d14b3f4ffaca42feae67dc04e392ce3f9eac00d90ab0293b1
5
5
  SHA512:
6
- metadata.gz: ddc9ef226ce8359cbe65ac4e80853dbd8e57f25c6e934e48837a163f614bcdbdf94f18609c788c6396f4b87f93d4e12c1fa326c8f291bc1156ec0eaf0387d377
7
- data.tar.gz: 20206a2b5011d4a869d0f4d29085b5b874fe1d34daa00ce14f842d55a71722c19329ea75b1f46adaebf88fc43805a467b089e038aa19fe5736010d1a9ca951ad
6
+ metadata.gz: 6f6111054718818537222c48d986ef6a6112c89b9ea019ba748176402142316e7c2eef54979904e3bc7078f8d3b809193afcfe4acf971e9498d5cf4e7c4020dd
7
+ data.tar.gz: 8c0f0b712a10a5751f267cb67afe3c7d369ef6f06df0f961a2e92168f288c667fd97503e4fbfb95531898a8fec4303202e29b093814618c0065a7fbba8e3701f
@@ -91,7 +91,7 @@ module Cabriolet
91
91
  # compressor = factory.create(3, :compressor,
92
92
  # io, input, output, 8192)
93
93
  def create(type, category, io_system, input, output, buffer_size,
94
- **kwargs)
94
+ **)
95
95
  validate_category!(category)
96
96
 
97
97
  normalized_type = normalize_type(type)
@@ -103,7 +103,7 @@ module Cabriolet
103
103
  end
104
104
 
105
105
  algorithm_info[:class].new(io_system, input, output, buffer_size,
106
- **kwargs)
106
+ **)
107
107
  end
108
108
 
109
109
  # Check if an algorithm is registered
@@ -57,8 +57,8 @@ module Cabriolet
57
57
  # @param options [Hash] Format-specific options
58
58
  # @return [FileEntry] Added entry
59
59
  # @raise [ArgumentError] if file doesn't exist
60
- def add_file(source_path, archive_path = nil, **options)
61
- @file_manager.add_file(source_path, archive_path, **options)
60
+ def add_file(source_path, archive_path = nil, **)
61
+ @file_manager.add_file(source_path, archive_path, **)
62
62
  end
63
63
 
64
64
  # Add file from memory to archive
@@ -67,8 +67,8 @@ module Cabriolet
67
67
  # @param archive_path [String] Path in archive
68
68
  # @param options [Hash] Format-specific options
69
69
  # @return [FileEntry] Added entry
70
- def add_data(data, archive_path, **options)
71
- @file_manager.add_data(data, archive_path, **options)
70
+ def add_data(data, archive_path, **)
71
+ @file_manager.add_data(data, archive_path, **)
72
72
  end
73
73
 
74
74
  # Generate archive (Template Method)
@@ -165,7 +165,7 @@ module Cabriolet
165
165
  # @option options [Integer] :window_bits Window size in bits
166
166
  # @option options [Integer] :mode Algorithm mode
167
167
  # @return [String] Compressed data
168
- def compress_data(data, algorithm:, **options)
168
+ def compress_data(data, algorithm:, **)
169
169
  input = System::MemoryHandle.new(data)
170
170
  output = System::MemoryHandle.new("", Constants::MODE_WRITE)
171
171
 
@@ -176,7 +176,7 @@ module Cabriolet
176
176
  input,
177
177
  output,
178
178
  data.bytesize,
179
- **options,
179
+ **,
180
180
  )
181
181
 
182
182
  compressor.compress
@@ -36,9 +36,9 @@ module Cabriolet
36
36
  # @param output_path [String] Where to write the file
37
37
  # @param options [Hash] Extraction options
38
38
  # @return [Integer] Number of bytes extracted
39
- def extract_file(file, output_path, **options)
39
+ def extract_file(file, output_path, **)
40
40
  extractor = Extractor.new(@io_system, self)
41
- extractor.extract_file(file, output_path, **options)
41
+ extractor.extract_file(file, output_path, **)
42
42
  end
43
43
 
44
44
  # Extract all files from the cabinet
@@ -47,9 +47,9 @@ module Cabriolet
47
47
  # @param output_dir [String] Directory to extract to
48
48
  # @param options [Hash] Extraction options
49
49
  # @return [Integer] Number of files extracted
50
- def extract_all(cabinet, output_dir, **options)
50
+ def extract_all(cabinet, output_dir, **)
51
51
  extractor = Extractor.new(@io_system, self)
52
- extractor.extract_all(cabinet, output_dir, **options)
52
+ extractor.extract_all(cabinet, output_dir, **)
53
53
  end
54
54
 
55
55
  # Create appropriate decompressor for a folder
@@ -60,7 +60,8 @@ module Cabriolet
60
60
  begin
61
61
  write_file_data(output_fh, filelen)
62
62
  rescue DecompressionError
63
- handle_extraction_error(output_fh, output_path, file.filename, salvage, filelen)
63
+ handle_extraction_error(output_fh, output_path, file.filename,
64
+ salvage, filelen)
64
65
  ensure
65
66
  output_fh.close
66
67
  end
@@ -72,6 +73,7 @@ module Cabriolet
72
73
  def reset_state
73
74
  @current_input&.close
74
75
  @current_input = nil
76
+ @current_decomp&.free # Free decompressor buffers to prevent memory leaks
75
77
  @current_decomp = nil
76
78
  @current_folder = nil
77
79
  @current_offset = 0
@@ -136,6 +138,9 @@ module Cabriolet
136
138
  warn "Salvage: #{failed_count} file(s) skipped due to extraction errors" if failed_count.positive?
137
139
 
138
140
  count
141
+ ensure
142
+ # Clean up resources to prevent memory leaks
143
+ reset_state
139
144
  end
140
145
 
141
146
  private
@@ -264,7 +269,8 @@ module Cabriolet
264
269
  # @param filelen [Integer] Number of bytes to write
265
270
  def write_file_data(output_fh, filelen)
266
271
  unless @current_decomp
267
- raise DecompressionError, "Decompressor not available (state was reset)"
272
+ raise DecompressionError,
273
+ "Decompressor not available (state was reset)"
268
274
  end
269
275
 
270
276
  @current_decomp.instance_variable_set(:@output, output_fh)
@@ -279,7 +285,8 @@ module Cabriolet
279
285
  # @param filename [String] Filename for error messages
280
286
  # @param salvage [Boolean] Salvage mode flag
281
287
  # @raise [DecompressionError] If not in salvage mode
282
- def handle_extraction_error(output_fh, output_path, filename, salvage, _filelen)
288
+ def handle_extraction_error(output_fh, output_path, filename, salvage,
289
+ _filelen)
283
290
  output_fh.close
284
291
  if salvage
285
292
  ::File.write(output_path, "", mode: "wb")
@@ -23,6 +23,9 @@ module Cabriolet
23
23
  cabinet = parse_handle(handle, filename)
24
24
  @io_system.close(handle)
25
25
  cabinet
26
+ rescue StandardError
27
+ @io_system.close(handle) if handle
28
+ raise
26
29
  end
27
30
 
28
31
  # Parse a CAB from an already-open handle
data/lib/cabriolet/cli.rb CHANGED
@@ -422,11 +422,11 @@ module Cabriolet
422
422
  # @param command [Symbol] Command to execute
423
423
  # @param file [String] File path
424
424
  # @param args [Array] Additional arguments
425
- def run_dispatcher(command, file, *args, **options)
425
+ def run_dispatcher(command, file, *, **options)
426
426
  setup_verbose(options[:verbose])
427
427
 
428
428
  dispatcher = Commands::CommandDispatcher.new(**options)
429
- dispatcher.dispatch(command, file, *args, **options)
429
+ dispatcher.dispatch(command, file, *, **options)
430
430
  end
431
431
 
432
432
  # Run command with explicit format override
@@ -435,12 +435,12 @@ module Cabriolet
435
435
  # @param format [Symbol] Format to force
436
436
  # @param file [String] File path
437
437
  # @param args [Array] Additional arguments
438
- def run_with_format(command, format, file, *args, **options)
438
+ def run_with_format(command, format, file, *, **options)
439
439
  setup_verbose(options[:verbose])
440
440
  options[:format] = format.to_s
441
441
 
442
442
  dispatcher = Commands::CommandDispatcher.new(**options)
443
- dispatcher.dispatch(command, file, *args, **options)
443
+ dispatcher.dispatch(command, file, *, **options)
444
444
  end
445
445
 
446
446
  # Detect format from output file extension
@@ -178,6 +178,23 @@ module Cabriolet
178
178
  @output_length = length if length.positive?
179
179
  end
180
180
 
181
+ # Free resources used by the decompressor
182
+ #
183
+ # Releases large memory buffers to prevent memory leaks when
184
+ # the decompressor is no longer needed.
185
+ #
186
+ # @return [void]
187
+ def free
188
+ @window = nil
189
+ @e8_buf = nil
190
+ @pending_frame_data = nil
191
+ @bitstream = nil
192
+ @maintree_lengths = nil
193
+ @length_lengths = nil
194
+ @pretree_lengths = nil
195
+ @aligned_lengths = nil
196
+ end
197
+
181
198
  # Decompress LZX data
182
199
  #
183
200
  # Per libmspack lzxd.c: the decompressor always decodes full frames
@@ -201,7 +218,9 @@ module Cabriolet
201
218
  if @pending_frame_data
202
219
  avail = @pending_frame_data.bytesize - @pending_frame_offset
203
220
  write_amount = [bytes, avail].min
204
- io_system.write(output, @pending_frame_data[@pending_frame_offset, write_amount])
221
+ io_system.write(output,
222
+ @pending_frame_data[@pending_frame_offset,
223
+ write_amount])
205
224
  total_written += write_amount
206
225
  @offset += write_amount
207
226
  @pending_frame_offset += write_amount
@@ -83,6 +83,21 @@ salvage: false, **_kwargs)
83
83
  @debug_mszip_symbols = ENV.fetch("DEBUG_MSZIP_SYMBOLS", nil)
84
84
  end
85
85
 
86
+ # Free resources used by the decompressor
87
+ #
88
+ # Releases memory buffers to prevent memory leaks when
89
+ # the decompressor is no longer needed.
90
+ #
91
+ # @return [void]
92
+ def free
93
+ @window = nil
94
+ @bitstream = nil
95
+ @literal_lengths = nil
96
+ @distance_lengths = nil
97
+ @literal_tree = nil
98
+ @distance_tree = nil
99
+ end
100
+
86
101
  # Decompress MSZIP data
87
102
  #
88
103
  # @param bytes [Integer] Number of bytes to decompress
@@ -2,30 +2,16 @@
2
2
 
3
3
  require_relative "../quantum_shared"
4
4
 
5
- # Compatibility shim for String#bytesplice (added in Ruby 3.2)
6
- unless String.method_defined?(:bytesplice)
7
- module StringBytespliceCompat
8
- # Compatibility implementation of bytesplice for Ruby < 3.2
9
- # Uses clear/append which is slower but works with mutable strings
10
- def bytesplice(index, length, other_string, other_index = 0,
11
- other_length = nil)
12
- other_length ||= other_string.bytesize
13
-
14
- # Build new string content
15
- prefix = byteslice(0, index)
16
- middle = other_string.byteslice(other_index, other_length)
17
- suffix = byteslice((index + length)..-1)
18
- new_content = prefix + middle + suffix
19
-
20
- # Modify receiver in place
21
- clear
22
- self << new_content
23
-
24
- self
5
+ # Helper for 5-argument bytesplice (added in Ruby 3.3)
6
+ # Ruby 3.2 has bytesplice but only 2-3 argument forms
7
+ unless String.method_defined?(:window_splice)
8
+ class String
9
+ # Copy bytes from source string into self at specified position
10
+ # Works on all Ruby versions including 3.2 which lacks 5-arg bytesplice
11
+ def window_splice(dest_idx, dest_len, src, src_idx, src_len)
12
+ self[dest_idx, dest_len] = src.byteslice(src_idx, src_len)
25
13
  end
26
14
  end
27
-
28
- String.prepend(StringBytespliceCompat)
29
15
  end
30
16
 
31
17
  module Cabriolet
@@ -61,11 +47,7 @@ module Cabriolet
61
47
  @window_size = 1 << window_bits
62
48
 
63
49
  # Initialize window (must be binary to avoid UTF-8 character vs byte mismatch)
64
- @window = if String.method_defined?(:bytesplice)
65
- ("\0" * @window_size).b
66
- else
67
- String.new("\0" * @window_size, encoding: Encoding::BINARY)
68
- end
50
+ @window = ("\0" * @window_size).b
69
51
  @window_posn = 0
70
52
  @frame_todo = FRAME_SIZE
71
53
 
@@ -82,6 +64,21 @@ module Cabriolet
82
64
  initialize_models
83
65
  end
84
66
 
67
+ # Free resources used by the decompressor
68
+ #
69
+ # Releases memory buffers to prevent memory leaks when
70
+ # the decompressor is no longer needed.
71
+ #
72
+ # @return [void]
73
+ def free
74
+ @window = nil
75
+ @bitstream = nil
76
+ @m0sym = @m1sym = @m2sym = @m3sym = nil
77
+ @m4sym = @m5sym = @m6sym = @m6lsym = nil
78
+ @model0 = @model1 = @model2 = @model3 = nil
79
+ @model4 = @model5 = @model6 = @model6len = nil
80
+ end
81
+
85
82
  # Decompress Quantum data
86
83
  #
87
84
  # @param bytes [Integer] Number of bytes to decompress
@@ -402,7 +399,7 @@ module Cabriolet
402
399
  end
403
400
  end
404
401
 
405
- # Bulk copy using bytesplice for better performance on longer matches
402
+ # Bulk copy using window_splice for better performance on longer matches
406
403
  def copy_match_bulk(offset, length)
407
404
  if offset > @window_posn
408
405
  # Match wraps around window
@@ -417,21 +414,23 @@ module Cabriolet
417
414
 
418
415
  if copy_len < length
419
416
  # Copy from end, then from beginning
420
- @window.bytesplice(@window_posn, copy_len, @window, src_pos,
421
- copy_len)
417
+ @window.window_splice(@window_posn, copy_len, @window, src_pos,
418
+ copy_len)
422
419
  @window_posn += copy_len
423
420
  remaining = length - copy_len
424
- @window.bytesplice(@window_posn, remaining, @window, 0, remaining)
421
+ @window.window_splice(@window_posn, remaining, @window, 0,
422
+ remaining)
425
423
  @window_posn += remaining
426
424
  else
427
425
  # Copy entirely from end
428
- @window.bytesplice(@window_posn, length, @window, src_pos, length)
426
+ @window.window_splice(@window_posn, length, @window, src_pos,
427
+ length)
429
428
  @window_posn += length
430
429
  end
431
430
  else
432
- # Normal copy - use bytesplice for bulk operation
431
+ # Normal copy - use window_splice for bulk operation
433
432
  src_pos = @window_posn - offset
434
- @window.bytesplice(@window_posn, length, @window, src_pos, length)
433
+ @window.window_splice(@window_posn, length, @window, src_pos, length)
435
434
  @window_posn += length
436
435
  end
437
436
  end
@@ -29,13 +29,13 @@ module Cabriolet
29
29
  # @param options [Hash] Format-specific options
30
30
  # @return [FileEntry] Added entry
31
31
  # @raise [ArgumentError] if file doesn't exist
32
- def add_file(source_path, archive_path = nil, **options)
32
+ def add_file(source_path, archive_path = nil, **)
33
33
  archive_path ||= File.basename(source_path)
34
34
 
35
35
  entry = FileEntry.new(
36
36
  source: source_path,
37
37
  archive_path: archive_path,
38
- **options,
38
+ **,
39
39
  )
40
40
 
41
41
  @entries << entry
@@ -48,11 +48,11 @@ module Cabriolet
48
48
  # @param archive_path [String] Path in archive
49
49
  # @param options [Hash] Format-specific options
50
50
  # @return [FileEntry] Added entry
51
- def add_data(data, archive_path, **options)
51
+ def add_data(data, archive_path, **)
52
52
  entry = FileEntry.new(
53
53
  data: data,
54
54
  archive_path: archive_path,
55
- **options,
55
+ **,
56
56
  )
57
57
 
58
58
  @entries << entry
@@ -44,7 +44,7 @@ module Cabriolet
44
44
  # @param size [Integer] Data size
45
45
  # @param options [Hash] Additional options for the compressor
46
46
  # @return [Object] Compressor instance
47
- def create_compressor(algorithm, input, output, size, **options)
47
+ def create_compressor(algorithm, input, output, size, **)
48
48
  @algorithm_factory.create(
49
49
  algorithm,
50
50
  :compressor,
@@ -52,7 +52,7 @@ module Cabriolet
52
52
  input,
53
53
  output,
54
54
  size,
55
- **options,
55
+ **,
56
56
  )
57
57
  end
58
58
 
@@ -64,7 +64,7 @@ module Cabriolet
64
64
  # @param size [Integer] Data size
65
65
  # @param options [Hash] Additional options for the decompressor
66
66
  # @return [Object] Decompressor instance
67
- def create_decompressor(algorithm, input, output, size, **options)
67
+ def create_decompressor(algorithm, input, output, size, **)
68
68
  @algorithm_factory.create(
69
69
  algorithm,
70
70
  :decompressor,
@@ -72,7 +72,7 @@ module Cabriolet
72
72
  input,
73
73
  output,
74
74
  size,
75
- **options,
75
+ **,
76
76
  )
77
77
  end
78
78
  end
@@ -45,8 +45,8 @@ module Cabriolet
45
45
  # @param output_file [String] Output file path
46
46
  # @param options [Hash] Format options
47
47
  # @return [Integer] Bytes written
48
- def generate(output_file, **options)
49
- @quickhelp.generate(output_file, **options)
48
+ def generate(output_file, **)
49
+ @quickhelp.generate(output_file, **)
50
50
  end
51
51
 
52
52
  # Create a Windows Help format HLP file
@@ -190,10 +190,10 @@ module Cabriolet
190
190
  # @example Register a format-specific decompressor
191
191
  # register_algorithm(:special, SpecialDecompressor,
192
192
  # category: :decompressor, format: :cab)
193
- def register_algorithm(type, klass, **options)
193
+ def register_algorithm(type, klass, **)
194
194
  raise PluginError, "Plugin manager not available" unless @manager
195
195
 
196
- Cabriolet.algorithm_factory.register(type, klass, **options)
196
+ Cabriolet.algorithm_factory.register(type, klass, **)
197
197
  end
198
198
 
199
199
  # Register a format handler
@@ -188,9 +188,9 @@ module Cabriolet
188
188
  # @param paths [Array<String>] Array of archive paths
189
189
  # @yield [file, archive_path] Yields each file with its archive path
190
190
  # @return [Hash] Processing statistics
191
- def process_archives(paths, &block)
191
+ def process_archives(paths, &)
192
192
  paths.each do |path|
193
- process_archive(path, &block)
193
+ process_archive(path, &)
194
194
  end
195
195
 
196
196
  @stats
@@ -324,9 +324,9 @@ module Cabriolet
324
324
  }
325
325
  end
326
326
 
327
- def to_json(*args)
327
+ def to_json(*)
328
328
  require "json"
329
- to_h.to_json(*args)
329
+ to_h.to_json(*)
330
330
  end
331
331
  end
332
332
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cabriolet
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
data/lib/cabriolet.rb CHANGED
@@ -169,7 +169,7 @@ module Cabriolet
169
169
  # @example
170
170
  # archive = Cabriolet.open('unknown.archive')
171
171
  # archive.files.each { |f| puts f.name }
172
- def open(path, **options)
172
+ def open(path, **)
173
173
  parser_class = FormatDetector.parser_for(path)
174
174
 
175
175
  unless parser_class
@@ -178,7 +178,7 @@ module Cabriolet
178
178
  "Unable to detect format or no parser available for: #{path} (detected: #{format || 'unknown'})"
179
179
  end
180
180
 
181
- parser_class.new(**options).parse(path)
181
+ parser_class.new(**).parse(path)
182
182
  end
183
183
 
184
184
  # Detect format of an archive file
@@ -209,9 +209,9 @@ module Cabriolet
209
209
  # @example Parallel extraction with 8 workers
210
210
  # stats = Cabriolet.extract('file.chm', 'docs/', workers: 8)
211
211
  # puts "Extracted #{stats[:extracted]} files"
212
- def extract(archive_path, output_dir, **options)
212
+ def extract(archive_path, output_dir, **)
213
213
  archive = open(archive_path)
214
- extractor = Extraction::Extractor.new(archive, output_dir, **options)
214
+ extractor = Extraction::Extractor.new(archive, output_dir, **)
215
215
  extractor.extract_all
216
216
  end
217
217
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cabriolet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-06 00:00:00.000000000 Z
11
+ date: 2026-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bindata