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,282 @@
|
|
|
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 HLP
|
|
9
|
+
# Command handler for HLP (Help) format
|
|
10
|
+
#
|
|
11
|
+
# This handler implements the unified command interface for HLP files,
|
|
12
|
+
# wrapping the existing HLP::Decompressor and HLP::Compressor classes.
|
|
13
|
+
# Supports both QuickHelp and Windows Help formats.
|
|
14
|
+
#
|
|
15
|
+
class CommandHandler < Commands::BaseCommandHandler
|
|
16
|
+
# List HLP file contents
|
|
17
|
+
#
|
|
18
|
+
# Displays information about the HLP file including format type,
|
|
19
|
+
# and lists all contained files with their sizes.
|
|
20
|
+
#
|
|
21
|
+
# @param file [String] Path to the HLP file
|
|
22
|
+
# @param options [Hash] Additional options (unused)
|
|
23
|
+
# @return [void]
|
|
24
|
+
def list(file, _options = {})
|
|
25
|
+
validate_file_exists(file)
|
|
26
|
+
|
|
27
|
+
decompressor = Decompressor.new
|
|
28
|
+
header = decompressor.open(file)
|
|
29
|
+
|
|
30
|
+
display_header(header, file)
|
|
31
|
+
display_files(decompressor, header)
|
|
32
|
+
|
|
33
|
+
decompressor.close(header)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Extract files from HLP archive
|
|
37
|
+
#
|
|
38
|
+
# Extracts all files from the HLP file to the specified output directory.
|
|
39
|
+
# Supports both QuickHelp and Windows Help formats.
|
|
40
|
+
#
|
|
41
|
+
# @param file [String] Path to the HLP file
|
|
42
|
+
# @param output_dir [String] Output directory path (default: current directory)
|
|
43
|
+
# @param options [Hash] Additional options (unused)
|
|
44
|
+
# @return [void]
|
|
45
|
+
def extract(file, output_dir = nil, _options = {})
|
|
46
|
+
validate_file_exists(file)
|
|
47
|
+
|
|
48
|
+
output_dir ||= "."
|
|
49
|
+
output_dir = ensure_output_dir(output_dir)
|
|
50
|
+
|
|
51
|
+
decompressor = Decompressor.new
|
|
52
|
+
header = decompressor.open(file)
|
|
53
|
+
|
|
54
|
+
count = decompressor.extract_all(header, output_dir)
|
|
55
|
+
decompressor.close(header)
|
|
56
|
+
|
|
57
|
+
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Create a new HLP archive
|
|
61
|
+
#
|
|
62
|
+
# Creates an HLP file from source files using QuickHelp format.
|
|
63
|
+
#
|
|
64
|
+
# @param output [String] Output HLP file path
|
|
65
|
+
# @param files [Array<String>] List of input files to add
|
|
66
|
+
# @param options [Hash] Additional options
|
|
67
|
+
# @option options [String] :format HLP format (:quickhelp, :winhelp)
|
|
68
|
+
# @return [void]
|
|
69
|
+
# @raise [ArgumentError] if no files specified
|
|
70
|
+
def create(output, files = [], options = {})
|
|
71
|
+
raise ArgumentError, "No files specified" if files.empty?
|
|
72
|
+
|
|
73
|
+
files.each do |f|
|
|
74
|
+
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
format = parse_format_option(options[:format])
|
|
78
|
+
|
|
79
|
+
if format == :winhelp
|
|
80
|
+
create_winhelp(output, files, options)
|
|
81
|
+
else
|
|
82
|
+
create_quickhelp(output, files, options)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Display detailed HLP file information
|
|
87
|
+
#
|
|
88
|
+
# Shows comprehensive information about the HLP structure,
|
|
89
|
+
# including format type, file count, and metadata.
|
|
90
|
+
#
|
|
91
|
+
# @param file [String] Path to the HLP file
|
|
92
|
+
# @param options [Hash] Additional options (unused)
|
|
93
|
+
# @return [void]
|
|
94
|
+
def info(file, _options = {})
|
|
95
|
+
validate_file_exists(file)
|
|
96
|
+
|
|
97
|
+
decompressor = Decompressor.new
|
|
98
|
+
header = decompressor.open(file)
|
|
99
|
+
|
|
100
|
+
display_hlp_info(header, file)
|
|
101
|
+
|
|
102
|
+
decompressor.close(header)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Test HLP file integrity
|
|
106
|
+
#
|
|
107
|
+
# Verifies the HLP file structure.
|
|
108
|
+
#
|
|
109
|
+
# @param file [String] Path to the HLP file
|
|
110
|
+
# @param options [Hash] Additional options (unused)
|
|
111
|
+
# @return [void]
|
|
112
|
+
def test(file, _options = {})
|
|
113
|
+
validate_file_exists(file)
|
|
114
|
+
|
|
115
|
+
decompressor = Decompressor.new
|
|
116
|
+
header = decompressor.open(file)
|
|
117
|
+
|
|
118
|
+
puts "Testing #{file}..."
|
|
119
|
+
# TODO: Implement full integrity testing
|
|
120
|
+
format_name = if header.respond_to?(:version)
|
|
121
|
+
version_value = header.version
|
|
122
|
+
# Convert BinData objects to integer for comparison
|
|
123
|
+
version_int = version_value.to_i if version_value.respond_to?(:to_i)
|
|
124
|
+
|
|
125
|
+
if version_value.is_a?(Integer) || version_int&.positive?
|
|
126
|
+
"QUICKHELP v#{version_value}"
|
|
127
|
+
elsif version_value.is_a?(Symbol)
|
|
128
|
+
version_value.to_s.upcase.sub("WINHELP", "WinHelp ")
|
|
129
|
+
else
|
|
130
|
+
"unknown"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
puts "OK: HLP file structure is valid (#{format_name} format)"
|
|
134
|
+
|
|
135
|
+
decompressor.close(header)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Display HLP header information
|
|
141
|
+
#
|
|
142
|
+
# @param header [Object] The HLP header object
|
|
143
|
+
# @param file [String] Original file path
|
|
144
|
+
# @return [void]
|
|
145
|
+
def display_header(header, file)
|
|
146
|
+
format_name = if header.respond_to?(:version)
|
|
147
|
+
version_value = header.version
|
|
148
|
+
# Convert BinData objects to integer for comparison
|
|
149
|
+
version_int = version_value.to_i if version_value.respond_to?(:to_i)
|
|
150
|
+
|
|
151
|
+
if version_value.is_a?(Integer) || version_int&.positive?
|
|
152
|
+
"QUICKHELP v#{version_value}"
|
|
153
|
+
elsif header.version.is_a?(Symbol)
|
|
154
|
+
header.version.to_s.upcase.sub("WINHELP", "WinHelp ")
|
|
155
|
+
else
|
|
156
|
+
header.version.to_s
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
puts "HLP File: #{file}"
|
|
160
|
+
puts "Format: #{format_name || 'unknown'}"
|
|
161
|
+
puts "\nFiles:"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Display list of files in HLP
|
|
165
|
+
#
|
|
166
|
+
# @param decompressor [Decompressor] The decompressor instance
|
|
167
|
+
# @param header [Object] The HLP header object
|
|
168
|
+
# @return [void]
|
|
169
|
+
def display_files(_decompressor, header)
|
|
170
|
+
if header.respond_to?(:files)
|
|
171
|
+
header.files.each do |f|
|
|
172
|
+
puts " #{f.filename} (#{f.length} bytes)"
|
|
173
|
+
end
|
|
174
|
+
else
|
|
175
|
+
puts " (File listing not available for this format)"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Display comprehensive HLP information
|
|
180
|
+
#
|
|
181
|
+
# @param header [Object] The HLP header object
|
|
182
|
+
# @param file [String] Original file path
|
|
183
|
+
# @return [void]
|
|
184
|
+
def display_hlp_info(header, file)
|
|
185
|
+
puts "HLP File Information"
|
|
186
|
+
puts "=" * 50
|
|
187
|
+
puts "Filename: #{file}"
|
|
188
|
+
|
|
189
|
+
if header.respond_to?(:version)
|
|
190
|
+
version_value = header.version
|
|
191
|
+
# Convert BinData objects to integer for comparison
|
|
192
|
+
version_int = version_value.to_i if version_value.respond_to?(:to_i)
|
|
193
|
+
|
|
194
|
+
format_name = if version_value.is_a?(Integer) || version_int&.positive?
|
|
195
|
+
"QUICKHELP v#{version_value}"
|
|
196
|
+
elsif version_value.is_a?(Symbol)
|
|
197
|
+
version_value.to_s.upcase.sub("WINHELP", "WinHelp ")
|
|
198
|
+
else
|
|
199
|
+
version_value.to_s
|
|
200
|
+
end
|
|
201
|
+
puts "Format: #{format_name}"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if header.respond_to?(:length)
|
|
205
|
+
puts "Size: #{header.length} bytes"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if header.respond_to?(:files)
|
|
209
|
+
puts "Files: #{header.files.size}"
|
|
210
|
+
puts ""
|
|
211
|
+
puts "Files:"
|
|
212
|
+
header.files.each do |f|
|
|
213
|
+
puts " #{f.filename}"
|
|
214
|
+
puts " Size: #{f.length} bytes"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Create QuickHelp format HLP file
|
|
220
|
+
#
|
|
221
|
+
# @param output [String] Output file path
|
|
222
|
+
# @param files [Array<String>] Input files
|
|
223
|
+
# @param options [Hash] Additional options
|
|
224
|
+
# @return [void]
|
|
225
|
+
def create_quickhelp(output, files, _options)
|
|
226
|
+
compressor = Compressor.new
|
|
227
|
+
|
|
228
|
+
files.each do |f|
|
|
229
|
+
# Default: add files with compression
|
|
230
|
+
archive_name = File.basename(f)
|
|
231
|
+
compressor.add_file(f, "/#{archive_name}", compress: true)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
puts "Creating #{output} with #{files.size} file(s) (QuickHelp format)" if verbose?
|
|
235
|
+
bytes = compressor.generate(output)
|
|
236
|
+
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Create Windows Help format HLP file
|
|
240
|
+
#
|
|
241
|
+
# @param output [String] Output file path
|
|
242
|
+
# @param files [Array<String>] Input files
|
|
243
|
+
# @param options [Hash] Additional options
|
|
244
|
+
# @return [void]
|
|
245
|
+
def create_winhelp(output, files, _options)
|
|
246
|
+
compressor = Compressor.create_winhelp
|
|
247
|
+
|
|
248
|
+
files.each do |f|
|
|
249
|
+
archive_name = File.basename(f)
|
|
250
|
+
# WinHelp compression uses different API
|
|
251
|
+
compressor.add_file(f, "/#{archive_name}")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
puts "Creating #{output} with #{files.size} file(s) (WinHelp format)" if verbose?
|
|
255
|
+
bytes = compressor.generate(output)
|
|
256
|
+
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Parse format option to symbol
|
|
260
|
+
#
|
|
261
|
+
# @param format_value [String, Symbol] The format type
|
|
262
|
+
# @return [Symbol] The format symbol
|
|
263
|
+
def parse_format_option(format_value)
|
|
264
|
+
return :quickhelp if format_value.nil?
|
|
265
|
+
|
|
266
|
+
format = format_value.to_sym
|
|
267
|
+
valid_formats = %i[quickhelp winhelp]
|
|
268
|
+
|
|
269
|
+
# Map :hlp to default :quickhelp format
|
|
270
|
+
format = :quickhelp if format == :hlp
|
|
271
|
+
|
|
272
|
+
unless valid_formats.include?(format)
|
|
273
|
+
raise ArgumentError,
|
|
274
|
+
"Invalid HLP format: #{format_value}. " \
|
|
275
|
+
"Valid options: #{valid_formats.join(', ')}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
format
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -1,271 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "quickhelp/compressor"
|
|
4
|
+
require_relative "winhelp/compressor"
|
|
5
|
+
|
|
3
6
|
module Cabriolet
|
|
4
7
|
module HLP
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# HLP files contain an internal file system where files can be compressed
|
|
8
|
-
# using LZSS MODE_MSHELP compression. The compressor builds the archive
|
|
9
|
-
# structure and compresses files as needed.
|
|
8
|
+
# Main compressor for HLP files
|
|
10
9
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# lack of test fixtures and incomplete libmspack implementation.
|
|
10
|
+
# Creates HLP files in either QuickHelp or Windows Help format.
|
|
11
|
+
# By default, uses QuickHelp format for compatibility.
|
|
14
12
|
class Compressor
|
|
15
13
|
attr_reader :io_system
|
|
16
14
|
|
|
17
|
-
#
|
|
18
|
-
DEFAULT_BUFFER_SIZE = 2048
|
|
19
|
-
|
|
20
|
-
# Initialize a new HLP compressor
|
|
15
|
+
# Initialize compressor
|
|
21
16
|
#
|
|
22
|
-
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
23
|
-
# default
|
|
17
|
+
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
|
|
24
18
|
def initialize(io_system = nil)
|
|
25
19
|
@io_system = io_system || System::IOSystem.new
|
|
26
|
-
@
|
|
20
|
+
@quickhelp = QuickHelp::Compressor.new(@io_system)
|
|
27
21
|
end
|
|
28
22
|
|
|
29
|
-
# Add a file to the
|
|
23
|
+
# Add a file to the archive
|
|
30
24
|
#
|
|
31
25
|
# @param source_path [String] Path to source file
|
|
32
|
-
# @param hlp_path [String] Path within
|
|
33
|
-
# @param compress [Boolean] Whether to compress
|
|
26
|
+
# @param hlp_path [String] Path within archive
|
|
27
|
+
# @param compress [Boolean] Whether to compress
|
|
34
28
|
# @return [void]
|
|
35
29
|
def add_file(source_path, hlp_path, compress: true)
|
|
36
|
-
@
|
|
37
|
-
source: source_path,
|
|
38
|
-
hlp_path: hlp_path,
|
|
39
|
-
compress: compress,
|
|
40
|
-
}
|
|
30
|
+
@quickhelp.add_file(source_path, hlp_path, compress: compress)
|
|
41
31
|
end
|
|
42
32
|
|
|
43
|
-
# Add data from memory
|
|
33
|
+
# Add data from memory
|
|
44
34
|
#
|
|
45
35
|
# @param data [String] Data to add
|
|
46
|
-
# @param hlp_path [String] Path within
|
|
47
|
-
# @param compress [Boolean] Whether to compress
|
|
36
|
+
# @param hlp_path [String] Path within archive
|
|
37
|
+
# @param compress [Boolean] Whether to compress
|
|
48
38
|
# @return [void]
|
|
49
39
|
def add_data(data, hlp_path, compress: true)
|
|
50
|
-
@
|
|
51
|
-
data: data,
|
|
52
|
-
hlp_path: hlp_path,
|
|
53
|
-
compress: compress,
|
|
54
|
-
}
|
|
40
|
+
@quickhelp.add_data(data, hlp_path, compress: compress)
|
|
55
41
|
end
|
|
56
42
|
|
|
57
|
-
# Generate HLP archive
|
|
43
|
+
# Generate HLP archive (QuickHelp format by default)
|
|
58
44
|
#
|
|
59
|
-
# @param output_file [String]
|
|
60
|
-
# @param options [Hash]
|
|
61
|
-
# @
|
|
62
|
-
# @return [Integer] Bytes written to output file
|
|
63
|
-
# @raise [Errors::CompressionError] if compression fails
|
|
45
|
+
# @param output_file [String] Output file path
|
|
46
|
+
# @param options [Hash] Format options
|
|
47
|
+
# @return [Integer] Bytes written
|
|
64
48
|
def generate(output_file, **options)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
|
|
68
|
-
|
|
69
|
-
begin
|
|
70
|
-
# Compress all files and collect metadata
|
|
71
|
-
compressed_files = compress_all_files
|
|
72
|
-
|
|
73
|
-
# Calculate directory size first
|
|
74
|
-
directory_size = calculate_directory_size(compressed_files)
|
|
75
|
-
|
|
76
|
-
# Calculate offsets
|
|
77
|
-
header_size = 18 # Header structure size
|
|
78
|
-
directory_offset = header_size
|
|
79
|
-
data_offset = header_size + directory_size
|
|
80
|
-
|
|
81
|
-
# Assign file offsets
|
|
82
|
-
current_offset = data_offset
|
|
83
|
-
compressed_files.each do |file_info|
|
|
84
|
-
file_info[:offset] = current_offset
|
|
85
|
-
current_offset += file_info[:compressed_data].bytesize
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Write header
|
|
89
|
-
header_bytes = write_header(
|
|
90
|
-
output_handle,
|
|
91
|
-
version,
|
|
92
|
-
compressed_files.size,
|
|
93
|
-
directory_offset,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# Write directory
|
|
97
|
-
directory_bytes = write_directory(output_handle, compressed_files)
|
|
98
|
-
|
|
99
|
-
# Write file data
|
|
100
|
-
data_bytes = write_file_data(output_handle, compressed_files)
|
|
101
|
-
|
|
102
|
-
header_bytes + directory_bytes + data_bytes
|
|
103
|
-
ensure
|
|
104
|
-
@io_system.close(output_handle) if output_handle
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
# Compress all files and collect metadata
|
|
111
|
-
#
|
|
112
|
-
# @return [Array<Hash>] Array of file information hashes
|
|
113
|
-
def compress_all_files
|
|
114
|
-
@files.map do |file_spec|
|
|
115
|
-
compress_file_spec(file_spec)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Compress a single file specification
|
|
120
|
-
#
|
|
121
|
-
# @param file_spec [Hash] File specification
|
|
122
|
-
# @return [Hash] File information with compressed data
|
|
123
|
-
def compress_file_spec(file_spec)
|
|
124
|
-
# Get source data
|
|
125
|
-
data = file_spec[:data] || read_file_data(file_spec[:source])
|
|
126
|
-
|
|
127
|
-
# Compress if requested
|
|
128
|
-
compressed_data = if file_spec[:compress]
|
|
129
|
-
compress_data_lzss(data)
|
|
130
|
-
else
|
|
131
|
-
data
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
hlp_path: file_spec[:hlp_path],
|
|
136
|
-
uncompressed_size: data.bytesize,
|
|
137
|
-
compressed_data: compressed_data,
|
|
138
|
-
compressed: file_spec[:compress],
|
|
139
|
-
}
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Read file data from disk
|
|
143
|
-
#
|
|
144
|
-
# @param filename [String] Path to file
|
|
145
|
-
# @return [String] File contents
|
|
146
|
-
def read_file_data(filename)
|
|
147
|
-
handle = @io_system.open(filename, Constants::MODE_READ)
|
|
148
|
-
begin
|
|
149
|
-
data = +""
|
|
150
|
-
loop do
|
|
151
|
-
chunk = @io_system.read(handle, DEFAULT_BUFFER_SIZE)
|
|
152
|
-
break if chunk.empty?
|
|
153
|
-
|
|
154
|
-
data << chunk
|
|
155
|
-
end
|
|
156
|
-
data
|
|
157
|
-
ensure
|
|
158
|
-
@io_system.close(handle)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Compress data using LZSS MODE_MSHELP
|
|
163
|
-
#
|
|
164
|
-
# @param data [String] Data to compress
|
|
165
|
-
# @return [String] Compressed data
|
|
166
|
-
def compress_data_lzss(data)
|
|
167
|
-
input_handle = System::MemoryHandle.new(data)
|
|
168
|
-
output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
169
|
-
|
|
170
|
-
compressor = Compressors::LZSS.new(
|
|
171
|
-
@io_system,
|
|
172
|
-
input_handle,
|
|
173
|
-
output_handle,
|
|
174
|
-
DEFAULT_BUFFER_SIZE,
|
|
175
|
-
Compressors::LZSS::MODE_MSHELP,
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
compressor.compress
|
|
179
|
-
output_handle.data
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Calculate directory size
|
|
183
|
-
#
|
|
184
|
-
# @param compressed_files [Array<Hash>] Compressed file information
|
|
185
|
-
# @return [Integer] Directory size in bytes
|
|
186
|
-
def calculate_directory_size(compressed_files)
|
|
187
|
-
size = 0
|
|
188
|
-
compressed_files.each do |file_info|
|
|
189
|
-
# 4 bytes for filename length
|
|
190
|
-
# N bytes for filename
|
|
191
|
-
# 4 + 4 + 4 + 1 = 13 bytes for file metadata
|
|
192
|
-
size += 4 + file_info[:hlp_path].bytesize + 13
|
|
193
|
-
end
|
|
194
|
-
size
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Write HLP header
|
|
198
|
-
#
|
|
199
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
200
|
-
# @param version [Integer] Format version
|
|
201
|
-
# @param file_count [Integer] Number of files
|
|
202
|
-
# @param directory_offset [Integer] Offset to directory
|
|
203
|
-
# @return [Integer] Number of bytes written
|
|
204
|
-
def write_header(output_handle, version, file_count, directory_offset)
|
|
205
|
-
header = Binary::HLPStructures::Header.new
|
|
206
|
-
header.signature = Binary::HLPStructures::SIGNATURE
|
|
207
|
-
header.version = version
|
|
208
|
-
header.file_count = file_count
|
|
209
|
-
header.directory_offset = directory_offset
|
|
210
|
-
|
|
211
|
-
header_data = header.to_binary_s
|
|
212
|
-
written = @io_system.write(output_handle, header_data)
|
|
213
|
-
|
|
214
|
-
unless written == header_data.bytesize
|
|
215
|
-
raise Errors::CompressionError,
|
|
216
|
-
"Failed to write HLP header"
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
written
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Write file directory
|
|
223
|
-
#
|
|
224
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
225
|
-
# @param compressed_files [Array<Hash>] Compressed file information
|
|
226
|
-
# @return [Integer] Number of bytes written
|
|
227
|
-
def write_directory(output_handle, compressed_files)
|
|
228
|
-
bytes_written = 0
|
|
229
|
-
|
|
230
|
-
compressed_files.each do |file_info|
|
|
231
|
-
# Write filename length
|
|
232
|
-
filename = file_info[:hlp_path].b
|
|
233
|
-
length_data = [filename.bytesize].pack("V")
|
|
234
|
-
bytes_written += @io_system.write(output_handle, length_data)
|
|
235
|
-
|
|
236
|
-
# Write filename
|
|
237
|
-
bytes_written += @io_system.write(output_handle, filename)
|
|
238
|
-
|
|
239
|
-
# Write file metadata
|
|
240
|
-
metadata = [
|
|
241
|
-
file_info[:offset],
|
|
242
|
-
file_info[:uncompressed_size],
|
|
243
|
-
file_info[:compressed_data].bytesize,
|
|
244
|
-
file_info[:compressed] ? 1 : 0,
|
|
245
|
-
].pack("V3C")
|
|
246
|
-
bytes_written += @io_system.write(output_handle, metadata)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
bytes_written
|
|
49
|
+
@quickhelp.generate(output_file, **options)
|
|
250
50
|
end
|
|
251
51
|
|
|
252
|
-
#
|
|
52
|
+
# Create a Windows Help format HLP file
|
|
253
53
|
#
|
|
254
|
-
# @param
|
|
255
|
-
# @param
|
|
256
|
-
# @return [
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
compressed_files.each do |file_info|
|
|
261
|
-
written = @io_system.write(
|
|
262
|
-
output_handle,
|
|
263
|
-
file_info[:compressed_data],
|
|
264
|
-
)
|
|
265
|
-
bytes_written += written
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
bytes_written
|
|
54
|
+
# @param output_file [String] Output file path
|
|
55
|
+
# @param options [Hash] Format options
|
|
56
|
+
# @return [WinHelp::Compressor] Compressor for building WinHelp file
|
|
57
|
+
def self.create_winhelp(io_system = nil)
|
|
58
|
+
WinHelp::Compressor.new(io_system)
|
|
269
59
|
end
|
|
270
60
|
end
|
|
271
61
|
end
|