this repo has no description
0
fork

Configure Feed

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

at tangled-ci 675 lines 28 kB view raw
1//! Job inference — maps user-provided CLI args and filenames into a concrete 2//! `Compressor` + action + input/output triple. 3//! 4//! Most of the user-facing ergonomics of `cmprss` live here: guessing whether 5//! we're compressing or extracting, whether the input is piped from stdin, 6//! which compressor a filename implies, and how to dispatch compound extensions 7//! like `.tar.gz` or `.tgz` into a pipeline. 8 9use anyhow::{anyhow, bail}; 10use is_terminal::IsTerminal; 11use std::path::{Path, PathBuf}; 12 13use crate::backends::{self, Pipeline}; 14use crate::utils::{CmprssInput, CmprssOutput, CommonArgs, Compressor, Result}; 15 16/// Extract an action hint from the CLI flags. Returns `None` when the user 17/// hasn't specified `--compress`/`--extract`/`--append`, in which case the 18/// action will be inferred from filenames downstream. 19fn action_from_flags(args: &CommonArgs) -> Option<Action> { 20 if args.compress { 21 Some(Action::Compress) 22 } else if args.extract { 23 Some(Action::Extract) 24 } else if args.append { 25 Some(Action::Append) 26 } else { 27 None 28 } 29} 30 31/// Partition the CLI path arguments (`-i`, `-o`, and the positional `io_list`) 32/// into a list of input paths and an optional output path. 33/// 34/// The heuristic for which trailing `io_list` entry becomes the output: 35/// * If it doesn't exist on disk → output (we'll create it). 36/// * If it exists and is a directory, and the action hint is `Extract` → 37/// output (extract into the directory). 38/// * Otherwise → treat as an input. This preserves the existing behavior for 39/// `cmprss tar file1.txt file2.txt existing_dir/`, where the trailing 40/// directory is treated as another input to archive. 41fn partition_paths( 42 args: &CommonArgs, 43 action_hint: Option<Action>, 44) -> Result<(Vec<PathBuf>, Option<PathBuf>)> { 45 let mut inputs = Vec::new(); 46 if let Some(in_file) = &args.input { 47 inputs 48 .push(get_path(in_file).ok_or_else(|| anyhow!("Specified input path does not exist"))?); 49 } 50 51 let mut output: Option<PathBuf> = match &args.output { 52 Some(output) => { 53 let path = PathBuf::from(output); 54 if !args.force && path.try_exists()? && !path.is_dir() { 55 bail!("Specified output path already exists (use --force to overwrite)"); 56 } 57 Some(path) 58 } 59 None => None, 60 }; 61 62 let mut io_list = args.io_list.clone(); 63 if output.is_none() 64 && let Some(possible_output) = io_list.last() 65 { 66 let path = PathBuf::from(possible_output); 67 if !path.try_exists()? { 68 output = Some(path); 69 io_list.pop(); 70 } else if path.is_dir() && action_hint == Some(Action::Extract) { 71 // Only treat an existing directory as the output when the user 72 // hinted extraction. In Compress/Unknown, we keep it as another 73 // input — this matches e.g. `cmprss tar dir1/ dir2/`. 74 output = Some(path); 75 io_list.pop(); 76 } else if !path.is_dir() && args.force { 77 // With --force, a trailing existing file is taken as the output 78 // (to overwrite). Without --force we fall through to treating it 79 // as another input. 80 output = Some(path); 81 io_list.pop(); 82 } else if !path.is_dir() && action_hint == Some(Action::Append) { 83 // --append takes the trailing existing file as the archive to 84 // grow. Same positional convention as compress: the target is 85 // last, the new inputs are before it. 86 output = Some(path); 87 io_list.pop(); 88 } 89 } 90 91 for input in &io_list { 92 inputs.push(get_path(input).ok_or_else(|| anyhow!("Specified input path does not exist"))?); 93 } 94 95 Ok((inputs, output)) 96} 97 98/// Turn the collected input paths into a `CmprssInput`, falling back to 99/// stdin when no paths were given and stdin is piped. 100fn resolve_input(inputs: Vec<PathBuf>, args: &CommonArgs) -> Result<CmprssInput> { 101 if !inputs.is_empty() { 102 return Ok(CmprssInput::Path(inputs)); 103 } 104 if !std::io::stdin().is_terminal() && !args.ignore_pipes && !args.ignore_stdin { 105 return Ok(CmprssInput::Pipe(std::io::stdin())); 106 } 107 bail!("No input specified"); 108} 109 110/// Whether we can send the output to stdout (piped, and the user hasn't 111/// suppressed pipe inference). 112fn stdout_pipe_usable(args: &CommonArgs) -> bool { 113 !std::io::stdout().is_terminal() && !args.ignore_pipes && !args.ignore_stdout 114} 115 116/// Defines a single compress/extract action to take. 117#[derive(Debug)] 118pub struct Job { 119 pub compressor: Box<dyn Compressor>, 120 pub input: CmprssInput, 121 pub output: CmprssOutput, 122 pub action: Action, 123} 124 125#[derive(Debug, PartialEq, Clone, Copy)] 126pub enum Action { 127 Compress, 128 Extract, 129 /// Print the archive's file listing to stdout. Only meaningful for 130 /// container formats; stream codecs fall through to `Compressor::list`'s 131 /// default bail. 132 List, 133 /// Append new entries to an existing archive. Only supported by container 134 /// formats that can grow in place (tar, zip). 135 Append, 136} 137 138/// Parse the common args and determine the details of the job requested. 139/// 140/// The resolution has three phases: 141/// 1. Collect explicit signals from CLI flags (action hint, `-i`/`-o`, the 142/// positional `io_list`) into an input list and an optional output path. 143/// 2. Build the final `CmprssInput` (falling back to stdin if no paths). 144/// 3. Decide the output and the missing pieces of (compressor, action). This 145/// branches on how the output is determined: an explicit path, stdout pipe, 146/// or a filename we invent from the resolved compressor + action. 147pub fn get_job(compressor: Option<Box<dyn Compressor>>, common_args: &CommonArgs) -> Result<Job> { 148 // --list short-circuits the output/action machinery: there's no output 149 // file, the action is fixed, and we only need the input and compressor. 150 if common_args.list { 151 let (input_paths, _) = partition_paths(common_args, Some(Action::List))?; 152 let input = resolve_input(input_paths, common_args)?; 153 let compressor = compressor 154 .or_else(|| get_compressor_from_filename(get_input_filename(&input).ok()?)) 155 .ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 156 return Ok(Job { 157 compressor, 158 input, 159 // List writes to stdout directly; this output slot is unused. 160 output: CmprssOutput::Pipe(std::io::stdout()), 161 action: Action::List, 162 }); 163 } 164 165 let action_hint = action_from_flags(common_args); 166 let (input_paths, output_path) = partition_paths(common_args, action_hint)?; 167 let input = resolve_input(input_paths, common_args)?; 168 169 // Branch 1: user gave us an output path. Resolve compressor + action 170 // using both sides' extensions. 171 if let Some(path) = output_path { 172 let output = CmprssOutput::Path(path); 173 let (compressor, action) = finalize_with_output(compressor, action_hint, &input, &output)?; 174 return Ok(Job { 175 compressor, 176 input, 177 output, 178 action, 179 }); 180 } 181 182 // Branch 2: stdout is a pipe. Same resolution, but the output has no path. 183 if stdout_pipe_usable(common_args) { 184 let output = CmprssOutput::Pipe(std::io::stdout()); 185 let (compressor, action) = finalize_with_output(compressor, action_hint, &input, &output)?; 186 return Ok(Job { 187 compressor, 188 input, 189 output, 190 action, 191 }); 192 } 193 194 // Branch 3: no output and stdout is a terminal. We must invent a filename, 195 // which requires the compressor and action up front. 196 let (compressor, action) = finalize_without_output(compressor, action_hint, &input)?; 197 let default_name = match action { 198 Action::Compress => compressor.default_compressed_filename(get_input_filename(&input)?), 199 Action::Extract => compressor.default_extracted_filename(get_input_filename(&input)?), 200 // List short-circuits above; finalize_without_output never returns it. 201 Action::List => unreachable!("List is handled before Branch 3"), 202 // Append without a target archive path has nothing to append to; 203 // `finalize_without_output` rejects it before reaching here. 204 Action::Append => unreachable!("Append requires an existing output path"), 205 }; 206 Ok(Job { 207 compressor, 208 input, 209 output: CmprssOutput::Path(PathBuf::from(default_name)), 210 action, 211 }) 212} 213 214/// Finalize compressor + action when the output is already materialized 215/// (either `CmprssOutput::Path` or `CmprssOutput::Pipe`). 216fn finalize_with_output( 217 mut compressor: Option<Box<dyn Compressor>>, 218 mut action: Option<Action>, 219 input: &CmprssInput, 220 output: &CmprssOutput, 221) -> Result<(Box<dyn Compressor>, Action)> { 222 if compressor.is_none() || action.is_none() { 223 fill_missing_from_io(&mut compressor, &mut action, input, output)?; 224 } 225 let compressor = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 226 let action = action.ok_or_else(|| anyhow!("Could not determine action to take"))?; 227 Ok((compressor, action)) 228} 229 230/// Finalize compressor + action when no output path is known (stdout is a 231/// terminal and we'll invent a filename next). All inference must come from 232/// the input side. 233fn finalize_without_output( 234 compressor: Option<Box<dyn Compressor>>, 235 action: Option<Action>, 236 input: &CmprssInput, 237) -> Result<(Box<dyn Compressor>, Action)> { 238 let input_path = get_input_filename(input)?; 239 match action { 240 Some(Action::Compress) => { 241 let c = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 242 Ok((c, Action::Compress)) 243 } 244 Some(Action::Extract) => { 245 let c = compressor 246 .or_else(|| get_compressor_from_filename(input_path)) 247 .ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 248 Ok((c, Action::Extract)) 249 } 250 // Append needs an existing archive path to grow; without one there's 251 // nothing to append to. 252 Some(Action::Append) => { 253 bail!("--append requires an existing archive as the output target") 254 } 255 // List is handled by the short-circuit at the top of get_job and 256 // never flows into this helper. 257 Some(Action::List) => unreachable!("List is handled before Branch 3"), 258 None => match compressor { 259 Some(c) => { 260 // Compare the compressor's extension against the input's. 261 let action = match get_compressor_from_filename(input_path) { 262 Some(ic) if ic.name() == c.name() => Action::Extract, 263 _ => Action::Compress, 264 }; 265 Ok((c, action)) 266 } 267 None => { 268 // The input has to be something we can identify as an archive. 269 let c = get_compressor_from_filename(input_path) 270 .ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 271 Ok((c, Action::Extract)) 272 } 273 }, 274 } 275} 276 277/// Fill in a missing compressor and/or action by inspecting the input and 278/// output shapes. Called after the output is known; covers every combination 279/// of (Path, Pipe) input × (Path, Pipe) output. 280fn fill_missing_from_io( 281 compressor: &mut Option<Box<dyn Compressor>>, 282 action: &mut Option<Action>, 283 input: &CmprssInput, 284 output: &CmprssOutput, 285) -> Result { 286 match *action { 287 Some(Action::Compress) => { 288 if let CmprssOutput::Path(path) = output { 289 *compressor = get_compressor_from_filename(path); 290 } 291 } 292 // List is handled by the short-circuit at the top of get_job. 293 Some(Action::List) => unreachable!("List is handled before fill_missing_from_io"), 294 Some(Action::Append) => { 295 // Append needs an existing archive path to grow; infer the 296 // compressor from that path's extension when it wasn't given. 297 match output { 298 CmprssOutput::Path(path) => { 299 if compressor.is_none() { 300 *compressor = get_compressor_from_filename(path); 301 } 302 } 303 _ => bail!("--append requires an archive path, not a pipe, as the target"), 304 } 305 } 306 Some(Action::Extract) => { 307 if let CmprssInput::Path(paths) = input { 308 let [archive_path] = paths.as_slice() else { 309 bail!("Expected exactly one input archive"); 310 }; 311 *compressor = get_compressor_from_filename(archive_path); 312 } 313 } 314 None => match (input, output) { 315 (CmprssInput::Path(paths), CmprssOutput::Path(path)) => match paths.as_slice() { 316 [single] if path.is_dir() => { 317 *compressor = get_compressor_from_filename(single); 318 *action = Some(Action::Extract); 319 if compressor.is_none() { 320 bail!("Could not determine compressor for {:?}", single); 321 } 322 } 323 _ => { 324 let (c, a) = guess_from_filenames(paths, path, compressor.take())?; 325 *compressor = Some(c); 326 *action = Some(a); 327 } 328 }, 329 (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => { 330 // `resolve_input` guarantees `paths` is non-empty when it 331 // returns `CmprssInput::Path`, so `first()` is always Some — 332 // but surface a clean error instead of relying on the invariant. 333 let first = paths 334 .first() 335 .ok_or_else(|| anyhow!("No input file specified"))?; 336 if let Some(c) = compressor.as_deref() { 337 *action = Some(match get_compressor_from_filename(first) { 338 Some(ic) if ic.name() == c.name() => Action::Extract, 339 _ => Action::Compress, 340 }); 341 } else { 342 if paths.len() != 1 { 343 bail!("Expected exactly one input file when writing to stdout"); 344 } 345 *compressor = get_compressor_from_filename(first); 346 if compressor.is_some() { 347 *action = Some(Action::Extract); 348 } else { 349 bail!("Could not determine compressor to use"); 350 } 351 } 352 } 353 (CmprssInput::Pipe(_), CmprssOutput::Path(path)) => { 354 if let Some(c) = compressor.as_deref() { 355 *action = Some( 356 if get_compressor_from_filename(path) 357 .is_some_and(|pc| c.name() == pc.name()) 358 { 359 Action::Compress 360 } else { 361 Action::Extract 362 }, 363 ); 364 } else { 365 *compressor = get_compressor_from_filename(path); 366 if compressor.is_some() { 367 *action = Some(Action::Compress); 368 } else { 369 bail!("Could not determine compressor to use"); 370 } 371 } 372 } 373 (CmprssInput::Pipe(_), CmprssOutput::Pipe(_)) => { 374 *action = Some(Action::Compress); 375 } 376 // Writer output and Reader input are only constructed internally 377 // by the Pipeline compressor; they don't reach get_job from main. 378 (_, CmprssOutput::Writer(_)) => *action = Some(Action::Compress), 379 (CmprssInput::Reader(_), _) => *action = Some(Action::Extract), 380 }, 381 } 382 Ok(()) 383} 384 385/// Get the input filename or return a default file 386/// This file will be used to generate the output filename 387fn get_input_filename(input: &CmprssInput) -> Result<&Path> { 388 match input { 389 CmprssInput::Path(paths) => match paths.first() { 390 Some(path) => Ok(path), 391 None => bail!("No input specified"), 392 }, 393 CmprssInput::Pipe(_) => Ok(Path::new("archive")), 394 CmprssInput::Reader(_) => Ok(Path::new("piped_data")), 395 } 396} 397 398/// Get a compressor pipeline from a filename by scanning extensions right-to-left 399pub fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> { 400 let file_name = filename.file_name()?.to_str()?; 401 let parts: Vec<&str> = file_name.split('.').collect(); 402 403 if parts.len() < 2 { 404 return None; 405 } 406 407 // Scan extensions right-to-left, collecting known compressors 408 // until hitting an unknown extension or the base name. 409 // e.g., "a.b.tar.gz" → gz ✓, tar ✓, b ✗ stop → [gz, tar] 410 // `chain_from_ext` handles both single-codec extensions and compound 411 // shortcuts like `tgz` (which expand to `[tar, gz]`). 412 let mut chain: Vec<Box<dyn Compressor>> = Vec::new(); 413 for ext in parts[1..].iter().rev() { 414 match backends::chain_from_ext(ext) { 415 Some(stage) => { 416 // stage is innermost→outermost; we walk the filename 417 // right-to-left so we push outermost first. 418 for c in stage.into_iter().rev() { 419 chain.push(c); 420 } 421 } 422 None => break, 423 } 424 } 425 426 if chain.is_empty() { 427 return None; 428 } 429 430 // Reverse to innermost-to-outermost order 431 chain.reverse(); 432 Some(Box::new(Pipeline::new(chain))) 433} 434 435/// Convert an input path into a Path 436fn get_path(input: &str) -> Option<PathBuf> { 437 let path = PathBuf::from(input); 438 if !path.try_exists().unwrap_or(false) { 439 return None; 440 } 441 Some(path) 442} 443 444/// Guess compressor/action from the two filenames. The compressor may already 445/// be given via the subcommand. 446/// 447/// Returns an error when the two filenames don't give enough information to 448/// pick an action (e.g. the same format on both sides and the output isn't a 449/// directory). 450fn guess_from_filenames( 451 input: &[PathBuf], 452 output: &Path, 453 compressor: Option<Box<dyn Compressor>>, 454) -> Result<(Box<dyn Compressor>, Action)> { 455 let input = match input { 456 [single] => single, 457 _ => { 458 if let Some(c) = get_compressor_from_filename(output) { 459 return Ok((c, Action::Compress)); 460 } 461 if output.is_dir() 462 && let Some(first) = input.first() 463 && let Some(c) = get_compressor_from_filename(first) 464 { 465 return Ok((c, Action::Extract)); 466 } 467 // No extension hint anywhere, but we were given a compressor — 468 // assume the user wants to extract multiple archives to a directory. 469 let c = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 470 return Ok((c, Action::Extract)); 471 } 472 }; 473 474 let output_guess = get_compressor_from_filename(output); 475 let input_guess = get_compressor_from_filename(input); 476 477 // If the user supplied a compressor via subcommand, pick the action by 478 // matching its name against the input/output extensions. 479 if let Some(c) = compressor { 480 let action = if output_guess 481 .as_ref() 482 .is_some_and(|og| og.name() == c.name()) 483 { 484 Action::Compress 485 } else if input_guess.as_ref().is_some_and(|ig| ig.name() == c.name()) { 486 Action::Extract 487 } else { 488 // Extensions don't match on either side; default to compressing. 489 Action::Compress 490 }; 491 return Ok((c, action)); 492 } 493 494 match (output_guess, input_guess) { 495 (None, None) => bail!("Could not determine compressor to use"), 496 (Some(c), None) => Ok((c, Action::Compress)), 497 (None, Some(e)) => Ok((e, Action::Extract)), 498 (Some(c), Some(e)) => { 499 // Both sides carry a known extension — decide whether this is 500 // adding or stripping a single outer layer (e.g. tar → tar.gz). 501 let input_file = input 502 .file_name() 503 .and_then(|f| f.to_str()) 504 .ok_or_else(|| anyhow!("Could not parse input filename"))?; 505 let input_ext = input 506 .extension() 507 .and_then(|e| e.to_str()) 508 .ok_or_else(|| anyhow!("Could not parse input extension"))?; 509 let output_file = output 510 .file_name() 511 .and_then(|f| f.to_str()) 512 .ok_or_else(|| anyhow!("Could not parse output filename"))?; 513 let output_ext = output 514 .extension() 515 .and_then(|e| e.to_str()) 516 .ok_or_else(|| anyhow!("Could not parse output extension"))?; 517 let layer_added = format!("{input_file}.{output_ext}"); 518 let layer_stripped = format!("{output_file}.{input_ext}"); 519 520 if layer_added == output_file { 521 // input="archive.tar", output="archive.tar.gz" — add the outer layer only. 522 let single = backends::compressor_from_str(output_ext).unwrap_or(c); 523 Ok((single, Action::Compress)) 524 } else if layer_stripped == input_file { 525 // input="archive.tar.gz", output="archive.tar" — strip the outer layer only. 526 let single = backends::compressor_from_str(input_ext).unwrap_or(e); 527 Ok((single, Action::Extract)) 528 } else if c.name() == e.name() { 529 // Same format on both sides: only meaningful when the output 530 // is a directory (extracting in place). 531 if output.is_dir() { 532 Ok((e, Action::Extract)) 533 } else { 534 bail!("Could not determine action to take"); 535 } 536 } else if output.is_dir() { 537 Ok((e, Action::Extract)) 538 } else { 539 bail!("Could not determine action to take"); 540 } 541 } 542 } 543} 544 545#[cfg(test)] 546mod tests { 547 use super::*; 548 use crate::utils::ExtractedTarget; 549 use std::path::Path; 550 551 fn compressor_name(path: &str) -> Option<String> { 552 get_compressor_from_filename(Path::new(path)).map(|c| c.name().to_string()) 553 } 554 555 fn compressor_extension(path: &str) -> Option<String> { 556 get_compressor_from_filename(Path::new(path)).map(|c| c.extension().to_string()) 557 } 558 559 #[test] 560 fn test_single_extension() { 561 assert_eq!(compressor_name("file.gz"), Some("gzip".into())); 562 assert_eq!(compressor_name("file.xz"), Some("xz".into())); 563 assert_eq!(compressor_name("file.bz2"), Some("bzip2".into())); 564 assert_eq!(compressor_name("file.zst"), Some("zstd".into())); 565 assert_eq!(compressor_name("file.lz4"), Some("lz4".into())); 566 assert_eq!(compressor_name("file.br"), Some("brotli".into())); 567 assert_eq!(compressor_name("file.sz"), Some("snappy".into())); 568 assert_eq!(compressor_name("file.lzma"), Some("lzma".into())); 569 assert_eq!(compressor_name("file.tar"), Some("tar".into())); 570 assert_eq!(compressor_name("file.zip"), Some("zip".into())); 571 } 572 573 #[test] 574 fn test_multi_extension() { 575 assert_eq!(compressor_name("archive.tar.gz"), Some("gzip".into())); 576 assert_eq!(compressor_name("archive.tar.xz"), Some("xz".into())); 577 assert_eq!(compressor_name("archive.tar.bz2"), Some("bzip2".into())); 578 assert_eq!(compressor_name("archive.tar.zst"), Some("zstd".into())); 579 } 580 581 #[test] 582 fn test_shortcut_extensions() { 583 // Shortcut extensions resolve to a tar + outer compressor pipeline, 584 // so the reported name is the outer compressor (same as the long form). 585 assert_eq!(compressor_name("archive.tgz"), Some("gzip".into())); 586 assert_eq!(compressor_name("archive.tbz"), Some("bzip2".into())); 587 assert_eq!(compressor_name("archive.tbz2"), Some("bzip2".into())); 588 assert_eq!(compressor_name("archive.txz"), Some("xz".into())); 589 assert_eq!(compressor_name("archive.tzst"), Some("zstd".into())); 590 } 591 592 #[test] 593 fn test_shortcut_extensions_extract_to_directory() { 594 // Shortcuts are tar-based, so they must extract to a directory. 595 for path in ["a.tgz", "a.tbz", "a.tbz2", "a.txz", "a.tzst"] { 596 let c = get_compressor_from_filename(Path::new(path)).unwrap(); 597 assert_eq!( 598 c.default_extracted_target(), 599 ExtractedTarget::Directory, 600 "{path} should extract to a directory", 601 ); 602 } 603 } 604 605 #[test] 606 fn test_unknown_middle_extension() { 607 // "b" is not a compressor, so only tar.gz should be detected 608 assert_eq!(compressor_name("a.b.tar.gz"), Some("gzip".into())); 609 assert_eq!(compressor_name("report.2024.tar.gz"), Some("gzip".into())); 610 } 611 612 #[test] 613 fn test_no_recognized_extension() { 614 assert_eq!(compressor_name("file.txt"), None); 615 assert_eq!(compressor_name("file.pdf"), None); 616 assert_eq!(compressor_name("file"), None); 617 } 618 619 #[test] 620 fn test_default_filenames_single_pipeline() { 621 let c = get_compressor_from_filename(Path::new("file.gz")).unwrap(); 622 assert_eq!( 623 c.default_compressed_filename(Path::new("data.txt")), 624 "data.txt.gz" 625 ); 626 assert_eq!(c.default_extracted_filename(Path::new("data.gz")), "data"); 627 } 628 629 #[test] 630 fn test_default_filenames_multi_pipeline() { 631 let c = get_compressor_from_filename(Path::new("archive.tar.gz")).unwrap(); 632 assert_eq!( 633 c.default_compressed_filename(Path::new("data")), 634 "data.tar.gz" 635 ); 636 // tar.gz extracts to a directory, so extracted filename is "." 637 assert_eq!(c.default_extracted_filename(Path::new("data.tar.gz")), "."); 638 } 639 640 #[test] 641 fn test_is_archive_single_pipeline() { 642 let c = get_compressor_from_filename(Path::new("file.gz")).unwrap(); 643 assert!(c.is_archive(Path::new("test.gz"))); 644 assert!(!c.is_archive(Path::new("test.xz"))); 645 } 646 647 #[test] 648 fn test_is_archive_multi_pipeline() { 649 let c = get_compressor_from_filename(Path::new("archive.tar.gz")).unwrap(); 650 assert!(c.is_archive(Path::new("foo.tar.gz"))); 651 assert!(!c.is_archive(Path::new("foo.gz"))); 652 } 653 654 #[test] 655 fn test_extracted_target_single_pipeline() { 656 let gz = get_compressor_from_filename(Path::new("file.gz")).unwrap(); 657 assert_eq!(gz.default_extracted_target(), ExtractedTarget::File); 658 659 let tar = get_compressor_from_filename(Path::new("file.tar")).unwrap(); 660 assert_eq!(tar.default_extracted_target(), ExtractedTarget::Directory); 661 } 662 663 #[test] 664 fn test_extracted_target_multi_pipeline() { 665 // tar.gz: innermost is tar, which extracts to directory 666 let c = get_compressor_from_filename(Path::new("archive.tar.gz")).unwrap(); 667 assert_eq!(c.default_extracted_target(), ExtractedTarget::Directory); 668 } 669 670 #[test] 671 fn test_single_extension_returns_correct_extension() { 672 assert_eq!(compressor_extension("file.gz"), Some("gz".into())); 673 assert_eq!(compressor_extension("file.tar"), Some("tar".into())); 674 } 675}