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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
@ -139,12 +129,12 @@ dependencies = [
|
||||||
name = "dtmt"
|
name = "dtmt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-tempfile",
|
|
||||||
"clap",
|
"clap",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glob",
|
"glob",
|
||||||
|
"nanorand",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -262,17 +252,6 @@ dependencies = [
|
||||||
"slab",
|
"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]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.26.2"
|
version = "0.26.2"
|
||||||
|
@ -366,6 +345,12 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nanorand"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
@ -713,15 +698,6 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uuid"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -6,8 +6,10 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.0.15", features = ["color", "std", "cargo", "unicode"] }
|
clap = { version = "4.0.15", features = ["color", "std", "cargo", "unicode"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
|
futures = "0.3.25"
|
||||||
futures-util = "0.3.24"
|
futures-util = "0.3.24"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
|
nanorand = "0.7.0"
|
||||||
pin-project-lite = "0.2.9"
|
pin-project-lite = "0.2.9"
|
||||||
tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util"] }
|
tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util"] }
|
||||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
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"] }
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
async-tempfile = "0.2.0"
|
|
||||||
futures = "0.3.25"
|
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
unstable_features = true
|
||||||
hard_tabs = false
|
hard_tabs = false
|
||||||
max_width = 100
|
max_width = 100
|
||||||
edition = "2021"
|
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 clap::{value_parser, Arg, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("build").about("Build a project").arg(
|
Command::new("build").about("Build a project").arg(
|
||||||
|
@ -19,6 +19,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
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 {
|
pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("decompress")
|
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\
|
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.",
|
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(
|
||||||
Arg::new("bundle")
|
Arg::new("bundle")
|
||||||
.required(true)
|
.required(true)
|
||||||
|
@ -43,7 +42,81 @@ pub(crate) fn command_definition() -> Command {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip(ctx))]
|
||||||
pub(crate) async fn run(_ctx: Arc<dtmt::Context>, _matches: &ArgMatches) -> Result<()> {
|
async fn decompress_bundle<P1, P2>(
|
||||||
unimplemented!()
|
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 clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use glob::Pattern;
|
use glob::Pattern;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
fn parse_glob_pattern(s: &str) -> Result<Pattern, String> {
|
fn parse_glob_pattern(s: &str) -> Result<Pattern, String> {
|
||||||
match Pattern::new(s) {
|
match Pattern::new(s) {
|
||||||
|
@ -76,16 +75,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
are supported for this.",
|
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(
|
.arg(Arg::new("ljd").long("ljd").help(
|
||||||
"Path to a custom ljd executable. If not set, \
|
"Path to a custom ljd executable. If not set, \
|
||||||
`ljd` will be called from PATH.",
|
`ljd` will be called from PATH.",
|
||||||
|
@ -102,6 +91,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("list")
|
Command::new("list")
|
||||||
|
@ -38,6 +37,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{ArgMatches, Command};
|
use clap::{Arg, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
mod decompress;
|
mod decompress;
|
||||||
mod extract;
|
mod extract;
|
||||||
|
@ -13,13 +12,31 @@ pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("bundle")
|
Command::new("bundle")
|
||||||
.subcommand_required(true)
|
.subcommand_required(true)
|
||||||
.about("Manipulate the game's bundle files")
|
.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(decompress::command_definition())
|
||||||
.subcommand(extract::command_definition())
|
.subcommand(extract::command_definition())
|
||||||
.subcommand(list::command_definition())
|
.subcommand(list::command_definition())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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() {
|
match matches.subcommand() {
|
||||||
Some(("decompress", sub_matches)) => decompress::run(ctx, sub_matches).await,
|
Some(("decompress", sub_matches)) => decompress::run(ctx, sub_matches).await,
|
||||||
Some(("extract", sub_matches)) => extract::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 clap::{Arg, ArgAction, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("murmur")
|
Command::new("murmur")
|
||||||
|
@ -32,6 +31,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{Arg, ArgMatches, Command};
|
use clap::{Arg, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("new")
|
Command::new("new")
|
||||||
|
@ -18,6 +17,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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!()
|
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 clap::{value_parser, Arg, ArgMatches, Command};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dtmt::Context;
|
|
||||||
|
|
||||||
pub(crate) fn command_definition() -> Command {
|
pub(crate) fn command_definition() -> Command {
|
||||||
Command::new("watch")
|
Command::new("watch")
|
||||||
|
@ -22,6 +21,6 @@ pub(crate) fn command_definition() -> Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[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!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
#![feature(io_error_more)]
|
#![feature(io_error_more)]
|
||||||
|
#![feature(let_chains)]
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{command, Arg, ArgAction};
|
use clap::{command, Arg, ArgAction};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
@ -15,6 +17,7 @@ mod cmd {
|
||||||
pub mod bundle;
|
pub mod bundle;
|
||||||
pub mod murmur;
|
pub mod murmur;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
|
mod util;
|
||||||
pub mod watch;
|
pub mod watch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,13 +58,14 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx = Context::new();
|
let ctx = Context::new();
|
||||||
|
let ctx = Arc::new(RwLock::new(ctx));
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
Some(("bundle", sub_matches)) => cmd::bundle::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(Arc::new(ctx), sub_matches).await?,
|
Some(("murmur", sub_matches)) => cmd::murmur::run(ctx, sub_matches).await?,
|
||||||
Some(("new", sub_matches)) => cmd::new::run(Arc::new(ctx), sub_matches).await?,
|
Some(("new", sub_matches)) => cmd::new::run(ctx, sub_matches).await?,
|
||||||
Some(("build", sub_matches)) => cmd::build::run(Arc::new(ctx), sub_matches).await?,
|
Some(("build", sub_matches)) => cmd::build::run(ctx, sub_matches).await?,
|
||||||
Some(("watch", sub_matches)) => cmd::watch::run(Arc::new(ctx), sub_matches).await?,
|
Some(("watch", sub_matches)) => cmd::watch::run(ctx, sub_matches).await?,
|
||||||
_ => unreachable!(
|
_ => unreachable!(
|
||||||
"clap is configured to require a subcommand, and they're all handled above"
|
"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 {
|
impl Context {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {}
|
Self { oodle: None }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
|
mod bundle;
|
||||||
mod context;
|
mod context;
|
||||||
|
mod oodle;
|
||||||
|
|
||||||
|
pub use bundle::decompress;
|
||||||
pub use context::Context;
|
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