K0lb3 / UnityPy

UnityPy is python module that makes it possible to extract/unpack and edit Unity assets
MIT License
810 stars 123 forks source link

SerializedType - fix `attrs` accidentally removing default class attributes #272

Closed mos9527 closed 1 day ago

mos9527 commented 5 days ago

When a bundle file doesn't contain its typetree (i.e. SerializedFile._enable_type_tree is False), the node attribute in SerializedType will never be written. Which would crash UnityPy.

This behavior is identical with previous versions of UnityPy as the default (None) would be used. However, since attrs.define is used whlist having a custom __init__ method on the class - these attributes will be removed unless written otherwise.

This crashes the ObjectReader as the node attribute is always expected to be present.


The patch invokes __attrs_init__ in SerializedType's cutsom init function (as per the docs) so that the default values are correctly preserved. Removing the decorator would also fix this issue.

BTW it seems like most of the classes have custom init while being decorated by @define too in SerializedFile.py. Is having the decorators there in the first place necessary at all?


The attached bundle file would reproduce the issue by simply reading all the objects as of commit 6350e2ec327334c8a9b7f494f344a761.

6350e2ec327334c8a9b7f494f344a761.zip

In [1]: import UnityPy

In [2]: env = UnityPy.load('6350e2ec327334c8a9b7f494f344a761')

In [3]: for obj in env.objects: obj.read()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 1
----> 1 for obj in env.objects: obj.read()

File ~\UnityPy\UnityPy\files\ObjectReader.py:174, in ObjectReader.read(self)
    173 def read(self) -> T:
--> 174     obj = self.read_typetree(wrap=True)
    175     self._read_until = self.reader.Position
    176     return obj

File ~\UnityPy\UnityPy\files\ObjectReader.py:208, in ObjectReader.read_typetree(self, nodes, wrap)
    202 def read_typetree(
    203     self,
    204     nodes: Optional[Union[TypeTreeNode, List[dict]]] = None,
    205     wrap: bool = False,
    206 ) -> Union[dict, T]:
    207     self.reset()
--> 208     node = self._get_typetree_node(nodes)
    209     ret = TypeTreeHelper.read_typetree(
    210         node,
    211         self.reader,
   (...)
    214         expected_read=self.byte_size,
    215     )
    216     if wrap:

File ~\UnityPy\UnityPy\files\ObjectReader.py:252, in ObjectReader._get_typetree_node(self, node)
    249     raise ValueError("nodes must be a list[dict] or TypeTreeNode")
    251 if self.serialized_type:
--> 252     node = self.serialized_type.node
    253 if not node:
    254     node = get_typetree_node(self.class_id, self.version)

AttributeError: 'SerializedType' object has no attribute 'node'
K0lb3 commented 1 day ago

Thanks a lot for the very throughout PR and explanation.

I already have another solution in mind, and basically already implemented in a local branch featuring the next bigger update reworking the "files" structure overall. One of the related changes is moving away from doing any processing in __init__ and instead having from_reader class methods. This will then also finally allow easily creating instance of the affected classes without hacks.

I was considering if I should just a part of that already to fix the bug, closing your PR in turn, or keeping that off for a complete update PR, and merging your PR. As you did a very good PR I feel more like the later due to your effort.

Is having the decorators there in the first place necessary at all?

The decorators are mainly there for making the classes use __slots__ without having to define the field oneself. The classes also receive some other goodies due to the decorator, like nice __repr__ and a working __eq__.