use std::collections::BTreeMap;
use crate::reader::Reader;
use crate::tpk::TypeTreePackage;
use crate::typetree::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],
}
#[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: Vec<u8>,
by_path_id: BTreeMap<i64, 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)
}
fn serialized_type(r: &mut Reader, reference: bool) -> 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)?;
Ok(SerializedType {
class_id,
stripped,
script_type_index,
script_id,
old_type_hash,
})
}
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> {
let mut h = Reader::new(&data, 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))
};
if file_size != data.len() as u64 {
return Err(invalid(format!(
"header file size {file_size}, file has {} bytes",
data.len()
)));
}
let meta_end = h
.pos
.checked_add(metadata_size as usize)
.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 = data.get(..meta_end).ok_or_else(|| {
invalid(format!(
"metadata ends at {meta_end}, past the file's {} bytes",
data.len()
))
})?;
let mut r = Reader::new(metadata, big_endian);
r.pos = h.pos;
let unity_version = r.cstr()?;
let unity = UnityVersion::parse(&unity_version)?;
let target_platform = r.i32()?;
if r.bool()? {
return Err(Error::Unsupported(
"embedded type trees (decode stripped player files with a type tree package)".into(),
));
}
let types = (0..count(&mut r, "type")?)
.map(|_| serialized_type(&mut r, false))
.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))
.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,
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 bytes(&self, o: &ObjectInfo) -> Result<&[u8]> {
let outside = || {
invalid(format!(
"object {}: bytes {}+{} are outside the file's {} bytes",
o.path_id,
o.offset,
o.size,
self.data.len()
))
};
let start = usize::try_from(o.offset).map_err(|_| outside())?;
let end = start
.checked_add(usize::try_from(o.size).map_err(|_| outside())?)
.ok_or_else(outside)?;
self.data.get(start..end).ok_or_else(outside)
}
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 tree = package.type_tree(o.class_id, self.unity)?;
let d = self.decode_with(o, &tree)?;
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)
}
}