diff --git a/Cargo.lock b/Cargo.lock index f5695a9..93ff37d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,16 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "async-tempfile" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121280bd2055a6bfbc7ff5a14f700a38b2e127cb8b4066b7ef7320421600dff0" -dependencies = [ - "tokio", - "uuid", -] - [[package]] name = "atty" version = "0.2.14" @@ -139,12 +129,12 @@ dependencies = [ name = "dtmt" version = "0.1.0" dependencies = [ - "async-tempfile", "clap", "color-eyre", "futures", "futures-util", "glob", + "nanorand", "pin-project-lite", "tempfile", "tokio", @@ -262,17 +252,6 @@ dependencies = [ "slab", ] -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "gimli" version = "0.26.2" @@ -366,6 +345,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -713,15 +698,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "uuid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" -dependencies = [ - "getrandom", -] - [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6775e7c..5abe55c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,10 @@ edition = "2021" [dependencies] clap = { version = "4.0.15", features = ["color", "std", "cargo", "unicode"] } color-eyre = "0.6.2" +futures = "0.3.25" futures-util = "0.3.24" glob = "0.3.0" +nanorand = "0.7.0" pin-project-lite = "0.2.9" tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util"] } tokio-stream = { version = "0.1.11", features = ["fs"] } @@ -16,6 +18,4 @@ tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } [dev-dependencies] -async-tempfile = "0.2.0" -futures = "0.3.25" tempfile = "3.3.0" diff --git a/rustfmt.toml b/rustfmt.toml index f8c1871..a031646 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +unstable_features = true hard_tabs = false max_width = 100 edition = "2021" diff --git a/src/bin/cmd/build.rs b/src/bin/cmd/build.rs index 557bb7d..a7089bb 100644 --- a/src/bin/cmd/build.rs +++ b/src/bin/cmd/build.rs @@ -1,9 +1,9 @@ -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; +use std::sync::Arc; use clap::{value_parser, Arg, ArgMatches, Command}; use color_eyre::eyre::Result; - -use dtmt::Context; +use tokio::sync::RwLock; pub(crate) fn command_definition() -> Command { Command::new("build").about("Build a project").arg( @@ -19,6 +19,6 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { unimplemented!() } diff --git a/src/bin/cmd/bundle/decompress.rs b/src/bin/cmd/bundle/decompress.rs index 2b35be9..804f489 100644 --- a/src/bin/cmd/bundle/decompress.rs +++ b/src/bin/cmd/bundle/decompress.rs @@ -1,8 +1,17 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; -use color_eyre::eyre::Result; +use color_eyre::eyre::{self, Context, Result}; +use color_eyre::{Help, SectionExt}; + +use dtmt::decompress; +use futures::future::try_join_all; +use tokio::fs::{self, File}; +use tokio::io::{BufReader, BufWriter}; +use tokio::sync::RwLock; + +use crate::cmd::util::collect_bundle_paths; pub(crate) fn command_definition() -> Command { Command::new("decompress") @@ -11,16 +20,6 @@ pub(crate) fn command_definition() -> Command { This is mostly useful for staring at the decompressed data in a hex editor,\n\ as neither the game nor this tool can read the decompressed bundles.", ) - .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) @@ -43,7 +42,81 @@ pub(crate) fn command_definition() -> Command { ) } -#[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { - unimplemented!() +#[tracing::instrument(skip(ctx))] +async fn decompress_bundle( + ctx: Arc>, + bundle: P1, + destination: P2, +) -> Result<()> +where + P1: AsRef + std::fmt::Debug, + P2: AsRef + std::fmt::Debug, +{ + let in_file = File::open(bundle).await?; + let out_file = File::create(destination).await?; + + decompress(ctx, BufReader::new(in_file), BufWriter::new(out_file)).await +} + +#[tracing::instrument(skip_all)] +pub(crate) async fn run(ctx: Arc>, matches: &ArgMatches) -> Result<()> { + let bundles = matches + .get_many::("bundle") + .unwrap_or_default() + .cloned(); + let out_path = matches + .get_one::("destination") + .expect("required parameter 'destination' is missing"); + + let is_dir = { + let meta = fs::metadata(out_path) + .await + .wrap_err("failed to access destination path") + .with_section(|| out_path.display().to_string().header("Path:"))?; + + meta.is_dir() + }; + + let paths = collect_bundle_paths(bundles).await; + + if paths.is_empty() { + return Err(eyre::eyre!("No bundle provided")); + } + + if paths.len() == 1 { + let bundle = &paths[0]; + let name = bundle.file_name(); + + if is_dir && name.is_some() { + decompress_bundle(ctx, bundle, out_path.join(name.unwrap())).await?; + } else { + decompress_bundle(ctx, bundle, out_path).await?; + } + } else { + if !is_dir { + return Err(eyre::eyre!( + "Multiple bundles provided, but destination is not a directory." + )) + .with_section(|| out_path.display().to_string().header("Path:"))?; + } + + let _ = try_join_all(paths.into_iter().map(|p| async { + let ctx = ctx.clone(); + async move { + let name = if let Some(name) = p.file_name() { + name + } else { + return Err(eyre::eyre!("Invalid bundle path. No file name.")) + .with_section(|| p.display().to_string().header("Path:"))?; + }; + + let dest = out_path.join(name); + decompress_bundle(ctx, p, dest).await + } + .await + })) + .await?; + } + + Ok(()) } diff --git a/src/bin/cmd/bundle/extract.rs b/src/bin/cmd/bundle/extract.rs index b94340c..83aef46 100644 --- a/src/bin/cmd/bundle/extract.rs +++ b/src/bin/cmd/bundle/extract.rs @@ -4,8 +4,7 @@ use std::sync::Arc; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use color_eyre::eyre::Result; use glob::Pattern; - -use dtmt::Context; +use tokio::sync::RwLock; fn parse_glob_pattern(s: &str) -> Result { match Pattern::new(s) { @@ -76,16 +75,6 @@ pub(crate) fn command_definition() -> Command { are supported for this.", ), ) - .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("ljd").long("ljd").help( "Path to a custom ljd executable. If not set, \ `ljd` will be called from PATH.", @@ -102,6 +91,6 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { unimplemented!() } diff --git a/src/bin/cmd/bundle/list.rs b/src/bin/cmd/bundle/list.rs index 2780197..f71c95f 100644 --- a/src/bin/cmd/bundle/list.rs +++ b/src/bin/cmd/bundle/list.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use color_eyre::eyre::Result; - -use dtmt::Context; +use tokio::sync::RwLock; pub(crate) fn command_definition() -> Command { Command::new("list") @@ -38,6 +37,6 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { unimplemented!() } diff --git a/src/bin/cmd/bundle/mod.rs b/src/bin/cmd/bundle/mod.rs index 6a3ffca..78139ae 100644 --- a/src/bin/cmd/bundle/mod.rs +++ b/src/bin/cmd/bundle/mod.rs @@ -1,9 +1,8 @@ use std::sync::Arc; -use clap::{ArgMatches, Command}; +use clap::{Arg, ArgMatches, Command}; use color_eyre::eyre::Result; - -use dtmt::Context; +use tokio::sync::RwLock; mod decompress; mod extract; @@ -13,13 +12,31 @@ pub(crate) fn command_definition() -> Command { Command::new("bundle") .subcommand_required(true) .about("Manipulate the game's bundle files") + .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.", + ), + ) .subcommand(decompress::command_definition()) .subcommand(extract::command_definition()) .subcommand(list::command_definition()) } #[tracing::instrument(skip_all)] -pub(crate) async fn run(ctx: Arc, matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(ctx: Arc>, matches: &ArgMatches) -> Result<()> { + let oodle_bin = matches + .get_one::("oodle") + .expect("no default value for 'oodle' parameter"); + { + let mut ctx = ctx.write().await; + ctx.oodle = Some(oodle_bin.clone()); + } + match matches.subcommand() { Some(("decompress", sub_matches)) => decompress::run(ctx, sub_matches).await, Some(("extract", sub_matches)) => extract::run(ctx, sub_matches).await, diff --git a/src/bin/cmd/murmur.rs b/src/bin/cmd/murmur.rs index 3c53b32..51cdd2a 100644 --- a/src/bin/cmd/murmur.rs +++ b/src/bin/cmd/murmur.rs @@ -2,8 +2,7 @@ use std::sync::Arc; use clap::{Arg, ArgAction, ArgMatches, Command}; use color_eyre::eyre::Result; - -use dtmt::Context; +use tokio::sync::RwLock; pub(crate) fn command_definition() -> Command { Command::new("murmur") @@ -32,6 +31,6 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { unimplemented!() } diff --git a/src/bin/cmd/new.rs b/src/bin/cmd/new.rs index c89361d..b076823 100644 --- a/src/bin/cmd/new.rs +++ b/src/bin/cmd/new.rs @@ -2,8 +2,7 @@ use std::sync::Arc; use clap::{Arg, ArgMatches, Command}; use color_eyre::eyre::Result; - -use dtmt::Context; +use tokio::sync::RwLock; pub(crate) fn command_definition() -> Command { Command::new("new") @@ -18,6 +17,6 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { unimplemented!() } diff --git a/src/bin/cmd/util.rs b/src/bin/cmd/util.rs new file mode 100644 index 0000000..f5846d9 --- /dev/null +++ b/src/bin/cmd/util.rs @@ -0,0 +1,131 @@ +use std::ffi::OsStr; +use std::io; +use std::path::{Path, PathBuf}; + +use tokio::fs; +use tokio_stream::wrappers::ReadDirStream; +use tokio_stream::StreamExt; + +#[tracing::instrument] +pub async fn resolve_bundle_path

(path: P) -> Vec +where + P: AsRef + std::fmt::Debug, +{ + let dir = match fs::read_dir(path.as_ref()).await { + Ok(dir) => { + tracing::trace!(is_dir = true); + dir + } + Err(err) => { + if err.kind() != io::ErrorKind::NotADirectory { + tracing::error!(%err, "Failed to read path"); + } + let paths = vec![PathBuf::from(path.as_ref())]; + tracing::debug!(is_dir = false, resolved_paths = ?paths); + return paths; + } + }; + + let stream = ReadDirStream::new(dir); + let paths: Vec = stream + .filter_map(|entry| { + if let Ok(path) = entry.map(|e| e.path()) { + match path.file_name().and_then(OsStr::to_str) { + Some(name) if name.len() == 16 => { + if name.chars().all(|c| c.is_ascii_hexdigit()) { + Some(path) + } else { + None + } + } + _ => None, + } + } else { + None + } + }) + .collect() + .await; + + tracing::debug!(resolved_paths = ?paths); + + paths +} + +#[tracing::instrument(skip_all)] +pub async fn collect_bundle_paths(paths: I) -> Vec +where + I: Iterator + std::fmt::Debug, +{ + let tasks = paths.map(|p| async move { + match tokio::spawn(async move { resolve_bundle_path(&p).await }).await { + Ok(paths) => paths, + Err(err) => { + tracing::error!(%err, "failed to spawn task to resolve bundle paths"); + vec![] + } + } + }); + + let results = futures_util::future::join_all(tasks).await; + results.into_iter().flatten().collect() +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use tempfile::tempdir; + use tokio::process::Command; + + use super::resolve_bundle_path; + + #[tokio::test] + async fn resolve_single_file() { + let path = PathBuf::from("foo"); + let paths = resolve_bundle_path(&path).await; + assert_eq!(paths.len(), 1); + assert_eq!(paths[0], path); + } + + #[tokio::test] + async fn resolve_empty_directory() { + let dir = tempdir().expect("failed to create temporary directory"); + let paths = resolve_bundle_path(dir).await; + assert!(paths.is_empty()); + } + + #[tokio::test] + async fn resolve_mixed_directory() { + let dir = tempdir().expect("failed to create temporary directory"); + let temp_dir = dir.path(); + + let bundle_names = ["000957451622b061", "000b7a0d86775831", "00231e322d01c363"]; + let other_names = ["settings.ini", "metadata_database.db"]; + let _ = futures::future::try_join_all( + bundle_names + .into_iter() + .chain(other_names.into_iter()) + .map(|name| async move { + Command::new("touch") + .arg(name) + .current_dir(temp_dir) + .status() + .await?; + + Ok::<_, std::io::Error>(name) + }), + ) + .await + .expect("failed to create temporary files"); + + let paths = resolve_bundle_path(dir).await; + + assert_eq!(bundle_names.len(), paths.len()); + + for p in paths.iter() { + let name = p.file_name().and_then(std::ffi::OsStr::to_str).unwrap(); + assert!(bundle_names.iter().find(|&n| n == &name).is_some()); + } + } +} diff --git a/src/bin/cmd/watch.rs b/src/bin/cmd/watch.rs index d3e9d96..07e8b4a 100644 --- a/src/bin/cmd/watch.rs +++ b/src/bin/cmd/watch.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use clap::{value_parser, Arg, ArgMatches, Command}; use color_eyre::eyre::Result; - -use dtmt::Context; +use tokio::sync::RwLock; pub(crate) fn command_definition() -> Command { Command::new("watch") @@ -22,6 +21,6 @@ pub(crate) fn command_definition() -> Command { } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: Arc, _matches: &ArgMatches) -> Result<()> { +pub(crate) async fn run(_ctx: Arc>, _matches: &ArgMatches) -> Result<()> { unimplemented!() } diff --git a/src/bin/dtmt.rs b/src/bin/dtmt.rs index d947c00..5118c0b 100644 --- a/src/bin/dtmt.rs +++ b/src/bin/dtmt.rs @@ -1,9 +1,11 @@ #![feature(io_error_more)] +#![feature(let_chains)] use std::sync::Arc; use clap::{command, Arg, ArgAction}; use color_eyre::eyre::Result; +use tokio::sync::RwLock; use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; @@ -15,6 +17,7 @@ mod cmd { pub mod bundle; pub mod murmur; pub mod new; + mod util; pub mod watch; } @@ -55,13 +58,14 @@ async fn main() -> Result<()> { } let ctx = Context::new(); + let ctx = Arc::new(RwLock::new(ctx)); match matches.subcommand() { - Some(("bundle", sub_matches)) => cmd::bundle::run(Arc::new(ctx), sub_matches).await?, - Some(("murmur", sub_matches)) => cmd::murmur::run(Arc::new(ctx), sub_matches).await?, - Some(("new", sub_matches)) => cmd::new::run(Arc::new(ctx), sub_matches).await?, - Some(("build", sub_matches)) => cmd::build::run(Arc::new(ctx), sub_matches).await?, - Some(("watch", sub_matches)) => cmd::watch::run(Arc::new(ctx), sub_matches).await?, + Some(("bundle", sub_matches)) => cmd::bundle::run(ctx, sub_matches).await?, + Some(("murmur", sub_matches)) => cmd::murmur::run(ctx, sub_matches).await?, + Some(("new", sub_matches)) => cmd::new::run(ctx, sub_matches).await?, + Some(("build", sub_matches)) => cmd::build::run(ctx, sub_matches).await?, + Some(("watch", sub_matches)) => cmd::watch::run(ctx, sub_matches).await?, _ => unreachable!( "clap is configured to require a subcommand, and they're all handled above" ), diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs new file mode 100644 index 0000000..e33f3e6 --- /dev/null +++ b/src/bundle/mod.rs @@ -0,0 +1,98 @@ +use std::io::SeekFrom; +use std::sync::Arc; + +use color_eyre::eyre::{self, Context, Result}; +use color_eyre::{Help, SectionExt}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; +use tokio::sync::RwLock; + +use crate::oodle; + +#[derive(Debug, PartialEq)] +enum BundleFormat { + Darktide, +} + +impl TryFrom for BundleFormat { + type Error = color_eyre::Report; + + fn try_from(value: u32) -> Result { + match value { + 0xF0000007 => Ok(Self::Darktide), + _ => Err(eyre::eyre!("Unknown bundle format '{:08X}'", value)), + } + } +} + +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"); + + 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 + } + } else { + res + } +} + +/// Returns a decompressed version of the bundle data. +/// This is mainly useful for debugging purposes or +/// to manullay inspect the raw data. +#[tracing::instrument(skip(ctx, r, w))] +pub async fn decompress(ctx: Arc>, mut r: R, mut w: W) -> Result<()> +where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + W: AsyncWrite + std::marker::Unpin, +{ + let format = read_u32(&mut r).await.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 i64; + + // Skip unknown 256 bytes + r.seek(SeekFrom::Current(256)).await?; + // Skip file meta + r.seek(SeekFrom::Current(num_entries * 20)).await?; + + let num_chunks = read_u32(&mut r).await? as usize; + // Skip chunk sizes + r.seek(SeekFrom::Current(num_chunks as i64 * 4)).await?; + + { + let size_1 = read_u32(&mut r).await?; + + // Skip unknown 4 bytes + r.seek(SeekFrom::Current(4)).await?; + + // NOTE: Unknown why there sometimes is a second value. + if size_1 == 0x0 { + // Skip unknown 4 bytes + r.seek(SeekFrom::Current(8)).await?; + } + } + + let chunks_start = r.stream_position().await?; + + { + // Pipe the header into the output + r.seek(SeekFrom::Start(0)).await?; + let mut buf = vec![0; chunks_start as usize]; + r.read_exact(&mut buf).await?; + w.write_all(&buf).await?; + } + + oodle::decompress(ctx, r, w, num_chunks).await +} diff --git a/src/context.rs b/src/context.rs index 0436bf5..79c73fd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,8 +1,10 @@ -pub struct Context {} +pub struct Context { + pub oodle: Option, +} impl Context { pub fn new() -> Self { - Self {} + Self { oodle: None } } } diff --git a/src/lib.rs b/src/lib.rs index b7c5344..a25ce0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,6 @@ +mod bundle; mod context; +mod oodle; + +pub use bundle::decompress; pub use context::Context; diff --git a/src/oodle.rs b/src/oodle.rs new file mode 100644 index 0000000..1c83ed5 --- /dev/null +++ b/src/oodle.rs @@ -0,0 +1,106 @@ +use std::process::Stdio; +use std::sync::Arc; + +use color_eyre::eyre::Context; +use color_eyre::{eyre, Help, Result, SectionExt}; +use nanorand::Rng; +use tokio::fs::File; +use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, BufReader, BufWriter}; +use tokio::process::Command; +use tokio::sync::RwLock; +use tokio::{fs, io}; +use tracing::Instrument; + +#[tracing::instrument(level = "debug", skip(ctx, r, w))] +pub(crate) async fn decompress( + ctx: Arc>, + r: R, + w: W, + num_chunks: usize, +) -> Result<()> +where + R: AsyncRead + AsyncSeek + std::marker::Unpin, + W: AsyncWrite + std::marker::Unpin, +{ + let mut r = BufReader::new(r); + let mut w = BufWriter::new(w); + + let padding_start = r.stream_position().await?; + + let mut rng = nanorand::WyRand::new(); + let leaf = rng.generate::(); + + let tmp_dir = std::env::temp_dir().join(format!("dtmt-{}", leaf)); + + fs::create_dir(&tmp_dir).await?; + tracing::trace!(tmp_dir = %tmp_dir.display()); + + let in_path = tmp_dir.join("in.bin"); + let out_path = tmp_dir.join("out.bin"); + + { + let mut in_file = File::create(&in_path).await?; + io::copy(&mut r, &mut in_file) + .await + .wrap_err("failed to write compressed data to file") + .with_section(|| in_path.display().to_string().header("Path"))?; + } + + { + let _span = tracing::span!(tracing::Level::INFO, "Run decompression helper"); + async { + let mut cmd = { + let ctx = ctx.read().await; + Command::new(ctx.oodle.as_ref().expect("`oodle` arg not passed through")) + }; + + let cmd = cmd + .args(["-v", "-v", "-v"]) + .args(["--padding", &padding_start.to_string()]) + .args(["--chunks", &num_chunks.to_string()]) + .arg("decompress") + .arg(&in_path) + .arg(&out_path) + .stdin(Stdio::null()); + + tracing::debug!(?cmd, "Running Oodle decompression helper"); + + let res = cmd + .output() + .await + .wrap_err("failed to spawn the Oodle decompression helper")?; + + tracing::trace!( + "Output of Oodle decompression helper:\n{}", + String::from_utf8_lossy(&res.stdout) + ); + + 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") + .with_section(move || stdout.to_string().header("Logs:")) + .with_section(move || stderr.to_string().header("Stderr:"))); + } + + Ok(()) + } + .instrument(_span) + .await + .with_section(|| tmp_dir.display().to_string().header("Temp Dir:"))? + } + + { + let mut out_file = File::open(&out_path).await?; + io::copy(&mut out_file, &mut w) + .await + .wrap_err("failed to read decompressed file") + .with_section(|| out_path.display().to_string().header("Path"))?; + } + + fs::remove_dir_all(tmp_dir) + .await + .wrap_err("failed to remove temporary directory")?; + + Ok(()) +}