Better error handling

This commit is contained in:
Truman Kilen 2023-02-23 16:05:06 -06:00
parent 27e69115a3
commit f1fe922c2d
4 changed files with 114 additions and 48 deletions

View file

@ -368,17 +368,12 @@ impl Entry {
Some(Compression::Gzip) => decompress!(flate2::read::GzDecoder<&[u8]>), Some(Compression::Gzip) => decompress!(flate2::read::GzDecoder<&[u8]>),
Some(Compression::Oodle) => { Some(Compression::Oodle) => {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
return Err(super::Error::Other( return Err(super::Error::Oodle());
"Oodle compression only supported on Windows (or WINE)",
));
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
unsafe { unsafe {
let lib = libloading::Library::new("oo2core_9_win64.dll").map_err(|_| { let lib = libloading::Library::new("oo2core_9_win64.dll")
super::Error::Other( .map_err(|_| super::Error::OodleMissing())?;
"Could not find oo2core_9_win64.dll for Oodle compression",
)
})?;
/* /*
let set_printf: libloading::Symbol< let set_printf: libloading::Symbol<
@ -451,7 +446,7 @@ impl Entry {
3, 3,
); );
if out == 0 { if out == 0 {
return Err(super::Error::Other("decompression failed")); return Err(super::Error::DecompressionFailed(Compression::Oodle));
} else { } else {
assert_eq!( assert_eq!(
out as u64, self.uncompressed, out as u64, self.uncompressed,

View file

@ -1,35 +1,79 @@
#[derive(thiserror::Error, Debug)] use crate::Compression;
#[derive(thiserror::Error)]
pub enum Error { pub enum Error {
// dependency errors // dependency errors
#[error("enum conversion: {0}")] #[error("enum conversion: {0}")]
Strum(#[from] strum::ParseError), Strum(#[from] strum::ParseError),
#[error("key hash is an incorrect length")] #[error("key hash is an incorrect length")]
Aes, Aes,
#[error("malformed base64")] #[error("malformed base64")]
Base64, Base64,
// std errors // std errors
#[error("io error: {0}")] #[error("io error: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("utf8 conversion: {0}")] #[error("utf8 conversion: {0}")]
Utf8(#[from] std::string::FromUtf8Error), Utf8(#[from] std::string::FromUtf8Error),
#[error("utf16 conversion: {0}")] #[error("utf16 conversion: {0}")]
Utf16(#[from] std::string::FromUtf16Error), Utf16(#[from] std::string::FromUtf16Error),
#[error("bufwriter dereference: {0}")] #[error("bufwriter dereference: {0}")]
IntoInner(#[from] std::io::IntoInnerError<std::io::BufWriter<Vec<u8>>>), IntoInner(#[from] std::io::IntoInnerError<std::io::BufWriter<Vec<u8>>>),
// crate errors // crate errors
#[error("got {0}, which is not a boolean")] #[error("got {0}, which is not a boolean")]
Bool(u8), Bool(u8),
#[error("found magic of {0:#x} instead of {:#x}", super::MAGIC)] #[error("found magic of {0:#x} instead of {:#x}", super::MAGIC)]
Magic(u32), Magic(u32),
#[error("Oodle compression only supported on Windows (or WINE)")]
Oodle(),
#[error("Could not find oo2core_9_win64.dll for Oodle compression")]
OodleMissing(),
#[error("No entry found at {0}")]
MissingEntry(String),
#[error("Prefix \"{prefix}\" does not match path \"{path}\"")]
PrefixMismatch { prefix: String, path: String },
#[error("Attempted to write to \"{0}\" which outside of output directory")]
WriteOutsideOutput(String),
#[error("Output directory is not empty: \"{0}\"")]
OutputNotEmpty(String),
#[error("Input is not a directory: \"{0}\"")]
InputNotADirectory(String),
#[error("{0} decompression failed")]
DecompressionFailed(Compression),
#[error("used version {used} but pak is version {version}")] #[error("used version {used} but pak is version {version}")]
Version { Version {
used: super::VersionMajor, used: super::VersionMajor,
version: super::VersionMajor, version: super::VersionMajor,
}, },
#[error("pak is encrypted but no key was provided")] #[error("pak is encrypted but no key was provided")]
Encrypted, Encrypted,
#[error("error with OsString")] #[error("error with OsString")]
OsString(std::ffi::OsString), OsString(std::ffi::OsString),
#[error("{0}")] #[error("{0}")]
Other(&'static str), Other(String),
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
} }

View file

@ -91,7 +91,7 @@ impl PakReader {
_ => continue, _ => continue,
} }
} }
Err(super::Error::Other("version unsupported")) Err(super::Error::Other("version unsupported".to_owned()))
} }
pub fn version(&self) -> super::Version { pub fn version(&self) -> super::Version {
@ -122,7 +122,7 @@ impl PakReader {
self.key.as_ref(), self.key.as_ref(),
writer, writer,
), ),
None => Err(super::Error::Other("no file found at given path")), None => Err(super::Error::MissingEntry(path.to_owned())),
} }
} }

View file

@ -53,6 +53,10 @@ struct ActionUnpack {
#[arg(short, long, default_value = "false")] #[arg(short, long, default_value = "false")]
verbose: bool, verbose: bool,
/// Force overwrite existing files/directories.
#[arg(short, long, default_value = "false")]
force: bool,
/// Files or directories to include. Can be specified multiple times. If not specified, everything is extracted. /// Files or directories to include. Can be specified multiple times. If not specified, everything is extracted.
#[arg(action = clap::ArgAction::Append, short, long)] #[arg(action = clap::ArgAction::Append, short, long)]
include: Vec<String>, include: Vec<String>,
@ -132,9 +136,6 @@ struct Args {
fn main() -> Result<(), repak::Error> { fn main() -> Result<(), repak::Error> {
let args = Args::parse(); let args = Args::parse();
//let aasdf = repak::Version::iter().map(|v| format!("{v}"));
//clap::builder::PossibleValuesParser::new(aasdf.map(|a| a.as_str()));
match args.action { match args.action {
Action::Info(args) => info(args), Action::Info(args) => info(args),
Action::List(args) => list(args), Action::List(args) => list(args),
@ -191,8 +192,10 @@ fn unpack(args: ActionUnpack) -> Result<(), repak::Error> {
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(e) => Err(e), Err(e) => Err(e),
}?; }?;
if output.read_dir()?.next().is_some() { if !args.force && output.read_dir()?.next().is_some() {
return Err(repak::Error::Other("output directory not empty")); return Err(repak::Error::OutputNotEmpty(
output.to_string_lossy().to_string(),
));
} }
let mount_point = PathBuf::from(pak.mount_point()); let mount_point = PathBuf::from(pak.mount_point());
let prefix = Path::new(&args.strip_prefix); let prefix = Path::new(&args.strip_prefix);
@ -203,43 +206,62 @@ fn unpack(args: ActionUnpack) -> Result<(), repak::Error> {
.map(|i| prefix.join(Path::new(i))) .map(|i| prefix.join(Path::new(i)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); struct UnpackEntry {
entry_path: String,
out_path: PathBuf,
out_dir: PathBuf,
}
pak.files().into_par_iter().try_for_each_init( let entries =
|| (File::open(&args.input), includes.clone(), counter.clone()), pak.files()
|(file, includes, counter), path| -> Result<(), repak::Error> { .into_iter()
let full_path = mount_point.join(&path); .map(|entry_path| {
if !includes.is_empty() && !includes.iter().any(|i| full_path.starts_with(i)) { let full_path = mount_point.join(&entry_path);
return Ok(()); if !includes.is_empty() && !includes.iter().any(|i| full_path.starts_with(i)) {
} return Ok(None);
}
let out_path = output
.join(full_path.strip_prefix(prefix).map_err(|_| {
repak::Error::PrefixMismatch {
path: full_path.to_string_lossy().to_string(),
prefix: prefix.to_string_lossy().to_string(),
}
})?)
.clean();
if !out_path.starts_with(&output) {
return Err(repak::Error::WriteOutsideOutput(
out_path.to_string_lossy().to_string(),
));
}
let out_dir = out_path.parent().expect("will be a file").to_path_buf();
Ok(Some(UnpackEntry {
entry_path,
out_path,
out_dir,
}))
})
.filter_map(|e| e.transpose())
.collect::<Result<Vec<_>, repak::Error>>()?;
entries.par_iter().try_for_each_init(
|| File::open(&args.input),
|file, entry| -> Result<(), repak::Error> {
if args.verbose { if args.verbose {
println!("extracting {path}"); println!("extracting {}", entry.entry_path);
} }
let file_path = output.join( fs::create_dir_all(&entry.out_dir)?;
full_path
.strip_prefix(prefix)
.map_err(|_| repak::Error::Other("prefix does not match"))?,
);
if !file_path.clean().starts_with(&output) {
return Err(repak::Error::Other(
"tried to write file outside of output directory",
));
}
fs::create_dir_all(file_path.parent().expect("will be a file"))?;
counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
pak.read_file( pak.read_file(
&path, &entry.entry_path,
&mut BufReader::new(file.as_ref().unwrap()), // TODO: avoid this unwrap &mut BufReader::new(file.as_ref().unwrap()), // TODO: avoid this unwrap
&mut fs::File::create(file_path)?, &mut fs::File::create(&entry.out_path)?,
) )
}, },
)?; )?;
println!( println!("Unpacked {} files to {}", entries.len(), output.display());
"Unpacked {} files to {}",
counter.load(std::sync::atomic::Ordering::Relaxed),
output.display()
);
Ok(()) Ok(())
} }
@ -264,7 +286,9 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> {
} }
let input_path = Path::new(&args.input); let input_path = Path::new(&args.input);
if !input_path.is_dir() { if !input_path.is_dir() {
return Err(repak::Error::Other("input is not a directory")); return Err(repak::Error::InputNotADirectory(
input_path.to_string_lossy().to_string(),
));
} }
let mut paths = vec![]; let mut paths = vec![];
collect_files(&mut paths, input_path)?; collect_files(&mut paths, input_path)?;
@ -309,7 +333,10 @@ fn get(args: ActionGet) -> Result<(), repak::Error> {
let full_path = mount_point.join(args.file); let full_path = mount_point.join(args.file);
let file = full_path let file = full_path
.strip_prefix(prefix) .strip_prefix(prefix)
.map_err(|_| repak::Error::Other("prefix does not match"))?; .map_err(|_| repak::Error::PrefixMismatch {
path: full_path.to_string_lossy().to_string(),
prefix: prefix.to_string_lossy().to_string(),
})?;
use std::io::Write; use std::io::Write;
std::io::stdout().write_all(&pak.get(&file.to_string_lossy(), &mut reader)?)?; std::io::stdout().write_all(&pak.get(&file.to_string_lossy(), &mut reader)?)?;