diff --git a/UnityPy/environment.py b/UnityPy/environment.py index 6b210aca..34135f0f 100644 --- a/UnityPy/environment.py +++ b/UnityPy/environment.py @@ -1,17 +1,14 @@ import io -import os import ntpath +import os import re -from typing import List, Callable, Dict, Union +from typing import Callable, Dict, List, Union from zipfile import ZipFile from fsspec import AbstractFileSystem from fsspec.implementations.local import LocalFileSystem - -from .files import File, ObjectReader, SerializedFile -from .enums import FileType -from .helpers import ImportHelper +from .files import File, ObjectReader, SerializedFile, parse_file from .streams import EndianBinaryReader reSplit = re.compile(r"(.*?([^\/\\]+?))\.split\d+") @@ -119,15 +116,13 @@ def load_file( file = self._load_split_file(file) # Unity paths are case insensitive, so we need to find "Resources/Foo.asset" when the record says "resources/foo.asset" elif not os.path.exists(file): - file = ImportHelper.find_sensitive_path(self.path, file) + file = find_sensitive_path(self.path, file) # nonexistent files might be packaging errors or references to Unity's global Library/ if file is None: return - if type(file) == str: + if isinstance(file, str): file = self.fs.open(file, "rb") - typ, reader = ImportHelper.check_file_type(file) - stream_name = ( name if name @@ -138,19 +133,18 @@ def load_file( ) ) - if typ == FileType.ZIP: - f = self.load_zip_file(file) - else: - f = ImportHelper.parse_file( - reader, self, name=stream_name, typ=typ, is_dependency=is_dependency - ) - + reader = EndianBinaryReader(file) + path = stream_name if not parent else f"{parent.path}/{stream_name}" + f = parse_file(reader, stream_name, path, parent=self) + + if f is None: + f = reader + if isinstance(f, (SerializedFile, EndianBinaryReader)): self.register_cab(stream_name, f) self.files[stream_name] = f - def load_zip_file(self, value): buffer = None if isinstance(value, str) and self.fs.exists(value): @@ -330,3 +324,57 @@ def simplify_name(name: str) -> str: - converting to lowercase """ return ntpath.basename(name).lower() + + +def file_name_without_extension(file_name: str) -> str: + return os.path.join( + os.path.dirname(file_name), os.path.splitext(os.path.basename(file_name))[0] + ) + + +def list_all_files(directory: str) -> List[str]: + return [ + val + for sublist in [ + [os.path.join(dir_path, filename) for filename in filenames] + for (dir_path, dirn_ames, filenames) in os.walk(directory) + if ".git" not in dir_path + ] + for val in sublist + ] + + +def find_all_files(directory: str, search_str: str) -> List[str]: + return [ + val + for sublist in [ + [ + os.path.join(dir_path, filename) + for filename in filenames + if search_str in filename + ] + for (dir_path, dirn_ames, filenames) in os.walk(directory) + if ".git" not in dir_path + ] + for val in sublist + ] + + +def find_sensitive_path(dir: str, insensitive_path: str) -> Union[str, None]: + parts = os.path.split(insensitive_path.strip(os.path.sep)) + + sensitive_path = dir + for part in parts: + part_lower = part.lower() + part = next( + (name for name in os.listdir(sensitive_path) if name.lower() == part_lower), + None, + ) + if part is None: + return None + sensitive_path = os.path.join(sensitive_path, part) + + return sensitive_path + + +__all__ = ["Environment"] diff --git a/UnityPy/files/BundleFile.py b/UnityPy/files/BundleFile.py index c35a0483..5c6425aa 100644 --- a/UnityPy/files/BundleFile.py +++ b/UnityPy/files/BundleFile.py @@ -1,171 +1,216 @@ # TODO: implement encryption for saving files -from collections import namedtuple -import re -from typing import Tuple, Union +from abc import ABC, ABCMeta +from enum import IntEnum, IntFlag +from typing import Annotated, List, Optional, Self, Union -from . import File -from ..enums import ArchiveFlags, ArchiveFlagsOld, CompressionFlags -from ..helpers import ArchiveStorageManager, CompressionHelper +from attr import define + +from ..helpers import CompressionHelper +from ..helpers.ArchiveStorageManager import ArchiveStorageDecryptor +from ..helpers.Tpk import UnityVersion from ..streams import EndianBinaryReader, EndianBinaryWriter +from .File import ContainerFile, DirectoryInfo, File, parseable_filetype + + +@define(slots=True) +class BlockInfo: + flags: int + compressed_size: int + decompressed_size: int + offset: int + + +class CompressionFlags(IntEnum): + NONE = 0 + LZMA = 1 + LZ4 = 2 + LZ4HC = 3 + LZHAM = 4 + -from .. import config +class ArchiveFlagsOld(IntFlag): + CompressionTypeMask = 0x3F + BlocksAndDirectoryInfoCombined = 0x40 + BlocksInfoAtTheEnd = 0x80 + OldWebPluginCompatibility = 0x100 + UsesAssetBundleEncryption = 0x200 -BlockInfo = namedtuple("BlockInfo", "uncompressedSize compressedSize flags") -DirectoryInfoFS = namedtuple("DirectoryInfoFS", "offset size flags path") -reVersion = re.compile(r"(\d+)\.(\d+)\.(\d+)\w.+") +class ArchiveFlags(IntFlag): + CompressionTypeMask = 0x3F + BlocksAndDirectoryInfoCombined = 0x40 + BlocksInfoAtTheEnd = 0x80 + OldWebPluginCompatibility = 0x100 + BlockInfoNeedPaddingAtStart = 0x200 + UsesAssetBundleEncryption = 0x400 -class BundleFile(File.File): - format: int - is_changed: bool + +class BundleFile(ContainerFile, ABC, metaclass=ABCMeta): signature: str - version_engine: str - version_player: str - dataflags: Tuple[ArchiveFlags, ArchiveFlagsOld] - decryptor: ArchiveStorageManager.ArchiveStorageDecryptor = None - _uses_block_alignment: bool = False - - def __init__( - self, reader: EndianBinaryReader, parent: File, name: str = None, **kwargs - ): - super().__init__(parent=parent, name=name, **kwargs) - signature = self.signature = reader.read_string_to_null() - self.version = reader.read_u_int() - self.version_player = reader.read_string_to_null() - self.version_engine = reader.read_string_to_null() - - if signature == "UnityArchive": - raise NotImplementedError("BundleFile - UnityArchive") - elif signature in ["UnityWeb", "UnityRaw"]: - m_DirectoryInfo, blocksReader = self.read_web_raw(reader) - elif signature == "UnityFS": - m_DirectoryInfo, blocksReader = self.read_fs(reader) - else: - raise NotImplementedError(f"Unknown Bundle signature: {signature}") + stream_version: int + unity_version: str + minimum_revision: str + block_infos: List[BlockInfo] + block_reader: EndianBinaryReader - self.read_files(blocksReader, m_DirectoryInfo) + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + reader = self._opt_get_set_reader(reader) - def read_web_raw(self, reader: EndianBinaryReader): - # def read_header_and_blocks_info(self, reader:EndianBinaryReader): - version = self.version - if version >= 4: - self._hash = reader.read_bytes(16) - self.crc = reader.read_u_int() + self.signature = reader.read_string_to_null() + self.stream_version = reader.read_u_int() + if self.stream_version > 0x1000000: + reader.seek(reader.tell() - 4) + reader.endian = "<" if reader.endian == ">" else ">" + self.stream_version = reader.read_u_int() - minimumStreamedBytes = reader.read_u_int() - headerSize = reader.read_u_int() - numberOfLevelsToDownloadBeforeStreaming = reader.read_u_int() - levelCount = reader.read_int() - reader.Position += 4 * 2 * (levelCount - 1) + self.unity_version = reader.read_string_to_null() + self.minimum_revision = reader.read_string_to_null() + return self - compressedSize = reader.read_u_int() - uncompressedSize = reader.read_u_int() + def dump(self, writer: Optional[EndianBinaryWriter] = None) -> EndianBinaryWriter: + writer = writer or EndianBinaryWriter(endian=self.reader.endian) - if version >= 2: - completeFileSize = reader.read_u_int() + writer.write_string_to_null(self.signature) + writer.write_u_int(self.stream_version) + writer.write_string_to_null(self.unity_version) + writer.write_string_to_null(self.minimum_revision) + return writer - if version >= 3: - fileInfoHeaderSize = reader.read_u_int() - reader.Position = headerSize +@parseable_filetype +class BundleFileArchive(BundleFile): + @classmethod + def probe(cls, reader: EndianBinaryReader) -> bool: + signature = reader.read_string_to_null() + + return signature == "UnityArchive" + + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + super().parse(reader) + raise NotImplementedError("BundleFile - UnityArchive") + + def dump(self, writer: Optional[EndianBinaryWriter] = None) -> EndianBinaryWriter: + raise NotImplementedError("BundleFile - UnityArchive") - uncompressedBytes = reader.read_bytes(compressedSize) - if self.signature == "UnityWeb": - uncompressedBytes = CompressionHelper.decompress_lzma(uncompressedBytes, True) - - blocksReader = EndianBinaryReader(uncompressedBytes, offset=headerSize) - nodesCount = blocksReader.read_int() - m_DirectoryInfo = [ - File.DirectoryInfo( - blocksReader.read_string_to_null(), # path - blocksReader.read_u_int(), # offset - blocksReader.read_u_int(), # size - ) - for _ in range(nodesCount) - ] - return m_DirectoryInfo, blocksReader +@parseable_filetype +class BundleFileFS(BundleFile): + size: int + version: UnityVersion + dataflags: Union[ArchiveFlags, ArchiveFlagsOld] + decompressed_data_hash: Annotated[bytes, 16] + uses_block_alignment: bool = False + decryptor: Optional[ArchiveStorageDecryptor] = None - def read_fs(self, reader: EndianBinaryReader): - size = reader.read_long() + @classmethod + def probe(cls, reader: EndianBinaryReader) -> bool: + signature = reader.read_string_to_null() + endian = reader.endian + reader.endian = ">" + version = reader.read_u_int() + reader.endian = endian + + return signature == "UnityFS" or (signature == "UnityRaw" and version == 6) + + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + reader = self._opt_get_set_reader(reader) + + super().parse(reader) + self.size = reader.read_long() # header - compressedSize = reader.read_u_int() - uncompressedSize = reader.read_u_int() - self.dataflags = reader.read_u_int() + header_compressed_size, header_uncompressed_size, dataflags = ( + reader.read_u_int_array(3) + ) - version = self.get_version_tuple() # https://issuetracker.unity3d.com/issues/files-within-assetbundles-do-not-start-on-aligned-boundaries-breaking-patching-on-nintendo-switch # Unity CN introduced encryption before the alignment fix was introduced. # Unity CN used the same flag for the encryption as later on the alignment fix, # so we have to check the version to determine the correct flag set. + version = UnityVersion.fromString(self.minimum_revision) + self.version = version if ( - version < (2020,) - or (version[0] == 2020 and version < (2020, 3, 34)) - or (version[0] == 2021 and version < (2021, 3, 2)) - or (version[0] == 2022 and version < (2022, 1, 1)) + # < 2000, < 2020.3.34, < 2021.3.2, < 2022.1.1 + version.major < 2020 + or (version.major == 2020 and version < UnityVersion.fromList(2020, 3, 34)) + or (version.major == 2021 and version < UnityVersion.fromList(2021, 3, 2)) + or (version.major == 2022 and version < UnityVersion.fromList(2022, 1, 1)) ): - self.dataflags = ArchiveFlagsOld(self.dataflags) + self.dataflags = ArchiveFlagsOld(dataflags) else: - self.dataflags = ArchiveFlags(self.dataflags) + self.dataflags = ArchiveFlags(dataflags) if self.dataflags & self.dataflags.UsesAssetBundleEncryption: - self.decryptor = ArchiveStorageManager.ArchiveStorageDecryptor(reader) + self.decryptor = ArchiveStorageDecryptor(reader) # check if we need to align the reader # - align to 16 bytes and check if all are 0 # - if not, reset the reader to the previous position - if self.version >= 7: + if self.stream_version >= 7: reader.align_stream(16) - self._uses_block_alignment = True - elif version >= (2019, 4): - pre_align = reader.Position + self.uses_block_alignment = True + elif version >= UnityVersion.fromList(2019, 4): + pre_align = reader.tell() align_data = reader.read((16 - pre_align % 16) % 16) if any(align_data): - reader.Position = pre_align + reader.seek(pre_align) else: - self._uses_block_alignment = True + self.uses_block_alignment = True - start = reader.Position + start = reader.tell() + seek_back = -1 if ( self.dataflags & ArchiveFlags.BlocksInfoAtTheEnd ): # kArchiveBlocksInfoAtTheEnd - reader.Position = reader.Length - compressedSize - blocksInfoBytes = reader.read_bytes(compressedSize) - reader.Position = start - else: # 0x40 kArchiveBlocksAndDirectoryInfoCombined - blocksInfoBytes = reader.read_bytes(compressedSize) - - blocksInfoBytes = self.decompress_data( - blocksInfoBytes, uncompressedSize, self.dataflags + seek_back = reader.tell() + reader.seek(reader.Length - header_compressed_size) + # else: # 0x40 kArchiveBlocksAndDirectoryInfoCombined + + blocksInfoBytes = reader.read_bytes(header_compressed_size) + blocksInfoBytes = self._decompress_data( + blocksInfoBytes, header_uncompressed_size, self.dataflags ) blocksInfoReader = EndianBinaryReader(blocksInfoBytes, offset=start) - uncompressedDataHash = blocksInfoReader.read_bytes(16) + self.decompressed_data_hash = blocksInfoReader.read_bytes(16) + blocksInfoCount = blocksInfoReader.read_int() + assert blocksInfoCount > 0, "blocksInfoCount <= 0" + block_offset = 0 - m_BlocksInfo = [ + def offset(compressed_size: int): + nonlocal block_offset + old_offset = block_offset + block_offset += compressed_size + return old_offset + + self.block_infos = [ BlockInfo( - blocksInfoReader.read_u_int(), # uncompressedSize - blocksInfoReader.read_u_int(), # compressedSize - blocksInfoReader.read_u_short(), # flags + flags, + compressed_size, + decompressed_size, + offset(compressed_size), ) - for _ in range(blocksInfoCount) + for ( + decompressed_size, + compressed_size, + flags, + ) in blocksInfoReader.unpack_array("IIH", blocksInfoCount) ] - nodesCount = blocksInfoReader.read_int() - m_DirectoryInfo = [ - DirectoryInfoFS( - blocksInfoReader.read_long(), # offset - blocksInfoReader.read_long(), # size - blocksInfoReader.read_u_int(), # flags - blocksInfoReader.read_string_to_null(), # path + directory_count = blocksInfoReader.read_int() + self.directory_infos = [ + DirectoryInfo( + offset=blocksInfoReader.read_long(), # offset + size=blocksInfoReader.read_long(), # size + flags=blocksInfoReader.read_u_int(), # flags + path=blocksInfoReader.read_string_to_null(), # path ) - for _ in range(nodesCount) + for _ in range(directory_count) ] - if m_BlocksInfo: - self._block_info_flags = m_BlocksInfo[0].flags + if seek_back != -1: + reader.seek(seek_back) if ( isinstance(self.dataflags, ArchiveFlags) @@ -173,248 +218,333 @@ def read_fs(self, reader: EndianBinaryReader): ): reader.align_stream(16) - blocksReader = EndianBinaryReader( + self.directory_reader = EndianBinaryReader( b"".join( - self.decompress_data( - reader.read_bytes(blockInfo.compressedSize), - blockInfo.uncompressedSize, + self._decompress_data( + reader.read_bytes(blockInfo.compressed_size), + blockInfo.decompressed_size, blockInfo.flags, i, ) - for i, blockInfo in enumerate(m_BlocksInfo) + for i, blockInfo in enumerate(self.block_infos) ), offset=(blocksInfoReader.real_offset()), ) + return self - return m_DirectoryInfo, blocksReader + def dump( + self, + writer: Optional[EndianBinaryWriter] = None, + block_chunk_flag: Optional[int] = None, + block_chunk_size: Optional[int] = None, + ) -> EndianBinaryWriter: + # File Structure: + # header: + # ... + # size + # compressedSize + # uncompressedSize + # dataflags + # ..align/seek to end + # compressed block info data + # hash + # blockCount + # blockInfos + # directoryCount + # directoryInfos + # compressed directory data (offset of previous in total) + + writer = super().dump(writer) + + # munch childs/files together + new_directory_infos: List[DirectoryInfo] = [] + childs = self.childs or self.directory_infos + directory_info_flag = ( + self.directory_infos[0].flags if self.directory_infos else 0 + ) - def save(self, packer=None): + directory_datas = [] + uncompressed_directory_datas_size = 0 + + childs = self.childs or self.directory_infos + for child in childs: + if isinstance(child, File): + child_data = child.dump().get_bytes() + path = child.name + else: + self.directory_reader.seek(child.offset) + child_data = self.directory_reader.read(child.size) + path = child.path + + new_directory_infos.append( + DirectoryInfo( + path=path, + offset=uncompressed_directory_datas_size, + size=len(child_data), + flags=directory_info_flag, + ) + ) + directory_datas.append(child_data) + uncompressed_directory_datas_size += len(child_data) + directory_datas = b"".join(directory_datas) + + # compress blocks + new_block_infos: List[BlockInfo] = [] + compressed_directory_datas = [] + compressed_directory_datas_size = 0 + + block_chunk_flag = block_chunk_flag or self.block_infos[0].flags + block_chunk_size = block_chunk_size or self.block_infos[0].decompressed_size + + for chunk_start in range(0, len(directory_datas), block_chunk_size): + chunk = directory_datas[chunk_start : chunk_start + block_chunk_size] + compressed_chunk = self._compress_data(chunk, block_chunk_flag) + new_block_infos.append( + BlockInfo( + flags=block_chunk_flag, + compressed_size=len(compressed_chunk), + decompressed_size=len(chunk), + offset=compressed_directory_datas_size, + ) + ) + compressed_directory_datas.append(compressed_chunk) + compressed_directory_datas_size += len(compressed_chunk) + + compressed_directory_datas = b"".join(compressed_directory_datas) + del directory_datas + + # write block info + block_info_writer = EndianBinaryWriter(endian=">") + # decompressed_data_hash seems to be nearly always 16x 0 + block_info_writer.write_bytes(self.decompressed_data_hash) + block_info_writer.write_int(len(new_block_infos)) + for block_info in new_block_infos: + block_info_writer.write_int(block_info.decompressed_size) + block_info_writer.write_int(block_info.compressed_size) + block_info_writer.write_u_short(block_info.flags) + block_info_writer.write_int(len(new_directory_infos)) + for directory_info in new_directory_infos: + block_info_writer.write_long(directory_info.offset) + block_info_writer.write_long(directory_info.size) + block_info_writer.write_u_int(directory_info.flags) + block_info_writer.write_string_to_null(directory_info.path) + + uncompressed_block_info_size = block_info_writer.tell() + compressed_block_info = self._compress_data( + block_info_writer.bytes, self.dataflags + ) + del block_info_writer + + # estimate size + def align(alignment: int, value: int) -> int: + return (alignment - value % alignment) % alignment + + size = writer.Position + 8 + 12 # total size, block header sizes & flag + if self.stream_version >= 7 or self.uses_block_alignment: + size += align(16, size) + if ( + self.dataflags & ArchiveFlags.BlocksInfoAtTheEnd + ): # kArchiveBlocksInfoAtTheEnd + if ( + isinstance(self.dataflags, ArchiveFlags) + and self.dataflags & ArchiveFlags.BlockInfoNeedPaddingAtStart + ): + size += align(16, size) + size += len(compressed_directory_datas) + size += len(compressed_block_info) + else: + size += len(compressed_block_info) + if ( + isinstance(self.dataflags, ArchiveFlags) + and self.dataflags & ArchiveFlags.BlockInfoNeedPaddingAtStart + ): + size += align(16, size) + size += len(compressed_directory_datas) + + # write file + writer.write_long(size) + writer.write_u_int(len(compressed_block_info)) + writer.write_u_int(uncompressed_block_info_size) + dataflags = self.dataflags + if dataflags & dataflags.UsesAssetBundleEncryption: + dataflags ^= dataflags.UsesAssetBundleEncryption + writer.write_u_int(dataflags) + + if self.stream_version >= 7 or self.uses_block_alignment: + writer.align_stream(16) + + if ( + self.dataflags & ArchiveFlags.BlocksInfoAtTheEnd + ): # kArchiveBlocksInfoAtTheEnd + if ( + isinstance(self.dataflags, ArchiveFlags) + and self.dataflags & ArchiveFlags.BlockInfoNeedPaddingAtStart + ): + writer.align_stream(16) + writer.write_bytes(compressed_directory_datas) + writer.write_bytes(compressed_block_info) + else: + writer.write_bytes(compressed_block_info) + if ( + isinstance(self.dataflags, ArchiveFlags) + and self.dataflags & ArchiveFlags.BlockInfoNeedPaddingAtStart + ): + writer.align_stream(16) + writer.write_bytes(compressed_directory_datas) + + return writer + + def _decompress_data( + self, + compressed_data: bytes, + uncompressed_size: int, + flags: Union[int, ArchiveFlags, ArchiveFlagsOld], + index: int = 0, + ) -> bytes: """ - Rewrites the BundleFile and returns it as bytes object. - - packer: - can be either one of the following strings - or tuple consisting of (block_info_flag, data_flag) - allowed strings: - none - no compression, default, safest bet - lz4 - lz4 compression - original - uses the original flags + Parameters + ---------- + compressed_data : bytes + The compressed data. + uncompressed_size : int + The uncompressed size of the data. + flags : int + The flags of the data. + + Returns + ------- + bytes + The decompressed data.""" + comp_flag = CompressionFlags(flags & ArchiveFlags.CompressionTypeMask) + + if comp_flag == CompressionFlags.LZMA: # LZMA + return CompressionHelper.decompress_lzma(compressed_data) + elif comp_flag in [CompressionFlags.LZ4, CompressionFlags.LZ4HC]: # LZ4, LZ4HC + if self.decryptor is not None and flags & 0x100: + compressed_data = self.decryptor.decrypt_block(compressed_data, index) + return CompressionHelper.decompress_lz4(compressed_data, uncompressed_size) + elif comp_flag == CompressionFlags.LZHAM: # LZHAM + raise NotImplementedError("LZHAM decompression not implemented") + else: + return compressed_data + + def _compress_data( + self, data: bytes, flags: Union[int, ArchiveFlags, ArchiveFlagsOld] + ) -> bytes: """ - # file_header - # signature (string_to_null) - # format (int) - # version_player (string_to_null) - # version_engine (string_to_null) - writer = EndianBinaryWriter() + Parameters + ---------- + data : bytes + The data to compress. + flags : int + The flags of the data. - writer.write_string_to_null(self.signature) - writer.write_u_int(self.version) - writer.write_string_to_null(self.version_player) - writer.write_string_to_null(self.version_engine) - - if self.signature == "UnityArchive": - raise NotImplementedError("BundleFile - UnityArchive") - elif self.signature in ["UnityWeb", "UnityRaw"]: - self.save_web_raw(writer) - elif self.signature == "UnityFS": - if not packer or packer == "none": - self.save_fs(writer, 64, 64) - elif packer == "original": - self.save_fs( - writer, - data_flag=self.dataflags, - block_info_flag=self._block_info_flags, - ) - elif packer == "lz4": - self.save_fs(writer, data_flag=194, block_info_flag=2) - elif packer == "lzma": - self.save_fs(writer, data_flag=65, block_info_flag=1) - elif isinstance(packer, tuple): - self.save_fs(writer, *packer) - else: - raise NotImplementedError("UnityFS - Packer:", packer) - return writer.bytes + Returns + ------- + bytes + The compressed data.""" + comp_flag = CompressionFlags(flags & ArchiveFlags.CompressionTypeMask) - def save_fs(self, writer: EndianBinaryWriter, data_flag: int, block_info_flag: int): - # header - # compressed blockinfo (block details & directionary) - # compressed assets + if comp_flag == CompressionFlags.LZMA: # LZMA + return CompressionHelper.compress_lzma(data) + elif comp_flag in [CompressionFlags.LZ4, CompressionFlags.LZ4HC]: # LZ4, LZ4HC + return CompressionHelper.compress_lz4(data) + elif comp_flag == CompressionFlags.LZHAM: # LZHAM + raise NotImplementedError("LZHAM compression not implemented") + else: + return data - # 0b1000000 / 0b11000000 | 64 / 192 - uncompressed - # 0b11000010 | 194 - lz4 - # block_info_flag - # 0 / 0b1000000 | 0 / 64 - uncompressed - # 0b1 | 1 - lzma - # 0b10 | 2 - lz4 - # 0b11 | 3 - lz4hc [not implemented] - # 0b100 | 4 - lzham [not implemented] - # data_flag +@parseable_filetype +class BundleFileWeb(BundleFile): + byteStart: int + numberOfLevelsToDownloadBeforeStreaming: int + hash: Optional[Annotated[bytes, 16]] + crc: Optional[int] + completeFileSize = Optional[int] + fileInfoHeaderSize = Optional[int] - # header: - # bundle_size (long) - # compressed_size (int) - # uncompressed_size (int) - # flag (int) - # ?padding? (bool) - # This will be written at the end, - # because the size can only be calculated after the data compression, - - # block_info: - # *flag & 0x80 ? at the end : right after header - # *decompression via flag & 0x3F - # *read compressed_size -> uncompressed_size - # 0x10 offset - # *read blocks infos of the data stream - # count (int) - # ( - # uncompressed_size(uint) - # compressed_size (uint) - # flag(short) - # ) - # *decompression via info.flag & 0x3F - - # *afterwards the file positions - # file_count (int) - # ( - # offset (long) - # size (long) - # flag (int) - # name (string_to_null) - # ) - - # file list & file data - # prep nodes and build up block data - data_writer = EndianBinaryWriter() - files = [ - ( - name, - f.flags, - data_writer.write_bytes( - f.bytes - if isinstance(f, (EndianBinaryReader, EndianBinaryWriter)) - else f.save() - ), + @classmethod + def probe(cls, reader: EndianBinaryReader) -> bool: + signature = reader.read_string_to_null() + endian = reader.endian + reader.endian = ">" + version = reader.read_u_int() + reader.endian = endian + + return signature == "UnityWeb" or (signature == "UnityRaw" and version != 6) + + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + reader = self._opt_get_set_reader(reader) + + super().parse(reader) + + version = self.stream_version + if version >= 4: + self.hash = reader.read_bytes(16) + self.crc = reader.read_u_int() + + self.byteStart = reader.read_u_int() + headerSize = reader.read_u_int() + self.numberOfLevelsToDownloadBeforeStreaming = reader.read_u_int() + levelCount = reader.read_int() + self.block_infos = [ + BlockInfo( + compressed_size=reader.read_u_int(), # compressedSize + decompressed_size=reader.read_u_int(), # uncompressedSize + flags=0, + offset=0, ) - for name, f in self.files.items() + for _ in range(levelCount) ] - file_data = data_writer.bytes - data_writer.dispose() + block_offset = 0 + for block in self.block_infos: + block.offset = block_offset + block_offset += block.compressed_size - file_data, block_info = CompressionHelper.chunk_based_compress( - file_data, block_info_flag - ) + if version >= 2: + self.completeFileSize = reader.read_u_int() + + if version >= 3: + self.fileInfoHeaderSize = reader.read_u_int() - # write the block_info - # uncompressedDataHash - block_writer = EndianBinaryWriter(b"\x00" * 0x10) - # data block info - block_writer.write_int(len(block_info)) - for block_uncompressed_size, block_compressed_size, block_flag in block_info: - # uncompressed size - block_writer.write_u_int(block_uncompressed_size) - # compressed size - block_writer.write_u_int(block_compressed_size) - # flag - block_writer.write_u_short(block_flag) - - # file block info - if not data_flag & 0x40: - raise NotImplementedError( - "UnityPy always writes DirectoryInfo, so data_flag must include 0x40" + reader.seek(headerSize) + + uncompressedBytes = reader.read_bytes(self.block_infos[-1].compressed_size) + if self.signature == "UnityWeb": + uncompressedBytes = CompressionHelper.decompress_lzma( + uncompressedBytes, True ) - # file count - block_writer.write_int(len(files)) - offset = 0 - for f_name, f_flag, f_len in files: - # offset - block_writer.write_long(offset) - # size - block_writer.write_long(f_len) - offset += f_len - # flag - block_writer.write_u_int(f_flag) - # name - block_writer.write_string_to_null(f_name) - - # compress the block data - block_data = block_writer.bytes - block_writer.dispose() - - uncompressed_block_data_size = len(block_data) - - switch = data_flag & 0x3F - if switch == 1: # LZMA - block_data = CompressionHelper.compress_lzma(block_data) - elif switch in [2, 3]: # LZ4, LZ4HC - block_data = CompressionHelper.compress_lz4(block_data) - elif switch == 4: # LZHAM - raise NotImplementedError - - compressed_block_data_size = len(block_data) - - # write the header info - ## file size - 0 for now, will be set at the end - writer_header_pos = writer.Position - writer.write_long(0) - # compressed blockInfoBytes size - writer.write_u_int(compressed_block_data_size) - # uncompressed size - writer.write_u_int(uncompressed_block_data_size) - # compression and file layout flag - writer.write_u_int(data_flag) - - if self._uses_block_alignment: - # UnityFS\x00 - 8 - # size 8 - # comp sizes 4+4 - # flag 4 - # sum : 28 -> +8 alignment - writer.align_stream(16) - if data_flag & 0x80: # at end of file - if data_flag & 0x200: - writer.align_stream(16) - writer.write(file_data) - writer.write(block_data) - else: - writer.write(block_data) - if data_flag & 0x200: - writer.align_stream(16) - writer.write(file_data) - - writer_end_pos = writer.Position - writer.Position = writer_header_pos - # correct file size - writer.write_long(writer_end_pos) - writer.Position = writer_end_pos - - - def save_web_raw(self, writer: EndianBinaryWriter): - # (version >= 4) hash - # (version >= 4) crc - # minimumStreamedBytes - # headerSize - # numberOfLevelsToDownloadBeforeStreaming - # levelCount - # compressedSize * levelCount - # uncompressedSize * levelCount - # (version >= 2) completeFileSize - # (version >= 3) file_info_header_size - # compressed assets - - if self.version > 3: - raise NotImplementedError("Saving Unity Web bundles with version > 3 is not supported") + directory_reader = EndianBinaryReader(uncompressedBytes, offset=headerSize) + self.directory_reader = directory_reader + nodesCount = directory_reader.read_int() + self.directory_infos = [ + DirectoryInfo( + path=directory_reader.read_string_to_null(), # path + offset=directory_reader.read_u_int(), # offset + size=directory_reader.read_u_int(), # size + ) + for _ in range(nodesCount) + ] + return self + + def dump(self, writer: Optional[EndianBinaryWriter] = None) -> EndianBinaryWriter: + writer = super().dump(writer) # Calculate fileInfoHeaderSize for set offsets file_info_header_size = 4 # for nodesCount - - for file_name in self.files.keys(): - file_info_header_size += len(file_name.encode()) + 1 # +1 for null terminator + childs = self.childs or self.directory_infos + for child in childs: + file_info_header_size += ( + len(child.name.encode("utf8")) + 1 + ) # +1 for null terminator file_info_header_size += 4 * 2 # 4 bytes each for offset and size - file_info_header_padding_size = 4 - (file_info_header_size % 4) if file_info_header_size % 4 != 0 else 0 + file_info_header_padding_size = ( + 4 - (file_info_header_size % 4) if file_info_header_size % 4 != 0 else 0 + ) file_info_header_size += file_info_header_padding_size # Prepare directory info @@ -424,15 +554,15 @@ def save_web_raw(self, writer: EndianBinaryWriter): file_content_writer = EndianBinaryWriter() current_offset = file_info_header_size - for file_name, f in self.files.items(): - directory_info_writer.write_string_to_null(file_name) + for child in childs: + directory_info_writer.write_string_to_null(child.name) directory_info_writer.write_u_int(current_offset) - # Get file content - if isinstance(f, (EndianBinaryReader, EndianBinaryWriter)): - file_data = f.bytes + if isinstance(child, File): + file_data = child.dump().get_bytes() else: - file_data = f.save() + self.directory_reader.seek(child.offset) + file_data = self.directory_reader.read_bytes(child.size) file_size = len(file_data) directory_info_writer.write_u_int(file_size) @@ -440,7 +570,7 @@ def save_web_raw(self, writer: EndianBinaryWriter): file_content_writer.write_bytes(file_data) current_offset += file_size - directory_info_writer.write(b'\x00' * file_info_header_padding_size) + directory_info_writer.write(b"\x00" * file_info_header_padding_size) uncompressed_directory_info = directory_info_writer.bytes uncompressed_file_content = file_content_writer.bytes @@ -448,24 +578,29 @@ def save_web_raw(self, writer: EndianBinaryWriter): uncompressed_content = uncompressed_directory_info + uncompressed_file_content compressed_content = uncompressed_content if self.signature == "UnityWeb": - compressed_content = CompressionHelper.compress_lzma(uncompressed_content, True) + compressed_content = CompressionHelper.compress_lzma( + uncompressed_content, True + ) # Write header - header_size = writer.Position + 24 # assuming levelCount = 1 - if self.version >= 2: + header_size = writer.Position + 24 # assuming levelCount = 1 + if self.stream_version >= 2: header_size += 4 - if self.version >= 3: + if self.stream_version >= 3: header_size += 4 - if self.version >= 4: + if self.stream_version >= 4: header_size += 20 + # pad to multiple of 4 header_size = (header_size + 3) & ~3 - if self.version >= 4: - writer.write_bytes(self._hash) + if self.stream_version >= 4: + writer.write_bytes(self.hash) writer.write_u_int(self.crc) - writer.write_u_int(header_size + len(compressed_content)) # minimumStreamedBytes (same as completeFileSize) + writer.write_u_int( + header_size + len(compressed_content) + ) # minimumStreamedBytes (same as completeFileSize) writer.write_u_int(header_size) # headerSize writer.write_u_int(1) # numberOfLevelsToDownloadBeforeStreaming (always 1) writer.write_int(1) # levelCount (always 1) @@ -473,56 +608,18 @@ def save_web_raw(self, writer: EndianBinaryWriter): writer.write_u_int(len(compressed_content)) # compressedSize writer.write_u_int(len(uncompressed_content)) # uncompressedSize - if self.version >= 2: - writer.write_u_int(header_size + len(compressed_content)) # completeFileSize + if self.stream_version >= 2: + writer.write_u_int( + header_size + len(compressed_content) + ) # completeFileSize - if self.version >= 3: + if self.stream_version >= 3: writer.write_u_int(file_info_header_size) # file_info_header_size # align header writer.align_stream(4) # Write compressed content - writer.write(compressed_content) - - - def decompress_data( - self, - compressed_data: bytes, - uncompressed_size: int, - flags: Union[int, ArchiveFlags, ArchiveFlagsOld], - index: int = 0, - ) -> bytes: - """ - Parameters - ---------- - compressed_data : bytes - The compressed data. - uncompressed_size : int - The uncompressed size of the data. - flags : int - The flags of the data. - - Returns - ------- - bytes - The decompressed data.""" - comp_flag = CompressionFlags(flags & ArchiveFlags.CompressionTypeMask) - - if comp_flag == CompressionFlags.LZMA: # LZMA - return CompressionHelper.decompress_lzma(compressed_data) - elif comp_flag in [CompressionFlags.LZ4, CompressionFlags.LZ4HC]: # LZ4, LZ4HC - if self.decryptor is not None and flags & 0x100: - compressed_data = self.decryptor.decrypt_block(compressed_data, index) - return CompressionHelper.decompress_lz4(compressed_data, uncompressed_size) - elif comp_flag == CompressionFlags.LZHAM: # LZHAM - raise NotImplementedError("LZHAM decompression not implemented") - else: - return compressed_data + writer.write_bytes(compressed_content) - def get_version_tuple(self) -> Tuple[int, int, int]: - """Returns the version as a tuple.""" - version = self.version_engine - if not version or version == "0.0.0": - version = config.get_fallback_version() - return tuple(map(int, reVersion.match(version).groups())) + return writer diff --git a/UnityPy/files/File.py b/UnityPy/files/File.py index 34cd3f0d..213b5d04 100644 --- a/UnityPy/files/File.py +++ b/UnityPy/files/File.py @@ -1,165 +1,247 @@ from __future__ import annotations -from collections import namedtuple -from os.path import basename -from typing import TYPE_CHECKING, Dict, Optional +from abc import ABC, ABCMeta, abstractmethod +from collections import defaultdict +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterator, + List, + Optional, + Self, + Type, + TypeVar, + cast, +) + +from attrs import define -from ..helpers import ImportHelper from ..streams import EndianBinaryReader, EndianBinaryWriter if TYPE_CHECKING: - from ..environment import Environment + from ..classes import Object, PPtr + from .ObjectReader import ObjectReader -DirectoryInfo = namedtuple("DirectoryInfo", "path offset size") +PARSEABLE_FILETYPES: List[Type[File]] = [] +T = TypeVar("T") -class File(object): + +def parseable_filetype(cls: Type[T]) -> Type[T]: + """Decorator to register a class as parseable.""" + assert issubclass(cls, File) + PARSEABLE_FILETYPES.append(cls) + return cls + + +def parse_file( + reader: EndianBinaryReader, + name: str, + path: Optional[str] = None, + parent: Optional[ContainerFile] = None, + is_dependency: bool = False, +) -> Optional[File]: + start_pos = reader.tell() + for cls in PARSEABLE_FILETYPES: + try: + probe_result = cls.probe(reader) + except (EOFError, UnicodeDecodeError, IndexError): + probe_result = False + + reader.seek(start_pos) + + if probe_result: + file = cls(name, path, parent, is_dependency) + file.parse(reader) + return file + + +class File(ABC, metaclass=ABCMeta): name: str - files: Dict[str, File] - environment: Environment - cab_file: str - is_changed: bool - signature: str - packer: str + path: str + parent: Optional[ContainerFile] is_dependency: bool - parent: Optional[File] + reader: Optional[EndianBinaryReader] def __init__( self, - parent: Optional[File] = None, - name: Optional[str] = None, + name: str, + path: str, + parent: Optional[ContainerFile] = None, is_dependency: bool = False, + reader: Optional[EndianBinaryReader] = None, ): - self.files = {} - self.is_changed = False - self.cab_file = "CAB-UnityPy_Mod.resS" + self.name = name + self.path = path or name self.parent = parent - self.environment = self.environment = ( - getattr(parent, "environment", parent) if parent else None - ) - self.name = basename(name) if isinstance(name, str) else "" self.is_dependency = is_dependency + self.reader = reader - def get_assets(self): - if isinstance(self, SerializedFile.SerializedFile): - return self - - for f in self.files.values(): - if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): - for asset in f.get_assets(): - yield asset - elif isinstance(f, SerializedFile.SerializedFile): - yield f - - def get_filtered_objects(self, obj_types=[]): - if len(obj_types) == 0: - return self.get_objects() - for f in self.files.values(): - if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): - for obj in f.objects: - if obj.type in obj_types: - yield obj - elif isinstance(f, SerializedFile.SerializedFile): - for obj in f.objects.values(): - if obj.type in obj_types: - yield obj - - def get_objects(self): - for f in self.files.values(): - if isinstance(f, (BundleFile.BundleFile, WebFile.WebFile)): - for obj in f.objects: - yield obj - elif isinstance(f, SerializedFile.SerializedFile): - for obj in f.objects.values(): - yield obj - elif isinstance(f, ObjectReader.ObjectReader): - yield f - - def read_files(self, reader: EndianBinaryReader, files: list): - # read file data and convert it - for node in files: - reader.Position = node.offset - name = node.path - node_reader = EndianBinaryReader( - reader.read(node.size), offset=(reader.BaseOffset + node.offset) - ) - f = ImportHelper.parse_file( - node_reader, self, name, is_dependency=self.is_dependency - ) + @classmethod + @abstractmethod + def probe(cls, reader: EndianBinaryReader) -> bool: + """Check if the file is of this type. - if isinstance(f, (EndianBinaryReader, SerializedFile.SerializedFile)): - if self.environment: - self.environment.register_cab(name, f) + Args: + reader (EndianBinaryReader): The reader to read from. + + Returns: + bool: Whether the file is of this type. + """ + pass - # required for BundleFiles - f.flags = getattr(node, "flags", 0) - self.files[name] = f + @abstractmethod + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + """Parse the file from the reader. - def get_writeable_cab(self, name: str = None): + Args: + reader (EndianBinaryReader): The reader to read from. """ - Creates a new cab file in the bundle that contains the given data. - This is usefull for asset types that use resource files. + pass + + @abstractmethod + def dump(self, writer: Optional[EndianBinaryWriter] = None) -> EndianBinaryWriter: + """Dump the file to the writer. + + Args: + writer (EndianBinaryWriter): The writer to write to. """ + pass - if not name: - name = self.cab_file - - if not name: - return None - - if name in self.files: - if isinstance(self.files[name], EndianBinaryWriter): - return self.files[name] - else: - raise ValueError( - "This cab already exists and isn't an EndianBinaryWriter" - ) - - writer = EndianBinaryWriter() - # try to find another resource file to copy the flags from - for fname, f in self.files.items(): - if fname.endswith(".resS"): - writer.flags = f.flags - writer.endian = f.endian - break - else: - writer.flags = 0 - writer.name = name - self.files[name] = writer - return writer + def __enter__(self): + return self - @property - def container(self): - return { - path: obj - for f in self.files.values() - if isinstance(f, File) - for path, obj in f.container.items() - } + def __exit__(self): + pass - def get(self, key, default=None): - return getattr(self, key, default) + @abstractmethod + def get_objects(self) -> List[ObjectReader[Any]]: + """Get all objects contained in this file and its childs. - def keys(self): - return self.files.keys() + Returns: + List[Object]: The objects in this file. + """ + pass + + @abstractmethod + def get_containers(self) -> Dict[str, List[PPtr[Object]]]: + """Get all containers contained in this file and its childs. + + Returns: + List[Tuple[str, Object]]: The containers in this file. + """ + pass + + def _opt_get_set_reader( + self, reader: Optional[EndianBinaryReader] = None + ) -> EndianBinaryReader: + if reader is not None: + self.reader = reader + if self.reader is None: + raise ValueError("No reader provided") + return self.reader + + +class ResourceFile(File): + reader: EndianBinaryReader # type: ignore + + @classmethod + def probe(cls, reader: EndianBinaryReader) -> bool: + return True + + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + if reader is not None: + self.reader = reader # type: ignore + return self + + def dump(self, writer: Optional[EndianBinaryWriter] = None) -> EndianBinaryWriter: + if writer is None: + writer = EndianBinaryWriter(endian="<") + writer.write_bytes(self.reader.get_bytes()) + return writer + + def get_objects(self) -> List[ObjectReader[Any]]: + raise ValueError("ResourceFile does not contain objects") + + def get_containers(self) -> Dict[str, List[ObjectReader[Any]]]: + raise ValueError("ResourceFile does not contain containers") - def items(self): - return self.files.items() - def values(self): - return self.files.values() +@define(slots=True) +class DirectoryInfo: + offset: int + size: int + path: str + flags: Optional[int] = None - def __getitem__(self, item): - return self.files[item] - def __repr__(self): - return f"<{self.__class__.__name__}>" +# TODO: refractor name to avoid confusion with get_container / container paths +class ContainerFile(File, ABC, metaclass=ABCMeta): + childs: List[File] + directory_infos: List[DirectoryInfo] + directory_reader: Optional[EndianBinaryReader] - def mark_changed(self): - if isinstance(self.parent, File): - self.parent.mark_changed() - self.is_changed = True + # @abstractmethod + # def get_serialized_files(self) -> List[SerializedFile]: + # """Get all serialized files contained in this file and its childs. + # Returns: + # List[SerializedFile]: The serialized files in this file. + # """ + # pass + def __init__( + self, + name: str, + path: str, + parent: Optional[ContainerFile] = None, + is_dependency: bool = False, + reader: Optional[EndianBinaryReader] = None, + ): + super().__init__(name, path, parent, is_dependency, reader) + self.childs = [] + self.directory_infos = [] + self.directory_reader = None + + def parse_files(self): + """Parse the directory infos and all files in this file.""" + if self.directory_reader is None: + raise ValueError("directory_reader is not set") + + for info in self.directory_infos: + path_parts = info.path.split("/") + name = path_parts[-1] + path = f"{self.path}/{info.path}" + + # subreader = self.directory_reader.create_sub_reader(info.offset, info.size) + # subreader.seek(0) + self.directory_reader.seek(info.offset) + file = parse_file( + self.directory_reader, name, path, self, self.is_dependency + ) + self.childs.append( + file + if file is not None + else ResourceFile(name, path, self, False, self.directory_reader) + ) -# recursive import requires the import down here -from . import BundleFile, ObjectReader, SerializedFile, WebFile + def traverse(self) -> Iterator[File]: + if not self.childs and self.directory_infos: + self.parse_files() + for child in self.childs: + if isinstance(child, ContainerFile): + yield from child.traverse() + elif isinstance(child, ResourceFile): + continue + yield child + + def get_objects(self) -> List[ObjectReader[Any]]: + return [obj for child in self.traverse() for obj in child.get_objects()] + + def get_containers(self) -> Dict[str, List[ObjectReader[Any]]]: + containers: Dict[str, List[ObjectReader[Any]]] = defaultdict(list) + for child in self.traverse(): + for container_path, obj in child.get_containers().items(): + containers[container_path].extend(obj) + + return cast(Dict[str, List[ObjectReader[Any]]], containers) diff --git a/UnityPy/files/ObjectReader.py b/UnityPy/files/ObjectReader.py index 4b9f7d62..87e8a542 100644 --- a/UnityPy/files/ObjectReader.py +++ b/UnityPy/files/ObjectReader.py @@ -11,7 +11,12 @@ if TYPE_CHECKING: from ..enums import BuildTarget - from ..files.SerializedFile import BuildType, SerializedFile, SerializedType + from ..files.SerializedFile import ( + BuildType, + SerializedFile, + SerializedFileHeader, + SerializedType, + ) T = TypeVar("T") @@ -103,7 +108,10 @@ def __init__(self, assets_file: SerializedFile, reader: EndianBinaryReader): self.stripped = reader.read_byte() def write( - self, header, writer: EndianBinaryWriter, data_writer: EndianBinaryWriter + self, + header: SerializedFileHeader, + writer: EndianBinaryWriter, + data_writer: EndianBinaryWriter, ): if self.assets_file.big_id_enabled: writer.write_long(self.path_id) diff --git a/UnityPy/files/WebFile.py b/UnityPy/files/WebFile.py index da7946aa..e5b8ee27 100644 --- a/UnityPy/files/WebFile.py +++ b/UnityPy/files/WebFile.py @@ -1,115 +1,152 @@ -from . import File -from ..helpers import CompressionHelper -from ..streams import EndianBinaryReader, EndianBinaryWriter +import gzip +from typing import List, Literal, Optional, Self, Tuple, cast +import brotli -class WebFile(File.File): +from ..streams import EndianBinaryWriter +from ..streams.EndianBinaryReader import EndianBinaryReader +from .File import ContainerFile, DirectoryInfo, parseable_filetype + +GZIP_MAGIC: bytes = b"\x1f\x8b" +BROTLI_MAGIC: bytes = b"brotli" + +TCompressionType = Literal["none", "gzip", "brotli"] + + +@parseable_filetype +class WebFile(ContainerFile): """A package which can hold other WebFiles, Bundles and SerialiedFiles. It may be compressed via gzip or brotli. files -- list of all files in the WebFile """ - def __init__(self, reader: EndianBinaryReader, parent: File, name=None, **kwargs): - """Constructor Method""" - super().__init__(parent=parent, name=name, **kwargs) + SIGNATURE_CHECK = b"UnityWebData" + signature: str + compression: TCompressionType + + @classmethod + def probe(cls, reader: EndianBinaryReader) -> bool: + start_pos = reader.tell() + if reader.read_bytes(2) == GZIP_MAGIC: + return True + + reader.seek(0x20) + if reader.read_bytes(6) == BROTLI_MAGIC: + return True + + reader.seek(start_pos) + if reader.read_bytes(len(cls.SIGNATURE_CHECK)) == cls.SIGNATURE_CHECK: + return True + + return False + + def parse(self, reader: Optional[EndianBinaryReader] = None) -> Self: + reader = self._opt_get_set_reader(reader) # check compression magic = reader.read_bytes(2) - reader.Position = 0 + reader.seek(0) - if magic == CompressionHelper.GZIP_MAGIC: - self.packer = "gzip" - data = CompressionHelper.decompress_gzip(reader.bytes) - reader = EndianBinaryReader(data, endian="<") + if magic == GZIP_MAGIC: + self.compression = "gzip" + compressed_data = reader.get_bytes() + decompressed_data = gzip.decompress(compressed_data) + reader = EndianBinaryReader(decompressed_data, endian="<") else: - reader.Position = 0x20 + reader.seek(0x20) magic = reader.read_bytes(6) - reader.Position = 0 - if CompressionHelper.BROTLI_MAGIC == magic: - self.packer = "brotli" - data = CompressionHelper.decompress_brotli(reader.bytes) - reader = EndianBinaryReader(data, endian="<") + reader.seek(0) + if BROTLI_MAGIC == magic: + self.compression = "brotli" + compressed_data = reader.read(reader.Length) + # no type hint for brotli.decompress + decompressed_data = cast(bytes, brotli.decompress(compressed_data)) # type: ignore + reader = EndianBinaryReader(decompressed_data, endian="<") else: - self.packer = "none" + self.compression = "none" reader.endian = "<" - # signature check - signature = reader.read_string_to_null() - if signature != "UnityWebData1.0": - return - self.signature = signature + # signature check not required as we already checked for it in probe + self.signature = reader.read_string_to_null() # read header -> contains file headers - head_length = reader.read_int() + file_header_end = reader.read_int() + self.directory_infos = [] + while reader.tell() < file_header_end: + self.directory_infos.append( + DirectoryInfo( + offset=reader.read_int(), + size=reader.read_int(), + path=reader.read_string(), + ) + ) + self.directory_reader = reader + return self + + def dump( + self, + writer: Optional[EndianBinaryWriter] = None, + compression: Optional[TCompressionType] = None, + ) -> EndianBinaryWriter: + if writer is None: + writer = EndianBinaryWriter(endian="<") + else: + raise NotImplementedError("WebFile - dump with writer") + + # write empty header to not having to keep the dumped files in memory + header_length = ( + # signature - ending with \0 + len(self.signature.encode("utf-8")) + + 1 + # file header end + + 4 + # directory infos + + sum( + # 4 - offset, 4 - size, 4 - string length, len(path) - string + 12 + len(child.path) + for child in self.childs or self.directory_infos + ) + ) + start_offset = writer.tell() + writer.write_bytes(b"\0" * header_length) + + child_offset_sizes: List[Tuple[int, int]] = [] + if self.childs: + for child in self.childs: + child_data = child.dump().get_bytes() + child_offset_sizes.append((writer.tell(), len(child_data))) + writer.write_bytes(child_data) + else: + for directory_info in self.directory_infos: + child_offset_sizes.append((writer.tell(), directory_info.size)) + self.directory_reader.seek(directory_info.offset) + writer.write_bytes(self.directory_reader.read(directory_info.size)) + + # write header + writer.seek(start_offset) + writer.write_string_to_null(self.signature) + writer.write_int(header_length) + for child, (offset, size) in zip( + self.childs or self.directory_infos, child_offset_sizes + ): + writer.write_int(offset) + writer.write_int(size) + writer.write_string(child.path) - files = [] - while reader.Position < head_length: - offset = reader.read_int() - length = reader.read_int() - path_length = reader.read_int() - name = bytes(reader.read_bytes(path_length)).decode("utf-8") - files.append(File.DirectoryInfo(name, offset, length)) + writer.seek(0, 2) - self.read_files(reader, files) + compression = compression or self.compression + if compression == "gzip": + compressed_data = gzip.compress(writer.get_bytes()) + writer = EndianBinaryWriter(compressed_data, endian="<") + elif compression == "brotli": + compressed_data = cast(bytes, brotli.compress(writer.get_bytes())) # type: ignore + writer = EndianBinaryWriter(compressed_data, endian="<") - def save( - self, - files: dict = None, - packer: str = "none", - signature: str = "UnityWebData1.0", - ) -> bytes: - # solve defaults - if not files: - files = self.files - if not packer: - packer = self.packer - - # get raw data - files = { - name: f.bytes if isinstance(f, EndianBinaryReader) else f.save() - for name, f in files.items() - } - - # create writer - writer = EndianBinaryWriter(endian="<") - # signature - writer.write_string_to_null(signature) - - # data offset - offset = sum( - [ - writer.Position, # signature - sum( - len(path.encode("utf-8")) for path in files.keys() - ), # path of each file - 4 * 3 * len(files), # 3 ints per file - 4, # offset int - ] - ) + return writer - writer.write_int(offset) - # 1. file headers - for name, data in files.items(): - # offset - writer.write_int(offset) - # length - length = len(data) - writer.write_int(length) - offset += length - # path - enc_path = name.encode("utf-8") - writer.write_int(len(enc_path)) - writer.write(enc_path) - - # 2. file data - for data in files.values(): - writer.write(data) - - if packer == "gzip": - return CompressionHelper.compress_gzip(writer.bytes) - elif packer == "brotli": - return CompressionHelper.compress_brotli(writer.bytes) - else: - return writer.bytes +@parseable_filetype +class TuanjieWebFile(WebFile): + SIGNATURE_CHECK = b"TuanjieWebData" diff --git a/UnityPy/files/__init__.py b/UnityPy/files/__init__.py index bdaecc90..9c368a8a 100644 --- a/UnityPy/files/__init__.py +++ b/UnityPy/files/__init__.py @@ -1,5 +1,16 @@ -from .File import File, DirectoryInfo -from .SerializedFile import SerializedFile -from .BundleFile import BundleFile -from .WebFile import WebFile -from .ObjectReader import ObjectReader +from .BundleFile import ( + BundleFile as BundleFile, +) +from .BundleFile import ( + BundleFileFS as BundleFileFS, +) +from .BundleFile import ( + BundleFileWeb as BundleFileWeb, +) +from .File import ContainerFile as ContainerFile +from .File import File as File +from .File import parse_file as parse_file +from .ObjectReader import ObjectReader as ObjectReader +from .SerializedFile import SerializedFile as SerializedFile +from .WebFile import TuanjieWebFile as TuanjieWebFile +from .WebFile import WebFile as WebFile diff --git a/UnityPy/helpers/ImportHelper.py b/UnityPy/helpers/ImportHelper.py deleted file mode 100644 index 7e7e2a4b..00000000 --- a/UnityPy/helpers/ImportHelper.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations -import os -from typing import Union, List -from .CompressionHelper import BROTLI_MAGIC, GZIP_MAGIC -from ..enums import FileType -from ..streams import EndianBinaryReader -from .. import files - - -def file_name_without_extension(file_name: str) -> str: - return os.path.join( - os.path.dirname(file_name), os.path.splitext(os.path.basename(file_name))[0] - ) - - -def list_all_files(directory: str) -> List[str]: - return [ - val - for sublist in [ - [os.path.join(dir_path, filename) for filename in filenames] - for (dir_path, dirn_ames, filenames) in os.walk(directory) - if ".git" not in dir_path - ] - for val in sublist - ] - - -def find_all_files(directory: str, search_str: str) -> List[str]: - return [ - val - for sublist in [ - [ - os.path.join(dir_path, filename) - for filename in filenames - if search_str in filename - ] - for (dir_path, dirn_ames, filenames) in os.walk(directory) - if ".git" not in dir_path - ] - for val in sublist - ] - - -def check_file_type(input_) -> Union[FileType, EndianBinaryReader]: - if isinstance(input_, str) and os.path.isfile(input_): - reader = EndianBinaryReader(open(input_, "rb")) - elif isinstance(input_, EndianBinaryReader): - reader = input_ - else: - try: - reader = EndianBinaryReader(input_) - except: - return None, None - - if reader.Length < 20: - return FileType.ResourceFile, reader - - signature = reader.read_string_to_null(20) - - reader.Position = 0 - if signature in [ - "UnityWeb", - "UnityRaw", - "\xFA\xFA\xFA\xFA\xFA\xFA\xFA\xFA", - "UnityFS", - ]: - return FileType.BundleFile, reader - elif signature == "UnityWebData1.0": - return FileType.WebFile, reader - elif signature == "PK\x03\x04": - return FileType.ZIP, reader - else: - if reader.Length < 128: - return FileType.ResourceFile, reader - - magic = bytes(reader.read_bytes(2)) - reader.Position = 0 - if GZIP_MAGIC == magic: - return FileType.WebFile, reader - reader.Position = 0x20 - magic = bytes(reader.read_bytes(6)) - reader.Position = 0 - if BROTLI_MAGIC == magic: - return FileType.WebFile, reader - - # check if AssetsFile - old_endian = reader.endian - # read as if assetsfile and check version - # ReadHeader - reader.Position = 0 - metadata_size = reader.read_u_int() - file_size = reader.read_u_int() - version = reader.read_u_int() - data_offset = reader.read_u_int() - - if version >= 22: - endian = ">" if reader.read_boolean() else "<" - reserved = reader.read_bytes(3) - metadata_size = reader.read_u_int() - file_size = reader.read_long() - data_offset = reader.read_long() - unknown = reader.read_long() # unknown - - # reset - reader.endian = old_endian - reader.Position = 0 - # check info - if any( - ( - version < 0, - version > 100, - *[ - x < 0 or x > reader.Length - for x in [file_size, metadata_size, version, data_offset] - ], - file_size < metadata_size, - file_size < data_offset, - ) - ): - return FileType.ResourceFile, reader - else: - return FileType.AssetsFile, reader - - -def parse_file( - reader: EndianBinaryReader, - parent, - name: str, - typ: FileType = None, - is_dependency=False, -) -> Union[files.File, EndianBinaryReader]: - if typ is None: - typ, _ = check_file_type(reader) - if typ == FileType.AssetsFile and not name.endswith( - (".resS", ".resource", ".config", ".xml", ".dat") - ): - f = files.SerializedFile(reader, parent, name=name, is_dependency=is_dependency) - elif typ == FileType.BundleFile: - f = files.BundleFile(reader, parent, name=name, is_dependency=is_dependency) - elif typ == FileType.WebFile: - f = files.WebFile(reader, parent, name=name, is_dependency=is_dependency) - else: - f = reader - return f - - -def find_sensitive_path(dir: str, insensitive_path: str) -> Union[str, None]: - parts = os.path.split(insensitive_path.strip(os.path.sep)) - - sensitive_path = dir - for part in parts: - part_lower = part.lower() - part = next( - (name for name in os.listdir(sensitive_path) if name.lower() == part_lower), - None, - ) - if part is None: - return None - sensitive_path = os.path.join(sensitive_path, part) - - return sensitive_path diff --git a/UnityPy/helpers/Tpk.py b/UnityPy/helpers/Tpk.py index a636d7d1..e8f53dd4 100644 --- a/UnityPy/helpers/Tpk.py +++ b/UnityPy/helpers/Tpk.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from enum import IntEnum, IntFlag from importlib.resources import open_binary from io import BytesIO @@ -300,7 +301,11 @@ def fromStream(stream: BytesIO) -> UnityVersion: @staticmethod def fromString(version: str) -> UnityVersion: - return UnityVersion(version.split(".")) + match = re.match(r"(\d+)\.(\d+)\.(\d+)(([a-zA-Z]+)(\d+))?", version) + if not match: + raise ValueError("Invalid version string") + major, minor, patch, _, type, build = match.groups() + return UnityVersion.fromList(int(major), int(minor), int(patch), int(build)) @staticmethod def fromList( diff --git a/UnityPy/helpers/__init__.py b/UnityPy/helpers/__init__.py index 066f4b96..32a12cde 100644 --- a/UnityPy/helpers/__init__.py +++ b/UnityPy/helpers/__init__.py @@ -1 +1 @@ -from . import ArchiveStorageManager, CompressionHelper, ImportHelper, TypeTreeHelper \ No newline at end of file +from . import ArchiveStorageManager, CompressionHelper, TypeTreeHelper \ No newline at end of file diff --git a/UnityPy/streams/EndianBinaryReader.py b/UnityPy/streams/EndianBinaryReader.py index c71b415e..a8b38862 100644 --- a/UnityPy/streams/EndianBinaryReader.py +++ b/UnityPy/streams/EndianBinaryReader.py @@ -1,77 +1,74 @@ -import sys -from struct import Struct, unpack +from __future__ import annotations + import re -from typing import List, Union -from io import BytesIO, BufferedIOBase, IOBase, BufferedReader +from abc import ABC, ABCMeta, abstractmethod +from io import BytesIO, IOBase +from struct import Struct, unpack +from typing import BinaryIO, List, Optional, Union -reNot0 = re.compile(b"(.*?)\x00", re.S) +from ._defines import TYPE_PARAM_SIZE_LIST, Endianess -SYS_ENDIAN = "<" if sys.byteorder == "little" else ">" - -from ..math import Color, Matrix4x4, Quaternion, Vector2, Vector3, Vector4, Rectangle - -# generate unpack and unpack_from functions -TYPE_PARAM_SIZE_LIST = [ - ("short", "h", 2), - ("u_short", "H", 2), - ("int", "i", 4), - ("u_int", "I", 4), - ("long", "q", 8), - ("u_long", "Q", 8), - ("half", "e", 2), - ("float", "f", 4), - ("double", "d", 8), - ("vector2", "2f", 8), - ("vector3", "3f", 12), - ("vector4", "4f", 16), -] - -LOCALS = locals() -for endian_s, endian_l in (("<", "little"), (">", "big")): - for typ, param, _ in TYPE_PARAM_SIZE_LIST: - LOCALS[f"unpack_{endian_l}_{typ}"] = Struct(f"{endian_s}{param}").unpack - LOCALS[f"unpack_{endian_l}_{typ}_from"] = Struct( - f"{endian_s}{param}" - ).unpack_from +reNot0 = re.compile(b"(.*?)\x00", re.S) -class EndianBinaryReader: - endian: str +class EndianBinaryReader(ABC, metaclass=ABCMeta): + endian: Endianess Length: int Position: int BaseOffset: int def __new__( cls, - item: Union[bytes, bytearray, memoryview, BytesIO, str], - endian: str = ">", + item: Union[bytes, bytearray, memoryview, BytesIO, str, IOBase], + endian: Endianess = ">", offset: int = 0, ): + """ + Creates a new instance of EndianBinaryReader, choosing the appropriate subclass based on the type of the input. + + Args: + item: The data source (bytes, memoryview, file path, or stream). + endian: Endianness to use for reading data ('>' for big-endian, '<' for little-endian). + offset: Initial offset to set for reading. + + Returns: + An instance of either EndianBinaryReader_Memoryview or EndianBinaryReader_Streamable. + + Raises: + ValueError: If the provided `item` type is unsupported. + """ if isinstance(item, (bytes, bytearray, memoryview)): - obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Memoryview) - elif isinstance(item, (IOBase, BufferedIOBase)): - obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Streamable) + # Handle in-memory binary data + obj = super().__new__(EndianBinaryReader_Memoryview) + elif isinstance(item, IOBase): # Includes BufferedIOBase + # Handle stream-like objects + obj = super().__new__(EndianBinaryReader_Streamable) elif isinstance(item, str): - item = open(item, "rb") - obj = super(EndianBinaryReader, cls).__new__(EndianBinaryReader_Streamable) + # Handle file paths as input + try: + item = open(item, "rb") + obj = super().__new__(EndianBinaryReader_Streamable) + except FileNotFoundError as e: + raise ValueError(f"File not found: {item}") from e elif isinstance(item, EndianBinaryReader): - item = ( + # Wrap another EndianBinaryReader instance + new_item = ( item.stream if isinstance(item, EndianBinaryReader_Streamable) else item.view ) - return EndianBinaryReader(item, endian, offset) + return cls(new_item, endian, offset) elif hasattr(item, "read"): - if hasattr(item, "seek") and hasattr(item, "tell"): - obj = super(EndianBinaryReader, cls).__new__( - EndianBinaryReader_Streamable - ) + # Handle generic objects with a `read` method + if hasattr(item, "seek"): + obj = super().__new__(EndianBinaryReader_Streamable) else: item = item.read() - obj = super(EndianBinaryReader, cls).__new__( - EndianBinaryReader_Memoryview - ) + obj = super().__new__(EndianBinaryReader_Memoryview) + else: + raise ValueError(f"Unsupported input type: {type(item).__name__}") + # Initialize the chosen subclass obj.__init__(item, endian) return obj @@ -80,14 +77,33 @@ def __init__(self, item, endian=">", offset=0): self.BaseOffset = offset self.Position = 0 + def seek(self, offset: int, whence: int = 0) -> int: + if whence == 0: + self.Position = offset + elif whence == 1: + self.Position += offset + elif whence == 2: + self.Position = self.Length + offset + else: + raise ValueError("Invalid whence") + return self.Position + + def tell(self) -> int: + return self.Position + + def get_bytes(self) -> bytes: + return self.bytes + + @abstractmethod + def create_sub_reader(self, offset: int, length: int) -> EndianBinaryReader: + pass + @property def bytes(self): - # implemented by Streamable and Memoryview versions - return b"" + raise NotImplementedError(f"{self.__class__.__name__}.read not implemented!") def read(self, *args): - # implemented by Streamable and Memoryview versions - return b"" + raise NotImplementedError(f"{self.__class__.__name__}.read not implemented!") def read_byte(self) -> int: return unpack(self.endian + "b", self.read(1))[0] @@ -125,16 +141,6 @@ def read_double(self) -> float: def read_boolean(self) -> bool: return bool(unpack(self.endian + "?", self.read(1))[0]) - def read_string(self, size=None, encoding="utf8") -> str: - if size is None: - ret = self.read_string_to_null() - else: - ret = unpack(f"{self.endian}{size}is", self.read(size))[0] - try: - return ret.decode(encoding) - except UnicodeDecodeError: - return ret - def read_string_to_null(self, max_length=32767) -> str: ret = [] c = b"" @@ -145,58 +151,26 @@ def read_string_to_null(self, max_length=32767) -> str: raise ValueError("Unterminated string: %r" % ret) return b"".join(ret).decode("utf8", "surrogateescape") - def read_aligned_string(self) -> str: - length = self.read_int() + def read_string(self, size: Optional[int] = None, encoding: str = "utf8") -> str: + length = size if size is not None else self.read_int() if 0 < length <= self.Length - self.Position: string_data = bytes(self.read_bytes(length)) - result = string_data.decode("utf8", "surrogateescape") - self.align_stream() + result = string_data.decode(encoding, "surrogateescape") return result return "" + def read_aligned_string(self) -> str: + ret = self.read_string() + self.align_stream() + return ret + def align_stream(self, alignment=4): self.Position += (alignment - self.Position % alignment) % alignment - - def read_quaternion(self) -> Quaternion: - return Quaternion( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) - - def read_vector2(self) -> Vector2: - return Vector2(self.read_float(), self.read_float()) - - def read_vector3(self) -> Vector3: - return Vector3(self.read_float(), self.read_float(), self.read_float()) - - def read_vector4(self) -> Vector4: - return Vector4( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) - - def read_rectangle_f(self) -> Rectangle: - return Rectangle( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) - - def read_color_uint(self): - r = self.read_u_byte() - g = self.read_u_byte() - b = self.read_u_byte() - a = self.read_u_byte() - - return Color(r / 255.0, g / 255.0, b / 255.0, a / 255.0) - - def read_color4(self) -> Color: - return Color( - self.read_float(), self.read_float(), self.read_float(), self.read_float() - ) + return self.Position def read_byte_array(self) -> bytes: return self.read(self.read_int()) - def read_matrix(self) -> Matrix4x4: - return Matrix4x4(self.read_float_array(16)) - def read_array(self, command, length: int) -> list: return [command() for _ in range(length)] @@ -230,29 +204,12 @@ def read_long_array(self, length: int = None) -> List[int]: def read_u_long_array(self, length: int = None) -> List[int]: return self.read_array_struct("Q", length) - def read_u_int_array_array(self, length: int = None) -> List[List[int]]: - return self.read_array( - self.read_u_int_array, length if length is not None else self.read_int() - ) - def read_float_array(self, length: int = None) -> List[float]: return self.read_array_struct("f", length) def read_double_array(self, length: int = None) -> List[float]: return self.read_array_struct("d", length) - def read_string_array(self) -> List[str]: - return self.read_array(self.read_aligned_string, self.read_int()) - - def read_vector2_array(self) -> List[Vector2]: - return self.read_array(self.read_vector2, self.read_int()) - - def read_vector4_array(self) -> List[Vector4]: - return self.read_array(self.read_vector4, self.read_int()) - - def read_matrix_array(self) -> List[Matrix4x4]: - return self.read_array(self.read_matrix, self.read_int()) - def real_offset(self) -> int: """Returns offset in the underlying file. (Not working with unpacked streams.) @@ -263,6 +220,10 @@ def read_the_rest(self, obj_start: int, obj_size: int) -> bytes: """Returns the rest of the current reader bytes.""" return self.read_bytes(obj_size - (self.Position - obj_start)) + def unpack_array(self, string: str, count: int) -> list: + struct = Struct(f"{self.endian}{string}") + return list(struct.iter_unpack(self.read(count * struct.size))) + class EndianBinaryReader_Memoryview(EndianBinaryReader): __slots__ = ("view", "_endian", "BaseOffset", "Position", "Length") @@ -279,17 +240,14 @@ def endian(self): return self._endian @endian.setter - def endian(self, value: str): + def endian(self, value): if value not in ("<", ">"): raise ValueError("Invalid endian") if value != self._endian: - setattr( - self, - "__class__", - EndianBinaryReader_Memoryview_LittleEndian - if value == "<" - else EndianBinaryReader_Memoryview_BigEndian, - ) + for typ, _, _ in TYPE_PARAM_SIZE_LIST: + func_name_e = f"read_{value}_{typ}" + func_name = f"read_{typ}" + setattr(self, func_name, getattr(self, func_name_e)) self._endian = value @property @@ -328,134 +286,15 @@ def read_string_to_null(self, max_length=32767) -> str: self.Position = match.end() return ret - -class EndianBinaryReader_Memoryview_LittleEndian(EndianBinaryReader_Memoryview): - def read_u_short(self): - (ret,) = unpack_little_u_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_short(self): - (ret,) = unpack_little_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_int(self): - (ret,) = unpack_little_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_u_int(self): - (ret,) = unpack_little_u_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_long(self): - (ret,) = unpack_little_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_u_long(self): - (ret,) = unpack_little_u_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_half(self): - (ret,) = unpack_little_half_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_float(self): - (ret,) = unpack_little_float_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_double(self): - (ret,) = unpack_little_double_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_vector2(self): - (x, y) = unpack_little_vector2_from(self.view, self.Position) - self.Position += 8 - return Vector2(x, y) - - def read_vector3(self): - (x, y, z) = unpack_little_vector3_from(self.view, self.Position) - self.Position += 12 - return Vector3(x, y, z) - - def read_vector4(self): - (x, y, z, w) = unpack_little_vector4_from(self.view, self.Position) - self.Position += 16 - return Vector4(x, y, z, w) - - -class EndianBinaryReader_Memoryview_BigEndian(EndianBinaryReader_Memoryview): - def read_u_short(self): - (ret,) = unpack_big_u_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_short(self): - (ret,) = unpack_big_short_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_int(self): - (ret,) = unpack_big_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_u_int(self): - (ret,) = unpack_big_u_int_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_long(self): - (ret,) = unpack_big_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_u_long(self): - (ret,) = unpack_big_u_long_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_half(self): - (ret,) = unpack_big_half_from(self.view, self.Position) - self.Position += 2 - return ret - - def read_float(self): - (ret,) = unpack_big_float_from(self.view, self.Position) - self.Position += 4 - return ret - - def read_double(self): - (ret,) = unpack_big_double_from(self.view, self.Position) - self.Position += 8 - return ret - - def read_vector2(self): - (x, y) = unpack_big_vector2_from(self.view, self.Position) - self.Position += 8 - return Vector2(x, y) - - def read_vector3(self): - (x, y, z) = unpack_big_vector3_from(self.view, self.Position) - self.Position += 12 - return Vector3(x, y, z) - - def read_vector4(self): - (x, y, z, w) = unpack_big_vector4_from(self.view, self.Position) - self.Position += 16 - return Vector4(x, y, z, w) + def create_sub_reader(self, offset: int, length: int) -> EndianBinaryReader: + return EndianBinaryReader_Memoryview( + self.view, self.endian, self.BaseOffset + offset + ) class EndianBinaryReader_Streamable(EndianBinaryReader): __slots__ = ("stream", "_endian", "BaseOffset") - stream: BufferedReader + stream: BinaryIO def __init__(self, stream, endian=">", offset=0): self._endian = "" @@ -478,13 +317,10 @@ def endian(self, value): if value not in ("<", ">"): raise ValueError("Invalid endian") if value != self._endian: - setattr( - self, - "__class__", - EndianBinaryReader_Streamable_LittleEndian - if value == "<" - else EndianBinaryReader_Streamable_BigEndian, - ) + for typ, _, _ in TYPE_PARAM_SIZE_LIST: + func_name_e = f"read_{value}_{typ}" + func_name = f"read_{typ}" + setattr(self, func_name, getattr(self, func_name_e)) self._endian = value @property @@ -508,78 +344,27 @@ def dispose(self): self.stream.close() pass + def create_sub_reader(self, offset: int, length: int) -> EndianBinaryReader: + return EndianBinaryReader_Streamable(self.stream, self.endian, offset) -class EndianBinaryReader_Streamable_LittleEndian(EndianBinaryReader_Streamable): - def read_u_short(self): - return unpack_little_u_short(self.read(2))[0] - - def read_short(self): - return unpack_little_short(self.read(2))[0] - - def read_int(self): - return unpack_little_int(self.read(4))[0] - - def read_u_int(self): - return unpack_little_u_int(self.read(4))[0] - - def read_long(self): - return unpack_little_long(self.read(8))[0] - - def read_u_long(self): - return unpack_little_u_long(self.read(8))[0] - def read_half(self): - return unpack_little_half(self.read(2))[0] - - def read_float(self): - return unpack_little_float(self.read(4))[0] - - def read_double(self): - return unpack_little_double(self.read(8))[0] - - def read_vector2(self): - return Vector2(*unpack_little_vector2(self.read(8))) - - def read_vector3(self): - return Vector3(*unpack_little_vector3(self.read(12))) - - def read_vector4(self): - return Vector4(*unpack_little_vector4(self.read(16))) - - -class EndianBinaryReader_Streamable_BigEndian(EndianBinaryReader_Streamable): - def read_u_short(self): - return unpack_big_u_short(self.read(2))[0] - - def read_short(self): - return unpack_big_short(self.read(2))[0] - - def read_int(self): - return unpack_big_int(self.read(4))[0] - - def read_u_int(self): - return unpack_big_u_int(self.read(4))[0] - - def read_long(self): - return unpack_big_long(self.read(8))[0] - - def read_u_long(self): - return unpack_big_u_long(self.read(8))[0] - - def read_half(self): - return unpack_big_half(self.read(2))[0] +# generate endianed functions +for endian_s in Endianess.__args__: + for typ, param, _ in TYPE_PARAM_SIZE_LIST: - def read_float(self): - return unpack_big_float(self.read(4))[0] + def generate_funcs(): + func_name = f"read_{endian_s}_{typ}" + struct = Struct(f"{endian_s}{param}") - def read_double(self): - return unpack_big_double(self.read(8))[0] + def mv_func(self: EndianBinaryReader_Memoryview): + value = struct.unpack_from(self.view, self.Position)[0] + self.Position += struct.size + return value - def read_vector2(self): - return Vector2(*unpack_big_vector2(self.read(8))) + def st_func(self: EndianBinaryReader_Streamable): + return struct.unpack(self.stream)[0] - def read_vector3(self): - return Vector3(*unpack_big_vector3(self.read(12))) + setattr(EndianBinaryReader_Memoryview, func_name, mv_func) + setattr(EndianBinaryReader_Streamable, func_name, st_func) - def read_vector4(self): - return Vector4(*unpack_big_vector4(self.read(16))) + generate_funcs() diff --git a/UnityPy/streams/EndianBinaryReader.pyi b/UnityPy/streams/EndianBinaryReader.pyi new file mode 100644 index 00000000..af756b7f --- /dev/null +++ b/UnityPy/streams/EndianBinaryReader.pyi @@ -0,0 +1,63 @@ +from __future__ import annotations +from abc import ABC, ABCMeta, abstractmethod +from typing import List, Optional, BinaryIO + +from ._defines import Endianess + +class EndianBinaryReader(ABC, BinaryIO, metaclass=ABCMeta): + endian: Endianess + Length: int + Position: int + BaseOffset: int + + def __init__(self, item, endian=">", offset=0): ... + def seek(self, offset: int, whence: int = 0) -> int: ... + def tell(self) -> int: ... + def get_bytes(self) -> bytes: ... + @abstractmethod + def create_sub_reader(self, offset: int, length: int) -> EndianBinaryReader: + pass + + @property + def bytes(self) -> bytes: ... + def read(self, *args) -> bytes: ... + def read_byte(self) -> int: ... + def read_u_byte(self) -> int: ... + def read_bytes(self, num: int) -> bytes: ... + def read_short(self) -> int: ... + def read_int(self) -> int: ... + def read_long(self) -> int: ... + def read_u_short(self) -> int: ... + def read_u_int(self) -> int: ... + def read_u_long(self) -> int: ... + def read_float(self) -> float: ... + def read_double(self) -> float: ... + def read_boolean(self) -> bool: ... + def read_string_to_null(self, max_length=32767) -> str: ... + def read_string( + self, size: Optional[int] = None, encoding: str = "utf8" + ) -> str: ... + def read_aligned_string(self) -> str: ... + def align_stream(self, alignment=4) -> int: ... + def read_byte_array(self) -> bytes: ... + def read_array(self, command, length: int) -> list: ... + def read_array_struct(self, param: str, length: int = None) -> List[tuple]: ... + def read_boolean_array(self, length: int = None) -> List[bool]: ... + def read_u_byte_array(self, length: int = None) -> List[int]: ... + def read_u_short_array(self, length: int = None) -> List[int]: ... + def read_short_array(self, length: int = None) -> List[int]: ... + def read_int_array(self, length: int = None) -> List[int]: ... + def read_u_int_array(self, length: int = None) -> List[int]: ... + def read_long_array(self, length: int = None) -> List[int]: ... + def read_u_long_array(self, length: int = None) -> List[int]: ... + def read_float_array(self, length: int = None) -> List[float]: ... + def read_double_array(self, length: int = None) -> List[float]: ... + def real_offset(self) -> int: ... + def read_the_rest(self, obj_start: int, obj_size: int) -> bytes: ... + def unpack_array(self, string: str, count: int) -> list: ... + +class EndianBinaryReader_Memoryview(EndianBinaryReader): + view: memoryview + +class EndianBinaryReader_Streamable(EndianBinaryReader): + stream: BinaryIO diff --git a/UnityPy/streams/EndianBinaryWriter.py b/UnityPy/streams/EndianBinaryWriter.py index 6bb92969..923638a5 100644 --- a/UnityPy/streams/EndianBinaryWriter.py +++ b/UnityPy/streams/EndianBinaryWriter.py @@ -1,26 +1,46 @@ -import io +from io import BytesIO from struct import pack +from typing import BinaryIO, Union -from ..math import Color, Matrix4x4, Quaternion, Vector2, Vector3, Vector4, Rectangle +from ..math import Color, Matrix4x4, Quaternion, Rectangle, Vector2, Vector3, Vector4 +from ._defines import Endianess class EndianBinaryWriter: - endian: str + endian: Endianess Length: int Position: int - stream: io.BufferedReader + stream: BinaryIO - def __init__(self, input_=b"", endian=">"): + def __init__( + self, input_: Union[bytes, bytearray, BinaryIO] = b"", endian: Endianess = ">" + ): if isinstance(input_, (bytes, bytearray)): - self.stream = io.BytesIO(input_) + self.stream = BytesIO(input_) self.stream.seek(0, 2) - elif isinstance(input_, io.IOBase): + elif isinstance(input_, BinaryIO): self.stream = input_ else: raise ValueError("Invalid input type - %s." % type(input_)) self.endian = endian self.Position = self.stream.tell() + def seek(self, offset: int, whence: int = 0): + if whence == 0: + self.Position = offset + elif whence == 1: + self.Position += offset + elif whence == 2: + self.Position = self.Length + offset + else: + raise ValueError("Invalid whence") + + def tell(self) -> int: + return self.Position + + def get_bytes(self) -> bytes: + return self.bytes + @property def bytes(self): self.stream.seek(0) @@ -30,9 +50,9 @@ def bytes(self): def Length(self) -> int: pos = self.stream.tell() self.stream.seek(0, 2) - l = self.stream.tell() + length = self.stream.tell() self.stream.seek(pos) - return l + return length def dispose(self): self.stream.close() @@ -85,10 +105,13 @@ def write_string_to_null(self, value: str): self.write(value.encode("utf8", "surrogateescape")) self.write(b"\0") - def write_aligned_string(self, value: str): + def write_string(self, value: str): bstring = value.encode("utf8", "surrogateescape") self.write_int(len(bstring)) self.write(bstring) + + def write_aligned_string(self, value: str): + self.write_string(value) self.align_stream(4) def align_stream(self, alignment=4): @@ -96,49 +119,6 @@ def align_stream(self, alignment=4): align = (alignment - pos % alignment) % alignment self.write(b"\0" * align) - def write_quaternion(self, value: Quaternion): - self.write_float(value.X) - self.write_float(value.Y) - self.write_float(value.Z) - self.write_float(value.W) - - def write_vector2(self, value: Vector2): - self.write_float(value.X) - self.write_float(value.Y) - - def write_vector3(self, value: Vector3): - self.write_float(value.X) - self.write_float(value.Y) - self.write_float(value.Z) - - def write_vector4(self, value: Vector4): - self.write_float(value.X) - self.write_float(value.Y) - self.write_float(value.Z) - self.write_float(value.W) - - def write_rectangle_f(self, value: Rectangle): - self.write_float(value.x) - self.write_float(value.y) - self.write_float(value.width) - self.write_float(value.height) - - def write_color_uint(self, value: Color): - self.write_u_byte(value.R * 255) - self.write_u_byte(value.G * 255) - self.write_u_byte(value.B * 255) - self.write_u_byte(value.A * 255) - - def write_color4(self, value: Color): - self.write_float(value.R) - self.write_float(value.G) - self.write_float(value.B) - self.write_float(value.A) - - def write_matrix(self, value: Matrix4x4): - for val in value.M: - self.write_float(val) - def write_array(self, command, value: list, write_length: bool = True): if write_length: self.write_int(len(value)) @@ -163,15 +143,3 @@ def write_u_int_array(self, value: list, write_length: bool = False): def write_float_array(self, value: list, write_length: bool = False): return self.write_array(self.write_float, value, write_length) - - def write_string_array(self, value: list): - self.write_array(self.write_aligned_string, value) - - def write_vector2_array(self, value: list): - self.write_array(self.write_vector2, value) - - def write_vector4_array(self, value: list): - self.write_array(self.write_vector4, value) - - def write_matrix_array(self, value: list): - self.write_array(self.write_matrix, value) diff --git a/UnityPy/streams/__init__.py b/UnityPy/streams/__init__.py index 0b713c1e..c080cf35 100644 --- a/UnityPy/streams/__init__.py +++ b/UnityPy/streams/__init__.py @@ -1,2 +1,3 @@ -from .EndianBinaryReader import EndianBinaryReader -from .EndianBinaryWriter import EndianBinaryWriter +from ._defines import Endianess as Endianess +from .EndianBinaryReader import EndianBinaryReader as EndianBinaryReader +from .EndianBinaryWriter import EndianBinaryWriter as EndianBinaryWriter diff --git a/UnityPy/streams/_defines.py b/UnityPy/streams/_defines.py new file mode 100644 index 00000000..0a2dd828 --- /dev/null +++ b/UnityPy/streams/_defines.py @@ -0,0 +1,19 @@ +import sys + +from typing_extensions import Literal + +Endianess = Literal["<", ">"] +SYS_ENDIAN = "<" if sys.byteorder == "little" else ">" + + +TYPE_PARAM_SIZE_LIST = [ + ("short", "h", 2), + ("u_short", "H", 2), + ("int", "i", 4), + ("u_int", "I", 4), + ("long", "q", 8), + ("u_long", "Q", 8), + ("half", "e", 2), + ("float", "f", 4), + ("double", "d", 8), +] diff --git a/examples/AssetPatch/522608825 b/examples/AssetPatch/522608825 deleted file mode 100644 index 4938f2f9..00000000 Binary files a/examples/AssetPatch/522608825 and /dev/null differ diff --git a/examples/AssetPatch/patch.py b/examples/AssetPatch/patch.py deleted file mode 100644 index 7a4d199b..00000000 --- a/examples/AssetPatch/patch.py +++ /dev/null @@ -1,65 +0,0 @@ -import json -import os -import time -import UnityPy -from googletrans import Translator - -root = os.path.dirname(os.path.realpath(__file__)) - -def main(): - # load the original japanese localisation - src = os.path.join(root, "522608825") - e = UnityPy.load(src) - # iterate over all localisation assets - for cont, obj in e.container.items(): - # read the asset data - data = obj.read() - # get the localisation data - script = json.loads(bytes(data.script)) # bytes wrapper to handle memoryview - if hasattr(script, "infos"): - continue - print(data.name) - # translate the localisation - for entry in script["infos"]: - entry["value"] = translate(entry["value"]) - # overwrite the original - data.script = json.dumps( - script, - ensure_ascii=False, indent=4 - ).encode("utf8") - # apply the changes - data.save() - - # save the modified Bundle as file - with open(os.path.join(root, "522608825_patched"), "wb") as f: - f.write(e.file.save()) - - -# simple cache for already translated strings -# prevents requesting the same string multiple times -# therefore saving time and reducing the chance of being IP-blocked -TL_CACHE = {} -def translate(text): - # check if the text is valid - if not text.strip(" "): - return text - # check if the text was already translated once - if text in TL_CACHE: - return TL_CACHE[text] - - # actual google translation - translator = Translator(service_urls=['translate.googleapis.com']) - try: - ret = translator.translate(text, "en", "ja").text - TL_CACHE[text] = ret - return ret - except json.JSONDecodeError: - input("DecodeError") - return translate(text) - except (ConnectionError, AttributeError) as e: - print(e) - time.sleep(1) - return translate(text) - -if __name__ == "__main__": - main() diff --git a/examples/CustomMonoBehaviour/data.unity3d b/examples/CustomMonoBehaviour/data.unity3d deleted file mode 100644 index 85abee47..00000000 Binary files a/examples/CustomMonoBehaviour/data.unity3d and /dev/null differ diff --git a/examples/CustomMonoBehaviour/get_scriptable_texture.py b/examples/CustomMonoBehaviour/get_scriptable_texture.py deleted file mode 100644 index 06ba8e73..00000000 --- a/examples/CustomMonoBehaviour/get_scriptable_texture.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -This example shows how to write a custom MonoBehaviour class. - - -The script is for a game that generates its encryption key based on a specific image. -This image is linked to by a MonoBheaviour called "ScriptableTexture2D". -It is linked to by a pointer after the default MonoBehaviour structure. -This intel was acquired from the assembly of the game. - -Unfortunately, the asset doesn't have a type-tree, so the default MonoBehaviour class can't find the pointer. -So a custom MonoBehaviour class is required to get the pointer comfortably. -Doing so is simple, as seen in the following code. -""" - -import os -import UnityPy -from UnityPy.classes import MonoBehaviour, PPtr - -class ScriptableTexture2D(MonoBehaviour): - def __init__(self, reader): - # calls the default MonoBehaviour init - super().__init__(reader=reader) - # here goes the implementation of the extra data - self.texture = PPtr(reader) - -# set the path for the target file -root = os.path.dirname(os.path.realpath(__file__)) -fp = os.path.join(root,"data.unity3d") - -env = UnityPy.load(fp) - -# find the correct asset -for obj in env.objects: - if obj.type == "MonoBehaviour": - data = obj.read() - if data.name == "ScriptableTexture2D": - # correct obj found - # lets read it with the custom class - st = ScriptableTexture2D(obj) - # read the linked image and save it - tex = st.texture.read() - print(st.texture.read().name) - #tex.image.save(os.path.join(root, f"{tex.name}.png")) \ No newline at end of file diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json deleted file mode 100644 index b1b654e8..00000000 --- a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.deps.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v3.1/win-x64", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v3.1": {}, - ".NETCoreApp,Version=v3.1/win-x64": { - "TypeTreeGenerator/1.0.0": { - "dependencies": { - "ILRepack": "2.0.18", - "ILRepack.Lib.MSBuild.Task": "2.0.18.2", - "Mono.Cecil": "0.11.3" - }, - "runtime": { - "TypeTreeGenerator.dll": {} - } - }, - "ILRepack/2.0.18": {}, - "ILRepack.Lib.MSBuild.Task/2.0.18.2": {}, - "Mono.Cecil/0.11.3": { - "runtime": { - "lib/netstandard2.0/Mono.Cecil.Mdb.dll": { - "assemblyVersion": "0.11.3.0", - "fileVersion": "0.11.3.0" - }, - "lib/netstandard2.0/Mono.Cecil.Pdb.dll": { - "assemblyVersion": "0.11.3.0", - "fileVersion": "0.11.3.0" - }, - "lib/netstandard2.0/Mono.Cecil.Rocks.dll": { - "assemblyVersion": "0.11.3.0", - "fileVersion": "0.11.3.0" - }, - "lib/netstandard2.0/Mono.Cecil.dll": { - "assemblyVersion": "0.11.3.0", - "fileVersion": "0.11.3.0" - } - } - } - } - }, - "libraries": { - "TypeTreeGenerator/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "ILRepack/2.0.18": { - "type": "package", - "serviceable": true, - "sha512": "sha512-sR5Aj3JLDbA8JwESfWYfigSz5k1oNSHPN434W1LX8sboWJYEOSQP/KkvZGmJKPgajzSUKkl2jDar3LPZiMFU4Q==", - "path": "ilrepack/2.0.18", - "hashPath": "ilrepack.2.0.18.nupkg.sha512" - }, - "ILRepack.Lib.MSBuild.Task/2.0.18.2": { - "type": "package", - "serviceable": true, - "sha512": "sha512-lreK19MMDA/ekeqjLY4w171cG7+pHwjeTMHMNq6tghuhTOIOfHT+b/LTMj5jpfbJoR7J6AMD3yOYZoyY/2wtJA==", - "path": "ilrepack.lib.msbuild.task/2.0.18.2", - "hashPath": "ilrepack.lib.msbuild.task.2.0.18.2.nupkg.sha512" - }, - "Mono.Cecil/0.11.3": { - "type": "package", - "serviceable": true, - "sha512": "sha512-DNYE+io5XfEE8+E+5padThTPHJARJHbz1mhbhMPNrrWGKVKKqj/KEeLvbawAmbIcT73NuxLV7itHZaYCZcVWGg==", - "path": "mono.cecil/0.11.3", - "hashPath": "mono.cecil.0.11.3.nupkg.sha512" - } - } -} \ No newline at end of file diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.dll b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.dll deleted file mode 100644 index f9df5439..00000000 Binary files a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.dll and /dev/null differ diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb deleted file mode 100644 index 527ffa1a..00000000 Binary files a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.pdb and /dev/null differ diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.dev.json b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.dev.json deleted file mode 100644 index 4a6bcc1f..00000000 --- a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.dev.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "runtimeOptions": { - "additionalProbingPaths": [ - "C:\\Users\\W0lf\\.dotnet\\store\\|arch|\\|tfm|", - "C:\\Users\\W0lf\\.nuget\\packages" - ] - } -} \ No newline at end of file diff --git a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json b/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json deleted file mode 100644 index bc456d78..00000000 --- a/examples/MonoBehaviourFromAssembly/TypeTreeGenerator/TypeTreeGenerator.runtimeconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "runtimeOptions": { - "tfm": "netcoreapp3.1", - "framework": { - "name": "Microsoft.NETCore.App", - "version": "3.1.0" - } - } -} \ No newline at end of file diff --git a/examples/MonoBehaviourFromAssembly/monobehaviour_from_assembly.py b/examples/MonoBehaviourFromAssembly/monobehaviour_from_assembly.py deleted file mode 100644 index 63e54712..00000000 --- a/examples/MonoBehaviourFromAssembly/monobehaviour_from_assembly.py +++ /dev/null @@ -1,170 +0,0 @@ -# py3 -# requirements: -# pythonnet 3+ -# pip install git+https://github.com/pythonnet/pythonnet/ -# TypeTreeGenerator -# https://github.com/K0lb3/TypeTreeGenerator -# requires .NET 5.0 SDK -# https://dotnet.microsoft.com/download/dotnet/5.0 -# -# pythonnet 2 and TypeTreeGenerator created with net4.8 works on Windows, -# so it can do without pythonnet_init, -# all other systems need pythonnet 3 and either .net 5 or .net core 3 and pythonnet_init - - -############################ -# -# Warning: This example isn't for beginners -# -############################ - -import os -import UnityPy -from typing import Dict -import json - -ROOT = os.path.dirname(os.path.realpath(__file__)) -TYPETREE_GENERATOR_PATH = os.path.join(ROOT, "TypeTreeGenerator") - -def main(): - # dump the trees for all classes in the assembly - dll_folder = os.path.join(ROOT, "DummyDll") - tree_path = os.path.join(ROOT, "assembly_typetrees.json") - trees = dump_assembly_trees(dll_folder, tree_path) - # by dumping it as json, it can be redistributed, - # so that other people don't have to setup pythonnet3 - # People who don't like to share their decrypted dlls could also share the relevant structures this way. - - export_monobehaviours(asset_path, trees) - -def export_monobehaviours(asset_path: str, trees: dict): - for r, d, fs in os.walk(asset_path): - for f in fs: - try: - env = UnityPy.load(os.path.join(r, f)) - except: - continue - for obj in env.objects: - if obj.type == "MonoBehaviour": - if obj.serialized_type and obj.serialized_type.node: - tree = obj.read_typetree() - else: - d = obj.read(check_read=False) - if not d.m_Script: - continue - # RIP, no referenced script - # can only dump raw - script = d.m_Script.read() - # on-demand solution without already dumped tree - #nodes = generate_tree( - # g, script.m_AssemblyName, script.m_ClassName, script.m_Namespace - #) - if script.m_ClassName not in trees: - # class not found in known trees, - # might have to add the classes of the other dlls - continue - nodes = FakeNode(**trees[script.m_ClassName]) - tree = obj.read_typetree(nodes) - - # save tree as json whereever you like - - - -def dump_assembly_trees(dll_folder: str, out_path: str): - # init pythonnet, so that it uses the correct .net for the generator - pythonnet_init() - # create generator - g = create_generator(dll_folder) - - # generate a typetree for all existing classes in the Assembly-CSharp - # while this could also be done dynamically for each required class, - # it's faster and easier overall to just fetch all at once - trees = generate_tree(g, "Assembly-CSharp.dll", "", "") - - if out_path: - with open("typetrees.json", "wt", encoding="utf8") as f: - json.dump(trees, f, ensure_ascii=False) - return trees - - - -def pythonnet_init(): - """correctly sets-up pythonnet for the typetree generator""" - # prepare correct runtime - from clr_loader import get_coreclr - from pythonnet import set_runtime - - rt = get_coreclr( - os.path.join(TYPETREE_GENERATOR_PATH, "TypeTreeGenerator.runtimeconfig.json") - ) - set_runtime(rt) - - -def create_generator(dll_folder: str): - """Loads TypeTreeGenerator library and returns an instance of the Generator class.""" - # temporarily add the typetree generator dir to paths, - # so that pythonnet can find its files - import sys - - sys.path.append(TYPETREE_GENERATOR_PATH) - - # - import clr - - clr.AddReference("TypeTreeGenerator") - - # import Generator class from the loaded library - from Generator import Generator - - # create an instance of the Generator class - g = Generator() - # load the dll folder into the generator - g.loadFolder(dll_folder) - return g - - -class FakeNode: - """A fake/minimal Node class for use in UnityPy.""" - - def __init__(self, **kwargs): - self.__dict__.update(**kwargs) - - -def generate_tree( - g: "Generator", - assembly: str, - class_name: str, - namespace: str, - unity_version=[2018, 4, 3, 1], -) -> Dict[str, Dict]: - """Generates the typetree structure / nodes for the specified class.""" - # C# System - from System import Array - - unity_version_cs = Array[int](unity_version) - - # fetch all type definitions - def_iter = g.getTypeDefs(assembly, class_name, namespace) - - # create the nodes - trees = {} - for d in def_iter: - try: - nodes = g.convertToTypeTreeNodes(d, unity_version_cs) - except Exception as e: - # print(d.Name, e) - continue - trees[d.Name] = [ - { - "level" : node.m_Level, - "type" : node.m_Type, - "name" : node.m_Name, - "meta_flag" : node.m_MetaFlag, - } - for node in nodes - ] - return trees - - -if __name__ == "__main__": - main() diff --git a/examples/rebundle.py b/examples/rebundle.py deleted file mode 100644 index 9e223aa8..00000000 --- a/examples/rebundle.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -This script shows how to create a bundle from dumped assets from the memory. - -The dumped assets consist of SerializedFiles and their resources(cabs). -A sample file of the original game is required for this script. -This example uses the globalgamemanager as this asset should exist in all Unity games. -""" - -import os -import uuid -import random -from copy import copy -import re - -import UnityPy -from UnityPy.enums import ClassIDType -from UnityPy.files import BundleFile -from UnityPy.files.SerializedFile import FileIdentifier, ObjectReader, SerializedType - - -SERIALIZED_PATH = r"globalgamemanagers" -DATA_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") - - -def main(): - bf = Fake( - signature="UnityFS", - version=6, - format=6, - version_engine="2017.4.30f1", - version_player="5.x.x", - _class=BundleFile, - files={}, - ) - # load default serialized file and prepare some variables for easier access to key objects - env = UnityPy.load(SERIALIZED_PATH) - sf = env.file # serialized file - or_bp = list(sf.objects.values())[0].__dict__ # object data - - bf.files["serialized_file"] = sf - sf.flags = 4 - - # remove all unnesessary stuff - for key in list(sf.objects.keys()): - del sf.objects[key] - sf.externals = [] - - # add all files from DATA_PATH - for root, dirs, files in os.walk(DATA_PATH): - for f in files: - fp = os.path.join(root, f) - if f[:3] == "CAB": - add_cab(bf, sf, root, f) - else: - add_object(sf, fp, or_bp) - - # save edited bundle - open("bundle_edited.unity3d", "wb").write(bf.save()) - - -def add_cab(bf, sf, root, f): - fp = os.path.join(root, f) - bf.files[f] = Fake(data=open(fp, "rb").read(), flags=4) - sf.externals.append( - Fake( - temp_empty="", - guid=generate_16_byte_uid(), - path=f"archive:/{os.path.basename(root)}/{f}", - type=0, - _class=FileIdentifier, - ) - ) - - -def add_object(sf, fp, or_bp): - # get correct type id - path_id, class_name = os.path.splitext(os.path.basename(fp)) - path_id = int(path_id) if re.match( - r"^\d+$", path_id) else generate_path_id(sf.objects) - class_id = getattr( - ClassIDType, class_name[1:], ClassIDType.UnknownType).value - type_id = -1 - for i, styp in enumerate(sf.types): - if styp.class_id == class_id: - type_id = i - if type_id == -1: # not found, add type - type_id = len(sf.types) - sf.types.append( - Fake( - class_id=class_id, - is_stripped_type=False, - node=[], - script_type_index=-1, - old_type_hash=generate_16_byte_uid(), - _class=SerializedType, - ) - ) - - # add new object - odata = copy(or_bp) - odata.update( - { - "data": open(fp, "rb").read(), - "path_id": generate_path_id(sf.objects), - "class_id": class_id, - "type_id": type_id, - } - ) - sf.objects[path_id] = Fake(**odata, _class=ObjectReader) - - -def generate_path_id(objects): - while True: - uid = random.randint(-(2 ** 16), 2 ** 16 - 1) - if uid not in objects: - return uid - - -def generate_16_byte_uid(): - return uuid.uuid1().urn[-16:].encode("ascii") - - -class Fake(object): - """ - fake class for easy class creation without init call - """ - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - if "_class" in kwargs: - self.__class__ = kwargs["_class"] - - def save(self): - return self.data - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/samples/Build6_Web.data.gz b/tests/samples/Build6_Web.data.gz new file mode 100644 index 00000000..51131015 Binary files /dev/null and b/tests/samples/Build6_Web.data.gz differ diff --git a/tests/samples/README.md b/tests/samples/README.md new file mode 100644 index 00000000..06576d8a --- /dev/null +++ b/tests/samples/README.md @@ -0,0 +1,5 @@ +Sources for the sample data: + +- audioclip: [Game background Music loop short](https://freesound.org/people/yummie/sounds/410574/) (Attribution 4.0) +- models, animation, some sounds: [unity-chan](https://unity-chan.com/) (close to CC BY-NC-SA, no controversial use (adult, ai, ...) and restricted commercial use) +- diced sprites: [elringus/sprite-dicing](https://github.com/elringus/sprite-dicing/tree/main/plugins/unity/Assets) (MIT) \ No newline at end of file diff --git a/tests/samples/atlas_test b/tests/samples/atlas_test deleted file mode 100644 index faa7e996..00000000 Binary files a/tests/samples/atlas_test and /dev/null differ diff --git a/tests/samples/banner_1 b/tests/samples/banner_1 deleted file mode 100644 index 132d0e3b..00000000 Binary files a/tests/samples/banner_1 and /dev/null differ diff --git a/tests/samples/char_118_yuki.ab b/tests/samples/char_118_yuki.ab deleted file mode 100644 index dda9c41a..00000000 Binary files a/tests/samples/char_118_yuki.ab and /dev/null differ diff --git a/tests/samples/xinzexi_2_n_tex b/tests/samples/xinzexi_2_n_tex deleted file mode 100644 index e80f3c81..00000000 Binary files a/tests/samples/xinzexi_2_n_tex and /dev/null differ diff --git a/tests/samples/xinzexi_2_n_tex_mesh b/tests/samples/xinzexi_2_n_tex_mesh deleted file mode 100644 index b0231623..00000000 --- a/tests/samples/xinzexi_2_n_tex_mesh +++ /dev/null @@ -1,1812 +0,0 @@ -g xinzexi_2_n-mesh -v -1152 671 0 -v -1152 1343 0 -v -1440 1343 0 -v -1440 671 0 -v -1152 31 0 -v -1152 671 0 -v -1440 671 0 -v -1440 31 0 -v -832 607 0 -v -832 927 0 -v -1152 927 0 -v -1152 607 0 -v -512 607 0 -v -512 927 0 -v -832 927 0 -v -832 607 0 -v -64 319 0 -v -64 607 0 -v -384 607 0 -v -384 319 0 -v -384 319 0 -v -384 607 0 -v -672 607 0 -v -672 319 0 -v -960 927 0 -v -960 1215 0 -v -1152 1215 0 -v -1152 927 0 -v -960 1215 0 -v -960 1503 0 -v -1152 1503 0 -v -1152 1215 0 -v -1440 735 0 -v -1440 991 0 -v -1632 991 0 -v -1632 735 0 -v -1440 991 0 -v -1440 1247 0 -v -1632 1247 0 -v -1632 991 0 -v -704 223 0 -v -704 479 0 -v -832 479 0 -v -832 223 0 -v -1440 1247 0 -v -1440 1407 0 -v -1632 1407 0 -v -1632 1247 0 -v -1152 1343 0 -v -1152 1503 0 -v -1312 1503 0 -v -1312 1343 0 -v -1632 1663 0 -v -1632 2047 0 -v -1696 2047 0 -v -1696 1663 0 -v -1632 1311 0 -v -1632 1663 0 -v -1696 1663 0 -v -1696 1311 0 -v -992 1503 0 -v -992 1599 0 -v -1216 1599 0 -v -1216 1503 0 -v -1440 223 0 -v -1440 447 0 -v -1536 447 0 -v -1536 223 0 -v -800 479 0 -v -800 607 0 -v -960 607 0 -v -960 479 0 -v -800 927 0 -v -800 1055 0 -v -960 1055 0 -v -960 927 0 -v -1024 95 0 -v -1024 255 0 -v -1152 255 0 -v -1152 95 0 -v -1440 31 0 -v -1440 223 0 -v -1536 223 0 -v -1536 31 0 -v -192 191 0 -v -192 255 0 -v -448 255 0 -v -448 191 0 -v -448 191 0 -v -448 255 0 -v -704 255 0 -v -704 191 0 -v -832 1311 0 -v -832 1439 0 -v -928 1439 0 -v -928 1311 0 -v -800 1599 0 -v -800 1727 0 -v -896 1727 0 -v -896 1599 0 -v -1632 1055 0 -v -1632 1183 0 -v -1728 1183 0 -v -1728 1055 0 -v -256 607 0 -v -256 703 0 -v -384 703 0 -v -384 607 0 -v -1632 927 0 -v -1632 1055 0 -v -1728 1055 0 -v -1728 927 0 -v -384 607 0 -v -384 703 0 -v -512 703 0 -v -512 607 0 -v -1600 31 0 -v -1600 159 0 -v -1696 159 0 -v -1696 31 0 -v -1600 159 0 -v -1600 287 0 -v -1696 287 0 -v -1696 159 0 -v -1888 735 0 -v -1888 895 0 -v -1937 895 0 -v -1937 735 0 -v -1760 959 0 -v -1760 1055 0 -v -1856 1055 0 -v -1856 959 0 -v -896 1535 0 -v -896 1631 0 -v -992 1631 0 -v -992 1535 0 -v -128 287 0 -v -128 319 0 -v -416 319 0 -v -416 287 0 -v -416 287 0 -v -416 319 0 -v -704 319 0 -v -704 287 0 -v -1888 607 0 -v -1888 735 0 -v -1937 735 0 -v -1937 607 0 -v -1408 1503 0 -v -1408 1567 0 -v -1536 1567 0 -v -1536 1503 0 -v -1344 1407 0 -v -1344 1535 0 -v -1408 1535 0 -v -1408 1407 0 -v -0 415 0 -v -0 543 0 -v -64 543 0 -v -64 415 0 -v -1088 479 0 -v -1088 607 0 -v -1152 607 0 -v -1152 479 0 -v -1536 31 0 -v -1536 223 0 -v -1568 223 0 -v -1568 31 0 -v -1536 1503 0 -v -1536 1567 0 -v -1632 1567 0 -v -1632 1503 0 -v -1536 223 0 -v -1536 415 0 -v -1568 415 0 -v -1568 223 0 -v -1696 159 0 -v -1696 255 0 -v -1760 255 0 -v -1760 159 0 -v -768 1343 0 -v -768 1407 0 -v -832 1407 0 -v -832 1343 0 -v -448 831 0 -v -448 895 0 -v -512 895 0 -v -512 831 0 -v -1280 0 0 -v -1280 31 0 -v -1408 31 0 -v -1408 0 0 -v -1440 1439 0 -v -1440 1503 0 -v -1504 1503 0 -v -1504 1439 0 -v -1472 671 0 -v -1472 735 0 -v -1536 735 0 -v -1536 671 0 -v -1792 479 0 -v -1792 543 0 -v -1856 543 0 -v -1856 479 0 -v -320 703 0 -v -320 767 0 -v -384 767 0 -v -384 703 0 -v -448 703 0 -v -448 767 0 -v -512 767 0 -v -512 703 0 -v -1568 319 0 -v -1568 447 0 -v -1600 447 0 -v -1600 319 0 -v -1856 863 0 -v -1856 991 0 -v -1888 991 0 -v -1888 863 0 -v -1696 95 0 -v -1696 159 0 -v -1760 159 0 -v -1760 95 0 -v -1568 191 0 -v -1568 319 0 -v -1600 319 0 -v -1600 191 0 -v -160 255 0 -v -160 287 0 -v -288 287 0 -v -288 255 0 -v -832 351 0 -v -832 479 0 -v -864 479 0 -v -864 351 0 -v -288 255 0 -v -288 287 0 -v -416 287 0 -v -416 255 0 -v -672 383 0 -v -672 479 0 -v -704 479 0 -v -704 383 0 -v -1440 639 0 -v -1440 735 0 -v -1472 735 0 -v -1472 639 0 -v -928 1119 0 -v -928 1215 0 -v -960 1215 0 -v -960 1119 0 -v -1856 767 0 -v -1856 863 0 -v -1888 863 0 -v -1888 767 0 -v -1568 63 0 -v -1568 159 0 -v -1600 159 0 -v -1600 63 0 -v -832 255 0 -v -832 351 0 -v -864 351 0 -v -864 255 0 -v -1696 415 0 -v -1696 447 0 -v -1792 447 0 -v -1792 415 0 -v -512 255 0 -v -512 287 0 -v -608 287 0 -v -608 255 0 -v -704 927 0 -v -704 959 0 -v -800 959 0 -v -800 927 0 -v -1600 415 0 -v -1600 447 0 -v -1696 447 0 -v -1696 415 0 -v -608 927 0 -v -608 959 0 -v -704 959 0 -v -704 927 0 -v -1408 0 0 -v -1408 31 0 -v -1504 31 0 -v -1504 0 0 -v -1696 1791 0 -v -1696 1887 0 -v -1728 1887 0 -v -1728 1791 0 -v -1408 1407 0 -v -1408 1503 0 -v -1440 1503 0 -v -1440 1407 0 -v -608 255 0 -v -608 287 0 -v -704 287 0 -v -704 255 0 -v -1696 1887 0 -v -1696 1983 0 -v -1728 1983 0 -v -1728 1887 0 -v -1600 1599 0 -v -1600 1663 0 -v -1632 1663 0 -v -1632 1599 0 -v -768 511 0 -v -768 575 0 -v -800 575 0 -v -800 511 0 -v -1504 1471 0 -v -1504 1503 0 -v -1568 1503 0 -v -1568 1471 0 -v -1216 1567 0 -v -1216 1599 0 -v -1280 1599 0 -v -1280 1567 0 -v -832 1567 0 -v -832 1599 0 -v -896 1599 0 -v -896 1567 0 -v -672 543 0 -v -672 607 0 -v -704 607 0 -v -704 543 0 -v -1440 479 0 -v -1440 543 0 -v -1472 543 0 -v -1472 479 0 -v -416 799 0 -v -416 863 0 -v -448 863 0 -v -448 799 0 -v -384 767 0 -v -384 831 0 -v -416 831 0 -v -416 767 0 -v -1632 383 0 -v -1632 415 0 -v -1696 415 0 -v -1696 383 0 -v -768 1663 0 -v -768 1727 0 -v -800 1727 0 -v -800 1663 0 -v -896 1631 0 -v -896 1695 0 -v -928 1695 0 -v -928 1631 0 -v -1760 223 0 -v -1760 287 0 -v -1792 287 0 -v -1792 223 0 -v -1728 31 0 -v -1728 95 0 -v -1760 95 0 -v -1760 31 0 -v -1120 255 0 -v -1120 319 0 -v -1152 319 0 -v -1152 255 0 -v -1792 223 0 -v -1792 287 0 -v -1824 287 0 -v -1824 223 0 -v -864 415 0 -v -864 479 0 -v -896 479 0 -v -896 415 0 -v -32 351 0 -v -32 415 0 -v -64 415 0 -v -64 351 0 -v -1088 63 0 -v -1088 95 0 -v -1152 95 0 -v -1152 63 0 -v -1600 0 0 -v -1600 31 0 -v -1664 31 0 -v -1664 0 0 -v -960 191 0 -v -960 255 0 -v -992 255 0 -v -992 191 0 -v -480 159 0 -v -480 191 0 -v -544 191 0 -v -544 159 0 -v -1888 543 0 -v -1888 607 0 -v -1920 607 0 -v -1920 543 0 -v -512 1087 0 -v -512 1151 0 -v -544 1151 0 -v -544 1087 0 -v -896 1055 0 -v -896 1119 0 -v -928 1119 0 -v -928 1055 0 -v -1600 1439 0 -v -1600 1503 0 -v -1632 1503 0 -v -1632 1439 0 -v -1632 1247 0 -v -1632 1311 0 -v -1664 1311 0 -v -1664 1247 0 -v -1888 895 0 -v -1888 959 0 -v -1920 959 0 -v -1920 895 0 -v -960 543 0 -v -960 607 0 -v -992 607 0 -v -992 543 0 -v -1760 1055 0 -v -1760 1119 0 -v -1792 1119 0 -v -1792 1055 0 -v -768 959 0 -v -768 1023 0 -v -800 1023 0 -v -800 959 0 -v -1824 895 0 -v -1824 959 0 -v -1856 959 0 -v -1856 895 0 -v -928 1343 0 -v -928 1407 0 -v -960 1407 0 -v -960 1343 0 -v -1504 1407 0 -v -1504 1439 0 -v -1568 1439 0 -v -1568 1407 0 -v -1312 1375 0 -v -1312 1439 0 -v -1344 1439 0 -v -1344 1375 0 -v -928 1407 0 -v -928 1471 0 -v -960 1471 0 -v -960 1407 0 -v -1728 1087 0 -v -1728 1151 0 -v -1760 1151 0 -v -1760 1087 0 -v -192 607 0 -v -192 639 0 -v -256 639 0 -v -256 607 0 -v -1856 575 0 -v -1856 639 0 -v -1888 639 0 -v -1888 575 0 -v -1728 1023 0 -v -1728 1087 0 -v -1760 1087 0 -v -1760 1023 0 -v -128 607 0 -v -128 639 0 -v -192 639 0 -v -192 607 0 -v -1312 1439 0 -v -1312 1503 0 -v -1344 1503 0 -v -1344 1439 0 -v -1440 575 0 -v -1440 639 0 -v -1472 639 0 -v -1472 575 0 -v -672 319 0 -v -672 383 0 -v -704 383 0 -v -704 319 0 -v -1408 1343 0 -v -1408 1407 0 -v -1440 1407 0 -v -1440 1343 0 -v -928 1055 0 -v -928 1119 0 -v -960 1119 0 -v -960 1055 0 -v -1568 0 0 -v -1568 63 0 -v -1600 63 0 -v -1600 0 0 -v -1504 1567 0 -v -1504 1599 0 -v -1568 1599 0 -v -1568 1567 0 -v -1568 1567 0 -v -1568 1599 0 -v -1632 1599 0 -v -1632 1567 0 -v -1696 1631 0 -v -1696 1695 0 -v -1728 1695 0 -v -1728 1631 0 -v -1696 1567 0 -v -1696 1631 0 -v -1728 1631 0 -v -1728 1567 0 -v -1856 511 0 -v -1856 575 0 -v -1888 575 0 -v -1888 511 0 -v -1600 287 0 -v -1600 351 0 -v -1632 351 0 -v -1632 287 0 -v -1568 1407 0 -v -1568 1439 0 -v -1632 1439 0 -v -1632 1407 0 -v -1600 351 0 -v -1600 415 0 -v -1632 415 0 -v -1632 351 0 -v -1152 1599 0 -v -1152 1631 0 -v -1216 1631 0 -v -1216 1599 0 -v -992 159 0 -v -992 223 0 -v -1024 223 0 -v -1024 159 0 -v -992 223 0 -v -992 287 0 -v -1024 287 0 -v -1024 223 0 -v -1760 447 0 -v -1760 479 0 -v -1824 479 0 -v -1824 447 0 -v -1120 351 0 -v -1120 415 0 -v -1152 415 0 -v -1152 351 0 -v -1120 415 0 -v -1120 479 0 -v -1152 479 0 -v -1152 415 0 -v -1696 447 0 -v -1696 479 0 -v -1760 479 0 -v -1760 447 0 -v -448 799 0 -v -448 831 0 -v -480 831 0 -v -480 799 0 -v -288 703 0 -v -288 735 0 -v -320 735 0 -v -320 703 0 -v -736 959 0 -v -736 991 0 -v -768 991 0 -v -768 959 0 -v -1792 927 0 -v -1792 959 0 -v -1824 959 0 -v -1824 927 0 -v -1536 703 0 -v -1536 735 0 -v -1568 735 0 -v -1568 703 0 -v -704 575 0 -v -704 607 0 -v -736 607 0 -v -736 575 0 -v -416 703 0 -v -416 735 0 -v -448 735 0 -v -448 703 0 -v -352 767 0 -v -352 799 0 -v -384 799 0 -v -384 767 0 -v -224 639 0 -v -224 671 0 -v -256 671 0 -v -256 639 0 -v -480 767 0 -v -480 799 0 -v -512 799 0 -v -512 767 0 -v -32 543 0 -v -32 575 0 -v -64 575 0 -v -64 543 0 -v -896 351 0 -v -896 383 0 -v -928 383 0 -v -928 351 0 -v -1472 447 0 -v -1472 479 0 -v -1504 479 0 -v -1504 447 0 -v -1632 287 0 -v -1632 319 0 -v -1664 319 0 -v -1664 287 0 -v -1760 0 0 -v -1760 31 0 -v -1792 31 0 -v -1792 0 0 -v -1024 255 0 -v -1024 287 0 -v -1056 287 0 -v -1056 255 0 -v -1824 543 0 -v -1824 575 0 -v -1856 575 0 -v -1856 543 0 -v -1824 255 0 -v -1824 287 0 -v -1856 287 0 -v -1856 255 0 -v -736 479 0 -v -736 511 0 -v -768 511 0 -v -768 479 0 -v -896 447 0 -v -896 479 0 -v -928 479 0 -v -928 447 0 -v -1760 479 0 -v -1760 511 0 -v -1792 511 0 -v -1792 479 0 -v -1792 1055 0 -v -1792 1087 0 -v -1824 1087 0 -v -1824 1055 0 -v -1440 447 0 -v -1440 479 0 -v -1472 479 0 -v -1472 447 0 -v -768 479 0 -v -768 511 0 -v -800 511 0 -v -800 479 0 -v -1696 383 0 -v -1696 415 0 -v -1728 415 0 -v -1728 383 0 -v -1728 0 0 -v -1728 31 0 -v -1760 31 0 -v -1760 0 0 -v -1760 191 0 -v -1760 223 0 -v -1792 223 0 -v -1792 191 0 -v -1824 863 0 -v -1824 895 0 -v -1856 895 0 -v -1856 863 0 -v -1216 1599 0 -v -1216 1631 0 -v -1248 1631 0 -v -1248 1599 0 -v -416 767 0 -v -416 799 0 -v -448 799 0 -v -448 767 0 -v -672 511 0 -v -672 543 0 -v -704 543 0 -v -704 511 0 -v -384 735 0 -v -384 767 0 -v -416 767 0 -v -416 735 0 -v -928 1631 0 -v -928 1663 0 -v -960 1663 0 -v -960 1631 0 -v -800 1311 0 -v -800 1343 0 -v -832 1343 0 -v -832 1311 0 -v -1376 1343 0 -v -1376 1375 0 -v -1408 1375 0 -v -1408 1343 0 -v -1632 1183 0 -v -1632 1215 0 -v -1664 1215 0 -v -1664 1183 0 -v -864 1055 0 -v -864 1087 0 -v -896 1087 0 -v -896 1055 0 -v -640 1087 0 -v -640 1119 0 -v -672 1119 0 -v -672 1087 0 -v -1248 1535 0 -v -1248 1567 0 -v -1280 1567 0 -v -1280 1535 0 -v -992 1599 0 -v -992 1631 0 -v -1024 1631 0 -v -1024 1599 0 -v -1216 1503 0 -v -1216 1535 0 -v -1248 1535 0 -v -1248 1503 0 -v -1344 1375 0 -v -1344 1407 0 -v -1376 1407 0 -v -1376 1375 0 -v -896 1439 0 -v -896 1471 0 -v -928 1471 0 -v -928 1439 0 -vt 0.00048828125 0.00102460384 -vt 0.00048828125 0.689549208 -vt 0.141113281 0.689549208 -vt 0.141113281 0.00102460384 -vt 0.142089844 0.00102460384 -vt 0.142089844 0.656762302 -vt 0.282714844 0.656762302 -vt 0.282714844 0.00102460384 -vt 0.283691406 0.00102460384 -vt 0.283691406 0.328893423 -vt 0.439941406 0.328893423 -vt 0.439941406 0.00102460384 -vt 0.440917969 0.00102460384 -vt 0.440917969 0.328893423 -vt 0.597167969 0.328893423 -vt 0.597167969 0.00102460384 -vt 0.598144531 0.00102460384 -vt 0.598144531 0.296106577 -vt 0.754394531 0.296106577 -vt 0.754394531 0.00102460384 -vt 0.755371094 0.00102460384 -vt 0.755371094 0.296106577 -vt 0.895996094 0.296106577 -vt 0.895996094 0.00102460384 -vt 0.896972656 0.00102460384 -vt 0.896972656 0.296106577 -vt 0.990722656 0.296106577 -vt 0.990722656 0.00102460384 -vt 0.598144531 0.298155725 -vt 0.598144531 0.593237698 -vt 0.691894531 0.593237698 -vt 0.691894531 0.298155725 -vt 0.692871094 0.298155725 -vt 0.692871094 0.560450792 -vt 0.786621094 0.560450792 -vt 0.786621094 0.298155725 -vt 0.787597656 0.298155725 -vt 0.787597656 0.560450792 -vt 0.881347656 0.560450792 -vt 0.881347656 0.298155725 -vt 0.882324219 0.298155725 -vt 0.882324219 0.560450792 -vt 0.944824219 0.560450792 -vt 0.944824219 0.298155725 -vt 0.283691406 0.330942631 -vt 0.283691406 0.49487704 -vt 0.377441406 0.49487704 -vt 0.377441406 0.330942631 -vt 0.378417969 0.330942631 -vt 0.378417969 0.49487704 -vt 0.456542969 0.49487704 -vt 0.456542969 0.330942631 -vt 0.945800781 0.298155725 -vt 0.945800781 0.691598356 -vt 0.977050781 0.691598356 -vt 0.977050781 0.298155725 -vt 0.457519531 0.330942631 -vt 0.457519531 0.691598356 -vt 0.488769531 0.691598356 -vt 0.488769531 0.330942631 -vt 0.283691406 0.496926248 -vt 0.283691406 0.595286846 -vt 0.393066406 0.595286846 -vt 0.393066406 0.496926248 -vt 0.489746094 0.330942631 -vt 0.489746094 0.560450792 -vt 0.536621094 0.560450792 -vt 0.536621094 0.330942631 -vt 0.489746094 0.5625 -vt 0.489746094 0.693647504 -vt 0.567871094 0.693647504 -vt 0.567871094 0.5625 -vt 0.692871094 0.5625 -vt 0.692871094 0.693647504 -vt 0.770996094 0.693647504 -vt 0.770996094 0.5625 -vt 0.394042969 0.496926248 -vt 0.394042969 0.660860658 -vt 0.456542969 0.660860658 -vt 0.456542969 0.496926248 -vt 0.771972656 0.5625 -vt 0.771972656 0.759221315 -vt 0.818847656 0.759221315 -vt 0.818847656 0.5625 -vt 0.819824219 0.5625 -vt 0.819824219 0.628073812 -vt 0.944824219 0.628073812 -vt 0.944824219 0.5625 -vt 0.819824219 0.63012296 -vt 0.819824219 0.695696712 -vt 0.944824219 0.695696712 -vt 0.944824219 0.63012296 -vt 0.598144531 0.595286846 -vt 0.598144531 0.726434469 -vt 0.645019531 0.726434469 -vt 0.645019531 0.595286846 -vt 0.283691406 0.597336054 -vt 0.283691406 0.728483617 -vt 0.330566406 0.728483617 -vt 0.330566406 0.597336054 -vt 0.331542969 0.597336054 -vt 0.331542969 0.728483617 -vt 0.378417969 0.728483617 -vt 0.378417969 0.597336054 -vt 0.142089844 0.65881145 -vt 0.142089844 0.757172108 -vt 0.204589844 0.757172108 -vt 0.204589844 0.65881145 -vt 0.205566406 0.65881145 -vt 0.205566406 0.789959013 -vt 0.252441406 0.789959013 -vt 0.252441406 0.65881145 -vt 0.394042969 0.662909865 -vt 0.394042969 0.761270523 -vt 0.456542969 0.761270523 -vt 0.456542969 0.662909865 -vt 0.00048828125 0.691598356 -vt 0.00048828125 0.822745919 -vt 0.0473632812 0.822745919 -vt 0.0473632812 0.691598356 -vt 0.0483398438 0.691598356 -vt 0.0483398438 0.822745919 -vt 0.0952148438 0.822745919 -vt 0.0952148438 0.691598356 -vt 0.568847656 0.330942631 -vt 0.568847656 0.49487704 -vt 0.593261719 0.49487704 -vt 0.593261719 0.330942631 -vt 0.945800781 0.693647504 -vt 0.945800781 0.792008162 -vt 0.992675781 0.792008162 -vt 0.992675781 0.693647504 -vt 0.489746094 0.695696712 -vt 0.489746094 0.794057369 -vt 0.536621094 0.794057369 -vt 0.536621094 0.695696712 -vt 0.598144531 0.728483617 -vt 0.598144531 0.761270523 -vt 0.738769531 0.761270523 -vt 0.738769531 0.728483617 -vt 0.771972656 0.761270523 -vt 0.771972656 0.794057369 -vt 0.912597656 0.794057369 -vt 0.912597656 0.761270523 -vt 0.568847656 0.496926248 -vt 0.568847656 0.628073812 -vt 0.593261719 0.628073812 -vt 0.593261719 0.496926248 -vt 0.283691406 0.730532765 -vt 0.283691406 0.796106577 -vt 0.346191406 0.796106577 -vt 0.346191406 0.730532765 -vt 0.0961914062 0.691598356 -vt 0.0961914062 0.822745919 -vt 0.127441406 0.822745919 -vt 0.127441406 0.691598356 -vt 0.457519531 0.693647504 -vt 0.457519531 0.824795067 -vt 0.488769531 0.824795067 -vt 0.488769531 0.693647504 -vt 0.537597656 0.695696712 -vt 0.537597656 0.826844275 -vt 0.568847656 0.826844275 -vt 0.568847656 0.695696712 -vt 0.569824219 0.63012296 -vt 0.569824219 0.826844275 -vt 0.585449219 0.826844275 -vt 0.585449219 0.63012296 -vt 0.142089844 0.759221315 -vt 0.142089844 0.824795067 -vt 0.188964844 0.824795067 -vt 0.188964844 0.759221315 -vt 0.253417969 0.65881145 -vt 0.253417969 0.855532765 -vt 0.269042969 0.855532765 -vt 0.269042969 0.65881145 -vt 0.739746094 0.695696712 -vt 0.739746094 0.794057369 -vt 0.770996094 0.794057369 -vt 0.770996094 0.695696712 -vt 0.913574219 0.697745919 -vt 0.913574219 0.763319671 -vt 0.944824219 0.763319671 -vt 0.944824219 0.697745919 -vt 0.347167969 0.730532765 -vt 0.347167969 0.796106577 -vt 0.378417969 0.796106577 -vt 0.378417969 0.730532765 -vt 0.394042969 0.763319671 -vt 0.394042969 0.796106577 -vt 0.456542969 0.796106577 -vt 0.456542969 0.763319671 -vt 0.598144531 0.763319671 -vt 0.598144531 0.828893423 -vt 0.629394531 0.828893423 -vt 0.629394531 0.763319671 -vt 0.630371094 0.763319671 -vt 0.630371094 0.828893423 -vt 0.661621094 0.828893423 -vt 0.661621094 0.763319671 -vt 0.662597656 0.763319671 -vt 0.662597656 0.828893423 -vt 0.693847656 0.828893423 -vt 0.693847656 0.763319671 -vt 0.694824219 0.763319671 -vt 0.694824219 0.828893423 -vt 0.726074219 0.828893423 -vt 0.726074219 0.763319671 -vt 0.913574219 0.765368819 -vt 0.913574219 0.830942631 -vt 0.944824219 0.830942631 -vt 0.944824219 0.765368819 -vt 0.205566406 0.792008162 -vt 0.205566406 0.923155725 -vt 0.221191406 0.923155725 -vt 0.221191406 0.792008162 -vt 0.222167969 0.792008162 -vt 0.222167969 0.923155725 -vt 0.237792969 0.923155725 -vt 0.237792969 0.792008162 -vt 0.945800781 0.794057369 -vt 0.945800781 0.859631181 -vt 0.977050781 0.859631181 -vt 0.977050781 0.794057369 -vt 0.978027344 0.794057369 -vt 0.978027344 0.925204933 -vt 0.993652344 0.925204933 -vt 0.993652344 0.794057369 -vt 0.739746094 0.796106577 -vt 0.739746094 0.828893423 -vt 0.802246094 0.828893423 -vt 0.802246094 0.796106577 -vt 0.489746094 0.796106577 -vt 0.489746094 0.927254081 -vt 0.505371094 0.927254081 -vt 0.505371094 0.796106577 -vt 0.803222656 0.796106577 -vt 0.803222656 0.828893423 -vt 0.865722656 0.828893423 -vt 0.865722656 0.796106577 -vt 0.506347656 0.796106577 -vt 0.506347656 0.894467235 -vt 0.521972656 0.894467235 -vt 0.521972656 0.796106577 -vt 0.866699219 0.796106577 -vt 0.866699219 0.894467235 -vt 0.882324219 0.894467235 -vt 0.882324219 0.796106577 -vt 0.883300781 0.796106577 -vt 0.883300781 0.894467235 -vt 0.898925781 0.894467235 -vt 0.898925781 0.796106577 -vt 0.283691406 0.798155725 -vt 0.283691406 0.896516383 -vt 0.299316406 0.896516383 -vt 0.299316406 0.798155725 -vt 0.300292969 0.798155725 -vt 0.300292969 0.896516383 -vt 0.315917969 0.896516383 -vt 0.315917969 0.798155725 -vt 0.316894531 0.798155725 -vt 0.316894531 0.896516383 -vt 0.332519531 0.896516383 -vt 0.332519531 0.798155725 -vt 0.333496094 0.798155725 -vt 0.333496094 0.830942631 -vt 0.380371094 0.830942631 -vt 0.380371094 0.798155725 -vt 0.394042969 0.798155725 -vt 0.394042969 0.830942631 -vt 0.440917969 0.830942631 -vt 0.440917969 0.798155725 -vt 0.00048828125 0.824795067 -vt 0.00048828125 0.857581973 -vt 0.0473632812 0.857581973 -vt 0.0473632812 0.824795067 -vt 0.0483398438 0.824795067 -vt 0.0483398438 0.857581973 -vt 0.0952148438 0.857581973 -vt 0.0952148438 0.824795067 -vt 0.142089844 0.826844275 -vt 0.142089844 0.859631181 -vt 0.188964844 0.859631181 -vt 0.188964844 0.826844275 -vt 0.537597656 0.828893423 -vt 0.537597656 0.861680329 -vt 0.584472656 0.861680329 -vt 0.584472656 0.828893423 -vt 0.0961914062 0.824795067 -vt 0.0961914062 0.923155725 -vt 0.111816406 0.923155725 -vt 0.111816406 0.824795067 -vt 0.112792969 0.824795067 -vt 0.112792969 0.923155725 -vt 0.128417969 0.923155725 -vt 0.128417969 0.824795067 -vt 0.598144531 0.830942631 -vt 0.598144531 0.863729477 -vt 0.645019531 0.863729477 -vt 0.645019531 0.830942631 -vt 0.457519531 0.826844275 -vt 0.457519531 0.925204933 -vt 0.473144531 0.925204933 -vt 0.473144531 0.826844275 -vt 0.645996094 0.830942631 -vt 0.645996094 0.896516383 -vt 0.661621094 0.896516383 -vt 0.661621094 0.830942631 -vt 0.662597656 0.830942631 -vt 0.662597656 0.896516383 -vt 0.678222656 0.896516383 -vt 0.678222656 0.830942631 -vt 0.679199219 0.830942631 -vt 0.679199219 0.863729477 -vt 0.710449219 0.863729477 -vt 0.710449219 0.830942631 -vt 0.711425781 0.830942631 -vt 0.711425781 0.863729477 -vt 0.742675781 0.863729477 -vt 0.742675781 0.830942631 -vt 0.743652344 0.830942631 -vt 0.743652344 0.863729477 -vt 0.774902344 0.863729477 -vt 0.774902344 0.830942631 -vt 0.775878906 0.830942631 -vt 0.775878906 0.896516383 -vt 0.791503906 0.896516383 -vt 0.791503906 0.830942631 -vt 0.792480469 0.830942631 -vt 0.792480469 0.896516383 -vt 0.808105469 0.896516383 -vt 0.808105469 0.830942631 -vt 0.809082031 0.830942631 -vt 0.809082031 0.896516383 -vt 0.824707031 0.896516383 -vt 0.824707031 0.830942631 -vt 0.825683594 0.830942631 -vt 0.825683594 0.896516383 -vt 0.841308594 0.896516383 -vt 0.841308594 0.830942631 -vt 0.333496094 0.832991838 -vt 0.333496094 0.865778685 -vt 0.364746094 0.865778685 -vt 0.364746094 0.832991838 -vt 0.842285156 0.830942631 -vt 0.842285156 0.896516383 -vt 0.857910156 0.896516383 -vt 0.857910156 0.830942631 -vt 0.365722656 0.832991838 -vt 0.365722656 0.89856559 -vt 0.381347656 0.89856559 -vt 0.381347656 0.832991838 -vt 0.394042969 0.832991838 -vt 0.394042969 0.89856559 -vt 0.409667969 0.89856559 -vt 0.409667969 0.832991838 -vt 0.410644531 0.832991838 -vt 0.410644531 0.89856559 -vt 0.426269531 0.89856559 -vt 0.426269531 0.832991838 -vt 0.427246094 0.832991838 -vt 0.427246094 0.89856559 -vt 0.442871094 0.89856559 -vt 0.442871094 0.832991838 -vt 0.913574219 0.832991838 -vt 0.913574219 0.89856559 -vt 0.929199219 0.89856559 -vt 0.929199219 0.832991838 -vt 0.253417969 0.857581973 -vt 0.253417969 0.923155725 -vt 0.269042969 0.923155725 -vt 0.269042969 0.857581973 -vt 0.00048828125 0.859631181 -vt 0.00048828125 0.925204933 -vt 0.0161132812 0.925204933 -vt 0.0161132812 0.859631181 -vt 0.0170898438 0.859631181 -vt 0.0170898438 0.892418027 -vt 0.0483398438 0.892418027 -vt 0.0483398438 0.859631181 -vt 0.0493164062 0.859631181 -vt 0.0493164062 0.892418027 -vt 0.0805664062 0.892418027 -vt 0.0805664062 0.859631181 -vt 0.142089844 0.861680329 -vt 0.142089844 0.927254081 -vt 0.157714844 0.927254081 -vt 0.157714844 0.861680329 -vt 0.158691406 0.861680329 -vt 0.158691406 0.894467235 -vt 0.189941406 0.894467235 -vt 0.189941406 0.861680329 -vt 0.945800781 0.861680329 -vt 0.945800781 0.927254081 -vt 0.961425781 0.927254081 -vt 0.961425781 0.861680329 -vt 0.537597656 0.863729477 -vt 0.537597656 0.929303288 -vt 0.553222656 0.929303288 -vt 0.553222656 0.863729477 -vt 0.554199219 0.863729477 -vt 0.554199219 0.929303288 -vt 0.569824219 0.929303288 -vt 0.569824219 0.863729477 -vt 0.570800781 0.863729477 -vt 0.570800781 0.929303288 -vt 0.586425781 0.929303288 -vt 0.586425781 0.863729477 -vt 0.598144531 0.865778685 -vt 0.598144531 0.931352437 -vt 0.613769531 0.931352437 -vt 0.613769531 0.865778685 -vt 0.614746094 0.865778685 -vt 0.614746094 0.931352437 -vt 0.630371094 0.931352437 -vt 0.630371094 0.865778685 -vt 0.679199219 0.865778685 -vt 0.679199219 0.931352437 -vt 0.694824219 0.931352437 -vt 0.694824219 0.865778685 -vt 0.695800781 0.865778685 -vt 0.695800781 0.931352437 -vt 0.711425781 0.931352437 -vt 0.711425781 0.865778685 -vt 0.712402344 0.865778685 -vt 0.712402344 0.931352437 -vt 0.728027344 0.931352437 -vt 0.728027344 0.865778685 -vt 0.729003906 0.865778685 -vt 0.729003906 0.931352437 -vt 0.744628906 0.931352437 -vt 0.744628906 0.865778685 -vt 0.745605469 0.865778685 -vt 0.745605469 0.931352437 -vt 0.761230469 0.931352437 -vt 0.761230469 0.865778685 -vt 0.333496094 0.867827892 -vt 0.333496094 0.900614738 -vt 0.364746094 0.900614738 -vt 0.364746094 0.867827892 -vt 0.0170898438 0.894467235 -vt 0.0170898438 0.960040987 -vt 0.0327148438 0.960040987 -vt 0.0327148438 0.894467235 -vt 0.0336914062 0.894467235 -vt 0.0336914062 0.960040987 -vt 0.0493164062 0.960040987 -vt 0.0493164062 0.894467235 -vt 0.0502929688 0.894467235 -vt 0.0502929688 0.960040987 -vt 0.0659179688 0.960040987 -vt 0.0659179688 0.894467235 -vt 0.158691406 0.896516383 -vt 0.158691406 0.929303288 -vt 0.189941406 0.929303288 -vt 0.189941406 0.896516383 -vt 0.0668945312 0.894467235 -vt 0.0668945312 0.960040987 -vt 0.0825195312 0.960040987 -vt 0.0825195312 0.894467235 -vt 0.506347656 0.896516383 -vt 0.506347656 0.962090135 -vt 0.521972656 0.962090135 -vt 0.521972656 0.896516383 -vt 0.866699219 0.896516383 -vt 0.866699219 0.929303288 -vt 0.897949219 0.929303288 -vt 0.897949219 0.896516383 -vt 0.283691406 0.89856559 -vt 0.283691406 0.964139342 -vt 0.299316406 0.964139342 -vt 0.299316406 0.89856559 -vt 0.300292969 0.89856559 -vt 0.300292969 0.964139342 -vt 0.315917969 0.964139342 -vt 0.315917969 0.89856559 -vt 0.316894531 0.89856559 -vt 0.316894531 0.964139342 -vt 0.332519531 0.964139342 -vt 0.332519531 0.89856559 -vt 0.645996094 0.89856559 -vt 0.645996094 0.964139342 -vt 0.661621094 0.964139342 -vt 0.661621094 0.89856559 -vt 0.662597656 0.89856559 -vt 0.662597656 0.964139342 -vt 0.678222656 0.964139342 -vt 0.678222656 0.89856559 -vt 0.775878906 0.89856559 -vt 0.775878906 0.964139342 -vt 0.791503906 0.964139342 -vt 0.791503906 0.89856559 -vt 0.792480469 0.89856559 -vt 0.792480469 0.931352437 -vt 0.823730469 0.931352437 -vt 0.823730469 0.89856559 -vt 0.824707031 0.89856559 -vt 0.824707031 0.931352437 -vt 0.855957031 0.931352437 -vt 0.855957031 0.89856559 -vt 0.365722656 0.900614738 -vt 0.365722656 0.96618855 -vt 0.381347656 0.96618855 -vt 0.381347656 0.900614738 -vt 0.394042969 0.900614738 -vt 0.394042969 0.96618855 -vt 0.409667969 0.96618855 -vt 0.409667969 0.900614738 -vt 0.410644531 0.900614738 -vt 0.410644531 0.96618855 -vt 0.426269531 0.96618855 -vt 0.426269531 0.900614738 -vt 0.427246094 0.900614738 -vt 0.427246094 0.96618855 -vt 0.442871094 0.96618855 -vt 0.442871094 0.900614738 -vt 0.913574219 0.900614738 -vt 0.913574219 0.933401644 -vt 0.944824219 0.933401644 -vt 0.944824219 0.900614738 -vt 0.333496094 0.902663946 -vt 0.333496094 0.968237698 -vt 0.349121094 0.968237698 -vt 0.349121094 0.902663946 -vt 0.0961914062 0.925204933 -vt 0.0961914062 0.957991779 -vt 0.127441406 0.957991779 -vt 0.127441406 0.925204933 -vt 0.205566406 0.925204933 -vt 0.205566406 0.990778685 -vt 0.221191406 0.990778685 -vt 0.221191406 0.925204933 -vt 0.222167969 0.925204933 -vt 0.222167969 0.990778685 -vt 0.237792969 0.990778685 -vt 0.237792969 0.925204933 -vt 0.457519531 0.927254081 -vt 0.457519531 0.960040987 -vt 0.488769531 0.960040987 -vt 0.488769531 0.927254081 -vt 0.253417969 0.925204933 -vt 0.253417969 0.990778685 -vt 0.269042969 0.990778685 -vt 0.269042969 0.925204933 -vt 0.00048828125 0.927254081 -vt 0.00048828125 0.992827892 -vt 0.0161132812 0.992827892 -vt 0.0161132812 0.927254081 -vt 0.945800781 0.929303288 -vt 0.945800781 0.962090135 -vt 0.977050781 0.962090135 -vt 0.977050781 0.929303288 -vt 0.978027344 0.927254081 -vt 0.978027344 0.960040987 -vt 0.993652344 0.960040987 -vt 0.993652344 0.927254081 -vt 0.142089844 0.929303288 -vt 0.142089844 0.962090135 -vt 0.157714844 0.962090135 -vt 0.157714844 0.929303288 -vt 0.489746094 0.929303288 -vt 0.489746094 0.962090135 -vt 0.505371094 0.962090135 -vt 0.505371094 0.929303288 -vt 0.158691406 0.931352437 -vt 0.158691406 0.964139342 -vt 0.174316406 0.964139342 -vt 0.174316406 0.931352437 -vt 0.175292969 0.931352437 -vt 0.175292969 0.964139342 -vt 0.190917969 0.964139342 -vt 0.190917969 0.931352437 -vt 0.537597656 0.931352437 -vt 0.537597656 0.964139342 -vt 0.553222656 0.964139342 -vt 0.553222656 0.931352437 -vt 0.554199219 0.931352437 -vt 0.554199219 0.964139342 -vt 0.569824219 0.964139342 -vt 0.569824219 0.931352437 -vt 0.570800781 0.931352437 -vt 0.570800781 0.964139342 -vt 0.586425781 0.964139342 -vt 0.586425781 0.931352437 -vt 0.866699219 0.931352437 -vt 0.866699219 0.964139342 -vt 0.882324219 0.964139342 -vt 0.882324219 0.931352437 -vt 0.883300781 0.931352437 -vt 0.883300781 0.964139342 -vt 0.898925781 0.964139342 -vt 0.898925781 0.931352437 -vt 0.598144531 0.933401644 -vt 0.598144531 0.96618855 -vt 0.613769531 0.96618855 -vt 0.613769531 0.933401644 -vt 0.614746094 0.933401644 -vt 0.614746094 0.96618855 -vt 0.630371094 0.96618855 -vt 0.630371094 0.933401644 -vt 0.679199219 0.933401644 -vt 0.679199219 0.96618855 -vt 0.694824219 0.96618855 -vt 0.694824219 0.933401644 -vt 0.695800781 0.933401644 -vt 0.695800781 0.96618855 -vt 0.711425781 0.96618855 -vt 0.711425781 0.933401644 -vt 0.712402344 0.933401644 -vt 0.712402344 0.96618855 -vt 0.728027344 0.96618855 -vt 0.728027344 0.933401644 -vt 0.729003906 0.933401644 -vt 0.729003906 0.96618855 -vt 0.744628906 0.96618855 -vt 0.744628906 0.933401644 -vt 0.745605469 0.933401644 -vt 0.745605469 0.96618855 -vt 0.761230469 0.96618855 -vt 0.761230469 0.933401644 -vt 0.792480469 0.933401644 -vt 0.792480469 0.96618855 -vt 0.808105469 0.96618855 -vt 0.808105469 0.933401644 -vt 0.809082031 0.933401644 -vt 0.809082031 0.96618855 -vt 0.824707031 0.96618855 -vt 0.824707031 0.933401644 -vt 0.825683594 0.933401644 -vt 0.825683594 0.96618855 -vt 0.841308594 0.96618855 -vt 0.841308594 0.933401644 -vt 0.842285156 0.933401644 -vt 0.842285156 0.96618855 -vt 0.857910156 0.96618855 -vt 0.857910156 0.933401644 -vt 0.913574219 0.935450792 -vt 0.913574219 0.968237698 -vt 0.929199219 0.968237698 -vt 0.929199219 0.935450792 -vt 0.0961914062 0.960040987 -vt 0.0961914062 0.992827892 -vt 0.111816406 0.992827892 -vt 0.111816406 0.960040987 -vt 0.112792969 0.960040987 -vt 0.112792969 0.992827892 -vt 0.128417969 0.992827892 -vt 0.128417969 0.960040987 -vt 0.0170898438 0.962090135 -vt 0.0170898438 0.99487704 -vt 0.0327148438 0.99487704 -vt 0.0327148438 0.962090135 -vt 0.0336914062 0.962090135 -vt 0.0336914062 0.99487704 -vt 0.0493164062 0.99487704 -vt 0.0493164062 0.962090135 -vt 0.0502929688 0.962090135 -vt 0.0502929688 0.99487704 -vt 0.0659179688 0.99487704 -vt 0.0659179688 0.962090135 -vt 0.0668945312 0.962090135 -vt 0.0668945312 0.99487704 -vt 0.0825195312 0.99487704 -vt 0.0825195312 0.962090135 -vt 0.457519531 0.962090135 -vt 0.457519531 0.99487704 -vt 0.473144531 0.99487704 -vt 0.473144531 0.962090135 -vt 0.978027344 0.962090135 -vt 0.978027344 0.99487704 -vt 0.993652344 0.99487704 -vt 0.993652344 0.962090135 -vt 0.142089844 0.964139342 -vt 0.142089844 0.996926248 -vt 0.157714844 0.996926248 -vt 0.157714844 0.964139342 -vt 0.489746094 0.964139342 -vt 0.489746094 0.996926248 -vt 0.505371094 0.996926248 -vt 0.505371094 0.964139342 -vt 0.506347656 0.964139342 -vt 0.506347656 0.996926248 -vt 0.521972656 0.996926248 -vt 0.521972656 0.964139342 -vt 0.945800781 0.964139342 -vt 0.945800781 0.996926248 -vt 0.961425781 0.996926248 -vt 0.961425781 0.964139342 -vt 0.158691406 0.96618855 -vt 0.158691406 0.998975396 -vt 0.174316406 0.998975396 -vt 0.174316406 0.96618855 -vt 0.175292969 0.96618855 -vt 0.175292969 0.998975396 -vt 0.190917969 0.998975396 -vt 0.190917969 0.96618855 -vt 0.283691406 0.96618855 -vt 0.283691406 0.998975396 -vt 0.299316406 0.998975396 -vt 0.299316406 0.96618855 -vt 0.300292969 0.96618855 -vt 0.300292969 0.998975396 -vt 0.315917969 0.998975396 -vt 0.315917969 0.96618855 -vt 0.316894531 0.96618855 -vt 0.316894531 0.998975396 -vt 0.332519531 0.998975396 -vt 0.332519531 0.96618855 -vt 0.537597656 0.96618855 -vt 0.537597656 0.998975396 -vt 0.553222656 0.998975396 -vt 0.553222656 0.96618855 -vt 0.554199219 0.96618855 -vt 0.554199219 0.998975396 -vt 0.569824219 0.998975396 -vt 0.569824219 0.96618855 -vt 0.570800781 0.96618855 -vt 0.570800781 0.998975396 -vt 0.586425781 0.998975396 -vt 0.586425781 0.96618855 -vt 0.645996094 0.96618855 -vt 0.645996094 0.998975396 -vt 0.661621094 0.998975396 -vt 0.661621094 0.96618855 -g xinzexi_2_n-mesh_0 -f 3/3/3 2/2/2 1/1/1 -f 1/1/1 4/4/4 3/3/3 -f 7/7/7 6/6/6 5/5/5 -f 5/5/5 8/8/8 7/7/7 -f 11/11/11 10/10/10 9/9/9 -f 9/9/9 12/12/12 11/11/11 -f 15/15/15 14/14/14 13/13/13 -f 13/13/13 16/16/16 15/15/15 -f 19/19/19 18/18/18 17/17/17 -f 17/17/17 20/20/20 19/19/19 -f 23/23/23 22/22/22 21/21/21 -f 21/21/21 24/24/24 23/23/23 -f 27/27/27 26/26/26 25/25/25 -f 25/25/25 28/28/28 27/27/27 -f 31/31/31 30/30/30 29/29/29 -f 29/29/29 32/32/32 31/31/31 -f 35/35/35 34/34/34 33/33/33 -f 33/33/33 36/36/36 35/35/35 -f 39/39/39 38/38/38 37/37/37 -f 37/37/37 40/40/40 39/39/39 -f 43/43/43 42/42/42 41/41/41 -f 41/41/41 44/44/44 43/43/43 -f 47/47/47 46/46/46 45/45/45 -f 45/45/45 48/48/48 47/47/47 -f 51/51/51 50/50/50 49/49/49 -f 49/49/49 52/52/52 51/51/51 -f 55/55/55 54/54/54 53/53/53 -f 53/53/53 56/56/56 55/55/55 -f 59/59/59 58/58/58 57/57/57 -f 57/57/57 60/60/60 59/59/59 -f 63/63/63 62/62/62 61/61/61 -f 61/61/61 64/64/64 63/63/63 -f 67/67/67 66/66/66 65/65/65 -f 65/65/65 68/68/68 67/67/67 -f 71/71/71 70/70/70 69/69/69 -f 69/69/69 72/72/72 71/71/71 -f 75/75/75 74/74/74 73/73/73 -f 73/73/73 76/76/76 75/75/75 -f 79/79/79 78/78/78 77/77/77 -f 77/77/77 80/80/80 79/79/79 -f 83/83/83 82/82/82 81/81/81 -f 81/81/81 84/84/84 83/83/83 -f 87/87/87 86/86/86 85/85/85 -f 85/85/85 88/88/88 87/87/87 -f 91/91/91 90/90/90 89/89/89 -f 89/89/89 92/92/92 91/91/91 -f 95/95/95 94/94/94 93/93/93 -f 93/93/93 96/96/96 95/95/95 -f 99/99/99 98/98/98 97/97/97 -f 97/97/97 100/100/100 99/99/99 -f 103/103/103 102/102/102 101/101/101 -f 101/101/101 104/104/104 103/103/103 -f 107/107/107 106/106/106 105/105/105 -f 105/105/105 108/108/108 107/107/107 -f 111/111/111 110/110/110 109/109/109 -f 109/109/109 112/112/112 111/111/111 -f 115/115/115 114/114/114 113/113/113 -f 113/113/113 116/116/116 115/115/115 -f 119/119/119 118/118/118 117/117/117 -f 117/117/117 120/120/120 119/119/119 -f 123/123/123 122/122/122 121/121/121 -f 121/121/121 124/124/124 123/123/123 -f 127/127/127 126/126/126 125/125/125 -f 125/125/125 128/128/128 127/127/127 -f 131/131/131 130/130/130 129/129/129 -f 129/129/129 132/132/132 131/131/131 -f 135/135/135 134/134/134 133/133/133 -f 133/133/133 136/136/136 135/135/135 -f 139/139/139 138/138/138 137/137/137 -f 137/137/137 140/140/140 139/139/139 -f 143/143/143 142/142/142 141/141/141 -f 141/141/141 144/144/144 143/143/143 -f 147/147/147 146/146/146 145/145/145 -f 145/145/145 148/148/148 147/147/147 -f 151/151/151 150/150/150 149/149/149 -f 149/149/149 152/152/152 151/151/151 -f 155/155/155 154/154/154 153/153/153 -f 153/153/153 156/156/156 155/155/155 -f 159/159/159 158/158/158 157/157/157 -f 157/157/157 160/160/160 159/159/159 -f 163/163/163 162/162/162 161/161/161 -f 161/161/161 164/164/164 163/163/163 -f 167/167/167 166/166/166 165/165/165 -f 165/165/165 168/168/168 167/167/167 -f 171/171/171 170/170/170 169/169/169 -f 169/169/169 172/172/172 171/171/171 -f 175/175/175 174/174/174 173/173/173 -f 173/173/173 176/176/176 175/175/175 -f 179/179/179 178/178/178 177/177/177 -f 177/177/177 180/180/180 179/179/179 -f 183/183/183 182/182/182 181/181/181 -f 181/181/181 184/184/184 183/183/183 -f 187/187/187 186/186/186 185/185/185 -f 185/185/185 188/188/188 187/187/187 -f 191/191/191 190/190/190 189/189/189 -f 189/189/189 192/192/192 191/191/191 -f 195/195/195 194/194/194 193/193/193 -f 193/193/193 196/196/196 195/195/195 -f 199/199/199 198/198/198 197/197/197 -f 197/197/197 200/200/200 199/199/199 -f 203/203/203 202/202/202 201/201/201 -f 201/201/201 204/204/204 203/203/203 -f 207/207/207 206/206/206 205/205/205 -f 205/205/205 208/208/208 207/207/207 -f 211/211/211 210/210/210 209/209/209 -f 209/209/209 212/212/212 211/211/211 -f 215/215/215 214/214/214 213/213/213 -f 213/213/213 216/216/216 215/215/215 -f 219/219/219 218/218/218 217/217/217 -f 217/217/217 220/220/220 219/219/219 -f 223/223/223 222/222/222 221/221/221 -f 221/221/221 224/224/224 223/223/223 -f 227/227/227 226/226/226 225/225/225 -f 225/225/225 228/228/228 227/227/227 -f 231/231/231 230/230/230 229/229/229 -f 229/229/229 232/232/232 231/231/231 -f 235/235/235 234/234/234 233/233/233 -f 233/233/233 236/236/236 235/235/235 -f 239/239/239 238/238/238 237/237/237 -f 237/237/237 240/240/240 239/239/239 -f 243/243/243 242/242/242 241/241/241 -f 241/241/241 244/244/244 243/243/243 -f 247/247/247 246/246/246 245/245/245 -f 245/245/245 248/248/248 247/247/247 -f 251/251/251 250/250/250 249/249/249 -f 249/249/249 252/252/252 251/251/251 -f 255/255/255 254/254/254 253/253/253 -f 253/253/253 256/256/256 255/255/255 -f 259/259/259 258/258/258 257/257/257 -f 257/257/257 260/260/260 259/259/259 -f 263/263/263 262/262/262 261/261/261 -f 261/261/261 264/264/264 263/263/263 -f 267/267/267 266/266/266 265/265/265 -f 265/265/265 268/268/268 267/267/267 -f 271/271/271 270/270/270 269/269/269 -f 269/269/269 272/272/272 271/271/271 -f 275/275/275 274/274/274 273/273/273 -f 273/273/273 276/276/276 275/275/275 -f 279/279/279 278/278/278 277/277/277 -f 277/277/277 280/280/280 279/279/279 -f 283/283/283 282/282/282 281/281/281 -f 281/281/281 284/284/284 283/283/283 -f 287/287/287 286/286/286 285/285/285 -f 285/285/285 288/288/288 287/287/287 -f 291/291/291 290/290/290 289/289/289 -f 289/289/289 292/292/292 291/291/291 -f 295/295/295 294/294/294 293/293/293 -f 293/293/293 296/296/296 295/295/295 -f 299/299/299 298/298/298 297/297/297 -f 297/297/297 300/300/300 299/299/299 -f 303/303/303 302/302/302 301/301/301 -f 301/301/301 304/304/304 303/303/303 -f 307/307/307 306/306/306 305/305/305 -f 305/305/305 308/308/308 307/307/307 -f 311/311/311 310/310/310 309/309/309 -f 309/309/309 312/312/312 311/311/311 -f 315/315/315 314/314/314 313/313/313 -f 313/313/313 316/316/316 315/315/315 -f 319/319/319 318/318/318 317/317/317 -f 317/317/317 320/320/320 319/319/319 -f 323/323/323 322/322/322 321/321/321 -f 321/321/321 324/324/324 323/323/323 -f 327/327/327 326/326/326 325/325/325 -f 325/325/325 328/328/328 327/327/327 -f 331/331/331 330/330/330 329/329/329 -f 329/329/329 332/332/332 331/331/331 -f 335/335/335 334/334/334 333/333/333 -f 333/333/333 336/336/336 335/335/335 -f 339/339/339 338/338/338 337/337/337 -f 337/337/337 340/340/340 339/339/339 -f 343/343/343 342/342/342 341/341/341 -f 341/341/341 344/344/344 343/343/343 -f 347/347/347 346/346/346 345/345/345 -f 345/345/345 348/348/348 347/347/347 -f 351/351/351 350/350/350 349/349/349 -f 349/349/349 352/352/352 351/351/351 -f 355/355/355 354/354/354 353/353/353 -f 353/353/353 356/356/356 355/355/355 -f 359/359/359 358/358/358 357/357/357 -f 357/357/357 360/360/360 359/359/359 -f 363/363/363 362/362/362 361/361/361 -f 361/361/361 364/364/364 363/363/363 -f 367/367/367 366/366/366 365/365/365 -f 365/365/365 368/368/368 367/367/367 -f 371/371/371 370/370/370 369/369/369 -f 369/369/369 372/372/372 371/371/371 -f 375/375/375 374/374/374 373/373/373 -f 373/373/373 376/376/376 375/375/375 -f 379/379/379 378/378/378 377/377/377 -f 377/377/377 380/380/380 379/379/379 -f 383/383/383 382/382/382 381/381/381 -f 381/381/381 384/384/384 383/383/383 -f 387/387/387 386/386/386 385/385/385 -f 385/385/385 388/388/388 387/387/387 -f 391/391/391 390/390/390 389/389/389 -f 389/389/389 392/392/392 391/391/391 -f 395/395/395 394/394/394 393/393/393 -f 393/393/393 396/396/396 395/395/395 -f 399/399/399 398/398/398 397/397/397 -f 397/397/397 400/400/400 399/399/399 -f 403/403/403 402/402/402 401/401/401 -f 401/401/401 404/404/404 403/403/403 -f 407/407/407 406/406/406 405/405/405 -f 405/405/405 408/408/408 407/407/407 -f 411/411/411 410/410/410 409/409/409 -f 409/409/409 412/412/412 411/411/411 -f 415/415/415 414/414/414 413/413/413 -f 413/413/413 416/416/416 415/415/415 -f 419/419/419 418/418/418 417/417/417 -f 417/417/417 420/420/420 419/419/419 -f 423/423/423 422/422/422 421/421/421 -f 421/421/421 424/424/424 423/423/423 -f 427/427/427 426/426/426 425/425/425 -f 425/425/425 428/428/428 427/427/427 -f 431/431/431 430/430/430 429/429/429 -f 429/429/429 432/432/432 431/431/431 -f 435/435/435 434/434/434 433/433/433 -f 433/433/433 436/436/436 435/435/435 -f 439/439/439 438/438/438 437/437/437 -f 437/437/437 440/440/440 439/439/439 -f 443/443/443 442/442/442 441/441/441 -f 441/441/441 444/444/444 443/443/443 -f 447/447/447 446/446/446 445/445/445 -f 445/445/445 448/448/448 447/447/447 -f 451/451/451 450/450/450 449/449/449 -f 449/449/449 452/452/452 451/451/451 -f 455/455/455 454/454/454 453/453/453 -f 453/453/453 456/456/456 455/455/455 -f 459/459/459 458/458/458 457/457/457 -f 457/457/457 460/460/460 459/459/459 -f 463/463/463 462/462/462 461/461/461 -f 461/461/461 464/464/464 463/463/463 -f 467/467/467 466/466/466 465/465/465 -f 465/465/465 468/468/468 467/467/467 -f 471/471/471 470/470/470 469/469/469 -f 469/469/469 472/472/472 471/471/471 -f 475/475/475 474/474/474 473/473/473 -f 473/473/473 476/476/476 475/475/475 -f 479/479/479 478/478/478 477/477/477 -f 477/477/477 480/480/480 479/479/479 -f 483/483/483 482/482/482 481/481/481 -f 481/481/481 484/484/484 483/483/483 -f 487/487/487 486/486/486 485/485/485 -f 485/485/485 488/488/488 487/487/487 -f 491/491/491 490/490/490 489/489/489 -f 489/489/489 492/492/492 491/491/491 -f 495/495/495 494/494/494 493/493/493 -f 493/493/493 496/496/496 495/495/495 -f 499/499/499 498/498/498 497/497/497 -f 497/497/497 500/500/500 499/499/499 -f 503/503/503 502/502/502 501/501/501 -f 501/501/501 504/504/504 503/503/503 -f 507/507/507 506/506/506 505/505/505 -f 505/505/505 508/508/508 507/507/507 -f 511/511/511 510/510/510 509/509/509 -f 509/509/509 512/512/512 511/511/511 -f 515/515/515 514/514/514 513/513/513 -f 513/513/513 516/516/516 515/515/515 -f 519/519/519 518/518/518 517/517/517 -f 517/517/517 520/520/520 519/519/519 -f 523/523/523 522/522/522 521/521/521 -f 521/521/521 524/524/524 523/523/523 -f 527/527/527 526/526/526 525/525/525 -f 525/525/525 528/528/528 527/527/527 -f 531/531/531 530/530/530 529/529/529 -f 529/529/529 532/532/532 531/531/531 -f 535/535/535 534/534/534 533/533/533 -f 533/533/533 536/536/536 535/535/535 -f 539/539/539 538/538/538 537/537/537 -f 537/537/537 540/540/540 539/539/539 -f 543/543/543 542/542/542 541/541/541 -f 541/541/541 544/544/544 543/543/543 -f 547/547/547 546/546/546 545/545/545 -f 545/545/545 548/548/548 547/547/547 -f 551/551/551 550/550/550 549/549/549 -f 549/549/549 552/552/552 551/551/551 -f 555/555/555 554/554/554 553/553/553 -f 553/553/553 556/556/556 555/555/555 -f 559/559/559 558/558/558 557/557/557 -f 557/557/557 560/560/560 559/559/559 -f 563/563/563 562/562/562 561/561/561 -f 561/561/561 564/564/564 563/563/563 -f 567/567/567 566/566/566 565/565/565 -f 565/565/565 568/568/568 567/567/567 -f 571/571/571 570/570/570 569/569/569 -f 569/569/569 572/572/572 571/571/571 -f 575/575/575 574/574/574 573/573/573 -f 573/573/573 576/576/576 575/575/575 -f 579/579/579 578/578/578 577/577/577 -f 577/577/577 580/580/580 579/579/579 -f 583/583/583 582/582/582 581/581/581 -f 581/581/581 584/584/584 583/583/583 -f 587/587/587 586/586/586 585/585/585 -f 585/585/585 588/588/588 587/587/587 -f 591/591/591 590/590/590 589/589/589 -f 589/589/589 592/592/592 591/591/591 -f 595/595/595 594/594/594 593/593/593 -f 593/593/593 596/596/596 595/595/595 -f 599/599/599 598/598/598 597/597/597 -f 597/597/597 600/600/600 599/599/599 -f 603/603/603 602/602/602 601/601/601 -f 601/601/601 604/604/604 603/603/603 -f 607/607/607 606/606/606 605/605/605 -f 605/605/605 608/608/608 607/607/607 -f 611/611/611 610/610/610 609/609/609 -f 609/609/609 612/612/612 611/611/611 -f 615/615/615 614/614/614 613/613/613 -f 613/613/613 616/616/616 615/615/615 -f 619/619/619 618/618/618 617/617/617 -f 617/617/617 620/620/620 619/619/619 -f 623/623/623 622/622/622 621/621/621 -f 621/621/621 624/624/624 623/623/623 -f 627/627/627 626/626/626 625/625/625 -f 625/625/625 628/628/628 627/627/627 -f 631/631/631 630/630/630 629/629/629 -f 629/629/629 632/632/632 631/631/631 -f 635/635/635 634/634/634 633/633/633 -f 633/633/633 636/636/636 635/635/635 -f 639/639/639 638/638/638 637/637/637 -f 637/637/637 640/640/640 639/639/639 -f 643/643/643 642/642/642 641/641/641 -f 641/641/641 644/644/644 643/643/643 -f 647/647/647 646/646/646 645/645/645 -f 645/645/645 648/648/648 647/647/647 -f 651/651/651 650/650/650 649/649/649 -f 649/649/649 652/652/652 651/651/651 -f 655/655/655 654/654/654 653/653/653 -f 653/653/653 656/656/656 655/655/655 -f 659/659/659 658/658/658 657/657/657 -f 657/657/657 660/660/660 659/659/659 -f 663/663/663 662/662/662 661/661/661 -f 661/661/661 664/664/664 663/663/663 -f 667/667/667 666/666/666 665/665/665 -f 665/665/665 668/668/668 667/667/667 -f 671/671/671 670/670/670 669/669/669 -f 669/669/669 672/672/672 671/671/671 -f 675/675/675 674/674/674 673/673/673 -f 673/673/673 676/676/676 675/675/675 -f 679/679/679 678/678/678 677/677/677 -f 677/677/677 680/680/680 679/679/679 -f 683/683/683 682/682/682 681/681/681 -f 681/681/681 684/684/684 683/683/683 -f 687/687/687 686/686/686 685/685/685 -f 685/685/685 688/688/688 687/687/687 -f 691/691/691 690/690/690 689/689/689 -f 689/689/689 692/692/692 691/691/691 -f 695/695/695 694/694/694 693/693/693 -f 693/693/693 696/696/696 695/695/695 -f 699/699/699 698/698/698 697/697/697 -f 697/697/697 700/700/700 699/699/699 -f 703/703/703 702/702/702 701/701/701 -f 701/701/701 704/704/704 703/703/703 -f 707/707/707 706/706/706 705/705/705 -f 705/705/705 708/708/708 707/707/707 -f 711/711/711 710/710/710 709/709/709 -f 709/709/709 712/712/712 711/711/711 -f 715/715/715 714/714/714 713/713/713 -f 713/713/713 716/716/716 715/715/715 -f 719/719/719 718/718/718 717/717/717 -f 717/717/717 720/720/720 719/719/719 -f 723/723/723 722/722/722 721/721/721 -f 721/721/721 724/724/724 723/723/723 diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 00000000..5c8007a5 --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,75 @@ +import gzip +import os + +from UnityPy.files.BundleFile import BundleFile, BundleFileFS +from UnityPy.files.File import parse_file +from UnityPy.files.WebFile import WebFile +from UnityPy.streams.EndianBinaryReader import EndianBinaryReader + +LOCAL = os.path.join(os.path.dirname(__file__), "samples") + +WEBFILE_FP = os.path.join(LOCAL, "Build6_Web.data.gz") + + +def test_webfile(): + with open(WEBFILE_FP, "rb") as f: + cdata = f.read() + + # test raw + data = gzip.decompress(cdata) + reader = EndianBinaryReader(data) + webfile = parse_file(reader, "Build6_Web.data.gz") + assert isinstance(webfile, WebFile) + redata = webfile.dump().get_bytes() + assert redata == data + + # test gzip compressed + reader = EndianBinaryReader(cdata) + webfile = parse_file(reader, "Build6_Web.data.gz") + assert isinstance(webfile, WebFile) + redata = webfile.dump().get_bytes() + rereader = EndianBinaryReader(redata) + re_webfile = parse_file(rereader, "Build6_Web.data.gz") + assert isinstance(re_webfile, WebFile) + assert all( + [a == b for a, b in zip(webfile.directory_infos, re_webfile.directory_infos)] + ) + + +def _get_bundlefile_raw() -> bytes: + with open(WEBFILE_FP, "rb") as f: + cdata = f.read() + + reader = EndianBinaryReader(cdata) + webfile = parse_file(reader, "Build6_Web.data.gz") + for directory_info in webfile.directory_infos: + if directory_info.path == "data.unity3d": + webfile.directory_reader.seek(directory_info.offset) + return webfile.directory_reader.read(directory_info.size) + raise FileNotFoundError("data.unity3d not found in Build6_Web.data.gz") + + +def test_bundlefile_fs(): + cdata = _get_bundlefile_raw() + + reader = EndianBinaryReader(cdata) + bundlefile = parse_file(reader, "data.unity3d", f"{WEBFILE_FP}/data.unity3d") + assert isinstance(bundlefile, BundleFile) + assert isinstance(bundlefile, BundleFileFS) + redata = bundlefile.dump().get_bytes() + rereader = EndianBinaryReader(redata) + re_bundlefile = parse_file(rereader, "data.unity3d", f"{WEBFILE_FP}/data.unity3d") + assert isinstance(re_bundlefile, BundleFile) + assert isinstance(re_bundlefile, BundleFileFS) + assert all( + [ + a == b + for a, b in zip(bundlefile.directory_infos, re_bundlefile.directory_infos) + ] + ) + print() + + +if __name__ == "__main__": + test_webfile() + test_bundlefile_fs()