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
|
@@ -17,8 +17,10 @@ module Cabriolet
|
|
|
17
17
|
#
|
|
18
18
|
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
19
19
|
# default
|
|
20
|
-
|
|
20
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
21
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
21
22
|
@io_system = io_system || System::IOSystem.new
|
|
23
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
22
24
|
@files = []
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -45,52 +47,640 @@ module Cabriolet
|
|
|
45
47
|
# @param output_file [String] Path to output LIT file
|
|
46
48
|
# @param options [Hash] Generation options
|
|
47
49
|
# @option options [Integer] :version LIT format version (default: 1)
|
|
48
|
-
# @option options [
|
|
49
|
-
#
|
|
50
|
+
# @option options [Integer] :language_id Language ID (default: 0x409 English)
|
|
51
|
+
# @option options [Integer] :creator_id Creator ID (default: 0)
|
|
50
52
|
# @return [Integer] Bytes written to output file
|
|
51
|
-
# @raise [Errors::CompressionError] if
|
|
52
|
-
# @raise [NotImplementedError] if encryption is requested
|
|
53
|
+
# @raise [Errors::CompressionError] if compression fails
|
|
53
54
|
def generate(output_file, **options)
|
|
54
55
|
version = options.fetch(:version, 1)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if encrypt
|
|
58
|
-
raise NotImplementedError,
|
|
59
|
-
"DES encryption is not implemented. " \
|
|
60
|
-
"LIT files will be created without encryption."
|
|
61
|
-
end
|
|
56
|
+
language_id = options.fetch(:language_id, 0x409) # English
|
|
57
|
+
creator_id = options.fetch(:creator_id, 0)
|
|
62
58
|
|
|
63
59
|
raise ArgumentError, "No files added to archive" if @files.empty?
|
|
60
|
+
raise ArgumentError, "Version must be 1" unless version == 1
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
# Prepare file data
|
|
63
|
+
file_data = prepare_files
|
|
66
64
|
|
|
65
|
+
# Build LIT structure
|
|
66
|
+
lit_structure = build_lit_structure(file_data, version, language_id,
|
|
67
|
+
creator_id)
|
|
68
|
+
|
|
69
|
+
# Write to output file
|
|
70
|
+
output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
|
|
67
71
|
begin
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
bytes_written = write_lit_file(output_handle, lit_structure)
|
|
73
|
+
bytes_written
|
|
74
|
+
ensure
|
|
75
|
+
@io_system.close(output_handle)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
header_bytes = write_header(
|
|
73
|
-
output_handle,
|
|
74
|
-
version,
|
|
75
|
-
file_data.size,
|
|
76
|
-
)
|
|
79
|
+
private
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
# Build complete LIT structure
|
|
82
|
+
def build_lit_structure(file_data, version, language_id, creator_id)
|
|
83
|
+
structure = {}
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
85
|
+
# Generate GUIDs
|
|
86
|
+
structure[:header_guid] = generate_guid
|
|
87
|
+
structure[:piece3_guid] = Binary::LITStructures::GUIDs::PIECE3
|
|
88
|
+
structure[:piece4_guid] = Binary::LITStructures::GUIDs::PIECE4
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
# Build directory
|
|
91
|
+
structure[:directory] = build_directory(file_data)
|
|
92
|
+
|
|
93
|
+
# Build sections
|
|
94
|
+
structure[:sections] = build_sections(file_data)
|
|
95
|
+
|
|
96
|
+
# Build manifest
|
|
97
|
+
structure[:manifest] = build_manifest(file_data)
|
|
98
|
+
|
|
99
|
+
# Build secondary header first (needed for piece calculation)
|
|
100
|
+
structure[:secondary_header] = build_secondary_header_metadata(
|
|
101
|
+
language_id,
|
|
102
|
+
creator_id,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Calculate piece offsets and sizes (uses secondary header length)
|
|
106
|
+
structure[:pieces] = calculate_pieces(structure)
|
|
107
|
+
|
|
108
|
+
# Update secondary header with content offset
|
|
109
|
+
update_secondary_header_content_offset(structure)
|
|
110
|
+
|
|
111
|
+
# Store metadata
|
|
112
|
+
structure[:version] = version
|
|
113
|
+
structure[:file_data] = file_data
|
|
114
|
+
|
|
115
|
+
structure
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Write complete LIT file
|
|
119
|
+
def write_lit_file(output_handle, structure)
|
|
120
|
+
# Write primary header (40 bytes)
|
|
121
|
+
bytes_written = write_primary_header(output_handle, structure)
|
|
122
|
+
|
|
123
|
+
# Write piece structures (5 * 16 bytes = 80 bytes)
|
|
124
|
+
bytes_written += write_piece_structures(output_handle,
|
|
125
|
+
structure[:pieces])
|
|
126
|
+
|
|
127
|
+
# Write secondary header
|
|
128
|
+
bytes_written += write_secondary_header_block(
|
|
129
|
+
output_handle,
|
|
130
|
+
structure[:secondary_header],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Write piece data
|
|
134
|
+
bytes_written += write_piece_data(output_handle, structure)
|
|
135
|
+
|
|
136
|
+
bytes_written
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Generate a random GUID
|
|
140
|
+
def generate_guid
|
|
141
|
+
require "securerandom"
|
|
142
|
+
SecureRandom.random_bytes(16)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Write primary header
|
|
146
|
+
def write_primary_header(output_handle, structure)
|
|
147
|
+
header = Binary::LITStructures::PrimaryHeader.new
|
|
148
|
+
header.signature = Binary::LITStructures::SIGNATURE
|
|
149
|
+
header.version = structure[:version]
|
|
150
|
+
header.header_length = 40
|
|
151
|
+
header.num_pieces = 5
|
|
152
|
+
header.secondary_header_length = structure[:secondary_header][:length]
|
|
153
|
+
header.header_guid = structure[:header_guid]
|
|
154
|
+
|
|
155
|
+
header_data = header.to_binary_s
|
|
156
|
+
@io_system.write(output_handle, header_data)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Write piece structures
|
|
160
|
+
def write_piece_structures(output_handle, pieces)
|
|
161
|
+
total_bytes = 0
|
|
162
|
+
|
|
163
|
+
pieces.each do |piece|
|
|
164
|
+
piece_struct = Binary::LITStructures::PieceStructure.new
|
|
165
|
+
piece_struct.offset_low = piece[:offset]
|
|
166
|
+
piece_struct.offset_high = 0
|
|
167
|
+
piece_struct.size_low = piece[:size]
|
|
168
|
+
piece_struct.size_high = 0
|
|
169
|
+
|
|
170
|
+
piece_data = piece_struct.to_binary_s
|
|
171
|
+
total_bytes += @io_system.write(output_handle, piece_data)
|
|
90
172
|
end
|
|
173
|
+
|
|
174
|
+
total_bytes
|
|
91
175
|
end
|
|
92
176
|
|
|
93
|
-
|
|
177
|
+
# Write secondary header block
|
|
178
|
+
def write_secondary_header_block(output_handle, sec_hdr)
|
|
179
|
+
# Build secondary header using Binary::LITStructures::SecondaryHeader
|
|
180
|
+
header = Binary::LITStructures::SecondaryHeader.new
|
|
181
|
+
|
|
182
|
+
# SECHDR block
|
|
183
|
+
header.sechdr_version = 2
|
|
184
|
+
header.sechdr_length = 152
|
|
185
|
+
|
|
186
|
+
# Entry directory info
|
|
187
|
+
header.entry_aoli_idx = 0
|
|
188
|
+
header.entry_aoli_idx_high = 0
|
|
189
|
+
header.entry_reserved1 = 0
|
|
190
|
+
header.entry_last_aoll = 0
|
|
191
|
+
header.entry_reserved2 = 0
|
|
192
|
+
header.entry_chunklen = sec_hdr[:entry_chunklen]
|
|
193
|
+
header.entry_two = 2
|
|
194
|
+
header.entry_reserved3 = 0
|
|
195
|
+
header.entry_depth = sec_hdr[:entry_depth]
|
|
196
|
+
header.entry_reserved4 = 0
|
|
197
|
+
header.entry_entries = sec_hdr[:entry_entries]
|
|
198
|
+
header.entry_reserved5 = 0
|
|
199
|
+
|
|
200
|
+
# Count directory info
|
|
201
|
+
header.count_aoli_idx = 0xFFFFFFFF
|
|
202
|
+
header.count_aoli_idx_high = 0xFFFFFFFF
|
|
203
|
+
header.count_reserved1 = 0
|
|
204
|
+
header.count_last_aoll = 0
|
|
205
|
+
header.count_reserved2 = 0
|
|
206
|
+
header.count_chunklen = sec_hdr[:count_chunklen]
|
|
207
|
+
header.count_two = 2
|
|
208
|
+
header.count_reserved3 = 0
|
|
209
|
+
header.count_depth = 1
|
|
210
|
+
header.count_reserved4 = 0
|
|
211
|
+
header.count_entries = sec_hdr[:count_entries]
|
|
212
|
+
header.count_reserved5 = 0
|
|
213
|
+
|
|
214
|
+
header.entry_unknown = sec_hdr[:entry_unknown]
|
|
215
|
+
header.count_unknown = sec_hdr[:count_unknown]
|
|
216
|
+
|
|
217
|
+
# CAOL block
|
|
218
|
+
header.caol_tag = Binary::LITStructures::Tags::CAOL
|
|
219
|
+
header.caol_version = 2
|
|
220
|
+
header.caol_length = 80 # 48 + 32
|
|
221
|
+
header.creator_id = sec_hdr[:creator_id]
|
|
222
|
+
header.caol_reserved1 = 0
|
|
223
|
+
header.caol_entry_chunklen = sec_hdr[:entry_chunklen]
|
|
224
|
+
header.caol_count_chunklen = sec_hdr[:count_chunklen]
|
|
225
|
+
header.caol_entry_unknown = sec_hdr[:entry_unknown]
|
|
226
|
+
header.caol_count_unknown = sec_hdr[:count_unknown]
|
|
227
|
+
header.caol_reserved2 = 0
|
|
228
|
+
|
|
229
|
+
# ITSF block
|
|
230
|
+
header.itsf_tag = Binary::LITStructures::Tags::ITSF
|
|
231
|
+
header.itsf_version = 4
|
|
232
|
+
header.itsf_length = 32
|
|
233
|
+
header.itsf_unknown = 1
|
|
234
|
+
header.content_offset_low = sec_hdr[:content_offset]
|
|
235
|
+
header.content_offset_high = 0
|
|
236
|
+
header.timestamp = sec_hdr[:timestamp]
|
|
237
|
+
header.language_id = sec_hdr[:language_id]
|
|
238
|
+
|
|
239
|
+
header_data = header.to_binary_s
|
|
240
|
+
@io_system.write(output_handle, header_data)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Build directory structure
|
|
244
|
+
def build_directory(file_data)
|
|
245
|
+
# Create directory entries for all files
|
|
246
|
+
entries = []
|
|
247
|
+
section = 0 # All files go in section 0 for now (uncompressed)
|
|
248
|
+
offset = 0
|
|
249
|
+
|
|
250
|
+
file_data.each do |file_info|
|
|
251
|
+
entry = {
|
|
252
|
+
name: file_info[:lit_path],
|
|
253
|
+
section: section,
|
|
254
|
+
offset: offset,
|
|
255
|
+
size: file_info[:uncompressed_size],
|
|
256
|
+
}
|
|
257
|
+
entries << entry
|
|
258
|
+
offset += file_info[:uncompressed_size]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Calculate NameList size
|
|
262
|
+
namelist_size = calculate_namelist_size(file_data)
|
|
263
|
+
|
|
264
|
+
# Calculate manifest size
|
|
265
|
+
manifest_size = calculate_manifest_size(file_data)
|
|
266
|
+
|
|
267
|
+
# Add special entries for LIT structure
|
|
268
|
+
# ::DataSpace/NameList entry
|
|
269
|
+
entries << {
|
|
270
|
+
name: Binary::LITStructures::Paths::NAMELIST,
|
|
271
|
+
section: 0,
|
|
272
|
+
offset: offset,
|
|
273
|
+
size: namelist_size,
|
|
274
|
+
}
|
|
275
|
+
offset += namelist_size
|
|
276
|
+
|
|
277
|
+
# Add manifest entry
|
|
278
|
+
entries << {
|
|
279
|
+
name: Binary::LITStructures::Paths::MANIFEST,
|
|
280
|
+
section: 0,
|
|
281
|
+
offset: offset,
|
|
282
|
+
size: manifest_size,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
{
|
|
286
|
+
entries: entries,
|
|
287
|
+
chunk_size: 0x2000, # 8KB chunks
|
|
288
|
+
num_chunks: 1, # Simple single-chunk directory for now
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Calculate NameList size (estimate)
|
|
293
|
+
def calculate_namelist_size(_file_data)
|
|
294
|
+
# Simple estimate: ~100 bytes for minimal NameList
|
|
295
|
+
100
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Calculate manifest size (estimate)
|
|
299
|
+
def calculate_manifest_size(file_data)
|
|
300
|
+
# Rough estimate: directory header + entries
|
|
301
|
+
size = 10 # Directory header
|
|
302
|
+
|
|
303
|
+
file_data.each do |file_info|
|
|
304
|
+
# Per entry: offset (4) + 3 length bytes + names + content type + terminator
|
|
305
|
+
size += 4 + 3
|
|
306
|
+
size += (file_info[:lit_path].bytesize * 2) + 20 + 1
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
size
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Build sections
|
|
313
|
+
def build_sections(_file_data)
|
|
314
|
+
# For simple implementation: single uncompressed section
|
|
315
|
+
# Advanced: could have multiple sections with different compression
|
|
316
|
+
sections = []
|
|
317
|
+
|
|
318
|
+
# Section 0 is always uncompressed content
|
|
319
|
+
section = {
|
|
320
|
+
name: "Uncompressed",
|
|
321
|
+
transforms: [],
|
|
322
|
+
compressed: false,
|
|
323
|
+
encrypted: false,
|
|
324
|
+
}
|
|
325
|
+
sections << section
|
|
326
|
+
|
|
327
|
+
sections
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Build manifest
|
|
331
|
+
def build_manifest(file_data)
|
|
332
|
+
mappings = []
|
|
333
|
+
|
|
334
|
+
file_data.each_with_index do |file_info, index|
|
|
335
|
+
mapping = {
|
|
336
|
+
offset: index, # Simple sequential offset
|
|
337
|
+
internal_name: file_info[:lit_path],
|
|
338
|
+
original_name: file_info[:lit_path],
|
|
339
|
+
content_type: guess_content_type(file_info[:lit_path]),
|
|
340
|
+
group: guess_file_group(file_info[:lit_path]),
|
|
341
|
+
}
|
|
342
|
+
mappings << mapping
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
{
|
|
346
|
+
mappings: mappings,
|
|
347
|
+
}
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Guess content type from filename
|
|
351
|
+
def guess_content_type(filename)
|
|
352
|
+
ext = File.extname(filename).downcase
|
|
353
|
+
case ext
|
|
354
|
+
when ".html", ".htm"
|
|
355
|
+
"text/html"
|
|
356
|
+
when ".css"
|
|
357
|
+
"text/css"
|
|
358
|
+
when ".jpg", ".jpeg"
|
|
359
|
+
"image/jpeg"
|
|
360
|
+
when ".png"
|
|
361
|
+
"image/png"
|
|
362
|
+
when ".gif"
|
|
363
|
+
"image/gif"
|
|
364
|
+
when ".txt"
|
|
365
|
+
"text/plain"
|
|
366
|
+
else
|
|
367
|
+
"application/octet-stream"
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Guess file group (0=HTML spine, 1=HTML other, 2=CSS, 3=Images)
|
|
372
|
+
def guess_file_group(filename)
|
|
373
|
+
ext = File.extname(filename).downcase
|
|
374
|
+
case ext
|
|
375
|
+
when ".html", ".htm"
|
|
376
|
+
0 # HTML spine (simplification - could be group 1 for non-spine)
|
|
377
|
+
when ".css"
|
|
378
|
+
2 # CSS
|
|
379
|
+
when ".jpg", ".jpeg", ".png", ".gif"
|
|
380
|
+
3 # Images
|
|
381
|
+
else
|
|
382
|
+
1 # Other
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Calculate piece offsets and sizes
|
|
387
|
+
def calculate_pieces(structure)
|
|
388
|
+
pieces = []
|
|
389
|
+
|
|
390
|
+
# Calculate starting offset (after headers and pieces)
|
|
391
|
+
# Primary header: 40 bytes
|
|
392
|
+
# Piece structures: 5 * 16 = 80 bytes
|
|
393
|
+
# Secondary header: variable
|
|
394
|
+
sec_hdr_length = structure[:secondary_header][:length]
|
|
395
|
+
current_offset = 40 + 80 + sec_hdr_length
|
|
396
|
+
|
|
397
|
+
# Piece 0: File size information (small, typically ~16 bytes)
|
|
398
|
+
piece0_size = 16
|
|
399
|
+
pieces << { offset: current_offset, size: piece0_size }
|
|
400
|
+
current_offset += piece0_size
|
|
401
|
+
|
|
402
|
+
# Piece 1: Directory (IFCM structure)
|
|
403
|
+
# For foundation: minimal size
|
|
404
|
+
piece1_size = 8192 # Typical directory size
|
|
405
|
+
pieces << { offset: current_offset, size: piece1_size }
|
|
406
|
+
current_offset += piece1_size
|
|
407
|
+
|
|
408
|
+
# Piece 2: Index information (typically empty or minimal)
|
|
409
|
+
piece2_size = 512
|
|
410
|
+
pieces << { offset: current_offset, size: piece2_size }
|
|
411
|
+
current_offset += piece2_size
|
|
412
|
+
|
|
413
|
+
# Piece 3: Standard GUID (fixed 16 bytes)
|
|
414
|
+
pieces << { offset: current_offset, size: 16 }
|
|
415
|
+
current_offset += 16
|
|
416
|
+
|
|
417
|
+
# Piece 4: Standard GUID (fixed 16 bytes)
|
|
418
|
+
pieces << { offset: current_offset, size: 16 }
|
|
419
|
+
current_offset + 16
|
|
420
|
+
|
|
421
|
+
pieces
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Build secondary header structure (initial metadata)
|
|
425
|
+
def build_secondary_header_metadata(language_id, creator_id)
|
|
426
|
+
# Calculate actual secondary header length from BinData structure
|
|
427
|
+
temp_header = Binary::LITStructures::SecondaryHeader.new
|
|
428
|
+
sec_hdr_length = temp_header.to_binary_s.bytesize
|
|
429
|
+
|
|
430
|
+
{
|
|
431
|
+
length: sec_hdr_length,
|
|
432
|
+
entry_chunklen: 0x2000, # 8KB chunks for entry directory
|
|
433
|
+
count_chunklen: 0x200, # 512B chunks for count directory
|
|
434
|
+
entry_unknown: 0x100000,
|
|
435
|
+
count_unknown: 0x20000,
|
|
436
|
+
entry_depth: 1, # No AOLI index layer
|
|
437
|
+
entry_entries: 0, # Will be set when directory built
|
|
438
|
+
count_entries: 0, # Will be set when directory built
|
|
439
|
+
content_offset: 0, # Will be calculated after pieces
|
|
440
|
+
timestamp: Time.now.to_i,
|
|
441
|
+
language_id: language_id,
|
|
442
|
+
creator_id: creator_id,
|
|
443
|
+
}
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Update secondary header with final content offset
|
|
447
|
+
def update_secondary_header_content_offset(structure)
|
|
448
|
+
pieces = structure[:pieces]
|
|
449
|
+
last_piece = pieces.last
|
|
450
|
+
content_offset = last_piece[:offset] + last_piece[:size]
|
|
451
|
+
|
|
452
|
+
structure[:secondary_header][:content_offset] = content_offset
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Write piece data
|
|
456
|
+
def write_piece_data(output_handle, structure)
|
|
457
|
+
total_bytes = 0
|
|
458
|
+
|
|
459
|
+
# Write piece 0: File size information
|
|
460
|
+
piece0_data = build_piece0_data(structure)
|
|
461
|
+
total_bytes += @io_system.write(output_handle, piece0_data)
|
|
462
|
+
|
|
463
|
+
# Write piece 1: Directory (IFCM structure)
|
|
464
|
+
piece1_data = build_piece1_data(structure)
|
|
465
|
+
total_bytes += @io_system.write(output_handle, piece1_data)
|
|
466
|
+
|
|
467
|
+
# Write piece 2: Index information
|
|
468
|
+
piece2_data = build_piece2_data(structure)
|
|
469
|
+
total_bytes += @io_system.write(output_handle, piece2_data)
|
|
470
|
+
|
|
471
|
+
# Write piece 3: GUID
|
|
472
|
+
total_bytes += @io_system.write(output_handle, structure[:piece3_guid])
|
|
473
|
+
|
|
474
|
+
# Write piece 4: GUID
|
|
475
|
+
total_bytes += @io_system.write(output_handle, structure[:piece4_guid])
|
|
476
|
+
|
|
477
|
+
# Write actual content data (after pieces, this is where files go)
|
|
478
|
+
total_bytes += write_content_data(output_handle, structure)
|
|
479
|
+
|
|
480
|
+
total_bytes
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Write content data (actual file contents)
|
|
484
|
+
def write_content_data(output_handle, structure)
|
|
485
|
+
total_bytes = 0
|
|
486
|
+
|
|
487
|
+
# Write each file's content
|
|
488
|
+
structure[:file_data].each do |file_info|
|
|
489
|
+
total_bytes += @io_system.write(output_handle, file_info[:data])
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Write NameList
|
|
493
|
+
namelist_data = build_namelist_data(structure[:sections])
|
|
494
|
+
total_bytes += @io_system.write(output_handle, namelist_data)
|
|
495
|
+
|
|
496
|
+
# Write manifest
|
|
497
|
+
manifest_data = build_manifest_data(structure[:manifest])
|
|
498
|
+
total_bytes += @io_system.write(output_handle, manifest_data)
|
|
499
|
+
|
|
500
|
+
total_bytes
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Build NameList data
|
|
504
|
+
def build_namelist_data(sections)
|
|
505
|
+
data = +""
|
|
506
|
+
data += [0].pack("v") # Initial field
|
|
507
|
+
|
|
508
|
+
# Write number of sections
|
|
509
|
+
data += [sections.size].pack("v")
|
|
510
|
+
|
|
511
|
+
# Write each section name
|
|
512
|
+
null_terminator = [0].pack("v")
|
|
513
|
+
sections.each do |section|
|
|
514
|
+
name = section[:name]
|
|
515
|
+
# Convert to UTF-16LE
|
|
516
|
+
name_utf16 = name.encode("UTF-16LE").force_encoding("ASCII-8BIT")
|
|
517
|
+
name_length = name_utf16.bytesize / 2
|
|
518
|
+
|
|
519
|
+
data += [name_length].pack("v")
|
|
520
|
+
data += name_utf16
|
|
521
|
+
data += null_terminator
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
data
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Build manifest data
|
|
528
|
+
def build_manifest_data(manifest)
|
|
529
|
+
data = +""
|
|
530
|
+
|
|
531
|
+
# For simplicity: single directory entry
|
|
532
|
+
data += [0].pack("C") # Empty directory name = end of directories
|
|
533
|
+
|
|
534
|
+
# Write 4 groups
|
|
535
|
+
terminator = [0].pack("C")
|
|
536
|
+
4.times do |group|
|
|
537
|
+
# Get mappings for this group
|
|
538
|
+
group_mappings = manifest[:mappings].select { |m| m[:group] == group }
|
|
539
|
+
|
|
540
|
+
data += [group_mappings.size].pack("V")
|
|
541
|
+
|
|
542
|
+
group_mappings.each do |mapping|
|
|
543
|
+
data += [mapping[:offset]].pack("V")
|
|
544
|
+
|
|
545
|
+
# Internal name
|
|
546
|
+
data += [mapping[:internal_name].bytesize].pack("C")
|
|
547
|
+
data += mapping[:internal_name]
|
|
548
|
+
|
|
549
|
+
# Original name
|
|
550
|
+
data += [mapping[:original_name].bytesize].pack("C")
|
|
551
|
+
data += mapping[:original_name]
|
|
552
|
+
|
|
553
|
+
# Content type
|
|
554
|
+
data += [mapping[:content_type].bytesize].pack("C")
|
|
555
|
+
data += mapping[:content_type]
|
|
556
|
+
|
|
557
|
+
# Terminator
|
|
558
|
+
data += terminator
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
data
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Build piece 0 data (file size information)
|
|
566
|
+
def build_piece0_data(structure)
|
|
567
|
+
# Calculate total content size
|
|
568
|
+
content_size = 0
|
|
569
|
+
structure[:file_data].each do |file_info|
|
|
570
|
+
content_size += file_info[:uncompressed_size]
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
data = [Binary::LITStructures::Tags::SIZE_PIECE].pack("V")
|
|
574
|
+
data += [content_size].pack("V")
|
|
575
|
+
data += [0, 0].pack("VV") # High bits, reserved
|
|
576
|
+
data
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Build piece 1 data (directory IFCM structure)
|
|
580
|
+
def build_piece1_data(structure)
|
|
581
|
+
# Build IFCM header
|
|
582
|
+
ifcm = Binary::LITStructures::IFCMHeader.new
|
|
583
|
+
ifcm.tag = Binary::LITStructures::Tags::IFCM
|
|
584
|
+
ifcm.version = 1
|
|
585
|
+
ifcm.chunk_size = structure[:directory][:chunk_size]
|
|
586
|
+
ifcm.param = 0x100000
|
|
587
|
+
ifcm.reserved1 = 0xFFFFFFFF
|
|
588
|
+
ifcm.reserved2 = 0xFFFFFFFF
|
|
589
|
+
ifcm.num_chunks = structure[:directory][:num_chunks]
|
|
590
|
+
ifcm.reserved3 = 0
|
|
591
|
+
|
|
592
|
+
data = ifcm.to_binary_s
|
|
593
|
+
|
|
594
|
+
# Build AOLL chunk with directory entries
|
|
595
|
+
aoll_chunk = build_aoll_chunk(structure[:directory][:entries])
|
|
596
|
+
data += aoll_chunk
|
|
597
|
+
|
|
598
|
+
# Pad to fill piece (8KB standard)
|
|
599
|
+
target_size = 8192
|
|
600
|
+
if data.bytesize < target_size
|
|
601
|
+
data += "\x00" * (target_size - data.bytesize)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
data
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Build AOLL (Archive Object List List) chunk
|
|
608
|
+
def build_aoll_chunk(entries)
|
|
609
|
+
# First, build all entry data to know the size
|
|
610
|
+
entries_data = +""
|
|
611
|
+
entries.each do |entry|
|
|
612
|
+
entries_data += encode_directory_entry(entry)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Calculate quickref offset (starts after entries data)
|
|
616
|
+
quickref_offset = entries_data.bytesize
|
|
617
|
+
|
|
618
|
+
# AOLL header (48 bytes)
|
|
619
|
+
header = Binary::LITStructures::AOLLHeader.new
|
|
620
|
+
header.tag = Binary::LITStructures::Tags::AOLL
|
|
621
|
+
header.quickref_offset = quickref_offset
|
|
622
|
+
header.current_chunk_low = 0
|
|
623
|
+
header.current_chunk_high = 0
|
|
624
|
+
header.prev_chunk_low = 0xFFFFFFFF
|
|
625
|
+
header.prev_chunk_high = 0xFFFFFFFF
|
|
626
|
+
header.next_chunk_low = 0xFFFFFFFF
|
|
627
|
+
header.next_chunk_high = 0xFFFFFFFF
|
|
628
|
+
header.entries_so_far = entries.size
|
|
629
|
+
header.reserved = 0
|
|
630
|
+
header.chunk_distance = 0
|
|
631
|
+
header.reserved2 = 0
|
|
632
|
+
|
|
633
|
+
chunk_data = header.to_binary_s
|
|
634
|
+
|
|
635
|
+
# Write directory entries
|
|
636
|
+
chunk_data += entries_data
|
|
637
|
+
|
|
638
|
+
chunk_data
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Encode a directory entry with variable-length integers
|
|
642
|
+
def encode_directory_entry(entry)
|
|
643
|
+
data = +""
|
|
644
|
+
|
|
645
|
+
# Encode name length and name
|
|
646
|
+
name = entry[:name].dup.force_encoding("UTF-8")
|
|
647
|
+
data += write_encoded_int(name.bytesize)
|
|
648
|
+
data += name
|
|
649
|
+
|
|
650
|
+
# Encode section, offset, size
|
|
651
|
+
data += write_encoded_int(entry[:section])
|
|
652
|
+
data += write_encoded_int(entry[:offset])
|
|
653
|
+
data += write_encoded_int(entry[:size])
|
|
654
|
+
|
|
655
|
+
data
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Write an encoded integer (variable length, MSB = continuation bit)
|
|
659
|
+
def write_encoded_int(value)
|
|
660
|
+
return [0x00].pack("C") if value.zero?
|
|
661
|
+
|
|
662
|
+
bytes = []
|
|
663
|
+
|
|
664
|
+
# Extract 7-bit chunks from value
|
|
665
|
+
loop do
|
|
666
|
+
bytes.unshift(value & 0x7F)
|
|
667
|
+
value >>= 7
|
|
668
|
+
break if value.zero?
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Set MSB on all bytes except the last
|
|
672
|
+
(0...(bytes.size - 1)).each do |i|
|
|
673
|
+
bytes[i] |= 0x80
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
bytes.pack("C*")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Build piece 2 data (index information)
|
|
680
|
+
def build_piece2_data(_structure)
|
|
681
|
+
# Minimal index data for foundation
|
|
682
|
+
"\x00" * 512
|
|
683
|
+
end
|
|
94
684
|
|
|
95
685
|
# Prepare file data for archiving
|
|
96
686
|
#
|
|
@@ -126,13 +716,16 @@ module Cabriolet
|
|
|
126
716
|
# @param version [Integer] LIT format version
|
|
127
717
|
# @param file_count [Integer] Number of files
|
|
128
718
|
# @return [Integer] Number of bytes written
|
|
129
|
-
def write_header(output_handle, version,
|
|
130
|
-
header
|
|
719
|
+
def write_header(output_handle, version, _file_count)
|
|
720
|
+
# NOTE: This is a simplified header format and does not match the actual LIT PrimaryHeader structure
|
|
721
|
+
# TODO: Implement proper LIT PrimaryHeader usage
|
|
722
|
+
header = Binary::LITStructures::PrimaryHeader.new
|
|
131
723
|
header.signature = Binary::LITStructures::SIGNATURE
|
|
132
724
|
header.version = version
|
|
133
|
-
header.
|
|
134
|
-
header.
|
|
135
|
-
header.
|
|
725
|
+
header.header_length = 40 # PrimaryHeader is 40 bytes
|
|
726
|
+
header.num_pieces = 5 # Standard for LIT files
|
|
727
|
+
header.secondary_header_length = 0 # Simplified for now
|
|
728
|
+
header.header_guid = "\x00" * 16 # Placeholder GUID
|
|
136
729
|
|
|
137
730
|
header_data = header.to_binary_s
|
|
138
731
|
written = @io_system.write(output_handle, header_data)
|
|
@@ -236,7 +829,9 @@ module Cabriolet
|
|
|
236
829
|
output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
237
830
|
|
|
238
831
|
begin
|
|
239
|
-
compressor =
|
|
832
|
+
compressor = @algorithm_factory.create(
|
|
833
|
+
Constants::COMP_TYPE_LZX,
|
|
834
|
+
:compressor,
|
|
240
835
|
@io_system,
|
|
241
836
|
input_handle,
|
|
242
837
|
output_handle,
|