use clap::Args;
use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub type Result<T = ()> = anyhow::Result<T>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtractedTarget {
FILE,
DIRECTORY,
}
#[derive(Args, Debug)]
pub struct CommonArgs {
#[arg(short, long)]
pub input: Option<String>,
#[arg(short, long)]
pub output: Option<String>,
#[arg(short, long)]
pub compress: bool,
#[arg(short, long)]
pub extract: bool,
#[arg(short, long)]
pub decompress: bool,
#[arg()]
pub io_list: Vec<String>,
#[arg(long)]
pub ignore_pipes: bool,
#[arg(long)]
pub ignore_stdin: bool,
#[arg(long)]
pub ignore_stdout: bool,
}
#[allow(dead_code)]
pub trait CompressionLevelValidator {
fn min_level(&self) -> i32;
fn max_level(&self) -> i32;
fn default_level(&self) -> i32;
fn name_to_level(&self, name: &str) -> Option<i32>;
fn is_valid_level(&self, level: i32) -> bool {
level >= self.min_level() && level <= self.max_level()
}
fn validate_and_clamp_level(&self, level: i32) -> i32 {
if level < self.min_level() {
self.min_level()
} else if level > self.max_level() {
self.max_level()
} else {
level
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultCompressionValidator;
impl CompressionLevelValidator for DefaultCompressionValidator {
fn min_level(&self) -> i32 {
0
}
fn max_level(&self) -> i32 {
9
}
fn default_level(&self) -> i32 {
6
}
fn name_to_level(&self, name: &str) -> Option<i32> {
match name.to_lowercase().as_str() {
"none" => Some(0),
"fast" => Some(1),
"best" => Some(9),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CompressionLevel {
pub level: i32,
}
impl Default for CompressionLevel {
fn default() -> Self {
CompressionLevel { level: 6 }
}
}
impl FromStr for CompressionLevel {
type Err = &'static str;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok(level) = s.parse::<i32>() {
return Ok(CompressionLevel { level });
}
let s = s.to_lowercase();
match s.as_str() {
"none" | "fast" | "best" => Ok(CompressionLevel {
level: DefaultCompressionValidator.name_to_level(&s).unwrap(),
}),
_ => Err("Invalid compression level"),
}
}
}
#[derive(Args, Debug, Default, Clone, Copy)]
pub struct LevelArgs {
#[arg(long, default_value = "fast")]
pub level: CompressionLevel,
}
#[allow(unused_variables)]
pub trait Compressor: Send + Sync {
fn name(&self) -> &str;
fn extension(&self) -> &str {
self.name()
}
fn default_extracted_target(&self) -> ExtractedTarget {
ExtractedTarget::FILE
}
fn is_archive(&self, in_path: &Path) -> bool {
if in_path.extension().is_none() {
return false;
}
in_path.extension().unwrap() == self.extension()
}
fn default_compressed_filename(&self, in_path: &Path) -> String {
format!(
"{}.{}",
in_path
.file_name()
.unwrap_or_else(|| OsStr::new("archive"))
.to_str()
.unwrap(),
self.extension()
)
}
fn default_extracted_filename(&self, in_path: &Path) -> String {
if self.default_extracted_target() == ExtractedTarget::DIRECTORY {
return ".".to_string();
}
if let Some(ext) = in_path.extension() {
if let Some(ext_str) = ext.to_str()
&& ext_str == self.extension()
&& let Some(stem) = in_path.file_stem()
&& let Some(stem_str) = stem.to_str()
{
return stem_str.to_string();
}
}
"archive".to_string()
}
fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result;
fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result;
}
impl fmt::Debug for dyn Compressor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Compressor {{ name: {} }}", self.name())
}
}
pub struct ReadWrapper(pub Box<dyn Read + Send>);
impl Read for ReadWrapper {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
impl fmt::Debug for ReadWrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ReadWrapper")
}
}
pub struct WriteWrapper(pub Box<dyn Write + Send>);
impl Write for WriteWrapper {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
impl fmt::Debug for WriteWrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "WriteWrapper")
}
}
#[derive(Debug)]
pub enum CmprssInput {
Path(Vec<PathBuf>),
Pipe(std::io::Stdin),
Reader(ReadWrapper),
}
#[derive(Debug)]
pub enum CmprssOutput {
Path(PathBuf),
Pipe(std::io::Stdout),
Writer(WriteWrapper),
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
struct TestCompressor;
impl Compressor for TestCompressor {
fn name(&self) -> &str {
"test"
}
fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result {
Ok(())
}
fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result {
Ok(())
}
}
struct CustomExtensionCompressor;
impl Compressor for CustomExtensionCompressor {
fn name(&self) -> &str {
"custom"
}
fn extension(&self) -> &str {
"cst"
}
fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result {
Ok(())
}
fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result {
Ok(())
}
}
#[test]
fn test_default_name_extension() {
let compressor = TestCompressor;
assert_eq!(compressor.name(), "test");
assert_eq!(compressor.extension(), "test");
}
#[test]
fn test_custom_extension() {
let compressor = CustomExtensionCompressor;
assert_eq!(compressor.name(), "custom");
assert_eq!(compressor.extension(), "cst");
}
#[test]
fn test_is_archive_detection() {
use tempfile::tempdir;
let compressor = TestCompressor;
let temp_dir = tempdir().expect("Failed to create temp dir");
let archive_path = temp_dir.path().join("archive.test");
std::fs::File::create(&archive_path).expect("Failed to create test file");
assert!(compressor.is_archive(&archive_path));
let non_archive_path = temp_dir.path().join("archive.txt");
std::fs::File::create(&non_archive_path).expect("Failed to create test file");
assert!(!compressor.is_archive(&non_archive_path));
let no_ext_path = temp_dir.path().join("archive");
std::fs::File::create(&no_ext_path).expect("Failed to create test file");
assert!(!compressor.is_archive(&no_ext_path));
}
#[test]
fn test_default_compressed_filename() {
let compressor = TestCompressor;
let path = Path::new("file.txt");
assert_eq!(
compressor.default_compressed_filename(path),
"file.txt.test"
);
let path = Path::new("file");
assert_eq!(compressor.default_compressed_filename(path), "file.test");
}
#[test]
fn test_default_extracted_filename() {
let compressor = TestCompressor;
let path = Path::new("archive.test");
assert_eq!(compressor.default_extracted_filename(path), "archive");
let path = Path::new("archive.txt");
assert_eq!(compressor.default_extracted_filename(path), "archive");
let path = Path::new("archive");
assert_eq!(compressor.default_extracted_filename(path), "archive");
}
#[test]
fn test_compression_level_parsing() {
assert_eq!(CompressionLevel::from_str("1").unwrap().level, 1);
assert_eq!(CompressionLevel::from_str("9").unwrap().level, 9);
let validator = DefaultCompressionValidator;
assert_eq!(
CompressionLevel::from_str("fast").unwrap().level,
validator.name_to_level("fast").unwrap()
);
assert_eq!(
CompressionLevel::from_str("best").unwrap().level,
validator.name_to_level("best").unwrap()
);
assert!(CompressionLevel::from_str("invalid").is_err());
}
#[test]
fn test_compression_level_defaults() {
let default_level = CompressionLevel::default();
let validator = DefaultCompressionValidator;
assert_eq!(default_level.level, validator.default_level());
}
#[test]
fn test_default_compression_validator() {
let validator = DefaultCompressionValidator;
use crate::test_utils::test_compression_validator_helper;
test_compression_validator_helper(
&validator,
0, 9, 6, Some(1), Some(9), Some(0), );
}
}