cabriolet 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +700 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +154 -14
- data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +35 -43
- data/lib/cabriolet/cab/decompressor.rb +14 -19
- data/lib/cabriolet/cab/extractor.rb +140 -31
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +34 -45
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +3 -2
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +633 -38
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +67 -17
- metadata +33 -2
|
@@ -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
|
|
@@ -1,197 +1,157 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "quickhelp/decompressor"
|
|
4
|
+
require_relative "winhelp/decompressor"
|
|
5
|
+
|
|
3
6
|
module Cabriolet
|
|
4
7
|
module HLP
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# HLP files use LZSS compression with MODE_MSHELP and contain an internal
|
|
8
|
-
# file system. Files are decompressed using the Decompressors::LZSS class.
|
|
8
|
+
# Main decompressor for HLP files
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# Detects the HLP format variant and delegates to the appropriate decompressor:
|
|
11
|
+
# - QuickHelp (DOS format)
|
|
12
|
+
# - Windows Help (WinHelp 3.x/4.x format)
|
|
13
13
|
class Decompressor
|
|
14
14
|
attr_reader :io_system, :parser
|
|
15
|
-
attr_accessor :buffer_size
|
|
16
15
|
|
|
17
|
-
#
|
|
18
|
-
DEFAULT_BUFFER_SIZE = 2048
|
|
19
|
-
|
|
20
|
-
# Initialize a new HLP decompressor
|
|
16
|
+
# Initialize decompressor
|
|
21
17
|
#
|
|
22
|
-
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
23
|
-
# default
|
|
18
|
+
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
|
|
24
19
|
def initialize(io_system = nil)
|
|
25
20
|
@io_system = io_system || System::IOSystem.new
|
|
26
21
|
@parser = Parser.new(@io_system)
|
|
27
|
-
@
|
|
22
|
+
@delegate = nil
|
|
23
|
+
@current_format = nil
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
# Open and parse
|
|
26
|
+
# Open and parse HLP file
|
|
31
27
|
#
|
|
32
|
-
# @param filename [String] Path to
|
|
33
|
-
# @return [Models::HLPHeader] Parsed header
|
|
34
|
-
# @raise [Errors::ParseError] if the file is not a valid HLP
|
|
28
|
+
# @param filename [String] Path to HLP file
|
|
29
|
+
# @return [Models::HLPHeader, Models::WinHelpHeader] Parsed header
|
|
35
30
|
def open(filename)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
@current_format = detect_format(filename)
|
|
32
|
+
|
|
33
|
+
case @current_format
|
|
34
|
+
when :quickhelp
|
|
35
|
+
@delegate = QuickHelp::Decompressor.new(@io_system)
|
|
36
|
+
@delegate.open(filename)
|
|
37
|
+
when :winhelp
|
|
38
|
+
@delegate = WinHelp::Decompressor.new(filename, @io_system)
|
|
39
|
+
@delegate.parse
|
|
40
|
+
else
|
|
41
|
+
raise Cabriolet::ParseError, "Unknown HLP format"
|
|
42
|
+
end
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
# Close
|
|
45
|
+
# Close HLP file
|
|
42
46
|
#
|
|
43
|
-
# @param
|
|
44
|
-
# @return [
|
|
45
|
-
def close(
|
|
46
|
-
|
|
47
|
-
# File handles are managed separately during extraction
|
|
47
|
+
# @param header [Models::HLPHeader, Models::WinHelpHeader] Header to close
|
|
48
|
+
# @return [nil]
|
|
49
|
+
def close(header)
|
|
50
|
+
@delegate&.close(header) if @delegate.respond_to?(:close)
|
|
48
51
|
nil
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
# Extract a file
|
|
54
|
+
# Extract a file
|
|
52
55
|
#
|
|
53
|
-
# @param header [Models::HLPHeader]
|
|
54
|
-
# @param hlp_file [Models::HLPFile] File to extract
|
|
55
|
-
# @param output_path [String]
|
|
56
|
-
# @return [Integer]
|
|
57
|
-
# @raise [Errors::DecompressionError] if extraction fails
|
|
56
|
+
# @param header [Models::HLPHeader, Models::WinHelpHeader] Parsed header
|
|
57
|
+
# @param hlp_file [Models::HLPFile] File to extract
|
|
58
|
+
# @param output_path [String] Output file path
|
|
59
|
+
# @return [Integer] Bytes written
|
|
58
60
|
def extract_file(header, hlp_file, output_path)
|
|
59
|
-
raise ArgumentError, "Header must not be nil"
|
|
60
|
-
raise ArgumentError, "HLP file must not be nil"
|
|
61
|
-
raise ArgumentError, "Output path must not be nil"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
bytes_written = if hlp_file.compressed?
|
|
72
|
-
decompress_file(input_handle, output_handle,
|
|
73
|
-
hlp_file)
|
|
74
|
-
else
|
|
75
|
-
copy_file(input_handle, output_handle, hlp_file)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Verify size if expected
|
|
79
|
-
if bytes_written != hlp_file.length && Cabriolet.verbose
|
|
80
|
-
warn "[Cabriolet] WARNING: extracted #{bytes_written} bytes, " \
|
|
81
|
-
"expected #{hlp_file.length} bytes"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
bytes_written
|
|
85
|
-
ensure
|
|
86
|
-
@io_system.close(input_handle) if input_handle
|
|
87
|
-
@io_system.close(output_handle) if output_handle
|
|
61
|
+
raise ArgumentError, "Header must not be nil" if header.nil?
|
|
62
|
+
raise ArgumentError, "HLP file must not be nil" if hlp_file.nil?
|
|
63
|
+
raise ArgumentError, "Output path must not be nil" if output_path.nil?
|
|
64
|
+
|
|
65
|
+
case @current_format
|
|
66
|
+
when :quickhelp
|
|
67
|
+
@delegate.extract_file(header, hlp_file, output_path)
|
|
68
|
+
when :winhelp
|
|
69
|
+
# WinHelp uses different extraction model
|
|
70
|
+
raise NotImplementedError,
|
|
71
|
+
"WinHelp file extraction not yet implemented via this API"
|
|
88
72
|
end
|
|
89
73
|
end
|
|
90
74
|
|
|
91
|
-
# Extract
|
|
75
|
+
# Extract file to memory
|
|
92
76
|
#
|
|
93
|
-
# @param header [Models::HLPHeader]
|
|
77
|
+
# @param header [Models::HLPHeader, Models::WinHelpHeader] Parsed header
|
|
94
78
|
# @param hlp_file [Models::HLPFile] File to extract
|
|
95
|
-
# @return [String]
|
|
96
|
-
# @raise [Errors::DecompressionError] if extraction fails
|
|
79
|
+
# @return [String] File contents
|
|
97
80
|
def extract_file_to_memory(header, hlp_file)
|
|
98
|
-
raise ArgumentError, "Header must not be nil"
|
|
99
|
-
raise ArgumentError, "HLP file must not be nil"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Constants::SEEK_START)
|
|
108
|
-
|
|
109
|
-
if hlp_file.compressed?
|
|
110
|
-
decompress_file(input_handle, output_handle, hlp_file)
|
|
111
|
-
else
|
|
112
|
-
copy_file(input_handle, output_handle, hlp_file)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
output_handle.data
|
|
116
|
-
ensure
|
|
117
|
-
@io_system.close(input_handle) if input_handle
|
|
81
|
+
raise ArgumentError, "Header must not be nil" if header.nil?
|
|
82
|
+
raise ArgumentError, "HLP file must not be nil" if hlp_file.nil?
|
|
83
|
+
|
|
84
|
+
case @current_format
|
|
85
|
+
when :quickhelp
|
|
86
|
+
@delegate.extract_file_to_memory(header, hlp_file)
|
|
87
|
+
when :winhelp
|
|
88
|
+
raise NotImplementedError,
|
|
89
|
+
"WinHelp memory extraction not yet implemented via this API"
|
|
118
90
|
end
|
|
119
91
|
end
|
|
120
92
|
|
|
121
|
-
# Extract all files
|
|
93
|
+
# Extract all files
|
|
122
94
|
#
|
|
123
|
-
# @param header [Models::HLPHeader]
|
|
124
|
-
# @param output_dir [String]
|
|
95
|
+
# @param header [Models::HLPHeader, Models::WinHelpHeader] Parsed header
|
|
96
|
+
# @param output_dir [String] Output directory
|
|
125
97
|
# @return [Integer] Number of files extracted
|
|
126
|
-
# @raise [Errors::DecompressionError] if extraction fails
|
|
127
98
|
def extract_all(header, output_dir)
|
|
128
|
-
raise ArgumentError, "Header must not be nil"
|
|
129
|
-
raise ArgumentError, "Output directory must not be nil" unless
|
|
130
|
-
output_dir
|
|
99
|
+
raise ArgumentError, "Header must not be nil" if header.nil?
|
|
131
100
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
header.files.each do |hlp_file|
|
|
137
|
-
output_path = ::File.join(output_dir, hlp_file.filename)
|
|
138
|
-
|
|
139
|
-
# Create subdirectories if needed
|
|
140
|
-
output_subdir = ::File.dirname(output_path)
|
|
141
|
-
FileUtils.mkdir_p(output_subdir)
|
|
101
|
+
if output_dir.nil?
|
|
102
|
+
raise ArgumentError,
|
|
103
|
+
"Output directory must not be nil"
|
|
104
|
+
end
|
|
142
105
|
|
|
143
|
-
|
|
144
|
-
|
|
106
|
+
case @current_format
|
|
107
|
+
when :quickhelp
|
|
108
|
+
@delegate.extract_all(header, output_dir)
|
|
109
|
+
when :winhelp
|
|
110
|
+
@delegate.extract_all(output_dir)
|
|
145
111
|
end
|
|
112
|
+
end
|
|
146
113
|
|
|
147
|
-
|
|
114
|
+
# Extract (alternate API taking filename directly)
|
|
115
|
+
#
|
|
116
|
+
# @param filename [String] Path to HLP file
|
|
117
|
+
# @param output_dir [String] Output directory
|
|
118
|
+
# @return [Integer] Number of files extracted
|
|
119
|
+
def self.extract(filename, output_dir, io_system = nil)
|
|
120
|
+
io_sys = io_system || System::IOSystem.new
|
|
121
|
+
decompressor = new(io_sys)
|
|
122
|
+
header = decompressor.open(filename)
|
|
123
|
+
decompressor.extract_all(header, output_dir)
|
|
148
124
|
end
|
|
149
125
|
|
|
150
126
|
private
|
|
151
127
|
|
|
152
|
-
#
|
|
128
|
+
# Detect HLP format
|
|
153
129
|
#
|
|
154
|
-
# @param
|
|
155
|
-
# @
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# @return [Integer] Number of bytes written
|
|
159
|
-
def decompress_file(input_handle, output_handle, hlp_file)
|
|
160
|
-
# Create LZSS decompressor with MODE_MSHELP
|
|
161
|
-
decompressor = Decompressors::LZSS.new(
|
|
162
|
-
@io_system,
|
|
163
|
-
input_handle,
|
|
164
|
-
output_handle,
|
|
165
|
-
@buffer_size,
|
|
166
|
-
Decompressors::LZSS::MODE_MSHELP,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# Decompress
|
|
170
|
-
decompressor.decompress(hlp_file.compressed_length)
|
|
171
|
-
end
|
|
130
|
+
# @param filename [String] Path to file
|
|
131
|
+
# @return [Symbol] :quickhelp or :winhelp
|
|
132
|
+
def detect_format(filename)
|
|
133
|
+
handle = @io_system.open(filename, Constants::MODE_READ)
|
|
172
134
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
remaining = hlp_file.length
|
|
183
|
-
|
|
184
|
-
while remaining.positive?
|
|
185
|
-
chunk_size = [remaining, @buffer_size].min
|
|
186
|
-
data = @io_system.read(input_handle, chunk_size)
|
|
187
|
-
break if data.empty?
|
|
188
|
-
|
|
189
|
-
written = @io_system.write(output_handle, data)
|
|
190
|
-
bytes_written += written
|
|
191
|
-
remaining -= written
|
|
192
|
-
end
|
|
135
|
+
begin
|
|
136
|
+
sig_data = @io_system.read(handle, 4)
|
|
137
|
+
|
|
138
|
+
# Check QuickHelp signature ("LN" = 0x4C 0x4E)
|
|
139
|
+
return :quickhelp if sig_data[0..1] == Binary::HLPStructures::SIGNATURE
|
|
140
|
+
|
|
141
|
+
# Check WinHelp 3.x magic (little-endian 16-bit: 0x35F3)
|
|
142
|
+
magic_word = sig_data[0..1].unpack1("v")
|
|
143
|
+
return :winhelp if magic_word == 0x35F3
|
|
193
144
|
|
|
194
|
-
|
|
145
|
+
# Check WinHelp 4.x magic (little-endian 32-bit, low 16 bits: 0x5F3F)
|
|
146
|
+
magic_dword = sig_data.unpack1("V")
|
|
147
|
+
return :winhelp if (magic_dword & 0xFFFF) == 0x5F3F
|
|
148
|
+
|
|
149
|
+
raise Cabriolet::ParseError, "Unknown HLP format: #{sig_data.bytes.map do |b|
|
|
150
|
+
format('0x%02X', b)
|
|
151
|
+
end.join(' ')}"
|
|
152
|
+
ensure
|
|
153
|
+
@io_system.close(handle)
|
|
154
|
+
end
|
|
195
155
|
end
|
|
196
156
|
end
|
|
197
157
|
end
|