unity_asset/environment/imp/
container.rs

1use super::*;
2
3impl Environment {
4    fn scan_pptr(value: &UnityValue) -> Option<(i32, i64)> {
5        match value {
6            UnityValue::Object(obj) => {
7                let file_id = obj
8                    .get("fileID")
9                    .or_else(|| obj.get("m_FileID"))
10                    .and_then(|v| v.as_i64())
11                    .and_then(|v| i32::try_from(v).ok());
12                let path_id = obj
13                    .get("pathID")
14                    .or_else(|| obj.get("m_PathID"))
15                    .and_then(|v| v.as_i64());
16
17                if let (Some(file_id), Some(path_id)) = (file_id, path_id) {
18                    return Some((file_id, path_id));
19                }
20
21                for (_, v) in obj.iter() {
22                    if let Some(pptr) = Self::scan_pptr(v) {
23                        return Some(pptr);
24                    }
25                }
26
27                None
28            }
29            UnityValue::Array(items) => {
30                for item in items {
31                    if let Some(pptr) = Self::scan_pptr(item) {
32                        return Some(pptr);
33                    }
34                }
35                None
36            }
37            _ => None,
38        }
39    }
40
41    fn extract_assetbundle_container_from_typetree(
42        &self,
43        context: &BinaryObjectRef<'_>,
44        parsed: &UnityObject,
45    ) -> Vec<BundleContainerEntry> {
46        let mut out = Vec::new();
47
48        let Some(UnityValue::Array(items)) = parsed.class.get("m_Container") else {
49            return out;
50        };
51
52        for item in items {
53            let (asset_path, second) = match item {
54                // Unity typetree `pair` is represented as `[first, second]` by our TypeTree deserializer.
55                UnityValue::Array(pair) if pair.len() == 2 => {
56                    let Some(asset_path) = pair[0].as_str() else {
57                        continue;
58                    };
59                    (asset_path.to_string(), &pair[1])
60                }
61                // Best-effort fallback for alternative pair representations.
62                UnityValue::Object(obj) => {
63                    let first = obj.get("first").and_then(|v| v.as_str());
64                    let second = obj.get("second").or_else(|| obj.get("value"));
65                    let (Some(first), Some(second)) = (first, second) else {
66                        continue;
67                    };
68                    (first.to_string(), second)
69                }
70                _ => continue,
71            };
72
73            let Some((file_id, path_id)) = Self::scan_pptr(second) else {
74                continue;
75            };
76            if path_id == 0 {
77                continue;
78            }
79
80            let key = self.resolve_binary_pptr(context, file_id, path_id);
81            out.push(BundleContainerEntry {
82                bundle_source: context.source.clone(),
83                asset_index: context.asset_index.unwrap_or(0),
84                asset_path,
85                file_id,
86                path_id,
87                key,
88            });
89        }
90
91        out
92    }
93
94    /// Extract best-effort `m_Container` entries from a loaded bundle source path.
95    ///
96    /// This scans for `AssetBundle` objects (class id `142`) inside the bundle and parses them to find
97    /// `m_Container` entries.
98    pub fn bundle_container_entries<P: AsRef<Path>>(
99        &self,
100        bundle_path: P,
101    ) -> Result<Vec<BundleContainerEntry>> {
102        let bundle_source = BinarySource::path(bundle_path.as_ref());
103        self.bundle_container_entries_source(&bundle_source)
104    }
105
106    pub fn bundle_container_entries_source(
107        &self,
108        bundle_source: &BinarySource,
109    ) -> Result<Vec<BundleContainerEntry>> {
110        match self.bundle_container_cache.read() {
111            Ok(cache) => {
112                if let Some(cached) = cache.get(bundle_source) {
113                    return Ok(cached.clone());
114                }
115            }
116            Err(e) => {
117                let cache = e.into_inner();
118                if let Some(cached) = cache.get(bundle_source) {
119                    return Ok(cached.clone());
120                }
121            }
122        }
123
124        let (key, bundle) = self.bundles.get_key_value(bundle_source).ok_or_else(|| {
125            UnityAssetError::format(format!(
126                "AssetBundle source not loaded: {}",
127                bundle_source.describe()
128            ))
129        })?;
130
131        let mut out: Vec<BundleContainerEntry> = Vec::new();
132        let typetree_options = self.options.typetree;
133        let reporter = self.reporter.clone();
134
135        for (asset_index, file) in bundle.assets.iter().enumerate() {
136            for object in file.object_handles() {
137                if object.class_id() != 142 {
138                    continue;
139                }
140                let obj_ref = BinaryObjectRef {
141                    source: key,
142                    source_kind: BinarySourceKind::AssetBundle,
143                    asset_index: Some(asset_index),
144                    object,
145                    typetree_options,
146                    reporter: reporter.clone(),
147                };
148
149                // First, try TypeTree extraction when available.
150                if object.file().enable_type_tree
151                    && let Ok(parsed) = obj_ref.read()
152                {
153                    let extracted =
154                        self.extract_assetbundle_container_from_typetree(&obj_ref, &parsed);
155                    if !extracted.is_empty() {
156                        out.extend(extracted);
157                        continue;
158                    }
159                }
160
161                // Fallback: raw parsing for stripped TypeTree bundles.
162                if let Ok(raw_entries) = object.file().assetbundle_container_raw(object.info()) {
163                    for (asset_path, file_id, path_id) in raw_entries {
164                        if path_id == 0 {
165                            continue;
166                        }
167                        let key = self
168                            .resolve_binary_pptr(&obj_ref, file_id, path_id)
169                            .or_else(|| {
170                                // Fallback: if external mapping fails, try to locate the object by `path_id`
171                                // within the same bundle. This is best-effort and only used when `file_id`
172                                // can't be resolved.
173                                let matches =
174                                    self.find_binary_objects_in_source_id(obj_ref.source, path_id);
175                                if matches.len() == 1 {
176                                    Some(matches[0].key())
177                                } else {
178                                    None
179                                }
180                            });
181                        out.push(BundleContainerEntry {
182                            bundle_source: obj_ref.source.clone(),
183                            asset_index,
184                            asset_path,
185                            file_id,
186                            path_id,
187                            key,
188                        });
189                    }
190                }
191            }
192        }
193
194        match self.bundle_container_cache.write() {
195            Ok(mut cache) => {
196                cache.insert(bundle_source.clone(), out.clone());
197            }
198            Err(e) => {
199                e.into_inner().insert(bundle_source.clone(), out.clone());
200            }
201        }
202        Ok(out)
203    }
204
205    /// Find container entries across all loaded bundles whose `asset_path` contains `pattern`.
206    pub fn find_bundle_container_entries(&self, pattern: &str) -> Vec<BundleContainerEntry> {
207        let mut bundle_sources: Vec<&BinarySource> = self.bundles.keys().collect();
208        bundle_sources.sort();
209
210        let mut out = Vec::new();
211        for bundle_source in bundle_sources {
212            if let Ok(entries) = self.bundle_container_entries_source(bundle_source) {
213                out.extend(
214                    entries
215                        .into_iter()
216                        .filter(|e| e.asset_path.contains(pattern)),
217                );
218            }
219        }
220        out
221    }
222
223    /// Find resolved `BinaryObjectKey`s from bundle containers by path substring.
224    pub fn find_binary_object_keys_in_bundle_container(
225        &self,
226        pattern: &str,
227    ) -> Vec<(String, BinaryObjectKey)> {
228        self.find_bundle_container_entries(pattern)
229            .into_iter()
230            .filter_map(|e| e.key.map(|k| (e.asset_path, k)))
231            .collect()
232    }
233}