cabriolet 0.2.0 → 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 +3 -0
- data/lib/cabriolet/binary/bitstream.rb +32 -21
- data/lib/cabriolet/binary/bitstream_writer.rb +21 -4
- data/lib/cabriolet/cab/compressor.rb +85 -53
- data/lib/cabriolet/cab/decompressor.rb +2 -1
- data/lib/cabriolet/cab/extractor.rb +2 -35
- 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/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/quantum.rb +3 -51
- data/lib/cabriolet/decompressors/quantum.rb +81 -52
- 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/format_base.rb +79 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +28 -503
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -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/huffman/encoder.rb +15 -12
- data/lib/cabriolet/lit/compressor.rb +45 -689
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- 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/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +114 -3
- metadata +38 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "topic_builder"
|
|
4
|
+
require_relative "topic_compressor"
|
|
5
|
+
require_relative "structure_builder"
|
|
6
|
+
require_relative "file_writer"
|
|
7
|
+
|
|
3
8
|
module Cabriolet
|
|
4
9
|
module HLP
|
|
5
10
|
module QuickHelp
|
|
@@ -18,8 +23,7 @@ module Cabriolet
|
|
|
18
23
|
|
|
19
24
|
# Initialize a new QuickHelp compressor
|
|
20
25
|
#
|
|
21
|
-
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
22
|
-
# default
|
|
26
|
+
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
|
|
23
27
|
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
24
28
|
def initialize(io_system = nil, algorithm_factory = nil)
|
|
25
29
|
@io_system = io_system || System::IOSystem.new
|
|
@@ -83,18 +87,20 @@ module Cabriolet
|
|
|
83
87
|
topics = prepare_topics
|
|
84
88
|
|
|
85
89
|
# Build QuickHelp structure
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
case_sensitive,
|
|
90
|
+
structure_builder = StructureBuilder.new(
|
|
91
|
+
version: version,
|
|
92
|
+
database_name: database_name,
|
|
93
|
+
control_char: control_char,
|
|
94
|
+
case_sensitive: case_sensitive,
|
|
92
95
|
)
|
|
96
|
+
qh_structure = structure_builder.build(topics)
|
|
93
97
|
|
|
94
98
|
# Write to output file
|
|
95
99
|
output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
|
|
96
100
|
begin
|
|
97
|
-
|
|
101
|
+
file_writer = FileWriter.new(@io_system)
|
|
102
|
+
bytes_written = file_writer.write_quickhelp_file(output_handle,
|
|
103
|
+
qh_structure)
|
|
98
104
|
bytes_written
|
|
99
105
|
ensure
|
|
100
106
|
@io_system.close(output_handle)
|
|
@@ -103,36 +109,21 @@ module Cabriolet
|
|
|
103
109
|
|
|
104
110
|
private
|
|
105
111
|
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
# @return [Array<Hash>] Array of file information hashes
|
|
109
|
-
def compress_all_files
|
|
110
|
-
@files.map do |file_spec|
|
|
111
|
-
compress_file_spec(file_spec)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Compress a single file specification
|
|
112
|
+
# Prepare topics from added files
|
|
116
113
|
#
|
|
117
|
-
# @
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# Compress if requested
|
|
124
|
-
compressed_data = if file_spec[:compress]
|
|
125
|
-
compress_data_lzss(data)
|
|
126
|
-
else
|
|
127
|
-
data
|
|
128
|
-
end
|
|
114
|
+
# @return [Array<Hash>] Topic information
|
|
115
|
+
def prepare_topics
|
|
116
|
+
@files.map.with_index do |file_spec, index|
|
|
117
|
+
# Get source data
|
|
118
|
+
data = file_spec[:data] || read_file_data(file_spec[:source])
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
{
|
|
121
|
+
index: index,
|
|
122
|
+
text: data,
|
|
123
|
+
context: file_spec[:hlp_path],
|
|
124
|
+
compress: file_spec[:compress],
|
|
125
|
+
}
|
|
126
|
+
end
|
|
136
127
|
end
|
|
137
128
|
|
|
138
129
|
# Read file data from disk
|
|
@@ -154,472 +145,6 @@ module Cabriolet
|
|
|
154
145
|
@io_system.close(handle)
|
|
155
146
|
end
|
|
156
147
|
end
|
|
157
|
-
|
|
158
|
-
# Compress data using LZSS MODE_MSHELP
|
|
159
|
-
#
|
|
160
|
-
# @param data [String] Data to compress
|
|
161
|
-
# @return [String] Compressed data
|
|
162
|
-
def compress_data_lzss(data)
|
|
163
|
-
input_handle = System::MemoryHandle.new(data)
|
|
164
|
-
output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
165
|
-
|
|
166
|
-
compressor = @algorithm_factory.create(
|
|
167
|
-
:lzss,
|
|
168
|
-
:compressor,
|
|
169
|
-
@io_system,
|
|
170
|
-
input_handle,
|
|
171
|
-
output_handle,
|
|
172
|
-
DEFAULT_BUFFER_SIZE,
|
|
173
|
-
mode: Compressors::LZSS::MODE_MSHELP,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
compressor.compress
|
|
177
|
-
output_handle.data
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Calculate directory size
|
|
181
|
-
#
|
|
182
|
-
# @param compressed_files [Array<Hash>] Compressed file information
|
|
183
|
-
# @return [Integer] Directory size in bytes
|
|
184
|
-
def calculate_directory_size(compressed_files)
|
|
185
|
-
size = 0
|
|
186
|
-
compressed_files.each do |file_info|
|
|
187
|
-
# 4 bytes for filename length
|
|
188
|
-
# N bytes for filename
|
|
189
|
-
# 4 + 4 + 4 + 1 = 13 bytes for file metadata
|
|
190
|
-
size += 4 + file_info[:hlp_path].bytesize + 13
|
|
191
|
-
end
|
|
192
|
-
size
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Write QuickHelp header
|
|
196
|
-
#
|
|
197
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
198
|
-
# @param version [Integer] Format version
|
|
199
|
-
# @param file_count [Integer] Number of files
|
|
200
|
-
# @param directory_offset [Integer] Offset to directory
|
|
201
|
-
# @return [Integer] Number of bytes written
|
|
202
|
-
def write_header(output_handle, version, file_count, directory_offset)
|
|
203
|
-
# NOTE: This is a simplified header format and does not match the actual QuickHelp FileHeader structure
|
|
204
|
-
# TODO: Implement proper QuickHelp FileHeader usage
|
|
205
|
-
header = Binary::HLPStructures::FileHeader.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 QuickHelp 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
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Write file data
|
|
253
|
-
#
|
|
254
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
255
|
-
# @param compressed_files [Array<Hash>] Compressed file information
|
|
256
|
-
# @return [Integer] Number of bytes written
|
|
257
|
-
def write_file_data(output_handle, compressed_files)
|
|
258
|
-
bytes_written = 0
|
|
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
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# Prepare topics from added files
|
|
272
|
-
#
|
|
273
|
-
# @return [Array<Hash>] Topic information
|
|
274
|
-
def prepare_topics
|
|
275
|
-
@files.map.with_index do |file_spec, index|
|
|
276
|
-
# Get source data
|
|
277
|
-
data = file_spec[:data] || read_file_data(file_spec[:source])
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
index: index,
|
|
281
|
-
text: data,
|
|
282
|
-
context: file_spec[:hlp_path],
|
|
283
|
-
compress: file_spec[:compress],
|
|
284
|
-
}
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# Build complete QuickHelp structure
|
|
289
|
-
#
|
|
290
|
-
# @param topics [Array<Hash>] Topic data
|
|
291
|
-
# @param version [Integer] Format version
|
|
292
|
-
# @param database_name [String] Database name
|
|
293
|
-
# @param control_char [Integer] Control character
|
|
294
|
-
# @param case_sensitive [Boolean] Case-sensitive contexts
|
|
295
|
-
# @return [Hash] Complete structure ready for writing
|
|
296
|
-
def build_quickhelp_structure(topics, version, database_name,
|
|
297
|
-
control_char, case_sensitive)
|
|
298
|
-
structure = {}
|
|
299
|
-
|
|
300
|
-
# Compress topics
|
|
301
|
-
structure[:topics] = compress_topics(topics)
|
|
302
|
-
|
|
303
|
-
# Build context data
|
|
304
|
-
structure[:contexts] = topics.map { |t| t[:context] }
|
|
305
|
-
structure[:context_map] = topics.map.with_index { |_t, i| i }
|
|
306
|
-
|
|
307
|
-
# Calculate offsets
|
|
308
|
-
structure[:offsets] = calculate_offsets(structure)
|
|
309
|
-
|
|
310
|
-
# Build header
|
|
311
|
-
structure[:header] = build_header(
|
|
312
|
-
structure,
|
|
313
|
-
version,
|
|
314
|
-
database_name,
|
|
315
|
-
control_char,
|
|
316
|
-
case_sensitive,
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
structure
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
# Compress all topics
|
|
323
|
-
#
|
|
324
|
-
# @param topics [Array<Hash>] Topic data
|
|
325
|
-
# @return [Array<Hash>] Compressed topics
|
|
326
|
-
def compress_topics(topics)
|
|
327
|
-
topics.map do |topic|
|
|
328
|
-
compressed = if topic[:compress]
|
|
329
|
-
compress_topic_text(topic[:text])
|
|
330
|
-
else
|
|
331
|
-
topic[:text]
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
{
|
|
335
|
-
text: topic[:text],
|
|
336
|
-
compressed: compressed,
|
|
337
|
-
decompressed_length: topic[:text].bytesize,
|
|
338
|
-
compressed_length: compressed.bytesize,
|
|
339
|
-
compress: topic[:compress],
|
|
340
|
-
}
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
# Compress topic text using QuickHelp keyword compression
|
|
345
|
-
#
|
|
346
|
-
# QuickHelp format uses:
|
|
347
|
-
# - 0x00-0x0F: Literal bytes
|
|
348
|
-
# - 0x10-0x17: Dictionary entry references
|
|
349
|
-
# - 0x18: Run of spaces
|
|
350
|
-
# - 0x19: Run of bytes (repeat)
|
|
351
|
-
# - 0x1A: Escape byte (next byte is literal)
|
|
352
|
-
# - 0x1B-0xFF: Literal bytes
|
|
353
|
-
#
|
|
354
|
-
# Without a dictionary, we encode literals directly and escape
|
|
355
|
-
# control characters (0x10-0x1A) with 0x1A prefix.
|
|
356
|
-
#
|
|
357
|
-
# Topic text format:
|
|
358
|
-
# - Each line: [len][text][newline][attr_len][attrs][0xFF]
|
|
359
|
-
# - len includes itself, text, newline, attr_len
|
|
360
|
-
# - attr_len includes itself and attrs, minimum 1 (just 0xFF terminator)
|
|
361
|
-
#
|
|
362
|
-
# @param text [String] Topic text
|
|
363
|
-
# @return [String] Compressed data with length header
|
|
364
|
-
def compress_topic_text(text)
|
|
365
|
-
# Build topic in QuickHelp internal format
|
|
366
|
-
topic_data = build_topic_data(text)
|
|
367
|
-
|
|
368
|
-
# Encode using QuickHelp keyword compression format
|
|
369
|
-
encoded = encode_keyword_compression(topic_data)
|
|
370
|
-
|
|
371
|
-
# Prepend decompressed length (2 bytes)
|
|
372
|
-
length_header = [topic_data.bytesize].pack("v")
|
|
373
|
-
length_header + encoded
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Build topic data in QuickHelp internal format
|
|
377
|
-
#
|
|
378
|
-
# Topic format as expected by decompressor:
|
|
379
|
-
# - Each line: [text_length][text][newline][attr_len][attrs][0xFF]
|
|
380
|
-
# - text_length = text + newline + 1 (for attr_len byte) = text_bytes + 2
|
|
381
|
-
# - Line structure: text_length byte + text + newline + attr_len byte + attr_data
|
|
382
|
-
#
|
|
383
|
-
# The decompressor reads:
|
|
384
|
-
# - text_length = data.getbyte(pos)
|
|
385
|
-
# - text_bytes = text_length - 2 (reads text, skips newline)
|
|
386
|
-
# - attr_length = data.getbyte(pos after text + newline)
|
|
387
|
-
#
|
|
388
|
-
# @param text [String] Raw topic text
|
|
389
|
-
# @return [String] Formatted topic data
|
|
390
|
-
def build_topic_data(text)
|
|
391
|
-
result = +""
|
|
392
|
-
|
|
393
|
-
# Split text into lines
|
|
394
|
-
lines = text.split("\n")
|
|
395
|
-
|
|
396
|
-
lines.each do |line|
|
|
397
|
-
text_bytes = line.b
|
|
398
|
-
newline = "\x0D" # Carriage return
|
|
399
|
-
|
|
400
|
-
# Attribute section: just 0xFF terminator (attr_len = 1)
|
|
401
|
-
attr_data = "\xFF"
|
|
402
|
-
attr_len = 1
|
|
403
|
-
|
|
404
|
-
# text_length = text + newline + 1 (for attr_len byte)
|
|
405
|
-
# This ensures text_bytes = text_length - 2 gives correct text length
|
|
406
|
-
text_length = text_bytes.bytesize + 2
|
|
407
|
-
|
|
408
|
-
result << text_length.chr
|
|
409
|
-
result << text_bytes
|
|
410
|
-
result << newline
|
|
411
|
-
result << attr_len.chr
|
|
412
|
-
result << attr_data
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
result
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
# Encode data using QuickHelp keyword compression format
|
|
419
|
-
#
|
|
420
|
-
# @param data [String] Data to encode
|
|
421
|
-
# @return [String] Encoded data
|
|
422
|
-
def encode_keyword_compression(data)
|
|
423
|
-
result = +""
|
|
424
|
-
|
|
425
|
-
data.bytes.each do |byte|
|
|
426
|
-
if byte < 0x10 || byte == 0x1B || byte > 0x1A
|
|
427
|
-
# Literal byte (except control range 0x10-0x1A)
|
|
428
|
-
result << byte.chr
|
|
429
|
-
elsif byte.between?(0x10, 0x1A)
|
|
430
|
-
# Control byte - escape it
|
|
431
|
-
result << 0x1A.chr << byte.chr
|
|
432
|
-
else
|
|
433
|
-
# 0x1B is also literal (above control range)
|
|
434
|
-
result << byte.chr
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
result
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
# Calculate all offsets in the file
|
|
442
|
-
#
|
|
443
|
-
# @param structure [Hash] QuickHelp structure
|
|
444
|
-
# @return [Hash] Calculated offsets
|
|
445
|
-
def calculate_offsets(structure)
|
|
446
|
-
offsets = {}
|
|
447
|
-
|
|
448
|
-
# Start after file header (70 bytes = 2 signature + 68 header)
|
|
449
|
-
current_offset = 70
|
|
450
|
-
|
|
451
|
-
# Topic index: (topic_count + 1) * 4 bytes
|
|
452
|
-
offsets[:topic_index] = current_offset
|
|
453
|
-
topic_count = structure[:topics].size
|
|
454
|
-
current_offset += (topic_count + 1) * 4
|
|
455
|
-
|
|
456
|
-
# Context strings: sum of string lengths + null terminators
|
|
457
|
-
offsets[:context_strings] = current_offset
|
|
458
|
-
structure[:contexts].each do |ctx|
|
|
459
|
-
current_offset += ctx.bytesize + 1 # +1 for null terminator
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Context map: context_count * 2 bytes
|
|
463
|
-
offsets[:context_map] = current_offset
|
|
464
|
-
current_offset += structure[:context_map].size * 2
|
|
465
|
-
|
|
466
|
-
# Keywords: not implemented yet, set to 0
|
|
467
|
-
offsets[:keywords] = 0
|
|
468
|
-
|
|
469
|
-
# Huffman tree: not implemented yet, set to 0
|
|
470
|
-
offsets[:huffman_tree] = 0
|
|
471
|
-
|
|
472
|
-
# Topic text: starts after context map
|
|
473
|
-
offsets[:topic_text] = current_offset
|
|
474
|
-
|
|
475
|
-
# Calculate topic text offsets
|
|
476
|
-
offsets[:topic_offsets] = []
|
|
477
|
-
structure[:topics].each do |topic|
|
|
478
|
-
offsets[:topic_offsets] << (current_offset - offsets[:topic_text])
|
|
479
|
-
current_offset += topic[:compressed_length]
|
|
480
|
-
end
|
|
481
|
-
# Add end marker
|
|
482
|
-
offsets[:topic_offsets] << (current_offset - offsets[:topic_text])
|
|
483
|
-
|
|
484
|
-
# Total database size
|
|
485
|
-
offsets[:database_size] = current_offset
|
|
486
|
-
|
|
487
|
-
offsets
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
# Build file header
|
|
491
|
-
#
|
|
492
|
-
# @param structure [Hash] QuickHelp structure
|
|
493
|
-
# @param version [Integer] Format version
|
|
494
|
-
# @param database_name [String] Database name
|
|
495
|
-
# @param control_char [Integer] Control character
|
|
496
|
-
# @param case_sensitive [Boolean] Case-sensitive contexts
|
|
497
|
-
# @return [Hash] Header information
|
|
498
|
-
def build_header(structure, version, database_name, control_char,
|
|
499
|
-
case_sensitive)
|
|
500
|
-
attributes = 0
|
|
501
|
-
attributes |= Binary::HLPStructures::Attributes::CASE_SENSITIVE if case_sensitive
|
|
502
|
-
|
|
503
|
-
{
|
|
504
|
-
version: version,
|
|
505
|
-
attributes: attributes,
|
|
506
|
-
control_character: control_char,
|
|
507
|
-
topic_count: structure[:topics].size,
|
|
508
|
-
context_count: structure[:contexts].size,
|
|
509
|
-
display_width: 80,
|
|
510
|
-
predefined_ctx_count: 0,
|
|
511
|
-
database_name: database_name.ljust(14, "\x00")[0, 14],
|
|
512
|
-
offsets: structure[:offsets],
|
|
513
|
-
}
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
# Write complete QuickHelp file
|
|
517
|
-
#
|
|
518
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
519
|
-
# @param structure [Hash] QuickHelp structure
|
|
520
|
-
# @return [Integer] Bytes written
|
|
521
|
-
def write_quickhelp_file(output_handle, structure)
|
|
522
|
-
bytes_written = 0
|
|
523
|
-
|
|
524
|
-
# Write file header
|
|
525
|
-
bytes_written += write_file_header(output_handle, structure[:header])
|
|
526
|
-
|
|
527
|
-
# Write topic index
|
|
528
|
-
bytes_written += write_topic_index(output_handle,
|
|
529
|
-
structure[:header][:offsets])
|
|
530
|
-
|
|
531
|
-
# Write context strings
|
|
532
|
-
bytes_written += write_context_strings(output_handle,
|
|
533
|
-
structure[:contexts])
|
|
534
|
-
|
|
535
|
-
# Write context map
|
|
536
|
-
bytes_written += write_context_map(output_handle,
|
|
537
|
-
structure[:context_map])
|
|
538
|
-
|
|
539
|
-
# Write topic texts
|
|
540
|
-
bytes_written += write_topic_texts(output_handle, structure[:topics])
|
|
541
|
-
|
|
542
|
-
bytes_written
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
# Write file header
|
|
546
|
-
#
|
|
547
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
548
|
-
# @param header_info [Hash] Header information
|
|
549
|
-
# @return [Integer] Bytes written
|
|
550
|
-
def write_file_header(output_handle, header_info)
|
|
551
|
-
header = Binary::HLPStructures::FileHeader.new
|
|
552
|
-
header.signature = Binary::HLPStructures::SIGNATURE
|
|
553
|
-
header.version = header_info[:version]
|
|
554
|
-
header.attributes = header_info[:attributes]
|
|
555
|
-
header.control_character = header_info[:control_character]
|
|
556
|
-
header.padding1 = 0
|
|
557
|
-
header.topic_count = header_info[:topic_count]
|
|
558
|
-
header.context_count = header_info[:context_count]
|
|
559
|
-
header.display_width = header_info[:display_width]
|
|
560
|
-
header.padding2 = 0
|
|
561
|
-
header.predefined_ctx_count = header_info[:predefined_ctx_count]
|
|
562
|
-
header.database_name = header_info[:database_name]
|
|
563
|
-
header.reserved1 = 0
|
|
564
|
-
header.topic_index_offset = header_info[:offsets][:topic_index]
|
|
565
|
-
header.context_strings_offset = header_info[:offsets][:context_strings]
|
|
566
|
-
header.context_map_offset = header_info[:offsets][:context_map]
|
|
567
|
-
header.keywords_offset = header_info[:offsets][:keywords]
|
|
568
|
-
header.huffman_tree_offset = header_info[:offsets][:huffman_tree]
|
|
569
|
-
header.topic_text_offset = header_info[:offsets][:topic_text]
|
|
570
|
-
header.reserved2 = 0
|
|
571
|
-
header.reserved3 = 0
|
|
572
|
-
header.database_size = header_info[:offsets][:database_size]
|
|
573
|
-
|
|
574
|
-
header_data = header.to_binary_s
|
|
575
|
-
@io_system.write(output_handle, header_data)
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
# Write topic index
|
|
579
|
-
#
|
|
580
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
581
|
-
# @param offsets [Hash] Offset information
|
|
582
|
-
# @return [Integer] Bytes written
|
|
583
|
-
def write_topic_index(output_handle, offsets)
|
|
584
|
-
# Write all topic offsets including end marker
|
|
585
|
-
index_data = offsets[:topic_offsets].pack("V*")
|
|
586
|
-
@io_system.write(output_handle, index_data)
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
# Write context strings
|
|
590
|
-
#
|
|
591
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
592
|
-
# @param contexts [Array<String>] Context strings
|
|
593
|
-
# @return [Integer] Bytes written
|
|
594
|
-
def write_context_strings(output_handle, contexts)
|
|
595
|
-
data = contexts.map { |ctx| "#{ctx}\u0000" }.join
|
|
596
|
-
@io_system.write(output_handle, data)
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
# Write context map
|
|
600
|
-
#
|
|
601
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
602
|
-
# @param context_map [Array<Integer>] Topic indices
|
|
603
|
-
# @return [Integer] Bytes written
|
|
604
|
-
def write_context_map(output_handle, context_map)
|
|
605
|
-
map_data = context_map.pack("v*")
|
|
606
|
-
@io_system.write(output_handle, map_data)
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
# Write topic texts
|
|
610
|
-
#
|
|
611
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
612
|
-
# @param topics [Array<Hash>] Compressed topics
|
|
613
|
-
# @return [Integer] Bytes written
|
|
614
|
-
def write_topic_texts(output_handle, topics)
|
|
615
|
-
total_bytes = 0
|
|
616
|
-
|
|
617
|
-
topics.each do |topic|
|
|
618
|
-
total_bytes += @io_system.write(output_handle, topic[:compressed])
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
total_bytes
|
|
622
|
-
end
|
|
623
148
|
end
|
|
624
149
|
end
|
|
625
150
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module HLP
|
|
5
|
+
module QuickHelp
|
|
6
|
+
# Writes QuickHelp files to disk
|
|
7
|
+
class FileWriter
|
|
8
|
+
# Initialize file writer
|
|
9
|
+
#
|
|
10
|
+
# @param io_system [System::IOSystem] I/O system for writing
|
|
11
|
+
def initialize(io_system)
|
|
12
|
+
@io_system = io_system
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Write complete QuickHelp file
|
|
16
|
+
#
|
|
17
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
18
|
+
# @param structure [Hash] QuickHelp structure
|
|
19
|
+
# @return [Integer] Bytes written
|
|
20
|
+
def write_quickhelp_file(output_handle, structure)
|
|
21
|
+
bytes_written = 0
|
|
22
|
+
|
|
23
|
+
# Write file header
|
|
24
|
+
bytes_written += write_file_header(output_handle, structure[:header])
|
|
25
|
+
|
|
26
|
+
# Write topic index
|
|
27
|
+
bytes_written += write_topic_index(output_handle,
|
|
28
|
+
structure[:header][:offsets])
|
|
29
|
+
|
|
30
|
+
# Write context strings
|
|
31
|
+
bytes_written += write_context_strings(output_handle,
|
|
32
|
+
structure[:contexts])
|
|
33
|
+
|
|
34
|
+
# Write context map
|
|
35
|
+
bytes_written += write_context_map(output_handle,
|
|
36
|
+
structure[:context_map])
|
|
37
|
+
|
|
38
|
+
# Write topic texts
|
|
39
|
+
bytes_written += write_topic_texts(output_handle, structure[:topics])
|
|
40
|
+
|
|
41
|
+
bytes_written
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Write file header
|
|
45
|
+
#
|
|
46
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
47
|
+
# @param header_info [Hash] Header information
|
|
48
|
+
# @return [Integer] Bytes written
|
|
49
|
+
def write_file_header(output_handle, header_info)
|
|
50
|
+
header = Binary::HLPStructures::FileHeader.new
|
|
51
|
+
header.signature = Binary::HLPStructures::SIGNATURE
|
|
52
|
+
header.version = header_info[:version]
|
|
53
|
+
header.attributes = header_info[:attributes]
|
|
54
|
+
header.control_character = header_info[:control_character]
|
|
55
|
+
header.padding1 = 0
|
|
56
|
+
header.topic_count = header_info[:topic_count]
|
|
57
|
+
header.context_count = header_info[:context_count]
|
|
58
|
+
header.display_width = header_info[:display_width]
|
|
59
|
+
header.padding2 = 0
|
|
60
|
+
header.predefined_ctx_count = header_info[:predefined_ctx_count]
|
|
61
|
+
header.database_name = header_info[:database_name]
|
|
62
|
+
header.reserved1 = 0
|
|
63
|
+
header.topic_index_offset = header_info[:offsets][:topic_index]
|
|
64
|
+
header.context_strings_offset = header_info[:offsets][:context_strings]
|
|
65
|
+
header.context_map_offset = header_info[:offsets][:context_map]
|
|
66
|
+
header.keywords_offset = header_info[:offsets][:keywords]
|
|
67
|
+
header.huffman_tree_offset = header_info[:offsets][:huffman_tree]
|
|
68
|
+
header.topic_text_offset = header_info[:offsets][:topic_text]
|
|
69
|
+
header.reserved2 = 0
|
|
70
|
+
header.reserved3 = 0
|
|
71
|
+
header.database_size = header_info[:offsets][:database_size]
|
|
72
|
+
|
|
73
|
+
header_data = header.to_binary_s
|
|
74
|
+
@io_system.write(output_handle, header_data)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Write topic index
|
|
78
|
+
#
|
|
79
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
80
|
+
# @param offsets [Hash] Offset information
|
|
81
|
+
# @return [Integer] Bytes written
|
|
82
|
+
def write_topic_index(output_handle, offsets)
|
|
83
|
+
# Write all topic offsets including end marker
|
|
84
|
+
index_data = offsets[:topic_offsets].pack("V*")
|
|
85
|
+
@io_system.write(output_handle, index_data)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Write context strings
|
|
89
|
+
#
|
|
90
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
91
|
+
# @param contexts [Array<String>] Context strings
|
|
92
|
+
# @return [Integer] Bytes written
|
|
93
|
+
def write_context_strings(output_handle, contexts)
|
|
94
|
+
data = contexts.map { |ctx| "#{ctx}\u0000" }.join
|
|
95
|
+
@io_system.write(output_handle, data)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Write context map
|
|
99
|
+
#
|
|
100
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
101
|
+
# @param context_map [Array<Integer>] Topic indices
|
|
102
|
+
# @return [Integer] Bytes written
|
|
103
|
+
def write_context_map(output_handle, context_map)
|
|
104
|
+
map_data = context_map.pack("v*")
|
|
105
|
+
@io_system.write(output_handle, map_data)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Write topic texts
|
|
109
|
+
#
|
|
110
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
111
|
+
# @param topics [Array<Hash>] Compressed topics
|
|
112
|
+
# @return [Integer] Bytes written
|
|
113
|
+
def write_topic_texts(output_handle, topics)
|
|
114
|
+
total_bytes = 0
|
|
115
|
+
|
|
116
|
+
topics.each do |topic|
|
|
117
|
+
total_bytes += @io_system.write(output_handle, topic[:compressed])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
total_bytes
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|