unity_asset_binary/
file.rs

1//! Unified Unity file model (UnityPy-aligned).
2//!
3//! Unity distributes multiple binary container formats:
4//! - AssetBundle containers (UnityFS/UnityWeb/UnityRaw)
5//! - SerializedFile assets (`.assets`)
6//! - WebFile containers (`UnityWebData*`)
7//!
8//! This module provides a single entry point to parse them into a tagged enum.
9
10use crate::asset::SerializedFile;
11use crate::asset::header::SerializedFileHeader;
12use crate::bundle::{AssetBundle, BundleLoadOptions, BundleParser};
13use crate::data_view::DataView;
14use crate::error::{BinaryError, Result};
15use crate::reader::{BinaryReader, ByteOrder};
16use crate::shared_bytes::SharedBytes;
17use std::ops::Range;
18use std::path::Path;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum UnityFileKind {
22    AssetBundle,
23    SerializedFile,
24    WebFile,
25}
26
27#[derive(Debug)]
28#[allow(clippy::large_enum_variant)]
29pub enum UnityFile {
30    AssetBundle(crate::bundle::AssetBundle),
31    SerializedFile(crate::asset::SerializedFile),
32    WebFile(crate::webfile::WebFile),
33}
34
35impl UnityFile {
36    pub fn kind(&self) -> UnityFileKind {
37        match self {
38            UnityFile::AssetBundle(_) => UnityFileKind::AssetBundle,
39            UnityFile::SerializedFile(_) => UnityFileKind::SerializedFile,
40            UnityFile::WebFile(_) => UnityFileKind::WebFile,
41        }
42    }
43
44    pub fn as_bundle(&self) -> Option<&crate::bundle::AssetBundle> {
45        match self {
46            UnityFile::AssetBundle(v) => Some(v),
47            _ => None,
48        }
49    }
50
51    pub fn as_serialized(&self) -> Option<&crate::asset::SerializedFile> {
52        match self {
53            UnityFile::SerializedFile(v) => Some(v),
54            _ => None,
55        }
56    }
57
58    pub fn as_web(&self) -> Option<&crate::webfile::WebFile> {
59        match self {
60            UnityFile::WebFile(v) => Some(v),
61            _ => None,
62        }
63    }
64}
65
66fn sniff_bundle(data: &[u8]) -> bool {
67    looks_like_bundle_prefix(data)
68}
69
70/// Return true if the provided byte prefix looks like an AssetBundle container signature.
71///
72/// Notes:
73/// - This mirrors UnityPy-style sniffing and is intentionally conservative.
74/// - `UnityWebData*` / `TuanjieWebData*` are WebFile containers and must not be classified as bundles.
75pub fn looks_like_bundle_prefix(prefix: &[u8]) -> bool {
76    if prefix.len() < 8 {
77        return false;
78    }
79    if prefix.starts_with(b"UnityFS\0") || prefix.starts_with(b"UnityRaw") {
80        return true;
81    }
82    if prefix.starts_with(b"UnityWeb") {
83        if prefix.starts_with(b"UnityWebData") || prefix.starts_with(b"TuanjieWebData") {
84            return false;
85        }
86        return true;
87    }
88    false
89}
90
91/// Return true if the provided byte prefix matches the UnityFS bundle signature.
92pub fn looks_like_unityfs_bundle_prefix(prefix: &[u8]) -> bool {
93    prefix.starts_with(b"UnityFS\0")
94}
95
96/// Return true if the provided byte prefix looks like an uncompressed WebFile container signature.
97pub fn looks_like_uncompressed_webfile_prefix(prefix: &[u8]) -> bool {
98    prefix.starts_with(b"UnityWebData") || prefix.starts_with(b"TuanjieWebData")
99}
100
101/// Return true if the provided byte prefix looks like a SerializedFile.
102///
103/// This performs a minimal header parse and validity check.
104pub fn looks_like_serialized_file_prefix(prefix: &[u8]) -> bool {
105    sniff_serialized_file(prefix)
106}
107
108/// Classify a file by inspecting an in-memory prefix.
109///
110/// This is a cheap, conservative helper intended for fast directory scans.
111pub fn sniff_unity_file_kind_prefix(prefix: &[u8]) -> Option<UnityFileKind> {
112    if looks_like_uncompressed_webfile_prefix(prefix) {
113        return Some(UnityFileKind::WebFile);
114    }
115    if looks_like_bundle_prefix(prefix) {
116        return Some(UnityFileKind::AssetBundle);
117    }
118    if looks_like_serialized_file_prefix(prefix) {
119        return Some(UnityFileKind::SerializedFile);
120    }
121    None
122}
123
124fn sniff_serialized_file(data: &[u8]) -> bool {
125    if data.len() < 20 {
126        return false;
127    }
128    let mut reader = BinaryReader::new(data, ByteOrder::Big);
129    let Ok(header) = SerializedFileHeader::from_reader(&mut reader) else {
130        return false;
131    };
132    header.is_valid()
133}
134
135/// Parse a Unity binary file from memory, returning a tagged [`UnityFile`] enum.
136///
137/// Notes:
138/// - The detection order is: bundle → serialized file → webfile.
139/// - WebFile detection can involve decompression, so it is attempted last.
140pub fn load_unity_file_from_memory(data: Vec<u8>) -> Result<UnityFile> {
141    let shared = SharedBytes::from_vec(data);
142    let len = shared.len();
143    load_unity_file_from_shared_range(shared, 0..len)
144}
145
146/// Parse a Unity binary file from a shared backing buffer + byte range.
147///
148/// This is useful for container formats that can provide a view into a larger buffer (e.g. WebFile entries).
149pub fn load_unity_file_from_shared_range(
150    data: SharedBytes,
151    range: Range<usize>,
152) -> Result<UnityFile> {
153    let view = DataView::from_shared_range(data, range)?;
154    let bytes = view.as_bytes();
155
156    if sniff_bundle(bytes) {
157        let bundle = crate::bundle::BundleParser::from_shared_range(
158            view.backing_shared(),
159            view.absolute_range(),
160        )?;
161        return Ok(UnityFile::AssetBundle(bundle));
162    }
163
164    if sniff_serialized_file(bytes) {
165        let file = crate::asset::SerializedFileParser::from_shared_range(
166            view.backing_shared(),
167            view.absolute_range(),
168        )?;
169        return Ok(UnityFile::SerializedFile(file));
170    }
171
172    if let Ok(web) =
173        crate::webfile::WebFile::from_shared_range(view.backing_shared(), view.absolute_range())
174    {
175        return Ok(UnityFile::WebFile(web));
176    }
177
178    Err(BinaryError::invalid_format(
179        "Unrecognized Unity binary file (not AssetBundle/SerializedFile/WebFile)",
180    ))
181}
182
183/// Parse a Unity binary file from a filesystem path.
184pub fn load_unity_file<P: AsRef<Path>>(path: P) -> Result<UnityFile> {
185    #[cfg(feature = "mmap")]
186    {
187        let file = std::fs::File::open(&path).map_err(|e| {
188            BinaryError::generic(format!("Failed to open file {:?}: {}", path.as_ref(), e))
189        })?;
190        let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| {
191            BinaryError::generic(format!("Failed to mmap file {:?}: {}", path.as_ref(), e))
192        })?;
193        let shared = SharedBytes::Mmap(std::sync::Arc::new(mmap));
194        let len = shared.len();
195        load_unity_file_from_shared_range(shared, 0..len)
196    }
197
198    #[cfg(not(feature = "mmap"))]
199    {
200        let data = std::fs::read(&path).map_err(|e| {
201            BinaryError::generic(format!("Failed to read file {:?}: {}", path.as_ref(), e))
202        })?;
203        load_unity_file_from_memory(data)
204    }
205}
206
207/// Load an AssetBundle from a filesystem path with explicit parser options.
208pub fn load_bundle_file_with_options<P: AsRef<Path>>(
209    path: P,
210    options: BundleLoadOptions,
211) -> Result<AssetBundle> {
212    #[cfg(feature = "mmap")]
213    {
214        let file = std::fs::File::open(&path).map_err(|e| {
215            BinaryError::generic(format!("Failed to open file {:?}: {}", path.as_ref(), e))
216        })?;
217        let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| {
218            BinaryError::generic(format!("Failed to mmap file {:?}: {}", path.as_ref(), e))
219        })?;
220        let shared = SharedBytes::Mmap(std::sync::Arc::new(mmap));
221        let len = shared.len();
222        BundleParser::from_shared_range_with_options(shared, 0..len, options)
223    }
224
225    #[cfg(not(feature = "mmap"))]
226    {
227        let data = std::fs::read(&path).map_err(|e| {
228            BinaryError::generic(format!("Failed to read file {:?}: {}", path.as_ref(), e))
229        })?;
230        BundleParser::from_bytes_with_options(data, options)
231    }
232}
233
234/// Load a SerializedFile from a filesystem path.
235pub fn load_serialized_file<P: AsRef<Path>>(
236    path: P,
237    preload_object_data: bool,
238) -> Result<SerializedFile> {
239    #[cfg(feature = "mmap")]
240    {
241        let file = std::fs::File::open(&path).map_err(|e| {
242            BinaryError::generic(format!("Failed to open file {:?}: {}", path.as_ref(), e))
243        })?;
244        let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| {
245            BinaryError::generic(format!("Failed to mmap file {:?}: {}", path.as_ref(), e))
246        })?;
247        let shared = SharedBytes::Mmap(std::sync::Arc::new(mmap));
248        let len = shared.len();
249        crate::asset::SerializedFileParser::from_shared_range_with_options(
250            shared,
251            0..len,
252            preload_object_data,
253        )
254    }
255
256    #[cfg(not(feature = "mmap"))]
257    {
258        let data = std::fs::read(&path).map_err(|e| {
259            BinaryError::generic(format!("Failed to read file {:?}: {}", path.as_ref(), e))
260        })?;
261        crate::asset::SerializedFileParser::from_bytes_with_options(data, preload_object_data)
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn sniff_bundle_excludes_uncompressed_webfile() {
271        let data = b"UnityWebData1.0\0";
272        assert!(!sniff_bundle(data));
273    }
274}