├── setup.py ├── ReadMe.md ├── UEManifestReader ├── classes │ ├── FCustomFields.py │ ├── FManifestHeader.py │ ├── stream_reader.py │ ├── FChunkDataList.py │ ├── FManifestData.py │ ├── FFileManifestList.py │ └── FManifestMeta.py ├── converter.py ├── enums.py └── __init__.py └── LICENSE /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = 'UEManifestReader', 5 | version = '0.0.1', 6 | description = 'Read and parse Unreal Engine Manifests', 7 | url = 'https://github.com/LupusLeaks/UEManifestReader', 8 | author_email = 'admin@ezfn.dev', 9 | packages = find_packages(), 10 | license='MIT', 11 | install_requires = ['aiohttp', 'bitstring'] 12 | ) -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # UEManifestReader 2 | 3 | [![Current pypi version](https://img.shields.io/pypi/v/UEManifestReader.svg)](https://pypi.org/project/UEManifestReader/) 4 | 5 | # Basic usage 6 | ```py 7 | import json 8 | import UEManifestReader 9 | from UEManifestReader.enums import Platform 10 | 11 | Reader = UEManifestReader.UEManifestReader() 12 | 13 | async def downloader(): 14 | manifest = await Reader.download_manifest(Platform.Android) 15 | open('android_manifest.json', 'w+').write(json.dumps(manifest, indent=2)) 16 | 17 | Reader.loop.run_until_complete(downloader()) 18 | ``` 19 | # Need help? 20 | If you need more help feel free to join this [discord server](https://discord.gg/jht3aM2). 21 | -------------------------------------------------------------------------------- /UEManifestReader/classes/FCustomFields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from UEManifestReader.enums import * 3 | from UEManifestReader.classes.stream_reader import ConstBitStreamWrapper 4 | 5 | class FCustomFields(): 6 | def __init__(self, reader: ConstBitStreamWrapper): 7 | StartPos = reader.bytepos 8 | DataSize = reader.read_uint32() 9 | DataVersion = reader.read_uint8() 10 | 11 | ElementCount = reader.read_int32() 12 | self.CustomFields = {} 13 | 14 | # Serialise the ManifestMetaVersion::Original version variables. 15 | if (DataVersion >= EChunkDataListVersion.Original.value): 16 | for _ in range(ElementCount): 17 | self.CustomFields[reader.read_string()] = None 18 | 19 | for key in self.CustomFields.keys(): 20 | self.CustomFields[key] = reader.read_string() 21 | 22 | # We must always make sure to seek the archive to the correct end location. 23 | reader.bytepos = StartPos + DataSize -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EZFN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UEManifestReader/converter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def ULongToHexHash(long: int): 3 | buffer = [None] * 8 4 | buffer[0] = (long >> 56).to_bytes(28,byteorder='little').hex()[:2] 5 | buffer[1] = (long >> 48).to_bytes(28,byteorder='little').hex()[:2] 6 | buffer[2] = (long >> 40).to_bytes(28,byteorder='little').hex()[:2] 7 | buffer[3] = (long >> 32).to_bytes(28,byteorder='little').hex()[:2] 8 | buffer[4] = (long >> 24).to_bytes(28,byteorder='little').hex()[:2] 9 | buffer[5] = (long >> 16).to_bytes(28,byteorder='little').hex()[:2] 10 | buffer[6] = (long >> 8).to_bytes(28,byteorder='little').hex()[:2] 11 | buffer[7] = (long).to_bytes(28,byteorder='little').hex()[:2] 12 | return (''.join(buffer)).upper() 13 | 14 | def SwapOrder(data: bytes) -> bytes: 15 | hex_str = data.hex() 16 | data = [None] * len(hex_str) 17 | # TODO: Improve this ^ 18 | if len(hex_str) == 8: 19 | data[0] = hex_str[6] 20 | data[1] = hex_str[7] 21 | data[2] = hex_str[4] 22 | data[3] = hex_str[5] 23 | 24 | data[4] = hex_str[2] 25 | data[5] = hex_str[3] 26 | data[6] = hex_str[0] 27 | data[7] = hex_str[1] 28 | 29 | return ''.join(data) 30 | 31 | def ParseIntBlob32(_hash: str) -> str: 32 | if (4 < (len(_hash) % 3) or len(_hash) % 3 != 0): 33 | raise ValueError(f'Failed to convert {_hash} to Blob 32') 34 | 35 | numbers = [] 36 | i = 0 37 | while i < len(_hash): 38 | numstr = int(_hash[i] + _hash[i+1] + _hash[i+2]) 39 | numbers.append(numstr) 40 | i += 3 41 | 42 | return int.from_bytes(bytearray(numbers), byteorder='little', signed=False) 43 | 44 | def ParseIntBlob64(_hash: str) -> str: 45 | if (len(_hash) % 3 != 0): 46 | raise ValueError(f'Failed to convert {_hash} to Blob 64') 47 | 48 | hex_str = "" 49 | i = 0 50 | while i < len(_hash): 51 | num_str = hex(int((str(_hash[i]) + str(_hash[i+1]) + str(_hash[i+2]))))[2:] 52 | if len(num_str) == 1: 53 | num_str = f'0{num_str}' 54 | 55 | hex_str = num_str + hex_str 56 | i += 3 57 | 58 | return hex_str -------------------------------------------------------------------------------- /UEManifestReader/classes/FManifestHeader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from UEManifestReader.enums import * 3 | from UEManifestReader.classes.stream_reader import ConstBitStreamWrapper 4 | 5 | # The manifest header magic codeword, for quick checking that the opened file is probably a manifest file. 6 | MANIFEST_HEADER_MAGIC = 0x44BEC00C 7 | 8 | class FManifestHeader(): 9 | def __init__(self, reader: ConstBitStreamWrapper, StartPos: int = 0): 10 | self.Magic = reader.read_uint32() 11 | # The size of this header. 12 | self.HeaderSize = reader.read_uint32() 13 | # The size of this data uncompressed. 14 | self.DataSizeUncompressed = reader.read_uint32() 15 | # The size of this data compressed. 16 | self.DataSizeCompressed = reader.read_uint32() 17 | # The SHA1 hash for the manifest data that follows. 18 | self.SHAHash = reader.read_bytes(20) 19 | # How the chunk data is stored. 20 | self.StoredAs = reader.read_uint8() 21 | 22 | bSuccess = self.Magic == MANIFEST_HEADER_MAGIC 23 | ExpectedSerializedBytes = ManifestHeaderVersionSizes[EFeatureLevel.Original.value] 24 | 25 | # After the Original with no specific version serialized, the header size increased and we had a version to load. 26 | if (bSuccess and self.HeaderSize > ManifestHeaderVersionSizes[EFeatureLevel.Original.value]): 27 | # The version of this header and manifest data format, driven by the feature level. 28 | Version = reader.read_int32() 29 | self.Version = ([e for e in EFeatureLevel.__members__.values() if e.value == Version])[0] 30 | ExpectedSerializedBytes = ManifestHeaderVersionSizes[self.Version.value] 31 | elif (bSuccess): 32 | # Otherwise, this header was at the version for a UObject class before this code refactor. 33 | self.Version = EFeatureLevel.StoredAsCompressedUClass 34 | 35 | # Make sure the expected number of bytes were serialized. In practice this will catch errors where type 36 | # serialization operators changed their format and that will need investigating. 37 | bSuccess = bSuccess and (reader.bytepos - StartPos) == ExpectedSerializedBytes 38 | if (bSuccess): 39 | # Make sure the archive now points to data location. 40 | reader.bytepos = StartPos + self.HeaderSize 41 | else: 42 | # If we had a serialization error when loading, raise an error 43 | raise Exception('Failed to read the Manifest Header') -------------------------------------------------------------------------------- /UEManifestReader/classes/stream_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Credits to: https://github.com/Shiqan/fortnite-replay-reader/blob/master/ray/reader.py 3 | import sys, bitstring 4 | from enum import Enum 5 | 6 | class BitTypes(Enum): 7 | """ See bitstring for more types """ 8 | INT_32 = 'intle:32' 9 | UINT8 = 'uint:8' 10 | UINT_32 = 'uintle:32' 11 | UINT_64 = 'uintle:64' 12 | BIT = 'bin:1' 13 | BYTE = 'bytes:1' 14 | 15 | class ConstBitStreamWrapper(bitstring.ConstBitStream): 16 | """ Wrapper for the bitstring.ConstBitStream class to provide some convience methods """ 17 | 18 | def skip(self, count): 19 | """ Skip the next count bytes """ 20 | self.bytepos += count 21 | 22 | def read_uint8(self): 23 | """ Read and interpret next 8 bits as an unassigned integer """ 24 | return self.read(BitTypes.UINT8.value) 25 | 26 | def read_uint32(self): 27 | """ Read and interpret next 32 bits as an unassigned integer """ 28 | return self.read(BitTypes.UINT_32.value) 29 | 30 | def read_int32(self): 31 | """ Read and interpret next 32 bits as an signed integer """ 32 | return self.read(BitTypes.INT_32.value) 33 | 34 | def read_uint64(self): 35 | """ Read and interpret next 64 bits as an unassigned integer """ 36 | return self.read(BitTypes.UINT_64.value) 37 | 38 | def read_byte(self): 39 | """ Read and interpret next bit as an integer """ 40 | return int.from_bytes(self.read(BitTypes.BYTE.value), byteorder='little') 41 | 42 | def read_bytes(self, size): 43 | """ Read and interpret next bit as an integer """ 44 | return self.read('bytes:'+str(size)) 45 | 46 | def read_array(self, f): 47 | """ Read an array where the first 32 bits indicate the length of the array """ 48 | length = self.read_uint32() 49 | return [f() for _ in range(length)] 50 | 51 | def read_string(self): 52 | """ Read and interpret next i bits as a string where i is determined defined by the first 32 bits """ 53 | size = self.read_int32() 54 | 55 | if size == 0: 56 | return "" 57 | 58 | is_unicode = size < 0 59 | 60 | if is_unicode: 61 | size *= -2 62 | return self.read_bytes(size)[:-2].decode('utf-16') 63 | 64 | stream_bytes = self.read_bytes(size) 65 | string = stream_bytes[:-1] 66 | if stream_bytes[-1] != 0: 67 | raise Exception('End of string not zero') 68 | 69 | try: 70 | return string.decode('utf-8') 71 | except UnicodeDecodeError: 72 | return string.decode('latin-1') -------------------------------------------------------------------------------- /UEManifestReader/classes/FChunkDataList.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from UEManifestReader.enums import * 3 | from UEManifestReader.converter import * 4 | from UEManifestReader.classes.stream_reader import ConstBitStreamWrapper 5 | 6 | class FChunkInfo(): 7 | def __init__(self): 8 | # The GUID for this data. 9 | self.Guid = None 10 | # The FRollingHash hashed value for this chunk data. 11 | self.Hash = None 12 | # The FSHA hashed value for this chunk data. 13 | self.ShaHash = None 14 | # The group number this chunk divides into. 15 | self.GroupNumber = None 16 | # The window size for this chunk 17 | self.WindowSize = 1048576 18 | # The file download size for this chunk. 19 | self.FileSize = None 20 | 21 | class FChunkDataList(): 22 | def __init__(self, reader: ConstBitStreamWrapper): 23 | StartPos = reader.bytepos 24 | DataSize = reader.read_uint32() 25 | DataVersion = reader.read_uint8() 26 | 27 | ElementCount = reader.read_int32() 28 | self.ChunkList = [FChunkInfo() for _ in range(ElementCount)] 29 | # For a struct list type of data, we serialise every variable as it's own flat list. 30 | # This makes it very simple to handle or skip, extra variables added to the struct later. 31 | 32 | # Serialise the ManifestMetaVersion::Original version variables. 33 | if (DataVersion >= EChunkDataListVersion.Original.value): 34 | for idx, _ in enumerate(self.ChunkList): 35 | self.ChunkList[idx].Guid = self.ReadFChunkInfoGuid(reader) 36 | 37 | for idx, _ in enumerate(self.ChunkList): 38 | self.ChunkList[idx].Hash = ULongToHexHash(reader.read_uint64()) 39 | 40 | for idx, _ in enumerate(self.ChunkList): 41 | self.ChunkList[idx].ShaHash = reader.read_bytes(20) 42 | 43 | for idx, _ in enumerate(self.ChunkList): 44 | self.ChunkList[idx].GroupNumber = int(reader.read_uint8()) 45 | 46 | for idx, _ in enumerate(self.ChunkList): 47 | self.ChunkList[idx].WindowSize = reader.read_int32() 48 | 49 | for idx, _ in enumerate(self.ChunkList): 50 | self.ChunkList[idx].FileSize = int(reader.read_uint8()) 51 | 52 | # We must always make sure to seek the archive to the correct end location. 53 | reader.bytepos = StartPos + DataSize 54 | 55 | def ReadFChunkInfoGuid(self, reader: ConstBitStreamWrapper) -> str: 56 | hex_str = '' 57 | hex_str += SwapOrder(reader.read_bytes(4)) 58 | hex_str += SwapOrder(reader.read_bytes(4)) 59 | hex_str += SwapOrder(reader.read_bytes(4)) 60 | hex_str += SwapOrder(reader.read_bytes(4)) 61 | return hex_str.upper() -------------------------------------------------------------------------------- /UEManifestReader/classes/FManifestData.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import zlib 3 | from UEManifestReader.enums import * 4 | from UEManifestReader.classes.FCustomFields import FCustomFields 5 | from UEManifestReader.classes.FManifestMeta import FManifestMeta 6 | from UEManifestReader.classes.FChunkDataList import FChunkDataList 7 | from UEManifestReader.classes.FManifestHeader import FManifestHeader 8 | from UEManifestReader.classes.stream_reader import ConstBitStreamWrapper 9 | from UEManifestReader.classes.FFileManifestList import FFileManifestList 10 | 11 | # FManifestData - The public interface to load/saving manifest files. 12 | class FManifestData(): 13 | def __init__(self, data: bytes): 14 | self.reader = ConstBitStreamWrapper(data) 15 | self.start() 16 | 17 | def start(self): 18 | StartPos = self.reader.bytepos 19 | 20 | # Read the Manifest Header 21 | self.Header = FManifestHeader(self.reader) 22 | 23 | # If we are loading an old format, defer to the old code! 24 | if (self.Header.Version.value < EFeatureLevel.StoredAsBinaryData.value): 25 | FullDataSize = GetFullDataSize(Header) 26 | FullData = reader.read_bytes(FullDataSize) 27 | self.reader.bytepos = StartPos 28 | 29 | temp = FManifestData(self.reader.read_bytes(FullDataSize)) 30 | self.Meta = temp.Meta 31 | self.ChunkDataList = temp.ChunkDataList 32 | self.FileManifestList = temp.FileManifestList 33 | self.CustomFields = temp.CustomFields 34 | return 35 | else: 36 | # Compression format selection - we only have one right now. 37 | # Fill the array with loaded data. 38 | # DataSizeCompressed always equals the size of the data following the header. 39 | if self.Header.StoredAs == EManifestStorageFlags.Compressed.value: 40 | Decompressed = zlib.decompress(self.reader.read_bytes(self.Header.DataSizeCompressed)) 41 | ManifestRawData = ConstBitStreamWrapper(Decompressed) 42 | elif self.Header.StoredAs == EManifestStorageFlags.Encrypted.value: 43 | raise Exception('Encrypted Manifests are not supported yet') 44 | 45 | # Read the Manifest Meta 46 | self.Meta = FManifestMeta(ManifestRawData) 47 | # Read the Manifest Chunk List 48 | self.ChunkDataList = FChunkDataList(ManifestRawData) 49 | # Read the Manifest File List 50 | self.FileManifestList = FFileManifestList(ManifestRawData) 51 | # Read the Custom Fields 52 | self.CustomFields = FCustomFields(ManifestRawData) 53 | 54 | def GetFullDataSize(self) -> int: 55 | bIsCompressed = self.Header.StoredAs == EManifestStorageFlags.Compressed 56 | return self.Header.HeaderSize + (bIsCompressed if Header.DataSizeCompressed else Header.DataSizeUncompressed) -------------------------------------------------------------------------------- /UEManifestReader/classes/FFileManifestList.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import List 3 | from UEManifestReader.enums import * 4 | from UEManifestReader.converter import * 5 | from UEManifestReader.classes.FChunkDataList import FChunkDataList 6 | from UEManifestReader.classes.stream_reader import ConstBitStreamWrapper 7 | 8 | # A data structure describing the part of a chunk used to construct a file 9 | class FChunkPart(): 10 | def __init__(self, Guid: str, Offset: int, Size: int): 11 | # The GUID of the chunk containing this part. 12 | self.Guid = Guid 13 | # The offset of the first byte into the chunk. 14 | self.Offset = Offset 15 | # The size of this part. 16 | self.Size = Size 17 | 18 | class FFileManifest(): 19 | def __init__(self): 20 | # The build relative filename. 21 | self.Filename = None 22 | # Whether this is a symlink to another file. 23 | self.SymlinkTarget = None 24 | # The file SHA1. 25 | self.FileHash = None 26 | # The flags for this file. 27 | self.FileMetaFlags = None 28 | # The install tags for this file. 29 | self.InstallTags = None 30 | # The list of chunk parts to stitch. 31 | self.ChunkParts = [] 32 | # The size of this file. 33 | self.FileSize = None 34 | 35 | class FFileManifestList(): 36 | def __init__(self, reader: ConstBitStreamWrapper): 37 | StartPos = reader.bytepos 38 | DataSize = reader.read_uint32() 39 | DataVersion = reader.read_uint8() 40 | 41 | ElementCount = reader.read_int32() 42 | self.FileManifest = [FFileManifest() for _ in range(ElementCount)] 43 | 44 | # Serialise the ManifestMetaVersion::Original version variables. 45 | if (DataVersion >= EFileManifestListVersion.Original.value): 46 | for idx, _ in enumerate(self.FileManifest): 47 | self.FileManifest[idx].Filename = reader.read_string() 48 | 49 | for idx, _ in enumerate(self.FileManifest): 50 | self.FileManifest[idx].SymlinkTarget = reader.read_string() 51 | 52 | for idx, _ in enumerate(self.FileManifest): 53 | self.FileManifest[idx].FileHash = reader.read_bytes(20) 54 | 55 | for idx, _ in enumerate(self.FileManifest): 56 | self.FileManifest[idx].FileMetaFlags = reader.read_uint8() 57 | 58 | for idx, _ in enumerate(self.FileManifest): 59 | self.FileManifest[idx].InstallTags = reader.read_array(reader.read_string) 60 | 61 | for idx, _ in enumerate(self.FileManifest): 62 | self.FileManifest[idx].ChunkParts = self.ReadChunkParts(reader) 63 | 64 | # We must always make sure to seek the archive to the correct end location. 65 | reader.bytepos = StartPos + DataSize 66 | 67 | def ReadChunkParts(self, reader: ConstBitStreamWrapper) -> List[FChunkPart]: 68 | ChunkCount = reader.read_int32() 69 | FChunkParts = [] 70 | for _ in range(ChunkCount): 71 | reader.skip(4) 72 | FChunkParts.append( 73 | FChunkPart( 74 | Guid = self.ReadFChunkPartGuid(reader), 75 | Offset = reader.read_int32(), 76 | Size = reader.read_int32() 77 | ) 78 | ) 79 | return FChunkParts 80 | 81 | def ReadFChunkPartGuid(self, reader: ConstBitStreamWrapper) -> str: 82 | hex_str = '' 83 | hex_str += SwapOrder(reader.read_bytes(4)) 84 | hex_str += SwapOrder(reader.read_bytes(4)) 85 | hex_str += SwapOrder(reader.read_bytes(4)) 86 | hex_str += SwapOrder(reader.read_bytes(4)) 87 | return hex_str.upper() -------------------------------------------------------------------------------- /UEManifestReader/classes/FManifestMeta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # from hashlib import sha1 3 | from UEManifestReader.enums import * 4 | from UEManifestReader.classes.stream_reader import ConstBitStreamWrapper 5 | 6 | # FManifestMeta - The data implementation for a build meta data. 7 | class FManifestMeta(): 8 | def __init__(self, reader: ConstBitStreamWrapper): 9 | # Serialise the data header type values. 10 | StartPos = reader.bytepos 11 | 12 | DataSize = reader.read_uint32() 13 | DataVersion = reader.read_uint8() 14 | 15 | # Serialise the ManifestMetaVersion::Original version variables. 16 | if (DataVersion >= EManifestMetaVersion.Original.value): 17 | self.FeatureLevelInt = reader.read_uint32() 18 | # Whether this is a legacy 'nochunks' build. 19 | self.IsFileData = reader.read_byte() == 1 20 | # The app id provided at generation. 21 | self.AppID = reader.read_uint32() 22 | # The app name string provided at generation. 23 | self.AppName = reader.read_string() 24 | # The build version string provided at generation. 25 | self.BuildVersion = reader.read_string() 26 | # The file in this manifest designated the application executable of the build. 27 | self.LaunchExe = reader.read_string() 28 | # The command line required when launching the application executable. 29 | self.LaunchCommand = reader.read_string() 30 | # The set of prerequisite ids for dependencies that this build's prerequisite installer will apply. 31 | self.PrereqIds = reader.read_array(reader.read_string) 32 | # A display string for the prerequisite provided at generation. 33 | self.PrereqName = reader.read_string() 34 | # The file in this manifest designated the launch executable of the prerequisite installer. 35 | self.PrereqPath = reader.read_string() 36 | # The command line required when launching the prerequisite installer. 37 | self.PrereqArgs = reader.read_string() 38 | 39 | # Serialise the BuildId. 40 | if (DataVersion >= EManifestMetaVersion.SerialisesBuildId.value): 41 | self.BuildId = reader.read_string() 42 | # Otherwise, initialise with backwards compatible default when loading. 43 | else: 44 | self.BuildId = 'Not added yet' # self.GetBackwardsCompatibleBuildId() 45 | 46 | # Chunk Sub Dir 47 | if (self.FeatureLevelInt < EFeatureLevel.DataFileRenames.value): 48 | self.ChunkSubDir = 'Chunks' 49 | elif (self.FeatureLevelInt < EFeatureLevel.ChunkCompressionSupport.value): 50 | self.ChunkSubDir = 'ChunksV2' 51 | elif (self.FeatureLevelInt < EFeatureLevel.VariableSizeChunksWithoutWindowSizeChunkInfo.value): 52 | self.ChunkSubDir = 'ChunksV3' 53 | else: 54 | self.ChunkSubDir = 'ChunksV4' 55 | 56 | # File Sub Dir 57 | if (self.FeatureLevelInt < EFeatureLevel.DataFileRenames.value): 58 | self.FileSubDir = 'Files' 59 | elif (self.FeatureLevelInt < EFeatureLevel.StoresChunkDataShaHashes.value): 60 | self.FileSubDir = 'FilesV2' 61 | else: 62 | self.FileSubDir = 'FilesV3' 63 | 64 | # We must always make sure to seek the archive to the correct end location. 65 | reader.bytepos = StartPos + DataSize 66 | 67 | def GetBackwardsCompatibleBuildId(self) -> str: 68 | # Sha.Update((const uint8*)&ManifestMeta.AppID, sizeof(ManifestMeta.AppID)); 69 | # // For platform agnostic result, we must use UTF8. TCHAR can be 16b, or 32b etc. 70 | # FTCHARToUTF8 UTF8AppName(*ManifestMeta.AppName); 71 | # FTCHARToUTF8 UTF8BuildVersion(*ManifestMeta.BuildVersion); 72 | # FTCHARToUTF8 UTF8LaunchExe(*ManifestMeta.LaunchExe); 73 | # FTCHARToUTF8 UTF8LaunchCommand(*ManifestMeta.LaunchCommand); 74 | # Sha.Update((const uint8*)UTF8AppName.Get(), sizeof(ANSICHAR) * UTF8AppName.Length()); 75 | # Sha.Update((const uint8*)UTF8BuildVersion.Get(), sizeof(ANSICHAR) * UTF8BuildVersion.Length()); 76 | # Sha.Update((const uint8*)UTF8LaunchExe.Get(), sizeof(ANSICHAR) * UTF8LaunchExe.Length()); 77 | # Sha.Update((const uint8*)UTF8LaunchCommand.Get(), sizeof(ANSICHAR) * UTF8LaunchCommand.Length()); 78 | # Sha.Final(); 79 | # Sha.GetHash(Hash.Hash); 80 | pass -------------------------------------------------------------------------------- /UEManifestReader/enums.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum 3 | 4 | class Platform(Enum): 5 | Windows = 0, 6 | Android = 1, 7 | iOS = 2, 8 | 9 | # Enum which describes the FManifestMeta data version. 10 | class EManifestMetaVersion(Enum): 11 | Original = 0 12 | SerialisesBuildId = 1 13 | # Always after the latest version, signifies the latest version plus 1 to allow initialization simplicity. 14 | LatestPlusOne = 2 15 | Latest = LatestPlusOne - 1 16 | 17 | ManifestHeaderVersionSizes = [ 18 | # EFeatureLevel::Original is 37B (32b Magic, 32b HeaderSize, 32b DataSizeUncompressed, 32b DataSizeCompressed, 160b SHA1, 8b StoredAs) 19 | # This remained the same all up to including EFeatureLevel::StoresPrerequisiteIds. 20 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 21 | # EFeatureLevel::StoredAsBinaryData is 41B, (296b Original, 32b Version). 22 | # This remained the same all up to including EFeatureLevel::UsesBuildTimeGeneratedBuildId. 23 | 41, 41, 41, 41, 41 24 | ] 25 | 26 | # An enum type to describe supported features of a certain manifest. 27 | 28 | class EFeatureLevel(Enum): 29 | # The original version. 30 | Original = 0 31 | 32 | # Support for custom fields. 33 | CustomFields = 1 34 | # Started storing the version number. 35 | StartStoringVersion = 2 36 | # Made after data files where renamed to include the hash value, these chunks now go to ChunksV2. 37 | DataFileRenames = 3 38 | # Manifest stores whether build was constructed with chunk or file data. 39 | StoresIfChunkOrFileData = 4 40 | # Manifest stores group number for each chunk/file data for reference so that external readers don't need to know how to calculate them. 41 | StoresDataGroupNumbers = 5 42 | # Added support for chunk compression, these chunks now go to ChunksV3. NB: Not File Data Compression yet. 43 | ChunkCompressionSupport = 6 44 | # Manifest stores product prerequisites info. 45 | StoresPrerequisitesInfo = 7 46 | # Manifest stores chunk download sizes. 47 | StoresChunkFileSizes = 8 48 | # Manifest can optionally be stored using UObject serialization and compressed. 49 | StoredAsCompressedUClass = 9 50 | # These two features were removed and never used. 51 | UNUSED_0 = 10 52 | UNUSED_1 = 11 53 | # Manifest stores chunk data SHA1 hash to use in place of data compare, for faster generation. 54 | StoresChunkDataShaHashes = 12 55 | # Manifest stores Prerequisite Ids. 56 | StoresPrerequisiteIds = 13 57 | # The first minimal binary format was added. UObject classes will no longer be saved out when binary selected. 58 | StoredAsBinaryData = 14 59 | # Temporary level where manifest can reference chunks with dynamic window size, but did not serialize them. Chunks from here onwards are stored in ChunksV4. 60 | VariableSizeChunksWithoutWindowSizeChunkInfo = 15 61 | # Manifest can reference chunks with dynamic window size, and also serializes them. 62 | VariableSizeChunks = 16 63 | # Manifest uses a build id generated from its metadata. 64 | UsesRuntimeGeneratedBuildId = 17 65 | # Manifest uses a build id generated unique at build time, and stored in manifest. 66 | UsesBuildTimeGeneratedBuildId = 18 67 | 68 | # !! Always after the latest version entry, signifies the latest version plus 1 to allow the following Latest alias. 69 | LatestPlusOne = 19 70 | # An alias for the actual latest version value. 71 | Latest = LatestPlusOne - 1 72 | # An alias to provide the latest version of a manifest supported by file data (nochunks). 73 | LatestNoChunks = StoresChunkFileSizes 74 | # An alias to provide the latest version of a manifest supported by a json serialized format. 75 | LatestJson = StoresPrerequisiteIds 76 | # An alias to provide the first available version of optimised delta manifest saving. 77 | FirstOptimisedDelta = UsesRuntimeGeneratedBuildId 78 | 79 | # More aliases, but this time for values that have been renamed 80 | StoresUniqueBuildId = UsesRuntimeGeneratedBuildId 81 | 82 | # JSON manifests were stored with a version of 255 during a certain CL range due to a bug. 83 | # We will treat this as being StoresChunkFileSizes in code. 84 | BrokenJsonVersion = 255 85 | # This is for UObject default, so that we always serialize it. 86 | 87 | Invalid = -1 88 | 89 | # A flags enum for manifest headers which specify storage types. 90 | class EManifestStorageFlags(Enum): 91 | # Stored as raw data. 92 | Null = 0, 93 | # Flag for compressed data. 94 | Compressed = 1 95 | # Flag for encrypted. If also compressed, decrypt first. Encryption will ruin compressibility. 96 | Encrypted = 1 << 1 97 | 98 | 99 | 100 | # Enum which describes the FChunkDataList data version. 101 | class EChunkDataListVersion(Enum): 102 | Original = 0 103 | # Always after the latest version, signifies the latest version plus 1 to allow initialization simplicity. 104 | LatestPlusOne = 1 105 | Latest = LatestPlusOne - 1 106 | 107 | # Enum which describes the FFileManifestList data version. 108 | class EFileManifestListVersion(Enum): 109 | Original = 0 110 | # Always after the latest version, signifies the latest version plus 1 to allow initialization simplicity. 111 | LatestPlusOne = 1 112 | Latest = LatestPlusOne - 1 113 | 114 | # Enum which describes the FChunkDataList data version. 115 | class EChunkDataListVersion(Enum): 116 | Original = 0 117 | # Always after the latest version, signifies the latest version plus 1 to allow initialization simplicity. 118 | LatestPlusOne = 1 119 | Latest = LatestPlusOne - 1 -------------------------------------------------------------------------------- /UEManifestReader/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import asyncio 4 | import aiohttp 5 | from .enums import Platform 6 | from .converter import * 7 | from .classes.FManifestData import FManifestData 8 | 9 | class UEManifestReader(): 10 | def __init__(self, **kwargs): 11 | self.loop = kwargs.get('loop', asyncio.get_event_loop()) 12 | self.is_serialized = False 13 | 14 | async def download_manifest(self, platform: Platform = Platform.Windows, return_parsed = True): 15 | if platform == Platform.Windows: 16 | launcher_asset_url = 'https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/Windows/4fe75bbc5a674f4f9b356b5c90567da5/Fortnite?label=Live' 17 | elif platform == Platform.Android: 18 | launcher_asset_url = 'https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/Android/5cb97847cee34581afdbc445400e2f77/FortniteContentBuilds?label=Live' 19 | elif platform == Platform.iOS: 20 | launcher_asset_url = 'https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/IOS/5cb97847cee34581afdbc445400e2f77/FortniteContentBuilds?label=Live' 21 | else: 22 | raise ValueError(Platform) 23 | 24 | async with aiohttp.ClientSession() as session: 25 | # Get the EG1 Token to fetch the manifest info 26 | async with session.post( 27 | url = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token', 28 | data = { 29 | 'grant_type': 'client_credentials', 30 | 'token_token': 'eg1' 31 | }, 32 | headers = { 33 | 'Authorization': 'basic MzRhMDJjZjhmNDQxNGUyOWIxNTkyMTg3NmRhMzZmOWE6ZGFhZmJjY2M3Mzc3NDUwMzlkZmZlNTNkOTRmYzc2Y2Y=' 34 | } 35 | ) as response: 36 | access_token = (await response.json())['access_token'] 37 | 38 | async with session.get( 39 | url = launcher_asset_url, 40 | headers = {'Authorization': f'bearer {access_token}'} 41 | ) as response: 42 | manifest_info = await response.json() 43 | 44 | distribution = manifest_info['items']['MANIFEST']['distribution'] 45 | path = manifest_info['items']['MANIFEST']['path'] 46 | signature = manifest_info['items']['MANIFEST']['signature'] 47 | manifest_url = f'{distribution}{path}?{signature}' 48 | 49 | async with session.get(manifest_url) as response: 50 | manifest = await response.read() 51 | 52 | if return_parsed: 53 | return self.parse_manifest(manifest) 54 | 55 | def return_manifest_as_json(self, manifest) -> dict: 56 | parsed = {} 57 | 58 | if self.is_serialized: 59 | return { 60 | "ManifestVersion": manifest.Meta.FeatureLevelInt, 61 | "bIsFileData": manifest.Meta.IsFileData, 62 | "AppID": manifest.Meta.AppID, 63 | "AppNameString": manifest.Meta.AppName, 64 | "BuildVersionString": manifest.Meta.BuildVersion, 65 | "LaunchExeString": manifest.Meta.LaunchExe, 66 | "LaunchCommand": manifest.Meta.LaunchCommand, 67 | "PrereqIds": manifest.Meta.PrereqIds, 68 | "PrereqName": manifest.Meta.PrereqName, 69 | "PrereqPath": manifest.Meta.PrereqPath, 70 | "PrereqArgs": manifest.Meta.PrereqArgs, 71 | "ChunkSubDir": manifest.Meta.ChunkSubDir, 72 | "FileSubDir": manifest.Meta.FileSubDir, 73 | "FileManifestList": [ 74 | { 75 | "Filename": fileManifest.Filename, 76 | "FileHash": fileManifest.FileHash.hex().upper(), 77 | "FileChunkParts": [{ 78 | "Offset": str(ChunkPart.Offset), 79 | "Size": str(ChunkPart.Size), 80 | "Guid": ChunkPart.Guid, 81 | } for ChunkPart in fileManifest.ChunkParts] 82 | } 83 | for fileManifest in manifest.FileManifestList.FileManifest], 84 | "ChunkHashList": {ChunkInfo.Guid: str(ChunkInfo.Hash) for ChunkInfo in manifest.ChunkDataList.ChunkList}, 85 | "DataGroupList": {ChunkInfo.Guid: str(ChunkInfo.GroupNumber)[-2:] for ChunkInfo in manifest.ChunkDataList.ChunkList} 86 | } 87 | else: 88 | manifest['ManifestFileVersion'] = str(int(manifest['ManifestFileVersion'])) 89 | manifest['AppID'] = str(int(manifest['AppID'])) 90 | 91 | for File in manifest['FileManifestList']: 92 | for chunk in File['FileChunkParts']: 93 | chunk['Offset'] = ParseIntBlob32(chunk['Offset']) 94 | chunk['Size'] = ParseIntBlob32(chunk["Size"]) 95 | 96 | for Guid, GroupNumber in manifest['DataGroupList'].items(): 97 | manifest['DataGroupList'][Guid] = str(GroupNumber)[-2:] 98 | 99 | for Guid, Hash in manifest['ChunkHashList'].items(): 100 | manifest['ChunkHashList'][Guid] = ParseIntBlob64(Hash) 101 | 102 | return manifest 103 | 104 | def parse_manifest(self, manifest: bytes): 105 | try: 106 | manifest = json.loads(manifest) 107 | except: 108 | self.is_serialized = True 109 | manifest = FManifestData(manifest) 110 | 111 | return self.return_manifest_as_json(manifest) --------------------------------------------------------------------------------