Skip to main content

unity_formats/
serialized.rs

1//! The SerializedFile container: what `globalgamemanagers`, `*.assets` and `levelN` are.
2//!
3//! Layout (container versions 21 and 22): a big-endian header followed by metadata in the file's
4//! endianness: Unity version, target platform, whether type trees are embedded, the types (class id, stripped,
5//! script index, hashes), the object table (path id, data offset, size, type index), script references, external
6//! files, reference types and a user string. Objects' bytes follow at the data offset.
7//! Version 21 stores metadata size, file size and data offset as 32-bit header fields, followed by an
8//! endianness byte and three reserved bytes; metadata starts at byte 20. Version 22 repeats those sizes
9//! after the legacy fields as 32/64/64-bit numbers plus eight reserved bytes; metadata starts at byte 48.
10//! Object offsets are unsigned 32-bit in version 21 and signed 64-bit in version 22.
11//!
12//! Only versions verified against real data are read; others are refused, as are embedded type trees (player
13//! builds strip them; decode with a [`TypeTreePackage`] instead).
14
15use std::collections::BTreeMap;
16
17use crate::reader::Reader;
18use crate::tpk::TypeTreePackage;
19use crate::typetree::TypeTree;
20use crate::value::Value;
21use crate::version::UnityVersion;
22use crate::{Error, Result, invalid};
23
24/// Container versions read (verified on real files).
25pub const VERSIONS: std::ops::RangeInclusive<u32> = 21..=22;
26
27/// The MonoBehaviour class: its type tree covers the base fields; the script's own fields follow.
28pub const MONO_BEHAVIOUR: i32 = 114;
29
30/// One entry of a file's type table: the class the objects of this type are of, as the file declares it.
31#[derive(Clone, Debug, PartialEq)]
32pub struct SerializedType {
33    /// Unity's class id (1 GameObject, 4 Transform, 114 MonoBehaviour, ...).
34    pub class_id: i32,
35    /// Whether the build stripped the class's type tree.
36    pub stripped: bool,
37    /// Index into the script references (-1: none).
38    pub script_type_index: i16,
39    /// The script's 16-byte hash: present for a MonoBehaviour type, and for a reference type whose script index is
40    /// not -1.
41    pub script_id: Option<[u8; 16]>,
42    /// The 16-byte hash the file stores for the class's layout.
43    pub old_type_hash: [u8; 16],
44}
45
46/// One entry of the object table.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct ObjectInfo {
49    /// The object's id within its file; unique per file.
50    pub path_id: i64,
51    /// The object's bytes: offset in the file and size.
52    pub offset: u64,
53    /// The object's size in bytes.
54    pub size: u32,
55    /// Index into [`SerializedFile::types`].
56    pub type_index: u32,
57    /// The class id of the object's type.
58    pub class_id: i32,
59}
60
61/// Another file this one's pointers can name (file id = index + 1).
62#[derive(Clone, Debug, PartialEq)]
63pub struct External {
64    /// The external file's 16-byte GUID as stored.
65    pub guid: [u8; 16],
66    /// The file's kind as stored (`type`).
67    pub kind: i32,
68    /// The external file's path as stored; [`crate::player::Player::resolve`] names the file by its last path segment,
69    /// lower-cased.
70    pub path: String,
71}
72
73/// A decoded object: its value, and the bytes after what its type tree covers.
74#[derive(Clone, Debug, PartialEq)]
75pub struct Decoded {
76    /// The decoded object.
77    pub value: Value,
78    /// How many of the object's bytes lie after what the type tree covers.
79    pub rest: usize,
80}
81
82impl Decoded {
83    /// Whether the tree covered the object: every byte, or a MonoBehaviour's base fields (its script's fields follow,
84    /// and a stripped build has no type trees for them).
85    pub fn covered(&self, class_id: i32) -> bool {
86        self.rest == 0 || class_id == MONO_BEHAVIOUR
87    }
88}
89
90/// A parsed SerializedFile: its header fields, type table, object table, script references and external files, with
91/// the file's bytes kept so objects can be read from it.
92#[derive(Clone, Debug)]
93pub struct SerializedFile {
94    /// The container version (21 or 22: see [`VERSIONS`]).
95    pub version: u32,
96    /// The Unity version string the file was built with (`2019.4.20f1`).
97    pub unity_version: String,
98    /// [`SerializedFile::unity_version`], parsed.
99    pub unity: UnityVersion,
100    /// Unity's `BuildTarget` number the file was built for.
101    pub target_platform: i32,
102    /// Whether the metadata and the objects' bytes are big-endian.
103    pub big_endian: bool,
104    /// The type table.
105    pub types: Vec<SerializedType>,
106    /// The object table, in file order.
107    pub objects: Vec<ObjectInfo>,
108    /// Script references: (file index, path id).
109    pub script_types: Vec<(i32, i64)>,
110    /// The files this file's pointers can name (file id = index + 1).
111    pub externals: Vec<External>,
112    /// The table of types of managed references (`[SerializeReference]`).
113    pub ref_types: Vec<SerializedType>,
114    /// The user string at the end of the metadata.
115    pub user_information: String,
116    data: Vec<u8>,
117    by_path_id: BTreeMap<i64, usize>,
118}
119
120fn hash(r: &mut Reader) -> Result<[u8; 16]> {
121    Ok(r.take(16)?.try_into().expect("16 bytes"))
122}
123
124fn count(r: &mut Reader, what: &str) -> Result<usize> {
125    let n = r.i32()?;
126    let n = usize::try_from(n).map_err(|_| invalid(format!("negative {what} count {n}")))?;
127    if n > r.remaining() {
128        return Err(invalid(format!("{what} count {n} larger than the metadata")));
129    }
130    Ok(n)
131}
132
133fn serialized_type(r: &mut Reader, reference: bool) -> Result<SerializedType> {
134    let class_id = r.i32()?;
135    let stripped = r.bool()?;
136    let script_type_index = r.i16()?;
137    let script_id = if (reference && script_type_index >= 0) || (!reference && class_id == MONO_BEHAVIOUR) {
138        Some(hash(r)?)
139    } else {
140        None
141    };
142    let old_type_hash = hash(r)?;
143    Ok(SerializedType {
144        class_id,
145        stripped,
146        script_type_index,
147        script_id,
148        old_type_hash,
149    })
150}
151
152impl SerializedFile {
153    /// Whether `bytes` start like a SerializedFile of a supported version (to tell them from other files).
154    pub fn looks_like(bytes: &[u8]) -> bool {
155        let be = |at: usize| -> Option<u32> {
156            Some(u32::from_be_bytes(
157                bytes.get(at..at.checked_add(4)?)?.try_into().ok()?,
158            ))
159        };
160        let Ok(len) = u64::try_from(bytes.len()) else {
161            return false;
162        };
163        let check = || -> Option<bool> {
164            Some(match be(8)? {
165                21 => {
166                    let end = 20u64.checked_add(u64::from(be(0)?))?;
167                    let (size, offset) = (u64::from(be(4)?), u64::from(be(12)?));
168                    be(0)? > 0 && size == len && end <= offset && offset <= size
169                }
170                22 => len >= 48 && be(0)? == 0,
171                _ => false,
172            })
173        };
174        check().unwrap_or(false)
175    }
176
177    /// The container version of `bytes` when they are a SerializedFile of any version, supported or not: a supported
178    /// version by its field alone (a damaged file of it is for [`SerializedFile::parse`] to refuse), another version
179    /// only when the header's own size fields agree with `bytes.len()`, so that other files (streamed resources,
180    /// configs) are not taken for one. `None` for bytes that are no SerializedFile.
181    pub fn container_version(bytes: &[u8]) -> Option<u32> {
182        let be = |at: usize| -> Option<u32> {
183            Some(u32::from_be_bytes(
184                bytes.get(at..at.checked_add(4)?)?.try_into().ok()?,
185            ))
186        };
187        let len = u64::try_from(bytes.len()).ok()?;
188        let version = be(8)?;
189        if VERSIONS.contains(&version) {
190            return Some(version);
191        }
192        let consistent = if version < *VERSIONS.start() {
193            // the legacy header: metadata size, file size, version, data offset
194            version > 0 && u64::from(be(4)?) == len && u64::from(be(12)?) <= len
195        } else {
196            // the header of 22 and later: the legacy fields are zero, the file size follows the metadata size
197            be(0)? == 0
198                && be(4)? == 0
199                && be(12)? == 0
200                && u64::from_be_bytes(bytes.get(24..32)?.try_into().ok()?) == len
201        };
202        consistent.then_some(version)
203    }
204
205    /// Parse the SerializedFile `data`. The header's sizes must match `data`, the object table must lie inside it and
206    /// the metadata must end exactly where the header says; a version outside [`VERSIONS`], a file with embedded type
207    /// trees, a duplicate path id and every length that runs past the bytes are refused with an [`Error`] that names
208    /// what was found.
209    pub fn parse(data: Vec<u8>) -> Result<Self> {
210        let mut h = Reader::new(&data, true);
211        let legacy_metadata = h.u32()?;
212        let legacy_size = h.u32()?;
213        let version = h.u32()?;
214        let legacy_offset = h.u32()?;
215        if !VERSIONS.contains(&version) {
216            return Err(Error::Unsupported(format!(
217                "SerializedFile version {version} (read: {}..={})",
218                VERSIONS.start(),
219                VERSIONS.end()
220            )));
221        }
222        let big_endian = h.bool()?;
223        h.take(3)?;
224        let (metadata_size, file_size, data_offset) = if version == 22 {
225            let metadata_size = h.u32()?;
226            let file_size = h.u64()?;
227            let data_offset = h.u64()?;
228            let _reserved = h.u64()?;
229            (metadata_size, file_size, data_offset)
230        } else {
231            (legacy_metadata, u64::from(legacy_size), u64::from(legacy_offset))
232        };
233        if file_size != data.len() as u64 {
234            return Err(invalid(format!(
235                "header file size {file_size}, file has {} bytes",
236                data.len()
237            )));
238        }
239        let meta_end = h
240            .pos
241            .checked_add(metadata_size as usize)
242            .ok_or_else(|| invalid("metadata size overflows its byte range"))?;
243        if meta_end as u64 > data_offset || data_offset > file_size {
244            return Err(invalid(format!(
245                "metadata ends at {meta_end}, data at {data_offset}, file size {file_size}"
246            )));
247        }
248        let metadata = data.get(..meta_end).ok_or_else(|| {
249            invalid(format!(
250                "metadata ends at {meta_end}, past the file's {} bytes",
251                data.len()
252            ))
253        })?;
254        let mut r = Reader::new(metadata, big_endian);
255        r.pos = h.pos;
256        let unity_version = r.cstr()?;
257        let unity = UnityVersion::parse(&unity_version)?;
258        let target_platform = r.i32()?;
259        if r.bool()? {
260            return Err(Error::Unsupported(
261                "embedded type trees (decode stripped player files with a type tree package)".into(),
262            ));
263        }
264        let types = (0..count(&mut r, "type")?)
265            .map(|_| serialized_type(&mut r, false))
266            .collect::<Result<Vec<_>>>()?;
267        let mut objects = Vec::new();
268        for _ in 0..count(&mut r, "object")? {
269            r.align(4)?;
270            let path_id = r.i64()?;
271            let start = if version == 21 {
272                i64::from(r.u32()?)
273            } else {
274                r.i64()?
275            };
276            let size = r.u32()?;
277            let type_index = r.u32()?;
278            let ty = types.get(type_index as usize).ok_or_else(|| {
279                invalid(format!(
280                    "object {path_id}: type index {type_index} of {}",
281                    types.len()
282                ))
283            })?;
284            let offset = u64::try_from(start)
285                .ok()
286                .and_then(|s| s.checked_add(data_offset))
287                .filter(|&o| o.checked_add(u64::from(size)).is_some_and(|end| end <= file_size))
288                .ok_or_else(|| invalid(format!("object {path_id}: bytes {start}+{size} outside the file")))?;
289            objects.push(ObjectInfo {
290                path_id,
291                offset,
292                size,
293                type_index,
294                class_id: ty.class_id,
295            });
296        }
297        let mut script_types = Vec::new();
298        for _ in 0..count(&mut r, "script")? {
299            let file = r.i32()?;
300            r.align(4)?;
301            script_types.push((file, r.i64()?));
302        }
303        let mut externals = Vec::new();
304        for _ in 0..count(&mut r, "external")? {
305            let _empty = r.cstr()?;
306            let guid = hash(&mut r)?;
307            let kind = r.i32()?;
308            let path = r.cstr()?;
309            externals.push(External { guid, kind, path });
310        }
311        let ref_types = (0..count(&mut r, "reference type")?)
312            .map(|_| serialized_type(&mut r, true))
313            .collect::<Result<Vec<_>>>()?;
314        let user_information = r.cstr()?;
315        if r.pos != meta_end {
316            return Err(invalid(format!(
317                "metadata read to {}, header says it ends at {meta_end}",
318                r.pos
319            )));
320        }
321        let mut by_path_id = BTreeMap::new();
322        for (i, o) in objects.iter().enumerate() {
323            if by_path_id.insert(o.path_id, i).is_some() {
324                return Err(invalid(format!("path id {} twice", o.path_id)));
325            }
326        }
327        Ok(SerializedFile {
328            version,
329            unity_version,
330            unity,
331            target_platform,
332            big_endian,
333            types,
334            objects,
335            script_types,
336            externals,
337            ref_types,
338            user_information,
339            data,
340            by_path_id,
341        })
342    }
343
344    /// The object with this path id, if the file has one.
345    pub fn object(&self, path_id: i64) -> Option<&ObjectInfo> {
346        self.by_path_id.get(&path_id).and_then(|&i| self.objects.get(i))
347    }
348
349    /// The bytes of an object of this file. `o` should come from this file's own [`SerializedFile::objects`]: an
350    /// [`ObjectInfo`] of another file that names bytes outside this one is an error naming the object and its range.
351    pub fn bytes(&self, o: &ObjectInfo) -> Result<&[u8]> {
352        let outside = || {
353            invalid(format!(
354                "object {}: bytes {}+{} are outside the file's {} bytes",
355                o.path_id,
356                o.offset,
357                o.size,
358                self.data.len()
359            ))
360        };
361        let start = usize::try_from(o.offset).map_err(|_| outside())?;
362        let end = start
363            .checked_add(usize::try_from(o.size).map_err(|_| outside())?)
364            .ok_or_else(outside)?;
365        self.data.get(start..end).ok_or_else(outside)
366    }
367
368    /// Decode an object's bytes from its start with `tree` (its class's type tree), reporting the bytes after what the
369    /// tree covers in [`Decoded::rest`] (see [`Decoded::covered`]).
370    pub fn decode_with(&self, o: &ObjectInfo, tree: &TypeTree) -> Result<Decoded> {
371        let bytes = self.bytes(o)?;
372        let (value, used) = tree
373            .decode(bytes, self.big_endian)
374            .map_err(|e| invalid(format!("object {} (class {}): {e}", o.path_id, o.class_id)))?;
375        let rest = bytes.len().checked_sub(used).ok_or_else(|| {
376            invalid(format!(
377                "object {} (class {}): decoded {used} of {} bytes",
378                o.path_id,
379                o.class_id,
380                bytes.len()
381            ))
382        })?;
383        Ok(Decoded { value, rest })
384    }
385
386    /// Decode an object with its class's type tree from `package`. The tree must cover the object's bytes, but for a
387    /// MonoBehaviour, whose script fields follow its base fields.
388    pub fn decode(&self, o: &ObjectInfo, package: &TypeTreePackage) -> Result<Decoded> {
389        let tree = package.type_tree(o.class_id, self.unity)?;
390        let d = self.decode_with(o, &tree)?;
391        if !d.covered(o.class_id) {
392            return Err(invalid(format!(
393                "object {} (class {}): {} bytes after its type tree",
394                o.path_id, o.class_id, d.rest
395            )));
396        }
397        Ok(d)
398    }
399}