mod casc;
mod commands;
pub mod exit_codes;
mod targets;
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
const DEBUG: bool = false;
#[derive(Debug)]
pub enum AppError {
Cancelled(&'static str),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Cancelled(op) => write!(f, "{} cancelled by user", op),
}
}
}
impl std::error::Error for AppError {}
pub static CANCELLED: AtomicBool = AtomicBool::new(false);
#[derive(Parser, Debug)]
#[command(
name = "casc",
version,
about = "Cross-platform CLI tool for Blizzard CASC archives"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug, PartialEq)]
enum Commands {
#[command(alias = "l")]
List {
archive_dir: PathBuf,
#[arg(verbatim_doc_comment)]
targets: Vec<String>,
},
#[command(alias = "x")]
Extract {
archive_dir: PathBuf,
#[arg(verbatim_doc_comment)]
targets: Vec<String>,
#[arg(short = 'o', long = "output", default_value = ".")]
output: PathBuf,
#[arg(short = 'f', long = "flatten")]
flatten: bool,
},
}
fn main() {
ctrlc::set_handler(move || {
CANCELLED.store(true, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
let cli = Cli::parse();
match run(cli) {
Ok(exit_code) => std::process::exit(exit_code),
Err(e) => std::process::exit(handle_error(e)),
}
}
fn handle_error(e: anyhow::Error) -> i32 {
if let Some(io_err) = e.downcast_ref::<std::io::Error>()
&& io_err.kind() == std::io::ErrorKind::BrokenPipe
{
return exit_codes::SIGPIPE;
}
if let Some(app_err) = e.downcast_ref::<AppError>() {
match app_err {
AppError::Cancelled(_) => {
if DEBUG {
eprintln!("Debug: {}", e);
}
return exit_codes::SIGINT;
}
}
}
eprintln!("Error: {}", e);
exit_codes::ERROR
}
fn run(cli: Cli) -> Result<i32> {
match cli.command {
Commands::List {
archive_dir,
targets,
} => commands::list::execute(&archive_dir, &targets),
Commands::Extract {
archive_dir,
targets,
output,
flatten,
} => commands::extract::execute(&archive_dir, &targets, &output, flatten),
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use clap::CommandFactory;
use std::sync::Mutex;
pub static CANCEL_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_handle_error_broken_pipe() {
let err = anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"Broken pipe",
));
assert_eq!(handle_error(err), exit_codes::SIGPIPE);
}
#[test]
fn test_handle_error_other() {
let err = anyhow::Error::msg("Some other error");
assert_eq!(handle_error(err), exit_codes::ERROR);
}
#[test]
fn test_handle_error_cancellation() {
let err = anyhow::Error::new(AppError::Cancelled( "Listing"));
assert_eq!(handle_error(err), exit_codes::SIGINT);
let err = anyhow::Error::new(AppError::Cancelled( "Extraction"));
assert_eq!(handle_error(err), exit_codes::SIGINT);
}
#[test]
fn test_cli_parsing_list() {
let cli = Cli::parse_from(["casc", "list", "/path/to/archive", "target1", "target2"]);
match cli.command {
Commands::List {
archive_dir,
targets,
} => {
assert_eq!(archive_dir, PathBuf::from("/path/to/archive"));
assert_eq!(targets, vec!["target1", "target2"]);
}
_ => panic!("Expected List subcommand"),
}
}
#[test]
fn test_cli_parsing_alias_l() {
let cli = Cli::parse_from(["casc", "l", "/path/to/archive"]);
match cli.command {
Commands::List {
archive_dir,
targets,
} => {
assert_eq!(archive_dir, PathBuf::from("/path/to/archive"));
assert!(targets.is_empty());
}
_ => panic!("Expected List subcommand"),
}
}
#[test]
fn test_cli_parsing_extract() {
let cli = Cli::parse_from(["casc", "extract", "/path/to/archive", "target1"]);
match cli.command {
Commands::Extract {
archive_dir,
targets,
output,
flatten,
} => {
assert_eq!(archive_dir, PathBuf::from("/path/to/archive"));
assert_eq!(targets, vec!["target1"]);
assert_eq!(output, PathBuf::from("."));
assert!(!flatten);
}
_ => panic!("Expected Extract subcommand"),
}
}
#[test]
fn test_cli_missing_arg() {
let res = Cli::try_parse_from(["casc", "list"]);
assert!(res.is_err());
}
#[test]
fn test_cli_invalid_subcommand() {
let res = Cli::try_parse_from(["casc", "invalid"]);
assert!(res.is_err());
}
#[test]
fn test_cli_help() {
Cli::command().debug_assert();
}
#[test]
fn test_cli_version() {
let res = Cli::try_parse_from(["casc", "--version"]);
assert!(res.is_err()); let err = res.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
}
}