feat: Implement bundle content listing

This commit is contained in:
Lucas Schwiderski 2022-11-03 20:26:19 +01:00
parent da188155e3
commit 109eb8ffa2
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
6 changed files with 694 additions and 29 deletions

View file

@ -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
View 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
View 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
}
}

View file

@ -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
}
}

View file

@ -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;

View file

@ -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(())