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.
- checksums.yaml +4 -4
- data/README.adoc +703 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +167 -16
- data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +108 -84
- data/lib/cabriolet/cab/decompressor.rb +16 -20
- data/lib/cabriolet/cab/extractor.rb +142 -66
- 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/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +36 -95
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +83 -53
- data/lib/cabriolet/errors.rb +3 -0
- 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/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/format_base.rb +79 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -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/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/encoder.rb +15 -12
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +119 -168
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- 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/parser.rb +670 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +181 -20
- metadata +69 -4
- data/lib/cabriolet/auto.rb +0 -173
- 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
|