1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
pub mod bench;
pub mod cancel;
pub mod completions;
pub mod inspect;
pub mod resume;
pub mod status;
pub mod verify;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "unpackr",
author = "Unpackr Authors",
version = "0.1.0",
about = "Production-quality, low-disk-space archive extraction engine",
long_about = "Unpackr extracts large ZIP archives while minimizing peak disk space usage through incremental verified extraction and storage reclamation."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// Verbose logging output
#[arg(short, long, global = true)]
pub verbose: bool,
/// Suppress progress bar output
#[arg(short, long, global = true)]
pub quiet: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Launch the modern minimalist native desktop GUI (default when no subcommand is given)
#[command(visible_alias = "gui")]
Ui {
/// Optional path to a target archive (.zip) to load on startup
archive: Option<PathBuf>,
},
/// Inspect an archive and display detailed entry metadata and offsets without extracting
#[command(visible_alias = "i")]
Inspect {
/// Path to the target archive (.zip)
archive: PathBuf,
/// Output inspection details in JSON format
#[arg(long)]
json: bool,
/// Limit the number of entries displayed in the table (default: 50)
#[arg(long, default_value = "50")]
limit: usize,
},
/// Extract an archive with streaming verification and low disk space usage
#[command(visible_alias = "x")]
Extract {
/// Path to the archive
archive: PathBuf,
/// Destination directory
destination: PathBuf,
/// Collision policy if destination files exist: fail, skip, overwrite, rename (default: fail)
#[arg(long, default_value = "fail", value_enum)]
collision: crate::extraction::CollisionPolicy,
/// Disable sparse file hole detection
#[arg(long)]
no_sparse: bool,
/// Maximum allowed compression ratio before aborting (zip bomb protection, default: 100.0)
#[arg(long, default_value = "100.0")]
max_ratio: f64,
/// Reclaim archive storage in-place during extraction (experimental/destructive mode)
#[arg(long)]
reclaim_archive: bool,
/// Maximum allowed total uncompressed bytes across all entries (DoS defense)
#[arg(long)]
max_total_size: Option<u64>,
/// Maximum allowed uncompressed bytes for any single entry (DoS defense)
#[arg(long)]
max_file_size: Option<u64>,
/// Maximum allowed entry count in archive (DoS defense, default: 100,000)
#[arg(long)]
max_entries: Option<usize>,
/// Custom state directory for crash recovery journals (default: <destination>/.unpackr/)
#[arg(long)]
state_dir: Option<PathBuf>,
/// Output extraction summary in JSON format
#[arg(long)]
json: bool,
},
/// Resume an interrupted extraction job
#[command(visible_alias = "r")]
Resume {
/// Job ID, destination directory, archive path, or manifest path
target: String,
/// Optional destination directory (if first argument is an archive)
destination: Option<PathBuf>,
/// Explicit path to archive if moved or not found in manifest
#[arg(short, long)]
archive: Option<PathBuf>,
/// Retry entries that previously failed
#[arg(long)]
retry_failed: bool,
/// Fully verify already extracted files before resuming
#[arg(long)]
verify: bool,
/// Collision policy: fail, skip, overwrite, rename
#[arg(long, value_enum)]
collision: Option<crate::extraction::CollisionPolicy>,
/// Reclaim archive storage in-place during resume
#[arg(long)]
reclaim_archive: bool,
/// Maximum allowed total uncompressed bytes across all entries (DoS defense)
#[arg(long)]
max_total_size: Option<u64>,
/// Maximum allowed uncompressed bytes for any single entry (DoS defense)
#[arg(long)]
max_file_size: Option<u64>,
/// Maximum allowed entry count in archive (DoS defense)
#[arg(long)]
max_entries: Option<usize>,
/// Output resume summary in JSON format
#[arg(long)]
json: bool,
},
/// Show current status and disk savings for a job
#[command(visible_alias = "s")]
Status {
/// Job ID, destination directory, or manifest path to inspect
job_id: String,
/// Output status in JSON format
#[arg(long)]
json: bool,
},
/// Verify an extracted destination against archive metadata
#[command(visible_alias = "v")]
Verify {
/// Job ID, destination directory, or manifest path to verify
job_id: String,
/// Output verification results in JSON format
#[arg(long)]
json: bool,
},
/// Cancel an interrupted or incomplete extraction job
#[command(visible_alias = "c")]
Cancel {
/// Job ID, destination directory, or manifest path to cancel
job_id: String,
/// Clean up and remove extracted files and destination directory
#[arg(long)]
clean: bool,
/// Output cancellation result in JSON format
#[arg(long)]
json: bool,
},
/// Benchmark extraction performance and peak storage comparison on an archive
#[command(visible_alias = "b")]
Bench {
/// Optional path to an archive to benchmark (or omit to generate an automated test workload)
archive: Option<PathBuf>,
/// Number of entries if generating a synthetic benchmark workload (default: 10)
#[arg(long, default_value = "10")]
entries: usize,
/// Entry size in megabytes if generating a synthetic workload (default: 4)
#[arg(long, default_value = "4")]
size_mb: usize,
/// Output benchmark results in JSON format
#[arg(long)]
json: bool,
},
/// Generate shell auto-completion scripts (bash, zsh, fish, elvish, powershell)
Completions {
/// Shell to generate completions for
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}