feat: Implement bundle decompression

It does share a large portion of logic with the actual opening of
bundles. But trying to combine everything would only make things more
complex.
This commit is contained in:
Lucas Schwiderski 2022-11-01 17:35:47 +01:00
parent 95414f1f11
commit cf2503214b
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
17 changed files with 485 additions and 88 deletions

38
Cargo.lock generated
View file

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

View file

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

View file

@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
unstable_features = true
hard_tabs = false
max_width = 100
edition = "2021"

View file

@ -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<Context>, _matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
}

View file

@ -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<dtmt::Context>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
#[tracing::instrument(skip(ctx))]
async fn decompress_bundle<P1, P2>(
ctx: Arc<RwLock<dtmt::Context>>,
bundle: P1,
destination: P2,
) -> Result<()>
where
P1: AsRef<Path> + std::fmt::Debug,
P2: AsRef<Path> + 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<RwLock<dtmt::Context>>, matches: &ArgMatches) -> Result<()> {
let bundles = matches
.get_many::<PathBuf>("bundle")
.unwrap_or_default()
.cloned();
let out_path = matches
.get_one::<PathBuf>("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(())
}

View file

@ -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<Pattern, String> {
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<Context>, _matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
}

View file

@ -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<Context>, _matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
}

View file

@ -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<Context>, matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(ctx: Arc<RwLock<dtmt::Context>>, matches: &ArgMatches) -> Result<()> {
let oodle_bin = matches
.get_one::<String>("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,

View file

@ -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<Context>, _matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
}

View file

@ -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<Context>, _matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
}

131
src/bin/cmd/util.rs Normal file
View file

@ -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<P>(path: P) -> Vec<PathBuf>
where
P: AsRef<Path> + 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<PathBuf> = 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<I>(paths: I) -> Vec<PathBuf>
where
I: Iterator<Item = PathBuf> + 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());
}
}
}

View file

@ -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<Context>, _matches: &ArgMatches) -> Result<()> {
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> {
unimplemented!()
}

View file

@ -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"
),

98
src/bundle/mod.rs Normal file
View file

@ -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<u32> for BundleFormat {
type Error = color_eyre::Report;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0xF0000007 => Ok(Self::Darktide),
_ => Err(eyre::eyre!("Unknown bundle format '{:08X}'", value)),
}
}
}
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");
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<R, W>(ctx: Arc<RwLock<crate::Context>>, 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
}

View file

@ -1,8 +1,10 @@
pub struct Context {}
pub struct Context {
pub oodle: Option<String>,
}
impl Context {
pub fn new() -> Self {
Self {}
Self { oodle: None }
}
}

View file

@ -1,2 +1,6 @@
mod bundle;
mod context;
mod oodle;
pub use bundle::decompress;
pub use context::Context;

106
src/oodle.rs Normal file
View file

@ -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<R, W>(
ctx: Arc<RwLock<crate::Context>>,
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::<u64>();
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(())
}