2021-03-15 23:48:28 +13:00
|
|
|
# coding: utf-8
|
|
|
|
|
|
|
|
import json
|
|
|
|
import struct
|
|
|
|
from copy import deepcopy
|
|
|
|
|
2021-03-18 01:47:58 +13:00
|
|
|
from custom_legendary.models.manifest import (
|
2021-03-15 23:48:28 +13:00
|
|
|
Manifest, ManifestMeta, CDL, ChunkPart, ChunkInfo, FML, FileManifest, CustomFields
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def blob_to_num(in_str):
|
|
|
|
"""
|
|
|
|
The JSON manifest use a rather strange format for storing numbers.
|
|
|
|
|
|
|
|
It's essentially %03d for each char concatenated to a string.
|
|
|
|
...instead of just putting the fucking number in the JSON...
|
|
|
|
|
|
|
|
Also it's still little endian so we have to bitshift it.
|
|
|
|
|
|
|
|
"""
|
|
|
|
num = 0
|
|
|
|
shift = 0
|
|
|
|
for i in range(0, len(in_str), 3):
|
|
|
|
num += (int(in_str[i:i + 3]) << shift)
|
|
|
|
shift += 8
|
|
|
|
return num
|
|
|
|
|
|
|
|
|
|
|
|
def guid_from_json(in_str):
|
|
|
|
return struct.unpack('>IIII', bytes.fromhex(in_str))
|
|
|
|
|
|
|
|
|
|
|
|
class JSONManifest(Manifest):
|
|
|
|
"""
|
|
|
|
Manifest-compatible reader for JSON based manifests
|
|
|
|
|
|
|
|
"""
|
2021-03-19 00:45:59 +13:00
|
|
|
|
2021-03-15 23:48:28 +13:00
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
self.json_data = None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def read_all(cls, manifest):
|
|
|
|
_m = cls.read(manifest)
|
|
|
|
_tmp = deepcopy(_m.json_data)
|
|
|
|
|
|
|
|
_m.meta = JSONManifestMeta.read(_tmp)
|
|
|
|
_m.chunk_data_list = JSONCDL.read(_tmp, manifest_version=_m.version)
|
|
|
|
_m.file_manifest_list = JSONFML.read(_tmp)
|
|
|
|
_m.custom_fields = CustomFields()
|
|
|
|
_m.custom_fields._dict = _tmp.pop('CustomFields', dict())
|
|
|
|
|
|
|
|
if _tmp.keys():
|
|
|
|
print(f'Did not read JSON keys: {_tmp.keys()}!')
|
|
|
|
|
|
|
|
# clear raw data after manifest has been loaded
|
|
|
|
_m.data = b''
|
|
|
|
_m.json_data = None
|
|
|
|
|
|
|
|
return _m
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def read(cls, manifest):
|
|
|
|
_manifest = cls()
|
|
|
|
_manifest.data = manifest
|
|
|
|
_manifest.json_data = json.loads(manifest.decode('utf-8'))
|
|
|
|
|
|
|
|
_manifest.stored_as = 0 # never compressed
|
|
|
|
_manifest.version = blob_to_num(_manifest.json_data.get('ManifestFileVersion', '013000000000'))
|
|
|
|
|
|
|
|
return _manifest
|
|
|
|
|
|
|
|
def write(self, *args, **kwargs):
|
|
|
|
# The version here only matters for the manifest header,
|
|
|
|
# the feature level in meta determines chunk folders etc.
|
|
|
|
# So all that's required for successful serialization is
|
|
|
|
# setting it to something high enough to be a binary manifest
|
|
|
|
self.version = 18
|
|
|
|
return super().write(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
class JSONManifestMeta(ManifestMeta):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def read(cls, json_data):
|
|
|
|
_meta = cls()
|
|
|
|
|
|
|
|
_meta.feature_level = blob_to_num(json_data.pop('ManifestFileVersion', '013000000000'))
|
|
|
|
_meta.is_file_data = json_data.pop('bIsFileData', False)
|
|
|
|
_meta.app_id = blob_to_num(json_data.pop('AppID', '000000000000'))
|
|
|
|
_meta.app_name = json_data.pop('AppNameString', '')
|
|
|
|
_meta.build_version = json_data.pop('BuildVersionString', '')
|
|
|
|
_meta.launch_exe = json_data.pop('LaunchExeString', '')
|
|
|
|
_meta.launch_command = json_data.pop('LaunchCommand', '')
|
|
|
|
_meta.prereq_ids = json_data.pop('PrereqIds', list())
|
|
|
|
_meta.prereq_name = json_data.pop('PrereqName', '')
|
|
|
|
_meta.prereq_path = json_data.pop('PrereqPath', '')
|
|
|
|
_meta.prereq_args = json_data.pop('PrereqArgs', '')
|
|
|
|
|
|
|
|
return _meta
|
|
|
|
|
|
|
|
|
|
|
|
class JSONCDL(CDL):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def read(cls, json_data, manifest_version=13):
|
|
|
|
_cdl = cls()
|
|
|
|
_cdl._manifest_version = manifest_version
|
|
|
|
_cdl.count = len(json_data['ChunkFilesizeList'])
|
|
|
|
|
|
|
|
cfl = json_data.pop('ChunkFilesizeList')
|
|
|
|
chl = json_data.pop('ChunkHashList')
|
|
|
|
csl = json_data.pop('ChunkShaList')
|
|
|
|
dgl = json_data.pop('DataGroupList')
|
|
|
|
_guids = list(cfl.keys())
|
|
|
|
|
|
|
|
for guid in _guids:
|
|
|
|
_ci = ChunkInfo(manifest_version=manifest_version)
|
|
|
|
_ci.guid = guid_from_json(guid)
|
|
|
|
_ci.file_size = blob_to_num(cfl.pop(guid))
|
|
|
|
_ci.hash = blob_to_num(chl.pop(guid))
|
|
|
|
_ci.sha_hash = bytes.fromhex(csl.pop(guid))
|
|
|
|
_ci.group_num = blob_to_num(dgl.pop(guid))
|
2021-03-19 00:45:59 +13:00
|
|
|
_ci.window_size = 1024 * 1024
|
2021-03-15 23:48:28 +13:00
|
|
|
_cdl.elements.append(_ci)
|
|
|
|
|
|
|
|
for _dc in (cfl, chl, csl, dgl):
|
|
|
|
if _dc:
|
|
|
|
print(f'Non-consumed CDL stuff: {_dc}')
|
|
|
|
|
|
|
|
return _cdl
|
|
|
|
|
|
|
|
|
|
|
|
class JSONFML(FML):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def read(cls, json_data):
|
|
|
|
_fml = cls()
|
|
|
|
_fml.count = len(json_data['FileManifestList'])
|
|
|
|
|
|
|
|
for _fmj in json_data.pop('FileManifestList'):
|
|
|
|
_fm = FileManifest()
|
|
|
|
_fm.filename = _fmj.pop('Filename', '')
|
2021-03-19 00:45:59 +13:00
|
|
|
_fm.hash = blob_to_num(_fmj.pop('FileHash')).to_bytes(160 // 8, 'little')
|
2021-03-15 23:48:28 +13:00
|
|
|
_fm.flags |= int(_fmj.pop('bIsReadOnly', False))
|
|
|
|
_fm.flags |= int(_fmj.pop('bIsCompressed', False)) << 1
|
|
|
|
_fm.flags |= int(_fmj.pop('bIsUnixExecutable', False)) << 2
|
|
|
|
_fm.file_size = 0
|
|
|
|
_fm.chunk_parts = []
|
|
|
|
_fm.install_tags = _fmj.pop('InstallTags', list())
|
|
|
|
|
|
|
|
_offset = 0
|
|
|
|
for _cpj in _fmj.pop('FileChunkParts'):
|
|
|
|
_cp = ChunkPart()
|
|
|
|
_cp.guid = guid_from_json(_cpj.pop('Guid'))
|
|
|
|
_cp.offset = blob_to_num(_cpj.pop('Offset'))
|
|
|
|
_cp.size = blob_to_num(_cpj.pop('Size'))
|
|
|
|
_cp.file_offset = _offset
|
|
|
|
_fm.file_size += _cp.size
|
|
|
|
if _cpj:
|
|
|
|
print(f'Non-read ChunkPart keys: {_cpj.keys()}')
|
|
|
|
_fm.chunk_parts.append(_cp)
|
|
|
|
_offset += _cp.size
|
|
|
|
|
|
|
|
if _fmj:
|
|
|
|
print(f'Non-read FileManifest keys: {_fmj.keys()}')
|
|
|
|
|
|
|
|
_fml.elements.append(_fm)
|
|
|
|
|
|
|
|
return _fml
|