this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 52ec9ed3a28388caee2e1d877b9b3eaf727655b6 514 lines 19 kB view raw
1pub mod backends; 2pub mod progress; 3pub mod test_utils; 4pub mod utils; 5 6use backends::*; 7use clap::{Parser, Subcommand}; 8use is_terminal::IsTerminal; 9use std::path::{Path, PathBuf}; 10use std::{io, vec}; 11use utils::*; 12 13/// A compression multi-tool 14#[derive(Parser, Debug)] 15#[command(author, version, about, long_about = None)] 16struct CmprssArgs { 17 /// Format 18 #[command(subcommand)] 19 format: Option<Format>, 20 21 // Base arguments for the non-subcommand behavior 22 #[clap(flatten)] 23 pub base_args: CommonArgs, 24} 25#[derive(Subcommand, Debug)] 26enum Format { 27 /// tar archive format 28 Tar(TarArgs), 29 30 /// gzip compression 31 #[clap(visible_alias = "gz")] 32 Gzip(GzipArgs), 33 34 /// xz compression 35 Xz(XzArgs), 36 37 /// bzip2 compression 38 #[clap(visible_alias = "bz2")] 39 Bzip2(Bzip2Args), 40 41 /// zip archive format 42 Zip(ZipArgs), 43 44 /// zstd compression 45 #[clap(visible_alias = "zst")] 46 Zstd(ZstdArgs), 47 48 /// lz4 compression 49 Lz4(Lz4Args), 50} 51 52/// Get the input filename or return a default file 53/// This file will be used to generate the output filename 54fn get_input_filename(input: &CmprssInput) -> Result<&Path, io::Error> { 55 match input { 56 CmprssInput::Path(paths) => { 57 if paths.is_empty() { 58 return Err(io::Error::new( 59 io::ErrorKind::Other, 60 "error: no input specified", 61 )); 62 } 63 Ok(paths.first().unwrap()) 64 } 65 CmprssInput::Pipe(_) => Ok(Path::new("archive")), 66 } 67} 68 69#[derive(Debug, PartialEq, Clone, Copy)] 70enum Action { 71 Compress, 72 Extract, 73 Unknown, 74} 75 76/// Defines a single compress/extract action to take. 77#[derive(Debug)] 78struct Job { 79 compressor: Box<dyn Compressor>, 80 input: CmprssInput, 81 output: CmprssOutput, 82 action: Action, 83} 84 85/// Get a compressor from a filename 86fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> { 87 // TODO: Support multi-level files, like tar.gz 88 let compressors: Vec<Box<dyn Compressor>> = vec![ 89 Box::<Tar>::default(), 90 Box::<Gzip>::default(), 91 Box::<Xz>::default(), 92 Box::<Bzip2>::default(), 93 Box::<Zip>::default(), 94 Box::<Zstd>::default(), 95 Box::<Lz4>::default(), 96 ]; 97 compressors.into_iter().find(|c| c.is_archive(filename)) 98} 99 100/// Convert an input path into a Path 101fn get_path(input: &str) -> Option<PathBuf> { 102 let path = PathBuf::from(input); 103 if !path.try_exists().unwrap_or(false) { 104 return None; 105 } 106 Some(path) 107} 108 109/// Guess compressor/action from the two filenames 110/// The compressor may already be given 111fn guess_from_filenames( 112 input: &[PathBuf], 113 output: &Path, 114 compressor: Option<Box<dyn Compressor>>, 115) -> (Option<Box<dyn Compressor>>, Action) { 116 if input.len() != 1 { 117 if let Some(guessed_compressor) = get_compressor_from_filename(output) { 118 return (Some(guessed_compressor), Action::Compress); 119 } 120 // In theory we could be extracting multiple files to a directory 121 // We'll fail somewhere else if that's not the case 122 return (compressor, Action::Extract); 123 } 124 let input = input.first().unwrap(); 125 126 let guessed_compressor = get_compressor_from_filename(output); 127 let guessed_extractor = get_compressor_from_filename(input); 128 let guessed_compressor_name = if let Some(c) = &guessed_compressor { 129 c.name() 130 } else { 131 "" 132 }; 133 let guessed_extractor_name = if let Some(e) = &guessed_extractor { 134 e.name() 135 } else { 136 "" 137 }; 138 139 if let Some(c) = &compressor { 140 if guessed_compressor_name == c.name() { 141 return (compressor, Action::Compress); 142 } else if guessed_extractor_name == c.name() { 143 return (compressor, Action::Extract); 144 } else { 145 // Default to compressing 146 return (compressor, Action::Compress); 147 } 148 } 149 150 match (guessed_compressor, guessed_extractor) { 151 (None, None) => (None, Action::Unknown), 152 (Some(c), None) => (Some(c), Action::Compress), 153 (None, Some(e)) => (Some(e), Action::Extract), 154 (Some(c), Some(e)) => { 155 if c.name() == e.name() { 156 return (Some(c), Action::Unknown); 157 } 158 // Compare the input and output extensions to see if one has an extra extension 159 let input_file = input.file_name().unwrap().to_str().unwrap(); 160 let input_ext = input.extension().unwrap_or_default(); 161 let output_file = output.file_name().unwrap().to_str().unwrap(); 162 let output_ext = output.extension().unwrap_or_default(); 163 let guessed_output = input_file.to_string() + "." + output_ext.to_str().unwrap(); 164 let guessed_input = output_file.to_string() + "." + input_ext.to_str().unwrap(); 165 if guessed_output == output_file { 166 (Some(c), Action::Compress) 167 } else if guessed_input == input_file { 168 (Some(e), Action::Extract) 169 } else { 170 (None, Action::Unknown) 171 } 172 } 173 } 174} 175 176/// Parse the common args and determine the details of the job requested 177fn get_job( 178 compressor: Option<Box<dyn Compressor>>, 179 common_args: &CommonArgs, 180) -> Result<Job, io::Error> { 181 let mut compressor = compressor; 182 let mut action = { 183 if common_args.compress { 184 Action::Compress 185 } else if common_args.extract || common_args.decompress { 186 Action::Extract 187 } else { 188 Action::Unknown 189 } 190 }; 191 192 let mut inputs = Vec::new(); 193 if let Some(in_file) = &common_args.input { 194 match get_path(in_file) { 195 Some(path) => inputs.push(path), 196 None => { 197 return Err(io::Error::new( 198 io::ErrorKind::Other, 199 "Specified input path does not exist", 200 )); 201 } 202 } 203 } 204 205 let mut output = match &common_args.output { 206 Some(output) => { 207 let path = Path::new(output); 208 if path.try_exists()? && !path.is_dir() { 209 // Output path exists, bail out 210 return Err(io::Error::new( 211 io::ErrorKind::Other, 212 "Specified output path already exists", 213 )); 214 } 215 Some(path) 216 } 217 None => None, 218 }; 219 220 // Process the io_list, check if there is an output first 221 let mut io_list = common_args.io_list.clone(); 222 if output.is_none() { 223 if let Some(possible_output) = common_args.io_list.last() { 224 let path = Path::new(possible_output); 225 if !path.try_exists()? { 226 // Use the given path if it doesn't exist 227 output = Some(path); 228 io_list.pop(); 229 } else if path.is_dir() { 230 match action { 231 Action::Compress => { 232 // A directory can potentially be a target output location or 233 // an input, for now assume it is an input. 234 } 235 Action::Extract => { 236 // Can extract to a directory, and it wouldn't make any sense as an input 237 output = Some(path); 238 io_list.pop(); 239 } 240 _ => { 241 // TODO: don't know if this is an input or output, assume we're compressing this directory 242 // This does cause problems for inferencing "cat archive.tar | cmprss tar ." 243 // Probably need to add some special casing 244 } 245 }; 246 } else { 247 // TODO: check for scenarios where we want to append to an existing archive 248 } 249 } 250 } 251 252 // Validate the specified inputs 253 // Everything in the io_list should be an input 254 for input in &io_list { 255 if let Some(path) = get_path(input) { 256 inputs.push(path); 257 } else { 258 return Err(io::Error::new( 259 io::ErrorKind::Other, 260 "Specified input path does not exist", 261 )); 262 } 263 } 264 265 // Fallback to stdin/stdout if we're missing files 266 let cmprss_input = match inputs.is_empty() { 267 true => { 268 if !std::io::stdin().is_terminal() 269 && !&common_args.ignore_pipes 270 && !&common_args.ignore_stdin 271 { 272 CmprssInput::Pipe(std::io::stdin()) 273 } else { 274 return Err(io::Error::new(io::ErrorKind::Other, "No specified input")); 275 } 276 } 277 false => CmprssInput::Path(inputs), 278 }; 279 280 let cmprss_output = match output { 281 Some(path) => CmprssOutput::Path(path.to_path_buf()), 282 None => { 283 if !std::io::stdout().is_terminal() 284 && !&common_args.ignore_pipes 285 && !&common_args.ignore_stdout 286 { 287 CmprssOutput::Pipe(std::io::stdout()) 288 } else { 289 match action { 290 Action::Compress => { 291 if compressor.is_none() { 292 return Err(io::Error::new( 293 io::ErrorKind::Other, 294 "Must specify a compressor", 295 )); 296 } 297 CmprssOutput::Path(PathBuf::from( 298 compressor 299 .as_ref() 300 .unwrap() 301 .default_compressed_filename(get_input_filename(&cmprss_input)?), 302 )) 303 } 304 Action::Extract => { 305 if compressor.is_none() { 306 compressor = 307 get_compressor_from_filename(get_input_filename(&cmprss_input)?); 308 if compressor.is_none() { 309 return Err(io::Error::new( 310 io::ErrorKind::Other, 311 "Must specify a compressor", 312 )); 313 } 314 } 315 CmprssOutput::Path(PathBuf::from( 316 compressor 317 .as_ref() 318 .unwrap() 319 .default_extracted_filename(get_input_filename(&cmprss_input)?), 320 )) 321 } 322 Action::Unknown => { 323 if compressor.is_none() { 324 // Can still work if the input is an archive 325 compressor = 326 get_compressor_from_filename(get_input_filename(&cmprss_input)?); 327 if compressor.is_none() { 328 return Err(io::Error::new( 329 io::ErrorKind::Other, 330 "Must specify a compressor", 331 )); 332 } 333 action = Action::Extract; 334 CmprssOutput::Path(PathBuf::from( 335 compressor 336 .as_ref() 337 .unwrap() 338 .default_extracted_filename(get_input_filename(&cmprss_input)?), 339 )) 340 } else { 341 // We know the compressor, does the input have the same extension? 342 if let Some(compressor_from_input) = 343 get_compressor_from_filename(get_input_filename(&cmprss_input)?) 344 { 345 if compressor.as_ref().unwrap().name() 346 == compressor_from_input.name() 347 { 348 action = Action::Extract; 349 CmprssOutput::Path(PathBuf::from( 350 compressor.as_ref().unwrap().default_extracted_filename( 351 get_input_filename(&cmprss_input)?, 352 ), 353 )) 354 } else { 355 action = Action::Compress; 356 CmprssOutput::Path(PathBuf::from( 357 compressor.as_ref().unwrap().default_compressed_filename( 358 get_input_filename(&cmprss_input)?, 359 ), 360 )) 361 } 362 } else { 363 action = Action::Compress; 364 CmprssOutput::Path(PathBuf::from( 365 compressor.as_ref().unwrap().default_compressed_filename( 366 get_input_filename(&cmprss_input)?, 367 ), 368 )) 369 } 370 } 371 } 372 } 373 } 374 } 375 }; 376 377 // If we don't have the compressor/action, we can attempt to infer 378 if compressor.is_none() || action == Action::Unknown { 379 match action { 380 Action::Compress => { 381 // Look at the output name 382 // TODO: tar.gz ?? 383 if let CmprssOutput::Path(path) = &cmprss_output { 384 compressor = get_compressor_from_filename(path); 385 } 386 } 387 Action::Extract => { 388 // Look at the input name 389 if let CmprssInput::Path(paths) = &cmprss_input { 390 if paths.len() != 1 { 391 // Can't guess if there are multiple inputs 392 return Err(io::Error::new( 393 io::ErrorKind::Other, 394 "Can't guess compressor with multiple inputs", 395 )); 396 } 397 compressor = get_compressor_from_filename(paths.first().unwrap()); 398 } 399 } 400 Action::Unknown => match (&cmprss_input, &cmprss_output) { 401 (CmprssInput::Pipe(_), CmprssOutput::Path(path)) => { 402 if compressor.is_none() { 403 compressor = get_compressor_from_filename(path); 404 if compressor.is_some() { 405 action = Action::Compress; 406 } else { 407 return Err(io::Error::new( 408 io::ErrorKind::Other, 409 "Can't guess compressor to use", 410 )); 411 } 412 } else if compressor.as_ref().unwrap().name() 413 == get_compressor_from_filename(path).unwrap().name() 414 { 415 action = Action::Compress; 416 } else { 417 action = Action::Extract; 418 } 419 } 420 (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => { 421 if compressor.is_none() { 422 if paths.len() != 1 { 423 return Err(io::Error::new( 424 io::ErrorKind::Other, 425 "Can't guess compressor with multiple inputs", 426 )); 427 } 428 compressor = get_compressor_from_filename(paths.first().unwrap()); 429 if compressor.is_some() { 430 action = Action::Extract; 431 } else { 432 return Err(io::Error::new( 433 io::ErrorKind::Other, 434 "Can't guess compressor to use", 435 )); 436 } 437 } else if let Some(c) = get_compressor_from_filename(paths.first().unwrap()) { 438 if compressor.as_ref().unwrap().name() == c.name() { 439 action = Action::Extract; 440 } else { 441 action = Action::Compress; 442 } 443 } else { 444 action = Action::Compress; 445 } 446 } 447 (CmprssInput::Pipe(_), CmprssOutput::Pipe(_)) => { 448 action = Action::Compress; 449 } 450 (CmprssInput::Path(paths), CmprssOutput::Path(path)) => { 451 let (guessed_compressor, guessed_action) = 452 guess_from_filenames(paths, path, compressor); 453 compressor = guessed_compressor; 454 action = guessed_action; 455 } 456 }, 457 } 458 } 459 460 if compressor.is_none() { 461 return Err(io::Error::new( 462 io::ErrorKind::Other, 463 "Could not determine compressor to use", 464 )); 465 } 466 if action == Action::Unknown { 467 return Err(io::Error::new( 468 io::ErrorKind::Other, 469 "Could not determine action to take", 470 )); 471 } 472 473 Ok(Job { 474 compressor: compressor.unwrap(), 475 input: cmprss_input, 476 output: cmprss_output, 477 action, 478 }) 479} 480 481fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result<(), io::Error> { 482 let job = get_job(compressor, args)?; 483 484 match job.action { 485 Action::Compress => job.compressor.compress(job.input, job.output)?, 486 Action::Extract => job.compressor.extract(job.input, job.output)?, 487 _ => { 488 return Err(io::Error::new( 489 io::ErrorKind::Other, 490 "Unknown action requested", 491 )); 492 } 493 }; 494 495 Ok(()) 496} 497 498fn main() { 499 let args = CmprssArgs::parse(); 500 match args.format { 501 Some(Format::Tar(a)) => command(Some(Box::new(Tar::new(&a))), &a.common_args), 502 Some(Format::Gzip(a)) => command(Some(Box::new(Gzip::new(&a))), &a.common_args), 503 Some(Format::Xz(a)) => command(Some(Box::new(Xz::new(&a))), &a.common_args), 504 Some(Format::Bzip2(a)) => command(Some(Box::new(Bzip2::new(&a))), &a.common_args), 505 Some(Format::Zip(a)) => command(Some(Box::new(Zip::new(&a))), &a.common_args), 506 Some(Format::Zstd(a)) => command(Some(Box::new(Zstd::new(&a))), &a.common_args), 507 Some(Format::Lz4(a)) => command(Some(Box::new(Lz4::new(&a))), &a.common_args), 508 _ => command(None, &args.base_args), 509 } 510 .unwrap_or_else(|e| { 511 eprintln!("ERROR(cmprss): {}", e); 512 std::process::exit(1); 513 }); 514}