#!/usr/bin/env python # coding: utf-8 import hashlib import logging import struct import zlib from io import BytesIO logger = logging.getLogger('Manifest') def read_fstring(bio): length = struct.unpack(' 0: s = bio.read(length - 1).decode('ascii') bio.seek(1, 1) # skip string null terminator else: # empty string, no terminators or anything s = '' return s def get_chunk_dir(version): # The lowest version I've ever seen was 12 (Unreal Tournament), but for completeness sake leave all of them in if version >= 15: return 'ChunksV4' elif version >= 6: return 'ChunksV3' elif version >= 3: return 'ChunksV2' else: return 'Chunks' class Manifest: header_magic = 0x44BEC00C def __init__(self): self.header_size = 0 self.size_compressed = 0 self.size_uncompressed = 0 self.sha_hash = '' self.stored_as = 0 self.version = 0 self.data = b'' # remainder self.meta = None self.chunk_data_list = None self.file_manifest_list = None self.custom_fields = None @property def compressed(self): return self.stored_as & 0x1 @classmethod def read_all(cls, data): _m = cls.read(data) _tmp = BytesIO(_m.data) _m.meta = ManifestMeta.read(_tmp) _m.chunk_data_list = CDL.read(_tmp, _m.version) _m.file_manifest_list = FML.read(_tmp) _m.custom_fields = CustomFields.read(_tmp) unhandled_data = _tmp.read() if unhandled_data: logger.warning(f'Did not read {len(unhandled_data)} remaining bytes in manifest! ' f'This may not be a problem.') return _m @classmethod def read(cls, data): bio = BytesIO(data) if struct.unpack(''.format( self.guid_str, self.hash, self.sha_hash.hex(), self.group_num, self.window_size, self.file_size ) @property def guid_str(self): if not self._guid_str: self._guid_str = '-'.join('{:08x}'.format(g) for g in self.guid) return self._guid_str @property def guid_num(self): if not self._guid_num: self._guid_num = self.guid[3] + (self.guid[2] << 32) + (self.guid[1] << 64) + (self.guid[0] << 96) return self._guid_num @property def path(self): return '{}/{:02d}/{:016X}_{}.chunk'.format( get_chunk_dir(self._manifest_version), # the result of this seems to always match the group number, but this is the "correct way" (zlib.crc32(struct.pack(''.format( self.filename, self.symlink_target, self.hash.hex(), self.flags, ', '.join(self.install_tags), cp_repr, self.file_size ) class ChunkPart: def __init__(self): self.guid = None self.offset = 0 self.size = 0 # caches for things that are "expensive" to compute self._guid_str = None self._guid_num = None @property def guid_str(self): if not self._guid_str: self._guid_str = '-'.join('{:08x}'.format(g) for g in self.guid) return self._guid_str @property def guid_num(self): if not self._guid_num: self._guid_num = self.guid[3] + (self.guid[2] << 32) + (self.guid[1] << 64) + (self.guid[0] << 96) return self._guid_num def __repr__(self): guid_readable = '-'.join('{:08x}'.format(g) for g in self.guid) return ''.format( guid_readable, self.offset, self.size) class CustomFields: # this could probably be replaced with just a dict def __init__(self): self.size = 0 self.version = 0 self.count = 0 self._dict = dict() def __getitem__(self, item): return self._dict.get(item, None) def __str__(self): return str(self._dict) def keys(self): return self._dict.keys() def values(self): return self._dict.values() @classmethod def read(cls, bio): _cf = cls() cf_start = bio.tell() _cf.size = struct.unpack('