diff --git a/Cargo.lock b/Cargo.lock index c599026..cec0180 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -142,6 +143,19 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -244,6 +258,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -518,6 +538,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -683,7 +713,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -701,14 +740,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -717,48 +773,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 02f13a1..6822f79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ rust-version = "1.75.0" [workspace.dependencies] anyhow = "1" chrono = "0.4" -clap = "4" +clap = { version = "4", features = ["derive", "color", "wrap_help"] } libc = "0.2" movavg = "2" rand = "0.9" diff --git a/disktest/src/args.rs b/disktest/src/args.rs index 7199100..28cdf82 100644 --- a/disktest/src/args.rs +++ b/disktest/src/args.rs @@ -12,8 +12,8 @@ use anyhow as ah; use clap::builder::ValueParser; use clap::error::ErrorKind::{DisplayHelp, DisplayVersion}; -use clap::{value_parser, Arg, ArgAction, Command}; -use disktest_lib::{gen_seed_string, parsebytes, DisktestQuiet, DtStreamType}; +use clap::{value_parser, Parser, ValueEnum}; +use disktest_lib::{gen_seed_string, parsebytes, Disktest, DisktestQuiet, DtStreamType}; use std::ffi::OsString; use std::path::PathBuf; @@ -25,103 +25,49 @@ Solid State Disk (SSD), Non-Volatile Memory Storage (NVMe), Hard Disk (HDD), USB This program can write a cryptographically secure pseudo random stream to a disk, read it back and verify it by comparing it to the expected stream. - -Example usage: "; #[cfg(not(target_os = "windows"))] const EXAMPLE: &str = "\ +Example usage: disktest --write --verify -j0 /dev/sdc"; #[cfg(target_os = "windows")] const EXAMPLE: &str = "\ +Example usage: disktest --write --verify -j0 \\\\.\\E:"; -const HELP_DEVICE: &str = "\ -Device node of the disk or file path to access. -"; - #[cfg(not(target_os = "windows"))] -const HELP_DEVICE_OS: &str = "\ +const HELP_DEVICE_LONG: &str = "\ +Device node of the disk or file path to access. This may be the /dev/sdX or /dev/mmcblkX or similar device node of the disk. It may also be an arbitrary path to a location in a filesystem."; #[cfg(target_os = "windows")] -const HELP_DEVICE_OS: &str = "\ +const HELP_DEVICE_LONG: &str = "\ +Device node of the disk or file path to access. This may be a path to the location on the disk to be tested (e.g. E:\\testfile) or a raw drive (e.g. \\\\.\\E: or \\\\.\\PhysicalDrive2)."; -const HELP_WRITE: &str = "\ -Write pseudo random data to the device. -If this option is not given, then disktest will operate in -verify-only mode instead, as if only --verify was given. -If both --write and --verify are specified, then the device -will first be written and then be verified with the same seed."; - -const HELP_VERIFY: &str = "\ -In verify-mode the disk will be read and compared to the expected pseudo -random sequence. -If both --write and --verify are specified, then the device -will first be written and then be verified with the same seed."; - -const HELP_SEEK: &str = "\ -Seek to the specified byte position on disk -before starting the write/verify operation. This skips the specified -amount of bytes on the disk and also fast forwards the random number generator. -"; - -const HELP_BYTES: &str = "\ -Number of bytes to write/verify. -If not given, then the whole disk will be overwritten/verified. -"; - -const HELP_ALGORITHM: &str = "\ -Select the random number generator algorithm. -ChaCha12 and ChaCha8 are less cryptographically secure than ChaCha20, but -faster. CRC is even faster, but not cryptographically secure at all. -"; - -const HELP_SEED: &str = "\ -The seed to use for random number stream generation. -The seed may be any random string (e.g. a long passphrase). -If no seed is given, then a secure random seed will be generated -and also printed to the console."; - -const HELP_INVERT_PATTERN: &str = "\ -Invert the bit pattern generated by the random number generator. -This can be useful, if a second write/verify run with a strictly -inverted test bit pattern is desired."; - -const HELP_THREADS: &str = "\ -The number of CPUs to use. -The special value 0 will select the maximum number of online CPUs in the -system. If the number of threads is equal to number of CPUs it is optimal -for performance. The number of threads must be equal during corresponding -verify and write mode runs. Otherwise the verification will fail. -"; - -const HELP_ROUNDS: &str = "\ -The number of rounds to execute the whole process. -This normally defaults to 1 to only run the write and/or verify once. -But you may specify more than one round to repeat write and/or verify -multiple times. -If --write mode is active, then different random data will be written -on each round. -The special value of 0 rounds will execute an infinite number of rounds. -"; - -const HELP_START_ROUND: &str = "\ -Start at the specified round index. (= Skip this many rounds). -Defaults to the first round (0). -"; +#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] +#[value(rename_all = "UPPER")] +enum AlgorithmChoice { + Chacha8, + Chacha12, + Chacha20, + Crc, +} -const HELP_QUIET: &str = "\ -Quiet level: -0: Normal verboseness. -1: Reduced verboseness. -2: No informational output. -3: No warnings. -"; +impl From for DtStreamType { + fn from(value: AlgorithmChoice) -> Self { + match value { + AlgorithmChoice::Chacha8 => DtStreamType::ChaCha8, + AlgorithmChoice::Chacha12 => DtStreamType::ChaCha12, + AlgorithmChoice::Chacha20 => DtStreamType::ChaCha20, + AlgorithmChoice::Crc => DtStreamType::Crc, + } + } +} /// All command line arguments. pub struct Args { @@ -140,119 +86,223 @@ pub struct Args { pub quiet: DisktestQuiet, } +#[derive(Debug, Parser)] +#[command( + name = "disktest", + version = env!("CARGO_PKG_VERSION"), + author = env!("CARGO_PKG_AUTHORS"), + about = ABOUT, + after_help = EXAMPLE, + verbatim_doc_comment +)] +struct CliArgs { + /// Device node of the disk or file path to access. + #[arg( + verbatim_doc_comment, + value_name = "DEVICE", + value_parser = value_parser!(PathBuf), + help = HELP_DEVICE_LONG + )] + device: PathBuf, + + /// Write pseudo random data to the device. + /// If this option is not given, then disktest will operate in + /// verify-only mode instead, as if only --verify was given. + /// If both --write and --verify are specified, then the device + /// will first be written and then be verified with the same seed. + #[arg(verbatim_doc_comment, short = 'w', long)] + write: bool, + + /// Verify pseudo random data on the device. + /// In verify-mode the disk will be read and compared to the expected pseudo + /// random sequence. + /// If both --write and --verify are specified, then the device + /// will first be written and then be verified with the same seed. + #[arg(verbatim_doc_comment, short = 'v', long)] + verify: bool, + + /// Seek to the specified byte position on disk + /// before starting the write/verify operation. This skips the specified + /// amount of bytes on the disk and also fast forwards the random number generator. + #[arg( + verbatim_doc_comment, + short = 's', + long, + value_name = "BYTES", + default_value_t = 0, + value_parser = ValueParser::new(parsebytes) + )] + seek: u64, + + /// Number of bytes to write/verify. + /// If not given, then the whole disk will be overwritten/verified. + #[arg( + verbatim_doc_comment, + short = 'b', + long = "bytes", + value_name = "BYTES", + default_value_t = Disktest::UNLIMITED, + value_parser = ValueParser::new(parsebytes) + )] + max_bytes: u64, + + /// Select the random number generator algorithm. + /// ChaCha12 and ChaCha8 are less cryptographically secure than ChaCha20, but + /// faster. CRC is even faster, but not cryptographically secure at all. + #[arg( + verbatim_doc_comment, + short = 'A', + long = "algorithm", + value_enum, + ignore_case = true, + default_value_t = AlgorithmChoice::Chacha20 + )] + algorithm: AlgorithmChoice, + + /// The seed to use for random number stream generation. + /// The seed may be any random string (e.g. a long passphrase). + /// If no seed is given, then a secure random seed will be generated + /// and also printed to the console. + #[arg(verbatim_doc_comment, short = 'S', long = "seed", value_name = "SEED")] + seed: Option, + + /// Invert the bit pattern generated by the random number generator. + /// This can be useful, if a second write/verify run with a strictly + /// inverted test bit pattern is desired. + #[arg(verbatim_doc_comment, short = 'i', long = "invert-pattern")] + invert_pattern: bool, + + /// Number of CPUs to use. + /// The special value 0 will select the maximum number of online CPUs in the + /// system. If the number of threads is equal to number of CPUs it is optimal + /// for performance. The number of threads must be equal during corresponding + /// verify and write mode runs. Otherwise the verification will fail. + #[arg( + verbatim_doc_comment, + short = 'j', + long = "threads", + value_name = "NUM", + default_value_t = 1, + value_parser = value_parser!(u32).range(0_i64..=u16::MAX as i64 + 1) + )] + threads: u32, + + /// The number of rounds to execute the whole process. + /// This normally defaults to 1 to only run the write and/or verify once. + /// But you may specify more than one round to repeat write and/or verify + /// multiple times. + /// If --write mode is active, then different random data will be written + /// on each round. + /// The special value of 0 rounds will execute an infinite number of rounds. + #[arg( + verbatim_doc_comment, + short = 'R', + long = "rounds", + value_name = "NUM", + default_value_t = 1, + value_parser = value_parser!(u64) + )] + rounds: u64, + + /// Start at the specified round index. (= Skip this many rounds). + /// Defaults to the first round (0). + #[arg( + verbatim_doc_comment, + long = "start-round", + value_name = "IDX", + default_value_t = 0, + value_parser = value_parser!(u64).range(0_u64..=u64::MAX - 1) + )] + start_round: u64, + + /// Quiet level: + /// 0: Normal verboseness. + /// 1: Reduced verboseness. + /// 2: No informational output. + /// 3: No warnings. + #[arg( + verbatim_doc_comment, + short = 'q', + long = "quiet", + value_name = "LVL", + default_value = "0", + value_parser = parse_quiet + )] + quiet: DisktestQuiet, +} + +impl CliArgs { + fn into_args(self) -> ah::Result { + let write = self.write; + let mut verify = self.verify; + if !write && !verify { + verify = true; + } + + let (seed, user_seed) = match self.seed { + Some(x) => (x, true), + None => (gen_seed_string(DEFAULT_GEN_SEED_LEN), false), + }; + if !user_seed && verify && !write { + return Err(ah::format_err!( + "Verify-only mode requires --seed. \ + Please either provide a --seed, \ + or enable --verify and --write mode." + )); + } + + let mut rounds = self.rounds; + if rounds == 0 { + rounds = u64::MAX; + } + let start_round = self.start_round; + if start_round >= rounds { + rounds = start_round + 1; + } + + Ok(Args { + device: self.device, + write, + verify, + seek: self.seek, + max_bytes: self.max_bytes, + algorithm: self.algorithm.into(), + seed, + user_seed, + invert_pattern: self.invert_pattern, + threads: self.threads as usize, + rounds, + start_round, + quiet: self.quiet, + }) + } +} + +fn parse_quiet(value: &str) -> Result { + let lvl = value.parse::().map_err(|e| e.to_string())?; + let quiet = match lvl { + x if x == DisktestQuiet::Normal as u8 => DisktestQuiet::Normal, + x if x == DisktestQuiet::Reduced as u8 => DisktestQuiet::Reduced, + x if x == DisktestQuiet::NoInfo as u8 => DisktestQuiet::NoInfo, + x if x == DisktestQuiet::NoWarn as u8 => DisktestQuiet::NoWarn, + _ => { + return Err(format!( + "Invalid quiet level '{}'. Allowed: 0, 1, 2, 3.", + value + )) + } + }; + Ok(quiet) +} + /// Parse all command line arguments and put them into a structure. pub fn parse_args(args: I) -> ah::Result where I: IntoIterator, T: Into + Clone, { - let about = ABOUT.to_string() + EXAMPLE; - let help_device = HELP_DEVICE.to_string() + HELP_DEVICE_OS; - - let args = Command::new("disktest") - .about(about) - .arg( - Arg::new("device") - .index(1) - .required(true) - .value_parser(value_parser!(PathBuf)) - .help(help_device), - ) - .arg( - Arg::new("write") - .long("write") - .short('w') - .action(ArgAction::SetTrue) - .help(HELP_WRITE), - ) - .arg( - Arg::new("verify") - .long("verify") - .short('v') - .action(ArgAction::SetTrue) - .help(HELP_VERIFY), - ) - .arg( - Arg::new("seek") - .long("seek") - .short('s') - .value_name("BYTES") - .default_value("0") - .value_parser(ValueParser::new(parsebytes)) - .help(HELP_SEEK), - ) - .arg( - Arg::new("bytes") - .long("bytes") - .short('b') - .value_name("BYTES") - .default_value("18446744073709551615") - .value_parser(ValueParser::new(parsebytes)) - .help(HELP_BYTES), - ) - .arg( - Arg::new("algorithm") - .long("algorithm") - .short('A') - .value_name("ALG") - .default_value("CHACHA20") - .value_parser(["CHACHA8", "CHACHA12", "CHACHA20", "CRC"]) - .ignore_case(true) - .help(HELP_ALGORITHM), - ) - .arg( - Arg::new("seed") - .long("seed") - .short('S') - .value_name("SEED") - .help(HELP_SEED), - ) - .arg( - Arg::new("invert-pattern") - .long("invert-pattern") - .short('i') - .action(ArgAction::SetTrue) - .help(HELP_INVERT_PATTERN), - ) - .arg( - Arg::new("threads") - .long("threads") - .short('j') - .value_name("NUM") - .default_value("1") - .value_parser(value_parser!(u32).range(0_i64..=u16::MAX as i64 + 1)) - .help(HELP_THREADS), - ) - .arg( - Arg::new("rounds") - .long("rounds") - .short('R') - .value_name("NUM") - .default_value("1") - .value_parser(value_parser!(u64)) - .help(HELP_ROUNDS), - ) - .arg( - Arg::new("start-round") - .long("start-round") - .value_name("IDX") - .default_value("0") - .value_parser(value_parser!(u64).range(0_u64..=u64::MAX - 1)) - .help(HELP_START_ROUND), - ) - .arg( - Arg::new("quiet") - .long("quiet") - .short('q') - .value_name("LVL") - .default_value("0") - .value_parser(value_parser!(u8)) - .help(HELP_QUIET), - ) - .try_get_matches_from(args); - - let args = match args { - Ok(x) => x, + match CliArgs::try_parse_from(args) { + Ok(cli) => cli.into_args(), Err(e) => { match e.kind() { DisplayHelp | DisplayVersion => { @@ -261,86 +311,9 @@ where } _ => (), }; - return Err(ah::format_err!("{}", e)); + Err(ah::format_err!("{}", e)) } - }; - - let quiet = *args.get_one::("quiet").unwrap(); - let quiet = if quiet == DisktestQuiet::Normal as u8 { - DisktestQuiet::Normal - } else if quiet == DisktestQuiet::Reduced as u8 { - DisktestQuiet::Reduced - } else if quiet == DisktestQuiet::NoInfo as u8 { - DisktestQuiet::NoInfo - } else { - DisktestQuiet::NoWarn - }; - - let device = args.get_one::("device").unwrap().clone(); - - let write = args.get_flag("write"); - let mut verify = args.get_flag("verify"); - if !write && !verify { - verify = true; - } - - let seek = *args.get_one::("seek").unwrap(); - - let max_bytes = *args.get_one::("bytes").unwrap(); - - let algorithm = match args - .get_one::("algorithm") - .unwrap() - .to_ascii_uppercase() - .as_str() - { - "CHACHA8" => DtStreamType::ChaCha8, - "CHACHA12" => DtStreamType::ChaCha12, - "CHACHA20" => DtStreamType::ChaCha20, - "CRC" => DtStreamType::Crc, - _ => panic!("Invalid algorithm parameter."), - }; - - let (seed, user_seed) = match args.get_one::("seed") { - Some(x) => (x.clone(), true), - None => (gen_seed_string(DEFAULT_GEN_SEED_LEN), false), - }; - if !user_seed && verify && !write { - return Err(ah::format_err!( - "Verify-only mode requires --seed. \ - Please either provide a --seed, \ - or enable --verify and --write mode." - )); } - - let invert_pattern = args.get_flag("invert-pattern"); - - let threads = *args.get_one::("threads").unwrap_or(&1) as usize; - - let mut rounds = *args.get_one::("rounds").unwrap_or(&1); - if rounds == 0 { - rounds = u64::MAX; - } - let start_round = *args.get_one::("start-round").unwrap_or(&0); - if start_round >= rounds { - rounds = start_round + 1; - } - - Ok(Args { - device, - write, - verify, - seek, - max_bytes, - algorithm, - seed, - user_seed, - invert_pattern, - threads, - rounds, - start_round, - quiet, - }) } #[cfg(test)]