unity_asset_binary/asset/
types.rs

1//! Asset type definitions
2//!
3//! This module defines the core data structures for Unity asset processing.
4
5use crate::error::{BinaryError, Result};
6use crate::reader::BinaryReader;
7use crate::typetree::{TypeTree, TypeTreeParser};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Type information for Unity objects
12///
13/// Contains metadata about Unity object types including class information,
14/// type trees, and script references.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SerializedType {
17    /// Unity class ID
18    pub class_id: i32,
19    /// Whether this type is stripped
20    pub is_stripped_type: bool,
21    /// Script type index (for MonoBehaviour / ref types); `-1` means not a script type.
22    pub script_type_index: i16,
23    /// Type tree for this type
24    pub type_tree: TypeTree,
25    /// Script ID hash
26    pub script_id: [u8; 16],
27    /// Old type hash
28    pub old_type_hash: [u8; 16],
29    /// Type dependencies
30    pub type_dependencies: Vec<i32>,
31    /// Class name
32    pub class_name: String,
33    /// Namespace
34    pub namespace: String,
35    /// Assembly name
36    pub assembly_name: String,
37}
38
39impl SerializedType {
40    /// Create a new SerializedType
41    pub fn new(class_id: i32) -> Self {
42        Self {
43            class_id,
44            is_stripped_type: false,
45            script_type_index: -1,
46            type_tree: TypeTree::new(),
47            script_id: [0; 16],
48            old_type_hash: [0; 16],
49            type_dependencies: Vec::new(),
50            class_name: String::new(),
51            namespace: String::new(),
52            assembly_name: String::new(),
53        }
54    }
55
56    /// Parse SerializedType from binary data
57    pub fn from_reader(
58        reader: &mut BinaryReader,
59        version: u32,
60        enable_type_tree: bool,
61        is_ref_type: bool,
62    ) -> Result<Self> {
63        let class_id = reader.read_i32()?;
64        let mut serialized_type = Self::new(class_id);
65
66        if version >= 16 {
67            serialized_type.is_stripped_type = reader.read_bool()?;
68        }
69
70        if version >= 17 {
71            serialized_type.script_type_index = reader.read_i16()?;
72        }
73
74        if version >= 13 {
75            // Based on UnityPy logic.
76            let should_read_script_id = (is_ref_type && serialized_type.script_type_index >= 0)
77                || (version < 16 && class_id < 0)
78                || (version >= 16 && class_id == 114); // MonoBehaviour
79
80            if should_read_script_id {
81                // Read script ID
82                let script_id_bytes = reader.read_bytes(16)?;
83                serialized_type.script_id.copy_from_slice(&script_id_bytes);
84            }
85
86            // Always read old type hash for version >= 13
87            let old_type_hash_bytes = reader.read_bytes(16)?;
88            serialized_type
89                .old_type_hash
90                .copy_from_slice(&old_type_hash_bytes);
91        }
92
93        if enable_type_tree {
94            // Use blob format for version >= 12 or version == 10 (like unity-rs)
95            if version >= 12 || version == 10 {
96                serialized_type.type_tree = TypeTreeParser::from_reader_blob(reader, version)?;
97            } else {
98                serialized_type.type_tree = TypeTreeParser::from_reader(reader, version)?;
99            }
100
101            if version >= 21 {
102                if is_ref_type {
103                    serialized_type.class_name = reader.read_cstring()?;
104                    serialized_type.namespace = reader.read_cstring()?;
105                    serialized_type.assembly_name = reader.read_cstring()?;
106                } else {
107                    serialized_type.type_dependencies = read_i32_array(reader)?;
108                }
109            }
110        }
111
112        Ok(serialized_type)
113    }
114
115    /// Check if this is a script type (MonoBehaviour)
116    pub fn is_script_type(&self) -> bool {
117        self.class_id == 114 || self.script_type_index >= 0
118    }
119
120    /// Check if this type has a TypeTree
121    pub fn has_type_tree(&self) -> bool {
122        !self.type_tree.is_empty()
123    }
124
125    /// Get the type name
126    pub fn type_name(&self) -> String {
127        if !self.class_name.is_empty() {
128            self.class_name.clone()
129        } else {
130            format!("Class_{}", self.class_id)
131        }
132    }
133
134    /// Get full type name including namespace
135    pub fn full_type_name(&self) -> String {
136        if !self.namespace.is_empty() {
137            format!("{}.{}", self.namespace, self.type_name())
138        } else {
139            self.type_name()
140        }
141    }
142
143    /// Validate the serialized type
144    pub fn validate(&self) -> Result<()> {
145        if self.class_id == 0 {
146            return Err(BinaryError::invalid_data("Class ID cannot be zero"));
147        }
148
149        if self.is_script_type() && self.script_id == [0; 16] {
150            return Err(BinaryError::invalid_data(
151                "Script type must have valid script ID",
152            ));
153        }
154
155        Ok(())
156    }
157}
158
159fn read_i32_array(reader: &mut BinaryReader) -> Result<Vec<i32>> {
160    let count = reader.read_i32()?;
161    if count < 0 {
162        return Err(BinaryError::invalid_data(format!(
163            "Negative array length: {}",
164            count
165        )));
166    }
167    let count = count as usize;
168    let mut values = Vec::with_capacity(count);
169    for _ in 0..count {
170        values.push(reader.read_i32()?);
171    }
172    Ok(values)
173}
174
175/// External reference to another Unity file
176///
177/// Represents a reference to an asset in another Unity file,
178/// used for cross-file asset dependencies.
179#[derive(Debug, Clone, Serialize, Deserialize, Default)]
180pub struct FileIdentifier {
181    /// Temporary empty string field for version >= 6.
182    pub temp_empty: String,
183    /// GUID of the referenced file
184    pub guid: [u8; 16],
185    /// Type of the reference
186    pub type_: i32,
187    /// Path to the referenced file
188    pub path: String,
189}
190
191impl FileIdentifier {
192    /// Parse FileIdentifier from binary data
193    pub fn from_reader(reader: &mut BinaryReader, version: u32) -> Result<Self> {
194        let temp_empty = if version >= 6 {
195            reader.read_cstring()?
196        } else {
197            String::new()
198        };
199
200        let mut guid = [0u8; 16];
201        let mut type_ = 0i32;
202
203        if version >= 5 {
204            let guid_bytes = reader.read_bytes(16)?;
205            guid.copy_from_slice(&guid_bytes);
206            type_ = reader.read_i32()?;
207        }
208
209        let path = reader.read_cstring()?;
210
211        Ok(Self {
212            temp_empty,
213            guid,
214            type_,
215            path,
216        })
217    }
218
219    /// Create a new FileIdentifier
220    pub fn new(guid: [u8; 16], type_: i32, path: String) -> Self {
221        Self {
222            temp_empty: String::new(),
223            guid,
224            type_,
225            path,
226        }
227    }
228
229    /// Check if this is a valid file identifier
230    pub fn is_valid(&self) -> bool {
231        self.guid != [0; 16] || !self.path.is_empty()
232    }
233
234    /// Get GUID as string
235    pub fn guid_string(&self) -> String {
236        format!(
237            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
238            self.guid[0],
239            self.guid[1],
240            self.guid[2],
241            self.guid[3],
242            self.guid[4],
243            self.guid[5],
244            self.guid[6],
245            self.guid[7],
246            self.guid[8],
247            self.guid[9],
248            self.guid[10],
249            self.guid[11],
250            self.guid[12],
251            self.guid[13],
252            self.guid[14],
253            self.guid[15]
254        )
255    }
256}
257
258/// Object information within a SerializedFile
259///
260/// Contains metadata about individual Unity objects including
261/// their location, type, and path ID.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct ObjectInfo {
264    /// Path ID of the object (unique within file)
265    pub path_id: i64,
266    /// Offset of object data in the file
267    pub byte_start: u64,
268    /// Size of object data
269    pub byte_size: u32,
270    /// Unity class ID of the object
271    pub type_id: i32,
272    /// Raw type ID from the object table (index into `types` for version >= 16, otherwise `-1`)
273    pub type_index: i32,
274    /// Object data
275    pub data: Vec<u8>,
276}
277
278impl ObjectInfo {
279    /// Create a new ObjectInfo
280    pub fn new(
281        path_id: i64,
282        byte_start: u64,
283        byte_size: u32,
284        type_id: i32,
285        type_index: i32,
286    ) -> Self {
287        Self {
288            path_id,
289            byte_start,
290            byte_size,
291            type_id,
292            type_index,
293            data: Vec::new(),
294        }
295    }
296
297    /// Check if object data is loaded
298    pub fn has_data(&self) -> bool {
299        !self.data.is_empty()
300    }
301
302    /// Get the end offset of this object
303    pub fn byte_end(&self) -> u64 {
304        self.byte_start + self.byte_size as u64
305    }
306
307    /// Validate object info
308    pub fn validate(&self) -> Result<()> {
309        if self.path_id == 0 {
310            return Err(BinaryError::invalid_data("Path ID cannot be zero"));
311        }
312
313        if self.byte_size == 0 {
314            return Err(BinaryError::invalid_data("Byte size cannot be zero"));
315        }
316
317        if self.type_id == 0 {
318            return Err(BinaryError::invalid_data("Type ID cannot be zero"));
319        }
320
321        Ok(())
322    }
323}
324
325/// Type registry for managing SerializedTypes
326///
327/// Provides efficient lookup and management of type information
328/// within a SerializedFile.
329#[derive(Debug, Clone, Default)]
330pub struct TypeRegistry {
331    types: HashMap<i32, SerializedType>,
332    script_types: HashMap<i16, SerializedType>,
333}
334
335impl TypeRegistry {
336    /// Create a new type registry
337    pub fn new() -> Self {
338        Self {
339            types: HashMap::new(),
340            script_types: HashMap::new(),
341        }
342    }
343
344    /// Add a type to the registry
345    pub fn add_type(&mut self, serialized_type: SerializedType) {
346        let class_id = serialized_type.class_id;
347
348        // Add to script types if applicable
349        if serialized_type.script_type_index >= 0 {
350            self.script_types
351                .insert(serialized_type.script_type_index, serialized_type.clone());
352        }
353
354        self.types.insert(class_id, serialized_type);
355    }
356
357    /// Get a type by class ID
358    pub fn get_type(&self, class_id: i32) -> Option<&SerializedType> {
359        self.types.get(&class_id)
360    }
361
362    /// Get a script type by index
363    pub fn get_script_type(&self, script_index: i16) -> Option<&SerializedType> {
364        self.script_types.get(&script_index)
365    }
366
367    /// Get all class IDs
368    pub fn class_ids(&self) -> Vec<i32> {
369        self.types.keys().copied().collect()
370    }
371
372    /// Get all script type indices
373    pub fn script_indices(&self) -> Vec<i16> {
374        self.script_types.keys().copied().collect()
375    }
376
377    /// Check if a class ID is registered
378    pub fn has_type(&self, class_id: i32) -> bool {
379        self.types.contains_key(&class_id)
380    }
381
382    /// Check if a script index is registered
383    pub fn has_script_type(&self, script_index: i16) -> bool {
384        self.script_types.contains_key(&script_index)
385    }
386
387    /// Get the number of registered types
388    pub fn len(&self) -> usize {
389        self.types.len()
390    }
391
392    /// Check if the registry is empty
393    pub fn is_empty(&self) -> bool {
394        self.types.is_empty()
395    }
396
397    /// Clear all types
398    pub fn clear(&mut self) {
399        self.types.clear();
400        self.script_types.clear();
401    }
402
403    /// Get types by predicate
404    pub fn find_types<F>(&self, predicate: F) -> Vec<&SerializedType>
405    where
406        F: Fn(&SerializedType) -> bool,
407    {
408        self.types.values().filter(|t| predicate(t)).collect()
409    }
410
411    /// Get all script types
412    pub fn script_types(&self) -> Vec<&SerializedType> {
413        self.script_types.values().collect()
414    }
415
416    /// Get all non-script types
417    pub fn non_script_types(&self) -> Vec<&SerializedType> {
418        self.types
419            .values()
420            .filter(|t| !t.is_script_type())
421            .collect()
422    }
423}
424
425/// Unity class ID constants (single source of truth: `unity-asset-core`)
426pub use unity_asset_core::class_ids;
427
428/// Script type reference (UnityPy: LocalSerializedObjectIdentifier)
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct LocalSerializedObjectIdentifier {
431    pub local_serialized_file_index: i32,
432    pub local_identifier_in_file: i64,
433}
434
435impl LocalSerializedObjectIdentifier {
436    pub fn from_reader(reader: &mut BinaryReader, version: u32) -> Result<Self> {
437        let local_serialized_file_index = reader.read_i32()?;
438        let local_identifier_in_file = if version < 14 {
439            reader.read_i32()? as i64
440        } else {
441            reader.align()?;
442            reader.read_i64()?
443        };
444        Ok(Self {
445            local_serialized_file_index,
446            local_identifier_in_file,
447        })
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_serialized_type_creation() {
457        let stype = SerializedType::new(114);
458        assert_eq!(stype.class_id, 114);
459        assert!(stype.is_script_type());
460    }
461
462    #[test]
463    fn test_file_identifier_guid() {
464        let guid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
465        let file_id = FileIdentifier::new(guid, 0, "test.unity".to_string());
466        let guid_str = file_id.guid_string();
467        assert!(guid_str.contains("01020304"));
468    }
469
470    #[test]
471    fn test_type_registry() {
472        let mut registry = TypeRegistry::new();
473        let stype = SerializedType::new(28); // Texture2D
474
475        registry.add_type(stype);
476        assert!(registry.has_type(28));
477        assert_eq!(registry.len(), 1);
478    }
479}