use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::bundle::Bundle;
use crate::reader::Reader;
use crate::tpk::{CommonStrings, TypeTreePackage};
use crate::typetree::{Node, TypeTree};
use crate::value::Value;
use crate::version::UnityVersion;
use crate::{Error, Result, invalid};
pub const VERSIONS: std::ops::RangeInclusive<u32> = 21..=22;
pub const MONO_BEHAVIOUR: i32 = 114;
#[derive(Clone, Debug, PartialEq)]
pub struct SerializedType {
pub class_id: i32,
pub stripped: bool,
pub script_type_index: i16,
pub script_id: Option<[u8; 16]>,
pub old_type_hash: [u8; 16],
pub tree: Option<Arc<TypeTree>>,
pub dependencies: Vec<i32>,
pub reference: Option<ReferenceClass>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReferenceClass {
pub class: String,
pub namespace: String,
pub assembly: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ObjectInfo {
pub path_id: i64,
pub offset: u64,
pub size: u32,
pub type_index: u32,
pub class_id: i32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct External {
pub guid: [u8; 16],
pub kind: i32,
pub path: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Decoded {
pub value: Value,
pub rest: usize,
}
impl Decoded {
pub fn covered(&self, class_id: i32) -> bool {
self.rest == 0 || class_id == MONO_BEHAVIOUR
}
}
#[derive(Clone, Debug)]
pub struct SerializedFile {
pub version: u32,
pub unity_version: String,
pub unity: UnityVersion,
pub target_platform: i32,
pub big_endian: bool,
pub types: Vec<SerializedType>,
pub objects: Vec<ObjectInfo>,
pub script_types: Vec<(i32, i64)>,
pub externals: Vec<External>,
pub ref_types: Vec<SerializedType>,
pub user_information: String,
data: Backing,
by_path_id: BTreeMap<i64, usize>,
}
#[derive(Clone, Debug)]
enum Backing {
Memory(Vec<u8>),
Node { bundle: Arc<Bundle>, node: usize },
}
fn hash(r: &mut Reader) -> Result<[u8; 16]> {
Ok(r.take(16)?.try_into().expect("16 bytes"))
}
fn count(r: &mut Reader, what: &str) -> Result<usize> {
let n = r.i32()?;
let n = usize::try_from(n).map_err(|_| invalid(format!("negative {what} count {n}")))?;
if n > r.remaining() {
return Err(invalid(format!("{what} count {n} larger than the metadata")));
}
Ok(n)
}
struct TreeContext<'a> {
common: Option<CommonStrings<'a>>,
version: UnityVersion,
strings: BTreeMap<String, Arc<str>>,
}
impl TreeContext<'_> {
fn intern(&mut self, s: &str) -> Arc<str> {
if let Some(a) = self.strings.get(s) {
return a.clone();
}
let a: Arc<str> = Arc::from(s);
self.strings.insert(s.to_owned(), a.clone());
a
}
fn string(&mut self, buffer: &[u8], offset: u32) -> Result<Arc<str>> {
if offset & COMMON_STRING != 0 {
let at = offset & !COMMON_STRING;
let common = self.common.ok_or_else(|| {
Error::Unsupported(format!(
"a type tree names common string {at}: read the file with the type tree package of Unity {}",
self.version
))
})?;
let s = common.get(at).ok_or_else(|| {
invalid(format!(
"a type tree names common string {at}, which the table of Unity {} lacks",
self.version
))
})?;
return Ok(self.intern(s));
}
let rest = usize::try_from(offset).ok().and_then(|o| buffer.get(o..));
let bytes = rest
.and_then(|r| r.iter().position(|&b| b == 0).and_then(|n| r.get(..n)))
.ok_or_else(|| {
invalid(format!(
"a type tree names string {offset}, outside its {} byte buffer",
buffer.len()
))
})?;
let s = std::str::from_utf8(bytes)
.map_err(|_| invalid(format!("type tree string {offset} is not UTF-8")))?;
Ok(self.intern(s))
}
}
const COMMON_STRING: u32 = 0x8000_0000;
const TREE_NODE_BYTES: usize = 32;
fn read_tree(r: &mut Reader, context: &mut TreeContext) -> Result<Arc<TypeTree>> {
let nodes = r.i32()?;
let buffer_size = r.i32()?;
let nodes = usize::try_from(nodes)
.ok()
.filter(|&n| n > 0 && n <= r.remaining() / TREE_NODE_BYTES)
.ok_or_else(|| invalid(format!("a type tree of {nodes} nodes in {} bytes", r.remaining())))?;
let buffer_size = usize::try_from(buffer_size)
.map_err(|_| invalid(format!("a type tree string buffer of {buffer_size} bytes")))?;
let raw = r.take(nodes.saturating_mul(TREE_NODE_BYTES))?;
let buffer = r.take(buffer_size)?;
let mut nodes: Vec<Node> = Vec::with_capacity(nodes);
let mut open: Vec<usize> = Vec::new();
let mut n = Reader::new(raw, r.big_endian);
while n.remaining() > 0 {
let version = n.i16()?;
let level = usize::from(n.u8()?);
let _type_flags = n.u8()?;
let (type_offset, name_offset) = (n.u32()?, n.u32()?);
let byte_size = n.i32()?;
let _index = n.i32()?;
let meta_flag = n.u32()?;
let _reference_hash = n.u64()?;
if level > open.len() || (level == 0 && !nodes.is_empty()) {
return Err(invalid(format!(
"type tree node {} is at level {level}, below {} open levels",
nodes.len(),
open.len()
)));
}
open.truncate(level);
let at = nodes.len();
if let Some(&parent) = open.last() {
nodes
.get_mut(parent)
.ok_or_else(|| invalid("a type tree node went missing"))?
.children
.push(at);
}
open.push(at);
nodes.push(Node {
ty: context.string(buffer, type_offset)?,
name: context.string(buffer, name_offset)?,
byte_size,
version: i32::from(version),
meta_flag,
children: Vec::new(),
});
}
Ok(Arc::new(TypeTree::new(nodes)?))
}
fn serialized_type(
r: &mut Reader,
reference: bool,
trees: Option<&mut TreeContext>,
) -> Result<SerializedType> {
let class_id = r.i32()?;
let stripped = r.bool()?;
let script_type_index = r.i16()?;
let script_id = if (reference && script_type_index >= 0) || (!reference && class_id == MONO_BEHAVIOUR) {
Some(hash(r)?)
} else {
None
};
let old_type_hash = hash(r)?;
let (mut tree, mut dependencies, mut reference_class) = (None, Vec::new(), None);
if let Some(context) = trees {
tree = Some(read_tree(r, context)?);
if reference {
reference_class = Some(ReferenceClass {
class: r.cstr()?,
namespace: r.cstr()?,
assembly: r.cstr()?,
});
} else {
let n = count(r, "type dependency")?;
dependencies = (0..n).map(|_| r.i32()).collect::<Result<Vec<_>>>()?;
}
}
Ok(SerializedType {
class_id,
stripped,
script_type_index,
script_id,
old_type_hash,
tree,
dependencies,
reference: reference_class,
})
}
struct Header {
version: u32,
big_endian: bool,
metadata_start: usize,
metadata_size: usize,
file_size: u64,
data_offset: u64,
head_len: u64,
}
impl Header {
fn of(head: &[u8]) -> Result<Header> {
let mut h = Reader::new(head, true);
let legacy_metadata = h.u32()?;
let legacy_size = h.u32()?;
let version = h.u32()?;
let legacy_offset = h.u32()?;
if !VERSIONS.contains(&version) {
return Err(Error::Unsupported(format!(
"SerializedFile version {version} (read: {}..={})",
VERSIONS.start(),
VERSIONS.end()
)));
}
let big_endian = h.bool()?;
h.take(3)?;
let (metadata_size, file_size, data_offset) = if version == 22 {
let metadata_size = h.u32()?;
let file_size = h.u64()?;
let data_offset = h.u64()?;
let _reserved = h.u64()?;
(metadata_size, file_size, data_offset)
} else {
(legacy_metadata, u64::from(legacy_size), u64::from(legacy_offset))
};
let metadata_size =
usize::try_from(metadata_size).map_err(|_| invalid("metadata size overflows its byte range"))?;
let head_len =
u64::try_from(h.pos.saturating_add(metadata_size)).map_err(|_| invalid("metadata size"))?;
Ok(Header {
version,
big_endian,
metadata_start: h.pos,
metadata_size,
file_size,
data_offset,
head_len,
})
}
}
impl SerializedFile {
pub fn looks_like(bytes: &[u8]) -> bool {
let be = |at: usize| -> Option<u32> {
Some(u32::from_be_bytes(
bytes.get(at..at.checked_add(4)?)?.try_into().ok()?,
))
};
let Ok(len) = u64::try_from(bytes.len()) else {
return false;
};
let check = || -> Option<bool> {
Some(match be(8)? {
21 => {
let end = 20u64.checked_add(u64::from(be(0)?))?;
let (size, offset) = (u64::from(be(4)?), u64::from(be(12)?));
be(0)? > 0 && size == len && end <= offset && offset <= size
}
22 => len >= 48 && be(0)? == 0,
_ => false,
})
};
check().unwrap_or(false)
}
pub fn container_version(bytes: &[u8]) -> Option<u32> {
let be = |at: usize| -> Option<u32> {
Some(u32::from_be_bytes(
bytes.get(at..at.checked_add(4)?)?.try_into().ok()?,
))
};
let len = u64::try_from(bytes.len()).ok()?;
let version = be(8)?;
if VERSIONS.contains(&version) {
return Some(version);
}
let consistent = if version < *VERSIONS.start() {
version > 0 && u64::from(be(4)?) == len && u64::from(be(12)?) <= len
} else {
be(0)? == 0
&& be(4)? == 0
&& be(12)? == 0
&& u64::from_be_bytes(bytes.get(24..32)?.try_into().ok()?) == len
};
consistent.then_some(version)
}
pub fn parse(data: Vec<u8>) -> Result<Self> {
Self::parse_with(data, None)
}
pub fn parse_with(data: Vec<u8>, package: Option<&TypeTreePackage>) -> Result<Self> {
let size = u64::try_from(data.len()).map_err(|_| invalid("file past 2^64 bytes"))?;
let mut file = Self::parse_head(&data, size, package)?;
file.data = Backing::Memory(data);
Ok(file)
}
pub fn from_bundle(bundle: &Arc<Bundle>, path: &str, package: Option<&TypeTreePackage>) -> Result<Self> {
let (index, node) = bundle
.nodes()
.iter()
.enumerate()
.find(|(_, n)| n.path == path)
.ok_or_else(|| invalid(format!("{}: no node {path:?}", bundle.path().display())))?;
let named = |e: Error| match e {
Error::Invalid(m) => invalid(format!("{}: {path}: {m}", bundle.path().display())),
Error::Unsupported(m) => Error::Unsupported(format!("{}: {path}: {m}", bundle.path().display())),
other => other,
};
let first = bundle
.read(
node,
0,
usize::try_from(node.size.min(Self::HEADER_BYTES)).unwrap_or(0),
)
.map_err(named)?;
let head_len = Header::of(&first).map_err(named)?.head_len;
let head = bundle
.read(
node,
0,
usize::try_from(head_len.min(node.size)).map_err(|_| invalid("metadata size"))?,
)
.map_err(named)?;
let mut file = Self::parse_head(&head, node.size, package).map_err(named)?;
file.data = Backing::Node {
bundle: bundle.clone(),
node: index,
};
Ok(file)
}
const HEADER_BYTES: u64 = 48;
fn parse_head(head: &[u8], file_size: u64, package: Option<&TypeTreePackage>) -> Result<Self> {
let Header {
version,
big_endian,
metadata_start,
metadata_size,
file_size: stated_size,
data_offset,
..
} = Header::of(head)?;
if stated_size != file_size {
return Err(invalid(format!(
"header file size {stated_size}, file has {file_size} bytes"
)));
}
let meta_end = metadata_start
.checked_add(metadata_size)
.ok_or_else(|| invalid("metadata size overflows its byte range"))?;
if meta_end as u64 > data_offset || data_offset > file_size {
return Err(invalid(format!(
"metadata ends at {meta_end}, data at {data_offset}, file size {file_size}"
)));
}
let metadata = head.get(..meta_end).ok_or_else(|| {
invalid(format!(
"metadata ends at {meta_end}, past the {} bytes read",
head.len()
))
})?;
let mut r = Reader::new(metadata, big_endian);
r.pos = metadata_start;
let unity_version = r.cstr()?;
let unity = UnityVersion::parse(&unity_version)?;
let target_platform = r.i32()?;
let has_trees = r.bool()?;
let mut context = TreeContext {
common: package.and_then(|p| p.common_strings(unity)),
version: unity,
strings: BTreeMap::new(),
};
let types = (0..count(&mut r, "type")?)
.map(|_| serialized_type(&mut r, false, has_trees.then_some(&mut context)))
.collect::<Result<Vec<_>>>()?;
let mut objects = Vec::new();
for _ in 0..count(&mut r, "object")? {
r.align(4)?;
let path_id = r.i64()?;
let start = if version == 21 {
i64::from(r.u32()?)
} else {
r.i64()?
};
let size = r.u32()?;
let type_index = r.u32()?;
let ty = types.get(type_index as usize).ok_or_else(|| {
invalid(format!(
"object {path_id}: type index {type_index} of {}",
types.len()
))
})?;
let offset = u64::try_from(start)
.ok()
.and_then(|s| s.checked_add(data_offset))
.filter(|&o| o.checked_add(u64::from(size)).is_some_and(|end| end <= file_size))
.ok_or_else(|| invalid(format!("object {path_id}: bytes {start}+{size} outside the file")))?;
objects.push(ObjectInfo {
path_id,
offset,
size,
type_index,
class_id: ty.class_id,
});
}
let mut script_types = Vec::new();
for _ in 0..count(&mut r, "script")? {
let file = r.i32()?;
r.align(4)?;
script_types.push((file, r.i64()?));
}
let mut externals = Vec::new();
for _ in 0..count(&mut r, "external")? {
let _empty = r.cstr()?;
let guid = hash(&mut r)?;
let kind = r.i32()?;
let path = r.cstr()?;
externals.push(External { guid, kind, path });
}
let ref_types = (0..count(&mut r, "reference type")?)
.map(|_| serialized_type(&mut r, true, has_trees.then_some(&mut context)))
.collect::<Result<Vec<_>>>()?;
let user_information = r.cstr()?;
if r.pos != meta_end {
return Err(invalid(format!(
"metadata read to {}, header says it ends at {meta_end}",
r.pos
)));
}
let mut by_path_id = BTreeMap::new();
for (i, o) in objects.iter().enumerate() {
if by_path_id.insert(o.path_id, i).is_some() {
return Err(invalid(format!("path id {} twice", o.path_id)));
}
}
Ok(SerializedFile {
version,
unity_version,
unity,
target_platform,
big_endian,
types,
objects,
script_types,
externals,
ref_types,
user_information,
data: Backing::Memory(Vec::new()),
by_path_id,
})
}
pub fn object(&self, path_id: i64) -> Option<&ObjectInfo> {
self.by_path_id.get(&path_id).and_then(|&i| self.objects.get(i))
}
pub fn tree_of(&self, o: &ObjectInfo) -> Option<&Arc<TypeTree>> {
self.types.get(usize::try_from(o.type_index).ok()?)?.tree.as_ref()
}
pub fn bytes(&self, o: &ObjectInfo) -> Result<Cow<'_, [u8]>> {
let outside = |len: u64| {
invalid(format!(
"object {}: bytes {}+{} are outside the file's {len} bytes",
o.path_id, o.offset, o.size,
))
};
match &self.data {
Backing::Memory(data) => {
let len = u64::try_from(data.len()).unwrap_or(u64::MAX);
let start = usize::try_from(o.offset).map_err(|_| outside(len))?;
let end = start
.checked_add(usize::try_from(o.size).map_err(|_| outside(len))?)
.ok_or_else(|| outside(len))?;
data.get(start..end)
.map(Cow::Borrowed)
.ok_or_else(|| outside(len))
}
Backing::Node { bundle, node } => {
let node = bundle
.nodes()
.get(*node)
.ok_or_else(|| invalid("the file's bundle node is gone"))?;
if o.offset
.checked_add(u64::from(o.size))
.is_none_or(|end| end > node.size)
{
return Err(outside(node.size));
}
let len = usize::try_from(o.size).map_err(|_| outside(node.size))?;
bundle.read(node, o.offset, len).map(Cow::Owned)
}
}
}
pub fn bundle(&self) -> Option<(&Arc<Bundle>, &crate::bundle::BundleNode)> {
match &self.data {
Backing::Memory(_) => None,
Backing::Node { bundle, node } => bundle.nodes().get(*node).map(|n| (bundle, n)),
}
}
pub fn decode_with(&self, o: &ObjectInfo, tree: &TypeTree) -> Result<Decoded> {
let bytes = self.bytes(o)?;
let (value, used) = tree
.decode(&bytes, self.big_endian)
.map_err(|e| invalid(format!("object {} (class {}): {e}", o.path_id, o.class_id)))?;
let rest = bytes.len().checked_sub(used).ok_or_else(|| {
invalid(format!(
"object {} (class {}): decoded {used} of {} bytes",
o.path_id,
o.class_id,
bytes.len()
))
})?;
Ok(Decoded { value, rest })
}
pub fn decode(&self, o: &ObjectInfo, package: &TypeTreePackage) -> Result<Decoded> {
let d = match self.tree_of(o) {
Some(tree) => self.decode_with(o, tree)?,
None => self.decode_with(o, &package.type_tree(o.class_id, self.unity)?)?,
};
if !d.covered(o.class_id) {
return Err(invalid(format!(
"object {} (class {}): {} bytes after its type tree",
o.path_id, o.class_id, d.rest
)));
}
Ok(d)
}
}