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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../binary/hlp_structures"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Cabriolet
|
|
7
|
+
module HLP
|
|
8
|
+
module WinHelp
|
|
9
|
+
# B+ tree builder for WinHelp 4.x directory format
|
|
10
|
+
#
|
|
11
|
+
# Builds B+ tree directory structure for WinHelp 4.x files.
|
|
12
|
+
# The directory maps filenames to file offsets using a B+ tree
|
|
13
|
+
# with fixed-size pages.
|
|
14
|
+
class BTreeBuilder
|
|
15
|
+
# Default page size for WinHelp 4.x directory (1KB for catalog/directory)
|
|
16
|
+
DEFAULT_PAGE_SIZE = 0x0400 # 1KB
|
|
17
|
+
|
|
18
|
+
# Page types
|
|
19
|
+
PAGE_TYPE_LEAF = 0
|
|
20
|
+
PAGE_TYPE_INDEX = 1
|
|
21
|
+
|
|
22
|
+
# B+ tree magic number
|
|
23
|
+
BTREE_MAGIC = 0x293B
|
|
24
|
+
|
|
25
|
+
# Flags for B+ tree header
|
|
26
|
+
# Bit 0x0002 is always 1
|
|
27
|
+
# Bit 0x0400 is 1 for catalog/directory
|
|
28
|
+
FLAGS_MAGIC_BIT = 0x0002
|
|
29
|
+
FLAGS_CATALOG_BIT = 0x0400
|
|
30
|
+
|
|
31
|
+
attr_reader :page_size, :structure
|
|
32
|
+
|
|
33
|
+
# Initialize B+ tree builder
|
|
34
|
+
#
|
|
35
|
+
# @param page_size [Integer] Page size in bytes (default: 1KB)
|
|
36
|
+
# @param structure [String] Structure string describing data format
|
|
37
|
+
def initialize(page_size: DEFAULT_PAGE_SIZE, structure: "FFz")
|
|
38
|
+
@page_size = page_size
|
|
39
|
+
@structure = structure
|
|
40
|
+
@entries = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add a file entry to the B+ tree
|
|
44
|
+
#
|
|
45
|
+
# @param filename [String] Internal filename (e.g., "|SYSTEM")
|
|
46
|
+
# @param offset [Integer] File offset in help file
|
|
47
|
+
# @param size [Integer] File size in bytes
|
|
48
|
+
def add_entry(filename, offset, size)
|
|
49
|
+
@entries << { filename: filename, offset: offset, size: size }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build B+ tree structure
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash] Hash containing :header, :pages
|
|
55
|
+
def build
|
|
56
|
+
return build_empty if @entries.empty?
|
|
57
|
+
|
|
58
|
+
# Sort entries by filename
|
|
59
|
+
sorted_entries = @entries.sort_by { |e| e[:filename] }
|
|
60
|
+
|
|
61
|
+
# Build leaf pages
|
|
62
|
+
leaf_pages = build_leaf_pages(sorted_entries)
|
|
63
|
+
|
|
64
|
+
# Build index pages if needed
|
|
65
|
+
if leaf_pages.size > 1
|
|
66
|
+
index_pages = build_index_pages(leaf_pages)
|
|
67
|
+
root_page = index_pages.first[:page_num]
|
|
68
|
+
n_levels = 2
|
|
69
|
+
else
|
|
70
|
+
index_pages = []
|
|
71
|
+
root_page = leaf_pages.first[:page_num]
|
|
72
|
+
n_levels = 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Build B+ tree header
|
|
76
|
+
header = build_header(
|
|
77
|
+
total_pages: leaf_pages.size + index_pages.size,
|
|
78
|
+
root_page: root_page,
|
|
79
|
+
n_levels: n_levels,
|
|
80
|
+
total_entries: @entries.size,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Combine all pages
|
|
84
|
+
all_pages = index_pages + leaf_pages
|
|
85
|
+
|
|
86
|
+
{ header: header, pages: all_pages }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Build empty B+ tree (single empty leaf page)
|
|
92
|
+
#
|
|
93
|
+
# @return [Hash] Hash containing :header, :pages
|
|
94
|
+
def build_empty
|
|
95
|
+
# Create empty leaf page
|
|
96
|
+
leaf_page = {
|
|
97
|
+
page_num: 0,
|
|
98
|
+
data: build_empty_leaf_page,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
header = build_header(
|
|
102
|
+
total_pages: 1,
|
|
103
|
+
root_page: 0,
|
|
104
|
+
n_levels: 1,
|
|
105
|
+
total_entries: 0,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
{ header: header, pages: [leaf_page] }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build B+ tree header
|
|
112
|
+
#
|
|
113
|
+
# @param total_pages [Integer] Total number of pages
|
|
114
|
+
# @param root_page [Integer] Root page number
|
|
115
|
+
# @param n_levels [Integer] Number of levels in tree
|
|
116
|
+
# @param total_entries [Integer] Total number of entries
|
|
117
|
+
# @return [Binary::HLPStructures::WinHelpBTreeHeader] B+ tree header
|
|
118
|
+
def build_header(total_pages:, root_page:, n_levels:, total_entries:)
|
|
119
|
+
Binary::HLPStructures::WinHelpBTreeHeader.new(
|
|
120
|
+
magic: BTREE_MAGIC,
|
|
121
|
+
flags: FLAGS_MAGIC_BIT | FLAGS_CATALOG_BIT,
|
|
122
|
+
page_size: @page_size,
|
|
123
|
+
structure: @structure.ljust(16, "\x00"),
|
|
124
|
+
must_be_zero: 0,
|
|
125
|
+
page_splits: 0,
|
|
126
|
+
root_page: root_page,
|
|
127
|
+
must_be_neg_one: 0xFFFF,
|
|
128
|
+
total_pages: total_pages,
|
|
129
|
+
n_levels: n_levels,
|
|
130
|
+
total_btree_entries: total_entries,
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Build leaf pages from entries
|
|
135
|
+
#
|
|
136
|
+
# @param entries [Array<Hash>] Sorted file entries
|
|
137
|
+
# @return [Array<Hash>] Array of page hashes with :page_num and :data
|
|
138
|
+
def build_leaf_pages(entries)
|
|
139
|
+
pages = []
|
|
140
|
+
current_page_data = StringIO.new
|
|
141
|
+
page_num = 0
|
|
142
|
+
|
|
143
|
+
entries.each do |entry|
|
|
144
|
+
# Check if this entry fits in current page
|
|
145
|
+
entry_size = entry[:filename].bytesize + 1 + 4 # filename + null + offset
|
|
146
|
+
header_size = 8 # leaf node header
|
|
147
|
+
|
|
148
|
+
# Check if we need a new page
|
|
149
|
+
if (current_page_data.size + entry_size + header_size) > @page_size
|
|
150
|
+
# Finish current page and start new one
|
|
151
|
+
pages << finish_leaf_page(current_page_data, page_num,
|
|
152
|
+
pages.empty?)
|
|
153
|
+
page_num += 1
|
|
154
|
+
current_page_data = StringIO.new
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Write entry to current page
|
|
158
|
+
current_page_data.write(entry[:filename])
|
|
159
|
+
current_page_data.write("\x00") # null terminator
|
|
160
|
+
current_page_data.write([entry[:offset]].pack("V")) # 4-byte offset
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Finish last page
|
|
164
|
+
if current_page_data.size.positive?
|
|
165
|
+
pages << finish_leaf_page(current_page_data, page_num, pages.empty?)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
pages
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Finish a leaf page by adding header
|
|
172
|
+
#
|
|
173
|
+
# @param page_data [StringIO] Page data without header
|
|
174
|
+
# @param page_num [Integer] Page number
|
|
175
|
+
# @param is_first [Boolean] Whether this is the first page
|
|
176
|
+
# @return [Hash] Page hash with :page_num and :data
|
|
177
|
+
def finish_leaf_page(page_data, page_num, is_first)
|
|
178
|
+
data = page_data.string
|
|
179
|
+
n_entries = count_entries(data)
|
|
180
|
+
|
|
181
|
+
# Build leaf node header
|
|
182
|
+
# - 2 bytes: unused (we use 0)
|
|
183
|
+
# - 2 bytes: nEntries
|
|
184
|
+
# - 2 bytes: PreviousPage (0xFFFF for first)
|
|
185
|
+
# - 2 bytes: NextPage (0xFFFF for last, to be determined)
|
|
186
|
+
header = [
|
|
187
|
+
0, # unused
|
|
188
|
+
n_entries,
|
|
189
|
+
is_first ? 0xFFFF : page_num - 1, # previous page
|
|
190
|
+
0xFFFF, # next page (will update if more pages added)
|
|
191
|
+
].pack("vvvv")
|
|
192
|
+
|
|
193
|
+
{ page_num: page_num, data: header + data }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Build empty leaf page
|
|
197
|
+
#
|
|
198
|
+
# @return [String] Empty leaf page data
|
|
199
|
+
def build_empty_leaf_page
|
|
200
|
+
# Empty leaf has header with nEntries = 0
|
|
201
|
+
[
|
|
202
|
+
0, # unused
|
|
203
|
+
0, # nEntries = 0
|
|
204
|
+
0xFFFF, # previous page
|
|
205
|
+
0xFFFF, # next page
|
|
206
|
+
].pack("vvvv")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Build index pages from leaf pages
|
|
210
|
+
#
|
|
211
|
+
# @param leaf_pages [Array<Hash>] Leaf page hashes
|
|
212
|
+
# @return [Array<Hash>] Array of index page hashes
|
|
213
|
+
def build_index_pages(leaf_pages)
|
|
214
|
+
# For simplicity, create single index page pointing to all leaf pages
|
|
215
|
+
# In a real implementation, this would recursively build index pages
|
|
216
|
+
index_data = StringIO.new
|
|
217
|
+
|
|
218
|
+
leaf_pages.each do |page|
|
|
219
|
+
# For index pages, entries are: (filename, page_number)
|
|
220
|
+
# We use the first filename from each leaf page as key
|
|
221
|
+
first_filename = extract_first_filename(page[:data])
|
|
222
|
+
index_data.write(first_filename)
|
|
223
|
+
index_data.write("\x00") # null terminator
|
|
224
|
+
index_data.write([page[:page_num]].pack("v")) # 2-byte page number
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
data = index_data.string
|
|
228
|
+
n_entries = leaf_pages.size
|
|
229
|
+
|
|
230
|
+
# Build index node header
|
|
231
|
+
# - 2 bytes: unused (we use 0)
|
|
232
|
+
# - 2 bytes: nEntries
|
|
233
|
+
# - 2 bytes: PreviousPage (0xFFFF - no previous)
|
|
234
|
+
header = [
|
|
235
|
+
0, # unused
|
|
236
|
+
n_entries,
|
|
237
|
+
0xFFFF, # previous page (none for root)
|
|
238
|
+
].pack("vvv")
|
|
239
|
+
|
|
240
|
+
[{
|
|
241
|
+
page_num: leaf_pages.size, # Index pages come after leaf pages
|
|
242
|
+
data: header + data,
|
|
243
|
+
}]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Extract first filename from page data
|
|
247
|
+
#
|
|
248
|
+
# @param page_data [String] Page data with header
|
|
249
|
+
# @return [String] First filename in page
|
|
250
|
+
def extract_first_filename(page_data)
|
|
251
|
+
# Skip 8-byte header
|
|
252
|
+
data_start = 8
|
|
253
|
+
data = page_data[data_start..]
|
|
254
|
+
|
|
255
|
+
# Filename is null-terminated
|
|
256
|
+
null_pos = data.index("\x00")
|
|
257
|
+
return "" if null_pos.nil?
|
|
258
|
+
|
|
259
|
+
data[0...null_pos]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Count entries in page data
|
|
263
|
+
#
|
|
264
|
+
# @param data [String] Page data without header
|
|
265
|
+
# @return [Integer] Number of entries
|
|
266
|
+
def count_entries(data)
|
|
267
|
+
count = 0
|
|
268
|
+
pos = 0
|
|
269
|
+
|
|
270
|
+
while pos < data.bytesize
|
|
271
|
+
# Find null terminator
|
|
272
|
+
null_pos = data.index("\x00", pos)
|
|
273
|
+
break if null_pos.nil?
|
|
274
|
+
|
|
275
|
+
# Skip filename
|
|
276
|
+
pos = null_pos + 1
|
|
277
|
+
|
|
278
|
+
# Skip 4-byte offset
|
|
279
|
+
pos += 4
|
|
280
|
+
|
|
281
|
+
count += 1
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
count
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "zeck_lz77"
|
|
4
|
+
require_relative "btree_builder"
|
|
5
|
+
|
|
6
|
+
module Cabriolet
|
|
7
|
+
module HLP
|
|
8
|
+
module WinHelp
|
|
9
|
+
# Compressor creates Windows Help (.HLP) files
|
|
10
|
+
#
|
|
11
|
+
# Creates WinHelp 3.x and 4.x format files with Zeck LZ77 compression.
|
|
12
|
+
# Supports creating |SYSTEM, |TOPIC, and other internal files.
|
|
13
|
+
class Compressor
|
|
14
|
+
attr_reader :io_system
|
|
15
|
+
|
|
16
|
+
# Default block size for WinHelp files (4096 bytes)
|
|
17
|
+
BLOCK_SIZE = 4096
|
|
18
|
+
|
|
19
|
+
# Initialize compressor
|
|
20
|
+
#
|
|
21
|
+
# @param io_system [System::IOSystem, nil] Custom I/O system
|
|
22
|
+
def initialize(io_system = nil)
|
|
23
|
+
@io_system = io_system || System::IOSystem.new
|
|
24
|
+
@internal_files = {}
|
|
25
|
+
@version = :winhelp3
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Add an internal file to the WinHelp archive
|
|
29
|
+
#
|
|
30
|
+
# @param name [String] Internal filename (e.g., "|SYSTEM", "|TOPIC")
|
|
31
|
+
# @param data [String] File data
|
|
32
|
+
# @return [void]
|
|
33
|
+
def add_internal_file(name, data)
|
|
34
|
+
@internal_files[name] = data
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Add |SYSTEM file with metadata
|
|
38
|
+
#
|
|
39
|
+
# @param options [Hash] System file options
|
|
40
|
+
# @option options [String] :title Help file title
|
|
41
|
+
# @option options [String] :copyright Copyright text
|
|
42
|
+
# @option options [String] :contents Contents file path
|
|
43
|
+
# @return [void]
|
|
44
|
+
def add_system_file(**options)
|
|
45
|
+
system_data = build_system_file(options)
|
|
46
|
+
add_internal_file("|SYSTEM", system_data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Add |TOPIC file with compressed topics
|
|
50
|
+
#
|
|
51
|
+
# @param topics [Array<String>] Array of topic texts
|
|
52
|
+
# @param compress [Boolean] Whether to compress topics
|
|
53
|
+
# @return [void]
|
|
54
|
+
def add_topic_file(topics, compress: true)
|
|
55
|
+
topic_data = build_topic_file(topics, compress)
|
|
56
|
+
add_internal_file("|TOPIC", topic_data)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generate WinHelp file
|
|
60
|
+
#
|
|
61
|
+
# @param output_file [String] Path to output file
|
|
62
|
+
# @param options [Hash] Generation options
|
|
63
|
+
# @option options [Symbol] :version Format version (:winhelp3 or :winhelp4)
|
|
64
|
+
# @return [Integer] Bytes written
|
|
65
|
+
def generate(output_file, **options)
|
|
66
|
+
@version = options.fetch(:version, :winhelp3)
|
|
67
|
+
|
|
68
|
+
if @internal_files.empty?
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"No internal files added"
|
|
71
|
+
end
|
|
72
|
+
raise ArgumentError, "Invalid version" unless %i[winhelp3
|
|
73
|
+
winhelp4].include?(@version)
|
|
74
|
+
|
|
75
|
+
# Build structure
|
|
76
|
+
structure = build_structure
|
|
77
|
+
|
|
78
|
+
# Write to file
|
|
79
|
+
output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
|
|
80
|
+
begin
|
|
81
|
+
write_winhelp_file(output_handle, structure)
|
|
82
|
+
ensure
|
|
83
|
+
@io_system.close(output_handle)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Build complete WinHelp structure
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] Complete structure
|
|
92
|
+
def build_structure
|
|
93
|
+
structure = {
|
|
94
|
+
version: @version,
|
|
95
|
+
internal_files: [],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Prepare internal files with block numbers
|
|
99
|
+
block_number = 1 # Block 0 is reserved for header
|
|
100
|
+
@internal_files.each do |name, data|
|
|
101
|
+
# Calculate blocks needed (round up)
|
|
102
|
+
blocks_needed = (data.bytesize.to_f / BLOCK_SIZE).ceil
|
|
103
|
+
|
|
104
|
+
structure[:internal_files] << {
|
|
105
|
+
name: name,
|
|
106
|
+
data: data,
|
|
107
|
+
size: data.bytesize,
|
|
108
|
+
starting_block: block_number,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
block_number += blocks_needed
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Calculate directory offset
|
|
115
|
+
header_size = @version == :winhelp3 ? 28 : 32
|
|
116
|
+
structure[:directory_offset] = header_size
|
|
117
|
+
|
|
118
|
+
# Calculate directory size
|
|
119
|
+
dir_size = calculate_directory_size(structure[:internal_files])
|
|
120
|
+
structure[:directory_size] = dir_size
|
|
121
|
+
|
|
122
|
+
# Calculate total file size
|
|
123
|
+
structure[:file_size] =
|
|
124
|
+
header_size + dir_size + (block_number * BLOCK_SIZE)
|
|
125
|
+
|
|
126
|
+
structure
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Calculate directory size
|
|
130
|
+
#
|
|
131
|
+
# @param files [Array<Hash>] Internal file list
|
|
132
|
+
# @return [Integer] Directory size in bytes
|
|
133
|
+
def calculate_directory_size(files)
|
|
134
|
+
size = 0
|
|
135
|
+
files.each do |file|
|
|
136
|
+
# 4 bytes size + 2 bytes block + filename + null + padding
|
|
137
|
+
size += 4 + 2 + file[:name].bytesize + 1
|
|
138
|
+
# Align to 2-byte boundary
|
|
139
|
+
size += 1 if size.odd?
|
|
140
|
+
end
|
|
141
|
+
# Add end marker (4 bytes of zeros)
|
|
142
|
+
size + 4
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Write complete WinHelp file
|
|
146
|
+
#
|
|
147
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
148
|
+
# @param structure [Hash] File structure
|
|
149
|
+
# @return [Integer] Bytes written
|
|
150
|
+
def write_winhelp_file(output_handle, structure)
|
|
151
|
+
bytes_written = 0
|
|
152
|
+
|
|
153
|
+
# Write header
|
|
154
|
+
bytes_written += write_header(output_handle, structure)
|
|
155
|
+
|
|
156
|
+
# Write directory
|
|
157
|
+
bytes_written += write_directory(output_handle, structure)
|
|
158
|
+
|
|
159
|
+
# Pad to first block boundary
|
|
160
|
+
padding_needed = BLOCK_SIZE - (bytes_written % BLOCK_SIZE)
|
|
161
|
+
if padding_needed < BLOCK_SIZE
|
|
162
|
+
bytes_written += @io_system.write(output_handle,
|
|
163
|
+
"\x00" * padding_needed)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Write file data at block boundaries
|
|
167
|
+
structure[:internal_files].each do |file|
|
|
168
|
+
# Seek to correct block
|
|
169
|
+
target_offset = file[:starting_block] * BLOCK_SIZE
|
|
170
|
+
current_offset = bytes_written
|
|
171
|
+
|
|
172
|
+
if target_offset > current_offset
|
|
173
|
+
padding = "\x00" * (target_offset - current_offset)
|
|
174
|
+
bytes_written += @io_system.write(output_handle, padding)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Write file data
|
|
178
|
+
bytes_written += @io_system.write(output_handle, file[:data])
|
|
179
|
+
|
|
180
|
+
# Pad to block boundary
|
|
181
|
+
remainder = file[:data].bytesize % BLOCK_SIZE
|
|
182
|
+
if remainder.positive?
|
|
183
|
+
padding = "\x00" * (BLOCK_SIZE - remainder)
|
|
184
|
+
bytes_written += @io_system.write(output_handle, padding)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
bytes_written
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Write file header
|
|
192
|
+
#
|
|
193
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
194
|
+
# @param structure [Hash] File structure
|
|
195
|
+
# @return [Integer] Bytes written
|
|
196
|
+
def write_header(output_handle, structure)
|
|
197
|
+
if structure[:version] == :winhelp3
|
|
198
|
+
write_header_3x(output_handle, structure)
|
|
199
|
+
else
|
|
200
|
+
write_header_4x(output_handle, structure)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Write WinHelp 3.x header
|
|
205
|
+
#
|
|
206
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
207
|
+
# @param structure [Hash] File structure
|
|
208
|
+
# @return [Integer] Bytes written
|
|
209
|
+
def write_header_3x(output_handle, structure)
|
|
210
|
+
header = Binary::HLPStructures::WinHelp3Header.new
|
|
211
|
+
header.magic = 0x35F3
|
|
212
|
+
header.unknown = 0x0001
|
|
213
|
+
header.directory_offset = structure[:directory_offset]
|
|
214
|
+
header.free_list_offset = 0
|
|
215
|
+
header.file_size = structure[:file_size]
|
|
216
|
+
header.reserved = "\x00" * 12
|
|
217
|
+
|
|
218
|
+
header_data = header.to_binary_s
|
|
219
|
+
@io_system.write(output_handle, header_data)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Write WinHelp 4.x header
|
|
223
|
+
#
|
|
224
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
225
|
+
# @param structure [Hash] File structure
|
|
226
|
+
# @return [Integer] Bytes written
|
|
227
|
+
def write_header_4x(output_handle, structure)
|
|
228
|
+
header = Binary::HLPStructures::WinHelp4Header.new
|
|
229
|
+
header.magic = 0x00033F5F # Magic with low 16 bits = 0x3F5F
|
|
230
|
+
header.directory_offset = structure[:directory_offset]
|
|
231
|
+
header.free_list_offset = 0
|
|
232
|
+
header.file_size = structure[:file_size]
|
|
233
|
+
header.reserved = "\x00" * 16
|
|
234
|
+
|
|
235
|
+
header_data = header.to_binary_s
|
|
236
|
+
@io_system.write(output_handle, header_data)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Write directory
|
|
240
|
+
#
|
|
241
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
242
|
+
# @param structure [Hash] File structure
|
|
243
|
+
# @return [Integer] Bytes written
|
|
244
|
+
def write_directory(output_handle, structure)
|
|
245
|
+
if structure[:version] == :winhelp4
|
|
246
|
+
write_directory_btree(output_handle, structure)
|
|
247
|
+
else
|
|
248
|
+
write_directory_simple(output_handle, structure)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Write simple directory (WinHelp 3.x format)
|
|
253
|
+
#
|
|
254
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
255
|
+
# @param structure [Hash] File structure
|
|
256
|
+
# @return [Integer] Bytes written
|
|
257
|
+
def write_directory_simple(output_handle, structure)
|
|
258
|
+
bytes_written = 0
|
|
259
|
+
|
|
260
|
+
structure[:internal_files].each do |file|
|
|
261
|
+
# Write file size (4 bytes)
|
|
262
|
+
bytes_written += @io_system.write(output_handle,
|
|
263
|
+
[file[:size]].pack("V"))
|
|
264
|
+
|
|
265
|
+
# Write starting block (2 bytes)
|
|
266
|
+
bytes_written += @io_system.write(output_handle,
|
|
267
|
+
[file[:starting_block]].pack("v"))
|
|
268
|
+
|
|
269
|
+
# Write filename with null terminator
|
|
270
|
+
bytes_written += @io_system.write(output_handle,
|
|
271
|
+
"#{file[:name]}\u0000")
|
|
272
|
+
|
|
273
|
+
# Align to 2-byte boundary
|
|
274
|
+
if bytes_written.odd?
|
|
275
|
+
bytes_written += @io_system.write(output_handle, "\x00")
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Write end marker
|
|
280
|
+
bytes_written += @io_system.write(output_handle, [0].pack("V"))
|
|
281
|
+
|
|
282
|
+
bytes_written
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Write B+ tree directory (WinHelp 4.x format)
|
|
286
|
+
#
|
|
287
|
+
# @param output_handle [System::FileHandle] Output handle
|
|
288
|
+
# @param structure [Hash] File structure
|
|
289
|
+
# @return [Integer] Bytes written
|
|
290
|
+
def write_directory_btree(output_handle, structure)
|
|
291
|
+
bytes_written = 0
|
|
292
|
+
|
|
293
|
+
# Build B+ tree from internal files
|
|
294
|
+
btree = BTreeBuilder.new
|
|
295
|
+
structure[:internal_files].each do |file|
|
|
296
|
+
# Add entry with filename, starting block (offset), and size
|
|
297
|
+
btree.add_entry(file[:name], file[:starting_block] * BLOCK_SIZE,
|
|
298
|
+
file[:size])
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Build the tree
|
|
302
|
+
tree = btree.build
|
|
303
|
+
|
|
304
|
+
# Write FILEHEADER (9 bytes) before BTREEHEADER
|
|
305
|
+
# FILEHEADER structure:
|
|
306
|
+
# - 4 bytes: reserved_space (reserved space in help file incl. FILEHEADER)
|
|
307
|
+
# - 4 bytes: used_space (used space in help file excl. FILEHEADER)
|
|
308
|
+
# - 1 byte: file_flags (normally 4)
|
|
309
|
+
# For directory, we set these to 0 for now
|
|
310
|
+
file_header = Binary::HLPStructures::WinHelpFileHeader.new
|
|
311
|
+
file_header.reserved_space = 0
|
|
312
|
+
file_header.used_space = 0
|
|
313
|
+
file_header.file_flags = 4
|
|
314
|
+
file_header_data = file_header.to_binary_s
|
|
315
|
+
bytes_written += @io_system.write(output_handle, file_header_data)
|
|
316
|
+
|
|
317
|
+
# Write BTREEHEADER (38 bytes)
|
|
318
|
+
header = tree[:header]
|
|
319
|
+
header_data = header.to_binary_s
|
|
320
|
+
bytes_written += @io_system.write(output_handle, header_data)
|
|
321
|
+
|
|
322
|
+
# Write pages (sorted by page_num)
|
|
323
|
+
sorted_pages = tree[:pages].sort_by { |p| p[:page_num] }
|
|
324
|
+
sorted_pages.each do |page|
|
|
325
|
+
# Write page data
|
|
326
|
+
bytes_written += @io_system.write(output_handle, page[:data])
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
bytes_written
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Build |SYSTEM file
|
|
333
|
+
#
|
|
334
|
+
# @param options [Hash] System file options
|
|
335
|
+
# @return [String] System file data
|
|
336
|
+
def build_system_file(options)
|
|
337
|
+
data = +""
|
|
338
|
+
|
|
339
|
+
# Write title if provided
|
|
340
|
+
if options[:title]
|
|
341
|
+
data << build_system_record(1, options[:title])
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Write copyright if provided
|
|
345
|
+
if options[:copyright]
|
|
346
|
+
data << build_system_record(2, options[:copyright])
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Write contents if provided
|
|
350
|
+
if options[:contents]
|
|
351
|
+
data << build_system_record(3, options[:contents])
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
data
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Build a system record
|
|
358
|
+
#
|
|
359
|
+
# @param type [Integer] Record type
|
|
360
|
+
# @param text [String] Record text
|
|
361
|
+
# @return [String] Record data
|
|
362
|
+
def build_system_record(type, text)
|
|
363
|
+
record = +""
|
|
364
|
+
record << [type].pack("v") # Record type (2 bytes)
|
|
365
|
+
record << [text.bytesize + 1].pack("v") # Length including null (2 bytes)
|
|
366
|
+
record << text
|
|
367
|
+
record << "\x00" # Null terminator
|
|
368
|
+
record
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Build |TOPIC file
|
|
372
|
+
#
|
|
373
|
+
# @param topics [Array<String>] Topic texts
|
|
374
|
+
# @param compress [Boolean] Whether to compress
|
|
375
|
+
# @return [String] Topic file data
|
|
376
|
+
def build_topic_file(topics, compress)
|
|
377
|
+
# Simplified: just concatenate topic data
|
|
378
|
+
# Full implementation would include topic headers and blocks
|
|
379
|
+
data = +""
|
|
380
|
+
zeck = ZeckLZ77.new
|
|
381
|
+
|
|
382
|
+
topics.each do |topic_text|
|
|
383
|
+
compressed_data = if compress
|
|
384
|
+
# Compress using Zeck LZ77
|
|
385
|
+
zeck.compress(topic_text)
|
|
386
|
+
else
|
|
387
|
+
topic_text
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Write topic with 2-byte length header
|
|
391
|
+
data << [compressed_data.bytesize].pack("v")
|
|
392
|
+
data << compressed_data
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
data
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|