unity_asset/
environment.rs

1//! Environment for managing multiple Unity assets.
2//!
3//! This module hosts the high-level `Environment` API, which provides:
4//! - multi-source loading (bundles, serialized files, web files)
5//! - container discovery (`m_Container`) and object key resolution
6//! - streamed resource reads (`.resS` / `.resource`) with best-effort fallbacks
7//! - strict/lenient TypeTree parsing knobs + structured warnings
8
9mod imp {
10    use crate::{Result, YamlDocument};
11    use std::collections::HashMap;
12    use std::fmt;
13    use std::path::{Path, PathBuf};
14    use std::sync::{Arc, Mutex, RwLock};
15    use unity_asset_binary::asset::SerializedFile;
16    use unity_asset_binary::bundle::AssetBundle;
17    use unity_asset_binary::file::{UnityFile, load_unity_file, load_unity_file_from_shared_range};
18    use unity_asset_binary::object::{ObjectHandle, UnityObject};
19    use unity_asset_binary::typetree::TypeTreeRegistry;
20    use unity_asset_binary::typetree::{
21        TypeTreeParseMode, TypeTreeParseOptions, TypeTreeParseWarning,
22    };
23    use unity_asset_binary::webfile::WebFile;
24    use unity_asset_core::UnityValue;
25    use unity_asset_core::{UnityAssetError, UnityClass, UnityDocument};
26
27    mod container;
28    mod key;
29    mod loader;
30    mod object_query;
31    mod pptr;
32    mod stream;
33
34    #[derive(Debug, Clone)]
35    pub enum EnvironmentWarning {
36        LoadFailed {
37            path: PathBuf,
38            error: String,
39        },
40        YamlDocumentSkipped {
41            path: PathBuf,
42            doc_index: usize,
43            error: String,
44        },
45    }
46
47    impl fmt::Display for EnvironmentWarning {
48        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49            match self {
50                EnvironmentWarning::LoadFailed { path, error } => {
51                    write!(f, "Failed to load {}: {}", path.to_string_lossy(), error)
52                }
53                EnvironmentWarning::YamlDocumentSkipped {
54                    path,
55                    doc_index,
56                    error,
57                } => write!(
58                    f,
59                    "YAML warning in {} (doc {}): {}",
60                    path.to_string_lossy(),
61                    doc_index,
62                    error
63                ),
64            }
65        }
66    }
67
68    pub trait EnvironmentReporter: Send + Sync {
69        fn warn(&self, warning: &EnvironmentWarning);
70        fn typetree_warning(&self, _key: &BinaryObjectKey, _warning: &TypeTreeParseWarning) {}
71    }
72
73    #[derive(Debug, Default)]
74    pub struct NoopReporter;
75
76    impl EnvironmentReporter for NoopReporter {
77        fn warn(&self, _warning: &EnvironmentWarning) {}
78    }
79
80    #[derive(Debug, Clone, Copy)]
81    pub struct EnvironmentOptions {
82        pub typetree: TypeTreeParseOptions,
83    }
84
85    impl EnvironmentOptions {
86        pub fn strict() -> Self {
87            Self {
88                typetree: TypeTreeParseOptions {
89                    mode: TypeTreeParseMode::Strict,
90                },
91            }
92        }
93
94        pub fn lenient() -> Self {
95            Self {
96                typetree: TypeTreeParseOptions {
97                    mode: TypeTreeParseMode::Lenient,
98                },
99            }
100        }
101    }
102
103    impl Default for EnvironmentOptions {
104        fn default() -> Self {
105            Self::lenient()
106        }
107    }
108
109    #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
110    pub enum BinarySource {
111        Path(PathBuf),
112        WebEntry {
113            web_path: PathBuf,
114            entry_name: String,
115        },
116    }
117
118    impl fmt::Display for BinarySource {
119        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120            match self {
121                BinarySource::Path(p) => write!(f, "{}", p.to_string_lossy()),
122                BinarySource::WebEntry {
123                    web_path,
124                    entry_name,
125                } => write!(f, "{}::{}", web_path.to_string_lossy(), entry_name),
126            }
127        }
128    }
129
130    impl BinarySource {
131        pub fn path<P: AsRef<Path>>(path: P) -> Self {
132            Self::Path(path.as_ref().to_path_buf())
133        }
134
135        pub fn describe(&self) -> String {
136            self.to_string()
137        }
138
139        fn as_path(&self) -> Option<&PathBuf> {
140            match self {
141                BinarySource::Path(p) => Some(p),
142                BinarySource::WebEntry { .. } => None,
143            }
144        }
145    }
146
147    /// A reference to a binary object within a `SerializedFile`.
148    ///
149    /// This is conceptually similar to UnityPy's `ObjectReader`: it is a lightweight handle that can be
150    /// converted into a parsed `UnityObject` on-demand.
151    #[derive(Clone)]
152    pub struct BinaryObjectRef<'a> {
153        pub source: &'a BinarySource,
154        pub source_kind: BinarySourceKind,
155        /// Asset index within a bundle. `None` for standalone serialized files.
156        pub asset_index: Option<usize>,
157        pub object: ObjectHandle<'a>,
158        typetree_options: TypeTreeParseOptions,
159        reporter: Option<Arc<dyn EnvironmentReporter>>,
160    }
161
162    impl<'a> fmt::Debug for BinaryObjectRef<'a> {
163        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164            f.debug_struct("BinaryObjectRef")
165                .field("source", &self.source)
166                .field("source_kind", &self.source_kind)
167                .field("asset_index", &self.asset_index)
168                .field("path_id", &self.object.path_id())
169                .finish()
170        }
171    }
172
173    impl<'a> BinaryObjectRef<'a> {
174        pub fn read(&self) -> Result<UnityObject> {
175            let obj = self
176                .object
177                .read_with_options(self.typetree_options)
178                .map_err(|e| {
179                    UnityAssetError::format(format!("Failed to parse binary object: {}", e))
180                })?;
181
182            if let Some(reporter) = &self.reporter {
183                let key = self.key();
184                for w in obj.typetree_warnings() {
185                    reporter.typetree_warning(&key, w);
186                }
187            }
188
189            Ok(obj)
190        }
191
192        /// Create a globally-unique key for this object reference.
193        pub fn key(&self) -> BinaryObjectKey {
194            BinaryObjectKey {
195                source: self.source.clone(),
196                source_kind: self.source_kind,
197                asset_index: self.asset_index,
198                path_id: self.object.path_id(),
199            }
200        }
201    }
202
203    /// A unified object reference across YAML and binary formats.
204    #[derive(Debug, Clone)]
205    pub enum EnvironmentObjectRef<'a> {
206        Yaml(&'a UnityClass),
207        Binary(BinaryObjectRef<'a>),
208    }
209
210    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
211    pub enum BinarySourceKind {
212        SerializedFile,
213        AssetBundle,
214    }
215
216    /// A globally-unique identifier for a binary object.
217    ///
218    /// `path_id` is only unique within a single `SerializedFile`, so we include a source path
219    /// (bundle/asset path) and optional bundle asset index.
220    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
221    pub struct BinaryObjectKey {
222        pub source: BinarySource,
223        pub source_kind: BinarySourceKind,
224        pub asset_index: Option<usize>,
225        pub path_id: i64,
226    }
227
228    /// A best-effort entry extracted from an AssetBundle `m_Container`.
229    #[derive(Debug, Clone, PartialEq, Eq)]
230    pub struct BundleContainerEntry {
231        pub bundle_source: BinarySource,
232        pub asset_index: usize,
233        pub asset_path: String,
234        pub file_id: i32,
235        pub path_id: i64,
236        pub key: Option<BinaryObjectKey>,
237    }
238
239    /// Unified environment for managing Unity assets
240    pub struct Environment {
241        /// Loaded YAML documents
242        yaml_documents: HashMap<PathBuf, YamlDocument>,
243        /// Loaded standalone SerializedFiles (e.g. `.assets`)
244        binary_assets: HashMap<BinarySource, SerializedFile>,
245        /// Loaded AssetBundles (e.g. `.bundle`, `.unity3d`, `.ab`)
246        bundles: HashMap<BinarySource, AssetBundle>,
247        webfiles: HashMap<PathBuf, WebFile>,
248        bundle_container_cache: RwLock<HashMap<BinarySource, Vec<BundleContainerEntry>>>,
249        warnings: Mutex<Vec<EnvironmentWarning>>,
250        reporter: Option<Arc<dyn EnvironmentReporter>>,
251        options: EnvironmentOptions,
252        type_tree_registry: Option<Arc<dyn TypeTreeRegistry>>,
253        /// Base path for relative file resolution
254        #[allow(dead_code)]
255        base_path: PathBuf,
256    }
257
258    impl Environment {
259        /// Create a new environment
260        pub fn new() -> Self {
261            Self::with_options(EnvironmentOptions::default())
262        }
263
264        pub fn with_options(options: EnvironmentOptions) -> Self {
265            Self {
266                yaml_documents: HashMap::new(),
267                binary_assets: HashMap::new(),
268                bundles: HashMap::new(),
269                webfiles: HashMap::new(),
270                bundle_container_cache: RwLock::new(HashMap::new()),
271                warnings: Mutex::new(Vec::new()),
272                reporter: None,
273                options,
274                type_tree_registry: None,
275                base_path: std::env::current_dir().unwrap_or_default(),
276            }
277        }
278
279        pub fn set_reporter(&mut self, reporter: Option<Arc<dyn EnvironmentReporter>>) {
280            self.reporter = reporter;
281        }
282
283        pub fn set_type_tree_registry(&mut self, registry: Option<Arc<dyn TypeTreeRegistry>>) {
284            self.type_tree_registry = registry.clone();
285
286            for file in self.binary_assets.values_mut() {
287                file.set_type_tree_registry(registry.clone());
288            }
289            for bundle in self.bundles.values_mut() {
290                for file in bundle.assets.iter_mut() {
291                    file.set_type_tree_registry(registry.clone());
292                }
293            }
294        }
295
296        pub fn options(&self) -> EnvironmentOptions {
297            self.options
298        }
299
300        pub fn warnings(&self) -> Vec<EnvironmentWarning> {
301            match self.warnings.lock() {
302                Ok(v) => v.clone(),
303                Err(e) => e.into_inner().clone(),
304            }
305        }
306
307        pub fn take_warnings(&self) -> Vec<EnvironmentWarning> {
308            match self.warnings.lock() {
309                Ok(mut v) => std::mem::take(&mut *v),
310                Err(e) => {
311                    let mut v = e.into_inner();
312                    std::mem::take(&mut *v)
313                }
314            }
315        }
316
317        fn push_warning(&self, warning: EnvironmentWarning) {
318            match self.warnings.lock() {
319                Ok(mut warnings) => warnings.push(warning.clone()),
320                Err(e) => e.into_inner().push(warning.clone()),
321            }
322            if let Some(reporter) = &self.reporter {
323                reporter.warn(&warning);
324            }
325        }
326
327        /// Iterate YAML Unity objects.
328        pub fn yaml_objects(&self) -> impl Iterator<Item = &UnityClass> {
329            self.yaml_documents.values().flat_map(|doc| doc.entries())
330        }
331
332        /// Find a YAML object by its YAML anchor (the `&<id>` part).
333        pub fn find_yaml_by_anchor(&self, anchor: &str) -> Option<&UnityClass> {
334            self.yaml_objects().find(|obj| obj.anchor == anchor)
335        }
336
337        /// Iterate all objects (YAML + binary) as lightweight references.
338        pub fn objects(&self) -> Box<dyn Iterator<Item = EnvironmentObjectRef<'_>> + '_> {
339            let yaml_iter = self.yaml_objects().map(EnvironmentObjectRef::Yaml);
340            let bin_iter = self.binary_object_infos().map(EnvironmentObjectRef::Binary);
341            Box::new(yaml_iter.chain(bin_iter))
342        }
343
344        /// Iterate parsed binary `UnityObject`s (best-effort).
345        pub fn binary_objects(&self) -> impl Iterator<Item = Result<UnityObject>> + '_ {
346            self.binary_object_infos().map(|r| r.read())
347        }
348
349        /// Filter YAML objects by class name.
350        pub fn filter_by_class(&self, class_name: &str) -> Vec<&UnityClass> {
351            self.yaml_objects()
352                .filter(|obj| obj.class_name == class_name)
353                .collect()
354        }
355
356        /// Get loaded YAML documents
357        pub fn yaml_documents(&self) -> &HashMap<PathBuf, YamlDocument> {
358            &self.yaml_documents
359        }
360
361        /// Get loaded standalone SerializedFiles.
362        pub fn binary_assets(&self) -> &HashMap<BinarySource, SerializedFile> {
363            &self.binary_assets
364        }
365
366        /// Get loaded AssetBundles.
367        pub fn bundles(&self) -> &HashMap<BinarySource, AssetBundle> {
368            &self.bundles
369        }
370
371        /// Get loaded WebFiles (containers).
372        pub fn webfiles(&self) -> &HashMap<PathBuf, WebFile> {
373            &self.webfiles
374        }
375    }
376
377    impl Default for Environment {
378        fn default() -> Self {
379            Self::new()
380        }
381    }
382
383    #[cfg(test)]
384    mod tests;
385}
386
387pub use imp::*;