use clap::{Parser, Subcommand, ValueEnum};
use indicatif::{ProgressBar, ProgressStyle};
use pklib::{explode_bytes, implode_bytes, CompressionMode, DictionarySize};
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Parser)]
#[command(name = "blast-cli")]
#[command(about = "A CLI tool for PKWare DCL compression and decompression")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
}
#[derive(Subcommand)]
enum Commands {
Compress {
input: PathBuf,
output: PathBuf,
#[arg(short, long, value_enum, default_value_t = CliCompressionMode::Binary)]
mode: CliCompressionMode,
#[arg(short, long, value_enum, default_value_t = CliDictionarySize::Size2K)]
dict_size: CliDictionarySize,
#[arg(short, long)]
force: bool,
},
Decompress {
input: PathBuf,
output: PathBuf,
#[arg(short, long)]
force: bool,
},
Info {
input: PathBuf,
},
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum CliCompressionMode {
Binary,
Ascii,
}
impl From<CliCompressionMode> for CompressionMode {
fn from(mode: CliCompressionMode) -> Self {
match mode {
CliCompressionMode::Binary => CompressionMode::Binary,
CliCompressionMode::Ascii => CompressionMode::ASCII,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum CliDictionarySize {
Size1K,
Size2K,
Size4K,
}
impl From<CliDictionarySize> for DictionarySize {
fn from(size: CliDictionarySize) -> Self {
match size {
CliDictionarySize::Size1K => DictionarySize::Size1K,
CliDictionarySize::Size2K => DictionarySize::Size2K,
CliDictionarySize::Size4K => DictionarySize::Size4K,
}
}
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Commands::Compress {
input,
output,
mode,
dict_size,
force,
} => compress_file(
&input,
&output,
mode.into(),
dict_size.into(),
force,
cli.verbose,
cli.quiet,
),
Commands::Decompress {
input,
output,
force,
} => decompress_file(&input, &output, force, cli.verbose, cli.quiet),
Commands::Info { input } => show_file_info(&input, cli.verbose),
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
fn compress_file(
input: &PathBuf,
output: &PathBuf,
mode: CompressionMode,
dict_size: DictionarySize,
force: bool,
verbose: bool,
quiet: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if !input.exists() {
return Err(format!("Input file '{}' does not exist", input.display()).into());
}
if output.exists() && !force {
return Err(format!(
"Output file '{}' already exists. Use --force to overwrite",
output.display()
)
.into());
}
if verbose {
println!(
"Compressing '{}' to '{}'",
input.display(),
output.display()
);
println!("Mode: {mode:?}, Dictionary: {dict_size:?}");
}
let start_time = Instant::now();
let input_data = fs::read(input)?;
let input_size = input_data.len();
if verbose {
println!("Input size: {input_size} bytes");
}
let progress = if !quiet && input_size > 1024 * 1024 {
let pb = ProgressBar::new(2);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}",
)
.unwrap()
.progress_chars("#>-"),
);
pb.set_message("Compressing...");
Some(pb)
} else {
None
};
if let Some(ref pb) = progress {
pb.inc(1);
}
let compressed_data = implode_bytes(&input_data, mode, dict_size)
.map_err(|e| format!("Compression failed: {e}"))?;
if let Some(ref pb) = progress {
pb.inc(1);
pb.finish_with_message("Compression complete");
}
fs::write(output, &compressed_data)?;
let compression_time = start_time.elapsed();
let output_size = compressed_data.len();
let compression_ratio = (output_size as f64 / input_size as f64) * 100.0;
if !quiet {
println!("✓ Compression successful!");
println!(" Input: {input_size} bytes");
println!(" Output: {output_size} bytes");
println!(" Ratio: {compression_ratio:.1}%");
println!(" Time: {compression_time:.2?}");
if compression_ratio > 100.0 {
println!(" Note: File expanded during compression (common for small/random data)");
}
}
Ok(())
}
fn decompress_file(
input: &PathBuf,
output: &PathBuf,
force: bool,
verbose: bool,
quiet: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if !input.exists() {
return Err(format!("Input file '{}' does not exist", input.display()).into());
}
if output.exists() && !force {
return Err(format!(
"Output file '{}' already exists. Use --force to overwrite",
output.display()
)
.into());
}
if verbose {
println!(
"Decompressing '{}' to '{}'",
input.display(),
output.display()
);
}
let start_time = Instant::now();
let compressed_data = fs::read(input)?;
let input_size = compressed_data.len();
if verbose {
println!("Compressed size: {input_size} bytes");
}
let progress = if !quiet && input_size > 1024 * 1024 {
let pb = ProgressBar::new(2);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}",
)
.unwrap()
.progress_chars("#>-"),
);
pb.set_message("Decompressing...");
Some(pb)
} else {
None
};
if let Some(ref pb) = progress {
pb.inc(1);
}
let decompressed_data =
explode_bytes(&compressed_data).map_err(|e| format!("Decompression failed: {e}"))?;
if let Some(ref pb) = progress {
pb.inc(1);
pb.finish_with_message("Decompression complete");
}
fs::write(output, &decompressed_data)?;
let decompression_time = start_time.elapsed();
let output_size = decompressed_data.len();
let compression_ratio = (input_size as f64 / output_size as f64) * 100.0;
if !quiet {
println!("✓ Decompression successful!");
println!(" Input: {input_size} bytes");
println!(" Output: {output_size} bytes");
println!(" Ratio: {compression_ratio:.1}%");
println!(" Time: {decompression_time:.2?}");
}
Ok(())
}
fn show_file_info(input: &PathBuf, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
if !input.exists() {
return Err(format!("Input file '{}' does not exist", input.display()).into());
}
let data = fs::read(input)?;
let file_size = data.len();
if data.len() < 3 {
return Err("File too small to be a valid PKLib compressed file".into());
}
let compression_type = data[0];
let dict_bits = data[1];
let mode_str = match compression_type {
0 => "Binary",
1 => "ASCII",
_ => "Unknown",
};
let dict_size_str = match dict_bits {
4 => "1KB (1024 bytes)",
5 => "2KB (2048 bytes)",
6 => "4KB (4096 bytes)",
_ => "Unknown",
};
println!("PKLib File Information:");
println!(" File: {}", input.display());
println!(" Size: {file_size} bytes");
println!(" Compression Mode: {mode_str} ({compression_type})");
println!(" Dictionary Size: {dict_size_str} ({dict_bits} bits)");
if verbose {
println!(
" Header bytes: {:02x} {:02x} {:02x}",
data[0], data[1], data[2]
);
}
match explode_bytes(&data) {
Ok(decompressed) => {
let decompressed_size = decompressed.len();
let compression_ratio = (file_size as f64 / decompressed_size as f64) * 100.0;
println!(" Decompressed Size: {decompressed_size} bytes");
println!(" Compression Ratio: {compression_ratio:.1}%");
println!(" Status: ✓ Valid PKLib file");
}
Err(e) => {
println!(" Status: ✗ Invalid or corrupted PKLib file");
if verbose {
println!(" Error: {e}");
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_round_trip() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let input_path = dir.path().join("input.txt");
let compressed_path = dir.path().join("compressed.pklib");
let output_path = dir.path().join("output.txt");
let test_data = b"Hello, World! This is a test of the PKLib CLI tool.";
fs::write(&input_path, test_data)?;
compress_file(
&input_path,
&compressed_path,
CompressionMode::ASCII,
DictionarySize::Size2K,
false,
false,
true,
)?;
decompress_file(&compressed_path, &output_path, false, false, true)?;
let result_data = fs::read(&output_path)?;
assert_eq!(test_data, &result_data[..]);
Ok(())
}
}