diff --git a/UnityPy/config.py b/UnityPy/config.py index a22a2962..f3edef74 100644 --- a/UnityPy/config.py +++ b/UnityPy/config.py @@ -1,20 +1,41 @@ -# used when no version is defined by the SerializedFile or its BundleFile -FALLBACK_UNITY_VERSION = "2.5.0f5" -# determines if the typetree structures for the Object types will be parsed -# disabling this will reduce the load time by a lot (half of the time is spend on parsing the typetrees) -# but it will also prevent saving an edited file +import warnings + +from .exceptions import UnityVersionFallbackError, UnityVersionFallbackWarning + + +FALLBACK_UNITY_VERSION = None +"""The Unity version to use when no version is defined + by the SerializedFile or its BundleFile. + + You may manually configure this value to a version string, e.g. `2.5.0f5`. +""" + SERIALIZED_FILE_PARSE_TYPETREE = True +"""Determines if the typetree structures for the Object types will be parsed. -# GLOBAL WARNING SUPPRESSION -FALLBACK_VERSION_WARNED = False # for FALLBACK_UNITY_VERSION + Disabling this will reduce the load time by a lot (half of the time is spend on parsing the typetrees), + but it will also prevent saving an edited file. +""" + + +# WARNINGS CONTROL +warnings.simplefilter("once", UnityVersionFallbackWarning) # GET FUNCTIONS def get_fallback_version(): - global FALLBACK_VERSION_WARNED - if not FALLBACK_VERSION_WARNED: - print( - f"Warning: 0.0.0 version found, defaulting to UnityPy.config.FALLBACK_UNITY_VERSION ({FALLBACK_UNITY_VERSION})" # noqa: E501 + global FALLBACK_UNITY_VERSION + + if not isinstance(FALLBACK_UNITY_VERSION, str): + raise UnityVersionFallbackError( + "No valid Unity version found, and the fallback version is not correctly configured. " + + "Please explicitly set the value of UnityPy.config.FALLBACK_UNITY_VERSION." ) - FALLBACK_VERSION_WARNED = True + + warnings.warn( + f"No valid Unity version found, defaulting to UnityPy.config.FALLBACK_UNITY_VERSION ({FALLBACK_UNITY_VERSION})", # noqa: E501 + category=UnityVersionFallbackWarning, + stacklevel=2 + ) + return FALLBACK_UNITY_VERSION diff --git a/UnityPy/exceptions.py b/UnityPy/exceptions.py index 1eda4850..86bd144d 100644 --- a/UnityPy/exceptions.py +++ b/UnityPy/exceptions.py @@ -1,5 +1,14 @@ class TypeTreeError(Exception): - def __init__(self, message, nodes): - # Call the base class constructor with the parameters it needs + def __init__(self, message, nodes): + super().__init__(message) + self.nodes = nodes + + +class UnityVersionFallbackError(Exception): + def __init__(self, message): + super().__init__(message) + + +class UnityVersionFallbackWarning(UserWarning): + def __init__(self, message): super().__init__(message) - self.nodes = nodes \ No newline at end of file diff --git a/UnityPy/files/BundleFile.py b/UnityPy/files/BundleFile.py index c35a0483..3cd650b5 100644 --- a/UnityPy/files/BundleFile.py +++ b/UnityPy/files/BundleFile.py @@ -1,10 +1,11 @@ # TODO: implement encryption for saving files from collections import namedtuple import re -from typing import Tuple, Union +from typing import Optional, Tuple, Union from . import File from ..enums import ArchiveFlags, ArchiveFlagsOld, CompressionFlags +from ..exceptions import UnityVersionFallbackError from ..helpers import ArchiveStorageManager, CompressionHelper from ..streams import EndianBinaryReader, EndianBinaryWriter @@ -21,12 +22,12 @@ class BundleFile(File.File): signature: str version_engine: str version_player: str - dataflags: Tuple[ArchiveFlags, ArchiveFlagsOld] - decryptor: ArchiveStorageManager.ArchiveStorageDecryptor = None + dataflags: Union[ArchiveFlags, ArchiveFlagsOld] + decryptor: Optional[ArchiveStorageManager.ArchiveStorageDecryptor] = None _uses_block_alignment: bool = False def __init__( - self, reader: EndianBinaryReader, parent: File, name: str = None, **kwargs + self, reader: EndianBinaryReader, parent: File, name: Optional[str] = None, **kwargs ): super().__init__(parent=parent, name=name, **kwargs) signature = self.signature = reader.read_string_to_null() @@ -92,7 +93,7 @@ def read_fs(self, reader: EndianBinaryReader): # header compressedSize = reader.read_u_int() uncompressedSize = reader.read_u_int() - self.dataflags = reader.read_u_int() + dataflagsValue = reader.read_u_int() version = self.get_version_tuple() # https://issuetracker.unity3d.com/issues/files-within-assetbundles-do-not-start-on-aligned-boundaries-breaking-patching-on-nintendo-switch @@ -105,9 +106,9 @@ def read_fs(self, reader: EndianBinaryReader): or (version[0] == 2021 and version < (2021, 3, 2)) or (version[0] == 2022 and version < (2022, 1, 1)) ): - self.dataflags = ArchiveFlagsOld(self.dataflags) + self.dataflags = ArchiveFlagsOld(dataflagsValue) else: - self.dataflags = ArchiveFlags(self.dataflags) + self.dataflags = ArchiveFlags(dataflagsValue) if self.dataflags & self.dataflags.UsesAssetBundleEncryption: self.decryptor = ArchiveStorageManager.ArchiveStorageDecryptor(reader) @@ -201,8 +202,8 @@ def save(self, packer=None): original - uses the original flags """ # file_header - # signature (string_to_null) - # format (int) + # signature (string_to_null) + # format (int) # version_player (string_to_null) # version_engine (string_to_null) writer = EndianBinaryWriter() @@ -252,11 +253,11 @@ def save_fs(self, writer: EndianBinaryWriter, data_flag: int, block_info_flag: i # data_flag # header: - # bundle_size (long) - # compressed_size (int) - # uncompressed_size (int) - # flag (int) - # ?padding? (bool) + # 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, @@ -266,21 +267,21 @@ def save_fs(self, writer: EndianBinaryWriter, data_flag: int, block_info_flag: i # *read compressed_size -> uncompressed_size # 0x10 offset # *read blocks infos of the data stream - # count (int) + # count (int) # ( - # uncompressed_size(uint) - # compressed_size (uint) - # flag(short) + # uncompressed_size (uint) + # compressed_size (uint) + # flag (short) # ) # *decompression via info.flag & 0x3F # *afterwards the file positions - # file_count (int) + # file_count (int) # ( # offset (long) - # size (long) - # flag (int) - # name (string_to_null) + # size (long) + # flag (int) + # name (string_to_null) # ) # file list & file data @@ -523,6 +524,14 @@ def decompress_data( 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())) + match = None + if version and version != "0.0.0": + match = reVersion.match(version) + if not match or len(match.groups()) < 3: + match = None + if not match: + match = reVersion.match(config.get_fallback_version()) + if not match or len(match.groups()) < 3: + raise UnityVersionFallbackError("Illegal fallback version format") + map_ = map(int, match.groups()) + return (next(map_), next(map_), next(map_)) diff --git a/UnityPy/files/File.py b/UnityPy/files/File.py index 34cd3f0d..896782f6 100644 --- a/UnityPy/files/File.py +++ b/UnityPy/files/File.py @@ -13,7 +13,7 @@ DirectoryInfo = namedtuple("DirectoryInfo", "path offset size") -class File(object): +class File: name: str files: Dict[str, File] environment: Environment @@ -34,7 +34,7 @@ def __init__( self.is_changed = False self.cab_file = "CAB-UnityPy_Mod.resS" self.parent = parent - self.environment = self.environment = ( + self.environment = ( getattr(parent, "environment", parent) if parent else None ) self.name = basename(name) if isinstance(name, str) else "" @@ -95,10 +95,10 @@ def read_files(self, reader: EndianBinaryReader, files: list): f.flags = getattr(node, "flags", 0) self.files[name] = f - def get_writeable_cab(self, name: str = None): + def get_writeable_cab(self, name: Optional[str] = None): """ Creates a new cab file in the bundle that contains the given data. - This is usefull for asset types that use resource files. + This is useful for asset types that use resource files. """ if not name: diff --git a/UnityPy/files/ObjectReader.py b/UnityPy/files/ObjectReader.py index 10b37813..39063b99 100644 --- a/UnityPy/files/ObjectReader.py +++ b/UnityPy/files/ObjectReader.py @@ -152,9 +152,11 @@ def write( writer.write_u_short(self.class_id) if header.version < 11: + assert self.is_destroyed is not None writer.write_u_short(self.is_destroyed) if 11 <= header.version < 17: + assert self.serialized_type is not None writer.write_short(self.serialized_type.script_type_index) if header.version == 15 or header.version == 16: @@ -254,7 +256,7 @@ def save_typetree( self, tree: dict, nodes: Optional[Union[TypeTreeNode, List[dict[str, Union[str, int]]]]] = None, - writer: EndianBinaryWriter = None, + writer: Optional[EndianBinaryWriter] = None, ): node = self._get_typetree_node(nodes) if not writer: diff --git a/UnityPy/files/SerializedFile.py b/UnityPy/files/SerializedFile.py index 7ee9fc93..366dbeb5 100644 --- a/UnityPy/files/SerializedFile.py +++ b/UnityPy/files/SerializedFile.py @@ -2,7 +2,7 @@ import re from ntpath import basename -from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Dict, Generator, Iterator, List, Optional, Tuple from attrs import define @@ -24,7 +24,7 @@ class SerializedFileHeader: file_size: int version: int data_offset: int - endian: bytes + endian: str reserved: bytes def __init__(self, reader: EndianBinaryReader): @@ -83,8 +83,13 @@ def __init__(self, header: SerializedFileHeader, reader: EndianBinaryReader): def write(self, header: SerializedFileHeader, writer: EndianBinaryWriter): if header.version >= 6: + assert self.temp_empty is not None writer.write_string_to_null(self.temp_empty) if header.version >= 5: + assert ( + self.guid is not None + and self.type is not None + ) writer.write_bytes(self.guid) writer.write_int(self.type) writer.write_string_to_null(self.path) @@ -110,7 +115,7 @@ def IsPatch(self): class SerializedType: class_id: int is_stripped_type: Optional[bool] = None - script_type_index: Optional[int] = -1 + script_type_index: int = -1 script_id: Optional[bytes] = None # Hash128 old_type_hash: Optional[bytes] = None # Hash128 node: Optional[TypeTreeNode] = None @@ -119,7 +124,7 @@ class SerializedType: m_NameSpace: Optional[str] = None m_AssemblyName: Optional[str] = None # 21+ - type_dependencies: Optional[List[int]] = None + type_dependencies: Optional[Tuple[int, ...]] = None def __init__( self, @@ -170,9 +175,11 @@ def write( writer.write_int(self.class_id) if version >= 16: + assert self.is_stripped_type is not None writer.write_boolean(self.is_stripped_type) if version >= 17: + assert self.script_type_index is not None writer.write_short(self.script_type_index) if version >= 13: @@ -181,7 +188,9 @@ def write( or (version < 16 and self.class_id < 0) or (version >= 16 and self.class_id == 114) ): + assert self.script_id is not None writer.write_bytes(self.script_id) # Hash128 + assert self.old_type_hash is not None writer.write_bytes(self.old_type_hash) # Hash128 if serialized_file._enable_type_tree: @@ -193,14 +202,20 @@ def write( if version >= 21: if is_ref_type: + assert ( + self.m_ClassName is not None + and self.m_NameSpace is not None + and self.m_AssemblyName is not None + ) writer.write_string_to_null(self.m_ClassName) writer.write_string_to_null(self.m_NameSpace) writer.write_string_to_null(self.m_AssemblyName) else: + assert self.type_dependencies is not None writer.write_int_array(self.type_dependencies, True) @property - def nodes(self) -> Union[TypeTreeNode, None]: + def nodes(self) -> Optional[TypeTreeNode]: # for compatibility with old versions return self.node @@ -221,8 +236,7 @@ class SerializedFile(File.File): _m_target_platform: int big_id_enabled: int userInformation: Optional[str] - assetbundle: AssetBundle - container: ContainerHelper + assetbundle: Optional[AssetBundle] _cache: Dict[str, Object] @property @@ -399,7 +413,7 @@ class FileIdentifierFake: return cab - def save(self, packer: str = None) -> bytes: + def save(self, packer: Optional[str] = None) -> bytes: # 1. header -> has to be delayed until the very end # 2. data -> types, objects, scripts, ... @@ -443,11 +457,13 @@ def save(self, packer: str = None) -> bytes: external.write(header, meta_writer) if header.version >= 20: + assert self.ref_types is not None meta_writer.write_int(len(self.ref_types)) for ref_type in self.ref_types: ref_type.write(self, meta_writer, True) if header.version >= 5: + assert self.userInformation is not None meta_writer.write_string_to_null(self.userInformation) # prepare header @@ -550,7 +566,7 @@ def __setitem__(self, key, value) -> None: def __delitem__(self, key) -> None: raise NotImplementedError("Deleting from the container is not allowed!") - def __iter__(self) -> Generator[str, None, None]: + def __iter__(self) -> Iterator[str]: return iter(self.keys()) def __len__(self) -> int: @@ -559,9 +575,6 @@ def __len__(self) -> int: def __getattr__(self, name: str) -> PPtr[Object]: return self.container_dict[name] - def __or__(self, other: ContainerHelper): - return ContainerHelper(list(set(self.container + other.container))) - def __str__(self) -> str: return f'{{{", ".join(f"{key}: {value}" for key, value in self.items())}}}' diff --git a/UnityPy/files/WebFile.py b/UnityPy/files/WebFile.py index da7946aa..1ee07aaf 100644 --- a/UnityPy/files/WebFile.py +++ b/UnityPy/files/WebFile.py @@ -1,4 +1,6 @@ -from . import File +from typing import Optional + +from . import File from ..helpers import CompressionHelper from ..streams import EndianBinaryReader, EndianBinaryWriter @@ -55,7 +57,7 @@ def __init__(self, reader: EndianBinaryReader, parent: File, name=None, **kwargs def save( self, - files: dict = None, + files: Optional[dict] = None, packer: str = "none", signature: str = "UnityWebData1.0", ) -> bytes: diff --git a/UnityPy/helpers/Tpk.py b/UnityPy/helpers/Tpk.py index 3b7dad02..94cc5a8c 100644 --- a/UnityPy/helpers/Tpk.py +++ b/UnityPy/helpers/Tpk.py @@ -475,7 +475,7 @@ def Count(self) -> int: class TpkCommonString: __slots__ = ("VersionInformation", "StringBufferIndices") VersionInformation: List[Tuple[UnityVersion, int]] - StringBufferIndices: Tuple[int] + StringBufferIndices: Tuple[int, ...] def __init__(self, stream: BytesIO) -> None: (versionCount,) = INT32.unpack(stream.read(INT32.size)) diff --git a/UnityPy/streams/EndianBinaryReader.py b/UnityPy/streams/EndianBinaryReader.py index 8cfeea9a..82dd6855 100644 --- a/UnityPy/streams/EndianBinaryReader.py +++ b/UnityPy/streams/EndianBinaryReader.py @@ -208,39 +208,39 @@ def read_array_struct(self, param: str, length: Optional[int] = None) -> tuple: struct = Struct(f"{self.endian}{length}{param}") return struct.unpack(self.read(struct.size)) - def read_boolean_array(self, length: Optional[int] = None) -> Tuple[bool]: + def read_boolean_array(self, length: Optional[int] = None) -> Tuple[bool, ...]: return self.read_array_struct("?", length) - def read_u_byte_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_u_byte_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("B", length) - def read_u_short_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_u_short_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("h", length) - def read_short_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_short_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("H", length) - def read_int_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_int_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("i", length) - def read_u_int_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_u_int_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("I", length) - def read_long_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_long_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("q", length) - def read_u_long_array(self, length: Optional[int] = None) -> Tuple[int]: + def read_u_long_array(self, length: Optional[int] = None) -> Tuple[int, ...]: return self.read_array_struct("Q", length) - def read_u_int_array_array(self, length: Optional[int] = None) -> List[Tuple[int]]: + def read_u_int_array_array(self, length: Optional[int] = None) -> List[Tuple[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: Optional[int] = None) -> Tuple[float]: + def read_float_array(self, length: Optional[int] = None) -> Tuple[float, ...]: return self.read_array_struct("f", length) - def read_double_array(self, length: Optional[int] = None) -> Tuple[float]: + def read_double_array(self, length: Optional[int] = None) -> Tuple[float, ...]: return self.read_array_struct("d", length) def read_string_array(self) -> List[str]: diff --git a/UnityPy/streams/EndianBinaryWriter.py b/UnityPy/streams/EndianBinaryWriter.py index 0f2ea8cc..932dc8f3 100644 --- a/UnityPy/streams/EndianBinaryWriter.py +++ b/UnityPy/streams/EndianBinaryWriter.py @@ -2,10 +2,12 @@ from io import BytesIO, IOBase import builtins -from typing import Callable, Union +from typing import Callable, Sequence, TypeVar, Union from ..math import Color, Matrix4x4, Quaternion, Vector2, Vector3, Vector4, Rectangle +T = TypeVar("T") + class EndianBinaryWriter: endian: str @@ -144,7 +146,7 @@ def write_matrix(self, value: Matrix4x4): for val in value.M: self.write_float(val) - def write_array(self, command: Callable, value: list, write_length: bool = True): + def write_array(self, command: Callable[[T], None], value: Sequence[T], write_length: bool = True): if write_length: self.write_int(len(value)) for val in value: @@ -154,29 +156,29 @@ def write_byte_array(self, value: builtins.bytes): self.write_int(len(value)) self.write(value) - def write_boolean_array(self, value: list): + def write_boolean_array(self, value: Sequence[bool]): self.write_array(self.write_boolean, value) - def write_u_short_array(self, value: list): + def write_u_short_array(self, value: Sequence[int]): self.write_array(self.write_u_short, value) - def write_int_array(self, value: list, write_length: bool = False): + def write_int_array(self, value: Sequence[int], write_length: bool = False): return self.write_array(self.write_int, value, write_length) - def write_u_int_array(self, value: list, write_length: bool = False): + def write_u_int_array(self, value: Sequence[int], write_length: bool = False): return self.write_array(self.write_u_int, value, write_length) - def write_float_array(self, value: list, write_length: bool = False): + def write_float_array(self, value: Sequence[float], write_length: bool = False): return self.write_array(self.write_float, value, write_length) - def write_string_array(self, value: list): + def write_string_array(self, value: Sequence[str]): self.write_array(self.write_aligned_string, value) - def write_vector2_array(self, value: list): + def write_vector2_array(self, value: Sequence[Vector2]): self.write_array(self.write_vector2, value) - def write_vector4_array(self, value: list): + def write_vector4_array(self, value: Sequence[Vector4]): self.write_array(self.write_vector4, value) - def write_matrix_array(self, value: list): + def write_matrix_array(self, value: Sequence[Matrix4x4]): self.write_array(self.write_matrix, value)