this repo has no description
0
fork

Configure Feed

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

feat(cli): add the magic cli

This is a large commit that adds the majority of the 'inferencing'
features that were the primary goal of this project from the
beginning.

This teaches cmprss how to pretty intelligently guess the correct
actions to take without being told explicitly.

This impl should be considered alpha.

+804 -44
+55 -6
README.md
··· 16 16 17 17 ## Usage 18 18 19 - The primary goal of the CLI is to make it easy and consistent to work with any compression format. 20 - All of the examples will work with _any_ of the supported compression formats. 21 - Though some formats will fail in certain scenarios as not all compression formats support all types of input/output. 22 - For example `tar` is unable to support compressing from `stdin` and extracting to `stdout`, because it expects to operate on files. 19 + The primary goal is to infer behavior based on the input, so that you don't need to remember esoteric CLI arguments. 20 + 21 + `cmprss` supports being very explicit about the inputs and outputs for scripting, but will also behave intelligently when you leave out info. 23 22 24 23 All commands read from left to right, input is always either piped from `stdin` or the first filename(s) specified, and output is either `stdout` or the last filename/directory. 25 24 26 - If output filenames are left out, `cmprss` will try to infer the filename based on the compression type. 25 + The easiest way to understand is to look at some examples 26 + 27 + Compress a file with gzip 28 + 29 + ```bash 30 + cmprss file.txt file.txt.gz 31 + ``` 32 + 33 + Compress 2 files into a tar archive 34 + 35 + ```bash 36 + cmprss file1.txt file2.txt archive.tar 37 + ``` 38 + 39 + Compress stdin with xz 40 + 41 + ```bash 42 + cat file.txt | cmprss file.xz 43 + ``` 44 + 45 + Extract a tar archive to the current directory 46 + 47 + ```bash 48 + cmprss archive.tar 49 + ``` 50 + 51 + Extract an xz compressed file 52 + 53 + ```bash 54 + cmprss file.xz file.txt 55 + ``` 56 + 57 + Extract a gzip compressed file to stdout 58 + 59 + ```bash 60 + cmprss file.txt.gz > file.txt 61 + ``` 27 62 28 - ### Examples 63 + `cmprss` doesn't yet support multiple levels of archiving, like `.tar.gz`, but they are easy to work with using pipes 64 + 65 + ```bash 66 + cmprss tar uncompressed_dir | cmprss gz > out.tar.gz 67 + cmprss gzip --extract out.tar.gz | cmprss tar -e output_dir 68 + 69 + # Or a full roundtrip in one line 70 + cmprss tar dir | cmprss gz | cmprss gz -e | cmprss tar -e 71 + ``` 72 + 73 + ### Examples of Explicit Behavior 74 + 75 + All these examples will work with _any_ of the supported compression formats, provided that they support the input/output formats. 76 + 77 + If output filenames are left out, `cmprss` will try to infer the filename based on the compression type. 29 78 30 79 Compress a file/directory to a `tar` archive: 31 80
+4 -1
Taskfile.yml
··· 37 37 desc: Run all formatters 38 38 sources: 39 39 - ./**/*.rs 40 - cmd: cargo fmt --all 40 + - ./**/*.nix 41 + cmds: 42 + - cargo fmt --all 43 + - alejandra . 41 44 test: 42 45 desc: Run all tests 43 46 aliases: [t]
+1 -1
bin/test.sh
··· 51 51 local dir="$2" 52 52 mkdir -p "$dir" 53 53 for i in $(seq 1 "$size"); do 54 - random_file 1024 "$dir/$i" 54 + random_file 128 "$dir/$i" 55 55 done 56 56 } 57 57
+309 -30
src/main.rs
··· 9 9 use clap::{Parser, Subcommand}; 10 10 use gzip::{Gzip, GzipArgs}; 11 11 use is_terminal::IsTerminal; 12 - use std::io; 13 12 use std::path::{Path, PathBuf}; 13 + use std::{io, vec}; 14 14 use tar::{Tar, TarArgs}; 15 15 use utils::*; 16 16 use xz::{Xz, XzArgs}; ··· 22 22 /// Format 23 23 #[command(subcommand)] 24 24 format: Option<Format>, 25 + 26 + // Base arguments for the non-subcommand behavior 27 + #[clap(flatten)] 28 + pub base_args: CommonArgs, 25 29 } 26 30 27 31 #[derive(Subcommand, Debug)] ··· 58 62 } 59 63 } 60 64 61 - #[derive(Debug)] 65 + #[derive(Debug, PartialEq, Clone, Copy)] 62 66 enum Action { 63 67 Compress, 64 68 Extract, 69 + Unknown, 65 70 } 66 71 67 72 /// Defines a single compress/extract action to take. 68 73 #[derive(Debug)] 69 74 struct Job { 75 + compressor: Box<dyn Compressor>, 70 76 input: CmprssInput, 71 77 output: CmprssOutput, 72 78 action: Action, 73 79 } 74 80 81 + /// Get a compressor from a filename 82 + fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> { 83 + // TODO: Support multi-level files, like tar.gz 84 + let compressors: Vec<Box<dyn Compressor>> = vec![ 85 + Box::<Tar>::default(), 86 + Box::<Gzip>::default(), 87 + Box::<Xz>::default(), 88 + Box::<Bzip2>::default(), 89 + ]; 90 + compressors.into_iter().find(|c| c.is_archive(filename)) 91 + } 92 + 93 + /// Convert an input path into a Path 94 + fn get_path(input: &str) -> Option<PathBuf> { 95 + let path = PathBuf::from(input); 96 + if !path.try_exists().unwrap_or(false) { 97 + return None; 98 + } 99 + Some(path) 100 + } 101 + 102 + /// Guess compressor/action from the two filenames 103 + /// The compressor may already be given 104 + fn guess_from_filenames( 105 + input: &Vec<PathBuf>, 106 + output: &Path, 107 + compressor: Option<Box<dyn Compressor>>, 108 + ) -> (Option<Box<dyn Compressor>>, Action) { 109 + if input.len() != 1 { 110 + if let Some(guessed_compressor) = get_compressor_from_filename(output) { 111 + return (Some(guessed_compressor), Action::Compress); 112 + } 113 + // In theory we could be extracting multiple files to a directory 114 + // We'll fail somewhere else if that's not the case 115 + return (compressor, Action::Extract); 116 + } 117 + let input = input.first().unwrap(); 118 + 119 + let guessed_compressor = get_compressor_from_filename(output); 120 + let guessed_extractor = get_compressor_from_filename(input); 121 + let guessed_compressor_name = if let Some(c) = &guessed_compressor { 122 + c.name() 123 + } else { 124 + "" 125 + }; 126 + let guessed_extractor_name = if let Some(e) = &guessed_extractor { 127 + e.name() 128 + } else { 129 + "" 130 + }; 131 + 132 + if let Some(c) = &compressor { 133 + if guessed_compressor_name == c.name() { 134 + return (compressor, Action::Compress); 135 + } else if guessed_extractor_name == c.name() { 136 + return (compressor, Action::Extract); 137 + } else { 138 + // Default to compressing 139 + return (compressor, Action::Compress); 140 + } 141 + } 142 + 143 + match (guessed_compressor, guessed_extractor) { 144 + (None, None) => (None, Action::Unknown), 145 + (Some(c), None) => (Some(c), Action::Compress), 146 + (None, Some(e)) => (Some(e), Action::Extract), 147 + (Some(c), Some(e)) => { 148 + if c.name() == e.name() { 149 + return (Some(c), Action::Unknown); 150 + } 151 + // Compare the input and output extensions to see if one has an extra extension 152 + let input_file = input.file_name().unwrap().to_str().unwrap(); 153 + let input_ext = input.extension().unwrap_or_default(); 154 + let output_file = output.file_name().unwrap().to_str().unwrap(); 155 + let output_ext = output.extension().unwrap_or_default(); 156 + let guessed_output = input_file.to_string() + "." + output_ext.to_str().unwrap(); 157 + let guessed_input = output_file.to_string() + "." + input_ext.to_str().unwrap(); 158 + if guessed_output == output_file { 159 + (Some(c), Action::Compress) 160 + } else if guessed_input == input_file { 161 + (Some(e), Action::Extract) 162 + } else { 163 + (None, Action::Unknown) 164 + } 165 + } 166 + } 167 + } 168 + 75 169 /// Parse the common args and determine the details of the job requested 76 - fn get_job<T: Compressor>(compressor: &T, common_args: &CommonArgs) -> Result<Job, io::Error> { 77 - let action = { 170 + fn get_job( 171 + compressor: Option<Box<dyn Compressor>>, 172 + common_args: &CommonArgs, 173 + ) -> Result<Job, io::Error> { 174 + let mut compressor = compressor; 175 + let mut action = { 78 176 if common_args.compress { 79 177 Action::Compress 80 178 } else if common_args.extract || common_args.decompress { 81 179 Action::Extract 82 180 } else { 83 - Action::Compress 181 + Action::Unknown 84 182 } 85 183 }; 86 184 87 - let mut inputs = match &common_args.input { 88 - Some(input) => { 89 - let path = PathBuf::from(input); 90 - if !path.try_exists()? { 185 + let mut inputs = Vec::new(); 186 + if let Some(in_file) = &common_args.input { 187 + match get_path(in_file) { 188 + Some(path) => inputs.push(path), 189 + None => { 91 190 return Err(io::Error::new( 92 191 io::ErrorKind::Other, 93 192 "Specified input path does not exist", 94 193 )); 95 194 } 96 - vec![path] 97 195 } 98 - None => Vec::new(), 99 - }; 196 + } 197 + 100 198 let mut output = match &common_args.output { 101 199 Some(output) => { 102 200 let path = Path::new(output); ··· 132 230 output = Some(path); 133 231 io_list.pop(); 134 232 } 233 + _ => { 234 + // TODO: don't know if this is an input or output, assume we're compressing this directory 235 + // This does cause problems for inferencing "cat archive.tar | cmprss tar ." 236 + // Probably need to add some special casing 237 + } 135 238 }; 136 239 } else { 137 - // TODO: append checks 240 + // TODO: check for scenarios where we want to append to an existing archive 138 241 } 139 242 } 140 243 } 244 + 141 245 // Validate the specified inputs 142 246 // Everything in the io_list should be an input 143 247 for input in &io_list { 144 - let path = PathBuf::from(input); 145 - if !path.try_exists()? { 248 + if let Some(path) = get_path(input) { 249 + inputs.push(path); 250 + } else { 146 251 return Err(io::Error::new( 147 252 io::ErrorKind::Other, 148 253 "Specified input path does not exist", 149 254 )); 150 255 } 151 - inputs.push(path); 152 256 } 153 257 154 258 // Fallback to stdin/stdout if we're missing files ··· 165 269 } 166 270 false => CmprssInput::Path(inputs), 167 271 }; 272 + 168 273 let cmprss_output = match output { 169 274 Some(path) => CmprssOutput::Path(path.to_path_buf()), 170 275 None => { ··· 176 281 } else { 177 282 match action { 178 283 Action::Compress => { 179 - // Use a default filename 284 + if compressor.is_none() { 285 + return Err(io::Error::new( 286 + io::ErrorKind::Other, 287 + "Must specify a compressor", 288 + )); 289 + } 180 290 CmprssOutput::Path(PathBuf::from( 181 291 compressor 292 + .as_ref() 293 + .unwrap() 182 294 .default_compressed_filename(get_input_filename(&cmprss_input)?), 183 295 )) 184 296 } 185 - Action::Extract => CmprssOutput::Path(PathBuf::from( 186 - compressor.default_extracted_filename(get_input_filename(&cmprss_input)?), 187 - )), 297 + Action::Extract => { 298 + if compressor.is_none() { 299 + compressor = 300 + get_compressor_from_filename(get_input_filename(&cmprss_input)?); 301 + if compressor.is_none() { 302 + return Err(io::Error::new( 303 + io::ErrorKind::Other, 304 + "Must specify a compressor", 305 + )); 306 + } 307 + } 308 + CmprssOutput::Path(PathBuf::from( 309 + compressor 310 + .as_ref() 311 + .unwrap() 312 + .default_extracted_filename(get_input_filename(&cmprss_input)?), 313 + )) 314 + } 315 + Action::Unknown => { 316 + if compressor.is_none() { 317 + // Can still work if the input is an archive 318 + compressor = 319 + get_compressor_from_filename(get_input_filename(&cmprss_input)?); 320 + if compressor.is_none() { 321 + return Err(io::Error::new( 322 + io::ErrorKind::Other, 323 + "Must specify a compressor", 324 + )); 325 + } 326 + action = Action::Extract; 327 + CmprssOutput::Path(PathBuf::from( 328 + compressor 329 + .as_ref() 330 + .unwrap() 331 + .default_extracted_filename(get_input_filename(&cmprss_input)?), 332 + )) 333 + } else { 334 + // We know the compressor, does the input have the same extension? 335 + if let Some(compressor_from_input) = 336 + get_compressor_from_filename(get_input_filename(&cmprss_input)?) 337 + { 338 + if compressor.as_ref().unwrap().name() 339 + == compressor_from_input.name() 340 + { 341 + action = Action::Extract; 342 + CmprssOutput::Path(PathBuf::from( 343 + compressor.as_ref().unwrap().default_extracted_filename( 344 + get_input_filename(&cmprss_input)?, 345 + ), 346 + )) 347 + } else { 348 + action = Action::Compress; 349 + CmprssOutput::Path(PathBuf::from( 350 + compressor.as_ref().unwrap().default_compressed_filename( 351 + get_input_filename(&cmprss_input)?, 352 + ), 353 + )) 354 + } 355 + } else { 356 + action = Action::Compress; 357 + CmprssOutput::Path(PathBuf::from( 358 + compressor.as_ref().unwrap().default_compressed_filename( 359 + get_input_filename(&cmprss_input)?, 360 + ), 361 + )) 362 + } 363 + } 364 + } 188 365 } 189 366 } 190 367 } 191 368 }; 192 369 370 + // If we don't have the compressor/action, we can attempt to infer 371 + if compressor.is_none() || action == Action::Unknown { 372 + match action { 373 + Action::Compress => { 374 + // Look at the output name 375 + // TODO: tar.gz ?? 376 + if let CmprssOutput::Path(path) = &cmprss_output { 377 + compressor = get_compressor_from_filename(path); 378 + } 379 + } 380 + Action::Extract => { 381 + // Look at the input name 382 + if let CmprssInput::Path(paths) = &cmprss_input { 383 + if paths.len() != 1 { 384 + // Can't guess if there are multiple inputs 385 + return Err(io::Error::new( 386 + io::ErrorKind::Other, 387 + "Can't guess compressor with multiple inputs", 388 + )); 389 + } 390 + compressor = get_compressor_from_filename(paths.first().unwrap()); 391 + } 392 + } 393 + Action::Unknown => match (&cmprss_input, &cmprss_output) { 394 + (CmprssInput::Pipe(_), CmprssOutput::Path(path)) => { 395 + if compressor.is_none() { 396 + compressor = get_compressor_from_filename(path); 397 + if compressor.is_some() { 398 + action = Action::Compress; 399 + } else { 400 + return Err(io::Error::new( 401 + io::ErrorKind::Other, 402 + "Can't guess compressor to use", 403 + )); 404 + } 405 + } else if compressor.as_ref().unwrap().name() 406 + == get_compressor_from_filename(path).unwrap().name() 407 + { 408 + action = Action::Compress; 409 + } else { 410 + action = Action::Extract; 411 + } 412 + } 413 + (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => { 414 + if compressor.is_none() { 415 + if paths.len() != 1 { 416 + return Err(io::Error::new( 417 + io::ErrorKind::Other, 418 + "Can't guess compressor with multiple inputs", 419 + )); 420 + } 421 + compressor = get_compressor_from_filename(paths.first().unwrap()); 422 + if compressor.is_some() { 423 + action = Action::Extract; 424 + } else { 425 + return Err(io::Error::new( 426 + io::ErrorKind::Other, 427 + "Can't guess compressor to use", 428 + )); 429 + } 430 + } else if let Some(c) = get_compressor_from_filename(paths.first().unwrap()) { 431 + if compressor.as_ref().unwrap().name() == c.name() { 432 + action = Action::Extract; 433 + } else { 434 + action = Action::Compress; 435 + } 436 + } else { 437 + action = Action::Compress; 438 + } 439 + } 440 + (CmprssInput::Pipe(_), CmprssOutput::Pipe(_)) => { 441 + action = Action::Compress; 442 + } 443 + (CmprssInput::Path(paths), CmprssOutput::Path(path)) => { 444 + let (guessed_compressor, guessed_action) = 445 + guess_from_filenames(paths, path, compressor); 446 + compressor = guessed_compressor; 447 + action = guessed_action; 448 + } 449 + }, 450 + } 451 + } 452 + 453 + if compressor.is_none() { 454 + return Err(io::Error::new( 455 + io::ErrorKind::Other, 456 + "Could not determine compressor to use", 457 + )); 458 + } 459 + if action == Action::Unknown { 460 + return Err(io::Error::new( 461 + io::ErrorKind::Other, 462 + "Could not determine action to take", 463 + )); 464 + } 465 + 193 466 Ok(Job { 467 + compressor: compressor.unwrap(), 194 468 input: cmprss_input, 195 469 output: cmprss_output, 196 470 action, 197 471 }) 198 472 } 199 473 200 - fn command<T: Compressor>(compressor: T, args: &CommonArgs) -> Result<(), io::Error> { 201 - let job = get_job(&compressor, args)?; 474 + fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result<(), io::Error> { 475 + let job = get_job(compressor, args)?; 202 476 203 - // TODO: Print expected actions, and ask for confirmation if there's ambiguity 204 477 match job.action { 205 - Action::Compress => compressor.compress(job.input, job.output)?, 206 - Action::Extract => compressor.extract(job.input, job.output)?, 478 + Action::Compress => job.compressor.compress(job.input, job.output)?, 479 + Action::Extract => job.compressor.extract(job.input, job.output)?, 480 + _ => { 481 + return Err(io::Error::new( 482 + io::ErrorKind::Other, 483 + "Unknown action requested", 484 + )); 485 + } 207 486 }; 208 487 209 488 Ok(()) ··· 212 491 fn main() { 213 492 let args = CmprssArgs::parse(); 214 493 match args.format { 215 - Some(Format::Tar(a)) => command(Tar::new(&a), &a.common_args), 216 - Some(Format::Gzip(a)) => command(Gzip::new(&a), &a.common_args), 217 - Some(Format::Xz(a)) => command(Xz::new(&a), &a.common_args), 218 - Some(Format::Bzip2(a)) => command(Bzip2::new(&a), &a.common_args), 219 - _ => Err(io::Error::new(io::ErrorKind::Other, "unknown input")), 494 + Some(Format::Tar(a)) => command(Some(Box::new(Tar::new(&a))), &a.common_args), 495 + Some(Format::Gzip(a)) => command(Some(Box::new(Gzip::new(&a))), &a.common_args), 496 + Some(Format::Xz(a)) => command(Some(Box::new(Xz::new(&a))), &a.common_args), 497 + Some(Format::Bzip2(a)) => command(Some(Box::new(Bzip2::new(&a))), &a.common_args), 498 + _ => command(None, &args.base_args), 220 499 } 221 500 .unwrap_or_else(|e| { 222 501 eprintln!("ERROR(cmprss): {}", e);
+19 -3
src/utils.rs
··· 1 1 use clap::Args; 2 + use std::ffi::OsStr; 3 + use std::fmt; 2 4 use std::io; 3 5 use std::path::{Path, PathBuf}; 4 6 use std::str::FromStr; ··· 97 99 } 98 100 99 101 /// Detect if the input is an archive of this type 100 - #[allow(dead_code)] 102 + /// Just checks the extension by default 103 + /// Some compressors may overwrite this to do more advanced detection 101 104 fn is_archive(&self, in_path: &Path) -> bool { 102 - in_path.extension().unwrap_or_default() == self.extension() 105 + if in_path.extension().is_none() { 106 + return false; 107 + } 108 + in_path.extension().unwrap() == self.extension() 103 109 } 104 110 105 111 /// Generate the default name for the compressed file 106 112 fn default_compressed_filename(&self, in_path: &Path) -> String { 107 113 format!( 108 114 "{}.{}", 109 - in_path.file_name().unwrap().to_str().unwrap(), 115 + in_path 116 + .file_name() 117 + .unwrap_or_else(|| OsStr::new("archive")) 118 + .to_str() 119 + .unwrap(), 110 120 self.extension() 111 121 ) 112 122 } ··· 131 141 132 142 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 133 143 cmprss_error("extract_target unimplemented") 144 + } 145 + } 146 + 147 + impl fmt::Debug for dyn Compressor { 148 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 149 + write!(f, "Compressor {{ name: {} }}", self.name()) 134 150 } 135 151 } 136 152
+416 -3
tests/cli.rs
··· 134 134 /// 135 135 /// ``` bash 136 136 /// cmprss tar test.txt test2.txt 137 - /// cmprss tar --extract test.txt.tar 137 + /// cmprss tar test.txt.tar 138 138 /// ``` 139 139 #[test] 140 140 fn tar_roundtrip_implicit_two() -> Result<(), Box<dyn std::error::Error>> { ··· 159 159 let mut extract = Command::cargo_bin("cmprss")?; 160 160 extract 161 161 .current_dir(&working_dir) 162 - .arg("tar") 163 162 .arg("--ignore-pipes") 164 - .arg("--extract") 165 163 .arg(archive.path()); 166 164 extract.assert().success(); 167 165 ··· 528 526 529 527 Ok(()) 530 528 } 529 + 530 + /// Magic roundtrip using stdin 531 + /// Compressing: input = stdin, output = test.txt.gz 532 + /// Extracting: input = test.txt.gz, output = test.txt 533 + /// 534 + /// ``` bash 535 + /// cat test.txt | cmprss test.txt.gz 536 + /// cmprss gz --extract test.txt.gz out.txt 537 + /// ``` 538 + #[test] 539 + fn magic_roundtrip_stdin() -> Result<(), Box<dyn std::error::Error>> { 540 + let file = assert_fs::NamedTempFile::new("test.txt")?; 541 + file.write_str("garbage data for testing")?; 542 + 543 + let working_dir = assert_fs::TempDir::new()?; 544 + let archive = working_dir.child("test.txt.gz"); 545 + archive.assert(predicate::path::missing()); 546 + 547 + // Pipe file to stdin 548 + let mut compress = Command::cargo_bin("cmprss")?; 549 + compress 550 + .current_dir(&working_dir) 551 + .arg("--ignore-stdout") 552 + .arg("test.txt.gz") 553 + .stdin(Stdio::from(File::open(file.path())?)); 554 + compress.assert().success(); 555 + archive.assert(predicate::path::is_file()); 556 + 557 + let mut extract = Command::cargo_bin("cmprss")?; 558 + extract 559 + .current_dir(&working_dir) 560 + .arg("gz") 561 + .arg("--ignore-pipes") 562 + .arg("--extract") 563 + .arg("test.txt.gz"); 564 + extract.assert().success(); 565 + 566 + // Assert the files are identical 567 + working_dir 568 + .child("test.txt") 569 + .assert(predicate::path::eq_file(file.path())); 570 + 571 + Ok(()) 572 + } 573 + 574 + /// Magic roundtrip using files 575 + /// Compressing: input = test.txt, output = test.txt.gz 576 + /// Extracting: input = test.txt.gz, output = stdout 577 + /// 578 + /// ``` bash 579 + /// cmprss test.txt test.txt.gz 580 + /// cmprss test.txt.gz out.txt 581 + /// ``` 582 + #[test] 583 + fn magic_roundtrip_files() -> Result<(), Box<dyn std::error::Error>> { 584 + let file = assert_fs::NamedTempFile::new("test.txt")?; 585 + file.write_str("garbage data for testing")?; 586 + 587 + let working_dir = assert_fs::TempDir::new()?; 588 + let archive = working_dir.child("test.txt.gz"); 589 + archive.assert(predicate::path::missing()); 590 + 591 + // Compress file to an archive 592 + let mut compress = Command::cargo_bin("cmprss")?; 593 + compress 594 + .current_dir(&working_dir) 595 + .arg("--ignore-pipes") 596 + .arg(file.path()) 597 + .arg("test.txt.gz"); 598 + compress.assert().success(); 599 + archive.assert(predicate::path::is_file()); 600 + 601 + // Extract file to given file 602 + let mut extract = Command::cargo_bin("cmprss")?; 603 + extract 604 + .current_dir(&working_dir) 605 + .arg("--ignore-pipes") 606 + .arg("test.txt.gz") 607 + .arg("out.txt"); 608 + extract.assert().success(); 609 + 610 + // Assert the files are identical 611 + working_dir 612 + .child("out.txt") 613 + .assert(predicate::path::eq_file(file.path())); 614 + 615 + Ok(()) 616 + } 617 + 618 + /// Magic roundtrip using stdout decompression 619 + /// Compressing: input = test.txt, output = test.txt.gz 620 + /// Extracting: input = test.txt.gz, output = stdout 621 + /// 622 + /// ``` bash 623 + /// cmprss test.txt test.txt.gz 624 + /// cmprss test.txt.gz > out.txt 625 + /// ``` 626 + #[test] 627 + fn magic_roundtrip_stdout_decompression() -> Result<(), Box<dyn std::error::Error>> { 628 + let file = assert_fs::NamedTempFile::new("test.txt")?; 629 + file.write_str("garbage data for testing")?; 630 + 631 + let working_dir = assert_fs::TempDir::new()?; 632 + let archive = working_dir.child("test.txt.gz"); 633 + archive.assert(predicate::path::missing()); 634 + 635 + let out_file = working_dir.child("out.txt"); 636 + 637 + // Compress file to an archive 638 + let mut compress = Command::cargo_bin("cmprss")?; 639 + compress 640 + .current_dir(&working_dir) 641 + .arg("--ignore-pipes") 642 + .arg(file.path()) 643 + .arg("test.txt.gz"); 644 + compress.assert().success(); 645 + archive.assert(predicate::path::is_file()); 646 + 647 + // Extract file to stdout 648 + let mut extract = Command::cargo_bin("cmprss")?; 649 + extract 650 + .current_dir(&working_dir) 651 + .arg("--ignore-stdin") 652 + .arg("test.txt.gz") 653 + .stdout(Stdio::from(File::create(&out_file)?)); 654 + extract.assert().success(); 655 + 656 + // Assert the files are identical 657 + out_file.assert(predicate::path::eq_file(file.path())); 658 + Ok(()) 659 + } 660 + 661 + /// Magic roundtrip using stdin compression 662 + /// Compressing: input = stdin, output = test.txt.gz 663 + /// Extracting: input = test.txt.gz, output = test.txt 664 + /// 665 + /// ``` bash 666 + /// cat test.txt | cmprss test.txt.gz 667 + /// cmprss test.txt.gz out.txt 668 + /// ``` 669 + #[test] 670 + fn magic_roundtrip_stdin_compression() -> Result<(), Box<dyn std::error::Error>> { 671 + let file = assert_fs::NamedTempFile::new("test.txt")?; 672 + file.write_str("garbage data for testing")?; 673 + 674 + let working_dir = assert_fs::TempDir::new()?; 675 + let archive = working_dir.child("test.txt.gz"); 676 + archive.assert(predicate::path::missing()); 677 + 678 + let out_file = working_dir.child("out.txt"); 679 + 680 + // Compress stdin to an archive 681 + let mut compress = Command::cargo_bin("cmprss")?; 682 + compress 683 + .current_dir(&working_dir) 684 + .arg("--ignore-stdout") 685 + .arg("test.txt.gz") 686 + .stdin(Stdio::from(File::open(file.path())?)); 687 + compress.assert().success(); 688 + archive.assert(predicate::path::is_file()); 689 + 690 + // Extract file to given file 691 + let mut extract = Command::cargo_bin("cmprss")?; 692 + extract 693 + .current_dir(&working_dir) 694 + .arg("--ignore-pipes") 695 + .arg("test.txt.gz") 696 + .arg(out_file.path()); 697 + extract.assert().success(); 698 + 699 + // Assert the files are identical 700 + out_file.assert(predicate::path::eq_file(file.path())); 701 + 702 + Ok(()) 703 + } 704 + 705 + /// Magic roundtrip using default filenames 706 + /// Compressing: input = test.txt, output = test.txt.gz 707 + /// Extracting: input = test.txt.gz, output = <default> 708 + /// 709 + /// ``` bash 710 + /// cmprss test.txt test.txt.gz 711 + /// cmprss test.txt.gz 712 + /// ``` 713 + #[test] 714 + fn magic_roundtrip_default_filenames() -> Result<(), Box<dyn std::error::Error>> { 715 + let file = assert_fs::NamedTempFile::new("test.txt")?; 716 + file.write_str("garbage data for testing")?; 717 + 718 + let working_dir = assert_fs::TempDir::new()?; 719 + let archive = working_dir.child("test.txt.gz"); 720 + archive.assert(predicate::path::missing()); 721 + 722 + // Compress file to an archive 723 + let mut compress = Command::cargo_bin("cmprss")?; 724 + compress 725 + .current_dir(&working_dir) 726 + .arg("--ignore-pipes") 727 + .arg(file.path()) 728 + .arg("test.txt.gz"); 729 + compress.assert().success(); 730 + archive.assert(predicate::path::is_file()); 731 + 732 + // Extract file to default filename 733 + let mut extract = Command::cargo_bin("cmprss")?; 734 + extract 735 + .current_dir(&working_dir) 736 + .arg("--ignore-pipes") 737 + .arg("test.txt.gz"); 738 + extract.assert().success(); 739 + 740 + // Assert the files are identical 741 + working_dir 742 + .child("test.txt") 743 + .assert(predicate::path::eq_file(file.path())); 744 + 745 + Ok(()) 746 + } 747 + 748 + /// Magic roundtrip using multiple files with tar 749 + /// Compressing: input = test.txt/test2.txt, output = archive.tar 750 + /// Extracting: input = archive.tar, output = <default> 751 + /// 752 + /// ``` bash 753 + /// cmprss test.txt test2.txt archive.tar 754 + /// cmprss archive.tar 755 + /// ``` 756 + #[test] 757 + fn magic_roundtrip_multiple_files_tar() -> Result<(), Box<dyn std::error::Error>> { 758 + let file = assert_fs::NamedTempFile::new("test.txt")?; 759 + file.write_str("garbage data for testing")?; 760 + let file2 = assert_fs::NamedTempFile::new("test2.txt")?; 761 + file2.write_str("more garbage data for testing")?; 762 + 763 + let working_dir = assert_fs::TempDir::new()?; 764 + let archive = working_dir.child("archive.tar"); 765 + archive.assert(predicate::path::missing()); 766 + 767 + // Compress files to an archive 768 + let mut compress = Command::cargo_bin("cmprss")?; 769 + compress 770 + .current_dir(&working_dir) 771 + .arg("--ignore-pipes") 772 + .arg(file.path()) 773 + .arg(file2.path()) 774 + .arg("archive.tar"); 775 + compress.assert().success(); 776 + archive.assert(predicate::path::is_file()); 777 + 778 + // Extract file to default filename 779 + let mut extract = Command::cargo_bin("cmprss")?; 780 + extract 781 + .current_dir(&working_dir) 782 + .arg("--ignore-pipes") 783 + .arg("archive.tar"); 784 + extract.assert().success(); 785 + 786 + // Assert the files are identical 787 + working_dir 788 + .child("test.txt") 789 + .assert(predicate::path::eq_file(file.path())); 790 + working_dir 791 + .child("test2.txt") 792 + .assert(predicate::path::eq_file(file2.path())); 793 + 794 + Ok(()) 795 + } 796 + 797 + /// Magic roundtrip with tar.gz 798 + /// Infer things as much as possible 799 + /// Compressing: input = test.txt + test2.txt, output = test.tar.gz 800 + /// Extracting: input = test.tar.gz, output = test.txt + test2.txt 801 + /// 802 + /// ``` bash 803 + /// cmprss test.txt test2.txt archive.tar 804 + /// cmprss archive.tar archive.tar.gz 805 + /// cmprss archive.tar.gz archive.tar 806 + /// cmprss archive.tar 807 + /// ``` 808 + #[test] 809 + fn magic_roundtrip_tar_gz() -> Result<(), Box<dyn std::error::Error>> { 810 + let file = assert_fs::NamedTempFile::new("test.txt")?; 811 + file.write_str("garbage data for testing")?; 812 + let file2 = assert_fs::NamedTempFile::new("test2.txt")?; 813 + file2.write_str("more garbage data for testing")?; 814 + 815 + let working_dir = assert_fs::TempDir::new()?; 816 + let archive = working_dir.child("archive.tar"); 817 + archive.assert(predicate::path::missing()); 818 + let archive2 = working_dir.child("archive.tar.gz"); 819 + archive2.assert(predicate::path::missing()); 820 + 821 + let extract_dir = assert_fs::TempDir::new()?; 822 + 823 + // Compress files to an archive 824 + let mut compress = Command::cargo_bin("cmprss")?; 825 + compress 826 + .current_dir(&working_dir) 827 + .arg("--ignore-pipes") 828 + .arg(file.path()) 829 + .arg(file2.path()) 830 + .arg("archive.tar"); 831 + compress.assert().success(); 832 + archive.assert(predicate::path::is_file()); 833 + 834 + // Compress tar to an archive 835 + let mut compress2 = Command::cargo_bin("cmprss")?; 836 + compress2 837 + .current_dir(&working_dir) 838 + .arg("--ignore-pipes") 839 + .arg("archive.tar") 840 + .arg("archive.tar.gz"); 841 + compress2.assert().success(); 842 + archive2.assert(predicate::path::is_file()); 843 + 844 + // Extract file to default filename 845 + let mut extract = Command::cargo_bin("cmprss")?; 846 + extract 847 + .current_dir(&extract_dir) 848 + .arg("--ignore-pipes") 849 + .arg(archive2.path()) 850 + .arg("archive.tar"); 851 + extract.assert().success(); 852 + 853 + // Extract file to default filename 854 + let mut extract2 = Command::cargo_bin("cmprss")?; 855 + extract2 856 + .current_dir(&extract_dir) 857 + .arg("--ignore-pipes") 858 + .arg("archive.tar"); 859 + extract2.assert().success(); 860 + 861 + // Assert the files are identical 862 + extract_dir 863 + .child("test.txt") 864 + .assert(predicate::path::eq_file(file.path())); 865 + extract_dir 866 + .child("test2.txt") 867 + .assert(predicate::path::eq_file(file2.path())); 868 + 869 + Ok(()) 870 + } 871 + 872 + /// Magic roundtrip with tar.gz using pipes 873 + /// Infer things as much as possible 874 + /// Compressing: input = test.txt + test2.txt, output = test.tar.gz 875 + /// Extracting: input = test.tar.gz, output = test.txt + test2.txt 876 + /// 877 + /// ``` bash 878 + /// cmprss tar test.txt test2.txt | cmprss gzip | cmprss gzip --extract | cmprss tar --extract 879 + /// ``` 880 + #[test] 881 + fn magic_roundtrip_tar_gz_pipes() -> Result<(), Box<dyn std::error::Error>> { 882 + let file = assert_fs::NamedTempFile::new("test.txt")?; 883 + file.write_str("garbage data for testing")?; 884 + let file2 = assert_fs::NamedTempFile::new("test2.txt")?; 885 + file2.write_str("more garbage data for testing")?; 886 + 887 + let working_dir = assert_fs::TempDir::new()?; 888 + let tee1 = working_dir.child("tee1"); 889 + let tee2 = working_dir.child("tee2"); 890 + let tee3 = working_dir.child("tee3"); 891 + 892 + let extract_dir = assert_fs::TempDir::new()?; 893 + 894 + let mut compress = Command::cargo_bin("cmprss")?; 895 + compress 896 + .current_dir(&working_dir) 897 + .arg("tar") 898 + .arg("--ignore-stdin") 899 + .arg(file.path()) 900 + .arg(file2.path()) 901 + .stdout(Stdio::from(File::create(tee1.path())?)); 902 + compress.assert().success(); 903 + tee1.assert(predicate::path::is_file()); 904 + 905 + let mut compress2 = Command::cargo_bin("cmprss")?; 906 + compress2 907 + .current_dir(&working_dir) 908 + .arg("gzip") 909 + .stdin(Stdio::from(File::open(tee1.path())?)) 910 + .stdout(Stdio::from(File::create(tee2.path())?)); 911 + compress2.assert().success(); 912 + tee2.assert(predicate::path::is_file()); 913 + 914 + let mut extract = Command::cargo_bin("cmprss")?; 915 + extract 916 + .current_dir(&working_dir) 917 + .arg("gzip") 918 + .arg("--extract") 919 + .stdin(Stdio::from(File::open(tee2.path())?)) 920 + .stdout(Stdio::from(File::create(tee3.path())?)); 921 + extract.assert().success(); 922 + tee3.assert(predicate::path::is_file()); 923 + 924 + // Extract file to default filename 925 + let mut extract2 = Command::cargo_bin("cmprss")?; 926 + extract2 927 + .current_dir(&extract_dir) 928 + .arg("tar") 929 + .arg("--ignore-stdout") 930 + .arg("--extract") 931 + .stdin(Stdio::from(File::open(tee3.path())?)); 932 + extract2.assert().success(); 933 + 934 + // Assert the files are identical 935 + extract_dir 936 + .child("test.txt") 937 + .assert(predicate::path::eq_file(file.path())); 938 + extract_dir 939 + .child("test2.txt") 940 + .assert(predicate::path::eq_file(file2.path())); 941 + 942 + Ok(()) 943 + }