unity_asset_binary/
object.rs

1//! Unity object representation and helpers.
2
3use crate::asset::{ObjectInfo, SerializedFile};
4use crate::error::{BinaryError, Result};
5use crate::reader::{BinaryReader, ByteOrder};
6use crate::shared_bytes::SharedBytes;
7use crate::typetree::{
8    PPtrScanResult, TypeTree, TypeTreeParseMode, TypeTreeParseOptions, TypeTreeParseOutput,
9    TypeTreeParseWarning, TypeTreeSerializer,
10};
11use crate::unity_objects::{GameObject, Transform};
12use std::sync::Arc;
13use unity_asset_core::{UnityClass, UnityValue};
14
15/// A lightweight reference to a binary object within a [`SerializedFile`].
16///
17/// This is conceptually similar to UnityPy's `ObjectReader`: it carries just enough context
18/// (file + object metadata) to parse the object on-demand.
19#[derive(Debug, Clone, Copy)]
20pub struct ObjectHandle<'a> {
21    file: &'a SerializedFile,
22    info: &'a ObjectInfo,
23}
24
25impl<'a> ObjectHandle<'a> {
26    pub fn new(file: &'a SerializedFile, info: &'a ObjectInfo) -> Self {
27        Self { file, info }
28    }
29
30    pub fn file(&self) -> &'a SerializedFile {
31        self.file
32    }
33
34    pub fn info(&self) -> &'a ObjectInfo {
35        self.info
36    }
37
38    pub fn path_id(&self) -> i64 {
39        self.info.path_id
40    }
41
42    pub fn class_id(&self) -> i32 {
43        self.info.type_id
44    }
45
46    pub fn byte_start(&self) -> u64 {
47        self.info.byte_start
48    }
49
50    pub fn byte_size(&self) -> u32 {
51        self.info.byte_size
52    }
53
54    /// Get the raw bytes for this object (preloaded if available, otherwise sliced from the file).
55    pub fn raw_data(&self) -> Result<&'a [u8]> {
56        if !self.info.data.is_empty() {
57            return Ok(self.info.data.as_slice());
58        }
59        self.file.object_bytes(self.info)
60    }
61
62    /// Parse this object into an owned [`UnityObject`] (best-effort).
63    pub fn read(&self) -> Result<UnityObject> {
64        UnityObject::from_serialized_file(self.file, self.info)
65    }
66
67    pub fn read_with_options(&self, options: TypeTreeParseOptions) -> Result<UnityObject> {
68        UnityObject::from_serialized_file_with_options(self.file, self.info, options)
69    }
70
71    /// Peek the object's name (`m_Name`/`name`) without parsing the full TypeTree.
72    ///
73    /// This mirrors UnityPy's `ObjectReader.peek_name()` behavior by parsing only a prefix of the
74    /// root TypeTree until the name field, when possible.
75    pub fn peek_name(&self) -> Result<Option<String>> {
76        self.peek_name_with_options(TypeTreeParseOptions {
77            mode: TypeTreeParseMode::Lenient,
78        })
79    }
80
81    pub fn peek_name_with_options(&self, options: TypeTreeParseOptions) -> Result<Option<String>> {
82        let Some(tree) = type_tree_for_object(self.file, self.info) else {
83            return Ok(None);
84        };
85        let tree = tree.as_ref();
86        let Some((prefix_len, field)) = tree.name_peek_prefix() else {
87            return Ok(None);
88        };
89
90        let bytes = self.raw_data()?;
91        let mut reader = BinaryReader::new(bytes, self.file.header.byte_order());
92        let serializer = TypeTreeSerializer::new(tree);
93        let out = serializer.parse_object_prefix_detailed(&mut reader, options, prefix_len)?;
94
95        match out.properties.get(&field) {
96            Some(UnityValue::String(s)) => Ok(Some(s.clone())),
97            _ => Ok(None),
98        }
99    }
100
101    /// Scan TypeTree-based object bytes and collect `PPtr` references (`fileID`, `pathID`) without
102    /// allocating a full parsed `UnityValue` tree.
103    pub fn scan_pptrs(&self) -> Result<Option<PPtrScanResult>> {
104        let Some(tree) = type_tree_for_object(self.file, self.info) else {
105            return Ok(None);
106        };
107        let tree = tree.as_ref();
108        if tree.is_empty() {
109            return Ok(None);
110        }
111
112        let bytes = self.raw_data()?;
113        let mut reader = BinaryReader::new(bytes, self.file.header.byte_order());
114        let serializer = TypeTreeSerializer::new(tree);
115        if self.file.ref_types.is_empty() {
116            Ok(Some(serializer.scan_pptrs(&mut reader)?))
117        } else {
118            Ok(Some(serializer.scan_pptrs_with_ref_types(
119                &mut reader,
120                Some(&self.file.ref_types),
121            )?))
122        }
123    }
124}
125
126#[derive(Debug, Clone)]
127enum ObjectBytes {
128    Empty,
129    Inline(Vec<u8>),
130    Shared {
131        data: SharedBytes,
132        start: usize,
133        end: usize,
134    },
135}
136
137const RAW_DATA_INLINE_LIMIT: usize = 4 * 1024;
138const RAW_DATA_PREVIEW_LEN: usize = 256;
139
140impl ObjectBytes {
141    fn as_slice(&self) -> &[u8] {
142        match self {
143            ObjectBytes::Empty => &[],
144            ObjectBytes::Inline(bytes) => bytes.as_slice(),
145            ObjectBytes::Shared { data, start, end } => &data.as_bytes()[*start..*end],
146        }
147    }
148}
149
150/// A parsed Unity object.
151///
152/// This is an owned wrapper which carries:
153/// - the raw `ObjectInfo` (from `asset` module)
154/// - the parsed `UnityClass` properties (best-effort)
155#[derive(Debug, Clone)]
156pub struct UnityObject {
157    pub info: ObjectInfo,
158    pub class: UnityClass,
159    byte_order: ByteOrder,
160    raw: ObjectBytes,
161    typetree_warnings: Vec<TypeTreeParseWarning>,
162}
163
164impl UnityObject {
165    /// Create a UnityObject from an already-parsed UnityClass (used by tests and higher-level code).
166    pub fn from_info_and_class(info: ObjectInfo, class: UnityClass) -> Self {
167        Self {
168            byte_order: ByteOrder::Little,
169            info,
170            class,
171            raw: ObjectBytes::Empty,
172            typetree_warnings: Vec::new(),
173        }
174    }
175
176    /// Create a UnityObject from raw bytes without TypeTree information.
177    ///
178    /// For large objects, this intentionally avoids expanding all bytes into a `UnityValue::Array`
179    /// to reduce memory pressure and parsing time; use `raw_data()` instead.
180    pub fn from_raw(class_id: i32, path_id: i64, data: Vec<u8>) -> Self {
181        let info = ObjectInfo::new(path_id, 0, data.len() as u32, class_id, -1);
182        let raw = ObjectBytes::Inline(data);
183        let mut class =
184            UnityClass::new(class_id, class_name_from_id(class_id), path_id.to_string());
185        let bytes = raw.as_slice();
186        class.set(
187            "_raw_data_len".to_string(),
188            UnityValue::Integer(bytes.len() as i64),
189        );
190        if bytes.len() <= RAW_DATA_INLINE_LIMIT {
191            class.set(
192                "_raw_data".to_string(),
193                UnityValue::Array(
194                    bytes
195                        .iter()
196                        .copied()
197                        .map(|b| UnityValue::Integer(b as i64))
198                        .collect(),
199                ),
200            );
201        } else {
202            class.set("_raw_data_truncated".to_string(), UnityValue::Bool(true));
203            let preview = bytes
204                .iter()
205                .take(RAW_DATA_PREVIEW_LEN)
206                .copied()
207                .map(|b| UnityValue::Integer(b as i64))
208                .collect();
209            class.set("_raw_data_preview".to_string(), UnityValue::Array(preview));
210        }
211        Self {
212            info,
213            class,
214            byte_order: ByteOrder::Little,
215            raw,
216            typetree_warnings: Vec::new(),
217        }
218    }
219
220    /// Create a UnityObject from a SerializedFile + ObjectInfo, using TypeTree when available.
221    pub fn from_serialized_file(file: &SerializedFile, info: &ObjectInfo) -> Result<Self> {
222        Self::from_serialized_file_with_options(file, info, TypeTreeParseOptions::default())
223    }
224
225    pub fn from_serialized_file_with_options(
226        file: &SerializedFile,
227        info: &ObjectInfo,
228        options: TypeTreeParseOptions,
229    ) -> Result<Self> {
230        let class_id = info.type_id;
231        let type_tree = type_tree_for_object(file, info);
232        let byte_order = file.header.byte_order();
233        let (start, end) = object_range(file, info)?;
234        let base = file.data_base_offset();
235        let raw = ObjectBytes::Shared {
236            data: file.data_shared(),
237            start: base + start,
238            end: base + end,
239        };
240
241        let mut class = UnityClass::new(
242            class_id,
243            class_name_from_id(class_id),
244            info.path_id.to_string(),
245        );
246
247        let mut warnings: Vec<TypeTreeParseWarning> = Vec::new();
248
249        if let Some(tree) = type_tree {
250            let tree = tree.as_ref();
251            match parse_object_data(file, info, byte_order, tree, options) {
252                Ok(out) => {
253                    class.update_properties(out.properties);
254                    warnings = out.warnings;
255                }
256                Err(e) => match options.mode {
257                    TypeTreeParseMode::Strict => return Err(e),
258                    TypeTreeParseMode::Lenient => {
259                        warnings.push(TypeTreeParseWarning {
260                            field: "<root>".to_string(),
261                            error: e.to_string(),
262                        });
263                        apply_raw_preview(&mut class, raw.as_slice());
264                    }
265                },
266            }
267        } else {
268            apply_raw_preview(&mut class, raw.as_slice());
269        }
270
271        Ok(Self {
272            info: {
273                let mut cloned = info.clone();
274                cloned.data.clear();
275                cloned
276            },
277            class,
278            byte_order,
279            raw,
280            typetree_warnings: warnings,
281        })
282    }
283
284    pub fn path_id(&self) -> i64 {
285        self.info.path_id
286    }
287
288    pub fn class_id(&self) -> i32 {
289        self.info.type_id
290    }
291
292    pub fn class_name(&self) -> &str {
293        &self.class.class_name
294    }
295
296    pub fn name(&self) -> Option<String> {
297        self.class.get("m_Name").and_then(|v| match v {
298            UnityValue::String(s) => Some(s.clone()),
299            _ => None,
300        })
301    }
302
303    pub fn get(&self, key: &str) -> Option<&UnityValue> {
304        self.class.get(key)
305    }
306
307    pub fn set(&mut self, key: String, value: UnityValue) {
308        self.class.set(key, value);
309    }
310
311    pub fn has_property(&self, key: &str) -> bool {
312        self.class.has_property(key)
313    }
314
315    pub fn property_names(&self) -> Vec<&String> {
316        self.class.properties().keys().collect()
317    }
318
319    pub fn as_unity_class(&self) -> &UnityClass {
320        &self.class
321    }
322
323    pub fn as_unity_class_mut(&mut self) -> &mut UnityClass {
324        &mut self.class
325    }
326
327    pub fn as_gameobject(&self) -> Result<GameObject> {
328        if self.class_id() != 1 {
329            return Err(BinaryError::invalid_data(format!(
330                "Object is not a GameObject (class_id: {})",
331                self.class_id()
332            )));
333        }
334        GameObject::from_typetree(self.class.properties())
335    }
336
337    pub fn as_transform(&self) -> Result<Transform> {
338        if self.class_id() != 4 {
339            return Err(BinaryError::invalid_data(format!(
340                "Object is not a Transform (class_id: {})",
341                self.class_id()
342            )));
343        }
344        Transform::from_typetree(self.class.properties())
345    }
346
347    pub fn is_gameobject(&self) -> bool {
348        self.class_id() == 1
349    }
350
351    pub fn is_transform(&self) -> bool {
352        self.class_id() == 4
353    }
354
355    pub fn describe(&self) -> String {
356        let name = self.name().unwrap_or_else(|| "<unnamed>".to_string());
357        format!(
358            "{} '{}' (ID:{}, PathID:{})",
359            self.class_name(),
360            name,
361            self.class_id(),
362            self.path_id()
363        )
364    }
365
366    pub fn raw_data(&self) -> &[u8] {
367        self.raw.as_slice()
368    }
369
370    pub fn typetree_warnings(&self) -> &[TypeTreeParseWarning] {
371        &self.typetree_warnings
372    }
373
374    pub fn byte_size(&self) -> u32 {
375        self.info.byte_size
376    }
377
378    pub fn byte_start(&self) -> u64 {
379        self.info.byte_start
380    }
381
382    pub fn byte_order(&self) -> ByteOrder {
383        self.byte_order
384    }
385}
386
387fn class_name_from_id(class_id: i32) -> String {
388    unity_asset_core::get_class_name(class_id).unwrap_or_else(|| format!("Class_{}", class_id))
389}
390
391enum TypeTreeSource<'a> {
392    Borrowed(&'a TypeTree),
393    Shared(Arc<TypeTree>),
394}
395
396impl TypeTreeSource<'_> {
397    fn as_ref(&self) -> &TypeTree {
398        match self {
399            Self::Borrowed(t) => t,
400            Self::Shared(t) => t.as_ref(),
401        }
402    }
403}
404
405fn type_tree_for_object<'a>(
406    file: &'a SerializedFile,
407    info: &ObjectInfo,
408) -> Option<TypeTreeSource<'a>> {
409    fn from_internal<'a>(file: &'a SerializedFile, info: &ObjectInfo) -> Option<&'a TypeTree> {
410        if info.type_index >= 0 {
411            return file
412                .types
413                .get(info.type_index as usize)
414                .map(|t| &t.type_tree);
415        }
416        file.types
417            .iter()
418            .find(|t| t.class_id == info.type_id)
419            .map(|t| &t.type_tree)
420    }
421
422    if file.enable_type_tree
423        && let Some(tree) = from_internal(file, info)
424        && !tree.is_empty()
425    {
426        return Some(TypeTreeSource::Borrowed(tree));
427    }
428
429    // Best-effort fallback: stripped files can supply a registry externally.
430    // We also allow this fallback even when `enable_type_tree = true` but the internal entry is missing/empty.
431    file.type_tree_registry
432        .as_ref()
433        .and_then(|r| r.resolve(&file.unity_version, info.type_id))
434        .map(TypeTreeSource::Shared)
435}
436
437fn object_bytes<'a>(file: &'a SerializedFile, info: &'a ObjectInfo) -> Result<&'a [u8]> {
438    if !info.data.is_empty() {
439        return Ok(&info.data);
440    }
441    file.object_bytes(info)
442}
443
444fn object_range(file: &SerializedFile, info: &ObjectInfo) -> Result<(usize, usize)> {
445    let start: usize = info.byte_start.try_into().map_err(|_| {
446        BinaryError::invalid_data(format!("Object byte_start overflow: {}", info.byte_start))
447    })?;
448    let end = start.saturating_add(info.byte_size as usize);
449    if end > file.data().len() {
450        return Err(BinaryError::invalid_data(format!(
451            "Object data out of bounds (path_id={}, start={}, size={}, file_len={})",
452            info.path_id,
453            start,
454            info.byte_size,
455            file.data().len()
456        )));
457    }
458    Ok((start, end))
459}
460
461fn parse_object_data(
462    file: &SerializedFile,
463    info: &ObjectInfo,
464    byte_order: ByteOrder,
465    tree: &TypeTree,
466    options: TypeTreeParseOptions,
467) -> Result<TypeTreeParseOutput> {
468    let bytes = object_bytes(file, info)?;
469    let mut reader = BinaryReader::new(bytes, byte_order);
470    let serializer = TypeTreeSerializer::new(tree);
471    if file.ref_types.is_empty() {
472        serializer.parse_object_detailed(&mut reader, options)
473    } else {
474        serializer.parse_object_detailed_with_ref_types(&mut reader, options, &file.ref_types)
475    }
476}
477
478fn apply_raw_preview(class: &mut UnityClass, bytes: &[u8]) {
479    class.set(
480        "_raw_data_len".to_string(),
481        UnityValue::Integer(bytes.len() as i64),
482    );
483    if bytes.len() <= RAW_DATA_INLINE_LIMIT {
484        class.set("_raw_data".to_string(), UnityValue::Bytes(bytes.to_vec()));
485    } else {
486        class.set("_raw_data_truncated".to_string(), UnityValue::Bool(true));
487        let preview_len = bytes.len().min(RAW_DATA_PREVIEW_LEN);
488        class.set(
489            "_raw_data_preview".to_string(),
490            UnityValue::Bytes(bytes[..preview_len].to_vec()),
491        );
492    }
493}