cabriolet 0.1.2 → 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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +703 -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 +167 -16
  6. data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
  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 +108 -84
  13. data/lib/cabriolet/cab/decompressor.rb +16 -20
  14. data/lib/cabriolet/cab/extractor.rb +142 -66
  15. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  16. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  17. data/lib/cabriolet/checksum.rb +49 -0
  18. data/lib/cabriolet/chm/command_handler.rb +227 -0
  19. data/lib/cabriolet/chm/compressor.rb +7 -3
  20. data/lib/cabriolet/chm/decompressor.rb +39 -21
  21. data/lib/cabriolet/chm/parser.rb +5 -2
  22. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  23. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  24. data/lib/cabriolet/cli/command_registry.rb +83 -0
  25. data/lib/cabriolet/cli.rb +356 -607
  26. data/lib/cabriolet/collections/file_collection.rb +175 -0
  27. data/lib/cabriolet/compressors/base.rb +1 -1
  28. data/lib/cabriolet/compressors/lzx.rb +241 -54
  29. data/lib/cabriolet/compressors/mszip.rb +35 -3
  30. data/lib/cabriolet/compressors/quantum.rb +36 -95
  31. data/lib/cabriolet/decompressors/base.rb +1 -1
  32. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  33. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  34. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  35. data/lib/cabriolet/decompressors/quantum.rb +83 -53
  36. data/lib/cabriolet/errors.rb +3 -0
  37. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  38. data/lib/cabriolet/extraction/extractor.rb +171 -0
  39. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  40. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  41. data/lib/cabriolet/file_entry.rb +156 -0
  42. data/lib/cabriolet/file_manager.rb +144 -0
  43. data/lib/cabriolet/format_base.rb +79 -0
  44. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  45. data/lib/cabriolet/hlp/compressor.rb +28 -238
  46. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  47. data/lib/cabriolet/hlp/parser.rb +52 -101
  48. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  49. data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
  50. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  51. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  52. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  53. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  54. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  55. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  56. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  57. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  58. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  59. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  60. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  61. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  62. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  63. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  64. data/lib/cabriolet/huffman/encoder.rb +15 -12
  65. data/lib/cabriolet/huffman/tree.rb +85 -1
  66. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  67. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  68. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  69. data/lib/cabriolet/lit/command_handler.rb +221 -0
  70. data/lib/cabriolet/lit/compressor.rb +119 -168
  71. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  72. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  73. data/lib/cabriolet/lit/decompressor.rb +518 -152
  74. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  75. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  76. data/lib/cabriolet/lit/header_writer.rb +124 -0
  77. data/lib/cabriolet/lit/parser.rb +670 -0
  78. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  79. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  80. data/lib/cabriolet/models/hlp_file.rb +130 -29
  81. data/lib/cabriolet/models/hlp_header.rb +105 -17
  82. data/lib/cabriolet/models/lit_header.rb +212 -25
  83. data/lib/cabriolet/models/szdd_header.rb +10 -2
  84. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  85. data/lib/cabriolet/oab/command_handler.rb +257 -0
  86. data/lib/cabriolet/oab/compressor.rb +17 -8
  87. data/lib/cabriolet/oab/decompressor.rb +41 -10
  88. data/lib/cabriolet/offset_calculator.rb +81 -0
  89. data/lib/cabriolet/plugin.rb +233 -0
  90. data/lib/cabriolet/plugin_manager.rb +453 -0
  91. data/lib/cabriolet/plugin_validator.rb +422 -0
  92. data/lib/cabriolet/quantum_shared.rb +105 -0
  93. data/lib/cabriolet/system/io_system.rb +3 -0
  94. data/lib/cabriolet/system/memory_handle.rb +17 -4
  95. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  96. data/lib/cabriolet/szdd/compressor.rb +15 -11
  97. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  98. data/lib/cabriolet/version.rb +1 -1
  99. data/lib/cabriolet.rb +181 -20
  100. metadata +69 -4
  101. data/lib/cabriolet/auto.rb +0 -173
  102. data/lib/cabriolet/parallel.rb +0 -333
@@ -0,0 +1,670 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../binary/lit_structures"
4
+ require_relative "../models/lit_header"
5
+ require_relative "../errors"
6
+
7
+ module Cabriolet
8
+ module LIT
9
+ # Parser for Microsoft Reader LIT files
10
+ #
11
+ # Handles parsing of the complex LIT file structure including:
12
+ # - Primary and secondary headers with piece table
13
+ # - IFCM/AOLL/AOLI directory chunks with encoded integers
14
+ # - DataSpace sections with transform layers
15
+ # - Manifest file with filename mappings
16
+ #
17
+ # Based on the openclit/SharpLit reference implementation.
18
+ class Parser
19
+ attr_reader :io_system
20
+
21
+ def initialize(io_system = nil)
22
+ @io_system = io_system || System::IOSystem.new
23
+ end
24
+
25
+ # Parse a LIT file and return the model
26
+ #
27
+ # @param filename [String] Path to LIT file
28
+ # @return [Models::LITFile] Parsed LIT file structure
29
+ # @raise [Errors::ParseError] if file is invalid or unsupported
30
+ def parse(filename)
31
+ handle = @io_system.open(filename, Constants::MODE_READ)
32
+
33
+ begin
34
+ lit_file = Models::LITFile.new
35
+
36
+ # Parse primary header
37
+ parse_primary_header(handle, lit_file)
38
+
39
+ # Parse pieces
40
+ pieces = parse_pieces(handle, lit_file)
41
+
42
+ # Parse secondary header
43
+ parse_secondary_header(handle, lit_file, pieces)
44
+
45
+ # Parse directory from piece 1
46
+ parse_directory(handle, lit_file, pieces[1])
47
+
48
+ # Parse sections
49
+ parse_sections(handle, lit_file)
50
+
51
+ # Parse manifest
52
+ parse_manifest(handle, lit_file)
53
+
54
+ lit_file
55
+ ensure
56
+ @io_system.close(handle) if handle
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Parse primary header (40 bytes)
63
+ def parse_primary_header(handle, lit_file)
64
+ @io_system.seek(handle, 0, Constants::SEEK_START)
65
+ header_data = @io_system.read(handle, 40)
66
+
67
+ header = Binary::LITStructures::PrimaryHeader.read(header_data)
68
+
69
+ # Verify signature
70
+ unless header.signature == Binary::LITStructures::SIGNATURE
71
+ raise Cabriolet::ParseError,
72
+ "Invalid LIT signature: #{header.signature.inspect}"
73
+ end
74
+
75
+ # Verify version
76
+ unless header.version == 1
77
+ raise Cabriolet::ParseError,
78
+ "Unsupported LIT version #{header.version}, only version 1 is supported"
79
+ end
80
+
81
+ # Store header info
82
+ lit_file.version = header.version
83
+ lit_file.header_guid = header.header_guid
84
+
85
+ [header, header.num_pieces, header.secondary_header_length]
86
+ end
87
+
88
+ # Parse piece structures
89
+ def parse_pieces(handle, lit_file)
90
+ _, num_pieces, = parse_primary_header(handle, lit_file)
91
+
92
+ # Skip to pieces (after primary header)
93
+ @io_system.seek(handle, 40, Constants::SEEK_START)
94
+
95
+ pieces = []
96
+ num_pieces.times do
97
+ piece_data = @io_system.read(handle, 16)
98
+ piece = Binary::LITStructures::PieceStructure.read(piece_data)
99
+
100
+ # Verify no 64-bit values
101
+ if piece.offset_high != 0 || piece.size_high != 0
102
+ raise Cabriolet::ParseError,
103
+ "64-bit piece values not supported"
104
+ end
105
+
106
+ pieces << {
107
+ offset: piece.offset_low,
108
+ size: piece.size_low,
109
+ }
110
+ end
111
+
112
+ # Read piece data
113
+ pieces.each_with_index do |piece, index|
114
+ @io_system.seek(handle, piece[:offset], Constants::SEEK_START)
115
+ piece[:data] = @io_system.read(handle, piece[:size])
116
+
117
+ # Store GUIDs from pieces 3 and 4
118
+ case index
119
+ when 3
120
+ lit_file.piece3_guid = piece[:data]
121
+ when 4
122
+ lit_file.piece4_guid = piece[:data]
123
+ end
124
+ end
125
+
126
+ pieces
127
+ end
128
+
129
+ # Parse secondary header (SECHDR + CAOL + ITSF)
130
+ def parse_secondary_header(handle, lit_file, pieces)
131
+ _, num_pieces, sec_hdr_len = parse_primary_header(handle, lit_file)
132
+
133
+ # Calculate content_offset: the content starts after all pieces
134
+ # Primary header: 40 bytes
135
+ # Piece structures: 5 * 16 = 80 bytes
136
+ # Secondary header: variable (sec_hdr_len)
137
+ # Then pieces 0-4 data
138
+ # Content starts after the last piece
139
+ if pieces&.length&.positive?
140
+ last_piece = pieces.last
141
+ lit_file.content_offset = last_piece[:offset] + last_piece[:size]
142
+ else
143
+ # Fallback calculation
144
+ offset = 40 + (num_pieces * 16)
145
+ @io_system.seek(handle, offset, Constants::SEEK_START)
146
+ sec_hdr_data = @io_system.read(handle, sec_hdr_len)
147
+ sec_hdr = Binary::LITStructures::SecondaryHeader.read(sec_hdr_data)
148
+ lit_file.content_offset = sec_hdr.content_offset
149
+ end
150
+
151
+ lit_file.timestamp = 0
152
+ lit_file.language_id = 0x409
153
+ lit_file.creator_id = 0
154
+ lit_file.entry_chunklen = 0x2000
155
+ lit_file.count_chunklen = 0x200
156
+ lit_file.entry_unknown = 0x100000
157
+ lit_file.count_unknown = 0x20000
158
+ end
159
+
160
+ # Parse directory structure from piece 1
161
+ def parse_directory(_handle, lit_file, piece)
162
+ data = piece[:data]
163
+ return unless data
164
+
165
+ # Parse IFCM header
166
+ ifcm = Binary::LITStructures::IFCMHeader.read(data[0, 32])
167
+
168
+ unless ifcm.tag == Binary::LITStructures::Tags::IFCM
169
+ raise Cabriolet::ParseError,
170
+ "Invalid IFCM tag: #{format('0x%08X', ifcm.tag)}"
171
+ end
172
+
173
+ # Create directory model
174
+ directory = Models::LITDirectory.new
175
+ directory.num_chunks = ifcm.num_chunks
176
+ directory.entry_chunklen = lit_file.entry_chunklen
177
+ directory.count_chunklen = lit_file.count_chunklen
178
+ directory.entries = []
179
+
180
+ # Parse each chunk
181
+ chunk_size = ifcm.chunk_size
182
+ ifcm.num_chunks.times do |chunk_idx|
183
+ chunk_offset = 32 + (chunk_idx * chunk_size)
184
+ chunk_data = data[chunk_offset, chunk_size]
185
+
186
+ parse_directory_chunk(chunk_data, chunk_size, directory)
187
+ end
188
+
189
+ lit_file.directory = directory
190
+ end
191
+
192
+ # Parse a single directory chunk (AOLL or AOLI)
193
+ def parse_directory_chunk(data, chunk_size, directory)
194
+ tag = data[0, 4].unpack1("V")
195
+
196
+ case tag
197
+ when Binary::LITStructures::Tags::AOLL
198
+ parse_aoll_chunk(data, chunk_size, directory)
199
+ when Binary::LITStructures::Tags::AOLI
200
+ # AOLI chunks are for indexing, we can skip them for reading
201
+ nil
202
+ end
203
+ end
204
+
205
+ # Parse AOLL (list) chunk
206
+ def parse_aoll_chunk(data, chunk_size, directory)
207
+ header = Binary::LITStructures::AOLLHeader.read(data[0, 48])
208
+
209
+ # Calculate data area
210
+ quickref_offset = header.quickref_offset
211
+ data_size = chunk_size - (quickref_offset + 48)
212
+ data_offset = 48
213
+
214
+ # Parse entries using encoded integers
215
+ remaining = data_size
216
+ pos = data_offset
217
+
218
+ while remaining.positive?
219
+ entry = parse_directory_entry(data, pos, remaining)
220
+ break unless entry
221
+
222
+ directory.entries << entry
223
+
224
+ # Update position using instance variable
225
+ entry_size = entry.instance_variable_get(:@_bytes_read)
226
+ pos += entry_size
227
+ remaining -= entry_size
228
+ end
229
+ end
230
+
231
+ # Parse directory entry with encoded integers
232
+ def parse_directory_entry(data, pos, remaining)
233
+ return nil if remaining <= 0
234
+
235
+ start_pos = pos
236
+
237
+ # Read name length (encoded integer)
238
+ name_length, bytes = read_encoded_int(data, pos, remaining)
239
+ return nil unless name_length
240
+
241
+ pos += bytes
242
+ remaining -= bytes
243
+
244
+ # Read name
245
+ return nil if remaining < name_length
246
+
247
+ name = data[pos, name_length].force_encoding("UTF-8")
248
+ pos += name_length
249
+ remaining -= name_length
250
+
251
+ # Read section (encoded integer)
252
+ section, bytes = read_encoded_int(data, pos, remaining)
253
+ return nil unless section
254
+
255
+ pos += bytes
256
+ remaining -= bytes
257
+
258
+ # Read offset (encoded integer)
259
+ offset, bytes = read_encoded_int(data, pos, remaining)
260
+ return nil unless offset
261
+
262
+ pos += bytes
263
+ remaining -= bytes
264
+
265
+ # Read size (encoded integer)
266
+ size, bytes = read_encoded_int(data, pos, remaining)
267
+ return nil unless size
268
+
269
+ pos += bytes
270
+ remaining - bytes
271
+
272
+ # Create entry
273
+ entry = Models::LITDirectoryEntry.new
274
+ entry.name = name
275
+ entry.section = section
276
+ entry.offset = offset
277
+ entry.size = size
278
+
279
+ # Attach metadata for position tracking
280
+ entry.instance_variable_set(:@_bytes_read, pos - start_pos)
281
+ entry
282
+ end
283
+
284
+ # Read an encoded integer (variable length)
285
+ #
286
+ # MSB indicates continuation, lower 7 bits are data
287
+ def read_encoded_int(data, pos, remaining)
288
+ return [nil, 0] if remaining <= 0
289
+
290
+ value = 0
291
+ bytes_read = 0
292
+
293
+ loop do
294
+ return [nil, 0] if bytes_read >= remaining
295
+
296
+ byte = data[pos + bytes_read].ord
297
+ bytes_read += 1
298
+
299
+ value <<= 7
300
+ value |= (byte & 0x7F)
301
+
302
+ break unless byte.anybits?(0x80)
303
+ end
304
+
305
+ [value, bytes_read]
306
+ end
307
+
308
+ # Parse sections from ::DataSpace/NameList
309
+ def parse_sections(handle, lit_file)
310
+ return unless lit_file.directory
311
+
312
+ # The NameList entry in the directory doesn't point to a valid NameList structure
313
+ # Instead, create sections based on the directory entries themselves
314
+
315
+ # Find unique section IDs from directory (excluding section 0 which is uncompressed)
316
+ section_ids = lit_file.directory.entries.map(&:section).uniq.sort
317
+ section_ids.delete(0) # Skip section 0 (uncompressed)
318
+
319
+ # Build an array indexed by section_id
320
+ # sections[section_id] gives the section for that ID
321
+ max_section_id = section_ids.last || 0
322
+ sections = Array.new(max_section_id + 1) # Create array with nil placeholders
323
+
324
+ section_ids.each do |section_id|
325
+ # Create a section object
326
+ section = Models::LITSection.new
327
+
328
+ # Determine section name based on directory entries
329
+ # Look for storage section entries in the directory
330
+ section.name = find_section_name(lit_file, section_id)
331
+
332
+ sections[section_id] = section
333
+ end
334
+
335
+ # Parse transform information for each section (skip nil entries)
336
+ sections.compact.each do |section|
337
+ parse_section_transforms(handle, lit_file, section)
338
+ end
339
+
340
+ lit_file.sections = sections
341
+ end
342
+
343
+ # Find section name from directory entries
344
+ def find_section_name(lit_file, section_id)
345
+ # Get all storage section names from section 0
346
+ storage_sections = lit_file.directory.entries.select do |e|
347
+ e.section.zero? &&
348
+ e.name.start_with?("::DataSpace/Storage/") &&
349
+ e.name.count("/") == 3 # ::DataSpace/Storage/SectionName/
350
+ end.map do |e|
351
+ e.name.match(/^::DataSpace\/Storage\/([^\/]+)\//)[1]
352
+ end.uniq
353
+
354
+ # Map section IDs to storage section names
355
+ # For LIT files, the mapping is typically:
356
+ # - Section 1: Not used
357
+ # - Section 2: MSCompressed (LZX compression)
358
+ # - Section 3: EbEncryptOnlyDS or EbEncryptDS (DES encryption)
359
+ case section_id
360
+ when 2
361
+ # Section 2 is typically MSCompressed
362
+ if storage_sections.include?("MSCompressed")
363
+ "MSCompressed"
364
+ elsif storage_sections.include?("EbEncryptDS")
365
+ "EbEncryptDS"
366
+ else
367
+ storage_sections.first || "Section2"
368
+ end
369
+ when 3
370
+ # Section 3 is typically encryption-related
371
+ if storage_sections.include?("EbEncryptOnlyDS")
372
+ "EbEncryptOnlyDS"
373
+ elsif storage_sections.include?("EbEncryptDS")
374
+ "EbEncryptDS"
375
+ else
376
+ "Section3"
377
+ end
378
+ else
379
+ format("Section%d", section_id)
380
+ end
381
+ end
382
+
383
+ # Parse transform information for a section
384
+ def parse_section_transforms(handle, lit_file, section)
385
+ # Build transform list path
386
+ transform_path = Binary::LITStructures::Paths::STORAGE +
387
+ section.name +
388
+ Binary::LITStructures::Paths::TRANSFORM_LIST
389
+
390
+ transform_entry = lit_file.directory.find(transform_path)
391
+ return unless transform_entry
392
+
393
+ # Read transform list data
394
+ @io_system.seek(
395
+ handle,
396
+ lit_file.content_offset + transform_entry.offset,
397
+ Constants::SEEK_START,
398
+ )
399
+ transform_data = @io_system.read(handle, transform_entry.size)
400
+
401
+ # Parse transforms (GUIDs)
402
+ section.transforms = []
403
+ pos = 0
404
+
405
+ while pos + 16 <= transform_data.bytesize
406
+ guid_bytes = transform_data[pos, 16]
407
+ guid = format_transform_guid(guid_bytes)
408
+ section.transforms << guid
409
+
410
+ # Set flags based on GUID
411
+ case guid
412
+ when Binary::LITStructures::GUIDs::DESENCRYPT
413
+ section.encrypted = true
414
+ lit_file.drm_level = 1
415
+ when Binary::LITStructures::GUIDs::LZXCOMPRESS
416
+ section.compressed = true
417
+ end
418
+
419
+ pos += 16
420
+ end
421
+
422
+ # Parse LZX control data if compressed
423
+ if section.compressed
424
+ parse_lzx_control_data(handle, lit_file, section)
425
+ end
426
+ end
427
+
428
+ # Parse LZX control data and reset table
429
+ def parse_lzx_control_data(handle, lit_file, section)
430
+ # Find control data entry
431
+ control_path = Binary::LITStructures::Paths::STORAGE +
432
+ section.name +
433
+ Binary::LITStructures::Paths::CONTROL_DATA
434
+
435
+ control_entry = lit_file.directory.find(control_path)
436
+ return unless control_entry
437
+
438
+ # Read control data
439
+ @io_system.seek(
440
+ handle,
441
+ lit_file.content_offset + control_entry.offset,
442
+ Constants::SEEK_START,
443
+ )
444
+ control_data = @io_system.read(handle, control_entry.size)
445
+
446
+ return unless control_data.bytesize >= 32
447
+
448
+ # Parse control data structure
449
+ control = Binary::LITStructures::LZXControlData.read(control_data)
450
+
451
+ # Calculate window size
452
+ window_size = 15
453
+ size_code = control.window_size_code
454
+ while size_code.positive?
455
+ size_code >>= 1
456
+ window_size += 1
457
+ end
458
+
459
+ section.window_size = window_size
460
+
461
+ # Parse reset table
462
+ parse_reset_table_info(handle, lit_file, section)
463
+ end
464
+
465
+ # Parse reset table information
466
+ def parse_reset_table_info(handle, lit_file, section)
467
+ # Find reset table entry
468
+ reset_table_path = Binary::LITStructures::Paths::STORAGE +
469
+ section.name +
470
+ "/Transform/#{Binary::LITStructures::GUIDs::LZXCOMPRESS}/InstanceData/ResetTable"
471
+
472
+ reset_entry = lit_file.directory.find(reset_table_path)
473
+ return unless reset_entry
474
+
475
+ # Read reset table
476
+ @io_system.seek(
477
+ handle,
478
+ lit_file.content_offset + reset_entry.offset,
479
+ Constants::SEEK_START,
480
+ )
481
+ reset_data = @io_system.read(handle, reset_entry.size)
482
+
483
+ return unless reset_data.bytesize >= 40
484
+
485
+ # Parse reset table header
486
+ header = Binary::LITStructures::ResetTableHeader.read(reset_data[0, 40])
487
+
488
+ section.uncompressed_length = header.uncompressed_length
489
+ section.compressed_length = header.compressed_length
490
+ section.reset_interval = header.reset_interval
491
+
492
+ # Parse reset points
493
+ entry_offset = header.header_length + 8
494
+ num_entries = header.num_entries
495
+
496
+ reset_points = []
497
+ (num_entries - 1).times do
498
+ break if entry_offset + 8 > reset_data.bytesize
499
+
500
+ offset_low = reset_data[entry_offset, 4].unpack1("V")
501
+ offset_high = reset_data[entry_offset + 4, 4].unpack1("V")
502
+
503
+ # Skip 64-bit offsets (not supported)
504
+ break if offset_high != 0
505
+
506
+ reset_points << offset_low
507
+ entry_offset += 8
508
+ end
509
+
510
+ section.reset_table = reset_points
511
+ end
512
+
513
+ # Format GUID bytes as string
514
+ def format_transform_guid(bytes)
515
+ parts = bytes.unpack("VvvnH12")
516
+ format(
517
+ "{%<part0>08X-%<part1>04X-%<part2>04X-%<part3>04X-%<part4>s}",
518
+ part0: parts[0], part1: parts[1], part2: parts[2],
519
+ part3: parts[3], part4: parts[4].upcase
520
+ )
521
+ end
522
+
523
+ # Parse NameList format
524
+ def parse_namelist(data)
525
+ sections = []
526
+ pos = 2 # Skip initial field
527
+
528
+ # Read number of sections
529
+ return sections if pos >= data.bytesize
530
+
531
+ num_sections = data[pos, 2].unpack1("v")
532
+ pos += 2
533
+
534
+ num_sections.times do
535
+ break if pos >= data.bytesize
536
+
537
+ # Read section name length
538
+ name_length = data[pos, 2].unpack1("v")
539
+ pos += 2
540
+
541
+ break if pos + (name_length * 2) > data.bytesize
542
+
543
+ # Read section name (UTF-16LE)
544
+ name_bytes = data[pos, name_length * 2]
545
+ name = name_bytes.unpack("v*").pack("U*").force_encoding("UTF-8")
546
+ pos += (name_length * 2) + 2 # +2 for null terminator
547
+
548
+ section = Models::LITSection.new
549
+ section.name = name
550
+ sections << section
551
+ end
552
+
553
+ sections
554
+ end
555
+
556
+ # Parse manifest file
557
+ def parse_manifest(handle, lit_file)
558
+ return unless lit_file.directory
559
+
560
+ # Find manifest entry
561
+ manifest_entry = lit_file.directory.find(
562
+ Binary::LITStructures::Paths::MANIFEST,
563
+ )
564
+ return unless manifest_entry
565
+
566
+ # Read manifest
567
+ @io_system.seek(
568
+ handle,
569
+ lit_file.content_offset + manifest_entry.offset,
570
+ Constants::SEEK_START,
571
+ )
572
+ manifest_data = @io_system.read(handle, manifest_entry.size)
573
+
574
+ # Parse manifest
575
+ lit_file.manifest = parse_manifest_data(manifest_data)
576
+ end
577
+
578
+ # Parse manifest data
579
+ def parse_manifest_data(data)
580
+ manifest = Models::LITManifest.new
581
+ manifest.mappings = []
582
+
583
+ pos = 0
584
+
585
+ while pos < data.bytesize
586
+ # Read directory name length
587
+ break if pos >= data.bytesize
588
+
589
+ dir_length = data[pos].ord
590
+ pos += 1
591
+ break if dir_length.zero?
592
+
593
+ # Skip directory name
594
+ pos += dir_length
595
+
596
+ # Read 4 groups (HTML spine, HTML other, CSS, Images)
597
+ 4.times do |group|
598
+ break if pos + 4 > data.bytesize
599
+
600
+ num_files = data[pos, 4].unpack1("V")
601
+ pos += 4
602
+
603
+ num_files.times do
604
+ mapping = parse_manifest_entry(data, pos, group)
605
+ break unless mapping
606
+
607
+ manifest.mappings << mapping
608
+ pos += mapping.instance_variable_get(:@_bytes_read)
609
+ end
610
+ end
611
+ end
612
+
613
+ manifest
614
+ end
615
+
616
+ # Parse single manifest entry
617
+ def parse_manifest_entry(data, pos, group)
618
+ return nil if pos + 5 > data.bytesize
619
+
620
+ start_pos = pos
621
+
622
+ # Read offset
623
+ offset = data[pos, 4].unpack1("V")
624
+ pos += 4
625
+
626
+ # Read internal name
627
+ internal_length = data[pos].ord
628
+ pos += 1
629
+ return nil if pos + internal_length > data.bytesize
630
+
631
+ internal_name = data[pos, internal_length].force_encoding("UTF-8")
632
+ pos += internal_length
633
+
634
+ # Read original name
635
+ return nil if pos >= data.bytesize
636
+
637
+ original_length = data[pos].ord
638
+ pos += 1
639
+ return nil if pos + original_length > data.bytesize
640
+
641
+ original_name = data[pos, original_length].force_encoding("UTF-8")
642
+ pos += original_length
643
+
644
+ # Read content type
645
+ return nil if pos >= data.bytesize
646
+
647
+ type_length = data[pos].ord
648
+ pos += 1
649
+ return nil if pos + type_length > data.bytesize
650
+
651
+ content_type = data[pos, type_length].force_encoding("UTF-8")
652
+ pos += type_length
653
+
654
+ # Skip terminator
655
+ pos += 1
656
+
657
+ mapping = Models::LITManifestMapping.new
658
+ mapping.offset = offset
659
+ mapping.internal_name = internal_name
660
+ mapping.original_name = original_name
661
+ mapping.content_type = content_type
662
+ mapping.group = group
663
+
664
+ # Attach metadata for position tracking
665
+ mapping.instance_variable_set(:@_bytes_read, pos - start_pos)
666
+ mapping
667
+ end
668
+ end
669
+ end
670
+ end