PyCriCodecsEx 0.0.5__cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

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.
PyCriCodecsEx/cpk.py ADDED
@@ -0,0 +1,743 @@
1
+ import os
2
+ from typing import BinaryIO, Generator
3
+ from io import BytesIO, FileIO
4
+ from PyCriCodecsEx.chunk import *
5
+ from PyCriCodecsEx.utf import UTF, UTFBuilder
6
+ from dataclasses import dataclass
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from tempfile import NamedTemporaryFile
9
+ import CriCodecsEx
10
+
11
+ def _crilayla_compress_to_file(src : str, dst: str):
12
+ with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
13
+ data = fsrc.read()
14
+ try:
15
+ compressed = CriCodecsEx.CriLaylaCompress(data)
16
+ fdst.write(compressed)
17
+ except:
18
+ # Fallback for failed compression
19
+ # Again. FIXME.
20
+ fdst.write(data)
21
+
22
+ @dataclass
23
+ class PackedFile():
24
+ """Helper class for packed files within a CPK."""
25
+ stream: BinaryIO
26
+ path: str
27
+ offset: int
28
+ size : int
29
+ compressed : bool = False
30
+
31
+ def get_bytes(self) -> bytes:
32
+ """Get the raw bytes of the packed file, decompressing if necessary."""
33
+ self.stream.seek(self.offset)
34
+ data = self.stream.read(self.size)
35
+ if self.compressed:
36
+ data = CriCodecsEx.CriLaylaDecompress(data)
37
+ return data
38
+
39
+ def save(self, path : str):
40
+ """Save the packed file to a specified path."""
41
+ with open(path, "wb") as f:
42
+ f.write(self.get_bytes())
43
+ class _TOC():
44
+ magic: bytes
45
+ encflag: int
46
+ packet_size: int
47
+ unk0C: int
48
+ stream: BinaryIO
49
+ table: dict
50
+ def __init__(self, stream: bytes) -> None:
51
+ self.stream = BytesIO(stream)
52
+ self.magic, self.encflag, self.packet_size, self.unk0C = CPKChunkHeader.unpack(
53
+ self.stream.read(CPKChunkHeader.size)
54
+ )
55
+ if self.magic not in [header.value for header in CPKChunkHeaderType]:
56
+ raise ValueError(f"{self.magic} header not supported.")
57
+ self.table = UTF(self.stream.read()).table
58
+
59
+ class CPK:
60
+ """Use this class to load CPK file table-of-content, and read files from them on-demand."""
61
+ magic: bytes
62
+ encflag: int
63
+ packet_size: int
64
+ unk0C: int
65
+ stream: BinaryIO
66
+ tables: dict
67
+ filename: str
68
+ def __init__(self, filename : str | BinaryIO) -> None:
69
+ """Loads a CPK archive's table-of-content and ready for file reading.
70
+
71
+ Args:
72
+ filename (str | BinaryIO): The path to the CPK file or a BinaryIO stream containing the CPK data.
73
+ """
74
+ if type(filename) == str:
75
+ self.filename = filename
76
+ self.stream = FileIO(filename)
77
+ else:
78
+ self.stream = BytesIO(filename)
79
+ self.filename = ''
80
+ self.magic, self.encflag, self.packet_size, self.unk0C = CPKChunkHeader.unpack(
81
+ self.stream.read(CPKChunkHeader.size)
82
+ )
83
+ if self.magic != CPKChunkHeaderType.CPK.value:
84
+ raise ValueError("Invalid CPK file.")
85
+ self.tables = dict(CPK = UTF(self.stream.read(0x800-CPKChunkHeader.size)).table)
86
+ self._load_tocs()
87
+
88
+ def _load_tocs(self) -> None:
89
+ for key, value in self.tables["CPK"].items():
90
+ if key == "TocOffset":
91
+ if value[0]:
92
+ self.stream.seek(value[0], 0)
93
+ self.tables["TOC"] = _TOC(self.stream.read(self.tables['CPK']["TocSize"][0])).table
94
+ elif key == "ItocOffset":
95
+ if value[0]:
96
+ self.stream.seek(value[0], 0)
97
+ self.tables["ITOC"] = _TOC(self.stream.read(self.tables['CPK']["ItocSize"][0])).table
98
+ if "DataL" in self.tables["ITOC"]:
99
+ self.tables["ITOC"]['DataL'][0] = UTF(self.tables["ITOC"]['DataL'][0]).table
100
+ if "DataH" in self.tables["ITOC"]:
101
+ self.tables["ITOC"]['DataH'][0] = UTF(self.tables["ITOC"]['DataH'][0]).table
102
+ elif key == "HtocOffset":
103
+ if value[0]:
104
+ self.stream.seek(value[0], 0)
105
+ self.tables["HTOC"] = _TOC(self.stream.read(self.tables['CPK']["HtocSize"][0])).table
106
+ elif key == "GtocOffset":
107
+ if value[0]:
108
+ self.stream.seek(value[0], 0)
109
+ self.tables["GTOC"] = _TOC(self.stream.read(self.tables['CPK']["GtocSize"][0])).table
110
+ if "AttrData" in self.tables["GTOC"]:
111
+ self.tables["GTOC"]['AttrData'][0] = UTF(self.tables["GTOC"]['AttrData'][0]).table
112
+ if "Fdata" in self.tables["GTOC"]:
113
+ self.tables["GTOC"]['Fdata'][0] = UTF(self.tables["GTOC"]['Fdata'][0]).table
114
+ if "Gdata" in self.tables["GTOC"]:
115
+ self.tables["GTOC"]['Gdata'][0] = UTF(self.tables["GTOC"]['Gdata'][0]).table
116
+ elif key == "HgtocOffset":
117
+ if value[0]:
118
+ self.stream.seek(value[0], 0)
119
+ self.tables["HGTOC"] = _TOC(self.stream.read(self.tables['CPK']["HgtocSize"][0])).table
120
+ elif key == "EtocOffset":
121
+ if value[0]:
122
+ self.stream.seek(value[0], 0)
123
+ self.tables["ETOC"] = _TOC(self.stream.read(self.tables['CPK']["EtocSize"][0])).table
124
+
125
+ @property
126
+ def mode(self):
127
+ """Get the current mode of the CPK archive. [0,1,2,3]
128
+
129
+ See also CPKBuilder"""
130
+ TOC, ITOC, GTOC = 'TOC' in self.tables, 'ITOC' in self.tables, 'GTOC' in self.tables
131
+ if TOC and ITOC and GTOC:
132
+ return 3
133
+ elif TOC and ITOC:
134
+ return 2
135
+ elif TOC:
136
+ return 1
137
+ elif ITOC:
138
+ return 0
139
+ raise ValueError("Unknown CPK mode.")
140
+
141
+ @property
142
+ def files(self) -> Generator[PackedFile, None, None]:
143
+ """Creates a generator for all files in the CPK archive as PackedFile."""
144
+ if "TOC" in self.tables:
145
+ toctable = self.tables['TOC']
146
+ rel_off = 0x800
147
+ for i in range(len(toctable['FileName'])):
148
+ dirname = toctable["DirName"][i%len(toctable["DirName"])]
149
+ filename = toctable['FileName'][i]
150
+ if len(filename) >= 255:
151
+ filename = filename[:250] + "_" + str(i) # 250 because i might be 4 digits long.
152
+ if toctable['ExtractSize'][i] > toctable['FileSize'][i]:
153
+ self.stream.seek(rel_off+toctable["FileOffset"][i], 0)
154
+ yield PackedFile(self.stream, os.path.join(dirname,filename), self.stream.tell(), toctable['FileSize'][i], compressed=True)
155
+ else:
156
+ self.stream.seek(rel_off+toctable["FileOffset"][i], 0)
157
+ yield PackedFile(self.stream, os.path.join(dirname,filename), self.stream.tell(), toctable['FileSize'][i])
158
+ elif "ITOC" in self.tables:
159
+ toctableL = self.tables["ITOC"]['DataL'][0]
160
+ toctableH = self.tables["ITOC"]['DataH'][0]
161
+ align = self.tables['CPK']["Align"][0]
162
+ offset = self.tables["CPK"]["ContentOffset"][0]
163
+ files = self.tables["CPK"]["Files"][0]
164
+ self.stream.seek(offset, 0)
165
+ for i in sorted(toctableH['ID']+toctableL['ID']):
166
+ if i in toctableH['ID']:
167
+ idx = toctableH['ID'].index(i)
168
+ if toctableH['ExtractSize'][idx] > toctableH['FileSize'][idx]:
169
+ yield PackedFile(self.stream, str(i), self.stream.tell(), toctableH['FileSize'][idx], compressed=True)
170
+ else:
171
+ yield PackedFile(self.stream, str(i), self.stream.tell(), toctableH['FileSize'][idx])
172
+ if toctableH['FileSize'][idx] % align != 0:
173
+ seek_size = (align - toctableH['FileSize'][idx] % align)
174
+ self.stream.seek(seek_size, 1)
175
+ elif i in toctableL['ID']:
176
+ idx = toctableL['ID'].index(i)
177
+ if toctableL['ExtractSize'][idx] > toctableL['FileSize'][idx]:
178
+ yield PackedFile(self.stream, str(i), self.stream.tell(), toctableL['FileSize'][idx], compressed=True)
179
+ else:
180
+ yield PackedFile(self.stream, str(i), self.stream.tell(), toctableL['FileSize'][idx])
181
+ if toctableL['FileSize'][idx] % align != 0:
182
+ seek_size = (align - toctableL['FileSize'][idx] % align)
183
+ self.stream.seek(seek_size, 1)
184
+ class CPKBuilder:
185
+ """ Use this class to build semi-custom CPK archives. """
186
+ mode: int
187
+ # CPK mode dictates (at least from what I saw) the use of filenames in TOC or the use of
188
+ # ITOC without any filenames (Use of ID's only, will be sorted).
189
+ # CPK mode of 0 = Use of ITOC only, CPK mode = 1, use of TOC, ITOC and optionally ETOC?
190
+ Tver: str
191
+ # Seems to be CPKMaker/CPKDLL version, I will put in one of the few ones I found as default.
192
+ # I am not sure if this affects the modding these files.
193
+ # However, you can change it.
194
+ dirname: str
195
+ itoc_size: int
196
+ encrypt: bool
197
+ encoding: str
198
+ fileslen: int
199
+ ITOCdata: bytearray
200
+ TOCdata: bytearray
201
+ CPKdata: bytearray
202
+ ContentSize: int
203
+ EnabledDataSize: int
204
+ EnabledPackedSize: int
205
+ outfile: BinaryIO
206
+ init_toc_len: int # This is a bit of a redundancy, but some CPK's need it.
207
+
208
+ in_files : list[tuple[str, str, bool]] # (source path, dest filename, compress or not)
209
+ os_files : list[tuple[str, bool]] # (os path, temp or not)
210
+ files: list[tuple[str, int, int]] # (filename, file size, compressed file size).
211
+
212
+ progress_cb : callable # Progress callback taking (task name, current, total)
213
+
214
+ def __init__(self, mode: int = 1, Tver: str = None, encrypt: bool = False, encoding: str = "utf-8", progress_cb : callable = None) -> None:
215
+ """Setup CPK file building
216
+
217
+ Args:
218
+ mode (int, optional): CPK mode. 0: ID Only (ITOC), 1: Name Only (TOC), 2: Name + ID (ITOC + TOC), 3: Name + ID + GTOC (GTOC). Defaults to 1.
219
+ Tver (str, optional): CPK version. Defaults to None.
220
+ encrypt (bool, optional): Enable encryption. Defaults to False.
221
+ encoding (str, optional): Filename encoding. Defaults to "utf-8".
222
+ progress_cb (callable, optional): Progress callback taking (task name, current, total). Defaults to None.
223
+ """
224
+ self.progress_cb = progress_cb
225
+ if not self.progress_cb:
226
+ self.progress_cb = lambda task_name, current, total: None
227
+ self.mode = mode
228
+ if not Tver:
229
+ # Some default ones I found with the matching CpkMode, hope they are good enough for all cases.
230
+ if self.mode == 0:
231
+ self.Tver = 'CPKMC2.18.04, DLL2.78.04'
232
+ elif self.mode == 1:
233
+ self.Tver = 'CPKMC2.45.00, DLL3.15.00'
234
+ elif self.mode == 2:
235
+ self.Tver = 'CPKMC2.49.32, DLL3.24.00'
236
+ elif self.mode == 3:
237
+ self.Tver = 'CPKFBSTD1.49.35, DLL3.24.00'
238
+ else:
239
+ raise ValueError("Unknown CpkMode.")
240
+ else:
241
+ self.Tver = Tver
242
+ if self.mode not in [0, 1, 2, 3]:
243
+ raise ValueError("Unknown CpkMode.")
244
+
245
+ self.encrypt = encrypt
246
+ self.encoding = encoding
247
+ self.EnabledDataSize = 0
248
+ self.EnabledPackedSize = 0
249
+ self.ContentSize = 0
250
+ self.in_files = []
251
+ self.os_files = []
252
+
253
+ def add_file(self, src : str, dst : str = None, compress=False):
254
+ """Add a file to the bundle.
255
+
256
+ Args:
257
+ src (str): The source file path.
258
+ dst (str): The destination full file name (containing directory). Can be None in ITOC Mode. Defaults to None.
259
+ compress (bool, optional): Whether to compress the file. Defaults to False.
260
+
261
+ NOTE:
262
+ - In ITOC-related mode, the insertion order determines the final integer ID of the files.
263
+ """
264
+ if not dst and self.mode != 0:
265
+ raise ValueError("Destination filename must be specified in non-ITOC mode.")
266
+
267
+ self.in_files.append((src, dst, compress))
268
+
269
+ def _writetofile(self, header) -> None:
270
+ self.outfile.write(header)
271
+ for i, ((path, _), (filename, file_size, pack_size)) in enumerate(zip(self.os_files, self.files)):
272
+ src = open(path, 'rb').read()
273
+ self.outfile.write(src)
274
+ self.outfile.write(bytes(0x800 - pack_size % 0x800))
275
+ self.progress_cb("Write %s" % os.path.basename(filename), i + 1, len(self.files))
276
+
277
+ def _populate_files(self, threads : int = 1):
278
+ self.files = []
279
+ for src, dst, compress in self.in_files:
280
+ if compress:
281
+ tmp = NamedTemporaryFile(delete=False)
282
+ self.os_files.append((tmp.name, True))
283
+ else:
284
+ self.os_files.append((src, False))
285
+ with ThreadPoolExecutor(max_workers=threads) as exec:
286
+ futures = []
287
+ for (src, _, _), (dst, compress) in zip(self.in_files,self.os_files):
288
+ if compress:
289
+ _crilayla_compress_to_file(src, dst)
290
+ # futures.append(exec.submit(_crilayla_compress_to_file, src, dst))
291
+ for i, fut in enumerate(as_completed(futures)):
292
+ fut.result()
293
+ self.progress_cb("Compress %s" % os.path.basename(src), i + 1, len(futures))
294
+ for (src, filename, _) , (dst, _) in zip(self.in_files,self.os_files):
295
+ file_size = os.stat(src).st_size
296
+ pack_size = os.stat(dst).st_size
297
+ self.files.append((filename, file_size, pack_size))
298
+
299
+ def _cleanup_files(self):
300
+ self.files = []
301
+ for path, is_temp in self.os_files:
302
+ if not is_temp:
303
+ continue
304
+ try:
305
+ os.unlink(path)
306
+ except:
307
+ pass
308
+ self.os_files = []
309
+
310
+ def save(self, outfile : str | BinaryIO, threads : int = 1):
311
+ """Build and save the bundle into a file
312
+
313
+
314
+ Args:
315
+ outfile (str | BinaryIO): The output file path or a writable binary stream.
316
+ threads (int, optional): The number of threads to use for file compression. Defaults to 1.
317
+
318
+ NOTE:
319
+ - Temporary files may be created during the process if compression is used.
320
+ """
321
+ assert self.in_files, "cannot save empty bundle"
322
+ self.outfile = outfile
323
+ if type(outfile) == str:
324
+ self.outfile = open(outfile, "wb")
325
+ self._populate_files(threads)
326
+ if self.encrypt:
327
+ encflag = 0
328
+ else:
329
+ encflag = 0xFF
330
+ data = None
331
+ if self.mode == 3:
332
+ self.TOCdata = self._generate_TOC()
333
+ self.TOCdata = bytearray(CPKChunkHeader.pack(b'TOC ', encflag, len(self.TOCdata), 0)) + self.TOCdata
334
+ self.TOCdata = self.TOCdata.ljust(len(self.TOCdata) + (0x800 - len(self.TOCdata) % 0x800), b'\x00')
335
+ assert self.init_toc_len == len(self.TOCdata)
336
+ self.GTOCdata = self._generate_GTOC()
337
+ self.GTOCdata = bytearray(CPKChunkHeader.pack(b'GTOC', encflag, len(self.GTOCdata), 0)) + self.GTOCdata
338
+ self.GTOCdata = self.GTOCdata.ljust(len(self.GTOCdata) + (0x800 - len(self.GTOCdata) % 0x800), b'\x00')
339
+ self.CPKdata = self._generate_CPK()
340
+ self.CPKdata = bytearray(CPKChunkHeader.pack(b'CPK ', encflag, len(self.CPKdata), 0)) + self.CPKdata
341
+ data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.TOCdata + self.GTOCdata
342
+ elif self.mode == 2:
343
+ self.TOCdata = self._generate_TOC()
344
+ self.TOCdata = bytearray(CPKChunkHeader.pack(b'TOC ', encflag, len(self.TOCdata), 0)) + self.TOCdata
345
+ self.TOCdata = self.TOCdata.ljust(len(self.TOCdata) + (0x800 - len(self.TOCdata) % 0x800), b'\x00')
346
+ assert self.init_toc_len == len(self.TOCdata)
347
+ self.ITOCdata = self._generate_ITOC()
348
+ self.ITOCdata = bytearray(CPKChunkHeader.pack(b'ITOC', encflag, len(self.ITOCdata), 0)) + self.ITOCdata
349
+ self.ITOCdata = self.ITOCdata.ljust(len(self.ITOCdata) + (0x800 - len(self.ITOCdata) % 0x800), b'\x00')
350
+ self.CPKdata = self._generate_CPK()
351
+ self.CPKdata = bytearray(CPKChunkHeader.pack(b'CPK ', encflag, len(self.CPKdata), 0)) + self.CPKdata
352
+ data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.TOCdata + self.ITOCdata
353
+ elif self.mode == 1:
354
+ self.TOCdata = self._generate_TOC()
355
+ self.TOCdata = bytearray(CPKChunkHeader.pack(b'TOC ', encflag, len(self.TOCdata), 0)) + self.TOCdata
356
+ self.TOCdata = self.TOCdata.ljust(len(self.TOCdata) + (0x800 - len(self.TOCdata) % 0x800), b'\x00')
357
+ assert self.init_toc_len == len(self.TOCdata)
358
+ self.CPKdata = self._generate_CPK()
359
+ self.CPKdata = bytearray(CPKChunkHeader.pack(b'CPK ', encflag, len(self.CPKdata), 0)) + self.CPKdata
360
+ data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.TOCdata
361
+ elif self.mode == 0:
362
+ self.ITOCdata = self._generate_ITOC()
363
+ self.ITOCdata = bytearray(CPKChunkHeader.pack(b'ITOC', encflag, len(self.ITOCdata), 0)) + self.ITOCdata
364
+ self.ITOCdata = self.ITOCdata.ljust(len(self.ITOCdata) + (0x800 - len(self.ITOCdata) % 0x800), b'\x00')
365
+ self.CPKdata = self._generate_CPK()
366
+ self.CPKdata = bytearray(CPKChunkHeader.pack(b'CPK ', encflag, len(self.CPKdata), 0)) + self.CPKdata
367
+ data = self.CPKdata.ljust(len(self.CPKdata) + (0x800 - len(self.CPKdata) % 0x800) - 6, b'\x00') + bytearray(b"(c)CRI") + self.ITOCdata
368
+ self._writetofile(data)
369
+ self._cleanup_files()
370
+ if type(outfile) == str:
371
+ self.outfile.close()
372
+
373
+ def _generate_GTOC(self) -> bytearray:
374
+ # NOTE: Practically useless
375
+ # I have no idea why are those numbers here.
376
+ Gdata = [
377
+ {
378
+ "Gname": (UTFTypeValues.string, ""),
379
+ "Child": (UTFTypeValues.int, -1),
380
+ "Next": (UTFTypeValues.int, 0)
381
+ },
382
+ {
383
+ "Gname": (UTFTypeValues.string, "(none)"),
384
+ "Child": (UTFTypeValues.int, 0),
385
+ "Next": (UTFTypeValues.int, 0)
386
+ }
387
+ ]
388
+ Fdata = [
389
+ {
390
+ "Next": (UTFTypeValues.int, -1),
391
+ "Child": (UTFTypeValues.int, -1),
392
+ "SortFlink": (UTFTypeValues.int, 2),
393
+ "Aindex": (UTFTypeValues.ushort, 0)
394
+ },
395
+ {
396
+ "Next": (UTFTypeValues.int, 2),
397
+ "Child": (UTFTypeValues.int, 0),
398
+ "SortFlink": (UTFTypeValues.int, 1),
399
+ "Aindex": (UTFTypeValues.ushort, 0)
400
+ },
401
+ {
402
+ "Next": (UTFTypeValues.int, 0),
403
+ "Child": (UTFTypeValues.int, 1),
404
+ "SortFlink": (UTFTypeValues.int, 2),
405
+ "Aindex": (UTFTypeValues.ushort, 0)
406
+ }
407
+ ]
408
+ Attrdata = [
409
+ {
410
+ "Aname": (UTFTypeValues.string, ""),
411
+ "Align": (UTFTypeValues.ushort, 0x800),
412
+ "Files": (UTFTypeValues.uint, 0),
413
+ "FileSize": (UTFTypeValues.uint, 0)
414
+ }
415
+ ]
416
+ payload = [
417
+ {
418
+ "Glink": (UTFTypeValues.uint, 2),
419
+ "Flink": (UTFTypeValues.uint, 3),
420
+ "Attr" : (UTFTypeValues.uint, 1),
421
+ "Gdata": (UTFTypeValues.bytes, UTFBuilder(Gdata, encrypt=False, encoding=self.encoding, table_name="CpkGtocGlink").bytes()),
422
+ "Fdata": (UTFTypeValues.bytes, UTFBuilder(Fdata, encrypt=False, encoding=self.encoding, table_name="CpkGtocFlink").bytes()),
423
+ "Attrdata": (UTFTypeValues.bytes, UTFBuilder(Attrdata, encrypt=False, encoding=self.encoding, table_name="CpkGtocAttr").bytes()),
424
+ }
425
+ ]
426
+ return UTFBuilder(payload, encrypt=self.encrypt, encoding=self.encoding, table_name="CpkGtocInfo").bytes()
427
+
428
+ def _generate_TOC(self) -> bytearray:
429
+ payload = []
430
+ temp = []
431
+ count = 0
432
+ lent = 0
433
+ switch = False
434
+ sf = set()
435
+ sd = set()
436
+ for filename, store_size, full_size in self.files:
437
+ # Dirname management.
438
+ # Must be POSIX path
439
+ dirname = os.path.dirname(filename)
440
+ if dirname not in sd:
441
+ switch = True
442
+ lent += len(dirname) + 1
443
+ sd.update({dirname})
444
+
445
+ # Filename management.
446
+ flname = os.path.basename(filename)
447
+ if flname not in sf:
448
+ lent += len(flname) + 1
449
+ sf.update({flname})
450
+ count += 1
451
+
452
+ # This estimates how large the TOC table size is.
453
+ if switch and len(sd) != 1:
454
+ lent = (lent + (4 + 4 + 4 + 4 + 8 + 4) * count + 0x47 + 0x51) # 0x47 is header len when there are mutiple dirs.
455
+ else:
456
+ lent = (lent + (4 + 4 + 4 + 8 + 4) * count + 0x4B + 0x51) # 0x4B is header len when there is only one dir.
457
+ if lent % 8 != 0:
458
+ lent = 8 + (lent - 8) + (8 - (lent - 8) % 8)
459
+ lent += 0x10
460
+ lent = lent + (0x800 - lent % 0x800)
461
+ # init_toc_len will also be the first file offset.
462
+ # Used to assert that the estimated TOC length is equal to the actual length, just in case the estimating went wrong.
463
+ self.init_toc_len = lent
464
+
465
+ self.fileslen = count
466
+ count = 0
467
+ for filename, store_size, full_size in self.files:
468
+ sz = store_size
469
+ fz = full_size
470
+ if sz > 0xFFFFFFFF:
471
+ raise OverflowError("4GBs is the max size of a single file that can be bundled in a CPK archive of mode 1.")
472
+ self.EnabledDataSize += fz
473
+ self.EnabledPackedSize += sz
474
+ if sz % 0x800 != 0:
475
+ self.ContentSize += sz + (0x800 - sz % 0x800)
476
+ else:
477
+ self.ContentSize += sz
478
+ dirname = os.path.dirname(filename)
479
+ payload.append(
480
+ {
481
+ "DirName": (UTFTypeValues.string, dirname),
482
+ "FileName": (UTFTypeValues.string, os.path.basename(filename)),
483
+ "FileSize": (UTFTypeValues.uint, sz),
484
+ "ExtractSize": (UTFTypeValues.uint, fz),
485
+ "FileOffset": (UTFTypeValues.ullong, lent),
486
+ "ID": (UTFTypeValues.uint, count),
487
+ "UserString": (UTFTypeValues.string, "<NULL>")
488
+ }
489
+ )
490
+ count += 1
491
+ if sz % 0x800 != 0:
492
+ lent += sz + (0x800 - sz % 0x800)
493
+ else:
494
+ lent += sz
495
+ return UTFBuilder(payload, encrypt=self.encrypt, encoding=self.encoding, table_name="CpkTocInfo").bytes()
496
+
497
+ def _generate_ITOC(self) -> bytearray:
498
+ if self.mode == 2:
499
+ payload = []
500
+ for i, (filename, store_size, full_size) in enumerate(self.files):
501
+ payload.append(
502
+ {
503
+ "ID": (UTFTypeValues.int, i),
504
+ "TocIndex": (UTFTypeValues.int, i)
505
+ }
506
+ )
507
+ return UTFBuilder(payload, encrypt=self.encrypt, encoding=self.encoding, table_name="CpkExtendId").bytes()
508
+ else:
509
+ assert len(self.files) < 65535, "ITOC requires less than 65535 files."
510
+ self.fileslen = len(self.files)
511
+ datal = []
512
+ datah = []
513
+ for i, (filename, store_size, full_size) in enumerate(self.files):
514
+ sz = store_size
515
+ fz = full_size
516
+ self.EnabledDataSize += fz
517
+ self.EnabledPackedSize += sz
518
+ if sz % 0x800 != 0:
519
+ self.ContentSize += sz + (0x800 - sz % 0x800)
520
+ else:
521
+ self.ContentSize += sz
522
+ if sz > 0xFFFF:
523
+ dicth = {
524
+ "ID": (UTFTypeValues.ushort, i),
525
+ "FileSize": (UTFTypeValues.uint, sz),
526
+ "ExtractSize": (UTFTypeValues.uint, sz)
527
+ }
528
+ datah.append(dicth)
529
+ else:
530
+ dictl = {
531
+ "ID": (UTFTypeValues.ushort, i),
532
+ "FileSize": (UTFTypeValues.ushort, sz),
533
+ "ExtractSize": (UTFTypeValues.ushort, sz)
534
+ }
535
+ datal.append(dictl)
536
+ datallen = len(datal)
537
+ datahlen = len(datah)
538
+ if len(datal) == 0:
539
+ datal.append({"ID": (UTFTypeValues.ushort, 0), "FileSize": (UTFTypeValues.ushort, 0), "ExtractSize": (UTFTypeValues.ushort, 0)})
540
+ elif len(datah) == 0:
541
+ datah.append({"ID": (UTFTypeValues.uint, 0), "FileSize": (UTFTypeValues.uint, 0), "ExtractSize": (UTFTypeValues.uint, 0)})
542
+ payload = [
543
+ {
544
+ "FilesL" : (UTFTypeValues.uint, datallen),
545
+ "FilesH" : (UTFTypeValues.uint, datahlen),
546
+ "DataL" : (UTFTypeValues.bytes, UTFBuilder(datal, table_name="CpkItocL", encrypt=False, encoding=self.encoding).bytes()),
547
+ "DataH" : (UTFTypeValues.bytes, UTFBuilder(datah, table_name="CpkItocH", encrypt=False, encoding=self.encoding).bytes())
548
+ }
549
+ ]
550
+ return UTFBuilder(payload, table_name="CpkItocInfo", encrypt=self.encrypt, encoding=self.encoding).bytes()
551
+
552
+ def _generate_CPK(self) -> bytearray:
553
+ if self.mode == 3:
554
+ ContentOffset = (0x800+len(self.TOCdata)+len(self.GTOCdata))
555
+ CpkHeader = [
556
+ {
557
+ "UpdateDateTime": (UTFTypeValues.ullong, 0),
558
+ "ContentOffset": (UTFTypeValues.ullong, ContentOffset),
559
+ "ContentSize": (UTFTypeValues.ullong, self.ContentSize),
560
+ "TocOffset": (UTFTypeValues.ullong, 0x800),
561
+ "TocSize": (UTFTypeValues.ullong, len(self.TOCdata)),
562
+ "EtocOffset": (UTFTypeValues.ullong, None),
563
+ "EtocSize": (UTFTypeValues.ullong, None),
564
+ "GtocOffset": (UTFTypeValues.ullong, 0x800+len(self.TOCdata)),
565
+ "GtocSize": (UTFTypeValues.ullong, len(self.GTOCdata)),
566
+ "EnabledPackedSize": (UTFTypeValues.ullong, self.EnabledPackedSize),
567
+ "EnabledDataSize": (UTFTypeValues.ullong, self.EnabledDataSize),
568
+ "Files": (UTFTypeValues.uint, self.fileslen),
569
+ "Groups": (UTFTypeValues.uint, 0),
570
+ "Attrs": (UTFTypeValues.uint, 0),
571
+ "Version": (UTFTypeValues.ushort, 7),
572
+ "Revision": (UTFTypeValues.ushort, 14),
573
+ "Align": (UTFTypeValues.ushort, 0x800),
574
+ "Sorted": (UTFTypeValues.ushort, 1),
575
+ "EnableFileName": (UTFTypeValues.ushort, 1),
576
+ "CpkMode": (UTFTypeValues.uint, self.mode),
577
+ "Tvers": (UTFTypeValues.string, self.Tver),
578
+ "Codec": (UTFTypeValues.uint, 0),
579
+ "DpkItoc": (UTFTypeValues.uint, 0),
580
+ "EnableTocCrc": (UTFTypeValues.ushort, None),
581
+ "EnableFileCrc": (UTFTypeValues.ushort, None),
582
+ "CrcMode": (UTFTypeValues.uint, None),
583
+ "CrcTable": (UTFTypeValues.bytes, b''),
584
+ "FileSize": (UTFTypeValues.ullong, None),
585
+ "TocCrc": (UTFTypeValues.uint, None),
586
+ "HtocOffset": (UTFTypeValues.ullong, None),
587
+ "HtocSize": (UTFTypeValues.ullong, None),
588
+ "ItocOffset": (UTFTypeValues.ullong, None),
589
+ "ItocSize": (UTFTypeValues.ullong, None),
590
+ "ItocCrc": (UTFTypeValues.uint, None),
591
+ "GtocCrc": (UTFTypeValues.uint, None),
592
+ "HgtocOffset": (UTFTypeValues.ullong, None),
593
+ "HgtocSize": (UTFTypeValues.ullong, None),
594
+ "TotalDataSize": (UTFTypeValues.ullong, None),
595
+ "Tocs": (UTFTypeValues.uint, None),
596
+ "TotalFiles": (UTFTypeValues.uint, None),
597
+ "Directories": (UTFTypeValues.uint, None),
598
+ "Updates": (UTFTypeValues.uint, None),
599
+ "EID": (UTFTypeValues.ushort, None),
600
+ "Comment": (UTFTypeValues.string, '<NULL>'),
601
+ }
602
+ ]
603
+ elif self.mode == 2:
604
+ ContentOffset = 0x800+len(self.TOCdata)+len(self.ITOCdata)
605
+ CpkHeader = [
606
+ {
607
+ "UpdateDateTime": (UTFTypeValues.ullong, 0),
608
+ "ContentOffset": (UTFTypeValues.ullong, ContentOffset),
609
+ "ContentSize": (UTFTypeValues.ullong, self.ContentSize),
610
+ "TocOffset": (UTFTypeValues.ullong, 0x800),
611
+ "TocSize": (UTFTypeValues.ullong, len(self.TOCdata)),
612
+ "EtocOffset": (UTFTypeValues.ullong, None),
613
+ "EtocSize": (UTFTypeValues.ullong, None),
614
+ "ItocOffset": (UTFTypeValues.ullong, 0x800+len(self.TOCdata)),
615
+ "ItocSize": (UTFTypeValues.ullong, len(self.ITOCdata)),
616
+ "EnabledPackedSize": (UTFTypeValues.ullong, self.EnabledPackedSize),
617
+ "EnabledDataSize": (UTFTypeValues.ullong, self.EnabledDataSize),
618
+ "Files": (UTFTypeValues.uint, self.fileslen),
619
+ "Groups": (UTFTypeValues.uint, 0),
620
+ "Attrs": (UTFTypeValues.uint, 0),
621
+ "Version": (UTFTypeValues.ushort, 7),
622
+ "Revision": (UTFTypeValues.ushort, 14),
623
+ "Align": (UTFTypeValues.ushort, 0x800),
624
+ "Sorted": (UTFTypeValues.ushort, 1),
625
+ "EnableFileName": (UTFTypeValues.ushort, 1),
626
+ "EID": (UTFTypeValues.ushort, None),
627
+ "CpkMode": (UTFTypeValues.uint, self.mode),
628
+ "Tvers": (UTFTypeValues.string, self.Tver),
629
+ "Codec": (UTFTypeValues.uint, 0),
630
+ "DpkItoc": (UTFTypeValues.uint, 0),
631
+ "EnableTocCrc": (UTFTypeValues.ushort, None),
632
+ "EnableFileCrc": (UTFTypeValues.ushort, None),
633
+ "CrcMode": (UTFTypeValues.uint, None),
634
+ "CrcTable": (UTFTypeValues.bytes, b''),
635
+ "FileSize": (UTFTypeValues.ullong, None),
636
+ "TocCrc": (UTFTypeValues.uint, None),
637
+ "HtocOffset": (UTFTypeValues.ullong, None),
638
+ "HtocSize": (UTFTypeValues.ullong, None),
639
+ "ItocCrc": (UTFTypeValues.uint, None),
640
+ "GtocOffset": (UTFTypeValues.ullong, None),
641
+ "GtocSize": (UTFTypeValues.ullong, None),
642
+ "HgtocOffset": (UTFTypeValues.ullong, None),
643
+ "HgtocSize": (UTFTypeValues.ullong, None),
644
+ "TotalDataSize": (UTFTypeValues.ullong, None),
645
+ "Tocs": (UTFTypeValues.uint, None),
646
+ "TotalFiles": (UTFTypeValues.uint, None),
647
+ "Directories": (UTFTypeValues.uint, None),
648
+ "Updates": (UTFTypeValues.uint, None),
649
+ "Comment": (UTFTypeValues.string, '<NULL>'),
650
+ }
651
+ ]
652
+ elif self.mode == 1:
653
+ ContentOffset = 0x800 + len(self.TOCdata)
654
+ CpkHeader = [
655
+ {
656
+ "UpdateDateTime": (UTFTypeValues.ullong, 0),
657
+ "FileSize": (UTFTypeValues.ullong, None),
658
+ "ContentOffset": (UTFTypeValues.ullong, ContentOffset),
659
+ "ContentSize": (UTFTypeValues.ullong, self.ContentSize),
660
+ "TocOffset": (UTFTypeValues.ullong, 0x800),
661
+ "TocSize": (UTFTypeValues.ullong, len(self.TOCdata)),
662
+ "TocCrc": (UTFTypeValues.uint, None),
663
+ "EtocOffset": (UTFTypeValues.ullong, None),
664
+ "EtocSize": (UTFTypeValues.ullong, None),
665
+ "ItocOffset": (UTFTypeValues.ullong, None),
666
+ "ItocSize": (UTFTypeValues.ullong, None),
667
+ "ItocCrc": (UTFTypeValues.uint, None),
668
+ "GtocOffset": (UTFTypeValues.ullong, None),
669
+ "GtocSize": (UTFTypeValues.ullong, None),
670
+ "GtocCrc": (UTFTypeValues.uint, None),
671
+ "EnabledPackedSize": (UTFTypeValues.ullong, self.EnabledPackedSize),
672
+ "EnabledDataSize": (UTFTypeValues.ullong, self.EnabledDataSize),
673
+ "TotalDataSize": (UTFTypeValues.ullong, None),
674
+ "Tocs": (UTFTypeValues.uint, None),
675
+ "Files": (UTFTypeValues.uint, self.fileslen),
676
+ "Groups": (UTFTypeValues.uint, 0),
677
+ "Attrs": (UTFTypeValues.uint, 0),
678
+ "TotalFiles": (UTFTypeValues.uint, None),
679
+ "Directories": (UTFTypeValues.uint, None),
680
+ "Updates": (UTFTypeValues.uint, None),
681
+ "Version": (UTFTypeValues.ushort, 7),
682
+ "Revision": (UTFTypeValues.ushort, 1),
683
+ "Align": (UTFTypeValues.ushort, 0x800),
684
+ "Sorted": (UTFTypeValues.ushort, 1),
685
+ "EID": (UTFTypeValues.ushort, None),
686
+ "CpkMode": (UTFTypeValues.uint, self.mode),
687
+ "Tvers": (UTFTypeValues.string, self.Tver),
688
+ "Comment": (UTFTypeValues.string, '<NULL>'),
689
+ "Codec": (UTFTypeValues.uint, 0),
690
+ "DpkItoc": (UTFTypeValues.uint, 0),
691
+ "EnableFileName": (UTFTypeValues.ushort, 1),
692
+ "EnableTocCrc": (UTFTypeValues.ushort, None),
693
+ "EnableFileCrc": (UTFTypeValues.ushort, None),
694
+ "CrcMode": (UTFTypeValues.uint, None),
695
+ "CrcTable": (UTFTypeValues.bytes, b''),
696
+ "HtocOffset": (UTFTypeValues.ullong, None),
697
+ "HtocSize": (UTFTypeValues.ullong, None),
698
+ "HgtocOffset": (UTFTypeValues.ullong, None),
699
+ "HgtocSize": (UTFTypeValues.ullong, None),
700
+ }
701
+ ]
702
+ elif self.mode == 0:
703
+ CpkHeader = [
704
+ {
705
+ "UpdateDateTime": (UTFTypeValues.ullong, 0),
706
+ "ContentOffset": (UTFTypeValues.ullong, 0x800+len(self.ITOCdata)),
707
+ "ContentSize": (UTFTypeValues.ullong, self.ContentSize),
708
+ "ItocOffset": (UTFTypeValues.ullong, 0x800),
709
+ "ItocSize": (UTFTypeValues.ullong, len(self.ITOCdata)),
710
+ "EnabledPackedSize": (UTFTypeValues.ullong, self.EnabledPackedSize),
711
+ "EnabledDataSize": (UTFTypeValues.ullong, self.EnabledDataSize),
712
+ "Files": (UTFTypeValues.uint, self.fileslen),
713
+ "Groups": (UTFTypeValues.uint, 0),
714
+ "Attrs": (UTFTypeValues.uint, 0),
715
+ "Version": (UTFTypeValues.ushort, 7), # 7?
716
+ "Revision": (UTFTypeValues.ushort, 0),
717
+ "Align": (UTFTypeValues.ushort, 0x800),
718
+ "Sorted": (UTFTypeValues.ushort, 0),
719
+ "EID": (UTFTypeValues.ushort, None),
720
+ "CpkMode": (UTFTypeValues.uint, self.mode),
721
+ "Tvers": (UTFTypeValues.string, self.Tver),
722
+ "Codec": (UTFTypeValues.uint, 0),
723
+ "DpkItoc": (UTFTypeValues.uint, 0),
724
+ "FileSize": (UTFTypeValues.ullong, None),
725
+ "TocOffset": (UTFTypeValues.ullong, None),
726
+ "TocSize": (UTFTypeValues.ullong, None),
727
+ "TocCrc": (UTFTypeValues.uint, None),
728
+ "EtocOffset": (UTFTypeValues.ullong, None),
729
+ "EtocSize": (UTFTypeValues.ullong, None),
730
+ "ItocCrc": (UTFTypeValues.uint, None),
731
+ "GtocOffset": (UTFTypeValues.ullong, None),
732
+ "GtocSize": (UTFTypeValues.ullong, None),
733
+ "GtocCrc": (UTFTypeValues.uint, None),
734
+ "TotalDataSize": (UTFTypeValues.ullong, None),
735
+ "Tocs": (UTFTypeValues.uint, None),
736
+ "TotalFiles": (UTFTypeValues.uint, None),
737
+ "Directories": (UTFTypeValues.uint, None),
738
+ "Updates": (UTFTypeValues.uint, None),
739
+ "Comment": (UTFTypeValues.string, '<NULL>'),
740
+ }
741
+ ]
742
+ return UTFBuilder(CpkHeader, encrypt=self.encrypt, encoding=self.encoding, table_name="CpkHeader").bytes()
743
+