Skip to main content

unity_formats/
tpk.rs

1//! Type tree packages (TPK, "TPK*" files): the layouts of Unity's built-in classes for every Unity version, which a
2//! player build strips from its files. Format versions 1 and 2, type tree data, uncompressed or LZ4 (LZMA and
3//! Brotli packages are refused: re-pack them uncompressed).
4
5use primase_foundations::compress::{Decompressor, LZ4_MAX_RATIO, Lz4};
6use std::collections::BTreeMap;
7use std::sync::Arc;
8
9use crate::reader::Reader;
10use crate::typetree::{Node, TypeTree};
11use crate::version::UnityVersion;
12use crate::{Error, Result, invalid};
13
14const MAGIC: u32 = 0x2A4B_5054;
15const HAS_EDITOR_ROOT: u8 = 64;
16const HAS_RELEASE_ROOT: u8 = 128;
17
18#[derive(Clone, Debug)]
19struct Class {
20    name: u16,
21    flags: u8,
22    release_root: Option<u16>,
23}
24
25#[derive(Clone, Debug)]
26struct PackedNode {
27    type_name: u16,
28    name: u16,
29    byte_size: i32,
30    version: i16,
31    meta_flag: u32,
32    children: Vec<u16>,
33}
34
35/// One Unity version's common string table: the names every embedded type tree of that version shares, as
36/// `(offset into the engine's table, index into the package's strings)` sorted by offset.
37#[derive(Clone, Debug)]
38struct CommonTable {
39    from: UnityVersion,
40    entries: Vec<(u32, u16)>,
41}
42
43/// A parsed type tree package.
44#[derive(Clone, Debug)]
45pub struct TypeTreePackage {
46    /// Per class id, the class from each listed version on (`None`: absent from that version).
47    classes: BTreeMap<i32, Vec<(UnityVersion, Option<Class>)>>,
48    nodes: Vec<PackedNode>,
49    strings: Vec<Arc<str>>,
50    /// The common string table from each listed version on, in version order.
51    common: Vec<CommonTable>,
52}
53
54/// The common string table of one Unity version, from [`TypeTreePackage::common_strings`]: what an embedded type
55/// tree's string offset with the top bit set names.
56#[derive(Clone, Copy, Debug)]
57pub struct CommonStrings<'a> {
58    package: &'a TypeTreePackage,
59    entries: &'a [(u32, u16)],
60}
61
62impl CommonStrings<'_> {
63    /// The string at `offset` of the engine's table (`0` is `AABB`, ...); `None` for an offset the table has no
64    /// string at.
65    pub fn get(&self, offset: u32) -> Option<&str> {
66        let at = self.entries.binary_search_by_key(&offset, |&(o, _)| o).ok()?;
67        let &(_, string) = self.entries.get(at)?;
68        self.package.strings.get(usize::from(string)).map(|s| &**s)
69    }
70
71    /// How many strings the table holds.
72    pub fn len(&self) -> usize {
73        self.entries.len()
74    }
75
76    /// Whether the table is empty.
77    pub fn is_empty(&self) -> bool {
78        self.entries.is_empty()
79    }
80
81    /// The strings with their offsets, in offset order.
82    pub fn iter(&self) -> impl Iterator<Item = (u32, &str)> + '_ {
83        self.entries.iter().filter_map(|&(offset, string)| {
84            self.package
85                .strings
86                .get(usize::from(string))
87                .map(|s| (offset, &**s))
88        })
89    }
90}
91
92#[cfg(test)]
93impl TypeTreePackage {
94    /// A package without classes, for tests that need a package but decode no object.
95    pub(crate) fn empty() -> Self {
96        TypeTreePackage {
97            classes: BTreeMap::new(),
98            nodes: Vec::new(),
99            strings: Vec::new(),
100            common: Vec::new(),
101        }
102    }
103}
104
105fn varint_string(r: &mut Reader) -> Result<Arc<str>> {
106    let (mut len, mut shift) = (0usize, 0u32);
107    loop {
108        let b = r.u8()?;
109        if shift > 28 {
110            return Err(invalid("TPK: string length varint too long"));
111        }
112        len |= usize::from(b & 0x7F) << shift;
113        shift = shift.saturating_add(7);
114        if b & 0x80 == 0 {
115            break;
116        }
117    }
118    let s = std::str::from_utf8(r.take(len)?).map_err(|_| invalid("TPK: string not UTF-8"))?;
119    Ok(Arc::from(s))
120}
121
122/// Skip `n` entries of `size` bytes each.
123fn skip(r: &mut Reader, n: usize, size: usize, what: &str) -> Result<()> {
124    let bytes = n.checked_mul(size).ok_or_else(|| {
125        invalid(format!(
126            "TPK: {n} {what} entries of {size} bytes at offset {}",
127            r.pos
128        ))
129    })?;
130    r.take(bytes).map(|_| ())
131}
132
133fn count(r: &mut Reader, what: &str) -> Result<usize> {
134    let n = r.i32()?;
135    usize::try_from(n).map_err(|_| invalid(format!("TPK: negative {what} count {n}")))
136}
137
138/// The most nodes one class's type tree may expand to (the trees seen have a few thousand).
139const MAX_TREE_NODES: usize = 1 << 20;
140
141impl TypeTreePackage {
142    /// Parse a type tree package (TPK format 1 or 2, uncompressed or LZ4). Every index in it is checked, the size an
143    /// LZ4 block claims is checked against what LZ4 can inflate to before anything is allocated for it, and a package
144    /// that is LZMA- or Brotli-compressed is refused as [`crate::Error::Unsupported`].
145    pub fn parse(bytes: &[u8]) -> Result<Self> {
146        let mut r = Reader::new(bytes, false);
147        if r.u32()? != MAGIC {
148            return Err(invalid("not a TPK file"));
149        }
150        let format = r.u8()?;
151        if !(1..=2).contains(&format) {
152            return Err(Error::Unsupported(format!("TPK format version {format}")));
153        }
154        let compression = r.u8()?;
155        let data_type = r.u8()?;
156        let _reserved = r.u8()?;
157        let _reserved2 = r.u32()?;
158        let compressed = r.u32()? as usize;
159        let uncompressed = r.u32()? as usize;
160        if data_type != 0 {
161            return Err(Error::Unsupported(format!(
162                "TPK data type {data_type} (not type trees)"
163            )));
164        }
165        let packed = r.take(compressed)?;
166        let blob: Vec<u8> = match compression {
167            0 => packed.to_vec(),
168            1 => {
169                // the header's size is the file's word: what the packed bytes cannot inflate to is not allocated
170                if uncompressed > packed.len().saturating_mul(LZ4_MAX_RATIO) {
171                    return Err(invalid(format!(
172                        "TPK: {uncompressed} bytes claimed of {} bytes of LZ4, which inflate to at most {}",
173                        packed.len(),
174                        packed.len().saturating_mul(LZ4_MAX_RATIO)
175                    )));
176                }
177                Lz4.decompress(packed, uncompressed)
178                    .map_err(|e| invalid(format!("TPK LZ4: {e}")))?
179            }
180            2 => {
181                return Err(Error::Unsupported(
182                    "LZMA-compressed TPK: use an uncompressed or LZ4 package".into(),
183                ));
184            }
185            3 => {
186                return Err(Error::Unsupported(
187                    "Brotli-compressed TPK: use an uncompressed or LZ4 package".into(),
188                ));
189            }
190            c => return Err(invalid(format!("TPK compression type {c}"))),
191        };
192        if blob.len() != uncompressed {
193            return Err(invalid(format!(
194                "TPK: {} bytes, header says {uncompressed}",
195                blob.len()
196            )));
197        }
198        Self::parse_blob(&blob, format)
199    }
200
201    fn parse_blob(blob: &[u8], format: u8) -> Result<Self> {
202        let mut r = Reader::new(blob, false);
203        let _created = r.i64()?;
204        let versions = count(&mut r, "version")?;
205        skip(&mut r, versions, 8, "version")?;
206        let mut classes = BTreeMap::new();
207        for _ in 0..count(&mut r, "class")? {
208            let id = r.i32()?;
209            let mut entries = Vec::new();
210            for _ in 0..count(&mut r, "class version")? {
211                let version = UnityVersion(r.u64()?);
212                let class = if r.u8()? != 0 {
213                    let name = r.u16()?;
214                    let _base = r.u16()?;
215                    let flags = r.u8()?;
216                    if flags & HAS_EDITOR_ROOT != 0 {
217                        r.u16()?;
218                    }
219                    let release_root = if flags & HAS_RELEASE_ROOT != 0 {
220                        Some(r.u16()?)
221                    } else {
222                        None
223                    };
224                    Some(Class {
225                        name,
226                        flags,
227                        release_root,
228                    })
229                } else {
230                    None
231                };
232                entries.push((version, class));
233            }
234            classes.insert(id, entries);
235        }
236        // the common string table: what the offsets with the top bit set in an embedded type tree stand for. Format 1
237        // lists, per version, how many of a shared index list's first strings the version has (the strings follow each
238        // other, a NUL after each); format 2 lists, per version, each string's offset and index.
239        let mut common_raw: Vec<(UnityVersion, Vec<(u16, u16)>)> = Vec::new();
240        let mut v1_counts: Vec<(UnityVersion, u8)> = Vec::new();
241        let mut v1_indices: Vec<u16> = Vec::new();
242        match format {
243            1 => {
244                for _ in 0..count(&mut r, "common string version")? {
245                    let version = UnityVersion(r.u64()?);
246                    v1_counts.push((version, r.u8()?));
247                }
248                for _ in 0..count(&mut r, "common string index")? {
249                    v1_indices.push(r.u16()?);
250                }
251            }
252            _ => {
253                for _ in 0..count(&mut r, "common string version")? {
254                    let version = UnityVersion(r.u64()?);
255                    let n = count(&mut r, "common string entry")?;
256                    let mut entries = Vec::new();
257                    for _ in 0..n {
258                        let offset = r.u16()?;
259                        entries.push((offset, r.u16()?));
260                    }
261                    common_raw.push((version, entries));
262                }
263            }
264        }
265        let mut nodes = Vec::new();
266        for _ in 0..count(&mut r, "node")? {
267            let type_name = r.u16()?;
268            let name = r.u16()?;
269            let byte_size = r.i32()?;
270            let version = r.i16()?;
271            let _type_flags = r.u8()?;
272            let meta_flag = r.u32()?;
273            let n = r.u16()?;
274            let children = (0..n).map(|_| r.u16()).collect::<Result<Vec<_>>>()?;
275            nodes.push(PackedNode {
276                type_name,
277                name,
278                byte_size,
279                version,
280                meta_flag,
281                children,
282            });
283        }
284        let strings = (0..count(&mut r, "string")?)
285            .map(|_| varint_string(&mut r))
286            .collect::<Result<Vec<_>>>()?;
287        if r.remaining() != 0 {
288            return Err(invalid(format!(
289                "TPK: {} bytes after the string buffer",
290                r.remaining()
291            )));
292        }
293        for (version, n) in v1_counts {
294            let mut offset = 0u16;
295            let mut entries = Vec::new();
296            for &index in v1_indices.iter().take(usize::from(n)) {
297                entries.push((offset, index));
298                let len = strings
299                    .get(usize::from(index))
300                    .ok_or_else(|| invalid("TPK: common string index out of range"))?
301                    .len();
302                offset = u16::try_from(len)
303                    .ok()
304                    .and_then(|l| offset.checked_add(l))
305                    .and_then(|o| o.checked_add(1))
306                    .ok_or_else(|| invalid("TPK: common string table longer than 64 KiB"))?;
307            }
308            common_raw.push((version, entries));
309        }
310        let common = common_raw
311            .into_iter()
312            .map(|(from, entries)| {
313                let mut entries: Vec<(u32, u16)> =
314                    entries.into_iter().map(|(o, s)| (u32::from(o), s)).collect();
315                entries.sort_unstable();
316                if entries
317                    .windows(2)
318                    .any(|w| w.first().map(|e| e.0) == w.get(1).map(|e| e.0))
319                {
320                    return Err(invalid("TPK: two common strings at one offset"));
321                }
322                Ok(CommonTable { from, entries })
323            })
324            .collect::<Result<Vec<_>>>()?;
325        let pkg = TypeTreePackage {
326            classes,
327            nodes,
328            strings,
329            common,
330        };
331        pkg.check()?;
332        Ok(pkg)
333    }
334
335    /// Every index in range, so lookups need no checks later.
336    fn check(&self) -> Result<()> {
337        let (ns, nn) = (self.strings.len(), self.nodes.len());
338        for n in &self.nodes {
339            if usize::from(n.type_name) >= ns || usize::from(n.name) >= ns {
340                return Err(invalid("TPK: node string index out of range"));
341            }
342            if n.children.iter().any(|&c| usize::from(c) >= nn) {
343                return Err(invalid("TPK: child node index out of range"));
344            }
345        }
346        for t in &self.common {
347            if t.entries.iter().any(|&(_, s)| usize::from(s) >= ns) {
348                return Err(invalid("TPK: common string index out of range"));
349            }
350        }
351        for c in self.classes.values().flatten().filter_map(|(_, c)| c.as_ref()) {
352            if usize::from(c.name) >= ns || c.release_root.is_some_and(|r| usize::from(r) >= nn) {
353                return Err(invalid("TPK: class index out of range"));
354            }
355        }
356        Ok(())
357    }
358
359    fn class(&self, class_id: i32, version: UnityVersion) -> Option<&Class> {
360        self.classes
361            .get(&class_id)?
362            .iter()
363            .take_while(|(v, _)| *v <= version)
364            .last()
365            .and_then(|(_, c)| c.as_ref())
366    }
367
368    /// The common string table of a Unity version: the table of the newest listed version that is not newer than
369    /// `version`. `None` for a package that lists none up to that version.
370    pub fn common_strings(&self, version: UnityVersion) -> Option<CommonStrings<'_>> {
371        let table = self.common.iter().take_while(|t| t.from <= version).last()?;
372        Some(CommonStrings {
373            package: self,
374            entries: &table.entries,
375        })
376    }
377
378    /// The class's name in that Unity version.
379    pub fn class_name(&self, class_id: i32, version: UnityVersion) -> Option<&str> {
380        self.class(class_id, version)
381            .and_then(|c| self.strings.get(usize::from(c.name)))
382            .map(|s| &**s)
383    }
384
385    /// The class's release (player) type tree in that Unity version.
386    pub fn type_tree(&self, class_id: i32, version: UnityVersion) -> Result<TypeTree> {
387        let class = self.class(class_id, version).ok_or_else(|| {
388            invalid(format!(
389                "class {class_id} not in the type tree package for {version}"
390            ))
391        })?;
392        let root = class.release_root.ok_or_else(|| {
393            invalid(format!(
394                "class {class_id} ({}) has no release type tree in {version} (flags {:#x})",
395                self.strings.get(usize::from(class.name)).map_or("?", |s| &**s),
396                class.flags
397            ))
398        })?;
399        let mut nodes: Vec<Node> = Vec::new();
400        // depth first, children in order; each node's children by their index in `nodes`
401        fn add(pkg: &TypeTreePackage, id: u16, depth: u32, nodes: &mut Vec<Node>) -> Result<usize> {
402            if depth > 64 {
403                return Err(invalid("TPK: type tree deeper than 64"));
404            }
405            // nodes shared by several parents are copied under each: a package whose nodes name each other twice over
406            // would copy 2^depth of them
407            if nodes.len() >= MAX_TREE_NODES {
408                return Err(invalid(format!(
409                    "TPK: a type tree of more than {MAX_TREE_NODES} nodes"
410                )));
411            }
412            let p = pkg
413                .nodes
414                .get(usize::from(id))
415                .ok_or_else(|| invalid(format!("TPK: node {id} of {}", pkg.nodes.len())))?;
416            let string = |i: u16| {
417                pkg.strings
418                    .get(usize::from(i))
419                    .cloned()
420                    .ok_or_else(|| invalid(format!("TPK: string {i} of {}", pkg.strings.len())))
421            };
422            let at = nodes.len();
423            nodes.push(Node {
424                ty: string(p.type_name)?,
425                name: string(p.name)?,
426                byte_size: p.byte_size,
427                version: i32::from(p.version),
428                meta_flag: p.meta_flag,
429                children: Vec::new(),
430            });
431            for &c in &p.children {
432                let ci = add(pkg, c, depth.saturating_add(1), nodes)?;
433                nodes
434                    .get_mut(at)
435                    .ok_or_else(|| invalid("TPK: a type tree node went missing"))?
436                    .children
437                    .push(ci);
438            }
439            Ok(at)
440        }
441        add(self, root, 0, &mut nodes)?;
442        TypeTree::new(nodes)
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    fn package(compression: u8, packed: &[u8], expected: u32) -> Vec<u8> {
451        let mut bytes = MAGIC.to_le_bytes().to_vec();
452        bytes.extend([2, compression, 0, 0]);
453        bytes.extend(0u32.to_le_bytes());
454        bytes.extend((packed.len() as u32).to_le_bytes());
455        bytes.extend(expected.to_le_bytes());
456        bytes.extend(packed);
457        bytes
458    }
459
460    /// An LZ4 block of one literal run (no matches), encoded by hand: the token's high nibble 15, then the rest of the
461    /// length in bytes of 255 and a final byte.
462    fn literal_lz4(blob: &[u8]) -> Vec<u8> {
463        let mut out = vec![0xf0];
464        let mut rest = blob.len() - 15;
465        while rest >= 255 {
466            out.push(255);
467            rest -= 255;
468        }
469        out.push(rest as u8);
470        out.extend(blob);
471        out
472    }
473
474    /// A format-2 package encoded by hand: one listed version, class 1 named by string 0 ("Base") from version 0
475    /// with release root node 0, no common strings, one node (type and name string 0, size -1, version 1), one string.
476    fn one_class_blob() -> Vec<u8> {
477        let mut b = Vec::new();
478        b.extend(0x0102_0304_0506_0708i64.to_le_bytes()); // created
479        b.extend(1i32.to_le_bytes()); // versions
480        b.extend(0u64.to_le_bytes());
481        b.extend(1i32.to_le_bytes()); // classes
482        b.extend(1i32.to_le_bytes()); // class id
483        b.extend(1i32.to_le_bytes()); // its versions
484        b.extend(0u64.to_le_bytes()); // from version 0
485        b.push(1); // present
486        b.extend(0u16.to_le_bytes()); // name: string 0
487        b.extend(0u16.to_le_bytes()); // base
488        b.push(HAS_RELEASE_ROOT);
489        b.extend(0u16.to_le_bytes()); // release root: node 0
490        b.extend(0i32.to_le_bytes()); // common string versions
491        b.extend(1i32.to_le_bytes()); // nodes
492        b.extend(0u16.to_le_bytes()); // type name: string 0
493        b.extend(0u16.to_le_bytes()); // name: string 0
494        b.extend((-1i32).to_le_bytes()); // byte size
495        b.extend(1i16.to_le_bytes()); // version
496        b.push(0); // type flags
497        b.extend(0u32.to_le_bytes()); // meta flag
498        b.extend(0u16.to_le_bytes()); // children
499        b.extend(1i32.to_le_bytes()); // strings
500        b.push(4);
501        b.extend(b"Base");
502        b
503    }
504
505    /// The stored and the LZ4 package decode to the same classes, nodes and strings, the ones encoded; wrong sizes
506    /// and a cut block are refused.
507    #[test]
508    fn literal_lz4_package_matches_uncompressed_and_rejects_bad_sizes() {
509        let blob = one_class_blob();
510        let n = blob.len() as u32;
511        let packed = literal_lz4(&blob);
512        let raw = TypeTreePackage::parse(&package(0, &blob, n)).unwrap();
513        let compressed = TypeTreePackage::parse(&package(1, &packed, n)).unwrap();
514        for p in [&raw, &compressed] {
515            assert_eq!(p.class_name(1, UnityVersion(1)), Some("Base"));
516            assert_eq!(p.class_name(2, UnityVersion(1)), None);
517            assert_eq!(p.classes[&1][0].1.as_ref().unwrap().release_root, Some(0));
518            assert_eq!(&*p.strings, &[std::sync::Arc::<str>::from("Base")]);
519            let node = &p.nodes[0];
520            assert_eq!(
521                (node.type_name, node.name, node.byte_size, node.version),
522                (0, 0, -1, 1)
523            );
524            assert!(p.type_tree(1, UnityVersion(1)).is_ok());
525        }
526        for expected in [0, n - 1, n + 1] {
527            assert!(TypeTreePackage::parse(&package(1, &packed, expected)).is_err());
528        }
529        assert!(TypeTreePackage::parse(&package(1, &packed[..packed.len() - 1], n)).is_err());
530        assert!(TypeTreePackage::parse(&package(1, &[], 0)).is_err());
531        // a size the packed bytes cannot inflate to is refused before anything is allocated for it
532        let e = TypeTreePackage::parse(&package(1, &packed, u32::MAX))
533            .unwrap_err()
534            .to_string();
535        assert!(e.contains("inflate to at most"), "{e}");
536    }
537
538    /// Nodes that name each other twice over are copied under each parent: thirty levels would be 2^30 nodes. The copy
539    /// stops at a count.
540    #[test]
541    fn a_type_tree_that_expands_past_a_million_nodes_is_refused() {
542        let levels = 30u16;
543        let nodes = (0..levels)
544            .map(|i| PackedNode {
545                type_name: 0,
546                name: 0,
547                byte_size: -1,
548                version: 1,
549                meta_flag: 0,
550                children: if i + 1 < levels {
551                    vec![i + 1, i + 1]
552                } else {
553                    vec![]
554                },
555            })
556            .collect();
557        let class = Class {
558            name: 0,
559            flags: HAS_RELEASE_ROOT,
560            release_root: Some(0),
561        };
562        let pkg = TypeTreePackage {
563            classes: BTreeMap::from([(1, vec![(UnityVersion(0), Some(class))])]),
564            nodes,
565            strings: vec![std::sync::Arc::from("Base")],
566            common: Vec::new(),
567        };
568        let e = pkg.type_tree(1, UnityVersion(1)).unwrap_err().to_string();
569        assert!(e.contains("more than 1048576 nodes"), "{e}");
570    }
571}