aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-03-15 15:18:40 +0100
committermetamuffin <metamuffin@disroot.org>2025-03-15 15:18:40 +0100
commitd836e24357b81496c61f3cc9195ba36758523578 (patch)
tree0028aee5a453cc761dd39e92430a35c55147537f
parent07fc3656274117c211ca0d6a54926d390a4d9b68 (diff)
downloadunity-tools-d836e24357b81496c61f3cc9195ba36758523578.tar
unity-tools-d836e24357b81496c61f3cc9195ba36758523578.tar.bz2
unity-tools-d836e24357b81496c61f3cc9195ba36758523578.tar.zst
more abstraction around unityfs to read multiple files from a single reader
-rw-r--r--exporter/src/bin/debug.rs2
-rw-r--r--exporter/src/bin/gltf.rs12
-rw-r--r--exporter/src/bin/json.rs2
-rw-r--r--exporter/src/bin/material_stats.rs52
-rw-r--r--exporter/src/bin/meshes.rs5
-rw-r--r--exporter/src/bin/probe.rs6
-rw-r--r--exporter/src/bin/textures.rs7
-rw-r--r--exporter/src/bin/typegraph.rs2
-rw-r--r--exporter/src/bin/yaml.rs2
-rw-r--r--src/classes/material.rs9
-rw-r--r--src/classes/mesh_renderer.rs6
-rw-r--r--src/classes/mod.rs1
-rw-r--r--src/classes/pptr.rs35
-rw-r--r--src/classes/shader.rs42
-rw-r--r--src/classes/streaminginfo.rs1
-rw-r--r--src/serialized_file.rs13
-rw-r--r--src/unityfs.rs327
-rw-r--r--src/unityfs/block_reader.rs99
-rw-r--r--src/unityfs/header.rs175
-rw-r--r--src/unityfs/mod.rs95
-rw-r--r--src/unityfs/multi_reader.rs50
21 files changed, 584 insertions, 359 deletions
diff --git a/exporter/src/bin/debug.rs b/exporter/src/bin/debug.rs
index 92713ba..867f6ea 100644
--- a/exporter/src/bin/debug.rs
+++ b/exporter/src/bin/debug.rs
@@ -4,7 +4,7 @@ use unity_tools::{serialized_file::SerializedFile, unityfs::UnityFS};
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
let file = BufReader::new(File::open(args().nth(1).unwrap())?);
- let mut fs = UnityFS::open(file)?;
+ let fs = UnityFS::open(file)?;
let node = fs.find_main_file().unwrap().to_owned();
let mut cab = fs.read(&node)?;
diff --git a/exporter/src/bin/gltf.rs b/exporter/src/bin/gltf.rs
index 86f48b4..544bd6f 100644
--- a/exporter/src/bin/gltf.rs
+++ b/exporter/src/bin/gltf.rs
@@ -23,8 +23,8 @@ use unity_tools::{
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
- let file = || BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
- let mut fs = UnityFS::open(file())?;
+ let file = BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
+ let fs = UnityFS::open(file)?;
let cabfile = fs
.find_main_file()
@@ -54,7 +54,7 @@ fn main() -> anyhow::Result<()> {
let go = file.read_object(ob)?.parse::<GameObject>()?;
let mut global_transform = Affine3A::default();
for comp in go.components {
- let ob = comp.load(&mut file)?;
+ let ob = comp.load(&mut file, None)?;
match ob.class_name().unwrap().as_str() {
"Transform" => {
let mut tr = ob.parse::<Transform>()?;
@@ -68,7 +68,7 @@ fn main() -> anyhow::Result<()> {
if tr.father.is_null() {
break;
} else {
- tr = tr.father.load(&mut file)?;
+ tr = tr.father.load(&mut file, None)?;
}
}
global_transform =
@@ -79,7 +79,7 @@ fn main() -> anyhow::Result<()> {
let mesh = import_mesh(
&mut root,
&mut buffer,
- smr.mesh_renderer.mesh.load(&mut file)?,
+ smr.mesh_renderer.mesh.load(&mut file, None)?,
)?;
nodes.push(root.push(Node {
mesh: Some(mesh),
@@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> {
bin: Some(Cow::Owned(buffer)),
json: Cow::Owned(json_string.into_bytes()),
};
- let writer = std::fs::File::create("triangle.glb").expect("I/O error");
+ let writer = std::fs::File::create("/tmp/a.glb").expect("I/O error");
glb.to_writer(writer).expect("glTF binary output error");
Ok(())
diff --git a/exporter/src/bin/json.rs b/exporter/src/bin/json.rs
index dd83de6..e03642b 100644
--- a/exporter/src/bin/json.rs
+++ b/exporter/src/bin/json.rs
@@ -8,7 +8,7 @@ use unity_tools::{serialized_file::SerializedFile, unityfs::UnityFS};
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
let file = BufReader::new(File::open(args().nth(1).unwrap())?);
- let mut fs = UnityFS::open(file)?;
+ let fs = UnityFS::open(file)?;
let filter = args().nth(2);
let pretty = var("PRETTY").is_ok();
diff --git a/exporter/src/bin/material_stats.rs b/exporter/src/bin/material_stats.rs
new file mode 100644
index 0000000..3eb4dc0
--- /dev/null
+++ b/exporter/src/bin/material_stats.rs
@@ -0,0 +1,52 @@
+use anyhow::anyhow;
+use std::{env::args, fs::File, io::BufReader};
+use unity_tools::{classes::material::Material, serialized_file::SerializedFile, unityfs::UnityFS};
+
+fn main() -> anyhow::Result<()> {
+ env_logger::init_from_env("LOG");
+ let file = BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
+ let fs = UnityFS::open(file)?;
+
+ let mode = args().nth(2).unwrap();
+
+ let cabfile = fs
+ .find_main_file()
+ .ok_or(anyhow!("no CAB file found"))?
+ .to_owned();
+
+ let cab = fs.read(&cabfile)?;
+ let mut file = SerializedFile::read(cab)?;
+ let mut sharedassets = file
+ .find_fs_shared_assets(&fs)
+ .map(|n| {
+ let f = fs.read(&n)?;
+ let f = SerializedFile::read(f)?;
+ Ok::<_, anyhow::Error>(f)
+ })
+ .transpose()?;
+
+ for ob in file.objects.clone() {
+ if file.get_object_type_tree(&ob)?.type_string != "Material" {
+ continue;
+ }
+ let mat = file.read_object(ob)?.parse::<Material>()?;
+
+ match mode.as_str() {
+ "material" => {
+ println!("{}", mat.name)
+ }
+ "shader" => {
+ println!(
+ "{}",
+ mat.shader
+ .load(&mut file, sharedassets.as_mut())?
+ .parsed
+ .name
+ )
+ }
+ x => panic!("unknown mode {x:?}"),
+ }
+ }
+
+ Ok(())
+}
diff --git a/exporter/src/bin/meshes.rs b/exporter/src/bin/meshes.rs
index e3758c4..db8f2f8 100644
--- a/exporter/src/bin/meshes.rs
+++ b/exporter/src/bin/meshes.rs
@@ -14,13 +14,14 @@ use unity_tools::{
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
- let file = || BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
- let mut fs = UnityFS::open(file())?;
+ let file = BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
+ let fs = UnityFS::open(file)?;
let mut i = 0;
create_dir_all("/tmp/a").unwrap();
let cabfile = fs
+ .header
.nodes()
.iter()
.find(|n| !n.name.ends_with(".resource") && !n.name.ends_with(".resS"))
diff --git a/exporter/src/bin/probe.rs b/exporter/src/bin/probe.rs
index feca633..043444d 100644
--- a/exporter/src/bin/probe.rs
+++ b/exporter/src/bin/probe.rs
@@ -4,16 +4,16 @@ use unity_tools::{serialized_file::SerializedFileHeader, unityfs::UnityFS};
fn main() -> Result<()> {
let file = BufReader::new(File::open(args().nth(1).unwrap())?);
- let mut fs = UnityFS::open(file)?;
+ let fs = UnityFS::open(file)?;
let node = fs.find_main_file().unwrap().to_owned();
let mut cab = fs.read(&node)?;
let ch = SerializedFileHeader::read(&mut cab)?;
- if fs.unity_version.is_ascii() && ch.generator_version.is_ascii() && ch.format < 100 {
+ if fs.header.unity_version.is_ascii() && ch.generator_version.is_ascii() && ch.format < 100 {
println!(
"{}\t{}\t{}\t{}",
- fs.file_version, fs.unity_version, ch.format, ch.generator_version
+ fs.header.file_version, fs.header.unity_version, ch.format, ch.generator_version
);
}
diff --git a/exporter/src/bin/textures.rs b/exporter/src/bin/textures.rs
index 2e077fe..a3b6cec 100644
--- a/exporter/src/bin/textures.rs
+++ b/exporter/src/bin/textures.rs
@@ -12,9 +12,8 @@ use unity_tools::{
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
- let file = || BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
- let mut fs = UnityFS::open(file())?;
- let mut fs2 = UnityFS::open(file())?;
+ let file = BufReader::new(File::open(args().nth(1).unwrap()).unwrap());
+ let mut fs = UnityFS::open(file)?;
let mut i = 0;
create_dir_all("/tmp/a").unwrap();
@@ -33,7 +32,7 @@ fn main() -> anyhow::Result<()> {
let value = file.read_object(ob)?;
let mut texture = Texture2D::from_value(value)?;
if texture.image_data.is_empty() {
- texture.image_data = texture.stream_data.read(&mut fs2)?;
+ texture.image_data = texture.stream_data.read(&mut fs)?;
}
let path = format!(
"/tmp/a/{}_{i}.png",
diff --git a/exporter/src/bin/typegraph.rs b/exporter/src/bin/typegraph.rs
index ea55e05..0d6568a 100644
--- a/exporter/src/bin/typegraph.rs
+++ b/exporter/src/bin/typegraph.rs
@@ -13,7 +13,7 @@ use unity_tools::{
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
let file = BufReader::new(File::open(args().nth(1).unwrap())?);
- let mut fs = UnityFS::open(file)?;
+ let fs = UnityFS::open(file)?;
let filter_prims = args().any(|a| a == "no_primitives");
let mut edges = BTreeSet::new();
diff --git a/exporter/src/bin/yaml.rs b/exporter/src/bin/yaml.rs
index 4ef8933..bc9e57e 100644
--- a/exporter/src/bin/yaml.rs
+++ b/exporter/src/bin/yaml.rs
@@ -9,7 +9,7 @@ use unity_tools::{classes::HValue, serialized_file::SerializedFile, unityfs::Uni
fn main() -> anyhow::Result<()> {
env_logger::init_from_env("LOG");
let file = BufReader::new(File::open(args().nth(1).unwrap())?);
- let mut fs = UnityFS::open(file)?;
+ let fs = UnityFS::open(file)?;
let filter = args().nth(2);
let node = fs.find_main_file().unwrap().to_owned();
diff --git a/src/classes/material.rs b/src/classes/material.rs
index 354e319..9124b0b 100644
--- a/src/classes/material.rs
+++ b/src/classes/material.rs
@@ -1,4 +1,4 @@
-use super::{pptr::PPtr, texture2d::Texture2D, vectors::ColorRGBA};
+use super::{pptr::PPtr, shader::Shader, texture2d::Texture2D, vectors::ColorRGBA};
use crate::object::{Value, parser::FromValue};
use glam::Vec2;
use serde::Serialize;
@@ -29,8 +29,11 @@ pub struct UnityTexEnv {
pub texture: PPtr<Texture2D>,
}
-#[derive(Debug, Serialize)]
-pub struct Shader {}
+impl UnityPropertySheet {
+ pub fn textures(&self) -> impl Iterator<Item = (&String, &UnityTexEnv)> {
+ self.textures.iter().filter(|(_, v)| !v.texture.is_null())
+ }
+}
impl FromValue for Material {
fn from_value(v: Value) -> anyhow::Result<Self> {
diff --git a/src/classes/mesh_renderer.rs b/src/classes/mesh_renderer.rs
index 94d6fd7..ac69483 100644
--- a/src/classes/mesh_renderer.rs
+++ b/src/classes/mesh_renderer.rs
@@ -1,4 +1,6 @@
-use super::{gameobject::GameObject, mesh::Mesh, pptr::PPtr, transform::Transform};
+use super::{
+ gameobject::GameObject, material::Material, mesh::Mesh, pptr::PPtr, transform::Transform,
+};
use crate::object::{
Value,
parser::{Fields, FromValue},
@@ -10,7 +12,7 @@ pub struct MeshRenderer {
pub mesh: PPtr<Mesh>,
pub cast_shadows: u8,
pub game_object: PPtr<GameObject>,
- pub materials: Vec<PPtr>,
+ pub materials: Vec<PPtr<Material>>,
}
pub struct SkinnedMeshRenderer {
diff --git a/src/classes/mod.rs b/src/classes/mod.rs
index b4fbe91..165d41e 100644
--- a/src/classes/mod.rs
+++ b/src/classes/mod.rs
@@ -8,6 +8,7 @@ pub mod texture2d;
pub mod transform;
pub mod vectors;
pub mod mesh_renderer;
+pub mod shader;
use crate::object::{Value, parser::FromValue};
use anyhow::Result;
diff --git a/src/classes/pptr.rs b/src/classes/pptr.rs
index 30f37ad..9b54cbb 100644
--- a/src/classes/pptr.rs
+++ b/src/classes/pptr.rs
@@ -54,17 +54,36 @@ impl<T: FromValue> PPtr<T> {
pub fn is_null(&self) -> bool {
self.path_id == 0 && self.file_id == 0
}
- pub fn load(&self, file: &mut SerializedFile<impl Read + Seek>) -> Result<T> {
+ pub fn load(
+ &self,
+ file: &mut SerializedFile<impl Read + Seek>,
+ shared_assets: Option<&mut SerializedFile<impl Read + Seek>>,
+ ) -> Result<T> {
debug!(
"loading PPtr<{}> file_id={} path_id={}",
self.class, self.file_id, self.path_id
);
- let ob = file
- .objects
- .iter()
- .find(|o| o.path_id == self.path_id)
- .ok_or(anyhow!("object with path id {} not found", self.path_id))?
- .to_owned();
- file.read_object(ob)?.parse()
+ match self.file_id {
+ 0 => {
+ let ob = file
+ .objects
+ .iter()
+ .find(|o| o.path_id == self.path_id)
+ .ok_or(anyhow!("object with path id {} not found", self.path_id))?
+ .to_owned();
+ file.read_object(ob)?.parse()
+ }
+ 1 => {
+ let file = shared_assets.unwrap();
+ let ob = file
+ .objects
+ .iter()
+ .find(|o| o.path_id == self.path_id)
+ .ok_or(anyhow!("object with path id {} not found", self.path_id))?
+ .to_owned();
+ file.read_object(ob)?.parse()
+ }
+ _ => unimplemented!(),
+ }
}
}
diff --git a/src/classes/shader.rs b/src/classes/shader.rs
new file mode 100644
index 0000000..86f32b8
--- /dev/null
+++ b/src/classes/shader.rs
@@ -0,0 +1,42 @@
+use super::pptr::PPtr;
+use crate::object::{Value, parser::FromValue};
+use anyhow::Result;
+use serde::Serialize;
+
+#[derive(Debug, Serialize)]
+pub struct Shader {
+ pub dependencies: Vec<PPtr<Shader>>,
+ pub name: String,
+ pub parsed: SerializedShader,
+}
+
+#[derive(Debug, Serialize)]
+pub struct SerializedShader {
+ pub name: String,
+}
+
+impl FromValue for Shader {
+ fn from_value(v: Value) -> Result<Self> {
+ let mut fields = v.as_class("Shader")?;
+ Ok(Self {
+ name: fields.field("m_Name")?,
+ parsed: fields.field("m_ParsedForm")?,
+ dependencies: fields
+ .remove("m_Dependencies")
+ .unwrap()
+ .as_vector()
+ .unwrap()
+ .into_iter()
+ .map(|e| e.parse().unwrap())
+ .collect(),
+ })
+ }
+}
+impl FromValue for SerializedShader {
+ fn from_value(v: Value) -> Result<Self> {
+ let mut fields = v.as_class("SerializedShader")?;
+ Ok(Self {
+ name: fields.field("m_Name")?,
+ })
+ }
+}
diff --git a/src/classes/streaminginfo.rs b/src/classes/streaminginfo.rs
index 83b9f20..21029f4 100644
--- a/src/classes/streaminginfo.rs
+++ b/src/classes/streaminginfo.rs
@@ -31,6 +31,7 @@ impl StreamingInfo {
bail!("StreamingInfo path does not start on archive:")
}
let nodeinfo = fs
+ .header
.nodes()
.iter()
.find(|n| self.path.ends_with(&n.name))
diff --git a/src/serialized_file.rs b/src/serialized_file.rs
index b3c1e3c..a6514ee 100644
--- a/src/serialized_file.rs
+++ b/src/serialized_file.rs
@@ -2,6 +2,7 @@ use crate::{
common_strings::COMMON_STRINGS,
helper::{AlignExt, Endianness, ReadExt},
object::Value,
+ unityfs::{UnityFS, header::NodeInfo},
};
use anyhow::{Result, anyhow, bail};
use log::{debug, info, trace, warn};
@@ -307,6 +308,18 @@ impl<T: Read + Seek> SerializedFile<T> {
})
}
+ pub fn find_fs_shared_assets(&self, fs: &UnityFS<impl Read + Seek>) -> Option<NodeInfo> {
+ let s = self
+ .externals
+ .iter()
+ .find(|e| e.path_name.starts_with("archive:"))?;
+ fs.header
+ .nodes()
+ .iter()
+ .find(|n| n.name.ends_with(&s.path_name))
+ .cloned()
+ }
+
pub fn get_object_type_tree(&self, ob: &ObjectInfo) -> Result<&'_ TypeTreeNode> {
let r#type = if ob.type_id < 0 {
unimplemented!()
diff --git a/src/unityfs.rs b/src/unityfs.rs
deleted file mode 100644
index 05ad922..0000000
--- a/src/unityfs.rs
+++ /dev/null
@@ -1,327 +0,0 @@
-use crate::helper::{AlignExt, ReadExt};
-use anyhow::{Result, anyhow, bail};
-use humansize::DECIMAL;
-use log::{debug, info, trace};
-use std::io::{Cursor, Error, ErrorKind, Read, Seek, SeekFrom};
-
-pub struct UnityFS<T> {
- nodes: Vec<NodeInfo>,
- reader: BlocksReader<T>,
- pub file_version: u32,
- pub player_version: String,
- pub unity_version: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct NodeInfo {
- pub name: String,
- pub size: u64,
- offset: u64,
- _status: u32,
-}
-
-struct BlockInfo {
- comp_size: u32,
- decomp_size: u32,
- comp_scheme: CompressionScheme,
-}
-
-pub struct NodeReader<'a, T> {
- inner: &'a mut BlocksReader<T>,
- position: u64,
- offset: u64,
- size: u64,
-}
-
-impl<T: Read + Seek> UnityFS<T> {
- pub fn open(mut file: T) -> Result<Self> {
- let signature = file.read_cstr()?;
- if signature.as_str() != "UnityFS" {
- bail!("unknown signature {signature:?}")
- }
-
- let file_version = file.read_u32_be()?;
- let player_version = file.read_cstr()?;
- let unity_version = file.read_cstr()?;
- let size = file.read_u64_be()?;
- let blockindex_comp_size = file.read_u32_be()?;
- let blockindex_decomp_size = file.read_u32_be()?;
- let flags = file.read_u32_be()?;
-
- let meta_comp_scheme = CompressionScheme::from_flag_num(flags as u8).ok_or(anyhow!(
- "unknown block compression 0x{:02x}",
- (flags & 0x3f) as u8
- ))?;
- let blockindex_eof = flags & 0x80 != 0;
- let blockindex_has_directory = flags & 0x40 != 0;
- let blockindex_need_padding = flags & 0x200 != 0;
-
- info!("File Version: {file_version:?}");
- info!("Player Version: {player_version:?}");
- info!("Unity Version: {unity_version:?}");
- debug!("size={size:?}");
- debug!("meta_comp_size={blockindex_comp_size:?}");
- debug!("meta_decomp_size={blockindex_decomp_size:?}");
- debug!("flags={flags:?}");
- debug!("meta_comp_scheme={meta_comp_scheme:?}");
- debug!("blockindex_eof={blockindex_eof:?}");
- debug!("blockindex_has_directory={blockindex_has_directory:?}");
- debug!("blockindex_need_padding={blockindex_need_padding:?}");
-
- let mut blockindex = {
- let restore_position = if blockindex_eof {
- let pos = file.stream_position()?;
- file.seek(SeekFrom::End(-(blockindex_comp_size as i64)))?;
- Some(pos)
- } else {
- None
- };
-
- let mut blockindex = vec![0u8; blockindex_comp_size as usize];
- file.read_exact(&mut blockindex)?;
-
- if let Some(pos) = restore_position {
- file.seek(SeekFrom::Start(pos))?;
- }
- let blockindex =
- meta_comp_scheme.decompress(blockindex, blockindex_decomp_size as usize)?;
- Cursor::new(blockindex)
- };
-
- file.align(16)?;
-
- blockindex.read_u128_be()?;
-
- let num_blocks = blockindex.read_u32_be()?;
- info!("File has {num_blocks} blocks");
- let mut blocks = Vec::new();
- for _ in 0..num_blocks {
- let decomp_size = blockindex.read_u32_be()?;
- let comp_size = blockindex.read_u32_be()?;
- let flags = blockindex.read_u16_be()?;
- let comp_scheme = CompressionScheme::from_flag_num(flags as u8)
- .ok_or(anyhow!("unknown block compression 0x{:02x}", flags & 0x3f))?;
- blocks.push(BlockInfo {
- comp_size,
- decomp_size,
- comp_scheme,
- })
- }
-
- let num_nodes = blockindex.read_u32_be()?;
- debug!("num_nodes={num_nodes:?}");
- let mut nodes = Vec::new();
- for _ in 0..num_nodes {
- let offset = blockindex.read_u64_be()?;
- let size = blockindex.read_u64_be()?;
- let status = blockindex.read_u32_be()?;
- let name = blockindex.read_cstr()?;
- info!(
- "found node {name:?} (size={}, status={status})",
- humansize::format_size(size, DECIMAL)
- );
- nodes.push(NodeInfo {
- offset,
- size,
- _status: status,
- name,
- })
- }
-
- let position = file.stream_position()?;
-
- Ok(Self {
- file_version,
- player_version,
- unity_version,
- nodes,
- reader: BlocksReader::new(blocks, file, position),
- })
- }
-
- pub fn nodes(&self) -> &[NodeInfo] {
- &self.nodes
- }
-
- pub fn find_main_file(&self) -> Option<&NodeInfo> {
- self.nodes().iter().find(|n| {
- !n.name.ends_with(".resource")
- && !n.name.ends_with(".resS")
- && !n.name.ends_with(".sharedAssets")
- })
- }
-
- pub fn read<'a>(&'a mut self, node: &NodeInfo) -> std::io::Result<NodeReader<'a, T>> {
- self.reader.seek(SeekFrom::Start(node.offset))?;
- Ok(NodeReader {
- size: node.size,
- offset: node.offset,
- position: 0,
- inner: &mut self.reader,
- })
- }
-}
-
-struct BlocksReader<T> {
- blocks: Vec<BlockInfo>,
- inner: T,
- inner_seek_offset: u64,
- nblock_index: usize,
- cblock_data: Vec<u8>,
- cblock_off: usize,
-}
-impl<T: Read> BlocksReader<T> {
- pub fn new(blocks: Vec<BlockInfo>, inner: T, inner_seek_offset: u64) -> Self {
- Self {
- blocks,
- inner,
- inner_seek_offset,
- nblock_index: 0,
- cblock_data: Vec::new(),
- cblock_off: 0,
- }
- }
- pub fn load_next_block(&mut self) -> std::io::Result<()> {
- trace!("loading block {}", self.nblock_index);
- let block = &self.blocks[self.nblock_index];
- let mut comp_buf = vec![0; block.comp_size as usize];
- self.inner.read_exact(&mut comp_buf)?;
- let decomp_buf = block
- .comp_scheme
- .decompress(comp_buf, block.decomp_size as usize)
- .map_err(|e| {
- std::io::Error::new(
- ErrorKind::InvalidData,
- format!("decompression failure: {e}"),
- )
- })?;
- self.nblock_index += 1;
- self.cblock_data = decomp_buf;
- self.cblock_off = 0;
- Ok(())
- }
-}
-impl<T: Read> Read for BlocksReader<T> {
- fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
- if self.cblock_off >= self.cblock_data.len() {
- self.load_next_block()?;
- }
- let size = (self.cblock_data.len() - self.cblock_off).min(buf.len());
- buf[..size].copy_from_slice(&self.cblock_data[self.cblock_off..self.cblock_off + size]);
- self.cblock_off += size;
- Ok(size)
- }
-}
-impl<T: Seek + Read> Seek for BlocksReader<T> {
- fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
- let SeekFrom::Start(pos) = pos else {
- unimplemented!()
- };
- debug!("seek decomp to {pos}");
- let mut comp_off = self.inner_seek_offset;
- let mut decomp_off = 0;
- let mut target_block = None;
- for (i, b) in self.blocks.iter().enumerate() {
- if pos <= decomp_off + b.decomp_size as u64 {
- target_block = Some(i);
- break;
- }
- decomp_off += b.decomp_size as u64;
- comp_off += b.comp_size as u64;
- }
-
- let Some(i) = target_block else {
- return Err(std::io::Error::new(
- ErrorKind::UnexpectedEof,
- "seek out of bounds",
- ));
- };
-
- let block_off = pos - decomp_off;
- debug!("target is block={i} offset={block_off}");
- if self.nblock_index == i + 1 {
- debug!("intra-block seek")
- } else {
- debug!("seek comp to {comp_off}");
- self.inner.seek(SeekFrom::Start(comp_off))?;
- self.nblock_index = i;
- self.load_next_block()?;
- }
- self.cblock_off = block_off as usize;
-
- Ok(pos)
- }
-}
-
-impl<T: Read> Read for NodeReader<'_, T> {
- fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
- let bytes_left = self.size - self.position;
- let end = buf.len().min(bytes_left as usize);
- let size = self.inner.read(&mut buf[..end])?;
- self.position += size as u64;
- Ok(size)
- }
-}
-impl<T: Seek + Read> Seek for NodeReader<'_, T> {
- fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
- match pos {
- SeekFrom::Current(n) if n >= 0 => {
- for _ in 0..n {
- self.read_exact(&mut [0u8])?;
- }
- Ok(self.stream_position()?)
- }
- SeekFrom::Start(n) => {
- debug!("seek node to {n} (off={})", self.offset);
- if n > self.size {
- return Err(Error::new(ErrorKind::NotSeekable, "seek out of bounds"));
- }
- self.position = n;
- self.inner.seek(SeekFrom::Start(self.offset + n))
- }
- _ => unimplemented!(),
- }
- }
- fn stream_position(&mut self) -> std::io::Result<u64> {
- Ok(self.position)
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-enum CompressionScheme {
- None,
- Lzma,
- Lz4,
- Lz4hc,
- Lzham,
-}
-impl CompressionScheme {
- pub fn from_flag_num(n: u8) -> Option<CompressionScheme> {
- Some(match n & 0x3f {
- 0 => CompressionScheme::None,
- 1 => CompressionScheme::Lzma,
- 2 => CompressionScheme::Lz4,
- 3 => CompressionScheme::Lz4hc,
- 4 => CompressionScheme::Lzham,
- _ => return None,
- })
- }
- pub fn decompress(&self, block: Vec<u8>, decomp_size: usize) -> Result<Vec<u8>> {
- match self {
- CompressionScheme::None => Ok(block),
- CompressionScheme::Lzma => {
- let mut r = lzma::Reader::from(Cursor::new(block))?;
- let mut buf = Vec::new();
- r.read_to_end(&mut buf)?;
- Ok(buf)
- }
- CompressionScheme::Lz4hc | CompressionScheme::Lz4 => {
- Ok(lz4_flex::block::decompress(&block, decomp_size)?)
- }
- // CompressionScheme::LZ4HC | CompressionScheme::LZ4 => {
- // Ok(lz4::block::decompress(&block, Some(decomp_size as i32))?)
- // }
- CompressionScheme::Lzham => todo!(),
- }
- }
-}
diff --git a/src/unityfs/block_reader.rs b/src/unityfs/block_reader.rs
new file mode 100644
index 0000000..8aa18c3
--- /dev/null
+++ b/src/unityfs/block_reader.rs
@@ -0,0 +1,99 @@
+use super::BlockInfo;
+use log::{debug, trace};
+use std::{
+ io::{ErrorKind, Read, Seek, SeekFrom},
+ sync::Arc,
+};
+
+pub struct BlockReader<T> {
+ blocks: Arc<Vec<BlockInfo>>,
+ inner: T,
+ inner_seek_offset: u64,
+ nblock_index: usize,
+ cblock_data: Vec<u8>,
+ cblock_off: usize,
+}
+
+impl<T: Read> BlockReader<T> {
+ pub fn new(blocks: Arc<Vec<BlockInfo>>, inner: T, inner_seek_offset: u64) -> Self {
+ Self {
+ blocks,
+ inner,
+ inner_seek_offset,
+ nblock_index: 0,
+ cblock_data: Vec::new(),
+ cblock_off: 0,
+ }
+ }
+ pub fn load_next_block(&mut self) -> std::io::Result<()> {
+ trace!("loading block {}", self.nblock_index);
+ let block = &self.blocks[self.nblock_index];
+ let mut comp_buf = vec![0; block.comp_size as usize];
+ self.inner.read_exact(&mut comp_buf)?;
+ let decomp_buf = block
+ .comp_scheme
+ .decompress(comp_buf, block.decomp_size as usize)
+ .map_err(|e| {
+ std::io::Error::new(
+ ErrorKind::InvalidData,
+ format!("decompression failure: {e}"),
+ )
+ })?;
+ self.nblock_index += 1;
+ self.cblock_data = decomp_buf;
+ self.cblock_off = 0;
+ Ok(())
+ }
+}
+
+impl<T: Read> Read for BlockReader<T> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ if self.cblock_off >= self.cblock_data.len() {
+ self.load_next_block()?;
+ }
+ let size = (self.cblock_data.len() - self.cblock_off).min(buf.len());
+ buf[..size].copy_from_slice(&self.cblock_data[self.cblock_off..self.cblock_off + size]);
+ self.cblock_off += size;
+ Ok(size)
+ }
+}
+impl<T: Seek + Read> Seek for BlockReader<T> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ let SeekFrom::Start(pos) = pos else {
+ unimplemented!()
+ };
+ debug!("seek decomp to {pos}");
+ let mut comp_off = self.inner_seek_offset;
+ let mut decomp_off = 0;
+ let mut target_block = None;
+ for (i, b) in self.blocks.iter().enumerate() {
+ if pos <= decomp_off + b.decomp_size as u64 {
+ target_block = Some(i);
+ break;
+ }
+ decomp_off += b.decomp_size as u64;
+ comp_off += b.comp_size as u64;
+ }
+
+ let Some(i) = target_block else {
+ return Err(std::io::Error::new(
+ ErrorKind::UnexpectedEof,
+ "seek out of bounds",
+ ));
+ };
+
+ let block_off = pos - decomp_off;
+ debug!("target is block={i} offset={block_off}");
+ if self.nblock_index == i + 1 {
+ debug!("intra-block seek")
+ } else {
+ debug!("seek comp to {comp_off}");
+ self.inner.seek(SeekFrom::Start(comp_off))?;
+ self.nblock_index = i;
+ self.load_next_block()?;
+ }
+ self.cblock_off = block_off as usize;
+
+ Ok(pos)
+ }
+}
diff --git a/src/unityfs/header.rs b/src/unityfs/header.rs
new file mode 100644
index 0000000..d4fc89f
--- /dev/null
+++ b/src/unityfs/header.rs
@@ -0,0 +1,175 @@
+use crate::helper::{AlignExt, ReadExt};
+use anyhow::{Result, anyhow, bail};
+use humansize::DECIMAL;
+use log::{debug, info};
+use std::io::{Cursor, Read, Seek, SeekFrom};
+
+#[derive(Debug, Clone)]
+pub struct NodeInfo {
+ pub name: String,
+ pub size: u64,
+ pub(super) offset: u64,
+ _status: u32,
+}
+
+pub struct BlockInfo {
+ pub comp_size: u32,
+ pub decomp_size: u32,
+ pub comp_scheme: CompressionScheme,
+}
+
+pub struct UnityFSHeader {
+ pub(crate) nodes: Vec<NodeInfo>,
+ pub file_version: u32,
+ pub player_version: String,
+ pub unity_version: String,
+}
+
+impl UnityFSHeader {
+ pub fn read(mut file: impl Read + Seek) -> Result<(Self, Vec<BlockInfo>)> {
+ let signature = file.read_cstr()?;
+ if signature.as_str() != "UnityFS" {
+ bail!("unknown signature {signature:?}")
+ }
+
+ let file_version = file.read_u32_be()?;
+ let player_version = file.read_cstr()?;
+ let unity_version = file.read_cstr()?;
+ let size = file.read_u64_be()?;
+ let blockindex_comp_size = file.read_u32_be()?;
+ let blockindex_decomp_size = file.read_u32_be()?;
+ let flags = file.read_u32_be()?;
+
+ let meta_comp_scheme = CompressionScheme::from_flag_num(flags as u8).ok_or(anyhow!(
+ "unknown block compression 0x{:02x}",
+ (flags & 0x3f) as u8
+ ))?;
+ let blockindex_eof = flags & 0x80 != 0;
+ let blockindex_has_directory = flags & 0x40 != 0;
+ let blockindex_need_padding = flags & 0x200 != 0;
+
+ info!("File Version: {file_version:?}");
+ info!("Player Version: {player_version:?}");
+ info!("Unity Version: {unity_version:?}");
+ debug!("size={size:?}");
+ debug!("meta_comp_size={blockindex_comp_size:?}");
+ debug!("meta_decomp_size={blockindex_decomp_size:?}");
+ debug!("flags={flags:?}");
+ debug!("meta_comp_scheme={meta_comp_scheme:?}");
+ debug!("blockindex_eof={blockindex_eof:?}");
+ debug!("blockindex_has_directory={blockindex_has_directory:?}");
+ debug!("blockindex_need_padding={blockindex_need_padding:?}");
+
+ let mut blockindex = {
+ let restore_position = if blockindex_eof {
+ let pos = file.stream_position()?;
+ file.seek(SeekFrom::End(-(blockindex_comp_size as i64)))?;
+ Some(pos)
+ } else {
+ None
+ };
+
+ let mut blockindex = vec![0u8; blockindex_comp_size as usize];
+ file.read_exact(&mut blockindex)?;
+
+ if let Some(pos) = restore_position {
+ file.seek(SeekFrom::Start(pos))?;
+ }
+ let blockindex =
+ meta_comp_scheme.decompress(blockindex, blockindex_decomp_size as usize)?;
+ Cursor::new(blockindex)
+ };
+
+ file.align(16)?;
+
+ blockindex.read_u128_be()?;
+
+ let num_blocks = blockindex.read_u32_be()?;
+ info!("File has {num_blocks} blocks");
+ let mut blocks = Vec::new();
+ for _ in 0..num_blocks {
+ let decomp_size = blockindex.read_u32_be()?;
+ let comp_size = blockindex.read_u32_be()?;
+ let flags = blockindex.read_u16_be()?;
+ let comp_scheme = CompressionScheme::from_flag_num(flags as u8)
+ .ok_or(anyhow!("unknown block compression 0x{:02x}", flags & 0x3f))?;
+ blocks.push(BlockInfo {
+ comp_size,
+ decomp_size,
+ comp_scheme,
+ })
+ }
+
+ let num_nodes = blockindex.read_u32_be()?;
+ debug!("num_nodes={num_nodes:?}");
+ let mut nodes = Vec::new();
+ for _ in 0..num_nodes {
+ let offset = blockindex.read_u64_be()?;
+ let size = blockindex.read_u64_be()?;
+ let status = blockindex.read_u32_be()?;
+ let name = blockindex.read_cstr()?;
+ info!(
+ "found node {name:?} (size={}, status={status})",
+ humansize::format_size(size, DECIMAL)
+ );
+ nodes.push(NodeInfo {
+ offset,
+ size,
+ _status: status,
+ name,
+ })
+ }
+
+ Ok((
+ Self {
+ file_version,
+ player_version,
+ unity_version,
+ nodes,
+ },
+ blocks,
+ ))
+ }
+ pub fn nodes(&self) -> &[NodeInfo] {
+ &self.nodes
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum CompressionScheme {
+ None,
+ Lzma,
+ Lz4,
+ Lz4hc,
+ Lzham,
+}
+impl CompressionScheme {
+ pub fn from_flag_num(n: u8) -> Option<CompressionScheme> {
+ Some(match n & 0x3f {
+ 0 => CompressionScheme::None,
+ 1 => CompressionScheme::Lzma,
+ 2 => CompressionScheme::Lz4,
+ 3 => CompressionScheme::Lz4hc,
+ 4 => CompressionScheme::Lzham,
+ _ => return None,
+ })
+ }
+ pub fn decompress(&self, block: Vec<u8>, decomp_size: usize) -> Result<Vec<u8>> {
+ match self {
+ CompressionScheme::None => Ok(block),
+ CompressionScheme::Lzma => {
+ let mut r = lzma::Reader::from(Cursor::new(block))?;
+ let mut buf = Vec::new();
+ r.read_to_end(&mut buf)?;
+ Ok(buf)
+ }
+ CompressionScheme::Lz4hc | CompressionScheme::Lz4 => {
+ Ok(lz4_flex::block::decompress(&block, decomp_size)?)
+ }
+ // CompressionScheme::LZ4HC | CompressionScheme::LZ4 => {
+ // Ok(lz4::block::decompress(&block, Some(decomp_size as i32))?)
+ // }
+ CompressionScheme::Lzham => todo!(),
+ }
+ }
+}
diff --git a/src/unityfs/mod.rs b/src/unityfs/mod.rs
new file mode 100644
index 0000000..bc7e3ec
--- /dev/null
+++ b/src/unityfs/mod.rs
@@ -0,0 +1,95 @@
+pub mod block_reader;
+pub mod header;
+pub mod multi_reader;
+
+use anyhow::Result;
+use block_reader::BlockReader;
+use header::{BlockInfo, NodeInfo, UnityFSHeader};
+use log::debug;
+use multi_reader::MultiReader;
+use std::{
+ io::{Error, ErrorKind, Read, Seek, SeekFrom},
+ sync::Arc,
+};
+
+pub struct UnityFS<T> {
+ reader: MultiReader<T>,
+ blocks: Arc<Vec<BlockInfo>>,
+ inner_seek_offset: u64,
+ pub header: UnityFSHeader,
+}
+
+pub struct NodeReader<T> {
+ inner: T,
+ position: u64,
+ offset: u64,
+ size: u64,
+}
+
+impl<T: Read + Seek> UnityFS<T> {
+ pub fn open(mut file: T) -> Result<Self> {
+ let (header, blocks) = UnityFSHeader::read(&mut file)?;
+ let inner_seek_offset = file.stream_position()?;
+
+ Ok(Self {
+ blocks: Arc::new(blocks),
+ header,
+ inner_seek_offset,
+ reader: MultiReader::new(file)?,
+ })
+ }
+
+ pub fn find_main_file(&self) -> Option<&NodeInfo> {
+ self.header.nodes().iter().find(|n| {
+ !n.name.ends_with(".resource")
+ && !n.name.ends_with(".resS")
+ && !n.name.ends_with(".sharedAssets")
+ })
+ }
+
+ pub fn read<'a>(&'a self, node: &NodeInfo) -> Result<NodeReader<BlockReader<MultiReader<T>>>> {
+ let mut inner = self.reader.clone();
+ inner.seek(SeekFrom::Start(self.inner_seek_offset))?;
+ let br = BlockReader::new(self.blocks.clone(), inner, self.inner_seek_offset);
+ Ok(NodeReader {
+ size: node.size,
+ offset: node.offset,
+ position: 0,
+ inner: br,
+ })
+ }
+}
+
+impl<T: Read> Read for NodeReader<T> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let bytes_left = self.size - self.position;
+ let end = buf.len().min(bytes_left as usize);
+ let size = self.inner.read(&mut buf[..end])?;
+ self.position += size as u64;
+ Ok(size)
+ }
+}
+impl<T: Seek + Read> Seek for NodeReader<T> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ match pos {
+ SeekFrom::Current(n) if n >= 0 => {
+ for _ in 0..n {
+ self.read_exact(&mut [0u8])?;
+ }
+ Ok(self.stream_position()?)
+ }
+ SeekFrom::Start(n) => {
+ debug!("seek node to {n} (off={})", self.offset);
+ if n > self.size {
+ return Err(Error::new(ErrorKind::NotSeekable, "seek out of bounds"));
+ }
+ self.position = n;
+ self.inner.seek(SeekFrom::Start(self.offset + n))
+ }
+ _ => unimplemented!(),
+ }
+ }
+ fn stream_position(&mut self) -> std::io::Result<u64> {
+ Ok(self.position)
+ }
+}
diff --git a/src/unityfs/multi_reader.rs b/src/unityfs/multi_reader.rs
new file mode 100644
index 0000000..3de6cd5
--- /dev/null
+++ b/src/unityfs/multi_reader.rs
@@ -0,0 +1,50 @@
+use std::{
+ io::{Read, Seek, SeekFrom},
+ sync::{Arc, Mutex},
+};
+
+use anyhow::Result;
+
+pub struct MultiReader<T> {
+ position: u64,
+ inner: Arc<Mutex<(u64, T)>>,
+}
+impl<T: Seek> MultiReader<T> {
+ pub fn new(mut inner: T) -> Result<Self> {
+ let position = inner.stream_position()?;
+ Ok(Self {
+ position,
+ inner: Arc::new(Mutex::new((position, inner))),
+ })
+ }
+}
+impl<T> Clone for MultiReader<T> {
+ fn clone(&self) -> Self {
+ Self {
+ position: self.position,
+ inner: self.inner.clone(),
+ }
+ }
+}
+impl<T: Read + Seek> Read for MultiReader<T> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let mut g = self.inner.lock().unwrap();
+ if g.0 != self.position {
+ g.1.seek(SeekFrom::Start(self.position))?;
+ }
+ let size = g.1.read(buf)?;
+ g.0 += size as u64;
+ self.position += size as u64;
+ Ok(size)
+ }
+}
+impl<T: Seek> Seek for MultiReader<T> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ self.position = match pos {
+ SeekFrom::Start(x) => x,
+ SeekFrom::Current(x) => self.position.saturating_add_signed(x),
+ SeekFrom::End(_) => unimplemented!(),
+ };
+ Ok(self.position)
+ }
+}