aboutsummaryrefslogtreecommitdiff
path: root/src/object
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-03-12 22:39:58 +0100
committermetamuffin <metamuffin@disroot.org>2025-03-12 22:39:58 +0100
commit5555c8bbefb4f52f5002603eb91b6c95cbdd97e4 (patch)
tree208e88359deb3cf5f7e2a4135693f12d76ad97e1 /src/object
parent4906844cbfd2717a29b434fb7d8f90c5117fddd5 (diff)
downloadunity-tools-5555c8bbefb4f52f5002603eb91b6c95cbdd97e4.tar
unity-tools-5555c8bbefb4f52f5002603eb91b6c95cbdd97e4.tar.bz2
unity-tools-5555c8bbefb4f52f5002603eb91b6c95cbdd97e4.tar.zst
more parsing helpers
Diffstat (limited to 'src/object')
-rw-r--r--src/object/helper.rs125
-rw-r--r--src/object/mod.rs28
-rw-r--r--src/object/parser.rs92
-rw-r--r--src/object/read.rs100
4 files changed, 345 insertions, 0 deletions
diff --git a/src/object/helper.rs b/src/object/helper.rs
new file mode 100644
index 0000000..3f41703
--- /dev/null
+++ b/src/object/helper.rs
@@ -0,0 +1,125 @@
+use super::Value;
+
+impl Value {
+ pub fn class_name(&self) -> Option<&String> {
+ if let Value::Object { class, .. } = self {
+ Some(class)
+ } else {
+ None
+ }
+ }
+ pub fn as_string(self) -> Option<String> {
+ if let Value::String(s) = self {
+ Some(s)
+ } else {
+ None
+ }
+ }
+ pub fn as_i64(&self) -> Option<i64> {
+ if let Value::I64(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_i32(&self) -> Option<i32> {
+ if let Value::I32(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_u32(&self) -> Option<u32> {
+ if let Value::U32(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_u8(&self) -> Option<u8> {
+ if let Value::U8(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_u64(&self) -> Option<u64> {
+ match self {
+ Self::U64(x) => Some(*x),
+ Self::U32(x) => Some(*x as u64),
+ _ => None,
+ }
+ }
+ pub fn as_f32(&self) -> Option<f32> {
+ if let Value::F32(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_u16(&self) -> Option<u16> {
+ if let Value::U16(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_bool(&self) -> Option<bool> {
+ if let Value::Bool(s) = self {
+ Some(*s)
+ } else {
+ None
+ }
+ }
+ pub fn as_array(self) -> Option<Vec<Value>> {
+ if let Value::Array(s) = self {
+ Some(s)
+ } else {
+ None
+ }
+ }
+ pub fn as_typeless(self) -> Option<Vec<u8>> {
+ if let Value::Typeless(s) = self {
+ Some(s)
+ } else {
+ None
+ }
+ }
+ pub fn to_json(self) -> serde_json::Value {
+ match self {
+ Value::Bool(x) => serde_json::Value::Bool(x),
+ Value::U8(x) => serde_json::Value::Number(x.into()),
+ Value::I8(x) => serde_json::Value::Number(x.into()),
+ Value::U16(x) => serde_json::Value::Number(x.into()),
+ Value::I16(x) => serde_json::Value::Number(x.into()),
+ Value::U32(x) => serde_json::Value::Number(x.into()),
+ Value::U64(x) => serde_json::Value::Number(x.into()),
+ Value::I32(x) => serde_json::Value::Number(x.into()),
+ Value::F32(x) => serde_json::Value::Number(
+ serde_json::Number::from_f64(x as f64).unwrap_or(0.into()),
+ ),
+ Value::I64(x) => serde_json::Value::Number(x.into()),
+ Value::F64(x) => serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap()),
+ Value::String(x) => serde_json::Value::String(x),
+ Value::Typeless(values) => serde_json::Value::Array(
+ values
+ .into_iter()
+ .map(|e| serde_json::Value::Number(e.into()))
+ .collect(),
+ ),
+ Value::Array(values) => {
+ serde_json::Value::Array(values.into_iter().map(Value::to_json).collect())
+ }
+ Value::Object { class, fields } => serde_json::Value::Object(
+ fields
+ .into_iter()
+ .map(|(k, v)| (k, v.to_json()))
+ .chain(Some((
+ "@class".to_string(),
+ serde_json::Value::String(class),
+ )))
+ .collect(),
+ ),
+ }
+ }
+}
diff --git a/src/object/mod.rs b/src/object/mod.rs
new file mode 100644
index 0000000..6e643de
--- /dev/null
+++ b/src/object/mod.rs
@@ -0,0 +1,28 @@
+use serde::Serialize;
+use std::collections::BTreeMap;
+
+pub mod helper;
+pub mod read;
+pub mod parser;
+
+#[derive(Debug, Clone, Serialize)]
+pub enum Value {
+ Bool(bool),
+ U8(u8),
+ I8(i8),
+ U16(u16),
+ I16(i16),
+ U32(u32),
+ I32(i32),
+ F32(f32),
+ U64(u64),
+ I64(i64),
+ F64(f64),
+ Array(Vec<Value>),
+ Object {
+ class: String,
+ fields: BTreeMap<String, Value>,
+ },
+ Typeless(Vec<u8>),
+ String(String),
+}
diff --git a/src/object/parser.rs b/src/object/parser.rs
new file mode 100644
index 0000000..5879e9a
--- /dev/null
+++ b/src/object/parser.rs
@@ -0,0 +1,92 @@
+use super::Value;
+use anyhow::{Context, Result, anyhow, bail};
+use std::collections::BTreeMap;
+
+pub trait FromValue: Sized {
+ fn from_value(v: Value) -> Result<Self>;
+}
+
+impl Value {
+ pub fn parse<T: FromValue>(self) -> Result<T> {
+ T::from_value(self)
+ }
+ pub fn as_class(self, name: &'static str) -> Result<Fields> {
+ if let Value::Object { class, fields } = self {
+ if class == name {
+ Ok(Fields {
+ class: name,
+ fields,
+ })
+ } else {
+ bail!("expected class {name} but found {class}")
+ }
+ } else {
+ bail!("expected class {name}")
+ }
+ }
+}
+
+pub struct Fields {
+ class: &'static str,
+ fields: BTreeMap<String, Value>,
+}
+
+impl Fields {
+ pub fn field<T: FromValue>(&mut self, name: &str) -> Result<T> {
+ self.fields
+ .remove(name)
+ .ok_or(anyhow!("expected {name} field in {}", self.class))?
+ .parse()
+ .context(anyhow!("in {}.{name} field", self.class))
+ }
+ pub fn remove(&mut self, key: &str) -> Option<Value> {
+ self.fields.remove(key)
+ }
+}
+
+impl FromValue for u8 {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_u8().ok_or(anyhow!("expected u8"))
+ }
+}
+impl FromValue for u16 {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_u16().ok_or(anyhow!("expected u16"))
+ }
+}
+impl FromValue for f32 {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_f32().ok_or(anyhow!("expected f32"))
+ }
+}
+impl FromValue for u32 {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_u32().ok_or(anyhow!("expected u32"))
+ }
+}
+impl FromValue for i32 {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_i32().ok_or(anyhow!("expected i32"))
+ }
+}
+impl FromValue for u64 {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_u64().ok_or(anyhow!("expected u64"))
+ }
+}
+impl FromValue for bool {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_bool().ok_or(anyhow!("expected bool"))
+ }
+}
+impl FromValue for String {
+ fn from_value(v: Value) -> anyhow::Result<Self> {
+ v.as_string().ok_or(anyhow!("expected string"))
+ }
+}
+
+impl Value {
+ pub fn as_vector(self) -> Option<Vec<Value>> {
+ self.as_class("vector").ok()?.remove("Array")?.as_array()
+ }
+}
diff --git a/src/object/read.rs b/src/object/read.rs
new file mode 100644
index 0000000..5adbe3f
--- /dev/null
+++ b/src/object/read.rs
@@ -0,0 +1,100 @@
+use super::Value;
+use crate::helper::{AlignExt, Endianness, ReadExt};
+use crate::serialized_file::TypeTreeNode;
+use anyhow::{Result, bail};
+use log::trace;
+use std::io::Seek;
+use std::{collections::BTreeMap, io::Read};
+
+pub fn read_value(
+ ty: &TypeTreeNode,
+ e: Endianness,
+ data: &mut (impl Read + Seek),
+) -> Result<Value> {
+ let mut align = false;
+ let pos_before = data.stream_position()?;
+ let r = match ty.type_string.as_str() {
+ "char" => {
+ assert_eq!(ty.byte_size, 1);
+ Ok(Value::U8(data.read_u8()?))
+ }
+ "Type*" => Ok(Value::U32(data.read_u32(e)?)),
+ "int" => Ok(Value::I32(data.read_i32(e)?)),
+ "unsigned int" => Ok(Value::U32(data.read_u32(e)?)),
+ "UInt8" => Ok(Value::U8(data.read_u8()?)),
+ "UInt16" => Ok(Value::U16(data.read_u16(e)?)),
+ "UInt32" => Ok(Value::U32(data.read_u32(e)?)),
+ "UInt64" => Ok(Value::U64(data.read_u64(e)?)),
+ "SInt8" => Ok(Value::I8(data.read_i8()?)),
+ "SInt16" => Ok(Value::I16(data.read_i16(e)?)),
+ "SInt32" => Ok(Value::I32(data.read_i32(e)?)),
+ "SInt64" => Ok(Value::I64(data.read_i64(e)?)),
+ "bool" => Ok(Value::Bool(data.read_u8()? != 0)),
+ "float" => {
+ data.align(4)?;
+ Ok(Value::F32(data.read_f32(e)?))
+ }
+ "double" => {
+ data.align(4)?;
+ Ok(Value::F64(data.read_f64(e)?))
+ }
+ "string" => {
+ let Value::Array(arr) = read_value(&ty.children[0], e, data)? else {
+ unreachable!()
+ };
+ let bytes = arr
+ .into_iter()
+ .map(|e| match e {
+ Value::U8(x) => x,
+ _ => unreachable!(),
+ })
+ .collect::<Vec<_>>();
+ Ok(Value::String(String::from_utf8(bytes)?))
+ }
+ "Array" => {
+ align |= ty.children[0].post_align();
+ assert_eq!(ty.byte_size, -1);
+ let Value::I32(size) = read_value(&ty.children[0], e, data)? else {
+ unreachable!()
+ };
+ trace!("array of size {size}");
+ let mut elems = Vec::new();
+ for _ in 0..size {
+ elems.push(read_value(&ty.children[1], e, data)?);
+ }
+ Ok(Value::Array(elems))
+ }
+ "TypelessData" => {
+ let len = data.read_u32(e)?;
+ let mut buf = vec![0u8; len as usize];
+ data.read_exact(&mut buf)?;
+ Ok(Value::Typeless(buf))
+ }
+ _ => {
+ if ty.children.is_empty() && ty.byte_size != 0 {
+ todo!("need type {:?}", ty.type_string);
+ }
+ let mut fields = BTreeMap::new();
+ for c in &ty.children {
+ fields.insert(c.name_string.clone(), read_value(&c, e, data)?);
+ }
+ Ok(Value::Object {
+ fields,
+ class: ty.type_string.clone(),
+ })
+ }
+ };
+ let pos_after = data.stream_position()?;
+ if ty.byte_size != -1 && pos_after - pos_before < ty.byte_size as u64 {
+ bail!(
+ "did not read enough data ({} expected, {} actual)",
+ ty.byte_size,
+ pos_after - pos_before
+ );
+ }
+ if align || ty.post_align() {
+ trace!("post align");
+ data.align(4)?;
+ }
+ r
+}