feat: Implement bundle content listing
This commit is contained in:
parent
da188155e3
commit
109eb8ffa2
6 changed files with 694 additions and 29 deletions
|
@ -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<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
|
||||
unimplemented!()
|
||||
pub(crate) async fn run(ctx: Arc<RwLock<dtmt::Context>>, matches: &ArgMatches) -> Result<()> {
|
||||
let bundles = matches
|
||||
.get_many::<PathBuf>("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(())
|
||||
}
|
||||
}
|
||||
|
|
61
src/binary.rs
Normal file
61
src/binary.rs
Normal file
|
@ -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<R>(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<R>(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);
|
425
src/bundle/file.rs
Normal file
425
src/bundle/file.rs
Normal file
|
@ -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<u64> for BundleFileType {
|
||||
fn from(value: u64) -> Self {
|
||||
Self::from(Murmur64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Murmur64> 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<BundleFileType> 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<R>(mut r: R) -> Result<Self>
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
impl BundleFileVariant {
|
||||
pub fn size(&self) -> usize {
|
||||
self.header.size
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &Vec<u8> {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BundleFile {
|
||||
file_type: BundleFileType,
|
||||
hash: Murmur64,
|
||||
name: String,
|
||||
variants: Vec<BundleFileVariant>,
|
||||
}
|
||||
|
||||
impl BundleFile {
|
||||
#[tracing::instrument(name = "File::read", skip_all)]
|
||||
pub async fn read<R>(ctx: Arc<RwLock<crate::Context>>, mut r: R) -> Result<Self>
|
||||
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<BundleFileVariant> {
|
||||
&self.variants
|
||||
}
|
||||
}
|
|
@ -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<u32> for BundleFormat {
|
|||
}
|
||||
}
|
||||
|
||||
async fn read_u32<R>(mut r: R) -> Result<u32>
|
||||
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<R>(mut r: R) -> Result<Self>
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
_name_hash: name_hash,
|
||||
_extension_hash: extension_hash,
|
||||
_flags: flags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bundle {
|
||||
_format: BundleFormat,
|
||||
_headers: Vec<EntryHeader>,
|
||||
files: Vec<BundleFile>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
#[tracing::instrument(name = "Bundle::open", skip(ctx))]
|
||||
pub async fn open<P>(ctx: Arc<RwLock<crate::Context>>, path: P) -> Result<Self>
|
||||
where
|
||||
P: AsRef<Path> + 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 {
|
||||
res
|
||||
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<BundleFile> {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
|
|
Loading…
Add table
Reference in a new issue