diff --git a/src/bin/cmd/bundle/list.rs b/src/bin/cmd/bundle/list.rs index f71c95f..edb04c0 100644 --- a/src/bin/cmd/bundle/list.rs +++ b/src/bin/cmd/bundle/list.rs @@ -2,7 +2,10 @@ use std::path::PathBuf; use std::sync::Arc; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; -use color_eyre::eyre::Result; +use color_eyre::eyre::{self, Result}; +use color_eyre::{Help, SectionExt}; +use dtmt::Bundle; +use futures::future::try_join_all; use tokio::sync::RwLock; pub(crate) fn command_definition() -> Command { @@ -14,16 +17,6 @@ pub(crate) fn command_definition() -> Command { .action(ArgAction::SetTrue) .help("Print machine-readable JSON"), ) - .arg( - Arg::new("oodle") - .long("oodle") - .default_value("oodle-cli") - .help( - "Name of or path to the Oodle decompression helper. \ - The helper is a small executable that wraps the Oodle library \ - with a CLI.", - ), - ) .arg( Arg::new("bundle") .required(true) @@ -37,6 +30,44 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { - unimplemented!() +pub(crate) async fn run(ctx: Arc>, matches: &ArgMatches) -> Result<()> { + let bundles = matches + .get_many::("bundle") + .unwrap_or_default() + .cloned(); + + let bundles = try_join_all(bundles.into_iter().map(|p| async { + let ctx = ctx.clone(); + let path_display = p.display().to_string(); + async move { Bundle::open(ctx, &p).await } + .await + .with_section(|| path_display.header("Bundle Path:")) + })) + .await?; + + if matches.get_flag("json") { + unimplemented!("JSON output is not implemented yet"); + } else { + for b in bundles.iter() { + println!("Bundle: {}", b.name()); + + for f in b.files().iter() { + if f.variants().len() != 1 { + return Err(eyre::eyre!("Expected exactly one version for this file.")) + .with_section(|| f.variants().len().to_string().header("Bundle:")) + .with_section(|| b.name().clone().header("Bundle:")); + } + + let v = &f.variants()[0]; + println!( + "\t{}.{}: {} bytes", + f.name(), + f.file_type().ext_name(), + v.size() + ); + } + } + + Ok(()) + } } diff --git a/src/binary.rs b/src/binary.rs new file mode 100644 index 0000000..4d95581 --- /dev/null +++ b/src/binary.rs @@ -0,0 +1,61 @@ +use color_eyre::eyre::WrapErr; +use color_eyre::{Help, Result, SectionExt}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt}; + +macro_rules! make_read { + ($func:ident, $op:ident, $type:ty) => { + pub(crate) async fn $func(mut r: R) -> Result<$type> + where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + { + let res = r + .$op() + .await + .wrap_err(concat!("failed to read ", stringify!($type))); + + if res.is_err() { + let pos = r.stream_position().await; + if pos.is_ok() { + res.with_section(|| { + format!("{pos:#X} ({pos})", pos = pos.unwrap()).header("Position: ") + }) + } else { + res + } + } else { + res + } + } + }; +} + +macro_rules! make_skip { + ($func:ident, $read:ident, $op:ident, $type:ty) => { + pub(crate) async fn $func(mut r: R, cmp: $type) -> Result<()> + where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + { + let val = $read(&mut r).await?; + + if val != cmp { + let pos = r.stream_position().await.unwrap_or(u64::MAX); + tracing::debug!( + pos, + expected = cmp, + actual = val, + "Unexpected value for skipped {}", + stringify!($type) + ); + } + + Ok(()) + } + }; +} + +make_read!(read_u8, read_u8, u8); +make_read!(read_u32, read_u32_le, u32); +make_read!(read_u64, read_u64_le, u64); + +make_skip!(skip_u8, read_u8, read_u8, u8); +make_skip!(skip_u32, read_u32, read_u32_le, u32); diff --git a/src/bundle/file.rs b/src/bundle/file.rs new file mode 100644 index 0000000..7bda17e --- /dev/null +++ b/src/bundle/file.rs @@ -0,0 +1,425 @@ +use std::ops::Deref; +use std::sync::Arc; + +use color_eyre::{Help, Result, SectionExt}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek}; +use tokio::sync::RwLock; + +use crate::binary::*; +use crate::context::lookup_hash; +use crate::murmur::{HashGroup, Murmur64}; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum BundleFileType { + Animation, + AnimationCurves, + Apb, + BakedLighting, + Bik, + BlendSet, + Bones, + Chroma, + CommonPackage, + Config, + Crypto, + Data, + Entity, + Flow, + Font, + Ies, + Ini, + Input, + Ivf, + Keys, + Level, + Lua, + Material, + Mod, + MouseCursor, + NavData, + NetworkConfig, + OddleNet, + Package, + Particles, + PhysicsProperties, + RenderConfig, + RtPipeline, + Scene, + Shader, + ShaderLibrary, + ShaderLibraryGroup, + ShadingEnvionmentMapping, + ShadingEnvironment, + Slug, + SlugAlbum, + SoundEnvironment, + SpuJob, + StateMachine, + StaticPVS, + Strings, + SurfaceProperties, + Texture, + TimpaniBank, + TimpaniMaster, + Tome, + Ugg, + Unit, + Upb, + VectorField, + Wav, + WwiseBank, + WwiseDep, + WwiseEvent, + WwiseMetadata, + WwiseStream, + Xml, + + Unknown(Murmur64), +} + +impl BundleFileType { + pub fn ext_name(&self) -> String { + match self { + BundleFileType::AnimationCurves => String::from("animation_curves"), + BundleFileType::Animation => String::from("animation"), + BundleFileType::Apb => String::from("apb"), + BundleFileType::BakedLighting => String::from("baked_lighting"), + BundleFileType::Bik => String::from("bik"), + BundleFileType::BlendSet => String::from("blend_set"), + BundleFileType::Bones => String::from("bones"), + BundleFileType::Chroma => String::from("chroma"), + BundleFileType::CommonPackage => String::from("common_package"), + BundleFileType::Config => String::from("config"), + BundleFileType::Crypto => String::from("crypto"), + BundleFileType::Data => String::from("data"), + BundleFileType::Entity => String::from("entity"), + BundleFileType::Flow => String::from("flow"), + BundleFileType::Font => String::from("font"), + BundleFileType::Ies => String::from("ies"), + BundleFileType::Ini => String::from("ini"), + BundleFileType::Input => String::from("input"), + BundleFileType::Ivf => String::from("ivf"), + BundleFileType::Keys => String::from("keys"), + BundleFileType::Level => String::from("level"), + BundleFileType::Lua => String::from("lua"), + BundleFileType::Material => String::from("material"), + BundleFileType::Mod => String::from("mod"), + BundleFileType::MouseCursor => String::from("mouse_cursor"), + BundleFileType::NavData => String::from("nav_data"), + BundleFileType::NetworkConfig => String::from("network_config"), + BundleFileType::OddleNet => String::from("oodle_net"), + BundleFileType::Package => String::from("package"), + BundleFileType::Particles => String::from("particles"), + BundleFileType::PhysicsProperties => String::from("physics_properties"), + BundleFileType::RenderConfig => String::from("render_config"), + BundleFileType::RtPipeline => String::from("rt_pipeline"), + BundleFileType::Scene => String::from("scene"), + BundleFileType::ShaderLibraryGroup => String::from("shader_library_group"), + BundleFileType::ShaderLibrary => String::from("shader_library"), + BundleFileType::Shader => String::from("shader"), + BundleFileType::ShadingEnvionmentMapping => String::from("shading_environment_mapping"), + BundleFileType::ShadingEnvironment => String::from("shading_environment"), + BundleFileType::SlugAlbum => String::from("slug_album"), + BundleFileType::Slug => String::from("slug"), + BundleFileType::SoundEnvironment => String::from("sound_environment"), + BundleFileType::SpuJob => String::from("spu_job"), + BundleFileType::StateMachine => String::from("state_machine"), + BundleFileType::StaticPVS => String::from("static_pvs"), + BundleFileType::Strings => String::from("strings"), + BundleFileType::SurfaceProperties => String::from("surface_properties"), + BundleFileType::Texture => String::from("texture"), + BundleFileType::TimpaniBank => String::from("timpani_bank"), + BundleFileType::TimpaniMaster => String::from("timpani_master"), + BundleFileType::Tome => String::from("tome"), + BundleFileType::Ugg => String::from("ugg"), + BundleFileType::Unit => String::from("unit"), + BundleFileType::Upb => String::from("upb"), + BundleFileType::VectorField => String::from("vector_field"), + BundleFileType::Wav => String::from("wav"), + BundleFileType::WwiseBank => String::from("wwise_bank"), + BundleFileType::WwiseDep => String::from("wwise_dep"), + BundleFileType::WwiseEvent => String::from("wwise_event"), + BundleFileType::WwiseMetadata => String::from("wwise_metadata"), + BundleFileType::WwiseStream => String::from("wwise_stream"), + BundleFileType::Xml => String::from("xml"), + + BundleFileType::Unknown(s) => format!("{:016X}", s), + } + } + + pub fn decompiled_ext_name(&self) -> String { + match self { + BundleFileType::Texture => String::from("dds"), + BundleFileType::WwiseBank => String::from("bnk"), + BundleFileType::WwiseStream => String::from("ogg"), + _ => self.ext_name(), + } + } +} + +impl From for BundleFileType { + fn from(value: u64) -> Self { + Self::from(Murmur64::from(value)) + } +} + +impl From for BundleFileType { + fn from(hash: Murmur64) -> BundleFileType { + match hash.deref() { + 0x931e336d7646cc26 => BundleFileType::Animation, + 0xdcfb9e18fff13984 => BundleFileType::AnimationCurves, + 0x3eed05ba83af5090 => BundleFileType::Apb, + 0x7ffdb779b04e4ed1 => BundleFileType::BakedLighting, + 0xaa5965f03029fa18 => BundleFileType::Bik, + 0xe301e8af94e3b5a3 => BundleFileType::BlendSet, + 0x18dead01056b72e9 => BundleFileType::Bones, + 0xb7893adf7567506a => BundleFileType::Chroma, + 0xfe9754bd19814a47 => BundleFileType::CommonPackage, + 0x82645835e6b73232 => BundleFileType::Config, + 0x69108ded1e3e634b => BundleFileType::Crypto, + 0x8fd0d44d20650b68 => BundleFileType::Data, + 0x9831ca893b0d087d => BundleFileType::Entity, + 0x92d3ee038eeb610d => BundleFileType::Flow, + 0x9efe0a916aae7880 => BundleFileType::Font, + 0x8f7d5a2c0f967655 => BundleFileType::Ies, + 0xd526a27da14f1dc5 => BundleFileType::Ini, + 0x2bbcabe5074ade9e => BundleFileType::Input, + 0xfa4a8e091a91201e => BundleFileType::Ivf, + 0xa62f9297dc969e85 => BundleFileType::Keys, + 0x2a690fd348fe9ac5 => BundleFileType::Level, + 0xa14e8dfa2cd117e2 => BundleFileType::Lua, + 0xeac0b497876adedf => BundleFileType::Material, + 0x3fcdd69156a46417 => BundleFileType::Mod, + 0xb277b11fe4a61d37 => BundleFileType::MouseCursor, + 0x169de9566953d264 => BundleFileType::NavData, + 0x3b1fa9e8f6bac374 => BundleFileType::NetworkConfig, + 0xb0f2c12eb107f4d8 => BundleFileType::OddleNet, + 0xad9c6d9ed1e5e77a => BundleFileType::Package, + 0xa8193123526fad64 => BundleFileType::Particles, + 0xbf21403a3ab0bbb1 => BundleFileType::PhysicsProperties, + 0x27862fe24795319c => BundleFileType::RenderConfig, + 0x9ca183c2d0e76dee => BundleFileType::RtPipeline, + 0x9d0a795bfe818d19 => BundleFileType::Scene, + 0xcce8d5b5f5ae333f => BundleFileType::Shader, + 0xe5ee32a477239a93 => BundleFileType::ShaderLibrary, + 0x9e5c3cc74575aeb5 => BundleFileType::ShaderLibraryGroup, + 0x250e0a11ac8e26f8 => BundleFileType::ShadingEnvionmentMapping, + 0xfe73c7dcff8a7ca5 => BundleFileType::ShadingEnvironment, + 0xa27b4d04a9ba6f9e => BundleFileType::Slug, + 0xe9fc9ea7042e5ec0 => BundleFileType::SlugAlbum, + 0xd8b27864a97ffdd7 => BundleFileType::SoundEnvironment, + 0xf97af9983c05b950 => BundleFileType::SpuJob, + 0xa486d4045106165c => BundleFileType::StateMachine, + 0xe3f0baa17d620321 => BundleFileType::StaticPVS, + 0x0d972bab10b40fd3 => BundleFileType::Strings, + 0xad2d3fa30d9ab394 => BundleFileType::SurfaceProperties, + 0xcd4238c6a0c69e32 => BundleFileType::Texture, + 0x99736be1fff739a4 => BundleFileType::TimpaniBank, + 0x00a3e6c59a2b9c6c => BundleFileType::TimpaniMaster, + 0x19c792357c99f49b => BundleFileType::Tome, + 0x712d6e3dd1024c9c => BundleFileType::Ugg, + 0xe0a48d0be9a7453f => BundleFileType::Unit, + 0xa99510c6e86dd3c2 => BundleFileType::Upb, + 0xf7505933166d6755 => BundleFileType::VectorField, + 0x786f65c00a816b19 => BundleFileType::Wav, + 0x535a7bd3e650d799 => BundleFileType::WwiseBank, + 0xaf32095c82f2b070 => BundleFileType::WwiseDep, + 0xaabdd317b58dfc8a => BundleFileType::WwiseEvent, + 0xd50a8b7e1c82b110 => BundleFileType::WwiseMetadata, + 0x504b55235d21440e => BundleFileType::WwiseStream, + 0x76015845a6003765 => BundleFileType::Xml, + + _ => BundleFileType::Unknown(hash), + } + } +} + +impl From for Murmur64 { + fn from(t: BundleFileType) -> Murmur64 { + match t { + BundleFileType::Animation => Murmur64::from(0x931e336d7646cc26), + BundleFileType::AnimationCurves => Murmur64::from(0xdcfb9e18fff13984), + BundleFileType::Apb => Murmur64::from(0x3eed05ba83af5090), + BundleFileType::BakedLighting => Murmur64::from(0x7ffdb779b04e4ed1), + BundleFileType::Bik => Murmur64::from(0xaa5965f03029fa18), + BundleFileType::BlendSet => Murmur64::from(0xe301e8af94e3b5a3), + BundleFileType::Bones => Murmur64::from(0x18dead01056b72e9), + BundleFileType::Chroma => Murmur64::from(0xb7893adf7567506a), + BundleFileType::CommonPackage => Murmur64::from(0xfe9754bd19814a47), + BundleFileType::Config => Murmur64::from(0x82645835e6b73232), + BundleFileType::Crypto => Murmur64::from(0x69108ded1e3e634b), + BundleFileType::Data => Murmur64::from(0x8fd0d44d20650b68), + BundleFileType::Entity => Murmur64::from(0x9831ca893b0d087d), + BundleFileType::Flow => Murmur64::from(0x92d3ee038eeb610d), + BundleFileType::Font => Murmur64::from(0x9efe0a916aae7880), + BundleFileType::Ies => Murmur64::from(0x8f7d5a2c0f967655), + BundleFileType::Ini => Murmur64::from(0xd526a27da14f1dc5), + BundleFileType::Input => Murmur64::from(0x2bbcabe5074ade9e), + BundleFileType::Ivf => Murmur64::from(0xfa4a8e091a91201e), + BundleFileType::Keys => Murmur64::from(0xa62f9297dc969e85), + BundleFileType::Level => Murmur64::from(0x2a690fd348fe9ac5), + BundleFileType::Lua => Murmur64::from(0xa14e8dfa2cd117e2), + BundleFileType::Material => Murmur64::from(0xeac0b497876adedf), + BundleFileType::Mod => Murmur64::from(0x3fcdd69156a46417), + BundleFileType::MouseCursor => Murmur64::from(0xb277b11fe4a61d37), + BundleFileType::NavData => Murmur64::from(0x169de9566953d264), + BundleFileType::NetworkConfig => Murmur64::from(0x3b1fa9e8f6bac374), + BundleFileType::OddleNet => Murmur64::from(0xb0f2c12eb107f4d8), + BundleFileType::Package => Murmur64::from(0xad9c6d9ed1e5e77a), + BundleFileType::Particles => Murmur64::from(0xa8193123526fad64), + BundleFileType::PhysicsProperties => Murmur64::from(0xbf21403a3ab0bbb1), + BundleFileType::RenderConfig => Murmur64::from(0x27862fe24795319c), + BundleFileType::RtPipeline => Murmur64::from(0x9ca183c2d0e76dee), + BundleFileType::Scene => Murmur64::from(0x9d0a795bfe818d19), + BundleFileType::Shader => Murmur64::from(0xcce8d5b5f5ae333f), + BundleFileType::ShaderLibrary => Murmur64::from(0xe5ee32a477239a93), + BundleFileType::ShaderLibraryGroup => Murmur64::from(0x9e5c3cc74575aeb5), + BundleFileType::ShadingEnvionmentMapping => Murmur64::from(0x250e0a11ac8e26f8), + BundleFileType::ShadingEnvironment => Murmur64::from(0xfe73c7dcff8a7ca5), + BundleFileType::Slug => Murmur64::from(0xa27b4d04a9ba6f9e), + BundleFileType::SlugAlbum => Murmur64::from(0xe9fc9ea7042e5ec0), + BundleFileType::SoundEnvironment => Murmur64::from(0xd8b27864a97ffdd7), + BundleFileType::SpuJob => Murmur64::from(0xf97af9983c05b950), + BundleFileType::StateMachine => Murmur64::from(0xa486d4045106165c), + BundleFileType::StaticPVS => Murmur64::from(0xe3f0baa17d620321), + BundleFileType::Strings => Murmur64::from(0x0d972bab10b40fd3), + BundleFileType::SurfaceProperties => Murmur64::from(0xad2d3fa30d9ab394), + BundleFileType::Texture => Murmur64::from(0xcd4238c6a0c69e32), + BundleFileType::TimpaniBank => Murmur64::from(0x99736be1fff739a4), + BundleFileType::TimpaniMaster => Murmur64::from(0x00a3e6c59a2b9c6c), + BundleFileType::Tome => Murmur64::from(0x19c792357c99f49b), + BundleFileType::Ugg => Murmur64::from(0x712d6e3dd1024c9c), + BundleFileType::Unit => Murmur64::from(0xe0a48d0be9a7453f), + BundleFileType::Upb => Murmur64::from(0xa99510c6e86dd3c2), + BundleFileType::VectorField => Murmur64::from(0xf7505933166d6755), + BundleFileType::Wav => Murmur64::from(0x786f65c00a816b19), + BundleFileType::WwiseBank => Murmur64::from(0x535a7bd3e650d799), + BundleFileType::WwiseDep => Murmur64::from(0xaf32095c82f2b070), + BundleFileType::WwiseEvent => Murmur64::from(0xaabdd317b58dfc8a), + BundleFileType::WwiseMetadata => Murmur64::from(0xd50a8b7e1c82b110), + BundleFileType::WwiseStream => Murmur64::from(0x504b55235d21440e), + BundleFileType::Xml => Murmur64::from(0x76015845a6003765), + + BundleFileType::Unknown(hash) => hash, + } + } +} + +struct BundleFileHeader { + size: usize, +} + +impl BundleFileHeader { + #[tracing::instrument(name = "FileHeader::read", skip_all)] + async fn read(mut r: R) -> Result + where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + { + // NOTE: One of these must be the version number, or any kind of + // identifier between the different file entries. + // Back in VT2 days, these different 'files' were used to separate + // versions, e.g. different languages for the same `.strings` file. + skip_u32(&mut r, 0).await?; + skip_u32(&mut r, 0).await?; + skip_u32(&mut r, 0).await?; + + let size_1 = read_u32(&mut r).await? as usize; + + skip_u8(&mut r, 1).await?; + + let size_2 = read_u32(&mut r).await? as usize; + + tracing::debug!(size_1, size_2); + + // NOTE: Very weird. There must be something affecting this. + let size = if size_2 == 30 { + size_1 + size_2 + } else { + size_1 + }; + + Ok(Self { size }) + } +} + +pub struct BundleFileVariant { + header: BundleFileHeader, + data: Vec, +} + +impl BundleFileVariant { + pub fn size(&self) -> usize { + self.header.size + } + + pub fn data(&self) -> &Vec { + &self.data + } +} + +pub struct BundleFile { + file_type: BundleFileType, + hash: Murmur64, + name: String, + variants: Vec, +} + +impl BundleFile { + #[tracing::instrument(name = "File::read", skip_all)] + pub async fn read(ctx: Arc>, mut r: R) -> Result + where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + { + let file_type = BundleFileType::from(read_u64(&mut r).await?); + let hash = Murmur64::from(read_u64(&mut r).await?); + let name = lookup_hash(ctx, hash, HashGroup::Filename).await; + + let header_count = read_u8(&mut r) + .await + .with_section(|| format!("{}.{}", name, file_type.ext_name()).header("File:"))?; + let header_count = header_count as usize; + + let mut headers = Vec::with_capacity(header_count); + + for _ in 0..header_count { + let header = BundleFileHeader::read(&mut r) + .await + .with_section(|| format!("{}.{}", name, file_type.ext_name()).header("File:"))?; + headers.push(header); + } + + let mut variants = Vec::with_capacity(header_count); + + for header in headers.into_iter() { + let mut data = vec![0; header.size]; + r.read_exact(&mut data).await?; + + let variant = BundleFileVariant { header, data }; + + variants.push(variant); + } + + Ok(Self { + variants, + file_type, + hash, + name, + }) + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn hash(&self) -> Murmur64 { + self.hash + } + + pub fn file_type(&self) -> BundleFileType { + self.file_type + } + + pub fn variants(&self) -> &Vec { + &self.variants + } +} diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index e33f3e6..b854970 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -1,13 +1,23 @@ -use std::io::SeekFrom; +use std::io::{Cursor, SeekFrom}; +use std::path::Path; use std::sync::Arc; use color_eyre::eyre::{self, Context, Result}; use color_eyre::{Help, SectionExt}; +use tokio::fs; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; use tokio::sync::RwLock; +use tracing::Instrument; +use crate::binary::*; +use crate::context::lookup_hash; +use crate::murmur::{HashGroup, Murmur64}; use crate::oodle; +mod file; + +use file::BundleFile; + #[derive(Debug, PartialEq)] enum BundleFormat { Darktide, @@ -24,21 +34,157 @@ impl TryFrom for BundleFormat { } } -async fn read_u32(mut r: R) -> Result -where - R: AsyncRead + AsyncSeek + std::marker::Unpin, -{ - let res = r.read_u32_le().await.wrap_err("failed to read u32"); +struct EntryHeader { + _name_hash: u64, + _extension_hash: u64, + _flags: u32, +} - if res.is_err() { - let pos = r.stream_position().await; - if pos.is_ok() { - res.with_section(|| pos.unwrap().to_string().header("Position: ")) - } else { - res +impl EntryHeader { + #[tracing::instrument(name = "FileMeta::read", skip_all)] + async fn read(mut r: R) -> Result + where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + { + let extension_hash = r.read_u64().await?; + let name_hash = r.read_u64().await?; + let flags = read_u32(r).await?; + + // NOTE: Known values so far: + // - 0x0: seems to be the default + // - 0x4: seems to be used for files that point to something in `data/` + // seems to correspond to a change in value in the header's 'unknown_3' + if flags != 0x0 { + tracing::debug!( + flags, + "Unexpected meta flags for file {:08X}.{:08X}", + name_hash, + extension_hash + ); } - } else { - res + + Ok(Self { + _name_hash: name_hash, + _extension_hash: extension_hash, + _flags: flags, + }) + } +} + +pub struct Bundle { + _format: BundleFormat, + _headers: Vec, + files: Vec, + name: String, +} + +impl Bundle { + #[tracing::instrument(name = "Bundle::open", skip(ctx))] + pub async fn open

(ctx: Arc>, path: P) -> Result + where + P: AsRef + std::fmt::Debug, + { + let path = path.as_ref(); + let bundle_name = if let Some(name) = path.file_name() { + let hash = Murmur64::try_from(name.to_string_lossy().as_ref())?; + lookup_hash(ctx.clone(), hash, HashGroup::Filename).await + } else { + return Err(eyre::eyre!("Invalid path to bundle file")) + .with_section(|| path.display().to_string().header("Path:")); + }; + + let mut r = fs::File::open(path) + .await + .wrap_err("Failed to open bundle file") + .with_section(|| path.display().to_string().header("Path"))?; + + let format = read_u32(&mut r) + .await + .wrap_err("failed to read from file") + .and_then(BundleFormat::try_from)?; + + if format != BundleFormat::Darktide { + return Err(eyre::eyre!("Unknown bundle format: {:?}", format)); + } + + // Skip unknown 4 bytes + r.seek(SeekFrom::Current(4)).await?; + + let num_entries = read_u32(&mut r).await? as usize; + + // Skip unknown 256 bytes. I believe this data is somewhat related to packaging and the + // `.package` files + r.seek(SeekFrom::Current(256)).await?; + + let mut meta = Vec::with_capacity(num_entries); + for _ in 0..num_entries { + meta.push(EntryHeader::read(&mut r).await?); + } + + let num_chunks = read_u32(&mut r).await? as usize; + tracing::debug!(num_chunks); + let mut chunk_sizes = Vec::with_capacity(num_chunks); + for _ in 0..num_chunks { + chunk_sizes.push(read_u32(&mut r).await? as usize); + } + + let unpacked_size = { + let size_1 = read_u32(&mut r).await? as usize; + + // Skip unknown 4 bytes + r.seek(SeekFrom::Current(4)).await?; + + // NOTE: Unknown why this sometimes needs a second value. + // Also unknown if there is a different part in the data that actually + // determines whether this second value exists. + if size_1 == 0x0 { + let size_2 = read_u32(&mut r).await? as usize; + // Skip unknown 4 bytes + r.seek(SeekFrom::Current(4)).await?; + size_2 + } else { + size_1 + } + }; + + let mut decompressed = Vec::new(); + oodle::decompress(ctx.clone(), r, &mut decompressed, num_chunks).await?; + + if decompressed.len() < unpacked_size { + return Err(eyre::eyre!( + "Decompressed data does not match the expected size" + )) + .with_section(|| decompressed.len().to_string().header("Actual:")) + .with_section(|| unpacked_size.to_string().header("Expected:")); + } + + // Truncate to the actual data size + decompressed.resize(unpacked_size, 0); + + let mut r = Cursor::new(decompressed); + let mut files = Vec::with_capacity(num_entries); + for i in 0..num_entries { + let span = tracing::trace_span!("", file_index = i); + let file = BundleFile::read(ctx.clone(), &mut r) + .instrument(span) + .await?; + files.push(file); + } + + Ok(Self { + name: bundle_name, + _format: format, + _headers: meta, + files, + }) + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn files(&self) -> &Vec { + &self.files } } diff --git a/src/lib.rs b/src/lib.rs index caef980..113859e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ +mod binary; mod bundle; mod context; pub mod murmur; mod oodle; pub use bundle::decompress; +pub use bundle::Bundle; pub use context::lookup_hash; pub use context::lookup_hash_short; pub use context::Context; diff --git a/src/oodle.rs b/src/oodle.rs index 1c83ed5..006b3ef 100644 --- a/src/oodle.rs +++ b/src/oodle.rs @@ -78,9 +78,9 @@ where if !res.status.success() { let stderr = String::from_utf8_lossy(&res.stderr); let stdout = String::from_utf8_lossy(&res.stdout); - return Err(eyre::eyre!("failed to run Oodle decompression helper") + return Err(eyre::eyre!("failed to run Oodle decompression helper")) .with_section(move || stdout.to_string().header("Logs:")) - .with_section(move || stderr.to_string().header("Stderr:"))); + .with_section(move || stderr.to_string().header("Stderr:")); } Ok(())