1use 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#[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 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 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 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 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#[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 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 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 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 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}