From e96212efa6133e84f29a25cb76d976a5145185bc Mon Sep 17 00:00:00 2001 From: garikello3d Date: Fri, 29 Dec 2023 19:39:15 +0100 Subject: [PATCH] threw away the invented wheel and used clap for command line parsing --- Cargo.toml | 1 + src/arg_opts.rs | 408 ++++++------------------------------ src/bin/bigarchiver/main.rs | 45 ++-- tests/external_test.sh | 21 ++ 4 files changed, 110 insertions(+), 365 deletions(-) create mode 100755 tests/external_test.sh diff --git a/Cargo.toml b/Cargo.toml index 6dc4e0e..894813f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ codegen-units = 1 strip = true [dependencies] +clap = { version = "4.4.12", features = ["derive"] } libc = "0.2.151" liblzma = { version = "0.2.1", features = ["parallel", "static"] } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } diff --git a/src/arg_opts.rs b/src/arg_opts.rs index a43c85b..06dbaac 100644 --- a/src/arg_opts.rs +++ b/src/arg_opts.rs @@ -1,361 +1,85 @@ -use std::{ffi::OsString, collections::HashMap}; +use clap::{Parser, Subcommand}; -#[derive(Debug, PartialEq, Eq)] -pub enum ArgModeSpecificOpts { - Backup { - out_template: String, - no_check: bool, - auth: String, - auth_every: usize, - split_size: usize, - compress_level: u8 - }, - Restore { - config_path: String, - check_free_space: Option, - no_check: bool, - }, - Check { - config_path: String - } -} - -#[derive(Debug, PartialEq, Eq)] +#[derive(Parser)] +#[command(name = "bigarchiver")] +#[command(author = "Igor Bezzubchenko")] +#[command(version = "0.0.1")] +#[command(about = "Reliably backup/restore data with compression and encryption", long_about = None)] pub struct ArgOpts { - pub pass: String, - pub buf_size: usize, - pub mode_specific_opts: ArgModeSpecificOpts -} - -#[derive(Debug, PartialEq, Eq, Hash)] -enum Mode { - Backup, Restore, Check + #[command(subcommand)] + pub command: Commands } -#[derive(Debug, PartialEq, Eq)] -enum Kind { - Single, Valued -} - -#[derive(Debug, PartialEq, Eq)] -struct OptProp { - must: bool, - modes: HashMap, - kind: Kind, - val: Option, - sample_param: Option<&'static str> -} - -impl ArgOpts { - pub fn from_os_args(args: &Vec) -> Result { - let mut cfg: HashMap<&str, OptProp> = HashMap::from_iter( - [ - ("backup", false, vec![(Mode::Backup, "select Backup mode: read data from stdin and write into output files(s)")], Kind::Single, None), - ("restore", false, vec![(Mode::Restore, "select Restore mode: restore data from file(s) and write into stdout")], Kind::Single, None), - ("check", false, vec![(Mode::Check, "select Check mode: check integrity of data from file(s)")], Kind::Single, None), - - ("pass", true, vec![ - (Mode::Backup, "password to encrypt data with"), - (Mode::Restore, "password to decrypt data with"), - (Mode::Check, "password to use to check data with") - ], Kind::Valued, Some("mysecret")), - - ("buf-size", true, vec![ - (Mode::Backup, "buffer size for reading stdin data, in MB"), - (Mode::Restore, "buffer size for reading disk files, in MB"), - (Mode::Check, "buffer size for reading disk files, in MB") - ], Kind::Valued, Some("256")), - - ("out-template", true, vec![(Mode::Backup, "template for output chunks; '%' symbols will transform into a sequence number")], Kind::Valued, Some("/path/to/files%%%%%%")), - - ("no-check", false, vec![ - (Mode::Backup, "do not check the integrity of the whole archive after backup is done (the default is to always check)"), - (Mode::Restore, "do not check the integrity of the whole archive before actual restore (the default is to always check)") - ], Kind::Single, None), - - ("auth", true, vec![(Mode::Backup, "public authentication data to embed")], Kind::Valued, Some("\"My Full Name\"")), - - ("auth-every", true, vec![(Mode::Backup, "apply authentication to every portion of data of indicated size, in MB")], Kind::Valued, Some("32")), - - ("split-size", true, vec![(Mode::Backup, "size of output chunks, in MB")], Kind::Valued, Some("1024")), - - ("compress-level", true, vec![(Mode::Backup, "XZ compression level, 0 - 9")], Kind::Valued, Some("6")), - - ("check-free-space", false, vec![(Mode::Restore, "check free space available on the indicated filesystem before restore")], Kind::Valued, Some("/data")), - - ("config", true, vec![ - (Mode::Restore, "full path to config file of the archive to restore"), - (Mode::Check, "full path to config file of the archive to check") - ], Kind::Valued, Some("/path/to/files000000.cfg")), - - ].into_iter().map(|(c, must, m, k, sample)|(c, OptProp{ - must, modes: HashMap::from_iter(m.into_iter()), kind: k, val: None, sample_param: sample } - )) - ); - - let mut usage = String::from("Usage:\n\n"); - for (title, mode, selector_option) in [ - ("1. to pack data coming from stdin into files", Mode::Backup, "backup"), - ("2. to unpack data from files to stdout", Mode::Restore, "restore"), - ("3. to verify the integrify of data from files", Mode::Check, "check")] - { - usage.push_str(title); - usage.push_str(":\n\n./bigarchiver --"); - - let mode_cfg = cfg.iter().filter_map(|(opt_name, opt_prop)| { - if let Some(descr) = opt_prop.modes.get(&mode) { - Some((opt_name, descr, opt_prop.must, opt_prop.sample_param)) - } else { - None - } - }).collect::< Vec<(&&str, &&str, bool, Option<&str>)> >(); - - usage.push_str(selector_option); - for (opt_name, _, must, opt_sample) in mode_cfg - .iter() - .filter(|(opt_name, _, _, _)| opt_name != &&selector_option) - { - if !*must { usage.push_str(" ["); } - usage.push_str(" --"); - usage.push_str(opt_name); - if let Some(opt_sample) = opt_sample { - usage.push_str(" "); - usage.push_str(opt_sample); - } - if !*must { usage.push_str(" ]"); } - } - usage.push_str("\n\nwhere:\n\n"); - - for (opt_name, descr, _, _) in mode_cfg { - usage.push_str(format!("\t--{}\n\t\t{}\n", opt_name, descr).as_str()); - } - usage.push_str("\n\n"); - } - - let mut args = args - .iter() - .cloned() - .map(|os| os.into_string()) - .collect::, OsString>>() - .map_err(|_| ("invalid encoding".to_string(), usage.clone()))? - .into_iter(); - - //println!("{cfg:#?}"); - - // track which options are given - while let Some(arg) = args.next() { - //println!("processing arg '{arg}'"); - if !arg.starts_with("--") { - return Err((format!("invalid argument '{}'", arg), usage.clone())); - } - let arg = &arg[2..]; - - if let Some(prop) = cfg.get_mut(arg) { - match prop.kind { - Kind::Single => prop.val = Some(String::new()), - Kind::Valued => prop.val = Some(args.next().ok_or((format!("missing parameter for option '--{}'", arg), usage.clone()))?) - } - } else { - return Err((format!("unknown argument '{}'", arg), usage.clone())); - } - } - - // detect mode - let mut mode: Option = None; - let mut mode_counter = 0; - if cfg.get("backup").is_some_and(|v| v.val.is_some()) { - mode = Some(Mode::Backup); - mode_counter += 1; - } - if cfg.get("restore").is_some_and(|v| v.val.is_some()) { - mode = Some(Mode::Restore); - mode_counter += 1; - } - if cfg.get("check").is_some_and(|v| v.val.is_some()) { - mode = Some(Mode::Check); - mode_counter += 1; - } - - if mode_counter > 1 { - return Err(("--backup, --restore and --check are mututally-exclusive".to_owned(), usage)); - } - if mode.is_none() { - return Err(("either --backup or --restore or --check must be provided".to_owned(), usage)); - } - let mode = mode.unwrap(); - - // must-have mode-specific options must be given depending on the mode - if !cfg.iter() - .filter(|(_,p)| p.must && p.modes.contains_key(&mode)) - .all(|(_,p)| p.val.is_some()) - { - return Err(("not all mandatory arguments are provided for chosen mode".to_owned(), usage)); - } - - // options for other mode(s) must no be present - if !cfg.iter() - .filter(|(_,p)| p.val.is_some()) - .all(|(_,p)| p.modes.contains_key(&mode) ) - { - return Err(("excessive options are provided for chosen mode".to_owned(), usage)); - } +#[derive(Subcommand)] +pub enum Commands { + /// Backup mode: read data from stdin and write into output files(s) + Backup { + /// Template for output chunks; '%' symbols will transform into a sequence number + #[arg(long, value_name = "path_with_%")] + out_template: String, - Ok(Self { - pass: cfg.get("pass").unwrap().val.clone().unwrap(), - buf_size: cfg.get("buf-size").unwrap().val.clone().unwrap().parse::() - .map_err(|_| ("invalid numeric value for '--buf-size'".to_owned(), usage.clone()))? * 1_048_576, - mode_specific_opts: match mode { - Mode::Backup => ArgModeSpecificOpts::Backup { - out_template: cfg.get("out-template").unwrap().val.clone().unwrap(), - no_check: cfg.get("no-check").unwrap().val.is_some(), - auth: cfg.get("auth").unwrap().val.clone().unwrap(), - auth_every: cfg.get("auth-every").unwrap().val.clone().unwrap().parse::() - .map_err(|_| ("invalid numeric value for '--auth-every'".to_owned(), usage.clone()))? * 1_048_576, - split_size: cfg.get("split-size").unwrap().val.clone().unwrap().parse::() - .map_err(|_| ("invalid numeric value for '--split-size'".to_owned(), usage.clone()))? * 1_048_576, - compress_level: cfg.get("compress-level").unwrap().val.clone().unwrap().parse::() - .map_err(|_| ("invalid numeric value for '--compress-level'".to_owned(), usage.clone()))?, - }, - Mode::Restore => ArgModeSpecificOpts::Restore { - config_path: cfg.get("config").unwrap().val.clone().unwrap(), - no_check: cfg.get("no-check").unwrap().val.is_some(), - check_free_space: cfg.get("check-free-space").unwrap().val.clone() - }, - Mode::Check => ArgModeSpecificOpts::Check { - config_path: cfg.get("config").unwrap().val.clone().unwrap(), - }, - } - }) + /// Password to encrypt data with + #[arg(long, value_name = "password")] + pass: String, - } -} + /// Public authentication data to embed + #[arg(long, value_name = "string")] + auth: String, -#[cfg(test)] -mod tests { - use super::*; - use std::ffi::OsString; + /// Embed authentication data to each portion of data of indicated size, in MB + #[arg(long, value_name = "size_mb")] + auth_every: usize, - fn to_os(vs: &Vec<&str>) -> Vec { - vs.into_iter().map(|s| OsString::from(s)).collect::>() - } + /// Size of output chunks, in MB + #[arg(long, value_name = "size_mb")] + split_size: usize, - #[test] - fn unknown_opt() { - ArgOpts::from_os_args(&to_os(&vec!["--dir", "--b"])).unwrap_err(); - } + /// LZMA compression level, 0 - 9 + #[arg(long, value_name = "level")] + compress_level: u8, - #[test] - fn missing_param() { - ArgOpts::from_os_args(&to_os(&vec!["--out-template", "outval"])).unwrap_err(); - } - - #[test] - fn missing_val() { - ArgOpts::from_os_args(&to_os(&vec!["--out-template", "outval", "--buf-size"])).unwrap_err(); - } + /// Buffer size for reading stdin data, in MB + #[arg(long, value_name ="size_mb")] + buf_size: usize, - #[test] - fn bad_num() { - ArgOpts::from_os_args(&to_os(&vec!["--out-template", "outval", "--buf-size", "x123", "--no-check"])).unwrap_err(); - } + /// Do not check the integrity of the whole archive after backup is done (the default is to always check) + #[arg(long, action)] + no_check: bool + }, + /// Restore mode: restore data from file(s) and write into stdout + Restore { + /// Full path to config file of the archive to restore + #[arg(long, value_name = "full_path")] + config: String, - #[test] - fn backup_opts() { - assert_eq!( - ArgOpts::from_os_args(&to_os(&vec![ - "--backup", "--out-template", "outval", "--pass", "passval", "--auth", "authval", - "--auth-every", "100", "--split-size", "1000", "--compress-level", "5", - "--buf-size", "10", "--no-check" - ])).unwrap(), - ArgOpts{ - pass: "passval".to_owned(), - buf_size: 10485760, - mode_specific_opts: ArgModeSpecificOpts::Backup { - out_template: "outval".to_owned(), - auth: "authval".to_owned(), - auth_every: 104857600, - split_size: 1048576000, - compress_level: 5, - no_check: true - } - }); - assert_eq!( - ArgOpts::from_os_args(&to_os(&vec![ - "--backup", "--out-template", "outval", "--pass", "passval", "--auth", "authval", - "--auth-every", "100", "--split-size", "1000", "--compress-level", "5", - "--buf-size", "10" - ])).unwrap(), - ArgOpts{ - pass: "passval".to_owned(), - buf_size: 10485760, - mode_specific_opts: ArgModeSpecificOpts::Backup { - out_template: "outval".to_owned(), - auth: "authval".to_owned(), - auth_every: 104857600, - split_size: 1048576000, - compress_level: 5, - no_check: false - } - }); - } + /// Password to decrypt data with + #[arg(long, value_name = "password")] + pass: String, - #[test] - fn restore_opts() { - assert_eq!( - ArgOpts::from_os_args(&to_os(&vec![ - "--restore", "--config", "configval", "--pass", "passval", "--buf-size", "10", "--check-free-space", "/mount" - ])).unwrap(), - ArgOpts{ - pass: "passval".to_owned(), - buf_size: 10485760, - mode_specific_opts: ArgModeSpecificOpts::Restore { - config_path: "configval".to_owned(), - no_check: false, - check_free_space: Some("/mount".to_owned()) - } - }); - assert_eq!( - ArgOpts::from_os_args(&to_os(&vec![ - "--restore", "--config", "configval", "--pass", "passval", "--buf-size", "10", "--no-check" - ])).unwrap(), - ArgOpts{ - pass: "passval".to_owned(), - buf_size: 10485760, - mode_specific_opts: ArgModeSpecificOpts::Restore { - config_path: "configval".to_owned(), - no_check: true, - check_free_space: None - } - }); - } + /// Buffer size for reading disk files, in MB + #[arg(long, value_name ="size_mb")] + buf_size: usize, + /// Check free space available on the indicated filesystem before restore + #[arg(long, value_name = "mountpoint_or_path")] + check_free_space: Option, - #[test] - fn check_opts() { - assert_eq!( - ArgOpts::from_os_args(&to_os(&vec![ - "--check", "--config", "configval", "--pass", "passval", "--buf-size", "10" - ])).unwrap(), - ArgOpts{ - pass: "passval".to_owned(), - buf_size: 10485760, - mode_specific_opts: ArgModeSpecificOpts::Check { - config_path: "configval".to_owned(), - } - }); + /// Do not check the integrity of the whole archive before actual restore (the default is to always check) + #[arg(long, action)] + no_check: bool + }, + /// Check mode: check integrity of data from file(s) + Check { + /// Full path to config file of the archive to restore + #[arg(long, value_name = "full_path")] + config: String, - let (e, u) = ArgOpts::from_os_args(&to_os(&vec![ - "--check", "--config", "configval", "--pass", "passval", "--buf-size", "123", "--split-size", "200" - ])).unwrap_err(); - println!("Error: {}\n\n=== usage start ===\n{}\n=== usage stop ====", e, u); + /// Password to decrypt data with + #[arg(long, value_name = "password")] + pass: String, - assert!( - ArgOpts::from_os_args(&to_os(&vec![ - "--check", "--config", "configval", "--pass", "passval" - ])).is_err()); - assert!( - ArgOpts::from_os_args(&to_os(&vec![ - "--check", "--pass", "passval", "--buf-size", "123" - ])).is_err()); + /// Buffer size for reading disk files, in MB + #[arg(long, value_name ="size_mb")] + buf_size: usize, } } diff --git a/src/bin/bigarchiver/main.rs b/src/bin/bigarchiver/main.rs index ddecbf2..aa3a5ac 100644 --- a/src/bin/bigarchiver/main.rs +++ b/src/bin/bigarchiver/main.rs @@ -1,7 +1,8 @@ -use bigarchiver::arg_opts::{ArgOpts, ArgModeSpecificOpts}; +use bigarchiver::arg_opts::{ArgOpts, Commands}; use bigarchiver::{backup, check}; use bigarchiver::file_set::cfg_from_pattern; use bigarchiver::finalizable::DataSink; +use clap::Parser; use std::io::{stdout, Write}; use std::process::ExitCode; @@ -19,52 +20,50 @@ impl DataSink for StdoutWriter { } fn process_args(args: &ArgOpts) -> Result<(), String> { - match &args.mode_specific_opts { - ArgModeSpecificOpts::Backup { - out_template, no_check, auth, auth_every, split_size, compress_level + match &args.command { + Commands::Backup { + out_template, pass, auth, auth_every, split_size, compress_level, buf_size, no_check } => { eprintln!("backing up..."); + let buf_size = *buf_size * 1_048_576; + let split_size = *split_size * 1_048_576; + let auth_every = *auth_every * 1_048_576; backup(&mut std::io::stdin(), - &auth, *auth_every, - *split_size, &out_template, - &args.pass, *compress_level, args.buf_size)?; + &auth, auth_every, + split_size, &out_template, + pass, *compress_level, buf_size)?; if !no_check { let cfg_path = cfg_from_pattern(&out_template); eprintln!("verifying..."); - check(None::, &cfg_path, &args.pass, args.buf_size, &None::<&str>) + check(None::, &cfg_path, pass, buf_size, &None::<&str>) } else { Ok(()) } }, - ArgModeSpecificOpts::Restore { config_path, no_check, check_free_space } => { + Commands::Restore { config, pass, buf_size, check_free_space, no_check } => { + let buf_size = *buf_size * 1_048_576; if !no_check { eprintln!("verifying before restore..."); - check(None::, &config_path, &args.pass, args.buf_size, &None) + check(None::, &config, pass, buf_size, &None) .map_err(|e| format!("will not restore data, integrity check error: {}", e))?; } eprintln!("restoring..."); let may_be_check = check_free_space.as_ref().map(|s| s.as_str()); - check(Some(StdoutWriter{}), &config_path, &args.pass, - args.buf_size, &may_be_check) + check(Some(StdoutWriter{}), &config, pass, + buf_size, &may_be_check) .map_err(|e| format!("error restoring data: {}", e)) }, - ArgModeSpecificOpts::Check { config_path } => { + Commands::Check { config, pass, buf_size } => { eprintln!("verifying..."); - check(None::, &config_path, &args.pass, - args.buf_size, &None) + let buf_size = *buf_size * 1_048_576; + check(None::, &config, pass, + buf_size, &None) } } } fn main() -> ExitCode { - let args = { - let args = ArgOpts::from_os_args(&std::env::args_os().skip(1).collect()); - if let Err((err_msg, usage)) = &args { - eprintln!("{}\n\n{}", err_msg, usage); - return ExitCode::from(2); - }; - args.unwrap() - }; + let args = ArgOpts::parse(); if let Err(e) = process_args(&args) { eprintln!("\nerror: {}\n", e); diff --git a/tests/external_test.sh b/tests/external_test.sh new file mode 100755 index 0000000..1fd5159 --- /dev/null +++ b/tests/external_test.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +RUN='cargo run --release --' +OUT_DIR=$(mktemp -d /tmp/bigarc.test.XXXXXX) + +dd if=/dev/urandom of=$OUT_DIR/src bs=4096 count=1024 + +cat $OUT_DIR/src | \ + $RUN backup --out-template $OUT_DIR/%%% --pass Pass \ + --buf-size 16 --auth Auth --auth-every 1 --split-size 1 --compress-level 6 --no-check + +$RUN check --config $OUT_DIR/000.cfg --pass Pass --buf-size 200 + +$RUN restore --config $OUT_DIR/000.cfg --pass Pass --buf-size 200 > $OUT_DIR/dst + +H1=$(sha1sum $OUT_DIR/src | cut -f1 -d' ') +H2=$(sha1sum $OUT_DIR/dst | cut -f1 -d' ') +rm -rf $OUT_DIR +[[ $H1 != $H2 ]] && ( echo Differs! ; exit 1 )