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:
parent
95414f1f11
commit
cf2503214b
17 changed files with 485 additions and 88 deletions
38
Cargo.lock
generated
38
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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
131
src/bin/cmd/util.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!()
|
||||
}
|
||||
|
|
|
@ -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
98
src/bundle/mod.rs
Normal 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
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
pub struct Context {}
|
||||
pub struct Context {
|
||||
pub oodle: Option<String>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
Self { oodle: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
mod bundle;
|
||||
mod context;
|
||||
mod oodle;
|
||||
|
||||
pub use bundle::decompress;
|
||||
pub use context::Context;
|
||||
|
|
106
src/oodle.rs
Normal file
106
src/oodle.rs
Normal 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(())
|
||||
}
|
Loading…
Add table
Reference in a new issue