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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +703 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +167 -16
  6. data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +108 -84
  13. data/lib/cabriolet/cab/decompressor.rb +16 -20
  14. data/lib/cabriolet/cab/extractor.rb +142 -66
  15. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  16. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  17. data/lib/cabriolet/checksum.rb +49 -0
  18. data/lib/cabriolet/chm/command_handler.rb +227 -0
  19. data/lib/cabriolet/chm/compressor.rb +7 -3
  20. data/lib/cabriolet/chm/decompressor.rb +39 -21
  21. data/lib/cabriolet/chm/parser.rb +5 -2
  22. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  23. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  24. data/lib/cabriolet/cli/command_registry.rb +83 -0
  25. data/lib/cabriolet/cli.rb +356 -607
  26. data/lib/cabriolet/collections/file_collection.rb +175 -0
  27. data/lib/cabriolet/compressors/base.rb +1 -1
  28. data/lib/cabriolet/compressors/lzx.rb +241 -54
  29. data/lib/cabriolet/compressors/mszip.rb +35 -3
  30. data/lib/cabriolet/compressors/quantum.rb +36 -95
  31. data/lib/cabriolet/decompressors/base.rb +1 -1
  32. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  33. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  34. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  35. data/lib/cabriolet/decompressors/quantum.rb +83 -53
  36. data/lib/cabriolet/errors.rb +3 -0
  37. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  38. data/lib/cabriolet/extraction/extractor.rb +171 -0
  39. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  40. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  41. data/lib/cabriolet/file_entry.rb +156 -0
  42. data/lib/cabriolet/file_manager.rb +144 -0
  43. data/lib/cabriolet/format_base.rb +79 -0
  44. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  45. data/lib/cabriolet/hlp/compressor.rb +28 -238
  46. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  47. data/lib/cabriolet/hlp/parser.rb +52 -101
  48. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  49. data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
  50. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  51. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  52. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  53. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  54. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  55. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  56. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  57. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  58. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  59. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  60. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  61. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  62. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  63. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  64. data/lib/cabriolet/huffman/encoder.rb +15 -12
  65. data/lib/cabriolet/huffman/tree.rb +85 -1
  66. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  67. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  68. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  69. data/lib/cabriolet/lit/command_handler.rb +221 -0
  70. data/lib/cabriolet/lit/compressor.rb +119 -168
  71. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  72. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  73. data/lib/cabriolet/lit/decompressor.rb +518 -152
  74. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  75. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  76. data/lib/cabriolet/lit/header_writer.rb +124 -0
  77. data/lib/cabriolet/lit/parser.rb +670 -0
  78. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  79. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  80. data/lib/cabriolet/models/hlp_file.rb +130 -29
  81. data/lib/cabriolet/models/hlp_header.rb +105 -17
  82. data/lib/cabriolet/models/lit_header.rb +212 -25
  83. data/lib/cabriolet/models/szdd_header.rb +10 -2
  84. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  85. data/lib/cabriolet/oab/command_handler.rb +257 -0
  86. data/lib/cabriolet/oab/compressor.rb +17 -8
  87. data/lib/cabriolet/oab/decompressor.rb +41 -10
  88. data/lib/cabriolet/offset_calculator.rb +81 -0
  89. data/lib/cabriolet/plugin.rb +233 -0
  90. data/lib/cabriolet/plugin_manager.rb +453 -0
  91. data/lib/cabriolet/plugin_validator.rb +422 -0
  92. data/lib/cabriolet/quantum_shared.rb +105 -0
  93. data/lib/cabriolet/system/io_system.rb +3 -0
  94. data/lib/cabriolet/system/memory_handle.rb +17 -4
  95. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  96. data/lib/cabriolet/szdd/compressor.rb +15 -11
  97. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  98. data/lib/cabriolet/version.rb +1 -1
  99. data/lib/cabriolet.rb +181 -20
  100. metadata +69 -4
  101. data/lib/cabriolet/auto.rb +0 -173
  102. 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
- # HLP internal file model
5
+ # QuickHelp topic model
6
6
  #
7
- # Represents a file within an HLP archive. HLP files contain an internal
8
- # file system where each file can be compressed using LZSS MODE_MSHELP.
9
- class HLPFile
10
- attr_accessor :filename, :offset, :length, :compressed_length,
11
- :compressed, :data
12
-
13
- # Initialize HLP file
14
- #
15
- # @param filename [String] File name within the HLP archive
16
- # @param offset [Integer] Offset in the HLP archive
17
- # @param length [Integer] Uncompressed file length
18
- # @param compressed_length [Integer] Compressed file length
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
- @length = length
25
- @compressed_length = compressed_length
26
- @compressed = compressed
27
- @data = nil
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
- # Check if file is compressed
70
+ # Apply style to a range of characters
31
71
  #
32
- # @return [Boolean] true if file is compressed
33
- def compressed?
34
- @compressed
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
- # Get the size to read from archive
82
+ # Apply link to a range of characters
38
83
  #
39
- # @return [Integer] Size to read (compressed or uncompressed)
40
- def read_size
41
- compressed? ? @compressed_length : @length
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
- # HLP file header model
5
+ # QuickHelp database header model
6
6
  #
7
- # NOTE: This implementation is based on the knowledge that HLP files use
8
- # LZSS compression with MODE_MSHELP, but cannot be fully validated due to
9
- # lack of test fixtures. Testing relies on round-trip
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, :filename, :length, :files
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
- # Initialize HLP header
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 specific to HLP)
18
- # @param version [Integer] Format version
19
- # @param filename [String] Original filename
20
- # @param length [Integer] Uncompressed file length
21
- def initialize(magic: nil, version: nil, filename: nil, length: 0)
22
- @magic = magic
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
- @length = length
26
- @files = []
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
- !@magic.nil? && !@version.nil?
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