cabriolet 0.2.2 → 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: 9d2875b9afed58332c6e9823f3f383a60d802bac8514cb2015fdbd6a7f1559dc
4
- data.tar.gz: 6979ce57ad3d47867bed19330d70b5db483bc33ac8920ee2986faa35828d6cbc
3
+ metadata.gz: cb8a4d045d1bc49ab6448b9d1890524386e743d27249ad5dd5c5e630140e8a20
4
+ data.tar.gz: c63202e8fe947a0d14b3f4ffaca42feae67dc04e392ce3f9eac00d90ab0293b1
5
5
  SHA512:
6
- metadata.gz: edd7b1345bee36e75fb5796a3840f087a9dfa51e41c599fa767938e8d64ab0abc985e563b9fb246fc86fc4798eb3f98539f774023eaf613f50a1c9e4a2a6d518
7
- data.tar.gz: c5cfc76ae8dc5239efa9e90d0956d2518eab56c669d4b7e4087debc4955f509cc97440296c53084152cb30059b25044b6231cd9a84ee94e85f446a20f3306e08
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
@@ -4,7 +4,7 @@ module Cabriolet
4
4
  module Binary
5
5
  # Bitstream provides bit-level I/O operations for reading compressed data
6
6
  class Bitstream
7
- attr_reader :io_system, :handle, :buffer_size, :bit_order
7
+ attr_reader :io_system, :handle, :buffer_size, :bit_order, :bits_left
8
8
 
9
9
  # Initialize a new bitstream
10
10
  #
@@ -29,6 +29,9 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
29
29
  # For MSB mode, we need to know the bit width of the buffer
30
30
  # Ruby integers are arbitrary precision, so we use 32 bits as standard
31
31
  @bitbuf_width = 32
32
+
33
+ # Cache ENV lookups once at initialization
34
+ @debug_bitstream = ENV.fetch("DEBUG_BITSTREAM", nil)
32
35
  end
33
36
 
34
37
  # Read specified number of bits from the stream
@@ -83,7 +86,7 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
83
86
  byte = 0 if byte.nil?
84
87
 
85
88
  # DEBUG
86
- if ENV["DEBUG_BITSTREAM"]
89
+ if @debug_bitstream
87
90
  warn "DEBUG LSB read_byte: buffer_pos=#{@buffer_pos} byte=#{byte} (#{byte.to_s(2).rjust(
88
91
  8, '0'
89
92
  )}) bits_left=#{@bits_left}"
@@ -101,7 +104,7 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
101
104
  @bits_left -= num_bits
102
105
 
103
106
  # DEBUG
104
- warn "DEBUG LSB read_bits(#{num_bits}): result=#{result} buffer=#{@bit_buffer.to_s(16)} bits_left=#{@bits_left}" if ENV["DEBUG_BITSTREAM"]
107
+ warn "DEBUG LSB read_bits(#{num_bits}): result=#{result} buffer=#{@bit_buffer.to_s(16)} bits_left=#{@bits_left}" if @debug_bitstream
105
108
 
106
109
  result
107
110
  end
@@ -116,7 +119,7 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
116
119
  word = read_msb_word
117
120
 
118
121
  # DEBUG
119
- warn "DEBUG MSB read_bytes: word=0x#{word.to_s(16)} bits_left=#{@bits_left}" if ENV["DEBUG_BITSTREAM"]
122
+ warn "DEBUG MSB read_bytes: word=0x#{word.to_s(16)} bits_left=#{@bits_left}" if @debug_bitstream
120
123
 
121
124
  # INJECT_BITS (MSB): inject at the left side
122
125
  @bit_buffer |= (word << (@bitbuf_width - 16 - @bits_left))
@@ -131,7 +134,7 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
131
134
  @bits_left -= num_bits
132
135
 
133
136
  # DEBUG
134
- warn "DEBUG MSB read_bits(#{num_bits}) result=#{result} (0x#{result.to_s(16)}) buffer=0x#{@bit_buffer.to_s(16)} bits_left=#{@bits_left}" if ENV["DEBUG_BITSTREAM"]
137
+ warn "DEBUG MSB read_bits(#{num_bits}) result=#{result} (0x#{result.to_s(16)}) buffer=0x#{@bit_buffer.to_s(16)} bits_left=#{@bits_left}" if @debug_bitstream
135
138
 
136
139
  result
137
140
  end
@@ -172,15 +175,62 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
172
175
  byte
173
176
  end
174
177
 
178
+ # Ensure at least num_bits are available in the bit buffer.
179
+ # Reads from input if needed. Used for alignment operations.
180
+ #
181
+ # @param num_bits [Integer] Minimum number of bits required
182
+ # @return [void]
183
+ def ensure_bits(num_bits)
184
+ if @bit_order == :msb
185
+ while @bits_left < num_bits
186
+ word = read_msb_word
187
+ @bit_buffer |= (word << (@bitbuf_width - 16 - @bits_left))
188
+ @bits_left += 16
189
+ end
190
+ else
191
+ while @bits_left < num_bits
192
+ byte = read_byte
193
+ byte = 0 if byte.nil?
194
+ @bit_buffer |= (byte << @bits_left)
195
+ @bits_left += 8
196
+ end
197
+ end
198
+ end
199
+
175
200
  # Align to the next byte boundary
176
201
  #
177
202
  # @return [void]
178
203
  def byte_align
179
204
  discard_bits = @bits_left % 8
180
- @bit_buffer >>= discard_bits
205
+ if @bit_order == :msb
206
+ # MSB mode: valid bits are at the left (high) end, shift left to discard
207
+ @bit_buffer = (@bit_buffer << discard_bits) & ((1 << @bitbuf_width) - 1)
208
+ else
209
+ @bit_buffer >>= discard_bits
210
+ end
181
211
  @bits_left -= discard_bits
182
212
  end
183
213
 
214
+ # Flush the bit buffer entirely (discard all remaining bits).
215
+ # Per libmspack lzxd.c: used when transitioning to raw byte reading
216
+ # for uncompressed blocks. Sets bits_left=0 and bit_buffer=0.
217
+ #
218
+ # @return [void]
219
+ def flush_bit_buffer
220
+ @bit_buffer = 0
221
+ @bits_left = 0
222
+ end
223
+
224
+ # Read a raw byte directly from the input, bypassing the bit buffer.
225
+ # Per libmspack lzxd.c: uncompressed block headers and data are read
226
+ # directly from the input pointer (i_ptr), not through the bitstream.
227
+ # Call flush_bit_buffer first to discard any residual bits.
228
+ #
229
+ # @return [Integer] Byte value (0 on EOF)
230
+ def read_raw_byte
231
+ read_byte || 0
232
+ end
233
+
184
234
  # Peek at bits without consuming them
185
235
  #
186
236
  # @param num_bits [Integer] Number of bits to peek at
@@ -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
@@ -157,8 +157,11 @@ module Cabriolet
157
157
  offset = cab_offset + 4
158
158
  end
159
159
  else
160
- # No cabinet found in this chunk, move to next
161
- offset += length
160
+ # No cabinet found in this chunk, move to next.
161
+ # Overlap by 20 bytes so MSCF signatures spanning chunk
162
+ # boundaries are not missed (state machine reads 20 bytes).
163
+ overlap = length > 20 ? 20 : 0
164
+ offset += [length - overlap, 1].max
162
165
  end
163
166
  end
164
167
 
@@ -452,7 +455,7 @@ file_length)
452
455
  cablen_u32, caboff, file_length)
453
456
 
454
457
  # Not valid, restart search after "MSCF"
455
- return nil
458
+ state = 0
456
459
  end
457
460
  end
458
461
 
@@ -22,6 +22,9 @@ module Cabriolet
22
22
  @current_decomp = nil
23
23
  @current_input = nil
24
24
  @current_offset = 0
25
+
26
+ # Cache ENV lookups once at initialization
27
+ @debug_block = ENV.fetch("DEBUG_BLOCK", nil)
25
28
  end
26
29
 
27
30
  # Extract a single file from the cabinet
@@ -57,7 +60,8 @@ module Cabriolet
57
60
  begin
58
61
  write_file_data(output_fh, filelen)
59
62
  rescue DecompressionError
60
- handle_extraction_error(output_fh, output_path, file.filename, salvage, filelen)
63
+ handle_extraction_error(output_fh, output_path, file.filename,
64
+ salvage, filelen)
61
65
  ensure
62
66
  output_fh.close
63
67
  end
@@ -69,6 +73,7 @@ module Cabriolet
69
73
  def reset_state
70
74
  @current_input&.close
71
75
  @current_input = nil
76
+ @current_decomp&.free # Free decompressor buffers to prevent memory leaks
72
77
  @current_decomp = nil
73
78
  @current_folder = nil
74
79
  @current_offset = 0
@@ -133,6 +138,9 @@ module Cabriolet
133
138
  warn "Salvage: #{failed_count} file(s) skipped due to extraction errors" if failed_count.positive?
134
139
 
135
140
  count
141
+ ensure
142
+ # Clean up resources to prevent memory leaks
143
+ reset_state
136
144
  end
137
145
 
138
146
  private
@@ -185,7 +193,7 @@ module Cabriolet
185
193
  # @param salvage [Boolean] Salvage mode flag
186
194
  # @param file_offset [Integer] File offset for reset condition check
187
195
  def setup_decompressor_for_folder(folder, salvage, file_offset)
188
- if ENV["DEBUG_BLOCK"]
196
+ if @debug_block
189
197
  warn "DEBUG extract_file: Checking reset condition"
190
198
  warn " @current_folder == folder: #{@current_folder == folder}"
191
199
  warn " @current_offset (#{@current_offset}) > file_offset (#{file_offset})"
@@ -193,7 +201,7 @@ module Cabriolet
193
201
  end
194
202
 
195
203
  if @current_folder != folder || @current_offset > file_offset || !@current_decomp
196
- if ENV["DEBUG_BLOCK"]
204
+ if @debug_block
197
205
  warn "DEBUG extract_file: RESETTING state (creating new BlockReader)"
198
206
  end
199
207
 
@@ -211,7 +219,20 @@ module Cabriolet
211
219
  # Create decompressor ONCE and reuse it
212
220
  @current_decomp = @decompressor.create_decompressor(folder,
213
221
  @current_input, nil)
214
- elsif ENV["DEBUG_BLOCK"]
222
+
223
+ # Per libmspack cabd.c: set output_length from the folder's total
224
+ # uncompressed size (max file.offset + file.length across all files
225
+ # in the folder). This allows the LZX decompressor to reduce the
226
+ # last frame's size so it doesn't read past the end of the stream.
227
+ if @current_decomp.respond_to?(:set_output_length)
228
+ cab = folder.data&.cabinet
229
+ if cab&.files
230
+ folder_files = cab.files.select { |f| f.folder == folder }
231
+ max_end = folder_files.map { |f| f.offset + f.length }.max
232
+ @current_decomp.set_output_length(max_end) if max_end&.positive?
233
+ end
234
+ end
235
+ elsif @debug_block
215
236
  warn "DEBUG extract_file: NOT resetting (reusing existing BlockReader)"
216
237
  end
217
238
  end
@@ -228,7 +249,6 @@ module Cabriolet
228
249
  null_output = System::MemoryHandle.new("", Constants::MODE_WRITE)
229
250
 
230
251
  @current_decomp.instance_variable_set(:@output, null_output)
231
- @current_decomp.set_output_length(skip_bytes) if @current_decomp.respond_to?(:set_output_length)
232
252
 
233
253
  begin
234
254
  @current_decomp.decompress(skip_bytes)
@@ -248,8 +268,12 @@ module Cabriolet
248
268
  # @param output_fh [System::FileHandle] Output file handle
249
269
  # @param filelen [Integer] Number of bytes to write
250
270
  def write_file_data(output_fh, filelen)
271
+ unless @current_decomp
272
+ raise DecompressionError,
273
+ "Decompressor not available (state was reset)"
274
+ end
275
+
251
276
  @current_decomp.instance_variable_set(:@output, output_fh)
252
- @current_decomp.set_output_length(filelen) if @current_decomp.respond_to?(:set_output_length)
253
277
  @current_decomp.decompress(filelen)
254
278
  @current_offset += filelen
255
279
  end
@@ -261,7 +285,8 @@ module Cabriolet
261
285
  # @param filename [String] Filename for error messages
262
286
  # @param salvage [Boolean] Salvage mode flag
263
287
  # @raise [DecompressionError] If not in salvage mode
264
- def handle_extraction_error(output_fh, output_path, filename, salvage, _filelen)
288
+ def handle_extraction_error(output_fh, output_path, filename, salvage,
289
+ _filelen)
265
290
  output_fh.close
266
291
  if salvage
267
292
  ::File.write(output_path, "", mode: "wb")
@@ -311,6 +336,9 @@ module Cabriolet
311
336
  @buffer_pos = 0
312
337
  @cab_handle = nil
313
338
 
339
+ # Cache ENV lookups once at initialization
340
+ @debug_block = ENV.fetch("DEBUG_BLOCK", nil)
341
+
314
342
  # Open first cabinet and seek to data offset
315
343
  open_current_cabinet
316
344
  end
@@ -318,7 +346,7 @@ module Cabriolet
318
346
  def read(bytes)
319
347
  # Early return if we've already exhausted all blocks and buffer
320
348
  if @current_block >= @num_blocks && @buffer_pos >= @buffer.bytesize
321
- if ENV["DEBUG_BLOCK"]
349
+ if @debug_block
322
350
  warn "DEBUG BlockReader.read(#{bytes}): Already exhausted, returning empty"
323
351
  end
324
352
  return +""
@@ -326,14 +354,14 @@ module Cabriolet
326
354
 
327
355
  result = +""
328
356
 
329
- if ENV["DEBUG_BLOCK"]
357
+ if @debug_block
330
358
  warn "DEBUG BlockReader.read(#{bytes}): buffer_size=#{@buffer.bytesize} buffer_pos=#{@buffer_pos} block=#{@current_block}/#{@num_blocks}"
331
359
  end
332
360
 
333
361
  while result.bytesize < bytes
334
362
  # Read more data if buffer is empty
335
363
  if (@buffer_pos >= @buffer.bytesize) && !read_next_block
336
- if ENV["DEBUG_BLOCK"]
364
+ if @debug_block
337
365
  warn "DEBUG BlockReader.read: EXHAUSTED at result.bytesize=#{result.bytesize} (wanted #{bytes})"
338
366
  end
339
367
  break
@@ -347,7 +375,7 @@ module Cabriolet
347
375
  @buffer_pos += to_copy
348
376
  end
349
377
 
350
- if ENV["DEBUG_BLOCK"]
378
+ if @debug_block
351
379
  warn "DEBUG BlockReader.read: returning #{result.bytesize} bytes"
352
380
  end
353
381
 
@@ -371,12 +399,12 @@ module Cabriolet
371
399
  private
372
400
 
373
401
  def read_next_block
374
- if ENV["DEBUG_BLOCK"]
402
+ if @debug_block
375
403
  warn "DEBUG read_next_block: current_block=#{@current_block} num_blocks=#{@num_blocks}"
376
404
  end
377
405
 
378
406
  if @current_block >= @num_blocks
379
- if ENV["DEBUG_BLOCK"]
407
+ if @debug_block
380
408
  warn "DEBUG read_next_block: EXHAUSTED (current_block >= num_blocks)"
381
409
  end
382
410
  return false
@@ -387,19 +415,19 @@ module Cabriolet
387
415
 
388
416
  loop do
389
417
  # Read CFDATA header
390
- if ENV["DEBUG_BLOCK"]
418
+ if @debug_block
391
419
  handle_pos = @cab_handle.tell
392
420
  warn "DEBUG read_next_block: About to read CFDATA header at position #{handle_pos}"
393
421
  end
394
422
 
395
423
  header_data = @cab_handle.read(Constants::CFDATA_SIZE)
396
424
 
397
- if ENV["DEBUG_BLOCK"]
425
+ if @debug_block
398
426
  warn "DEBUG read_next_block: Read #{header_data.bytesize} bytes (expected #{Constants::CFDATA_SIZE})"
399
427
  end
400
428
 
401
429
  if header_data.bytesize != Constants::CFDATA_SIZE
402
- if ENV["DEBUG_BLOCK"]
430
+ if @debug_block
403
431
  warn "DEBUG read_next_block: FAILED - header read returned #{header_data.bytesize} bytes"
404
432
  end
405
433
  return false
@@ -427,18 +455,18 @@ module Cabriolet
427
455
  end
428
456
 
429
457
  # Read compressed data
430
- if ENV["DEBUG_BLOCK"]
458
+ if @debug_block
431
459
  warn "DEBUG read_next_block: About to read #{cfdata.compressed_size} bytes of compressed data"
432
460
  end
433
461
 
434
462
  compressed_data = @cab_handle.read(cfdata.compressed_size)
435
463
 
436
- if ENV["DEBUG_BLOCK"]
464
+ if @debug_block
437
465
  warn "DEBUG read_next_block: Read #{compressed_data.bytesize} bytes of compressed data (expected #{cfdata.compressed_size})"
438
466
  end
439
467
 
440
468
  if compressed_data.bytesize != cfdata.compressed_size
441
- if ENV["DEBUG_BLOCK"]
469
+ if @debug_block
442
470
  warn "DEBUG read_next_block: FAILED - compressed data read returned #{compressed_data.bytesize} bytes"
443
471
  end
444
472
  return false
@@ -482,7 +510,7 @@ module Cabriolet
482
510
  end
483
511
 
484
512
  def open_current_cabinet
485
- if ENV["DEBUG_BLOCK"]
513
+ if @debug_block
486
514
  warn "DEBUG open_current_cabinet: filename=#{@current_data.cabinet.filename} offset=#{@current_data.offset}"
487
515
  end
488
516
 
@@ -490,7 +518,7 @@ module Cabriolet
490
518
  @cab_handle = @io_system.open(@current_data.cabinet.filename, Constants::MODE_READ)
491
519
  @cab_handle.seek(@current_data.offset, Constants::SEEK_START)
492
520
 
493
- if ENV["DEBUG_BLOCK"]
521
+ if @debug_block
494
522
  actual_pos = @cab_handle.tell
495
523
  warn "DEBUG open_current_cabinet: seeked to position #{actual_pos} (expected #{@current_data.offset})"
496
524
  end
@@ -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
@@ -28,14 +28,17 @@ module Cabriolet
28
28
  ul = 0
29
29
  offset = bytes.size - remainder
30
30
 
31
+ # Match libmspack's cabd_checksum remainder handling:
32
+ # The C fall-through switch processes bytes in decreasing shift
33
+ # order (first remaining byte gets the highest shift).
31
34
  case remainder
32
35
  when 3
33
- ul |= bytes[offset + 2] << 16
36
+ ul |= bytes[offset] << 16
34
37
  ul |= bytes[offset + 1] << 8
35
- ul |= bytes[offset]
38
+ ul |= bytes[offset + 2]
36
39
  when 2
37
- ul |= bytes[offset + 1] << 8
38
- ul |= bytes[offset]
40
+ ul |= bytes[offset] << 8
41
+ ul |= bytes[offset + 1]
39
42
  when 1
40
43
  ul |= bytes[offset]
41
44
  end
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
@@ -89,7 +89,7 @@ module Cabriolet
89
89
  buffer_size, bit_order: :msb)
90
90
 
91
91
  # Initialize sliding window for LZ77
92
- @window = "\0" * @window_size
92
+ @window = ("\0" * @window_size).b
93
93
  @window_pos = 0
94
94
 
95
95
  # Initialize R0, R1, R2 (LRU offset registers)
@@ -153,6 +153,11 @@ module Cabriolet
153
153
 
154
154
  # Compress a single frame (32KB)
155
155
  #
156
+ # Per libmspack lzxd.c: uncompressed blocks write R0/R1/R2 and data
157
+ # as raw bytes directly to the stream, NOT through the MSB bitstream.
158
+ # The bitstream is flushed (padded to 16-bit boundary) after the
159
+ # block header, then raw bytes follow.
160
+ #
156
161
  # @param data [String] Frame data to compress
157
162
  # @return [void]
158
163
  def compress_frame(data)
@@ -163,12 +168,12 @@ module Cabriolet
163
168
  # Write UNCOMPRESSED block header
164
169
  write_block_header(BLOCKTYPE_UNCOMPRESSED, block_length)
165
170
 
166
- # Write offset registers (R0, R1, R2)
171
+ # Write offset registers (R0, R1, R2) as raw bytes
167
172
  write_offset_registers
168
173
 
169
- # Write raw uncompressed data
174
+ # Write raw uncompressed data (bypassing MSB bitstream)
170
175
  data.each_byte do |byte|
171
- @bitstream.write_bits(byte, 8)
176
+ @bitstream.write_raw_byte(byte)
172
177
  end
173
178
  end
174
179
 
@@ -571,14 +576,17 @@ module Cabriolet
571
576
 
572
577
  # Write offset registers (R0, R1, R2) for uncompressed blocks
573
578
  #
579
+ # Per libmspack lzxd.c: R0/R1/R2 are written as raw bytes directly
580
+ # to the stream (not through the MSB bitstream) to avoid byte-swapping.
581
+ #
574
582
  # @return [void]
575
583
  def write_offset_registers
576
- # Write R0, R1, R2 as 32-bit little-endian values (12 bytes total)
584
+ # Write R0, R1, R2 as 32-bit little-endian values (12 raw bytes total)
577
585
  [@r0, @r1, @r2].each do |offset|
578
- @bitstream.write_bits(offset & 0xFF, 8)
579
- @bitstream.write_bits((offset >> 8) & 0xFF, 8)
580
- @bitstream.write_bits((offset >> 16) & 0xFF, 8)
581
- @bitstream.write_bits((offset >> 24) & 0xFF, 8)
586
+ @bitstream.write_raw_byte(offset & 0xFF)
587
+ @bitstream.write_raw_byte((offset >> 8) & 0xFF)
588
+ @bitstream.write_raw_byte((offset >> 16) & 0xFF)
589
+ @bitstream.write_raw_byte((offset >> 24) & 0xFF)
582
590
  end
583
591
  end
584
592
 
@@ -66,7 +66,7 @@ module Cabriolet
66
66
  @fixed_codes = Huffman::Encoder.build_fixed_codes
67
67
 
68
68
  # Initialize sliding window for LZ77
69
- @window = "\0" * WINDOW_SIZE
69
+ @window = ("\0" * WINDOW_SIZE).b
70
70
  @window_pos = 0
71
71
  end
72
72
 
@@ -31,11 +31,12 @@ module Cabriolet
31
31
  mode = MODE_EXPAND)
32
32
  super(io_system, input, output, buffer_size)
33
33
  @mode = mode
34
- @window = Array.new(WINDOW_SIZE, WINDOW_FILL)
34
+ @window = (WINDOW_FILL.chr * WINDOW_SIZE).b
35
35
  @window_pos = initialize_window_position
36
36
  @input_buffer = ""
37
37
  @input_pos = 0
38
38
  @invert = mode == MODE_MSHELP ? 0xFF : 0x00
39
+ @output_buffer = String.new(encoding: Encoding::BINARY, capacity: 4096)
39
40
  end
40
41
 
41
42
  # Decompress LZSS data
@@ -69,8 +70,8 @@ module Cabriolet
69
70
  literal = read_input_byte
70
71
  break if literal.nil?
71
72
 
72
- @window[@window_pos] = literal
73
- write_output_byte(literal)
73
+ @window.setbyte(@window_pos, literal)
74
+ buffer_output_byte(literal)
74
75
  bytes_written += 1
75
76
 
76
77
  @window_pos = (@window_pos + 1) & (WINDOW_SIZE - 1)
@@ -91,9 +92,9 @@ module Cabriolet
91
92
  # Check if we've reached the limit mid-match
92
93
  break if enforce_limit && bytes_written >= bytes
93
94
 
94
- byte = @window[match_pos]
95
- @window[@window_pos] = byte
96
- write_output_byte(byte)
95
+ byte = @window.getbyte(match_pos)
96
+ @window.setbyte(@window_pos, byte)
97
+ buffer_output_byte(byte)
97
98
  bytes_written += 1
98
99
 
99
100
  @window_pos = (@window_pos + 1) & (WINDOW_SIZE - 1)
@@ -103,6 +104,7 @@ module Cabriolet
103
104
  end
104
105
  end
105
106
 
107
+ flush_output_buffer
106
108
  bytes_written
107
109
  end
108
110
 
@@ -131,17 +133,23 @@ module Cabriolet
131
133
  byte
132
134
  end
133
135
 
134
- # Write a single byte to the output
136
+ # Buffer an output byte and flush when buffer is full
135
137
  #
136
- # @param byte [Integer] Byte to write
138
+ # @param byte [Integer] Byte to buffer
137
139
  # @return [void]
138
- # @raise [Errors::DecompressionError] if write fails
139
- def write_output_byte(byte)
140
- data = [byte].pack("C")
141
- written = @io_system.write(@output, data)
142
- return if written == 1
140
+ def buffer_output_byte(byte)
141
+ @output_buffer << byte.chr
142
+ flush_output_buffer if @output_buffer.bytesize >= 4096
143
+ end
144
+
145
+ # Flush the output buffer to the output stream
146
+ #
147
+ # @return [void]
148
+ def flush_output_buffer
149
+ return if @output_buffer.empty?
143
150
 
144
- raise Errors::DecompressionError, "Failed to write output byte"
151
+ @io_system.write(@output, @output_buffer)
152
+ @output_buffer.clear
145
153
  end
146
154
  end
147
155
  end