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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +700 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +154 -14
  6. data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +35 -43
  13. data/lib/cabriolet/cab/decompressor.rb +14 -19
  14. data/lib/cabriolet/cab/extractor.rb +140 -31
  15. data/lib/cabriolet/chm/command_handler.rb +227 -0
  16. data/lib/cabriolet/chm/compressor.rb +7 -3
  17. data/lib/cabriolet/chm/decompressor.rb +39 -21
  18. data/lib/cabriolet/chm/parser.rb +5 -2
  19. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  20. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  21. data/lib/cabriolet/cli/command_registry.rb +83 -0
  22. data/lib/cabriolet/cli.rb +356 -607
  23. data/lib/cabriolet/compressors/base.rb +1 -1
  24. data/lib/cabriolet/compressors/lzx.rb +241 -54
  25. data/lib/cabriolet/compressors/mszip.rb +35 -3
  26. data/lib/cabriolet/compressors/quantum.rb +34 -45
  27. data/lib/cabriolet/decompressors/base.rb +1 -1
  28. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  29. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  30. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  31. data/lib/cabriolet/decompressors/quantum.rb +3 -2
  32. data/lib/cabriolet/errors.rb +3 -0
  33. data/lib/cabriolet/file_entry.rb +156 -0
  34. data/lib/cabriolet/file_manager.rb +144 -0
  35. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  36. data/lib/cabriolet/hlp/compressor.rb +28 -238
  37. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  38. data/lib/cabriolet/hlp/parser.rb +52 -101
  39. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  40. data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
  41. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  42. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  43. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  44. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  45. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  46. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  47. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  48. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  49. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  50. data/lib/cabriolet/huffman/tree.rb +85 -1
  51. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  52. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  53. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  54. data/lib/cabriolet/lit/command_handler.rb +221 -0
  55. data/lib/cabriolet/lit/compressor.rb +633 -38
  56. data/lib/cabriolet/lit/decompressor.rb +518 -152
  57. data/lib/cabriolet/lit/parser.rb +670 -0
  58. data/lib/cabriolet/models/hlp_file.rb +130 -29
  59. data/lib/cabriolet/models/hlp_header.rb +105 -17
  60. data/lib/cabriolet/models/lit_header.rb +212 -25
  61. data/lib/cabriolet/models/szdd_header.rb +10 -2
  62. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  63. data/lib/cabriolet/oab/command_handler.rb +257 -0
  64. data/lib/cabriolet/oab/compressor.rb +17 -8
  65. data/lib/cabriolet/oab/decompressor.rb +41 -10
  66. data/lib/cabriolet/offset_calculator.rb +81 -0
  67. data/lib/cabriolet/plugin.rb +233 -0
  68. data/lib/cabriolet/plugin_manager.rb +453 -0
  69. data/lib/cabriolet/plugin_validator.rb +422 -0
  70. data/lib/cabriolet/system/io_system.rb +3 -0
  71. data/lib/cabriolet/system/memory_handle.rb +17 -4
  72. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  73. data/lib/cabriolet/szdd/compressor.rb +15 -11
  74. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  75. data/lib/cabriolet/version.rb +1 -1
  76. data/lib/cabriolet.rb +67 -17
  77. 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
- def initialize(io_system = nil)
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 [Boolean] :encrypt Whether to encrypt (not supported,
49
- # raises error)
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 generation fails
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
- encrypt = options.fetch(:encrypt, false)
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
- output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
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
- # Prepare file data
69
- file_data = prepare_files
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
- # Write header
72
- header_bytes = write_header(
73
- output_handle,
74
- version,
75
- file_data.size,
76
- )
79
+ private
77
80
 
78
- # Write file entries
79
- entries_bytes = write_file_entries(output_handle, file_data)
81
+ # Build complete LIT structure
82
+ def build_lit_structure(file_data, version, language_id, creator_id)
83
+ structure = {}
80
84
 
81
- # Write file contents
82
- content_bytes = write_file_contents(
83
- output_handle,
84
- file_data,
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
- header_bytes + entries_bytes + content_bytes
88
- ensure
89
- @io_system.close(output_handle) if output_handle
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
- private
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, file_count)
130
- header = Binary::LITStructures::LITHeader.new
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.flags = 0 # Not encrypted
134
- header.file_count = file_count
135
- header.header_size = 24 # Size of the header structure
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 = Compressors::LZX.new(
832
+ compressor = @algorithm_factory.create(
833
+ Constants::COMP_TYPE_LZX,
834
+ :compressor,
240
835
  @io_system,
241
836
  input_handle,
242
837
  output_handle,