stuffr_cli/cli.rs
1//! The `stuffr` command's argument surface.
2//!
3//! Pulled out of `main.rs` and re-exported from `lib.rs` so an integration
4//! test can ask clap itself — via [`clap::CommandFactory`] — what flags and
5//! subcommands exist, rather than keeping a second, hand-maintained list that
6//! the examples page could silently drift out of sync with. `main.rs` still
7//! owns everything that isn't the argument shape: parsing, dispatch, and the
8//! examples text itself.
9
10use clap::{Parser, Subcommand};
11
12#[derive(Parser)]
13#[command(
14 name = "stuffr",
15 version,
16 about = "Universal compression and archive toolkit"
17)]
18pub struct Cli {
19 #[command(subcommand)]
20 pub command: Option<Command>,
21 /// Print detailed usage examples covering every feature, then exit.
22 #[arg(long)]
23 pub examples: bool,
24}
25
26#[derive(Subcommand)]
27pub enum Command {
28 /// Compress a file, or collect several into an archive.
29 Pack {
30 /// Input paths, or `-` for stdin.
31 ///
32 /// More than one path needs an output that names a CONTAINER (`-o
33 /// bundle.tar`, or `--format tar`): a codec compresses one stream and
34 /// has nowhere to put a second.
35 #[arg(required = true, num_args = 1.., value_name = "PATHS")]
36 paths: Vec<String>,
37 /// Output path. Defaults to INPUT plus the format's extension.
38 #[arg(short, long)]
39 output: Option<String>,
40 /// Use this format instead of inferring one from the output name.
41 #[arg(long)]
42 format: Option<String>,
43 /// Compression level. gzip accepts 0-9.
44 #[arg(long)]
45 level: Option<i32>,
46 /// Overwrite an existing output.
47 #[arg(long)]
48 force: bool,
49 /// Skip the fsync that makes the output durable before it is published.
50 #[arg(long)]
51 no_sync: bool,
52 /// Use a weak fallback encoder that this build ships in place of the
53 /// format's real one. Worse ratio, and buffers the whole input.
54 #[arg(long)]
55 allow_weak_encoder: bool,
56 /// Encode with N worker threads. 0 means auto-detect.
57 ///
58 /// OMITTING this flag is not the same as passing 0: without it, stuffr
59 /// encodes single-threaded, so the same input always produces the same
60 /// bytes on any machine. Multi-threaded xz and zstd split the input
61 /// per worker, so their output depends on the worker count.
62 #[arg(long)]
63 threads: Option<usize>,
64 /// Use the full detected CPU budget, uncapped.
65 ///
66 /// Lifts the CPU cap only — `--memory-limit` still binds. Turbo means
67 /// "use my cores", not "ignore the OOM killer".
68 #[arg(long)]
69 turbo: bool,
70 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
71 ///
72 /// On encode this bounds the worker count. Defaults to 25% of
73 /// available RAM, honouring cgroup limits.
74 #[arg(long, value_name = "SIZE")]
75 memory_limit: Option<String>,
76 /// Fail (exit 4) if anything was approximated or lost.
77 ///
78 /// On the pack side that means something the walk met and could not
79 /// store — a socket, an undecodable name, an unreadable directory —
80 /// or an entry shape this container has none of, such as a directory
81 /// or a symlink in an `ar`.
82 #[arg(long)]
83 strict_fidelity: bool,
84 },
85 /// Decompress a file, or extract an archive's entries.
86 Unpack {
87 /// Input path, or `-` for stdin.
88 input: String,
89 /// Entries to extract; every entry when none are given.
90 ///
91 /// An exact entry name, or a directory selecting everything beneath
92 /// it. Needs -C, which is what asks for entry-aware extraction.
93 #[arg(value_name = "PATTERNS")]
94 patterns: Vec<String>,
95 /// Extract the entry at this position instead of naming it. Repeatable.
96 ///
97 /// 0-BASED, counting in archive order — the same number `stuffr list`
98 /// prints in its first column. This is how you reach an entry whose
99 /// name you cannot type: two entries sharing a name in a zip's index,
100 /// or `Makefile` and `makefile` from a case-sensitive filesystem on a
101 /// case-insensitive one. Needs -C, like PATTERNS, and cannot be
102 /// combined with them — they are two ways of saying the same thing.
103 #[arg(long, value_name = "N")]
104 index: Vec<usize>,
105 /// Extract the archive's entries into this directory.
106 ///
107 /// Absolute entry paths, `..` traversal and symlinks escaping this
108 /// directory are refused (exit 7), never silently sanitised.
109 #[arg(short = 'C', long, value_name = "DIR")]
110 directory: Option<String>,
111 /// Fail (exit 4) if anything was approximated or lost.
112 ///
113 /// On extraction that means an entry whose mode or mtime could not be
114 /// restored, or one skipped for having no shape on disk.
115 #[arg(long)]
116 strict_fidelity: bool,
117 /// Output path. Defaults to INPUT with its extension removed.
118 #[arg(short, long)]
119 output: Option<String>,
120 /// Overwrite an existing output.
121 #[arg(long)]
122 force: bool,
123 /// Use this format instead of detecting one. Required for formats with
124 /// no magic bytes and no extension.
125 #[arg(long)]
126 format: Option<String>,
127 /// Refuse a decode expanding by more than this ratio.
128 #[arg(long)]
129 max_ratio: Option<u64>,
130 /// Skip the fsync that makes the output durable before it is published.
131 #[arg(long)]
132 no_sync: bool,
133 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
134 ///
135 /// Defaults to 25% of available RAM, honouring cgroup limits.
136 #[arg(long, value_name = "SIZE")]
137 memory_limit: Option<String>,
138 },
139 /// Decompress a file, or stream an archive's entries, to stdout.
140 Cat {
141 /// Input path, or `-` for stdin.
142 input: String,
143 /// Entries whose bytes to stream, in archive order.
144 ///
145 /// An exact entry name, or a directory selecting everything beneath
146 /// it. Naming one is what asks for entry-aware streaming; without
147 /// any, `cat` decodes the input as a single stream.
148 #[arg(value_name = "PATTERNS")]
149 patterns: Vec<String>,
150 /// Stream the entry at this position instead of naming it. Repeatable.
151 ///
152 /// 0-BASED, counting in archive order — the same number `stuffr list`
153 /// prints in its first column. `stuffr cat a.zip --index 6 > out.sh`
154 /// is how you pull out an entry whose name is ambiguous or
155 /// untypeable. Like PATTERNS, naming one asks for entry-aware
156 /// streaming; the two cannot be combined.
157 #[arg(long, value_name = "N")]
158 index: Vec<usize>,
159 /// Use this format instead of detecting one. Required for formats with
160 /// no magic bytes and no extension.
161 #[arg(long)]
162 format: Option<String>,
163 /// Refuse a decode expanding by more than this ratio.
164 #[arg(long)]
165 max_ratio: Option<u64>,
166 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
167 ///
168 /// Defaults to 25% of available RAM, honouring cgroup limits.
169 #[arg(long, value_name = "SIZE")]
170 memory_limit: Option<String>,
171 },
172 /// Identify a stream without decoding it.
173 Info {
174 /// Input path, or `-` for stdin.
175 input: String,
176 /// Emit machine-readable JSON instead of the human-readable report.
177 #[arg(long)]
178 json: bool,
179 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
180 ///
181 /// Defaults to 25% of available RAM, honouring cgroup limits.
182 #[arg(long, value_name = "SIZE")]
183 memory_limit: Option<String>,
184 },
185 /// List the formats this build contains.
186 Formats,
187 /// List an archive's entries without extracting.
188 ///
189 /// The first column is the entry's 0-based position in archive order,
190 /// which is what `cat --index` and `unpack --index` select by.
191 #[command(alias = "ls")]
192 List {
193 /// Archive path, or `-` for stdin.
194 input: String,
195 /// Emit machine-readable JSON instead of one entry per line.
196 #[arg(long)]
197 json: bool,
198 /// Refuse a codec layer beneath the container expanding by more
199 /// than this ratio (e.g. a `.tar.gz` bomb).
200 #[arg(long)]
201 max_ratio: Option<u64>,
202 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
203 ///
204 /// Bounds the codec layer beneath the container, which `--max-ratio`
205 /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
206 /// a value the file declares in its own header, before producing any
207 /// output. Defaults to 25% of available RAM, honouring cgroup limits.
208 #[arg(long, value_name = "SIZE")]
209 memory_limit: Option<String>,
210 /// Fail (exit 4) if the read approximated or lost anything.
211 ///
212 /// A listing can be incomplete — a zip whose index shadows records
213 /// enumerates fewer entries than the archive declares — so `list`
214 /// gates on fidelity for the same reason `test` and `unpack -C` do.
215 #[arg(long)]
216 strict_fidelity: bool,
217 },
218 /// Verify every entry's integrity without extracting.
219 Test {
220 /// Archive path, or `-` for stdin.
221 input: String,
222 /// Refuse a codec layer beneath the container expanding by more
223 /// than this ratio (e.g. a `.tar.gz` bomb).
224 #[arg(long)]
225 max_ratio: Option<u64>,
226 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
227 ///
228 /// Bounds the codec layer beneath the container, which `--max-ratio`
229 /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
230 /// a value the file declares in its own header, before producing any
231 /// output. Defaults to 25% of available RAM, honouring cgroup limits.
232 #[arg(long, value_name = "SIZE")]
233 memory_limit: Option<String>,
234 /// Fail (exit 4) if the read approximated or lost anything.
235 #[arg(long)]
236 strict_fidelity: bool,
237 },
238}