cabriolet 0.1.2 → 0.2.0
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/README.adoc +700 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +154 -14
- data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +35 -43
- data/lib/cabriolet/cab/decompressor.rb +14 -19
- data/lib/cabriolet/cab/extractor.rb +140 -31
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +34 -45
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +3 -2
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +633 -38
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +67 -17
- metadata +33 -2
|
@@ -10,8 +10,10 @@ module Cabriolet
|
|
|
10
10
|
# Initialize a new compressor
|
|
11
11
|
#
|
|
12
12
|
# @param io_system [System::IOSystem] I/O system for writing
|
|
13
|
-
|
|
13
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
14
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
14
15
|
@io_system = io_system || System::IOSystem.new
|
|
16
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
15
17
|
@files = []
|
|
16
18
|
@compression = :mszip
|
|
17
19
|
@set_id = rand(0xFFFF)
|
|
@@ -161,54 +163,44 @@ module Cabriolet
|
|
|
161
163
|
|
|
162
164
|
# Compress a single chunk of data
|
|
163
165
|
def compress_chunk(data)
|
|
164
|
-
|
|
165
|
-
when :none
|
|
166
|
-
data
|
|
167
|
-
when :mszip
|
|
168
|
-
compress_mszip(data)
|
|
169
|
-
when :lzx
|
|
170
|
-
compress_lzx(data)
|
|
171
|
-
when :quantum
|
|
172
|
-
compress_quantum(data)
|
|
173
|
-
else
|
|
174
|
-
raise ArgumentError, "Unsupported compression type: #{@compression}"
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# Compress data using MSZIP
|
|
179
|
-
def compress_mszip(data)
|
|
180
|
-
input = System::MemoryHandle.new(data, Constants::MODE_READ)
|
|
181
|
-
output = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
182
|
-
|
|
183
|
-
compressor = Compressors::MSZIP.new(@io_system, input, output,
|
|
184
|
-
Cabriolet.default_buffer_size)
|
|
185
|
-
compressor.compress
|
|
186
|
-
|
|
187
|
-
output.data
|
|
188
|
-
end
|
|
166
|
+
return data if @compression == :none
|
|
189
167
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
input = System::MemoryHandle.new(data, Constants::MODE_READ)
|
|
168
|
+
# Create temporary handles for compression
|
|
169
|
+
input = System::MemoryHandle.new(data)
|
|
193
170
|
output = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
194
171
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
172
|
+
# Get compression method value
|
|
173
|
+
compression_method = begin
|
|
174
|
+
{
|
|
175
|
+
none: Constants::COMP_TYPE_NONE,
|
|
176
|
+
mszip: Constants::COMP_TYPE_MSZIP,
|
|
177
|
+
lzx: Constants::COMP_TYPE_LZX,
|
|
178
|
+
quantum: Constants::COMP_TYPE_QUANTUM,
|
|
179
|
+
}.fetch(@compression)
|
|
180
|
+
rescue KeyError
|
|
181
|
+
raise ArgumentError,
|
|
182
|
+
"Unsupported compression type: #{@compression}"
|
|
183
|
+
end
|
|
201
184
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
185
|
+
# Determine window bits based on compression type
|
|
186
|
+
window_bits = case @compression
|
|
187
|
+
when :lzx then 15
|
|
188
|
+
when :quantum then 10
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
compressor = @algorithm_factory.create(
|
|
192
|
+
compression_method,
|
|
193
|
+
:compressor,
|
|
194
|
+
@io_system,
|
|
195
|
+
input,
|
|
196
|
+
output,
|
|
197
|
+
data.bytesize,
|
|
198
|
+
window_bits: window_bits,
|
|
199
|
+
)
|
|
206
200
|
|
|
207
|
-
compressor = Compressors::Quantum.new(@io_system, input, output,
|
|
208
|
-
Cabriolet.default_buffer_size, window_bits: 10)
|
|
209
201
|
compressor.compress
|
|
210
|
-
|
|
211
|
-
output.
|
|
202
|
+
output.rewind
|
|
203
|
+
output.read
|
|
212
204
|
end
|
|
213
205
|
|
|
214
206
|
# Write the complete cabinet file
|
|
@@ -10,8 +10,10 @@ module Cabriolet
|
|
|
10
10
|
# Initialize a new CAB decompressor
|
|
11
11
|
#
|
|
12
12
|
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
|
|
13
|
-
|
|
13
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
14
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
14
15
|
@io_system = io_system || System::IOSystem.new
|
|
16
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
15
17
|
@parser = Parser.new(@io_system)
|
|
16
18
|
@buffer_size = Cabriolet.default_buffer_size
|
|
17
19
|
@fix_mszip = false
|
|
@@ -57,24 +59,17 @@ module Cabriolet
|
|
|
57
59
|
# @param output [System::FileHandle, System::MemoryHandle] Output handle
|
|
58
60
|
# @return [Decompressors::Base] Appropriate decompressor instance
|
|
59
61
|
def create_decompressor(folder, input, output)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
window_bits = folder.compression_level
|
|
72
|
-
Decompressors::Quantum.new(@io_system, input, output, @buffer_size,
|
|
73
|
-
window_bits: window_bits)
|
|
74
|
-
else
|
|
75
|
-
raise UnsupportedFormatError,
|
|
76
|
-
"Unsupported compression type: #{folder.compression_method}"
|
|
77
|
-
end
|
|
62
|
+
@algorithm_factory.create(
|
|
63
|
+
folder.compression_method,
|
|
64
|
+
:decompressor,
|
|
65
|
+
@io_system,
|
|
66
|
+
input,
|
|
67
|
+
output,
|
|
68
|
+
@buffer_size,
|
|
69
|
+
fix_mszip: @fix_mszip,
|
|
70
|
+
salvage: @salvage,
|
|
71
|
+
window_bits: folder.compression_level,
|
|
72
|
+
)
|
|
78
73
|
end
|
|
79
74
|
|
|
80
75
|
# Append a cabinet to another, merging their folders and files
|
|
@@ -15,6 +15,12 @@ module Cabriolet
|
|
|
15
15
|
def initialize(io_system, decompressor)
|
|
16
16
|
@io_system = io_system
|
|
17
17
|
@decompressor = decompressor
|
|
18
|
+
|
|
19
|
+
# State reuse for multi-file extraction (like libmspack self->d)
|
|
20
|
+
@current_folder = nil
|
|
21
|
+
@current_decomp = nil
|
|
22
|
+
@current_input = nil
|
|
23
|
+
@current_offset = 0
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
# Extract a single file from the cabinet
|
|
@@ -45,7 +51,6 @@ module Cabriolet
|
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
filelen = Constants::LENGTH_MAX - file.offset
|
|
48
|
-
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
# Check for merge requirements
|
|
@@ -66,38 +71,74 @@ module Cabriolet
|
|
|
66
71
|
output_dir = ::File.dirname(output_path)
|
|
67
72
|
FileUtils.mkdir_p(output_dir) unless ::File.directory?(output_dir)
|
|
68
73
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
# Check if we need to change folder or reset (libmspack lines 1076-1078)
|
|
75
|
+
if ENV["DEBUG_BLOCK"]
|
|
76
|
+
warn "DEBUG extract_file: Checking reset condition for file #{file.filename} (offset=#{file.offset}, length=#{file.length})"
|
|
77
|
+
warn " @current_folder == folder: #{@current_folder == folder} (current=#{@current_folder.object_id}, new=#{folder.object_id})"
|
|
78
|
+
warn " @current_offset (#{@current_offset}) > file.offset (#{file.offset}): #{@current_offset > file.offset}"
|
|
79
|
+
warn " @current_decomp.nil?: #{@current_decomp.nil?}"
|
|
80
|
+
warn " Reset needed?: #{@current_folder != folder || @current_offset > file.offset || !@current_decomp}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if @current_folder != folder || @current_offset > file.offset || !@current_decomp
|
|
84
|
+
if ENV["DEBUG_BLOCK"]
|
|
85
|
+
warn "DEBUG extract_file: RESETTING state (creating new BlockReader)"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Reset state
|
|
89
|
+
@current_input&.close
|
|
90
|
+
@current_input = nil
|
|
91
|
+
@current_decomp = nil
|
|
92
|
+
|
|
93
|
+
# Create new input (libmspack lines 1092-1095)
|
|
94
|
+
# This BlockReader will be REUSED across all files in this folder
|
|
95
|
+
@current_input = BlockReader.new(@io_system, folder.data,
|
|
96
|
+
folder.num_blocks, salvage)
|
|
97
|
+
@current_folder = folder
|
|
98
|
+
@current_offset = 0
|
|
99
|
+
|
|
100
|
+
# Create decompressor ONCE and reuse it (this is the key fix!)
|
|
101
|
+
# The decompressor maintains bitstream state across files
|
|
102
|
+
@current_decomp = @decompressor.create_decompressor(folder,
|
|
103
|
+
@current_input, nil)
|
|
104
|
+
elsif ENV["DEBUG_BLOCK"]
|
|
105
|
+
warn "DEBUG extract_file: NOT resetting (reusing existing BlockReader and decompressor)"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Skip ahead if needed (libmspack lines 1130-1134)
|
|
109
|
+
if file.offset > @current_offset
|
|
110
|
+
skip_bytes = file.offset - @current_offset
|
|
111
|
+
|
|
112
|
+
# Decompress with NULL output to skip (libmspack line 1130: self->d->outfh = NULL)
|
|
113
|
+
null_output = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
114
|
+
|
|
115
|
+
# Reuse existing decompressor, change output to NULL
|
|
116
|
+
@current_decomp.instance_variable_set(:@output, null_output)
|
|
117
|
+
|
|
118
|
+
# Set output length for LZX frame limiting
|
|
119
|
+
@current_decomp.set_output_length(skip_bytes) if @current_decomp.respond_to?(:set_output_length)
|
|
120
|
+
|
|
121
|
+
@current_decomp.decompress(skip_bytes)
|
|
122
|
+
@current_offset += skip_bytes
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Extract actual file (libmspack lines 1137-1141)
|
|
126
|
+
output_fh = @io_system.open(output_path, Constants::MODE_WRITE)
|
|
72
127
|
|
|
73
128
|
begin
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
begin
|
|
78
|
-
# Create decompressor
|
|
79
|
-
decomp = @decompressor.create_decompressor(folder, input_handle,
|
|
80
|
-
output_fh)
|
|
81
|
-
|
|
82
|
-
# Skip to file offset if needed
|
|
83
|
-
if file.offset.positive?
|
|
84
|
-
# Decompress and discard bytes before file start
|
|
85
|
-
temp_output = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
86
|
-
temp_decomp = @decompressor.create_decompressor(folder,
|
|
87
|
-
input_handle, temp_output)
|
|
88
|
-
temp_decomp.decompress(file.offset)
|
|
89
|
-
end
|
|
129
|
+
# Reuse existing decompressor, change output to real file
|
|
130
|
+
@current_decomp.instance_variable_set(:@output, output_fh)
|
|
90
131
|
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
# Set output length for LZX frame limiting
|
|
133
|
+
@current_decomp.set_output_length(filelen) if @current_decomp.respond_to?(:set_output_length)
|
|
93
134
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
output_fh.close
|
|
97
|
-
end
|
|
135
|
+
@current_decomp.decompress(filelen)
|
|
136
|
+
@current_offset += filelen
|
|
98
137
|
ensure
|
|
99
|
-
|
|
138
|
+
output_fh.close
|
|
100
139
|
end
|
|
140
|
+
|
|
141
|
+
filelen
|
|
101
142
|
end
|
|
102
143
|
|
|
103
144
|
# Extract all files from a cabinet
|
|
@@ -192,11 +233,28 @@ module Cabriolet
|
|
|
192
233
|
end
|
|
193
234
|
|
|
194
235
|
def read(bytes)
|
|
236
|
+
# Early return if we've already exhausted all blocks and buffer
|
|
237
|
+
if @current_block >= @num_blocks && @buffer_pos >= @buffer.bytesize
|
|
238
|
+
if ENV["DEBUG_BLOCK"]
|
|
239
|
+
warn "DEBUG BlockReader.read(#{bytes}): Already exhausted, returning empty"
|
|
240
|
+
end
|
|
241
|
+
return +""
|
|
242
|
+
end
|
|
243
|
+
|
|
195
244
|
result = +""
|
|
196
245
|
|
|
246
|
+
if ENV["DEBUG_BLOCK"]
|
|
247
|
+
warn "DEBUG BlockReader.read(#{bytes}): buffer_size=#{@buffer.bytesize} buffer_pos=#{@buffer_pos} block=#{@current_block}/#{@num_blocks}"
|
|
248
|
+
end
|
|
249
|
+
|
|
197
250
|
while result.bytesize < bytes
|
|
198
251
|
# Read more data if buffer is empty
|
|
199
|
-
|
|
252
|
+
if (@buffer_pos >= @buffer.bytesize) && !read_next_block
|
|
253
|
+
if ENV["DEBUG_BLOCK"]
|
|
254
|
+
warn "DEBUG BlockReader.read: EXHAUSTED at result.bytesize=#{result.bytesize} (wanted #{bytes})"
|
|
255
|
+
end
|
|
256
|
+
break
|
|
257
|
+
end
|
|
200
258
|
|
|
201
259
|
# Copy from buffer
|
|
202
260
|
available = @buffer.bytesize - @buffer_pos
|
|
@@ -206,6 +264,10 @@ module Cabriolet
|
|
|
206
264
|
@buffer_pos += to_copy
|
|
207
265
|
end
|
|
208
266
|
|
|
267
|
+
if ENV["DEBUG_BLOCK"]
|
|
268
|
+
warn "DEBUG BlockReader.read: returning #{result.bytesize} bytes"
|
|
269
|
+
end
|
|
270
|
+
|
|
209
271
|
result
|
|
210
272
|
end
|
|
211
273
|
|
|
@@ -226,15 +288,39 @@ module Cabriolet
|
|
|
226
288
|
private
|
|
227
289
|
|
|
228
290
|
def read_next_block
|
|
229
|
-
|
|
291
|
+
if ENV["DEBUG_BLOCK"]
|
|
292
|
+
warn "DEBUG read_next_block: current_block=#{@current_block} num_blocks=#{@num_blocks}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if @current_block >= @num_blocks
|
|
296
|
+
if ENV["DEBUG_BLOCK"]
|
|
297
|
+
warn "DEBUG read_next_block: EXHAUSTED (current_block >= num_blocks)"
|
|
298
|
+
end
|
|
299
|
+
return false
|
|
300
|
+
end
|
|
230
301
|
|
|
231
302
|
# Read blocks, potentially spanning multiple cabinets
|
|
232
303
|
accumulated_data = +""
|
|
233
304
|
|
|
234
305
|
loop do
|
|
235
306
|
# Read CFDATA header
|
|
307
|
+
if ENV["DEBUG_BLOCK"]
|
|
308
|
+
handle_pos = @cab_handle.tell
|
|
309
|
+
warn "DEBUG read_next_block: About to read CFDATA header at position #{handle_pos}"
|
|
310
|
+
end
|
|
311
|
+
|
|
236
312
|
header_data = @cab_handle.read(Constants::CFDATA_SIZE)
|
|
237
|
-
|
|
313
|
+
|
|
314
|
+
if ENV["DEBUG_BLOCK"]
|
|
315
|
+
warn "DEBUG read_next_block: Read #{header_data.bytesize} bytes (expected #{Constants::CFDATA_SIZE})"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
if header_data.bytesize != Constants::CFDATA_SIZE
|
|
319
|
+
if ENV["DEBUG_BLOCK"]
|
|
320
|
+
warn "DEBUG read_next_block: FAILED - header read returned #{header_data.bytesize} bytes"
|
|
321
|
+
end
|
|
322
|
+
return false
|
|
323
|
+
end
|
|
238
324
|
|
|
239
325
|
cfdata = Binary::CFData.read(header_data)
|
|
240
326
|
|
|
@@ -258,8 +344,22 @@ module Cabriolet
|
|
|
258
344
|
end
|
|
259
345
|
|
|
260
346
|
# Read compressed data
|
|
347
|
+
if ENV["DEBUG_BLOCK"]
|
|
348
|
+
warn "DEBUG read_next_block: About to read #{cfdata.compressed_size} bytes of compressed data"
|
|
349
|
+
end
|
|
350
|
+
|
|
261
351
|
compressed_data = @cab_handle.read(cfdata.compressed_size)
|
|
262
|
-
|
|
352
|
+
|
|
353
|
+
if ENV["DEBUG_BLOCK"]
|
|
354
|
+
warn "DEBUG read_next_block: Read #{compressed_data.bytesize} bytes of compressed data (expected #{cfdata.compressed_size})"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if compressed_data.bytesize != cfdata.compressed_size
|
|
358
|
+
if ENV["DEBUG_BLOCK"]
|
|
359
|
+
warn "DEBUG read_next_block: FAILED - compressed data read returned #{compressed_data.bytesize} bytes"
|
|
360
|
+
end
|
|
361
|
+
return false
|
|
362
|
+
end
|
|
263
363
|
|
|
264
364
|
# Verify checksum if present and not in salvage mode
|
|
265
365
|
if cfdata.checksum.positive? && !@salvage
|
|
@@ -299,9 +399,18 @@ module Cabriolet
|
|
|
299
399
|
end
|
|
300
400
|
|
|
301
401
|
def open_current_cabinet
|
|
402
|
+
if ENV["DEBUG_BLOCK"]
|
|
403
|
+
warn "DEBUG open_current_cabinet: filename=#{@current_data.cabinet.filename} offset=#{@current_data.offset}"
|
|
404
|
+
end
|
|
405
|
+
|
|
302
406
|
@cab_handle&.close
|
|
303
407
|
@cab_handle = @io_system.open(@current_data.cabinet.filename, Constants::MODE_READ)
|
|
304
408
|
@cab_handle.seek(@current_data.offset, Constants::SEEK_START)
|
|
409
|
+
|
|
410
|
+
if ENV["DEBUG_BLOCK"]
|
|
411
|
+
actual_pos = @cab_handle.tell
|
|
412
|
+
warn "DEBUG open_current_cabinet: seeked to position #{actual_pos} (expected #{@current_data.offset})"
|
|
413
|
+
end
|
|
305
414
|
end
|
|
306
415
|
|
|
307
416
|
def advance_to_next_cabinet
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cli/base_command_handler"
|
|
4
|
+
require_relative "decompressor"
|
|
5
|
+
require_relative "compressor"
|
|
6
|
+
|
|
7
|
+
module Cabriolet
|
|
8
|
+
module CHM
|
|
9
|
+
# Command handler for CHM (Compiled HTML Help) format
|
|
10
|
+
#
|
|
11
|
+
# This handler implements the unified command interface for CHM files,
|
|
12
|
+
# wrapping the existing CHM::Decompressor and CHM::Compressor classes.
|
|
13
|
+
#
|
|
14
|
+
class CommandHandler < Commands::BaseCommandHandler
|
|
15
|
+
# List CHM file contents
|
|
16
|
+
#
|
|
17
|
+
# Displays information about the CHM file including version,
|
|
18
|
+
# language, and lists all contained files with their sizes.
|
|
19
|
+
#
|
|
20
|
+
# @param file [String] Path to the CHM file
|
|
21
|
+
# @param options [Hash] Additional options (unused)
|
|
22
|
+
# @return [void]
|
|
23
|
+
def list(file, _options = {})
|
|
24
|
+
validate_file_exists(file)
|
|
25
|
+
|
|
26
|
+
decompressor = Decompressor.new
|
|
27
|
+
chm = decompressor.open(file)
|
|
28
|
+
|
|
29
|
+
display_header(chm)
|
|
30
|
+
display_files(chm.all_files)
|
|
31
|
+
|
|
32
|
+
decompressor.close
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Extract files from CHM archive
|
|
36
|
+
#
|
|
37
|
+
# Extracts all non-system files from the CHM file to the
|
|
38
|
+
# specified output directory.
|
|
39
|
+
#
|
|
40
|
+
# @param file [String] Path to the CHM file
|
|
41
|
+
# @param output_dir [String] Output directory path (default: current directory)
|
|
42
|
+
# @param options [Hash] Additional options (unused)
|
|
43
|
+
# @return [void]
|
|
44
|
+
def extract(file, output_dir = nil, _options = {})
|
|
45
|
+
validate_file_exists(file)
|
|
46
|
+
|
|
47
|
+
output_dir ||= "."
|
|
48
|
+
output_dir = ensure_output_dir(output_dir)
|
|
49
|
+
|
|
50
|
+
decompressor = Decompressor.new
|
|
51
|
+
chm = decompressor.open(file)
|
|
52
|
+
|
|
53
|
+
count = 0
|
|
54
|
+
chm.all_files.each do |f|
|
|
55
|
+
next if f.system_file?
|
|
56
|
+
|
|
57
|
+
output_path = File.join(output_dir, f.filename)
|
|
58
|
+
output_subdir = File.dirname(output_path)
|
|
59
|
+
FileUtils.mkdir_p(output_subdir)
|
|
60
|
+
|
|
61
|
+
puts "Extracting: #{f.filename}" if verbose?
|
|
62
|
+
decompressor.extract(f, output_path)
|
|
63
|
+
count += 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
decompressor.close
|
|
67
|
+
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Create a new CHM archive
|
|
71
|
+
#
|
|
72
|
+
# Creates a CHM file from HTML source files.
|
|
73
|
+
#
|
|
74
|
+
# @param output [String] Output CHM file path
|
|
75
|
+
# @param files [Array<String>] List of input HTML files
|
|
76
|
+
# @param options [Hash] Additional options
|
|
77
|
+
# @option options [Integer] :window_bits LZX window size (15-21, default: 16)
|
|
78
|
+
# @return [void]
|
|
79
|
+
# @raise [ArgumentError] if no files specified
|
|
80
|
+
def create(output, files = [], options = {})
|
|
81
|
+
raise ArgumentError, "No files specified" if files.empty?
|
|
82
|
+
|
|
83
|
+
files.each do |f|
|
|
84
|
+
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
window_bits = options[:window_bits] || 16
|
|
88
|
+
|
|
89
|
+
compressor = Compressor.new
|
|
90
|
+
files.each do |f|
|
|
91
|
+
# Default to compressed section for .html, uncompressed for images
|
|
92
|
+
section = f.end_with?(".html", ".htm") ? :compressed : :uncompressed
|
|
93
|
+
compressor.add_file(f, "/#{File.basename(f)}", section: section)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "Creating #{output} with #{files.size} file(s) (window_bits: #{window_bits})" if verbose?
|
|
97
|
+
bytes = compressor.generate(output, window_bits: window_bits)
|
|
98
|
+
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Display detailed CHM file information
|
|
102
|
+
#
|
|
103
|
+
# Shows comprehensive information about the CHM structure,
|
|
104
|
+
# including directory, sections, and files.
|
|
105
|
+
#
|
|
106
|
+
# @param file [String] Path to the CHM file
|
|
107
|
+
# @param options [Hash] Additional options (unused)
|
|
108
|
+
# @return [void]
|
|
109
|
+
def info(file, _options = {})
|
|
110
|
+
validate_file_exists(file)
|
|
111
|
+
|
|
112
|
+
decompressor = Decompressor.new
|
|
113
|
+
chm = decompressor.open(file)
|
|
114
|
+
|
|
115
|
+
display_chm_info(chm)
|
|
116
|
+
|
|
117
|
+
decompressor.close
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Test CHM file integrity
|
|
121
|
+
#
|
|
122
|
+
# Verifies the CHM file structure.
|
|
123
|
+
#
|
|
124
|
+
# @param file [String] Path to the CHM file
|
|
125
|
+
# @param options [Hash] Additional options (unused)
|
|
126
|
+
# @return [void]
|
|
127
|
+
def test(file, _options = {})
|
|
128
|
+
validate_file_exists(file)
|
|
129
|
+
|
|
130
|
+
decompressor = Decompressor.new
|
|
131
|
+
chm = decompressor.open(file)
|
|
132
|
+
|
|
133
|
+
puts "Testing #{chm.filename}..."
|
|
134
|
+
puts "OK: CHM file structure is valid (#{chm.all_files.size} files)"
|
|
135
|
+
puts "Note: Full integrity validation not yet implemented"
|
|
136
|
+
|
|
137
|
+
decompressor.close
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# Display CHM header information
|
|
143
|
+
#
|
|
144
|
+
# @param chm [CHMFile] The CHM file object
|
|
145
|
+
# @return [void]
|
|
146
|
+
def display_header(chm)
|
|
147
|
+
puts "CHM File: #{chm.filename}"
|
|
148
|
+
puts "Version: #{chm.version}"
|
|
149
|
+
puts "Language: #{chm.language}"
|
|
150
|
+
puts "Chunks: #{chm.num_chunks}, Chunk Size: #{chm.chunk_size}"
|
|
151
|
+
puts "\nFiles:"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Display list of files in CHM
|
|
155
|
+
#
|
|
156
|
+
# @param files [Array<CHMFile>] Array of file objects
|
|
157
|
+
# @return [void]
|
|
158
|
+
def display_files(files)
|
|
159
|
+
files.each do |f|
|
|
160
|
+
section_name = f.section.id.zero? ? "Uncompressed" : "MSCompressed"
|
|
161
|
+
puts " #{f.filename} (#{f.length} bytes, #{section_name})"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Display comprehensive CHM information
|
|
166
|
+
#
|
|
167
|
+
# @param chm [CHMFile] The CHM file object
|
|
168
|
+
# @return [void]
|
|
169
|
+
def display_chm_info(chm)
|
|
170
|
+
puts "CHM File Information"
|
|
171
|
+
puts "=" * 50
|
|
172
|
+
puts "Filename: #{chm.filename}"
|
|
173
|
+
puts "Version: #{chm.version}"
|
|
174
|
+
puts "Language ID: #{chm.language}"
|
|
175
|
+
puts "Timestamp: #{chm.timestamp}"
|
|
176
|
+
puts "Size: #{chm.length} bytes"
|
|
177
|
+
puts ""
|
|
178
|
+
puts "Directory:"
|
|
179
|
+
puts " Offset: #{chm.dir_offset}"
|
|
180
|
+
puts " Chunks: #{chm.num_chunks}"
|
|
181
|
+
puts " Chunk Size: #{chm.chunk_size}"
|
|
182
|
+
puts " First PMGL: #{chm.first_pmgl}"
|
|
183
|
+
puts " Last PMGL: #{chm.last_pmgl}"
|
|
184
|
+
puts ""
|
|
185
|
+
puts "Sections:"
|
|
186
|
+
puts " Section 0 (Uncompressed): offset #{chm.sec0.offset}"
|
|
187
|
+
puts " Section 1 (MSCompressed): LZX compression"
|
|
188
|
+
puts ""
|
|
189
|
+
|
|
190
|
+
regular_files = chm.all_files
|
|
191
|
+
system_files = chm.all_sysfiles
|
|
192
|
+
|
|
193
|
+
puts "Files: #{regular_files.length} regular, #{system_files.length} system"
|
|
194
|
+
puts ""
|
|
195
|
+
display_regular_files(regular_files)
|
|
196
|
+
display_system_files(system_files) if system_files.any?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Display regular files
|
|
200
|
+
#
|
|
201
|
+
# @param files [Array<CHMFile>] Array of regular file objects
|
|
202
|
+
# @return [void]
|
|
203
|
+
def display_regular_files(files)
|
|
204
|
+
puts "Regular Files:"
|
|
205
|
+
files.each do |f|
|
|
206
|
+
section_name = f.section.id.zero? ? "Sec0" : "Sec1"
|
|
207
|
+
puts " #{f.filename}"
|
|
208
|
+
puts " Size: #{f.length} bytes (#{section_name})"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Display system files
|
|
213
|
+
#
|
|
214
|
+
# @param files [Array<CHMFile>] Array of system file objects
|
|
215
|
+
# @return [void]
|
|
216
|
+
def display_system_files(files)
|
|
217
|
+
puts ""
|
|
218
|
+
puts "System Files:"
|
|
219
|
+
files.each do |f|
|
|
220
|
+
section_name = f.section.id.zero? ? "Sec0" : "Sec1"
|
|
221
|
+
puts " #{f.filename}"
|
|
222
|
+
puts " Size: #{f.length} bytes (#{section_name})"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -34,8 +34,10 @@ module Cabriolet
|
|
|
34
34
|
# Initialize CHM compressor
|
|
35
35
|
#
|
|
36
36
|
# @param io_system [System::IOSystem] I/O system for file operations
|
|
37
|
-
|
|
37
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
38
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
38
39
|
@io_system = io_system || System::IOSystem.new
|
|
40
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
39
41
|
@files = []
|
|
40
42
|
@timestamp = Time.now.to_i
|
|
41
43
|
@language_id = 0x0409 # English (US)
|
|
@@ -156,7 +158,9 @@ module Cabriolet
|
|
|
156
158
|
input_handle = System::MemoryHandle.new(uncompressed_data, Constants::MODE_READ)
|
|
157
159
|
output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
158
160
|
|
|
159
|
-
compressor =
|
|
161
|
+
compressor = @algorithm_factory.create(
|
|
162
|
+
Constants::COMP_TYPE_LZX,
|
|
163
|
+
:compressor,
|
|
160
164
|
@io_system,
|
|
161
165
|
input_handle,
|
|
162
166
|
output_handle,
|
|
@@ -255,7 +259,7 @@ module Cabriolet
|
|
|
255
259
|
|
|
256
260
|
# Build control data for LZX
|
|
257
261
|
def build_control_data
|
|
258
|
-
control = Binary::
|
|
262
|
+
control = Binary::CHMLZXControlData.new
|
|
259
263
|
control.len = 28
|
|
260
264
|
control.signature = "LZXC"
|
|
261
265
|
control.version = 2
|