Skip to content

Commit d4041b6

Browse files
add new zip-cli crate in workspace
- implement absurd arg parsing - add help text - add long help text - first iteration of compression - add --stdout - add large file support - make the compress command mostly work - make compress flags work better - verbose output works! - reduce size of zip-cli from 2.9M->1.3M - make a new subcrate clite for different features/opt flags - remove clap - set --large-file automatically - clarify info and extract are a TODO for now - move OutputHandle to lib.rs - factor out main method into lib.rs - clarify the behavior of -f and -r around symlinks - add --append option - rename CompressError -> CommandError - make much more subcommand logic generic through traits - wrap compress help to 80 chars - begin extract help text squashed commits: - implement matching logic except for --match - FINALLY fix input zips iteration - implement a couple basic transforms - add terrible ContentTransformer - add entry spec transformer - give up and use an unsafecell - impl transform_name - initial extract impl - add name matchers - impl --match - modularize extract - begin impl transforms - refactor args modules - init entry data - stub out utterly absurd cli spec - parse our absurd cli spec - impl merged input - do absurd stuff without checking for compilation success - ok it compiles now - ok it might even run correctly now? - make output files work - default to regexp for replacement and disallow globs from replacement - add --{min,max}-size match exprs - impl pattern transformers - support --transform!! - anchoring, prefixes, the whole shebang. i think we're done here - make glob and rx optional dependencies - add stub for info command - parameterize the match help text to reuse for info - stub out info format specs - write out some more help text - remove unnecessary unsafe - parse archive overview format strings - make a trait for format parsing - parse entry format - finish help text for info - add info command stub - implement basic entry info - write directly to the output stream, don't allocate a string - add escaped tab component - rename some traits and methods - pass around a reference to EntryData instead - mess with mode bits to make them look more like ls - add some verbose logs - refactor info modules - refactor parseable directive - make compiled format strings much more generic (!) - refactor modules of compiled formatting - archive format works!!!!! omg - move entry and archive iteration into helper methods - make archive formatting work fully with ArchiveData - archive data works for stdin! - add more logging to extraction - remove allocations in perms todo sorting - several preliminary notes added to extract command - make process_entry() helper method - remove UnsafeCell!!! - don't reallocate the symlink target - move symlink processing to a helper - refactor a lot of extraction - ok extract makes a lot more sense now - support --archive-comment for compression - add a TODO - make symlink creation more readable - refactor output parsing into modules - make parsing outputs much MUCH more readable with a builder - all info directives are now supported
1 parent 810d18a commit d4041b6

File tree

24 files changed

+7576
-50
lines changed

24 files changed

+7576
-50
lines changed

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ authors = [
1010
license = "MIT"
1111
repository = "https://github.com/zip-rs/zip2.git"
1212
keywords = ["zip", "archive", "compression"]
13+
categories = ["compression", "filesystem", "parser-implementations"]
1314
rust-version = "1.73.0"
1415
description = """
1516
Library to support the reading and writing of zip files.
@@ -23,7 +24,9 @@ all-features = true
2324
rustdoc-args = ["--cfg", "docsrs"]
2425

2526
[workspace.dependencies]
26-
time = { version = "0.3.1", default-features = false }
27+
arbitrary = { version = "1.3.2", features = ["derive"] }
28+
time = { version = "0.3.36", default-features = false }
29+
zip = { path = ".", default-features = false }
2730

2831
[dependencies]
2932
aes = { version = "0.8", optional = true }
@@ -53,7 +56,7 @@ lzma-rs = { version = "0.3", default-features = false, optional = true }
5356
crossbeam-utils = "0.8.20"
5457

5558
[target.'cfg(fuzzing)'.dependencies]
56-
arbitrary = { version = "1.3.2", features = ["derive"] }
59+
arbitrary.workspace = true
5760

5861
[dev-dependencies]
5962
bencher = "0.1.5"

cli/Cargo.toml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
[package]
2+
name = "zip-cli"
3+
version = "0.0.1"
4+
authors = [
5+
"Danny McClanahan <[email protected]>",
6+
]
7+
license = "MIT"
8+
repository = "https://github.com/zip-rs/zip2.git"
9+
keywords = ["zip", "archive", "compression", "cli"]
10+
categories = ["command-line-utilities", "compression", "filesystem", "development-tools::build-utils"]
11+
rust-version = "1.74.0"
12+
description = """
13+
Binary for creation and manipulation of zip files.
14+
"""
15+
edition = "2021"
16+
17+
# Prevent this from interfering with workspaces
18+
[workspace]
19+
members = ["."]
20+
21+
[lib]
22+
23+
[[bin]]
24+
name = "zip-cli"
25+
26+
[dependencies]
27+
glob = { version = "0.3", optional = true }
28+
regex = { version = "1", optional = true }
29+
30+
[dependencies.zip]
31+
path = ".."
32+
default-features = false
33+
34+
[features]
35+
aes-crypto = ["zip/aes-crypto"]
36+
bzip2 = ["zip/bzip2"]
37+
chrono = ["zip/chrono"]
38+
deflate64 = ["zip/deflate64"]
39+
deflate = ["zip/deflate"]
40+
deflate-flate2 = ["zip/deflate-flate2"]
41+
deflate-zlib = ["zip/deflate-zlib"]
42+
deflate-zlib-ng = ["zip/deflate-zlib-ng"]
43+
deflate-zopfli = ["zip/deflate-zopfli"]
44+
lzma = ["zip/lzma"]
45+
time = ["zip/time"]
46+
xz = ["zip/xz"]
47+
zstd = ["zip/zstd"]
48+
49+
glob = ["dep:glob"]
50+
rx = ["dep:regex"]
51+
52+
default = [
53+
"aes-crypto",
54+
"bzip2",
55+
"deflate64",
56+
"deflate",
57+
"lzma",
58+
"time",
59+
"xz",
60+
"zstd",
61+
"glob",
62+
"rx",
63+
]
64+
65+
66+
[profile.release]
67+
strip = true
68+
lto = true
69+
opt-level = 3
70+
codegen-units = 1

cli/clite/Cargo.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "zip-clite"
3+
version = "0.0.1"
4+
authors = [
5+
"Danny McClanahan <[email protected]>",
6+
]
7+
license = "MIT"
8+
repository = "https://github.com/zip-rs/zip2.git"
9+
keywords = ["zip", "archive", "compression", "cli"]
10+
categories = ["command-line-utilities", "compression", "filesystem", "development-tools::build-utils"]
11+
rust-version = "1.74.0"
12+
description = """
13+
Binary for creation and manipulation of zip files.
14+
"""
15+
edition = "2021"
16+
17+
# Prevent this from interfering with workspaces
18+
[workspace]
19+
members = ["."]
20+
21+
[[bin]]
22+
name = "zip-clite"
23+
24+
[dependencies]
25+
26+
[dependencies.zip-cli]
27+
path = ".."
28+
default-features = false
29+
features = ["deflate-flate2", "deflate-zlib"]
30+
31+
[profile.release]
32+
strip = true
33+
lto = true
34+
opt-level = "s"
35+
codegen-units = 1

cli/clite/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
zip_cli::driver::main();
3+
}

cli/src/args.rs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
use std::{collections::VecDeque, ffi::OsString, fmt, sync::OnceLock};
2+
3+
#[derive(Debug)]
4+
pub enum ArgParseError {
5+
StdoutMessage(String),
6+
StderrMessage(String),
7+
}
8+
9+
#[derive(Debug)]
10+
pub struct ZipCli {
11+
pub verbose: bool,
12+
pub command: ZipCommand,
13+
}
14+
15+
#[derive(Debug)]
16+
enum SubcommandName {
17+
Compress,
18+
Info,
19+
Extract,
20+
}
21+
22+
static PARSED_EXE_NAME: OnceLock<String> = OnceLock::new();
23+
24+
impl ZipCli {
25+
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
26+
const DESCRIPTION: &'static str = env!("CARGO_PKG_DESCRIPTION");
27+
28+
pub const INTERNAL_ERROR_EXIT_CODE: i32 = 3;
29+
pub const ARGV_PARSE_FAILED_EXIT_CODE: i32 = 2;
30+
pub const NON_FAILURE_EXIT_CODE: i32 = 0;
31+
32+
pub fn binary_name() -> &'static str {
33+
PARSED_EXE_NAME.get().expect("binary name was not set yet")
34+
}
35+
36+
fn generate_version_text() -> String {
37+
format!("{} {}\n", Self::binary_name(), Self::VERSION)
38+
}
39+
40+
fn generate_usage_line() -> String {
41+
format!("Usage: {} [OPTIONS] <COMMAND>", Self::binary_name())
42+
}
43+
44+
fn generate_full_help_text() -> String {
45+
format!(
46+
"\
47+
{}
48+
49+
{}
50+
51+
Commands:
52+
{}{}{}
53+
{}{}{}
54+
{}{}{}
55+
56+
Options:
57+
-v, --verbose Write information logs to stderr
58+
-h, --help Print help
59+
-V, --version Print version
60+
",
61+
Self::DESCRIPTION,
62+
Self::generate_usage_line(),
63+
compress::Compress::COMMAND_NAME,
64+
compress::Compress::COMMAND_TABS,
65+
compress::Compress::COMMAND_DESCRIPTION,
66+
info::Info::COMMAND_NAME,
67+
info::Info::COMMAND_TABS,
68+
info::Info::COMMAND_DESCRIPTION,
69+
extract::Extract::COMMAND_NAME,
70+
extract::Extract::COMMAND_TABS,
71+
extract::Extract::COMMAND_DESCRIPTION,
72+
)
73+
}
74+
75+
fn generate_brief_help_text(context: &str) -> String {
76+
format!(
77+
"\
78+
error: {context}
79+
80+
{}
81+
82+
For more information, try '--help'.
83+
",
84+
Self::generate_usage_line()
85+
)
86+
}
87+
88+
fn parse_up_to_subcommand_name(
89+
argv: &mut VecDeque<OsString>,
90+
) -> Result<(bool, SubcommandName), ArgParseError> {
91+
let mut verbose: bool = false;
92+
let mut subcommand_name: Option<SubcommandName> = None;
93+
while subcommand_name.is_none() {
94+
match argv.pop_front() {
95+
None => {
96+
let help_text = Self::generate_full_help_text();
97+
return Err(ArgParseError::StderrMessage(help_text));
98+
}
99+
Some(arg) => match arg.as_encoded_bytes() {
100+
b"-v" | b"--verbose" => verbose = true,
101+
b"-V" | b"--version" => {
102+
let version_text = Self::generate_version_text();
103+
return Err(ArgParseError::StdoutMessage(version_text));
104+
}
105+
b"-h" | b"--help" => {
106+
let help_text = Self::generate_full_help_text();
107+
return Err(ArgParseError::StdoutMessage(help_text));
108+
}
109+
b"compress" => subcommand_name = Some(SubcommandName::Compress),
110+
b"info" => subcommand_name = Some(SubcommandName::Info),
111+
b"extract" => subcommand_name = Some(SubcommandName::Extract),
112+
arg_bytes => {
113+
let context = if arg_bytes.starts_with(b"-") {
114+
format!("unrecognized global flag {arg:?}")
115+
} else {
116+
format!("unrecognized subcommand name {arg:?}")
117+
};
118+
let help_text = Self::generate_brief_help_text(&context);
119+
return Err(ArgParseError::StderrMessage(help_text));
120+
}
121+
},
122+
}
123+
}
124+
Ok((verbose, subcommand_name.unwrap()))
125+
}
126+
127+
pub fn parse_argv(argv: impl IntoIterator<Item = OsString>) -> Result<Self, ArgParseError> {
128+
let mut argv: VecDeque<OsString> = argv.into_iter().collect();
129+
let exe_name: String = argv
130+
.pop_front()
131+
.expect("exe name not on command line")
132+
.into_string()
133+
.expect("exe name not valid unicode");
134+
PARSED_EXE_NAME
135+
.set(exe_name)
136+
.expect("exe name already written");
137+
let (verbose, subcommand_name) = Self::parse_up_to_subcommand_name(&mut argv)?;
138+
let command = match subcommand_name {
139+
SubcommandName::Info => ZipCommand::Info(info::Info::parse_argv(argv)?),
140+
SubcommandName::Extract => ZipCommand::Extract(extract::Extract::parse_argv(argv)?),
141+
SubcommandName::Compress => ZipCommand::Compress(compress::Compress::parse_argv(argv)?),
142+
};
143+
Ok(Self { verbose, command })
144+
}
145+
}
146+
147+
#[derive(Debug)]
148+
pub enum ZipCommand {
149+
Compress(compress::Compress),
150+
Info(info::Info),
151+
Extract(extract::Extract),
152+
}
153+
154+
pub trait CommandFormat: fmt::Debug {
155+
const COMMAND_NAME: &'static str;
156+
const COMMAND_TABS: &'static str;
157+
const COMMAND_DESCRIPTION: &'static str;
158+
159+
const USAGE_LINE: &'static str;
160+
161+
fn generate_usage_line() -> String {
162+
format!(
163+
"Usage: {} {} {}",
164+
ZipCli::binary_name(),
165+
Self::COMMAND_NAME,
166+
Self::USAGE_LINE,
167+
)
168+
}
169+
170+
fn generate_help() -> String;
171+
172+
fn generate_full_help_text() -> String {
173+
format!(
174+
"\
175+
{}
176+
177+
{}
178+
{}",
179+
Self::COMMAND_DESCRIPTION,
180+
Self::generate_usage_line(),
181+
Self::generate_help(),
182+
)
183+
}
184+
185+
fn generate_brief_help_text(context: &str) -> String {
186+
format!(
187+
"\
188+
error: {context}
189+
190+
{}
191+
",
192+
Self::generate_usage_line()
193+
)
194+
}
195+
196+
fn exit_arg_invalid(context: &str) -> ArgParseError {
197+
let message = Self::generate_brief_help_text(context);
198+
ArgParseError::StderrMessage(message)
199+
}
200+
201+
fn parse_argv(argv: VecDeque<OsString>) -> Result<Self, ArgParseError>
202+
where
203+
Self: Sized;
204+
}
205+
206+
pub mod compress;
207+
pub mod extract;
208+
pub mod info;

0 commit comments

Comments
 (0)