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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +3 -0
  3. data/lib/cabriolet/binary/bitstream.rb +32 -21
  4. data/lib/cabriolet/binary/bitstream_writer.rb +21 -4
  5. data/lib/cabriolet/cab/compressor.rb +85 -53
  6. data/lib/cabriolet/cab/decompressor.rb +2 -1
  7. data/lib/cabriolet/cab/extractor.rb +2 -35
  8. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  9. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  10. data/lib/cabriolet/checksum.rb +49 -0
  11. data/lib/cabriolet/collections/file_collection.rb +175 -0
  12. data/lib/cabriolet/compressors/quantum.rb +3 -51
  13. data/lib/cabriolet/decompressors/quantum.rb +81 -52
  14. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  15. data/lib/cabriolet/extraction/extractor.rb +171 -0
  16. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  17. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  18. data/lib/cabriolet/format_base.rb +79 -0
  19. data/lib/cabriolet/hlp/quickhelp/compressor.rb +28 -503
  20. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  21. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  22. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  23. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  24. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  25. data/lib/cabriolet/huffman/encoder.rb +15 -12
  26. data/lib/cabriolet/lit/compressor.rb +45 -689
  27. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  28. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  29. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  30. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  31. data/lib/cabriolet/lit/header_writer.rb +124 -0
  32. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  33. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  34. data/lib/cabriolet/quantum_shared.rb +105 -0
  35. data/lib/cabriolet/version.rb +1 -1
  36. data/lib/cabriolet.rb +114 -3
  37. metadata +38 -4
  38. data/lib/cabriolet/auto.rb +0 -173
  39. 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) # English
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
- lit_structure = build_lit_structure(file_data, version, language_id,
67
- creator_id)
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 = write_primary_header(output_handle, structure)
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
- structure[:pieces])
106
+ bytes_written += header_writer.write_piece_structures(output_handle,
107
+ structure[:pieces])
126
108
 
127
109
  # Write secondary header
128
- bytes_written += write_secondary_header_block(
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 = build_piece0_data(structure)
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 = build_piece1_data(structure)
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 = build_piece2_data(structure)
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