unity_asset_binary/
file.rs1use 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
70pub 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
91pub fn looks_like_unityfs_bundle_prefix(prefix: &[u8]) -> bool {
93 prefix.starts_with(b"UnityFS\0")
94}
95
96pub fn looks_like_uncompressed_webfile_prefix(prefix: &[u8]) -> bool {
98 prefix.starts_with(b"UnityWebData") || prefix.starts_with(b"TuanjieWebData")
99}
100
101pub fn looks_like_serialized_file_prefix(prefix: &[u8]) -> bool {
105 sniff_serialized_file(prefix)
106}
107
108pub 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
135pub 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
146pub 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
183pub 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
207pub 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
234pub 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}