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 +4 -4
- data/lib/cabriolet/algorithm_factory.rb +2 -2
- data/lib/cabriolet/base_compressor.rb +6 -6
- data/lib/cabriolet/binary/bitstream.rb +56 -6
- data/lib/cabriolet/cab/decompressor.rb +10 -7
- data/lib/cabriolet/cab/extractor.rb +49 -21
- data/lib/cabriolet/cab/parser.rb +3 -0
- data/lib/cabriolet/checksum.rb +7 -4
- data/lib/cabriolet/cli.rb +4 -4
- data/lib/cabriolet/compressors/lzx.rb +17 -9
- data/lib/cabriolet/compressors/mszip.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +22 -14
- data/lib/cabriolet/decompressors/lzx.rb +136 -24
- data/lib/cabriolet/decompressors/mszip.rb +36 -17
- data/lib/cabriolet/decompressors/quantum.rb +34 -36
- data/lib/cabriolet/file_manager.rb +4 -4
- data/lib/cabriolet/format_base.rb +4 -4
- data/lib/cabriolet/hlp/compressor.rb +2 -2
- data/lib/cabriolet/huffman/decoder.rb +8 -2
- data/lib/cabriolet/plugin.rb +2 -2
- data/lib/cabriolet/plugin_manager.rb +5 -5
- data/lib/cabriolet/streaming.rb +2 -2
- data/lib/cabriolet/system/file_handle.rb +1 -1
- data/lib/cabriolet/validator.rb +2 -2
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +96 -98
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb8a4d045d1bc49ab6448b9d1890524386e743d27249ad5dd5c5e630140e8a20
|
|
4
|
+
data.tar.gz: c63202e8fe947a0d14b3f4ffaca42feae67dc04e392ce3f9eac00d90ab0293b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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, **
|
|
61
|
-
@file_manager.add_file(source_path, archive_path, **
|
|
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, **
|
|
71
|
-
@file_manager.add_data(data, archive_path, **
|
|
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:, **
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
@
|
|
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, **
|
|
39
|
+
def extract_file(file, output_path, **)
|
|
40
40
|
extractor = Extractor.new(@io_system, self)
|
|
41
|
-
extractor.extract_file(file, output_path, **
|
|
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, **
|
|
50
|
+
def extract_all(cabinet, output_dir, **)
|
|
51
51
|
extractor = Extractor.new(@io_system, self)
|
|
52
|
-
extractor.extract_all(cabinet, output_dir, **
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
data/lib/cabriolet/cab/parser.rb
CHANGED
data/lib/cabriolet/checksum.rb
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
579
|
-
@bitstream.
|
|
580
|
-
@bitstream.
|
|
581
|
-
@bitstream.
|
|
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
|
|
|
@@ -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 =
|
|
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
|
|
73
|
-
|
|
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
|
|
95
|
-
@window
|
|
96
|
-
|
|
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
|
-
#
|
|
136
|
+
# Buffer an output byte and flush when buffer is full
|
|
135
137
|
#
|
|
136
|
-
# @param byte [Integer] Byte to
|
|
138
|
+
# @param byte [Integer] Byte to buffer
|
|
137
139
|
# @return [void]
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
+
@io_system.write(@output, @output_buffer)
|
|
152
|
+
@output_buffer.clear
|
|
145
153
|
end
|
|
146
154
|
end
|
|
147
155
|
end
|