feat: Implement raw file extraction

This commit is contained in:
Lucas Schwiderski 2022-11-05 10:31:59 +01:00
parent 1f44e0bdfc
commit 4b830d613b
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
8 changed files with 367 additions and 22 deletions

View file

@ -2,9 +2,14 @@ use std::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},
Help, Report, SectionExt,
};
use dtmt::Bundle;
use futures::future::try_join_all;
use glob::Pattern; use glob::Pattern;
use tokio::sync::RwLock; use tokio::{fs, sync::RwLock};
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) {
@ -13,6 +18,10 @@ fn parse_glob_pattern(s: &str) -> Result<Pattern, String> {
} }
} }
fn flatten_name(s: &str) -> String {
s.replace('/', "_")
}
pub(crate) fn command_definition() -> Command { pub(crate) fn command_definition() -> Command {
Command::new("extract") Command::new("extract")
.about("Extract files from the bundle(s).") .about("Extract files from the bundle(s).")
@ -75,22 +84,236 @@ pub(crate) fn command_definition() -> Command {
are supported for this.", are supported for this.",
), ),
) )
.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.",
)) )
.arg(Arg::new("revorb").long("revorb").help( .default_value("ljd"),
)
.arg(
Arg::new("revorb")
.long("revorb")
.help(
"Path to a custom revorb executable. If not set, \ "Path to a custom revorb executable. If not set, \
`revorb` will be called from PATH.", `revorb` will be called from PATH.",
)) )
.arg(Arg::new("ww2ogg").long("ww2ogg").help( .default_value("revorb"),
)
.arg(
Arg::new("ww2ogg")
.long("ww2ogg")
.help(
"Path to a custom ww2ogg executable. If not set, \ "Path to a custom ww2ogg executable. If not set, \
`ww2ogg` will be called from PATH.\nSee the documentation for how \ `ww2ogg` will be called from PATH.\nSee the documentation for how \
to set up the script.", to set up the script for this.",
)) )
.default_value("ww2ogg"),
)
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub(crate) async fn run(_ctx: Arc<RwLock<dtmt::Context>>, _matches: &ArgMatches) -> Result<()> { pub(crate) async fn run(ctx: Arc<RwLock<dtmt::Context>>, matches: &ArgMatches) -> Result<()> {
unimplemented!() {
let ljd_bin = matches
.get_one::<String>("ljd")
.expect("no default value for 'ljd' parameter");
let revorb_bin = matches
.get_one::<String>("revorb")
.expect("no default value for 'revorb' parameter");
let ww2ogg_bin = matches
.get_one::<String>("ww2ogg")
.expect("no default value for 'ww2ogg' parameter");
let mut ctx = ctx.write().await;
ctx.ljd = Some(ljd_bin.clone());
ctx.revorb = Some(revorb_bin.clone());
ctx.ww2ogg = Some(ww2ogg_bin.clone());
}
let includes = match matches.get_many::<Pattern>("include") {
Some(values) => values.collect(),
None => Vec::new(),
};
let excludes = match matches.get_many::<Pattern>("exclude") {
Some(values) => values.collect(),
None => Vec::new(),
};
let bundles = matches
.get_many::<PathBuf>("bundle")
.unwrap_or_default()
.cloned();
let bundles = try_join_all(bundles.into_iter().map(|p| async {
let ctx = ctx.clone();
let path_display = p.display().to_string();
async move { Bundle::open(ctx, &p).await }
.await
.with_section(|| path_display.header("Bundle Path:"))
}))
.await?;
let files: Vec<_> = bundles
.iter()
.flat_map(|bundle| bundle.files())
.filter(|file| {
let name = file.base_name();
// When there is no `includes`, all files are included
let is_included = includes.is_empty() || includes.iter().any(|glob| glob.matches(name));
// When there is no `excludes`, no file is excluded
let is_excluded =
!excludes.is_empty() && excludes.iter().any(|glob| glob.matches(name));
is_included && !is_excluded
})
.collect();
let should_decompile = matches.get_flag("decompile");
let should_flatten = matches.get_flag("flatten");
let is_dry_run = matches.get_flag("dry-run");
let dest = matches
.get_one::<PathBuf>("destination")
.expect("required argument 'destination' missing");
{
let res = match fs::metadata(&dest).await {
Ok(meta) if !meta.is_dir() => Err(eyre::eyre!("Destination path is not a directory")),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Err(eyre::eyre!("Destination path does not exist"))
.with_suggestion(|| "Create the directory")
}
Err(err) => Err(Report::new(err)),
_ => Ok(()),
};
if res.is_err() {
return res.wrap_err(format!(
"Failed to open destination directory: {}",
dest.display()
));
}
}
let mut tasks = Vec::with_capacity(files.len());
for file in files {
let name = file.name(should_decompile);
let data = if should_decompile {
file.decompiled(ctx.clone()).await
} else {
file.raw()
};
match data {
Ok(mut files) => {
match files.len() {
0 => {
println!(
"Decompilation did not produce any data for file {}",
file.name(should_decompile)
);
}
// For a single file we want to use the bundle file's name.
1 => {
// We already checked `files.len()`.
let file = files.pop().unwrap();
let name = file.name().unwrap_or(&name);
let name = if should_flatten {
flatten_name(name)
} else {
name.clone()
};
let mut path = dest.clone();
path.push(name);
if is_dry_run {
tracing::info!(path = %path.display(), "Writing file");
} else {
tracing::debug!(path = %path.display(), "Writing file");
tasks.push(tokio::spawn(async move {
fs::write(&path, file.data())
.await
.wrap_err("failed to write extracted file to disc")
.with_section(|| path.display().to_string().header("Path"))
}));
}
}
// For multiple files we create a directory and name files
// by index.
_ => {
for (i, file) in files.into_iter().enumerate() {
let mut path = dest.clone();
let name = file
.name()
.map(|name| {
if should_flatten {
flatten_name(name)
} else {
name.clone()
}
})
.unwrap_or(format!("{}", i));
path.push(name);
if is_dry_run {
tracing::info!(path = %path.display(), "Writing file");
} else {
tracing::debug!(path = %path.display(), "Writing file");
tasks.push(tokio::spawn(async move {
let parent = match path.parent() {
Some(parent) => parent,
None => {
eyre::bail!(
"Decompilation produced invalid path: {}",
&path.display()
)
}
};
fs::create_dir_all(parent)
.await
.wrap_err("failed to create parent directory")
.with_section(|| {
parent.display().to_string().header("Path")
})?;
fs::write(&path, file.data())
.await
.wrap_err("failed to write extracted file to disc")
.with_section(|| path.display().to_string().header("Path"))
}));
}
}
}
}
}
Err(err) => {
let err = err
.wrap_err("Failed to decompile")
.with_section(|| name.header("File"));
tracing::error!("{:#}", err);
}
};
}
let results = try_join_all(tasks).await?;
for res in results {
if let Err(err) = res {
tracing::error!("{:#}", err);
}
}
Ok(())
} }

View file

@ -61,7 +61,7 @@ pub(crate) async fn run(ctx: Arc<RwLock<dtmt::Context>>, matches: &ArgMatches) -
let v = &f.variants()[0]; let v = &f.variants()[0];
println!( println!(
"\t{}.{}: {} bytes", "\t{}.{}: {} bytes",
f.name(), f.base_name(),
f.file_type().ext_name(), f.file_type().ext_name(),
v.size() v.size()
); );

View file

@ -2,11 +2,13 @@ use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use color_eyre::{Help, Result, SectionExt}; use color_eyre::{Help, Result, SectionExt};
use futures::future::join_all;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::binary::*; use crate::binary::*;
use crate::context::lookup_hash; use crate::context::lookup_hash;
use crate::filetype::*;
use crate::murmur::{HashGroup, Murmur64}; use crate::murmur::{HashGroup, Murmur64};
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
@ -407,10 +409,18 @@ impl BundleFile {
}) })
} }
pub fn name(&self) -> &String { pub fn base_name(&self) -> &String {
&self.name &self.name
} }
pub fn name(&self, decompiled: bool) -> String {
if decompiled {
format!("{}.{}", self.name, self.file_type.decompiled_ext_name())
} else {
format!("{}.{}", self.name, self.file_type.ext_name())
}
}
pub fn hash(&self) -> Murmur64 { pub fn hash(&self) -> Murmur64 {
self.hash self.hash
} }
@ -422,4 +432,89 @@ impl BundleFile {
pub fn variants(&self) -> &Vec<BundleFileVariant> { pub fn variants(&self) -> &Vec<BundleFileVariant> {
&self.variants &self.variants
} }
pub fn raw(&self) -> Result<Vec<UserFile>> {
let files = self
.variants
.iter()
.map(|variant| UserFile {
data: variant.data().clone(),
name: Some(self.name(false)),
})
.collect();
Ok(files)
}
#[tracing::instrument(skip_all)]
pub async fn decompiled(&self, ctx: Arc<RwLock<crate::Context>>) -> Result<Vec<UserFile>> {
let file_type = self.file_type();
if tracing::enabled!(tracing::Level::DEBUG) {
tracing::debug!(
name = self.name(true),
variants = self.variants.len(),
"Attempting to decompile"
);
}
let tasks = self.variants.iter().map(|variant| {
let ctx = ctx.clone();
async move {
let res = match file_type {
BundleFileType::Lua => lua::decompile(ctx, variant.data()).await,
_ => {
tracing::debug!("Can't decompile, unknown file type");
Ok(vec![UserFile::with_name(
variant.data.clone(),
self.name(true),
)])
}
};
match res {
Ok(files) => files,
Err(err) => {
let err = err
.wrap_err("failed to decompile file")
.with_section(|| self.name(true).header("File:"));
tracing::error!("{}", err);
vec![]
}
}
}
});
let results = join_all(tasks).await;
Ok(results.into_iter().flatten().collect())
}
}
pub struct UserFile {
// TODO: Might be able to avoid some allocations with a Cow here
data: Vec<u8>,
name: Option<String>,
}
impl UserFile {
pub fn new(data: Vec<u8>) -> Self {
Self { data, name: None }
}
pub fn with_name(data: Vec<u8>, name: String) -> Self {
Self {
data,
name: Some(name),
}
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn name(&self) -> Option<&String> {
self.name.as_ref()
}
} }

View file

@ -14,7 +14,7 @@ use crate::context::lookup_hash;
use crate::murmur::{HashGroup, Murmur64}; use crate::murmur::{HashGroup, Murmur64};
use crate::oodle; use crate::oodle;
mod file; pub(crate) mod file;
use file::BundleFile; use file::BundleFile;

View file

@ -7,6 +7,9 @@ use crate::murmur::{Dictionary, HashGroup, Murmur32, Murmur64};
pub struct Context { pub struct Context {
pub lookup: Dictionary, pub lookup: Dictionary,
pub oodle: Option<String>, pub oodle: Option<String>,
pub ljd: Option<String>,
pub revorb: Option<String>,
pub ww2ogg: Option<String>,
} }
impl Context { impl Context {
@ -14,6 +17,9 @@ impl Context {
Self { Self {
lookup: Dictionary::new(), lookup: Dictionary::new(),
oodle: None, oodle: None,
ljd: None,
revorb: None,
ww2ogg: None,
} }
} }
} }

19
src/filetype/lua.rs Normal file
View file

@ -0,0 +1,19 @@
use std::io::Cursor;
use std::sync::Arc;
use color_eyre::Result;
use tokio::sync::RwLock;
use crate::bundle::file::UserFile;
#[tracing::instrument(skip_all,fields(buf_len = data.as_ref().len()))]
pub(crate) async fn decompile<T>(
_ctx: Arc<RwLock<crate::Context>>,
data: T,
) -> Result<Vec<UserFile>>
where
T: AsRef<[u8]>,
{
let mut _r = Cursor::new(data.as_ref());
todo!();
}

1
src/filetype/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod lua;

View file

@ -1,6 +1,7 @@
mod binary; mod binary;
mod bundle; mod bundle;
mod context; mod context;
mod filetype;
pub mod murmur; pub mod murmur;
mod oodle; mod oodle;