legendary/legendary/models/json_manifest.py
derrod e00e534f2c First public test version of legendary
Unfortunately the history from before this commit is lost to time. And
that's probably for the best.
2020-04-14 15:40:41 +02:00

160 lines
4.9 KiB
Python

#!/usr/bin/env python
# coding: utf-8
import json
import struct
from copy import deepcopy
from legendary.models.manifest import Manifest, ManifestMeta, CDL, ChunkPart, ChunkInfo, FML, FileManifest
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
"""
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 = _tmp.pop('CustomFields', dict())
if _tmp.keys():
print(f'Did not read JSON keys: {_tmp.keys()}!')
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
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 = csl.pop(guid) # todo; figure out if we have to decode this somehow
_ci.group_num = blob_to_num(dgl.pop(guid))
_ci.window_size = 1024*1024
_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', '')
_fm.hash = blob_to_num(_fmj.pop('FileHash')).to_bytes(160//8, 'little')
_fm.flags = int(_fmj.pop('bIsUnixExecutable', False)) << 2
_fm.file_size = 0
_fm.chunk_parts = []
_fm.install_tags = _fmj.pop('InstallTags', list())
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'))
_fm.file_size += _cp.size
if _cpj:
print(f'Non-read ChunkPart keys: {_cpj.keys()}')
_fm.chunk_parts.append(_cp)
if _fmj:
print(f'Non-read FileManifest keys: {_fmj.keys()}')
_fml.elements.append(_fm)
return _fml