this repo has no description
0
fork

Configure Feed

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

feat(cli): recognize .tgz/.tbz/.tbz2/.txz/.tzst shortcut extensions

+125 -1
+9
README.md
··· 98 98 cmprss archive.tar.xz archive.tar 99 99 ``` 100 100 101 + The common compound shortcut extensions also work and behave identically to their long forms: 102 + 103 + | Shortcut | Equivalent to | 104 + | -------------- | ------------- | 105 + | `.tgz` | `.tar.gz` | 106 + | `.tbz`/`.tbz2` | `.tar.bz2` | 107 + | `.txz` | `.tar.xz` | 108 + | `.tzst` | `.tar.zst` | 109 + 101 110 Pipes can still be used if preferred: 102 111 103 112 ```bash
+46 -1
src/main.rs
··· 89 89 action: Action, 90 90 } 91 91 92 + /// Expand a compound shortcut extension like `.tgz` into its equivalent 93 + /// compressor chain, in innermost-to-outermost order. Returns `None` for 94 + /// extensions that aren't a known shortcut. 95 + fn expand_shortcut_ext(ext: &str) -> Option<&'static [&'static str]> { 96 + match ext { 97 + "tgz" => Some(&["tar", "gz"]), 98 + "tbz" | "tbz2" => Some(&["tar", "bz2"]), 99 + "txz" => Some(&["tar", "xz"]), 100 + "tzst" => Some(&["tar", "zst"]), 101 + _ => None, 102 + } 103 + } 104 + 92 105 /// Get a compressor pipeline from a filename by scanning extensions right-to-left 93 106 fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> { 94 107 let file_name = filename.file_name()?.to_str()?; ··· 101 114 // Scan extensions right-to-left, collecting known compressors 102 115 // until hitting an unknown extension or the base name. 103 116 // e.g., "a.b.tar.gz" → gz ✓, tar ✓, b ✗ stop → [gz, tar] 117 + // Compound shortcuts like "tgz" expand to their component chain, so 118 + // "archive.tgz" behaves identically to "archive.tar.gz". 104 119 let mut compressor_names: Vec<String> = Vec::new(); 105 120 for ext in parts[1..].iter().rev() { 106 - if let Some(c) = backends::compressor_from_str(ext) { 121 + if let Some(chain) = expand_shortcut_ext(ext) { 122 + // chain is innermost→outermost; we push in right-to-left order 123 + // (outermost first) to match how we're walking the filename. 124 + for name in chain.iter().rev() { 125 + compressor_names.push((*name).to_string()); 126 + } 127 + } else if let Some(c) = backends::compressor_from_str(ext) { 107 128 compressor_names.push(c.name().to_string()); 108 129 } else { 109 130 break; ··· 563 584 assert_eq!(compressor_name("archive.tar.xz"), Some("xz".into())); 564 585 assert_eq!(compressor_name("archive.tar.bz2"), Some("bzip2".into())); 565 586 assert_eq!(compressor_name("archive.tar.zst"), Some("zstd".into())); 587 + } 588 + 589 + #[test] 590 + fn test_shortcut_extensions() { 591 + // Shortcut extensions resolve to a tar + outer compressor pipeline, 592 + // so the reported name is the outer compressor (same as the long form). 593 + assert_eq!(compressor_name("archive.tgz"), Some("gzip".into())); 594 + assert_eq!(compressor_name("archive.tbz"), Some("bzip2".into())); 595 + assert_eq!(compressor_name("archive.tbz2"), Some("bzip2".into())); 596 + assert_eq!(compressor_name("archive.txz"), Some("xz".into())); 597 + assert_eq!(compressor_name("archive.tzst"), Some("zstd".into())); 598 + } 599 + 600 + #[test] 601 + fn test_shortcut_extensions_extract_to_directory() { 602 + // Shortcuts are tar-based, so they must extract to a directory. 603 + for path in ["a.tgz", "a.tbz", "a.tbz2", "a.txz", "a.tzst"] { 604 + let c = get_compressor_from_filename(Path::new(path)).unwrap(); 605 + assert_eq!( 606 + c.default_extracted_target(), 607 + ExtractedTarget::DIRECTORY, 608 + "{path} should extract to a directory", 609 + ); 610 + } 566 611 } 567 612 568 613 #[test]
+70
tests/shortcuts.rs
··· 1 + use assert_cmd::prelude::*; 2 + use assert_fs::prelude::*; 3 + use predicates::prelude::*; 4 + use std::process::Command; 5 + 6 + mod common; 7 + use common::*; 8 + 9 + /// Roundtrip helper: compress two files into `archive.<ext>` and extract into 10 + /// a fresh directory, asserting that the original files come back identical. 11 + fn shortcut_roundtrip(ext: &str) -> Result<(), Box<dyn std::error::Error>> { 12 + let file = create_test_file("test.txt", "garbage data for testing")?; 13 + let file2 = create_test_file("test2.txt", "more garbage data for testing")?; 14 + let working_dir = create_working_dir()?; 15 + let archive_name = format!("archive.{ext}"); 16 + let archive = working_dir.child(&archive_name); 17 + archive.assert(predicate::path::missing()); 18 + 19 + let mut compress = Command::cargo_bin("cmprss")?; 20 + compress 21 + .current_dir(&working_dir) 22 + .arg("--ignore-pipes") 23 + .arg(file.path()) 24 + .arg(file2.path()) 25 + .arg(&archive_name); 26 + compress.assert().success(); 27 + archive.assert(predicate::path::is_file()); 28 + 29 + let extract_dir = create_working_dir()?; 30 + let mut extract = Command::cargo_bin("cmprss")?; 31 + extract 32 + .current_dir(&extract_dir) 33 + .arg("--ignore-pipes") 34 + .arg(archive.path()); 35 + extract.assert().success(); 36 + 37 + assert_files_equal(file.path(), &extract_dir.child("test.txt")); 38 + assert_files_equal(file2.path(), &extract_dir.child("test2.txt")); 39 + 40 + Ok(()) 41 + } 42 + 43 + mod shortcuts { 44 + use super::*; 45 + 46 + #[test] 47 + fn tgz() -> Result<(), Box<dyn std::error::Error>> { 48 + shortcut_roundtrip("tgz") 49 + } 50 + 51 + #[test] 52 + fn tbz() -> Result<(), Box<dyn std::error::Error>> { 53 + shortcut_roundtrip("tbz") 54 + } 55 + 56 + #[test] 57 + fn tbz2() -> Result<(), Box<dyn std::error::Error>> { 58 + shortcut_roundtrip("tbz2") 59 + } 60 + 61 + #[test] 62 + fn txz() -> Result<(), Box<dyn std::error::Error>> { 63 + shortcut_roundtrip("txz") 64 + } 65 + 66 + #[test] 67 + fn tzst() -> Result<(), Box<dyn std::error::Error>> { 68 + shortcut_roundtrip("tzst") 69 + } 70 + }