diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 62b415e1e..2bae23bb8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT" repository = "https://github.com/zip-rs/zip2.git" keywords = ["zip", "archive", "compression", "cli"] categories = ["command-line-utilities", "compression", "filesystem", "development-tools::build-utils"] -# Keep this up to date with clap! rust-version = "1.74.0" description = """ Binary for creation and manipulation of zip files. @@ -25,8 +24,7 @@ members = ["."] name = "zip-cli" [dependencies] -clap = { version = "4.5.15", features = ["derive"] } -eyre = "0.6" +color-eyre = "0.6" [dependencies.zip] path = ".." @@ -46,6 +44,7 @@ lzma = ["zip/lzma"] time = ["zip/time"] xz = ["zip/xz"] zstd = ["zip/zstd"] + default = [ "aes-crypto", "bzip2", @@ -58,7 +57,6 @@ default = [ ] -# Reduce the size of the zip-cli binary. [profile.release] strip = true lto = true diff --git a/cli/clite/Cargo.toml b/cli/clite/Cargo.toml index 25a019119..e4cfd056f 100644 --- a/cli/clite/Cargo.toml +++ b/cli/clite/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT" repository = "https://github.com/zip-rs/zip2.git" keywords = ["zip", "archive", "compression", "cli"] categories = ["command-line-utilities", "compression", "filesystem", "development-tools::build-utils"] -# Keep this up to date with clap! rust-version = "1.74.0" description = """ Binary for creation and manipulation of zip files. @@ -23,7 +22,6 @@ members = ["."] name = "zip-clite" [dependencies] -clap = { version = "4.5.15", features = ["derive"] } eyre = "0.6" [dependencies.zip-cli] @@ -31,7 +29,6 @@ path = ".." default-features = false features = ["deflate-flate2", "deflate-zlib"] -# Reduce the size of the zip-cli binary. [profile.release] strip = true lto = true diff --git a/cli/clite/src/main.rs b/cli/clite/src/main.rs index af2067fea..6a066aec2 100644 --- a/cli/clite/src/main.rs +++ b/cli/clite/src/main.rs @@ -1,6 +1,6 @@ +use std::env; use std::io; -use clap::{error::ErrorKind, Parser}; use eyre::Report; use zip_cli::args::*; @@ -8,13 +8,7 @@ use zip_cli::compress::execute_compress; use zip_cli::ErrHandle; fn main() -> Result<(), Report> { - let ZipCli { verbose, command } = match ZipCli::try_parse() { - Ok(args) => args, - Err(e) => match e.kind() { - ErrorKind::Format | ErrorKind::Io | ErrorKind::InvalidUtf8 => return Err(e.into()), - _ => e.exit(), - }, - }; + let ZipCli { verbose, command } = ZipCli::parse_argv(env::args_os())?; let mut err = if verbose { ErrHandle::Output(io::stderr()) } else { @@ -22,7 +16,9 @@ fn main() -> Result<(), Report> { }; match command { - ZipCommand::Info | ZipCommand::Extract => Ok(()), - ZipCommand::Compress(compress) => execute_compress(&mut err, compress), + ZipCommand::Info => eyre::bail!("info command not implemented"), + ZipCommand::Extract => eyre::bail!("extract command not implemented"), + ZipCommand::Compress(compress) => execute_compress(&mut err, compress)?, } + Ok(()) } diff --git a/cli/src/args.rs b/cli/src/args.rs index 7fdcb1d01..9888ff591 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -1,46 +1,171 @@ -use std::{collections::VecDeque, ffi::OsString, num::ParseIntError, path::PathBuf}; +use color_eyre::eyre::Report; -use clap::{ - builder::ValueParser, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Args, Command, - FromArgMatches, Parser, Subcommand, ValueEnum, +use std::{ + collections::VecDeque, + ffi::OsString, + io::{self, Write}, + num::ParseIntError, + path::PathBuf, + process, + sync::OnceLock, }; -#[derive(Parser, Debug)] -#[command(version, about)] +#[derive(Debug)] pub struct ZipCli { - /// Write additional output to stderr. - #[arg(short, long)] pub verbose: bool, - #[command(subcommand)] pub command: ZipCommand, } -#[derive(Subcommand, Debug)] +#[derive(Debug)] +enum SubcommandName { + Compress, + Info, + Extract, +} + +static PARSED_EXE_NAME: OnceLock = OnceLock::new(); + +impl ZipCli { + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); + const DESCRIPTION: &'static str = env!("CARGO_PKG_DESCRIPTION"); + + pub const ARGV_PARSE_FAILED_EXIT_CODE: i32 = 2; + pub const NON_FAILURE_EXIT_CODE: i32 = 0; + + pub const INFO_DESCRIPTION: &'static str = "do an info"; + pub const EXTRACT_DESCRIPTION: &'static str = "do an extract"; + + pub fn binary_name() -> &'static str { + PARSED_EXE_NAME.get().expect("binary name was not set yet") + } + + fn generate_version_text() -> String { + format!("{} {}\n", Self::binary_name(), Self::VERSION) + } + + fn generate_usage_line() -> String { + format!("Usage: {} [OPTIONS] ", Self::binary_name()) + } + + fn generate_full_help_text() -> String { + format!( + "\ +{} + +{} + +Commands: + {} {} + info {} + extract {} + +Options: + -v, --verbose Write information logs to stderr + -h, --help Print help + -V, --version Print version + +Build this binary with '--features clap' for more thorough help text. +", + Self::DESCRIPTION, + Self::generate_usage_line(), + Compress::COMMAND_NAME, + Compress::COMMAND_DESCRIPTION, + Self::INFO_DESCRIPTION, + Self::EXTRACT_DESCRIPTION, + ) + } + + fn generate_brief_help_text(context: &str) -> String { + format!( + "\ +error: {context} + +{} + +For more information, try '--help'. +", + Self::generate_usage_line() + ) + } + + fn parse_up_to_subcommand_name( + argv: &mut VecDeque, + ) -> Result<(bool, SubcommandName), Report> { + let mut verbose: bool = false; + let mut subcommand_name: Option = None; + while subcommand_name.is_none() { + match argv.pop_front() { + None => { + let help_text = Self::generate_full_help_text(); + let _ = io::stderr().write_all(help_text.as_bytes()); + process::exit(Self::ARGV_PARSE_FAILED_EXIT_CODE); + } + Some(arg) => match arg.as_encoded_bytes() { + b"-v" | b"--verbose" => verbose = true, + b"-V" | b"--version" => { + let version_text = Self::generate_version_text(); + io::stdout().write_all(version_text.as_bytes())?; + process::exit(Self::NON_FAILURE_EXIT_CODE) + } + b"-h" | b"--help" => { + let help_text = Self::generate_full_help_text(); + io::stdout().write_all(help_text.as_bytes())?; + process::exit(Self::NON_FAILURE_EXIT_CODE); + } + b"compress" => subcommand_name = Some(SubcommandName::Compress), + b"info" => subcommand_name = Some(SubcommandName::Info), + b"extract" => subcommand_name = Some(SubcommandName::Extract), + arg_bytes => { + let context = if arg_bytes.starts_with(b"-") { + format!("unrecognized flag {arg:?}") + } else { + format!("unrecognized subcommand name {arg:?}") + }; + let help_text = Self::generate_brief_help_text(&context); + let _ = io::stderr().write_all(help_text.as_bytes()); + process::exit(Self::ARGV_PARSE_FAILED_EXIT_CODE) + } + }, + } + } + Ok((verbose, subcommand_name.unwrap())) + } + + pub fn parse_argv(argv: impl IntoIterator) -> Result { + let mut argv: VecDeque = argv.into_iter().collect(); + let exe_name: String = argv + .pop_front() + .expect("exe name not on command line") + .into_string() + .expect("exe name not valid unicode"); + PARSED_EXE_NAME + .set(exe_name) + .expect("exe name already written"); + let (verbose, subcommand_name) = Self::parse_up_to_subcommand_name(&mut argv)?; + let command = match subcommand_name { + SubcommandName::Info => ZipCommand::Info, + SubcommandName::Extract => ZipCommand::Extract, + SubcommandName::Compress => ZipCommand::Compress(Compress::parse_argv(argv)?), + }; + Ok(Self { verbose, command }) + } +} + +#[derive(Debug)] pub enum ZipCommand { - /// do a compress - /// - /// Attributes can be set once and subsequently apply to all later entry arguments - /// until overridden. Compress(Compress), - /// do an info Info, - /// do an extract Extract, } -#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum CompressionMethodArg { - /// uncompressed Stored, - /// with deflate (default) Deflate, /* requires having zip/_deflate-any set to compile */ - /// with deflate64 #[cfg(feature = "deflate64")] Deflate64, - /// with bzip2 #[cfg(feature = "bzip2")] Bzip2, - /// with zstd #[cfg(feature = "zstd")] Zstd, } @@ -79,234 +204,307 @@ pub struct Compress { pub positional_paths: Vec, } -impl FromArgMatches for Compress { - fn from_arg_matches(matches: &ArgMatches) -> Result { - let allow_stdout = matches.get_flag("stdout"); - let output_path = matches.get_one("output-file").cloned(); - assert!(!(allow_stdout && output_path.is_some())); - - /* (1) Extract each arg type and associated indices. This is extremely boilerplate. */ - let methods: Vec = matches - .get_many("compression-method") - .map(|vs| vs.copied().collect()) - .unwrap_or_default(); - let methods_indices: Vec = matches - .indices_of("compression-method") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut methods: VecDeque<(CompressionMethodArg, usize)> = - methods.into_iter().zip(methods_indices).collect(); - - let levels: Vec = matches - .get_many("compression-level") - .map(|vs| vs.copied().map(|i: i64| CompressionLevel(i)).collect()) - .unwrap_or_default(); - let levels_indices: Vec = matches - .indices_of("compression-level") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut levels: VecDeque<(CompressionLevel, usize)> = - levels.into_iter().zip(levels_indices).collect(); - - let modes: Vec = matches - .get_many("mode") - .map(|vs| vs.copied().collect()) - .unwrap_or_default(); - let modes_indices: Vec = matches - .indices_of("mode") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut modes: VecDeque<(Mode, usize)> = modes.into_iter().zip(modes_indices).collect(); - - let large_files: Vec = matches - .get_many("large-file") - .map(|vs| vs.copied().collect()) - .unwrap_or_default(); - let large_files_indices: Vec = matches - .indices_of("large-file") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut large_files: VecDeque<(bool, usize)> = - large_files.into_iter().zip(large_files_indices).collect(); - - let names: Vec = matches - .get_many("name") - .map(|vs| vs.cloned().collect()) - .unwrap_or_default(); - let names_indices: Vec = matches - .indices_of("name") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut names: VecDeque<(String, usize)> = names.into_iter().zip(names_indices).collect(); - - let mut directories_indices: VecDeque = matches - .indices_of("directory") - .map(|is| is.collect()) - .unwrap_or_default(); - - let mut symlinks_indices: VecDeque = matches - .indices_of("symlink") - .map(|is| is.collect()) - .unwrap_or_default(); - - let immediates: Vec = matches - .get_many("immediate") - .map(|vs| vs.cloned().collect()) - .unwrap_or_default(); - let immediates_indices: Vec = matches - .indices_of("immediate") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut immediates: VecDeque<(OsString, usize)> = - immediates.into_iter().zip(immediates_indices).collect(); - - let file_paths: Vec = matches - .get_many("file") - .map(|vs| vs.cloned().collect()) - .unwrap_or_default(); - let file_paths_indices: Vec = matches - .indices_of("file") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut file_paths: VecDeque<(PathBuf, usize)> = - file_paths.into_iter().zip(file_paths_indices).collect(); - - let recursive_dir_paths: Vec = matches - .get_many("recursive-directory") - .map(|vs| vs.cloned().collect()) - .unwrap_or_default(); - let recursive_dir_paths_indices: Vec = matches - .indices_of("recursive-directory") - .map(|is| is.collect()) - .unwrap_or_default(); - let mut recursive_dir_paths: VecDeque<(PathBuf, usize)> = recursive_dir_paths - .into_iter() - .zip(recursive_dir_paths_indices) - .collect(); - - /* (2) Map each arg back to its original order by popping the minimum index entry off its - * queue. */ +impl Compress { + #[cfg(feature = "deflate64")] + const DEFLATE64_HELP_LINE: &'static str = " - deflate64: with deflate64\n"; + #[cfg(not(feature = "deflate64"))] + const DEFLATE64_HELP_LINE: &'static str = ""; + + #[cfg(feature = "bzip2")] + const BZIP2_HELP_LINE: &'static str = " - bzip2: with bzip2\n"; + #[cfg(not(feature = "bzip2"))] + const BZIP2_HELP_LINE: &'static str = ""; + + #[cfg(feature = "zstd")] + const ZSTD_HELP_LINE: &'static str = " - zstd: with zstd\n"; + #[cfg(not(feature = "zstd"))] + const ZSTD_HELP_LINE: &'static str = ""; + + pub const COMMAND_NAME: &'static str = "compress"; + pub const COMMAND_DESCRIPTION: &'static str = "do a compress"; + + fn generate_usage_line() -> String { + format!( + "Usage: {} {} [-h|--help] [OUTPUT-FLAG] [ENTRIES]... [--] [PATH]...", + ZipCli::binary_name(), + Self::COMMAND_NAME, + ) + } + + fn generate_full_help_text() -> String { + format!( + "\ +{} + +{} + + -h, --help Print help + +Output flags: +Where and how to write the generated zip archive. + -o, --output-file + Output zip file path to write. + The output file is currently always truncated if it already exists. + If not provided, output is written to stdout. + + --stdout + Allow writing output to stdout even if stdout is a tty. + +ENTRIES: +After at most one output flag is provided, the rest of the command line is attributes and +entry data. Attributes modify later entries. + +Sticky attributes: +These flags apply to everything that comes after them until reset by another instance of the +same attribute. Sticky attributes continue to apply to positional arguments received after +processing all flags. + + -c, --compression-method + Which compression technique to use. + Defaults to deflate if not specified. + + Possible values: + - stored: uncompressed + - deflate: with deflate (default) +{}{}{} + + -l, --compression-level + How much compression to perform, from 0..=24. + The accepted range of values differs for each technique. + + -m, --mode + Unix permissions to apply to the file, in octal (like chmod). + + --large-file [true|false] + Whether to enable large file support. + This may take up more space for records, but allows files over 32 bits in length to be + written, up to 64 bit sizes. + +Non-sticky attributes: +These flags only apply to the next entry after them, and may not be repeated. + + -n, --name + The name to apply to the entry. + + -s, --symlink + Make the next entry into a symlink entry. + A symlink entry may be immediate with -i, or it may read the symlink value from the + filesystem with -f. + +Entry data: +Each of these flags creates an entry in the output zip archive. + + -d, --dir + Create a directory entry. + A name must be provided beforehand with -n. + + -i, --imm + Write an entry containing this data. + A name must be provided beforehand with -n. + + -f, --file + Write an entry with the contents of this file path. + A name may be provided beforehand with -n, otherwise the name will be inferred from + relativizing the given path to the working directory. + + -r, --recursive-dir + Write all the recursive contents of this directory path. + A name may be provided beforehand with -n, which will be used as the prefix for all + recursive contents of this directory. Otherwise, the name will be inferred from + relativizing the given path to the working directory. + +Positional entries: + [PATH]... + Write the file or recursive directory contents, relativizing the path. + If the given path points to a file, then a single file entry will be written. + If the given path is a symlink, then a single symlink entry will be written. + If the given path refers to a directory, then the recursive contents will be written. +", + Self::COMMAND_DESCRIPTION, + Self::generate_usage_line(), + Self::DEFLATE64_HELP_LINE, + Self::BZIP2_HELP_LINE, + Self::ZSTD_HELP_LINE, + ) + } + + fn generate_brief_help_text(context: &str) -> String { + format!( + "\ +error: {context} + +{} +", + Self::generate_usage_line() + ) + } + + pub fn exit_arg_invalid(context: &str) -> ! { + let message = Self::generate_brief_help_text(context); + let _ = io::stderr().write_all(message.as_bytes()); + process::exit(ZipCli::ARGV_PARSE_FAILED_EXIT_CODE) + } + + pub fn parse_argv(mut argv: VecDeque) -> Result { + let mut allow_stdout: bool = false; + let mut output_path: Option = None; let mut args: Vec = Vec::new(); - enum ArgType { - Method, - Level, - Mode, - LargeFile, - Name, - Dir, - Sym, - Imm, - FilePath, - RecDirPath, - } - let mut min_index: usize = usize::MAX; - let mut min_arg_type: Option = None; - - while !methods.is_empty() - || !levels.is_empty() - || !modes.is_empty() - || !large_files.is_empty() - || !names.is_empty() - || !directories_indices.is_empty() - || !symlinks_indices.is_empty() - || !immediates.is_empty() - || !file_paths.is_empty() - || !recursive_dir_paths.is_empty() - { - if let Some((_, i)) = methods.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Method); - } - } - if let Some((_, i)) = levels.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Level); - } - } - if let Some((_, i)) = modes.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Mode); - } - } - if let Some((_, i)) = large_files.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::LargeFile); - } - } - if let Some((_, i)) = names.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Name); - } - } - if let Some(i) = directories_indices.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Dir); - } - } - if let Some(i) = symlinks_indices.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Sym); - } - } - if let Some((_, i)) = immediates.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::Imm); - } - } - if let Some((_, i)) = file_paths.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::FilePath); - } - } - if let Some((_, i)) = recursive_dir_paths.front() { - if *i < min_index { - min_index = *i; - min_arg_type = Some(ArgType::RecDirPath); - } - } - assert_ne!(min_index, usize::MAX); + let mut positional_paths: Vec = Vec::new(); - let new_arg = match min_arg_type.take().unwrap() { - ArgType::Method => { - CompressionArg::CompressionMethod(methods.pop_front().unwrap().0) + while let Some(arg) = argv.pop_front() { + match arg.as_encoded_bytes() { + b"-h" | b"--help" => { + let help_text = Self::generate_full_help_text(); + io::stdout().write_all(help_text.as_bytes())?; + process::exit(ZipCli::NON_FAILURE_EXIT_CODE); } - ArgType::Level => CompressionArg::Level(levels.pop_front().unwrap().0), - ArgType::Mode => CompressionArg::Mode(modes.pop_front().unwrap().0), - ArgType::LargeFile => CompressionArg::LargeFile(large_files.pop_front().unwrap().0), - ArgType::Name => CompressionArg::Name(names.pop_front().unwrap().0), - ArgType::Dir => { - directories_indices.pop_front().unwrap(); - CompressionArg::Dir - } - ArgType::Sym => { - symlinks_indices.pop_front().unwrap(); - CompressionArg::Symlink + + /* Output flags */ + b"--stdout" => { + if output_path.is_some() { + Self::exit_arg_invalid("--stdout provided along with output file"); + } else if !args.is_empty() || !positional_paths.is_empty() { + Self::exit_arg_invalid("--stdout provided after entries"); + } else if allow_stdout { + Self::exit_arg_invalid("--stdout provided twice"); + } else { + allow_stdout = true; + } } - ArgType::Imm => CompressionArg::Immediate(immediates.pop_front().unwrap().0), - ArgType::FilePath => CompressionArg::FilePath(file_paths.pop_front().unwrap().0), - ArgType::RecDirPath => { - CompressionArg::RecursiveDirPath(recursive_dir_paths.pop_front().unwrap().0) + b"-o" | b"--output-file" => { + if output_path.is_some() { + Self::exit_arg_invalid("--output-file provided twice"); + } else if allow_stdout { + Self::exit_arg_invalid("--stdout provided along with output file"); + } else if !args.is_empty() || !positional_paths.is_empty() { + Self::exit_arg_invalid("-o/--output-file provided after entries"); + } else { + match argv.pop_front() { + Some(path) => { + output_path = Some(path.into()); + } + None => { + Self::exit_arg_invalid("no argument provided for -o/--output-file"); + } + } + } } - }; - args.push(new_arg); - min_index = usize::MAX; + /* Attributes */ + b"-c" | b"--compression-method" => match argv.pop_front() { + None => { + Self::exit_arg_invalid("no argument provided for -c/--compression-method") + } + Some(name) => match name.as_encoded_bytes() { + b"stored" => args.push(CompressionArg::CompressionMethod( + CompressionMethodArg::Stored, + )), + b"deflate" => args.push(CompressionArg::CompressionMethod( + CompressionMethodArg::Deflate, + )), + #[cfg(feature = "deflate64")] + b"deflate64" => args.push(CompressionArg::CompressionMethod( + CompressionMethodArg::Deflate64, + )), + #[cfg(feature = "bzip2")] + b"bzip2" => args.push(CompressionArg::CompressionMethod( + CompressionMethodArg::Bzip2, + )), + #[cfg(feature = "zstd")] + b"zstd" => args.push(CompressionArg::CompressionMethod( + CompressionMethodArg::Zstd, + )), + _ => Self::exit_arg_invalid("unrecognized compression method {name:?}"), + }, + }, + b"-l" | b"--compression-level" => match argv.pop_front() { + None => { + Self::exit_arg_invalid("no argument provided for -l/--compression-level") + } + Some(level) => match level.into_string() { + Err(level) => Self::exit_arg_invalid(&format!( + "invalid unicode provided for compression level: {level:?}" + )), + Ok(level) => match i64::from_str_radix(&level, 10) { + Err(e) => Self::exit_arg_invalid(&format!( + "failed to parse integer for compression level: {e}" + )), + Ok(level) => { + if (0..=24).contains(&level) { + args.push(CompressionArg::Level(CompressionLevel(level))) + } else { + Self::exit_arg_invalid(&format!( + "compression level {level} was not between 0 and 24" + )) + } + } + }, + }, + }, + b"-m" | b"--mode" => match argv.pop_front() { + None => Self::exit_arg_invalid("no argument provided for -m/--mode"), + Some(mode) => match mode.into_string() { + Err(mode) => Self::exit_arg_invalid(&format!( + "invalid unicode provided for mode: {mode:?}" + )), + Ok(mode) => match Mode::parse(&mode) { + Err(e) => Self::exit_arg_invalid(&format!( + "failed to parse integer for mode: {e}" + )), + Ok(mode) => args.push(CompressionArg::Mode(mode)), + }, + }, + }, + b"--large-file" => match argv.pop_front() { + None => Self::exit_arg_invalid("no argument provided for --large-file"), + Some(large_file) => match large_file.as_encoded_bytes() { + b"true" => args.push(CompressionArg::LargeFile(true)), + b"false" => args.push(CompressionArg::LargeFile(false)), + _ => Self::exit_arg_invalid(&format!( + "unrecognized value for --large-file: {large_file:?}" + )), + }, + }, + + /* Data */ + b"-n" | b"--name" => match argv.pop_front() { + None => Self::exit_arg_invalid("no argument provided for -n/--name"), + Some(name) => match name.into_string() { + Err(name) => Self::exit_arg_invalid(&format!( + "invalid unicode provided for name: {name:?}" + )), + Ok(name) => args.push(CompressionArg::Name(name)), + }, + }, + b"-s" | b"--symlink" => args.push(CompressionArg::Symlink), + b"-d" | b"--dir" => args.push(CompressionArg::Dir), + b"-i" | b"--immediate" => match argv.pop_front() { + None => Self::exit_arg_invalid("no argument provided for -i/--immediate"), + Some(data) => args.push(CompressionArg::Immediate(data)), + }, + b"-f" | b"--file" => match argv.pop_front() { + None => Self::exit_arg_invalid("no argument provided for -f/--file"), + Some(file) => args.push(CompressionArg::FilePath(file.into())), + }, + b"-r" | b"--recursive-dir" => match argv.pop_front() { + None => Self::exit_arg_invalid("no argument provided for -r/--recursive-dir"), + Some(dir) => args.push(CompressionArg::RecursiveDirPath(dir.into())), + }, + + /* Transition to positional args */ + b"--" => break, + arg_bytes => { + if arg_bytes.starts_with(b"-") { + Self::exit_arg_invalid(&format!("unrecognized flag {arg:?}")) + } else { + argv.push_front(arg); + break; + } + } + } } - /* (3) Collect positional arguments. */ - let positional_paths: Vec = matches - .get_many("positional-path") - .map(|vs| vs.cloned().collect()) - .unwrap_or_default(); + positional_paths.extend(argv.into_iter().map(|arg| arg.into())); Ok(Self { allow_stdout, @@ -315,164 +513,4 @@ impl FromArgMatches for Compress { positional_paths, }) } - fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> { - let Self { - allow_stdout, - output_path, - args, - positional_paths, - } = Self::from_arg_matches(matches)?; - self.allow_stdout |= allow_stdout; - if let Some(output_path) = output_path { - self.output_path.get_or_insert(output_path); - } - self.args.extend(args); - self.positional_paths.extend(positional_paths); - Ok(()) - } -} - -fn positional_only_arg(arg: Arg) -> Arg { - arg.action(ArgAction::Append) - .num_args(0) - .value_parser(value_parser!(bool)) - .default_value("false") - .default_missing_value("true") -} - -impl Args for Compress { - fn augment_args(cmd: Command) -> Command { - cmd.group(ArgGroup::new("output").multiple(false)) - .args( - [ - Arg::new("output-file") - .short('o') - .long("output-file") - .action(ArgAction::Set) - .value_parser(value_parser!(PathBuf)) - .help("Output zip file path to write.") - .long_help("Output zip file path to write. -The output file is currently always truncated if it already exists. -If not provided, output is written to stdout."), - Arg::new("stdout") - .long("stdout") - .action(ArgAction::SetTrue) - .help("Allow writing output to stdout even if stdout is a tty."), - ] - .into_iter() - .map(|arg| arg.group("output")) - .collect::>(), - ) - .group(ArgGroup::new("attributes").multiple(true)) - .next_help_heading("ATTRIBUTES") - .args( - [ - Arg::new("compression-method") - .short('c') - .long("compression-method") - .action(ArgAction::Append) - .value_parser(value_parser!(CompressionMethodArg)) - .help("Which compression technique to use. -Defaults to deflate if not specified."), - Arg::new("compression-level") - .short('l') - .long("compression-level") - .action(ArgAction::Append) - .value_parser(value_parser!(i64).range(0..=24)) - .help("How much compression to perform, from 0..=24.") - .long_help( - "How much compression to perform, from 0..=24. -The accepted range of values differs for each technique.", - ), - Arg::new("mode") - .short('m') - .long("mode") - .action(ArgAction::Append) - .value_parser(ValueParser::new(Mode::parse)) - .help("Unix permissions to apply to the file, in octal (like chmod)."), - Arg::new("large-file") - .long("large-file") - .action(ArgAction::Append) - .value_parser(value_parser!(bool)) - .help("Whether to enable large file support.") - .long_help("Whether to enable large file support. -This may take up more space for records, but allows files over 32 bits in length to be written, up -to 64 bit sizes.") - ] - .into_iter() - .map(|arg| arg.group("attributes")) - .collect::>(), - ) - .group(ArgGroup::new("entries").multiple(true)) - .next_help_heading("ENTRIES") - .args( - [ - Arg::new("name") - .short('n') - .long("name") - .action(ArgAction::Append) - .value_parser(value_parser!(String)) - .help("The name to apply to the entry."), - positional_only_arg(Arg::new("directory") - .short('d') - .long("dir")) - .help("Create a directory entry.") - .long_help("Create a directory entry. -A name must be provided beforehand with -n."), - positional_only_arg(Arg::new("symlink") - .short('s') - .long("symlink")) - .help("Make the next entry into a symlink entry.") - .long_help("Make the next entry into a symlink entry. -A symlink entry may be immediate with -i, or read the symlink value from the filesystem with -f."), - Arg::new("immediate") - .short('i') - .long("imm") - .action(ArgAction::Append) - .value_parser(value_parser!(OsString)) - .help("Write an entry containing this data.") - .long_help("Write an entry containing this data. -A name must be provided beforehand with -n."), - Arg::new("file") - .short('f') - .long("file") - .action(ArgAction::Append) - .value_parser(value_parser!(PathBuf)) - .help("Write an entry with the contents of this file path.") - .long_help( - "Write an entry with the contents of this file path. -A name may be provided beforehand with -n, otherwise the name will be inferred from relativizing the -given path to the working directory.", - ), - Arg::new("recursive-directory") - .short('r') - .long("recursive-dir") - .action(ArgAction::Append) - .value_parser(value_parser!(PathBuf)) - .help("Write all the recursive contents of this directory path.") - .long_help( - "Write all the recursive contents of this directory path. -A name may be provided beforehand with -n, which will be used as the prefix for all recursive -contents of this directory. Otherwise, the name will be inferred from relativizing the given path to -the working directory.", - ), - Arg::new("positional-path") - .action(ArgAction::Append) - .value_parser(value_parser!(PathBuf)) - .help("Write the file or recursive directory contents, relativizing the path.") - .long_help( - "Write the file or recursive directory contents, relativizing the path. -If the given path points to a file, then a single file entry will be written. -If the given path is a symlink, then a single symlink entry will be written. -If the given path refers to a directory, then the recursive contents will be written.", - ), - ] - .into_iter() - .map(|arg| arg.group("entries")) - .collect::>(), - ) - } - fn augment_args_for_update(cmd: Command) -> Command { - Self::augment_args(cmd) - } } diff --git a/cli/src/compress.rs b/cli/src/compress.rs index a85ad2d8d..6be418afa 100644 --- a/cli/src/compress.rs +++ b/cli/src/compress.rs @@ -4,7 +4,7 @@ use std::{ path::Path, }; -use eyre::{eyre, Report}; +use color_eyre::eyre::{self, Report}; use zip::{ unstable::path_to_string, @@ -70,7 +70,7 @@ fn enter_recursive_dir_entries( let entry_basename: String = dir_entry .file_name() .into_string() - .map_err(|name| eyre!("failed to decode basename {name:?}"))?; + .map_err(|name| eyre::eyre!("failed to decode basename {name:?}"))?; components.push(&entry_basename); let full_path: String = components.join("/"); readdir_stack.push((readdir, top_component)); @@ -129,7 +129,8 @@ pub fn execute_compress( "writing to stdout and buffering compressed zip in memory" )?; if io::stdout().is_terminal() && !allow_stdout { - return Err(eyre!("stdout is a tty, but --stdout was not set")); + /* TODO: maybe figure out some way to ensure --stdout is still the correct flag */ + Compress::exit_arg_invalid("stdout is a tty, but --stdout was not set"); } OutputHandle::InMem(Cursor::new(Vec::new())) } @@ -174,7 +175,7 @@ pub fn execute_compress( CompressionArg::Name(name) => { writeln!(err, "setting name of next entry to {name:?}")?; if let Some(last_name) = last_name { - return Err(eyre!( + Compress::exit_arg_invalid(&format!( "got two names before an entry: {last_name} and {name}" )); } @@ -183,30 +184,32 @@ pub fn execute_compress( CompressionArg::Dir => { writeln!(err, "writing dir entry")?; if symlink_flag { - return Err(eyre!("symlink flag provided before dir entry with ")); + Compress::exit_arg_invalid("symlink flag provided before dir entry"); } - let dirname = last_name - .take() - .ok_or_else(|| eyre!("no name provided before dir entry with"))?; + let dirname = last_name.take().unwrap_or_else(|| { + Compress::exit_arg_invalid("no name provided before dir entry") + }); writer.add_directory(dirname, options)?; } CompressionArg::Symlink => { writeln!(err, "setting symlink flag for next entry")?; if symlink_flag { /* TODO: make this a warning? */ - return Err(eyre!("symlink flag provided twice before entry")); + Compress::exit_arg_invalid("symlink flag provided twice before entry"); } symlink_flag = true; } CompressionArg::Immediate(data) => { - let name = last_name - .take() - .ok_or_else(|| eyre!("no name provided for immediate data {data:?}"))?; + let name = last_name.take().unwrap_or_else(|| { + Compress::exit_arg_invalid("no name provided for immediate data {data:?}") + }); if symlink_flag { /* This is a symlink entry. */ - let target = data - .into_string() - .map_err(|target| eyre!("failed to decode symlink target {target:?}"))?; + let target = data.into_string().unwrap_or_else(|target| { + Compress::exit_arg_invalid(&format!( + "failed to decode immediate symlink target {target:?}" + )) + }); writeln!( err, "writing immediate symlink entry with name {name:?} and target {target:?}" @@ -248,7 +251,7 @@ pub fn execute_compress( } CompressionArg::RecursiveDirPath(r) => { if symlink_flag { - return Err(eyre!("symlink flag provided before recursive dir entry")); + Compress::exit_arg_invalid("symlink flag provided before recursive dir entry"); } writeln!( err, @@ -259,14 +262,12 @@ pub fn execute_compress( } } if symlink_flag { - return Err(eyre!( - "symlink flag remaining after all entry flags processed" - )); + Compress::exit_arg_invalid("symlink flag remaining after all entry flags processed"); } if let Some(last_name) = last_name { - return Err(eyre!( + Compress::exit_arg_invalid(&format!( "name {last_name} remaining after all entry flags processed" - )); + )) } for pos_arg in positional_paths.into_iter() { let file_type = fs::symlink_metadata(&pos_arg)?.file_type(); diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 58c2517fb..a543cbdb5 100755 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -20,7 +20,7 @@ where fn write(&mut self, buf: &[u8]) -> io::Result { match self { Self::Output(w) => w.write(buf), - Self::NoOutput => Ok(0), + Self::NoOutput => Ok(buf.len()), } } diff --git a/cli/src/main.rs b/cli/src/main.rs index af2067fea..c4d83b619 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,20 +1,16 @@ +use std::env; use std::io; -use clap::{error::ErrorKind, Parser}; -use eyre::Report; +use color_eyre::eyre::{self, Report}; use zip_cli::args::*; use zip_cli::compress::execute_compress; use zip_cli::ErrHandle; fn main() -> Result<(), Report> { - let ZipCli { verbose, command } = match ZipCli::try_parse() { - Ok(args) => args, - Err(e) => match e.kind() { - ErrorKind::Format | ErrorKind::Io | ErrorKind::InvalidUtf8 => return Err(e.into()), - _ => e.exit(), - }, - }; + color_eyre::install()?; + + let ZipCli { verbose, command } = ZipCli::parse_argv(env::args_os())?; let mut err = if verbose { ErrHandle::Output(io::stderr()) } else { @@ -22,7 +18,9 @@ fn main() -> Result<(), Report> { }; match command { - ZipCommand::Info | ZipCommand::Extract => Ok(()), - ZipCommand::Compress(compress) => execute_compress(&mut err, compress), + ZipCommand::Info => eyre::bail!("info command not implemented"), + ZipCommand::Extract => eyre::bail!("extract command not implemented"), + ZipCommand::Compress(compress) => execute_compress(&mut err, compress)?, } + Ok(()) } diff --git a/src/write.rs b/src/write.rs index 48276cb9d..40d98937e 100644 --- a/src/write.rs +++ b/src/write.rs @@ -15,7 +15,7 @@ use crate::types::{ ZipRawValues, MIN_VERSION, }; use crate::write::ffi::S_IFLNK; -#[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))] +#[cfg(feature = "deflate-zopfli")] use core::num::NonZeroU64; use crc32fast::Hasher; use indexmap::IndexMap;