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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +700 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +154 -14
  6. data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +35 -43
  13. data/lib/cabriolet/cab/decompressor.rb +14 -19
  14. data/lib/cabriolet/cab/extractor.rb +140 -31
  15. data/lib/cabriolet/chm/command_handler.rb +227 -0
  16. data/lib/cabriolet/chm/compressor.rb +7 -3
  17. data/lib/cabriolet/chm/decompressor.rb +39 -21
  18. data/lib/cabriolet/chm/parser.rb +5 -2
  19. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  20. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  21. data/lib/cabriolet/cli/command_registry.rb +83 -0
  22. data/lib/cabriolet/cli.rb +356 -607
  23. data/lib/cabriolet/compressors/base.rb +1 -1
  24. data/lib/cabriolet/compressors/lzx.rb +241 -54
  25. data/lib/cabriolet/compressors/mszip.rb +35 -3
  26. data/lib/cabriolet/compressors/quantum.rb +34 -45
  27. data/lib/cabriolet/decompressors/base.rb +1 -1
  28. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  29. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  30. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  31. data/lib/cabriolet/decompressors/quantum.rb +3 -2
  32. data/lib/cabriolet/errors.rb +3 -0
  33. data/lib/cabriolet/file_entry.rb +156 -0
  34. data/lib/cabriolet/file_manager.rb +144 -0
  35. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  36. data/lib/cabriolet/hlp/compressor.rb +28 -238
  37. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  38. data/lib/cabriolet/hlp/parser.rb +52 -101
  39. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  40. data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
  41. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  42. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  43. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  44. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  45. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  46. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  47. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  48. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  49. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  50. data/lib/cabriolet/huffman/tree.rb +85 -1
  51. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  52. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  53. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  54. data/lib/cabriolet/lit/command_handler.rb +221 -0
  55. data/lib/cabriolet/lit/compressor.rb +633 -38
  56. data/lib/cabriolet/lit/decompressor.rb +518 -152
  57. data/lib/cabriolet/lit/parser.rb +670 -0
  58. data/lib/cabriolet/models/hlp_file.rb +130 -29
  59. data/lib/cabriolet/models/hlp_header.rb +105 -17
  60. data/lib/cabriolet/models/lit_header.rb +212 -25
  61. data/lib/cabriolet/models/szdd_header.rb +10 -2
  62. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  63. data/lib/cabriolet/oab/command_handler.rb +257 -0
  64. data/lib/cabriolet/oab/compressor.rb +17 -8
  65. data/lib/cabriolet/oab/decompressor.rb +41 -10
  66. data/lib/cabriolet/offset_calculator.rb +81 -0
  67. data/lib/cabriolet/plugin.rb +233 -0
  68. data/lib/cabriolet/plugin_manager.rb +453 -0
  69. data/lib/cabriolet/plugin_validator.rb +422 -0
  70. data/lib/cabriolet/system/io_system.rb +3 -0
  71. data/lib/cabriolet/system/memory_handle.rb +17 -4
  72. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  73. data/lib/cabriolet/szdd/compressor.rb +15 -11
  74. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  75. data/lib/cabriolet/version.rb +1 -1
  76. data/lib/cabriolet.rb +67 -17
  77. 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
- def initialize(io_system = nil)
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
- case @compression
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
- # Compress data using LZX
191
- def compress_lzx(data)
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
- compressor = Compressors::LZX.new(@io_system, input, output,
196
- Cabriolet.default_buffer_size, window_bits: 15)
197
- compressor.compress
198
-
199
- output.data
200
- end
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
- # Compress data using Quantum
203
- def compress_quantum(data)
204
- input = System::MemoryHandle.new(data, Constants::MODE_READ)
205
- output = System::MemoryHandle.new("", Constants::MODE_WRITE)
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.data
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
- def initialize(io_system = nil)
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
- case folder.compression_method
61
- when Constants::COMP_TYPE_NONE
62
- Decompressors::None.new(@io_system, input, output, @buffer_size)
63
- when Constants::COMP_TYPE_MSZIP
64
- Decompressors::MSZIP.new(@io_system, input, output, @buffer_size,
65
- fix_mszip: @fix_mszip)
66
- when Constants::COMP_TYPE_LZX
67
- window_bits = folder.compression_level
68
- Decompressors::LZX.new(@io_system, input, output, @buffer_size,
69
- window_bits: window_bits)
70
- when Constants::COMP_TYPE_QUANTUM
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
- # Create input wrapper that reads CFDATA blocks across cabinets
70
- input_handle = BlockReader.new(@io_system, folder.data,
71
- folder.num_blocks, salvage)
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
- # Create output file
75
- output_fh = @io_system.open(output_path, Constants::MODE_WRITE)
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
- # Decompress the file
92
- decomp.decompress(filelen)
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
- filelen
95
- ensure
96
- output_fh.close
97
- end
135
+ @current_decomp.decompress(filelen)
136
+ @current_offset += filelen
98
137
  ensure
99
- input_handle.close
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
- break if (@buffer_pos >= @buffer.bytesize) && !read_next_block
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
- return false if @current_block >= @num_blocks
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
- return false if header_data.bytesize != Constants::CFDATA_SIZE
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
- return false if compressed_data.bytesize != cfdata.compressed_size
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
- def initialize(io_system = nil)
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 = Compressors::LZX.new(
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::LZXControlData.new
262
+ control = Binary::CHMLZXControlData.new
259
263
  control.len = 28
260
264
  control.signature = "LZXC"
261
265
  control.version = 2