diff options
-rw-r--r-- | exporter/src/bin/debug.rs | 2 | ||||
-rw-r--r-- | exporter/src/bin/gltf.rs | 12 | ||||
-rw-r--r-- | exporter/src/bin/json.rs | 2 | ||||
-rw-r--r-- | exporter/src/bin/material_stats.rs | 52 | ||||
-rw-r--r-- | exporter/src/bin/meshes.rs | 5 | ||||
-rw-r--r-- | exporter/src/bin/probe.rs | 6 | ||||
-rw-r--r-- | exporter/src/bin/textures.rs | 7 | ||||
-rw-r--r-- | exporter/src/bin/typegraph.rs | 2 | ||||
-rw-r--r-- | exporter/src/bin/yaml.rs | 2 | ||||
-rw-r--r-- | src/classes/material.rs | 9 | ||||
-rw-r--r-- | src/classes/mesh_renderer.rs | 6 | ||||
-rw-r--r-- | src/classes/mod.rs | 1 | ||||
-rw-r--r-- | src/classes/pptr.rs | 35 | ||||
-rw-r--r-- | src/classes/shader.rs | 42 | ||||
-rw-r--r-- | src/classes/streaminginfo.rs | 1 | ||||
-rw-r--r-- | src/serialized_file.rs | 13 | ||||
-rw-r--r-- | src/unityfs.rs | 327 | ||||
-rw-r--r-- | src/unityfs/block_reader.rs | 99 | ||||
-rw-r--r-- | src/unityfs/header.rs | 175 | ||||
-rw-r--r-- | src/unityfs/mod.rs | 95 | ||||
-rw-r--r-- | src/unityfs/multi_reader.rs | 50 |
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) + } +} |