cabriolet 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +3 -0
- data/lib/cabriolet/binary/bitstream.rb +32 -21
- data/lib/cabriolet/binary/bitstream_writer.rb +21 -4
- data/lib/cabriolet/cab/compressor.rb +85 -53
- data/lib/cabriolet/cab/decompressor.rb +2 -1
- data/lib/cabriolet/cab/extractor.rb +2 -35
- data/lib/cabriolet/cab/file_compression_work.rb +52 -0
- data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
- data/lib/cabriolet/checksum.rb +49 -0
- data/lib/cabriolet/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/quantum.rb +3 -51
- data/lib/cabriolet/decompressors/quantum.rb +81 -52
- data/lib/cabriolet/extraction/base_extractor.rb +88 -0
- data/lib/cabriolet/extraction/extractor.rb +171 -0
- data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
- data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
- data/lib/cabriolet/format_base.rb +79 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +28 -503
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
- data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
- data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
- data/lib/cabriolet/huffman/encoder.rb +15 -12
- data/lib/cabriolet/lit/compressor.rb +45 -689
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/directory_builder.rb +153 -0
- data/lib/cabriolet/lit/guid_generator.rb +16 -0
- data/lib/cabriolet/lit/header_writer.rb +124 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +114 -3
- metadata +38 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "guid_generator"
|
|
4
|
+
require_relative "content_type_detector"
|
|
5
|
+
require_relative "directory_builder"
|
|
6
|
+
require_relative "structure_builder"
|
|
7
|
+
require_relative "header_writer"
|
|
8
|
+
require_relative "piece_builder"
|
|
9
|
+
require_relative "content_encoder"
|
|
10
|
+
|
|
3
11
|
module Cabriolet
|
|
4
12
|
module LIT
|
|
5
13
|
# Compressor creates LIT eBook files
|
|
@@ -10,13 +18,11 @@ module Cabriolet
|
|
|
10
18
|
# NOTE: This implementation creates non-encrypted LIT files only.
|
|
11
19
|
# DES encryption (DRM protection) is not implemented.
|
|
12
20
|
class Compressor
|
|
13
|
-
attr_reader :io_system
|
|
14
|
-
attr_accessor :files
|
|
21
|
+
attr_reader :io_system, :files
|
|
15
22
|
|
|
16
23
|
# Initialize a new LIT compressor
|
|
17
24
|
#
|
|
18
|
-
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
19
|
-
# default
|
|
25
|
+
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
|
|
20
26
|
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
21
27
|
def initialize(io_system = nil, algorithm_factory = nil)
|
|
22
28
|
@io_system = io_system || System::IOSystem.new
|
|
@@ -29,8 +35,7 @@ module Cabriolet
|
|
|
29
35
|
# @param source_path [String] Path to the source file
|
|
30
36
|
# @param lit_path [String] Path within the LIT archive
|
|
31
37
|
# @param options [Hash] Options for the file
|
|
32
|
-
# @option options [Boolean] :compress Whether to compress the file
|
|
33
|
-
# (default: true)
|
|
38
|
+
# @option options [Boolean] :compress Whether to compress the file (default: true)
|
|
34
39
|
# @return [void]
|
|
35
40
|
def add_file(source_path, lit_path, **options)
|
|
36
41
|
compress = options.fetch(:compress, true)
|
|
@@ -53,7 +58,7 @@ module Cabriolet
|
|
|
53
58
|
# @raise [Errors::CompressionError] if compression fails
|
|
54
59
|
def generate(output_file, **options)
|
|
55
60
|
version = options.fetch(:version, 1)
|
|
56
|
-
language_id = options.fetch(:language_id, 0x409)
|
|
61
|
+
language_id = options.fetch(:language_id, 0x409)
|
|
57
62
|
creator_id = options.fetch(:creator_id, 0)
|
|
58
63
|
|
|
59
64
|
raise ArgumentError, "No files added to archive" if @files.empty?
|
|
@@ -63,8 +68,13 @@ module Cabriolet
|
|
|
63
68
|
file_data = prepare_files
|
|
64
69
|
|
|
65
70
|
# Build LIT structure
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
structure_builder = StructureBuilder.new(
|
|
72
|
+
io_system: @io_system,
|
|
73
|
+
version: version,
|
|
74
|
+
language_id: language_id,
|
|
75
|
+
creator_id: creator_id,
|
|
76
|
+
)
|
|
77
|
+
lit_structure = structure_builder.build(file_data)
|
|
68
78
|
|
|
69
79
|
# Write to output file
|
|
70
80
|
output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
|
|
@@ -78,54 +88,26 @@ module Cabriolet
|
|
|
78
88
|
|
|
79
89
|
private
|
|
80
90
|
|
|
81
|
-
# Build complete LIT structure
|
|
82
|
-
def build_lit_structure(file_data, version, language_id, creator_id)
|
|
83
|
-
structure = {}
|
|
84
|
-
|
|
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
|
|
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
91
|
# Write complete LIT file
|
|
92
|
+
#
|
|
93
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
94
|
+
# @param structure [Hash] LIT structure
|
|
95
|
+
# @return [Integer] Bytes written
|
|
119
96
|
def write_lit_file(output_handle, structure)
|
|
97
|
+
header_writer = HeaderWriter.new(@io_system)
|
|
98
|
+
|
|
99
|
+
bytes_written = 0
|
|
100
|
+
|
|
120
101
|
# Write primary header (40 bytes)
|
|
121
|
-
bytes_written
|
|
102
|
+
bytes_written += header_writer.write_primary_header(output_handle,
|
|
103
|
+
structure)
|
|
122
104
|
|
|
123
105
|
# Write piece structures (5 * 16 bytes = 80 bytes)
|
|
124
|
-
bytes_written += write_piece_structures(output_handle,
|
|
125
|
-
|
|
106
|
+
bytes_written += header_writer.write_piece_structures(output_handle,
|
|
107
|
+
structure[:pieces])
|
|
126
108
|
|
|
127
109
|
# Write secondary header
|
|
128
|
-
bytes_written +=
|
|
110
|
+
bytes_written += header_writer.write_secondary_header(
|
|
129
111
|
output_handle,
|
|
130
112
|
structure[:secondary_header],
|
|
131
113
|
)
|
|
@@ -136,336 +118,24 @@ module Cabriolet
|
|
|
136
118
|
bytes_written
|
|
137
119
|
end
|
|
138
120
|
|
|
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)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
total_bytes
|
|
175
|
-
end
|
|
176
|
-
|
|
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
121
|
# Write piece data
|
|
122
|
+
#
|
|
123
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
124
|
+
# @param structure [Hash] LIT structure
|
|
125
|
+
# @return [Integer] Bytes written
|
|
456
126
|
def write_piece_data(output_handle, structure)
|
|
457
127
|
total_bytes = 0
|
|
458
128
|
|
|
459
129
|
# Write piece 0: File size information
|
|
460
|
-
piece0_data =
|
|
130
|
+
piece0_data = PieceBuilder.build_piece0(structure[:file_data])
|
|
461
131
|
total_bytes += @io_system.write(output_handle, piece0_data)
|
|
462
132
|
|
|
463
133
|
# Write piece 1: Directory (IFCM structure)
|
|
464
|
-
piece1_data =
|
|
134
|
+
piece1_data = PieceBuilder.build_piece1(structure[:directory])
|
|
465
135
|
total_bytes += @io_system.write(output_handle, piece1_data)
|
|
466
136
|
|
|
467
137
|
# Write piece 2: Index information
|
|
468
|
-
piece2_data =
|
|
138
|
+
piece2_data = PieceBuilder.build_piece2
|
|
469
139
|
total_bytes += @io_system.write(output_handle, piece2_data)
|
|
470
140
|
|
|
471
141
|
# Write piece 3: GUID
|
|
@@ -481,6 +151,10 @@ module Cabriolet
|
|
|
481
151
|
end
|
|
482
152
|
|
|
483
153
|
# Write content data (actual file contents)
|
|
154
|
+
#
|
|
155
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
156
|
+
# @param structure [Hash] LIT structure
|
|
157
|
+
# @return [Integer] Bytes written
|
|
484
158
|
def write_content_data(output_handle, structure)
|
|
485
159
|
total_bytes = 0
|
|
486
160
|
|
|
@@ -490,198 +164,16 @@ module Cabriolet
|
|
|
490
164
|
end
|
|
491
165
|
|
|
492
166
|
# Write NameList
|
|
493
|
-
namelist_data = build_namelist_data(structure[:sections])
|
|
167
|
+
namelist_data = ContentEncoder.build_namelist_data(structure[:sections])
|
|
494
168
|
total_bytes += @io_system.write(output_handle, namelist_data)
|
|
495
169
|
|
|
496
170
|
# Write manifest
|
|
497
|
-
manifest_data = build_manifest_data(structure[:manifest])
|
|
171
|
+
manifest_data = ContentEncoder.build_manifest_data(structure[:manifest])
|
|
498
172
|
total_bytes += @io_system.write(output_handle, manifest_data)
|
|
499
173
|
|
|
500
174
|
total_bytes
|
|
501
175
|
end
|
|
502
176
|
|
|
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
|
|
684
|
-
|
|
685
177
|
# Prepare file data for archiving
|
|
686
178
|
#
|
|
687
179
|
# @return [Array<Hash>] Array of file information hashes
|
|
@@ -709,142 +201,6 @@ module Cabriolet
|
|
|
709
201
|
}
|
|
710
202
|
end
|
|
711
203
|
end
|
|
712
|
-
|
|
713
|
-
# Write LIT header
|
|
714
|
-
#
|
|
715
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
716
|
-
# @param version [Integer] LIT format version
|
|
717
|
-
# @param file_count [Integer] Number of files
|
|
718
|
-
# @return [Integer] Number of bytes written
|
|
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
|
|
723
|
-
header.signature = Binary::LITStructures::SIGNATURE
|
|
724
|
-
header.version = version
|
|
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
|
|
729
|
-
|
|
730
|
-
header_data = header.to_binary_s
|
|
731
|
-
written = @io_system.write(output_handle, header_data)
|
|
732
|
-
|
|
733
|
-
unless written == header_data.bytesize
|
|
734
|
-
raise Errors::CompressionError,
|
|
735
|
-
"Failed to write LIT header"
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
written
|
|
739
|
-
end
|
|
740
|
-
|
|
741
|
-
# Write file entries directory
|
|
742
|
-
#
|
|
743
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
744
|
-
# @param file_data [Array<Hash>] Array of file information
|
|
745
|
-
# @return [Integer] Number of bytes written
|
|
746
|
-
def write_file_entries(output_handle, file_data)
|
|
747
|
-
total_bytes = 0
|
|
748
|
-
current_offset = calculate_header_size(file_data)
|
|
749
|
-
|
|
750
|
-
file_data.each do |file_info|
|
|
751
|
-
# Compress or store data
|
|
752
|
-
if file_info[:compress]
|
|
753
|
-
compressed = compress_data(file_info[:data])
|
|
754
|
-
compressed_size = compressed.bytesize
|
|
755
|
-
flags = Binary::LITStructures::FileFlags::COMPRESSED
|
|
756
|
-
else
|
|
757
|
-
compressed = file_info[:data]
|
|
758
|
-
compressed_size = compressed.bytesize
|
|
759
|
-
flags = 0
|
|
760
|
-
end
|
|
761
|
-
|
|
762
|
-
# Store compressed data for later writing
|
|
763
|
-
file_info[:compressed_data] = compressed
|
|
764
|
-
file_info[:compressed_size] = compressed_size
|
|
765
|
-
file_info[:offset] = current_offset
|
|
766
|
-
|
|
767
|
-
# Write file entry
|
|
768
|
-
entry = Binary::LITStructures::LITFileEntry.new
|
|
769
|
-
entry.filename_length = file_info[:lit_path].bytesize
|
|
770
|
-
entry.filename = file_info[:lit_path]
|
|
771
|
-
entry.offset = current_offset
|
|
772
|
-
entry.compressed_size = compressed_size
|
|
773
|
-
entry.uncompressed_size = file_info[:uncompressed_size]
|
|
774
|
-
entry.flags = flags
|
|
775
|
-
|
|
776
|
-
entry_data = entry.to_binary_s
|
|
777
|
-
written = @io_system.write(output_handle, entry_data)
|
|
778
|
-
total_bytes += written
|
|
779
|
-
|
|
780
|
-
current_offset += compressed_size
|
|
781
|
-
end
|
|
782
|
-
|
|
783
|
-
total_bytes
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
# Write file contents
|
|
787
|
-
#
|
|
788
|
-
# @param output_handle [System::FileHandle] Output file handle
|
|
789
|
-
# @param file_data [Array<Hash>] Array of file information with
|
|
790
|
-
# compressed data
|
|
791
|
-
# @return [Integer] Number of bytes written
|
|
792
|
-
def write_file_contents(output_handle, file_data)
|
|
793
|
-
total_bytes = 0
|
|
794
|
-
|
|
795
|
-
file_data.each do |file_info|
|
|
796
|
-
written = @io_system.write(
|
|
797
|
-
output_handle,
|
|
798
|
-
file_info[:compressed_data],
|
|
799
|
-
)
|
|
800
|
-
total_bytes += written
|
|
801
|
-
end
|
|
802
|
-
|
|
803
|
-
total_bytes
|
|
804
|
-
end
|
|
805
|
-
|
|
806
|
-
# Calculate total header size (header + all file entries)
|
|
807
|
-
#
|
|
808
|
-
# @param file_data [Array<Hash>] Array of file information
|
|
809
|
-
# @return [Integer] Total header size in bytes
|
|
810
|
-
def calculate_header_size(file_data)
|
|
811
|
-
# Header: 24 bytes
|
|
812
|
-
header_size = 24
|
|
813
|
-
|
|
814
|
-
# File entries: variable size
|
|
815
|
-
file_data.each do |file_info|
|
|
816
|
-
# 4 bytes filename length + filename + 28 bytes metadata
|
|
817
|
-
header_size += 4 + file_info[:lit_path].bytesize + 28
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
header_size
|
|
821
|
-
end
|
|
822
|
-
|
|
823
|
-
# Compress data using LZX
|
|
824
|
-
#
|
|
825
|
-
# @param data [String] Data to compress
|
|
826
|
-
# @return [String] Compressed data
|
|
827
|
-
def compress_data(data)
|
|
828
|
-
input_handle = System::MemoryHandle.new(data)
|
|
829
|
-
output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
830
|
-
|
|
831
|
-
begin
|
|
832
|
-
compressor = @algorithm_factory.create(
|
|
833
|
-
Constants::COMP_TYPE_LZX,
|
|
834
|
-
:compressor,
|
|
835
|
-
@io_system,
|
|
836
|
-
input_handle,
|
|
837
|
-
output_handle,
|
|
838
|
-
32_768,
|
|
839
|
-
)
|
|
840
|
-
|
|
841
|
-
compressor.compress
|
|
842
|
-
|
|
843
|
-
output_handle.data
|
|
844
|
-
|
|
845
|
-
# Memory handles don't need closing but maintain consistency
|
|
846
|
-
end
|
|
847
|
-
end
|
|
848
204
|
end
|
|
849
205
|
end
|
|
850
206
|
end
|