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