cabriolet 0.1.2 → 0.2.1

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +703 -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 +167 -16
  6. data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
  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 +108 -84
  13. data/lib/cabriolet/cab/decompressor.rb +16 -20
  14. data/lib/cabriolet/cab/extractor.rb +142 -66
  15. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  16. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  17. data/lib/cabriolet/checksum.rb +49 -0
  18. data/lib/cabriolet/chm/command_handler.rb +227 -0
  19. data/lib/cabriolet/chm/compressor.rb +7 -3
  20. data/lib/cabriolet/chm/decompressor.rb +39 -21
  21. data/lib/cabriolet/chm/parser.rb +5 -2
  22. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  23. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  24. data/lib/cabriolet/cli/command_registry.rb +83 -0
  25. data/lib/cabriolet/cli.rb +356 -607
  26. data/lib/cabriolet/collections/file_collection.rb +175 -0
  27. data/lib/cabriolet/compressors/base.rb +1 -1
  28. data/lib/cabriolet/compressors/lzx.rb +241 -54
  29. data/lib/cabriolet/compressors/mszip.rb +35 -3
  30. data/lib/cabriolet/compressors/quantum.rb +36 -95
  31. data/lib/cabriolet/decompressors/base.rb +1 -1
  32. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  33. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  34. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  35. data/lib/cabriolet/decompressors/quantum.rb +83 -53
  36. data/lib/cabriolet/errors.rb +3 -0
  37. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  38. data/lib/cabriolet/extraction/extractor.rb +171 -0
  39. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  40. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  41. data/lib/cabriolet/file_entry.rb +156 -0
  42. data/lib/cabriolet/file_manager.rb +144 -0
  43. data/lib/cabriolet/format_base.rb +79 -0
  44. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  45. data/lib/cabriolet/hlp/compressor.rb +28 -238
  46. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  47. data/lib/cabriolet/hlp/parser.rb +52 -101
  48. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  49. data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
  50. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  51. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  52. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  53. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  54. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  55. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  56. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  57. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  58. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  59. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  60. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  61. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  62. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  63. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  64. data/lib/cabriolet/huffman/encoder.rb +15 -12
  65. data/lib/cabriolet/huffman/tree.rb +85 -1
  66. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  67. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  68. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  69. data/lib/cabriolet/lit/command_handler.rb +221 -0
  70. data/lib/cabriolet/lit/compressor.rb +119 -168
  71. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  72. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  73. data/lib/cabriolet/lit/decompressor.rb +518 -152
  74. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  75. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  76. data/lib/cabriolet/lit/header_writer.rb +124 -0
  77. data/lib/cabriolet/lit/parser.rb +670 -0
  78. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  79. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  80. data/lib/cabriolet/models/hlp_file.rb +130 -29
  81. data/lib/cabriolet/models/hlp_header.rb +105 -17
  82. data/lib/cabriolet/models/lit_header.rb +212 -25
  83. data/lib/cabriolet/models/szdd_header.rb +10 -2
  84. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  85. data/lib/cabriolet/oab/command_handler.rb +257 -0
  86. data/lib/cabriolet/oab/compressor.rb +17 -8
  87. data/lib/cabriolet/oab/decompressor.rb +41 -10
  88. data/lib/cabriolet/offset_calculator.rb +81 -0
  89. data/lib/cabriolet/plugin.rb +233 -0
  90. data/lib/cabriolet/plugin_manager.rb +453 -0
  91. data/lib/cabriolet/plugin_validator.rb +422 -0
  92. data/lib/cabriolet/quantum_shared.rb +105 -0
  93. data/lib/cabriolet/system/io_system.rb +3 -0
  94. data/lib/cabriolet/system/memory_handle.rb +17 -4
  95. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  96. data/lib/cabriolet/szdd/compressor.rb +15 -11
  97. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  98. data/lib/cabriolet/version.rb +1 -1
  99. data/lib/cabriolet.rb +181 -20
  100. metadata +69 -4
  101. data/lib/cabriolet/auto.rb +0 -173
  102. data/lib/cabriolet/parallel.rb +0 -333
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ module CAB
5
+ # Worker for compressing files in a CAB archive
6
+ class FileCompressionWorker < Fractor::Worker
7
+ # Process a file compression work item
8
+ #
9
+ # @param work [FileCompressionWork] Work item to process
10
+ # @return [Fractor::WorkResult] Result with compressed blocks
11
+ def process(work)
12
+ # Read source file
13
+ file_data = ::File.binread(work.source_path)
14
+ file_size = file_data.bytesize
15
+
16
+ # Split into blocks and compress
17
+ blocks = []
18
+ offset = 0
19
+
20
+ while offset < file_size
21
+ remaining = file_size - offset
22
+ chunk_size = [work.block_size, remaining].min
23
+ chunk = file_data[offset, chunk_size]
24
+
25
+ # Compress chunk
26
+ compressed_chunk = compress_chunk(chunk, work)
27
+
28
+ blocks << {
29
+ uncompressed_size: chunk.bytesize,
30
+ compressed_size: compressed_chunk.bytesize,
31
+ data: compressed_chunk,
32
+ }
33
+
34
+ offset += chunk_size
35
+ end
36
+
37
+ # Return success result
38
+ Fractor::WorkResult.new(
39
+ result: {
40
+ source_path: work.source_path,
41
+ blocks: blocks,
42
+ total_uncompressed: file_size,
43
+ total_compressed: blocks.sum { |b| b[:compressed_size] },
44
+ },
45
+ work: work,
46
+ )
47
+ rescue StandardError => e
48
+ # Return error result
49
+ Fractor::WorkResult.new(
50
+ error: {
51
+ message: e.message,
52
+ class: e.class.name,
53
+ source_path: work.source_path,
54
+ },
55
+ work: work,
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ # Compress a single chunk of data
62
+ #
63
+ # @param chunk [String] Data chunk to compress
64
+ # @param work [FileCompressionWork] Work item with compression settings
65
+ # @return [String] Compressed data
66
+ def compress_chunk(chunk, work)
67
+ input_handle = System::MemoryHandle.new(chunk)
68
+ output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
69
+
70
+ begin
71
+ compressor = work.algorithm_factory.create(
72
+ work.compression_method,
73
+ :compressor,
74
+ work.io_system,
75
+ input_handle,
76
+ output_handle,
77
+ chunk.bytesize,
78
+ )
79
+
80
+ compressor.compress
81
+
82
+ output_handle.data
83
+
84
+ # Memory handles don't need closing
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ # Utility module for checksum calculations
5
+ module Checksum
6
+ # Calculate CAB-style checksum (XOR-based)
7
+ #
8
+ # @param data [String] Data to calculate checksum for
9
+ # @param initial [Integer] Initial checksum value (default: 0)
10
+ # @return [Integer] Checksum value (32-bit)
11
+ def self.calculate(data, initial = 0)
12
+ cksum = initial
13
+ bytes = data.bytes
14
+
15
+ # Process 4-byte chunks
16
+ (bytes.size / 4).times do |i|
17
+ offset = i * 4
18
+ value = bytes[offset] |
19
+ (bytes[offset + 1] << 8) |
20
+ (bytes[offset + 2] << 16) |
21
+ (bytes[offset + 3] << 24)
22
+ cksum ^= value
23
+ end
24
+
25
+ # Process remaining bytes
26
+ remainder = bytes.size % 4
27
+ if remainder.positive?
28
+ ul = 0
29
+ offset = bytes.size - remainder
30
+
31
+ case remainder
32
+ when 3
33
+ ul |= bytes[offset + 2] << 16
34
+ ul |= bytes[offset + 1] << 8
35
+ ul |= bytes[offset]
36
+ when 2
37
+ ul |= bytes[offset + 1] << 8
38
+ ul |= bytes[offset]
39
+ when 1
40
+ ul |= bytes[offset]
41
+ end
42
+
43
+ cksum ^= ul
44
+ end
45
+
46
+ cksum & 0xFFFFFFFF
47
+ end
48
+ end
49
+ end
@@ -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
@@ -13,8 +13,9 @@ module Cabriolet
13
13
 
14
14
  attr_reader :io_system, :chm
15
15
 
16
- def initialize(io_system = nil)
16
+ def initialize(io_system = nil, algorithm_factory = nil)
17
17
  @io_system = io_system || System::IOSystem.new
18
+ @algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
18
19
  @chm = nil
19
20
  @input_handle = nil
20
21
  @lzx_state = nil
@@ -72,7 +73,7 @@ module Cabriolet
72
73
  when 1
73
74
  extract_compressed(file, output_path)
74
75
  else
75
- raise Errors::FormatError, "Invalid section ID: #{file.section.id}"
76
+ raise Cabriolet::FormatError, "Invalid section ID: #{file.section.id}"
76
77
  end
77
78
  end
78
79
 
@@ -108,13 +109,20 @@ module Cabriolet
108
109
  while remaining.positive?
109
110
  chunk_size = [buffer_size, remaining].min
110
111
  data = @input_handle.read(chunk_size)
111
- if data.nil? || data.length < chunk_size
112
- raise Errors::ReadError,
112
+ if data.nil?
113
+ raise Cabriolet::ReadError,
114
+ "Unexpected end of file"
115
+ end
116
+
117
+ # It's OK if we read less than chunk_size (e.g., last chunk or EOF)
118
+ # Only raise an error if we read nothing when we expected data
119
+ if data.empty? && remaining.positive?
120
+ raise Cabriolet::ReadError,
113
121
  "Unexpected end of file"
114
122
  end
115
123
 
116
124
  output_handle.write(data)
117
- remaining -= chunk_size
125
+ remaining -= data.length
118
126
  end
119
127
 
120
128
  output_handle.close
@@ -174,15 +182,18 @@ module Cabriolet
174
182
  control = sec.control || find_system_file(Parser::CONTROL_NAME)
175
183
 
176
184
  unless content
177
- raise Errors::FormatError,
185
+ raise Cabriolet::FormatError,
178
186
  "MSCompressed Content file not found"
179
187
  end
180
- raise Errors::FormatError, "ControlData file not found" unless control
188
+ unless control
189
+ raise Cabriolet::FormatError,
190
+ "ControlData file not found"
191
+ end
181
192
 
182
193
  # Read control data
183
194
  control_data = read_system_file(control)
184
195
  unless control_data.length == 28
185
- raise Errors::FormatError,
196
+ raise Cabriolet::FormatError,
186
197
  "ControlData wrong size"
187
198
  end
188
199
 
@@ -198,13 +209,14 @@ module Cabriolet
198
209
  when 0x100000 then 20
199
210
  when 0x200000 then 21
200
211
  else
201
- raise Errors::FormatError,
212
+ raise Cabriolet::FormatError,
202
213
  "Invalid window size: #{window_size}"
203
214
  end
204
215
 
205
216
  # Validate reset interval
206
217
  if reset_interval.zero? || (reset_interval % LZX_FRAME_SIZE) != 0
207
- raise Errors::FormatError, "Invalid reset interval: #{reset_interval}"
218
+ raise Cabriolet::FormatError,
219
+ "Invalid reset interval: #{reset_interval}"
208
220
  end
209
221
 
210
222
  # Find reset table entry for this file
@@ -227,7 +239,9 @@ module Cabriolet
227
239
  output_handle = System::MemoryHandle.new("")
228
240
 
229
241
  # Initialize LZX decompressor
230
- @lzx_state = Decompressors::LZX.new(
242
+ @lzx_state = @algorithm_factory.create(
243
+ Constants::COMP_TYPE_LZX,
244
+ :decompressor,
231
245
  @io_system,
232
246
  @input_handle,
233
247
  output_handle,
@@ -242,7 +256,7 @@ module Cabriolet
242
256
  def parse_control_data(data)
243
257
  signature = data[4, 4]
244
258
  unless signature == "LZXC"
245
- raise Errors::SignatureError,
259
+ raise Cabriolet::SignatureError,
246
260
  "Invalid LZXC signature"
247
261
  end
248
262
 
@@ -255,7 +269,8 @@ module Cabriolet
255
269
  reset_interval *= LZX_FRAME_SIZE
256
270
  window_size *= LZX_FRAME_SIZE
257
271
  elsif version != 1
258
- raise Errors::FormatError, "Unknown ControlData version: #{version}"
272
+ raise Cabriolet::FormatError,
273
+ "Unknown ControlData version: #{version}"
259
274
  end
260
275
 
261
276
  [window_size, reset_interval]
@@ -272,7 +287,7 @@ module Cabriolet
272
287
  # Fall back to SpanInfo
273
288
  spaninfo = sec.spaninfo || find_system_file(Parser::SPANINFO_NAME)
274
289
  unless spaninfo
275
- raise Errors::FormatError,
290
+ raise Cabriolet::FormatError,
276
291
  "Neither ResetTable nor SpanInfo found"
277
292
  end
278
293
 
@@ -284,12 +299,12 @@ module Cabriolet
284
299
  # Read an entry from the reset table
285
300
  def read_reset_table_entry(rtable, entry, reset_interval)
286
301
  data = read_system_file(rtable)
287
- raise Errors::FormatError, "ResetTable too short" if data.length < 40
302
+ raise Cabriolet::FormatError, "ResetTable too short" if data.length < 40
288
303
 
289
304
  # Check frame length
290
305
  frame_len = data[32, 8].unpack1("Q<")
291
306
  unless frame_len == LZX_FRAME_SIZE
292
- raise Errors::FormatError,
307
+ raise Cabriolet::FormatError,
293
308
  "Invalid frame length"
294
309
  end
295
310
 
@@ -307,7 +322,7 @@ module Cabriolet
307
322
  when 4 then data[pos, 4].unpack1("V")
308
323
  when 8 then data[pos, 8].unpack1("Q<")
309
324
  else
310
- raise Errors::FormatError,
325
+ raise Cabriolet::FormatError,
311
326
  "Invalid entry size: #{entry_size}"
312
327
  end
313
328
 
@@ -325,11 +340,14 @@ module Cabriolet
325
340
  # Read SpanInfo to get uncompressed length
326
341
  def read_spaninfo(spaninfo)
327
342
  data = read_system_file(spaninfo)
328
- raise Errors::FormatError, "SpanInfo wrong size" unless data.length == 8
343
+ unless data.length == 8
344
+ raise Cabriolet::FormatError,
345
+ "SpanInfo wrong size"
346
+ end
329
347
 
330
348
  length = data.unpack1("Q<")
331
349
  unless length.positive?
332
- raise Errors::FormatError,
350
+ raise Cabriolet::FormatError,
333
351
  "Invalid SpanInfo length"
334
352
  end
335
353
 
@@ -350,7 +368,7 @@ module Cabriolet
350
368
  # Read a system file's contents
351
369
  def read_system_file(file)
352
370
  unless file.section.id.zero?
353
- raise Errors::FormatError,
371
+ raise Cabriolet::FormatError,
354
372
  "System file must be in section 0"
355
373
  end
356
374
 
@@ -417,7 +435,7 @@ module Cabriolet
417
435
  file.length = length
418
436
  return file
419
437
  end
420
- rescue Errors::FormatError
438
+ rescue Cabriolet::FormatError
421
439
  break
422
440
  end
423
441
  end
@@ -56,9 +56,12 @@ module Cabriolet
56
56
  end
57
57
 
58
58
  # Check GUIDs
59
- unless header.guid1 == GUID1 && header.guid2 == GUID2
59
+ # Note: Some CHM files have both GUIDs set to GUID1 (unusual but valid)
60
+ # Standard files have GUID1 and GUID2 as expected
61
+ # We validate that guid2 matches either GUID1 or GUID2
62
+ unless [GUID1, GUID2].include?(header.guid2)
60
63
  raise SignatureError,
61
- "Invalid CHM GUIDs"
64
+ "Invalid CHM GUIDs (guid2 should match CHM format GUID)"
62
65
  end
63
66
 
64
67
  @chm.version = header.version