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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "directory_builder"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module LIT
|
|
7
|
+
# Builds piece data for LIT files
|
|
8
|
+
class PieceBuilder
|
|
9
|
+
# Build piece 0 data (file size information)
|
|
10
|
+
#
|
|
11
|
+
# @param file_data [Array<Hash>] File data array
|
|
12
|
+
# @return [String] Binary piece 0 data
|
|
13
|
+
def self.build_piece0(file_data)
|
|
14
|
+
# Calculate total content size
|
|
15
|
+
content_size = file_data.sum { |f| f[:uncompressed_size] }
|
|
16
|
+
|
|
17
|
+
data = [Binary::LITStructures::Tags::SIZE_PIECE].pack("V")
|
|
18
|
+
data += [content_size].pack("V")
|
|
19
|
+
data += [0, 0].pack("VV") # High bits, reserved
|
|
20
|
+
data
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Build piece 1 data (directory IFCM structure)
|
|
24
|
+
#
|
|
25
|
+
# @param directory [Hash] Directory structure from DirectoryBuilder
|
|
26
|
+
# @return [String] Binary piece 1 data
|
|
27
|
+
def self.build_piece1(directory)
|
|
28
|
+
builder = DirectoryBuilder.new(chunk_size: directory[:chunk_size])
|
|
29
|
+
|
|
30
|
+
# Build IFCM header
|
|
31
|
+
ifcm = Binary::LITStructures::IFCMHeader.new
|
|
32
|
+
ifcm.tag = Binary::LITStructures::Tags::IFCM
|
|
33
|
+
ifcm.version = 1
|
|
34
|
+
ifcm.chunk_size = directory[:chunk_size]
|
|
35
|
+
ifcm.param = 0x100000
|
|
36
|
+
ifcm.reserved1 = 0xFFFFFFFF
|
|
37
|
+
ifcm.reserved2 = 0xFFFFFFFF
|
|
38
|
+
ifcm.num_chunks = directory[:num_chunks]
|
|
39
|
+
ifcm.reserved3 = 0
|
|
40
|
+
|
|
41
|
+
data = ifcm.to_binary_s
|
|
42
|
+
|
|
43
|
+
# Build AOLL chunk with directory entries
|
|
44
|
+
directory[:entries].each do |entry|
|
|
45
|
+
builder.add_entry(
|
|
46
|
+
name: entry[:name],
|
|
47
|
+
section: entry[:section],
|
|
48
|
+
offset: entry[:offset],
|
|
49
|
+
size: entry[:size],
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
aoll_chunk = builder.build_aoll_chunk
|
|
54
|
+
data += aoll_chunk
|
|
55
|
+
|
|
56
|
+
# Pad to fill piece (8KB standard)
|
|
57
|
+
target_size = 8192
|
|
58
|
+
if data.bytesize < target_size
|
|
59
|
+
data += "\x00" * (target_size - data.bytesize)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
data
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build piece 2 data (index information)
|
|
66
|
+
#
|
|
67
|
+
# @return [String] Binary piece 2 data
|
|
68
|
+
def self.build_piece2
|
|
69
|
+
# Minimal index data for foundation
|
|
70
|
+
"\x00" * 512
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "guid_generator"
|
|
4
|
+
require_relative "content_type_detector"
|
|
5
|
+
require_relative "directory_builder"
|
|
6
|
+
|
|
7
|
+
module Cabriolet
|
|
8
|
+
module LIT
|
|
9
|
+
# Builds complete LIT structure from file data
|
|
10
|
+
class StructureBuilder
|
|
11
|
+
attr_reader :io_system, :version, :language_id, :creator_id
|
|
12
|
+
|
|
13
|
+
# Initialize structure builder
|
|
14
|
+
#
|
|
15
|
+
# @param io_system [System::IOSystem] I/O system for file operations
|
|
16
|
+
# @param version [Integer] LIT format version
|
|
17
|
+
# @param language_id [Integer] Language ID
|
|
18
|
+
# @param creator_id [Integer] Creator ID
|
|
19
|
+
def initialize(io_system:, version: 1, language_id: 0x409, creator_id: 0)
|
|
20
|
+
@io_system = io_system
|
|
21
|
+
@version = version
|
|
22
|
+
@language_id = language_id
|
|
23
|
+
@creator_id = creator_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Build complete LIT structure from file data
|
|
27
|
+
#
|
|
28
|
+
# @param file_data [Array<Hash>] File data array from prepare_files
|
|
29
|
+
# @return [Hash] Complete LIT structure
|
|
30
|
+
def build(file_data)
|
|
31
|
+
structure = {}
|
|
32
|
+
|
|
33
|
+
# Generate GUIDs
|
|
34
|
+
structure[:header_guid] = GuidGenerator.generate
|
|
35
|
+
structure[:piece3_guid] = Binary::LITStructures::GUIDs::PIECE3
|
|
36
|
+
structure[:piece4_guid] = Binary::LITStructures::GUIDs::PIECE4
|
|
37
|
+
|
|
38
|
+
# Build directory
|
|
39
|
+
structure[:directory] = build_directory(file_data)
|
|
40
|
+
|
|
41
|
+
# Build sections
|
|
42
|
+
structure[:sections] = build_sections
|
|
43
|
+
|
|
44
|
+
# Build manifest
|
|
45
|
+
structure[:manifest] = build_manifest(file_data)
|
|
46
|
+
|
|
47
|
+
# Build secondary header metadata
|
|
48
|
+
structure[:secondary_header] = build_secondary_header_metadata
|
|
49
|
+
|
|
50
|
+
# Calculate piece offsets and sizes
|
|
51
|
+
structure[:pieces] = calculate_pieces(structure)
|
|
52
|
+
|
|
53
|
+
# Update secondary header with content offset
|
|
54
|
+
update_secondary_header_content_offset(structure)
|
|
55
|
+
|
|
56
|
+
# Store metadata
|
|
57
|
+
structure[:version] = @version
|
|
58
|
+
structure[:file_data] = file_data
|
|
59
|
+
|
|
60
|
+
structure
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Build directory structure from file data
|
|
66
|
+
#
|
|
67
|
+
# @param file_data [Array<Hash>] File data array
|
|
68
|
+
# @return [Hash] Directory structure
|
|
69
|
+
def build_directory(file_data)
|
|
70
|
+
builder = DirectoryBuilder.new
|
|
71
|
+
|
|
72
|
+
# Add entries for all files
|
|
73
|
+
section = 0
|
|
74
|
+
offset = 0
|
|
75
|
+
|
|
76
|
+
file_data.each do |file_info|
|
|
77
|
+
builder.add_entry(
|
|
78
|
+
name: file_info[:lit_path],
|
|
79
|
+
section: section,
|
|
80
|
+
offset: offset,
|
|
81
|
+
size: file_info[:uncompressed_size],
|
|
82
|
+
)
|
|
83
|
+
offset += file_info[:uncompressed_size]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Calculate NameList size
|
|
87
|
+
namelist_size = calculate_namelist_size
|
|
88
|
+
|
|
89
|
+
# Calculate manifest size
|
|
90
|
+
manifest_size = calculate_manifest_size(file_data)
|
|
91
|
+
|
|
92
|
+
# Add special entries for LIT structure
|
|
93
|
+
builder.add_entry(
|
|
94
|
+
name: Binary::LITStructures::Paths::NAMELIST,
|
|
95
|
+
section: 0,
|
|
96
|
+
offset: offset,
|
|
97
|
+
size: namelist_size,
|
|
98
|
+
)
|
|
99
|
+
offset += namelist_size
|
|
100
|
+
|
|
101
|
+
builder.add_entry(
|
|
102
|
+
name: Binary::LITStructures::Paths::MANIFEST,
|
|
103
|
+
section: 0,
|
|
104
|
+
offset: offset,
|
|
105
|
+
size: manifest_size,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
builder.build
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build sections array
|
|
112
|
+
#
|
|
113
|
+
# @return [Array<Hash>] Sections array
|
|
114
|
+
def build_sections
|
|
115
|
+
# For simple implementation: single uncompressed section
|
|
116
|
+
[
|
|
117
|
+
{
|
|
118
|
+
name: "Uncompressed",
|
|
119
|
+
transforms: [],
|
|
120
|
+
compressed: false,
|
|
121
|
+
encrypted: false,
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Build manifest from file data
|
|
127
|
+
#
|
|
128
|
+
# @param file_data [Array<Hash>] File data array
|
|
129
|
+
# @return [Hash] Manifest structure
|
|
130
|
+
def build_manifest(file_data)
|
|
131
|
+
mappings = []
|
|
132
|
+
|
|
133
|
+
file_data.each_with_index do |file_info, index|
|
|
134
|
+
mappings << {
|
|
135
|
+
offset: index,
|
|
136
|
+
internal_name: file_info[:lit_path],
|
|
137
|
+
original_name: file_info[:lit_path],
|
|
138
|
+
content_type: ContentTypeDetector.content_type(file_info[:lit_path]),
|
|
139
|
+
group: ContentTypeDetector.file_group(file_info[:lit_path]),
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
{ mappings: mappings }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Build secondary header metadata
|
|
147
|
+
#
|
|
148
|
+
# @return [Hash] Secondary header metadata
|
|
149
|
+
def build_secondary_header_metadata
|
|
150
|
+
# Calculate actual secondary header length
|
|
151
|
+
temp_header = Binary::LITStructures::SecondaryHeader.new
|
|
152
|
+
sec_hdr_length = temp_header.to_binary_s.bytesize
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
length: sec_hdr_length,
|
|
156
|
+
entry_chunklen: 0x2000, # 8KB chunks for entry directory
|
|
157
|
+
count_chunklen: 0x200, # 512B chunks for count directory
|
|
158
|
+
entry_unknown: 0x100000,
|
|
159
|
+
count_unknown: 0x20000,
|
|
160
|
+
entry_depth: 1, # No AOLI index layer
|
|
161
|
+
entry_entries: 0, # Will be set when directory built
|
|
162
|
+
count_entries: 0, # Will be set when directory built
|
|
163
|
+
content_offset: 0, # Will be calculated after pieces
|
|
164
|
+
timestamp: Time.now.to_i,
|
|
165
|
+
language_id: @language_id,
|
|
166
|
+
creator_id: @creator_id,
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Calculate piece offsets and sizes
|
|
171
|
+
#
|
|
172
|
+
# @param structure [Hash] Partial structure (needs secondary_header)
|
|
173
|
+
# @return [Array<Hash>] Pieces array
|
|
174
|
+
def calculate_pieces(structure)
|
|
175
|
+
pieces = []
|
|
176
|
+
|
|
177
|
+
# Calculate starting offset (after headers and pieces)
|
|
178
|
+
sec_hdr_length = structure[:secondary_header][:length]
|
|
179
|
+
current_offset = 40 + 80 + sec_hdr_length
|
|
180
|
+
|
|
181
|
+
# Piece 0: File size information (16 bytes)
|
|
182
|
+
pieces << { offset: current_offset, size: 16 }
|
|
183
|
+
current_offset += 16
|
|
184
|
+
|
|
185
|
+
# Piece 1: Directory (IFCM structure)
|
|
186
|
+
# Build DirectoryBuilder to calculate size
|
|
187
|
+
dir_builder = DirectoryBuilder.new(chunk_size: structure[:directory][:chunk_size])
|
|
188
|
+
structure[:directory][:entries].each do |entry|
|
|
189
|
+
dir_builder.add_entry(
|
|
190
|
+
name: entry[:name],
|
|
191
|
+
section: entry[:section],
|
|
192
|
+
offset: entry[:offset],
|
|
193
|
+
size: entry[:size],
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
piece1_size = dir_builder.calculate_size
|
|
197
|
+
pieces << { offset: current_offset, size: piece1_size }
|
|
198
|
+
current_offset += piece1_size
|
|
199
|
+
|
|
200
|
+
# Piece 2: Index information (typically empty or minimal)
|
|
201
|
+
piece2_size = 512
|
|
202
|
+
pieces << { offset: current_offset, size: piece2_size }
|
|
203
|
+
current_offset += piece2_size
|
|
204
|
+
|
|
205
|
+
# Piece 3: Standard GUID (16 bytes)
|
|
206
|
+
pieces << { offset: current_offset, size: 16 }
|
|
207
|
+
current_offset += 16
|
|
208
|
+
|
|
209
|
+
# Piece 4: Standard GUID (16 bytes)
|
|
210
|
+
pieces << { offset: current_offset, size: 16 }
|
|
211
|
+
|
|
212
|
+
pieces
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Update secondary header with final content offset
|
|
216
|
+
#
|
|
217
|
+
# @param structure [Hash] Structure to update
|
|
218
|
+
def update_secondary_header_content_offset(structure)
|
|
219
|
+
pieces = structure[:pieces]
|
|
220
|
+
last_piece = pieces.last
|
|
221
|
+
content_offset = last_piece[:offset] + last_piece[:size]
|
|
222
|
+
|
|
223
|
+
structure[:secondary_header][:content_offset] = content_offset
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Calculate NameList size (estimate)
|
|
227
|
+
#
|
|
228
|
+
# @return [Integer] Estimated size
|
|
229
|
+
def calculate_namelist_size
|
|
230
|
+
# Simple estimate: ~100 bytes for minimal NameList
|
|
231
|
+
100
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Calculate manifest size (estimate)
|
|
235
|
+
#
|
|
236
|
+
# @param file_data [Array<Hash>] File data array
|
|
237
|
+
# @return [Integer] Estimated size
|
|
238
|
+
def calculate_manifest_size(file_data)
|
|
239
|
+
# Rough estimate: directory header + entries
|
|
240
|
+
size = 10 # Directory header
|
|
241
|
+
|
|
242
|
+
file_data.each do |file_info|
|
|
243
|
+
# Per entry: offset (4) + 3 length bytes + names + content type + terminator
|
|
244
|
+
size += 4 + 3
|
|
245
|
+
size += (file_info[:lit_path].bytesize * 2) + 20 + 1
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
size
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -2,44 +2,145 @@
|
|
|
2
2
|
|
|
3
3
|
module Cabriolet
|
|
4
4
|
module Models
|
|
5
|
-
#
|
|
5
|
+
# QuickHelp topic model
|
|
6
6
|
#
|
|
7
|
-
# Represents a
|
|
8
|
-
#
|
|
9
|
-
class
|
|
10
|
-
attr_accessor :
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @param
|
|
16
|
-
# @param
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# @param compressed [Boolean] Whether the file is compressed
|
|
20
|
-
def initialize(filename: nil, offset: 0, length: 0,
|
|
21
|
-
compressed_length: 0, compressed: true)
|
|
22
|
-
@filename = filename
|
|
7
|
+
# Represents a single topic in a QuickHelp help database.
|
|
8
|
+
# Each topic contains formatted text lines with styles and hyperlinks.
|
|
9
|
+
class HLPTopic
|
|
10
|
+
attr_accessor :index, :offset, :size, :lines, :source_data, :metadata
|
|
11
|
+
|
|
12
|
+
# Initialize a QuickHelp topic
|
|
13
|
+
#
|
|
14
|
+
# @param index [Integer] Topic index in the database
|
|
15
|
+
# @param offset [Integer] Offset of topic data in file
|
|
16
|
+
# @param size [Integer] Size of compressed topic data
|
|
17
|
+
def initialize(index: 0, offset: 0, size: 0)
|
|
18
|
+
@index = index
|
|
23
19
|
@offset = offset
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
20
|
+
@size = size
|
|
21
|
+
@lines = []
|
|
22
|
+
@source_data = nil
|
|
23
|
+
@metadata = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if topic has any content
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if topic has lines
|
|
29
|
+
def empty?
|
|
30
|
+
@lines.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get plain text content (without formatting)
|
|
34
|
+
#
|
|
35
|
+
# @return [String] plain text of all lines
|
|
36
|
+
def plain_text
|
|
37
|
+
@lines.map(&:text).join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Add a line to the topic
|
|
41
|
+
#
|
|
42
|
+
# @param line [HLPLine] line to add
|
|
43
|
+
# @return [void]
|
|
44
|
+
def add_line(line)
|
|
45
|
+
@lines << line
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# QuickHelp topic line model
|
|
50
|
+
#
|
|
51
|
+
# Represents a single line within a topic, including text, styles, and links.
|
|
52
|
+
class HLPLine
|
|
53
|
+
attr_accessor :text, :attributes
|
|
54
|
+
|
|
55
|
+
# Initialize a topic line
|
|
56
|
+
#
|
|
57
|
+
# @param text [String] plain text content
|
|
58
|
+
def initialize(text = "")
|
|
59
|
+
@text = text
|
|
60
|
+
@attributes = Array.new(text.length) { TextAttribute.new }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get line length in characters
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer] character count
|
|
66
|
+
def length
|
|
67
|
+
@text.length
|
|
28
68
|
end
|
|
29
69
|
|
|
30
|
-
#
|
|
70
|
+
# Apply style to a range of characters
|
|
31
71
|
#
|
|
32
|
-
# @
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
# @param start_index [Integer] start position (0-based)
|
|
73
|
+
# @param end_index [Integer] end position (0-based, inclusive)
|
|
74
|
+
# @param style [Integer] style flags
|
|
75
|
+
# @return [void]
|
|
76
|
+
def apply_style(start_index, end_index, style)
|
|
77
|
+
(start_index..end_index).each do |i|
|
|
78
|
+
@attributes[i].style = style if i < @attributes.length
|
|
79
|
+
end
|
|
35
80
|
end
|
|
36
81
|
|
|
37
|
-
#
|
|
82
|
+
# Apply link to a range of characters
|
|
38
83
|
#
|
|
39
|
-
# @
|
|
40
|
-
|
|
41
|
-
|
|
84
|
+
# @param start_index [Integer] start position (1-based, as per format)
|
|
85
|
+
# @param end_index [Integer] end position (1-based, inclusive)
|
|
86
|
+
# @param link [String] link target (context string or topic index)
|
|
87
|
+
# @return [void]
|
|
88
|
+
def apply_link(start_index, end_index, link)
|
|
89
|
+
# Convert from 1-based to 0-based indexing
|
|
90
|
+
start_idx = start_index - 1
|
|
91
|
+
end_idx = end_index - 1
|
|
92
|
+
|
|
93
|
+
(start_idx..end_idx).each do |i|
|
|
94
|
+
@attributes[i].link = link if i >= 0 && i < @attributes.length
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Text attribute model
|
|
100
|
+
#
|
|
101
|
+
# Represents style and link information for a single character.
|
|
102
|
+
class TextAttribute
|
|
103
|
+
attr_accessor :style, :link
|
|
104
|
+
|
|
105
|
+
# Initialize text attribute
|
|
106
|
+
#
|
|
107
|
+
# @param style [Integer] style flags (bold, italic, underline)
|
|
108
|
+
# @param link [String, nil] link target if any
|
|
109
|
+
def initialize(style = 0, link = nil)
|
|
110
|
+
@style = style
|
|
111
|
+
@link = link
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if character is bold
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true if bold
|
|
117
|
+
def bold?
|
|
118
|
+
@style.anybits?(Binary::HLPStructures::TextStyle::BOLD)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if character is italic
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] true if italic
|
|
124
|
+
def italic?
|
|
125
|
+
@style.anybits?(Binary::HLPStructures::TextStyle::ITALIC)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if character is underlined
|
|
129
|
+
#
|
|
130
|
+
# @return [Boolean] true if underlined
|
|
131
|
+
def underline?
|
|
132
|
+
@style.anybits?(Binary::HLPStructures::TextStyle::UNDERLINE)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if character has a link
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] true if linked
|
|
138
|
+
def linked?
|
|
139
|
+
!@link.nil?
|
|
42
140
|
end
|
|
43
141
|
end
|
|
142
|
+
|
|
143
|
+
# Backward compatibility alias
|
|
144
|
+
HLPFile = HLPTopic
|
|
44
145
|
end
|
|
45
146
|
end
|
|
@@ -2,35 +2,123 @@
|
|
|
2
2
|
|
|
3
3
|
module Cabriolet
|
|
4
4
|
module Models
|
|
5
|
-
#
|
|
5
|
+
# QuickHelp database header model
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# compression/decompression and comparison with libmspack tools if
|
|
11
|
-
# available.
|
|
7
|
+
# Represents the metadata of a QuickHelp help database (.HLP file).
|
|
8
|
+
# HLP files contain topics, context strings, and optional compression
|
|
9
|
+
# (keyword dictionary and Huffman coding).
|
|
12
10
|
class HLPHeader
|
|
13
|
-
attr_accessor :magic, :version, :
|
|
11
|
+
attr_accessor :magic, :version, :attributes, :control_character,
|
|
12
|
+
:topic_count, :context_count, :display_width, :predefined_ctx_count, :database_name, :topic_index_offset, :context_strings_offset, :context_map_offset, :keywords_offset, :huffman_tree_offset, :topic_text_offset, :database_size, :filename, :keywords, :huffman_tree
|
|
14
13
|
|
|
15
|
-
#
|
|
14
|
+
# Topics and context data
|
|
15
|
+
attr_accessor :topics, :contexts, :context_map
|
|
16
|
+
|
|
17
|
+
# Initialize QuickHelp database header
|
|
16
18
|
#
|
|
17
|
-
# @param magic [String] Magic number (should be
|
|
18
|
-
# @param version [Integer] Format version
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
# @param magic [String] Magic number (should be 0x4C 0x4E)
|
|
20
|
+
# @param version [Integer] Format version (should be 2)
|
|
21
|
+
# @param attributes [Integer] Attribute flags
|
|
22
|
+
# @param control_character [Integer] Control character (usually ':' or 0xFF)
|
|
23
|
+
# @param topic_count [Integer] Number of topics
|
|
24
|
+
# @param context_count [Integer] Number of context strings
|
|
25
|
+
# @param display_width [Integer] Display width in characters
|
|
26
|
+
# @param database_name [String] Database name for external links
|
|
27
|
+
def initialize(
|
|
28
|
+
magic: nil,
|
|
29
|
+
version: 2,
|
|
30
|
+
attributes: 0,
|
|
31
|
+
control_character: 0x3A,
|
|
32
|
+
topic_count: 0,
|
|
33
|
+
context_count: 0,
|
|
34
|
+
display_width: 80,
|
|
35
|
+
predefined_ctx_count: 0,
|
|
36
|
+
database_name: "",
|
|
37
|
+
topic_index_offset: 0,
|
|
38
|
+
context_strings_offset: 0,
|
|
39
|
+
context_map_offset: 0,
|
|
40
|
+
keywords_offset: 0,
|
|
41
|
+
huffman_tree_offset: 0,
|
|
42
|
+
topic_text_offset: 0,
|
|
43
|
+
database_size: 0,
|
|
44
|
+
filename: nil
|
|
45
|
+
)
|
|
46
|
+
@magic = magic || Binary::HLPStructures::SIGNATURE
|
|
23
47
|
@version = version
|
|
48
|
+
@attributes = attributes
|
|
49
|
+
@control_character = control_character
|
|
50
|
+
@topic_count = topic_count
|
|
51
|
+
@context_count = context_count
|
|
52
|
+
@display_width = display_width
|
|
53
|
+
@predefined_ctx_count = predefined_ctx_count
|
|
54
|
+
@database_name = database_name
|
|
55
|
+
@topic_index_offset = topic_index_offset
|
|
56
|
+
@context_strings_offset = context_strings_offset
|
|
57
|
+
@context_map_offset = context_map_offset
|
|
58
|
+
@keywords_offset = keywords_offset
|
|
59
|
+
@huffman_tree_offset = huffman_tree_offset
|
|
60
|
+
@topic_text_offset = topic_text_offset
|
|
61
|
+
@database_size = database_size
|
|
24
62
|
@filename = filename
|
|
25
|
-
|
|
26
|
-
|
|
63
|
+
|
|
64
|
+
# Collections
|
|
65
|
+
@topics = []
|
|
66
|
+
@contexts = []
|
|
67
|
+
@context_map = []
|
|
68
|
+
@keywords = []
|
|
69
|
+
@huffman_tree = nil
|
|
27
70
|
end
|
|
28
71
|
|
|
29
72
|
# Check if header is valid
|
|
30
73
|
#
|
|
31
74
|
# @return [Boolean] true if header appears valid
|
|
32
75
|
def valid?
|
|
33
|
-
|
|
76
|
+
@magic == Binary::HLPStructures::SIGNATURE &&
|
|
77
|
+
@version == 2 &&
|
|
78
|
+
@topic_count >= 0 &&
|
|
79
|
+
@context_count >= 0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if case-sensitive context strings
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] true if case-sensitive
|
|
85
|
+
def case_sensitive?
|
|
86
|
+
@attributes.anybits?(Binary::HLPStructures::Attributes::CASE_SENSITIVE)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if database is locked (cannot be decoded by HELPMAKE)
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean] true if locked
|
|
92
|
+
def locked?
|
|
93
|
+
@attributes.anybits?(Binary::HLPStructures::Attributes::LOCKED)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if keyword compression is used
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean] true if keywords present
|
|
99
|
+
def has_keywords?
|
|
100
|
+
@keywords_offset.positive? && !@keywords.empty?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if Huffman compression is used
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean] true if Huffman tree present
|
|
106
|
+
def has_huffman?
|
|
107
|
+
@huffman_tree_offset.positive? && !@huffman_tree.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get control character as string
|
|
111
|
+
#
|
|
112
|
+
# @return [String] control character
|
|
113
|
+
def control_char
|
|
114
|
+
@control_character.chr(Encoding::ASCII)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get database name without null padding
|
|
118
|
+
#
|
|
119
|
+
# @return [String] trimmed database name
|
|
120
|
+
def db_name
|
|
121
|
+
@database_name.split("\x00").first || ""
|
|
34
122
|
end
|
|
35
123
|
end
|
|
36
124
|
end
|