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.
- checksums.yaml +4 -4
- data/README.adoc +703 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +167 -16
- data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
- 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 +108 -84
- data/lib/cabriolet/cab/decompressor.rb +16 -20
- data/lib/cabriolet/cab/extractor.rb +142 -66
- data/lib/cabriolet/cab/file_compression_work.rb +52 -0
- data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
- data/lib/cabriolet/checksum.rb +49 -0
- 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/collections/file_collection.rb +175 -0
- 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 +36 -95
- 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 +83 -53
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/extraction/base_extractor.rb +88 -0
- data/lib/cabriolet/extraction/extractor.rb +171 -0
- data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
- data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/format_base.rb +79 -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 +151 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -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/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
- data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
- data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -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/encoder.rb +15 -12
- 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 +119 -168
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/directory_builder.rb +153 -0
- data/lib/cabriolet/lit/guid_generator.rb +16 -0
- data/lib/cabriolet/lit/header_writer.rb +124 -0
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -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/quantum_shared.rb +105 -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 +181 -20
- metadata +69 -4
- data/lib/cabriolet/auto.rb +0 -173
- 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
|
-
|
|
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
|
|
@@ -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
|
|
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?
|
|
112
|
-
raise
|
|
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 -=
|
|
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
|
|
185
|
+
raise Cabriolet::FormatError,
|
|
178
186
|
"MSCompressed Content file not found"
|
|
179
187
|
end
|
|
180
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
438
|
+
rescue Cabriolet::FormatError
|
|
421
439
|
break
|
|
422
440
|
end
|
|
423
441
|
end
|
data/lib/cabriolet/chm/parser.rb
CHANGED
|
@@ -56,9 +56,12 @@ module Cabriolet
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Check GUIDs
|
|
59
|
-
|
|
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
|