Skip to main content

zzz_arc/
cli.rs

1//! command line interface
2
3use clap::{Parser, Subcommand};
4use std::path::{Path, PathBuf};
5
6#[derive(Parser)]
7#[command(
8    name = "zzz",
9    version,
10    about = "zzz: compression multitool",
11    long_about = "Create and extract archives in multiple formats (zst, tgz, txz, zip, 7z) with smart file filtering and magic number detection"
12)]
13pub struct Cli {
14    #[command(subcommand)]
15    pub command: Commands,
16
17    /// verbose output
18    #[arg(short, long, global = true)]
19    pub verbose: bool,
20
21    /// number of threads (0 = auto-detect)
22    #[arg(short = 'j', long, global = true, default_value = "0")]
23    pub threads: u32,
24}
25
26#[derive(Subcommand)]
27pub enum Commands {
28    /// compress files/directories (supports .zst, .tgz, .txz, .zip, .7z)
29    #[command(alias = "c")]
30    Compress {
31        /// compression level (1-22)
32        #[arg(short, long, default_value = "19", value_parser = clap::value_parser!(i32).range(1..=22))]
33        level: i32,
34
35        /// output file path
36        #[arg(short, long)]
37        output: Option<PathBuf>,
38
39        /// show progress bar
40        #[arg(short = 'P', long)]
41        progress: bool,
42
43        /// exclude files matching pattern (repeatable)
44        #[arg(short = 'e', long)]
45        exclude: Vec<String>,
46
47        /// preserve extended attributes (xattrs) in tar-based archives
48        #[arg(long)]
49        keep_xattrs: bool,
50
51        /// preserve original file permissions in archive entries
52        #[arg(long)]
53        keep_permissions: bool,
54
55        /// preserve ownership (uid/gid) in tar-based archives
56        #[arg(long)]
57        keep_ownership: bool,
58
59        /// follow symlinks and archive target contents (may include files outside input)
60        #[arg(long)]
61        follow_symlinks: bool,
62
63        /// allow symlink targets outside the input root (requires --follow-symlinks)
64        #[arg(long)]
65        allow_symlink_escape: bool,
66
67        /// strip timestamps and xattrs, normalize ownership/permissions, and exclude common secrets (overrides keep flags)
68        #[arg(long)]
69        redact: bool,
70
71        /// strip filesystem timestamps in archive entries
72        #[arg(long)]
73        strip_timestamps: bool,
74
75        /// disable built-in garbage file filtering
76        #[arg(short = 'E', long)]
77        no_default_excludes: bool,
78
79        /// force specific format (zst, tgz, txz, zip, 7z)
80        #[arg(short = 'f', long, value_parser = parse_format)]
81        format: Option<crate::formats::Format>,
82
83        /// overwrite existing output file
84        #[arg(short = 'y', long)]
85        overwrite: bool,
86
87        /// input file or directory
88        input: PathBuf,
89
90        /// password for encryption (supported by zst and 7z)
91        #[arg(short = 'p', long)]
92        password: Option<String>,
93    },
94
95    /// extract archives (auto-detects format: .zst, .tgz, .txz, .zip, .7z)
96    #[command(alias = "x")]
97    Extract {
98        /// archive file to extract
99        archive: PathBuf,
100
101        /// destination directory
102        destination: Option<PathBuf>,
103
104        /// extract to specific directory
105        #[arg(short = 'C', long)]
106        directory: Option<PathBuf>,
107
108        /// show progress bar
109        #[arg(short = 'P', long)]
110        progress: bool,
111
112        /// strip leading path components
113        #[arg(long, default_value = "0")]
114        strip_components: usize,
115
116        /// preserve extended attributes (xattrs) when extracting tar-based archives
117        #[arg(long)]
118        keep_xattrs: bool,
119
120        /// strip filesystem timestamps when extracting tar-based archives
121        #[arg(long)]
122        strip_timestamps: bool,
123
124        /// preserve file permissions when extracting
125        #[arg(long)]
126        keep_permissions: bool,
127
128        /// preserve ownership (uid/gid) when extracting tar-based archives
129        #[arg(long)]
130        keep_ownership: bool,
131
132        /// overwrite existing files
133        #[arg(short = 'y', long)]
134        overwrite: bool,
135
136        /// password for decryption (for zst and 7z)
137        #[arg(short = 'p', long)]
138        password: Option<String>,
139    },
140
141    /// list archive contents
142    #[command(alias = "l")]
143    List {
144        /// archive file to list
145        archive: PathBuf,
146    },
147
148    /// test archive integrity
149    #[command(alias = "t")]
150    Test {
151        /// archive file to test
152        archive: PathBuf,
153    },
154}
155
156/// Parse format string into Format enum
157fn parse_format(s: &str) -> Result<crate::formats::Format, String> {
158    match s.to_lowercase().as_str() {
159        "zst" | "zstd" => Ok(crate::formats::Format::Zstd),
160        "tgz" | "gz" | "gzip" => Ok(crate::formats::Format::Gzip),
161        "txz" | "xz" => Ok(crate::formats::Format::Xz),
162        "zip" => Ok(crate::formats::Format::Zip),
163        "7z" | "sevenz" => Ok(crate::formats::Format::SevenZ),
164        _ => Err(format!(
165            "unsupported format '{s}'. Supported formats: zst, tgz, txz, zip, 7z"
166        )),
167    }
168}
169
170impl Cli {
171    /// get output path for compression, defaulting to input + appropriate extension
172    pub fn get_output_path(
173        input: &Path,
174        output: Option<PathBuf>,
175        format: Option<crate::formats::Format>,
176    ) -> PathBuf {
177        output.unwrap_or_else(|| {
178            let mut path = input.to_path_buf();
179            let extension = match format {
180                Some(f) => f.extension(),
181                None => "zst",
182            };
183
184            if let Some(filename) = path.file_name() {
185                let mut new_filename = filename.to_os_string();
186                new_filename.push(".");
187                new_filename.push(extension);
188                path.set_file_name(new_filename);
189            } else {
190                path.set_extension(extension);
191            }
192            path
193        })
194    }
195
196    /// get extraction directory, defaulting to current directory
197    pub fn get_extract_dir(destination: Option<PathBuf>, directory: Option<PathBuf>) -> PathBuf {
198        directory
199            .or(destination)
200            .unwrap_or_else(|| PathBuf::from("."))
201    }
202}