use std::collections::VecDeque;
use clap::Parser;
pub const PORT_UPPER_DEFAULT: u16 = 1000;
#[derive(Parser, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
#[command(version, about)]
pub struct Cli {
#[clap(default_value = "127.0.0.1", default_value_if("ip_v6", "true", "::1"))]
address: String,
#[clap(short = 'a', default_value_t = false, conflicts_with = "ports")]
all_ports: bool,
#[clap(short = 'c', value_name = "concurrent", default_value_t = 1000)]
concurrent: u16,
#[clap(
short = 'p',
value_name = "ports",
default_value = "-1000",
conflicts_with = "all_ports",
allow_hyphen_values = true
)]
ports: String,
#[clap(short = 'm', default_value_t = false)]
monochrome: bool,
#[clap(short = 'r', value_name = "retries", default_value_t = 1)]
retry: u8,
#[clap(short = 't', value_name = "ms", default_value_t = 2000)]
timeout: u32,
#[clap(short = '6', default_value_t = false)]
ip_v6: bool,
#[clap(long = "verbose", default_value_t = false)]
verbose: bool,
}
impl From<&Cli> for PortRange {
fn from(cli: &Cli) -> Self {
let (start, end) = if cli.all_ports {
(1, u16::MAX)
} else if cli.ports.contains('-') {
let (start, end) = cli.ports.split_once('-').unwrap_or_default();
let start = start.parse::<u16>().unwrap_or(1);
let end = end.parse::<u16>().unwrap_or(PORT_UPPER_DEFAULT);
if start <= end {
(start, end)
} else {
(end, start)
}
} else {
cli.ports
.parse::<u16>()
.map_or((1, PORT_UPPER_DEFAULT), |i| (i, i))
};
Self {
start,
end,
ports: (start..=end).collect::<VecDeque<_>>(),
}
}
}
#[derive(Debug, Clone)]
pub struct PortRange {
pub start: u16,
pub end: u16,
ports: VecDeque<u16>,
}
#[derive(Debug, Clone)]
pub struct CliArgs {
pub address: String,
pub concurrent: u16,
pub ip6: bool,
pub monochrome: bool,
pub port_range: PortRange,
pub retry: u8,
pub timeout: u32,
pub verbose: Option<()>,
}
impl CliArgs {
fn check_if_zero<T: PartialEq + From<u8>>(x: T) -> T {
if x == 0.into() { T::from(1) } else { x }
}
pub fn new() -> Self {
Self::from(Cli::parse())
}
pub fn ports_len(&self) -> u16 {
u16::try_from(self.port_range.ports.len()).unwrap_or_default()
}
pub fn ports_pop(&mut self) -> Option<u16> {
self.port_range.ports.pop_front()
}
}
impl From<Cli> for CliArgs {
fn from(cli: Cli) -> Self {
let port_range = PortRange::from(&cli);
Self {
address: cli.address,
concurrent: Self::check_if_zero(cli.concurrent),
ip6: cli.ip_v6,
monochrome: cli.monochrome,
port_range,
retry: cli.retry,
timeout: Self::check_if_zero(cli.timeout),
verbose: if cli.verbose { Some(()) } else { None },
}
}
}
#[cfg(test)]
impl CliArgs {
pub fn test_new(ports: String, concurrent: u16, address: Option<&str>, ip_v6: bool) -> Self {
let adr = if ip_v6 && address.is_none() {
"::1"
} else {
"127.0.0.1"
};
let cli = Cli {
address: address.unwrap_or(adr).to_owned(),
all_ports: false,
concurrent,
ip_v6,
monochrome: false,
ports,
retry: 1,
timeout: 1250,
verbose: false,
};
let ports = PortRange::from(&cli);
Self {
address: cli.address,
concurrent: cli.concurrent,
ip6: cli.ip_v6,
monochrome: cli.monochrome,
port_range: ports,
retry: cli.retry,
timeout: cli.timeout,
verbose: None,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use clap::Parser;
use crate::parse_arg::{CliArgs, PortRange};
use super::Cli;
fn test(ports: &str, min: u16, max: u16, range: u16) {
let result = PortRange::from(&Cli {
address: "127.0.0.1".to_owned(),
all_ports: false,
concurrent: 1024,
ip_v6: false,
monochrome: false,
ports: ports.to_owned(),
retry: 1,
timeout: 1000,
verbose: false,
});
assert_eq!(result.start, min);
assert_eq!(result.end, max);
assert_eq!(result.ports.len(), usize::from(range));
}
#[test]
fn test_cli_port_range() {
test("1-1000", 1, 1000, 1000);
test("1000-1", 1, 1000, 1000);
test("100-200", 100, 200, 101);
test("100-", 100, 1000, 901);
test("80", 80, 80, 1);
test("1-1000000", 1, 1000, 1000);
test("65536-1000000", 1, 1000, 1000);
test("random", 1, 1000, 1000);
let result = PortRange::from(&Cli {
monochrome: false,
address: "127.0.0.1".to_owned(),
all_ports: true,
concurrent: 1024,
ip_v6: false,
ports: String::new(),
retry: 100,
timeout: 1000,
verbose: false,
});
assert_eq!(result.start, 1);
assert_eq!(result.end, 65535);
assert_eq!(result.ports.len(), 65535);
}
#[test]
fn test_cli_zero_values() {
let cli = Cli::try_parse_from(["havn", "-c", "0", "-t", "0"]).unwrap();
let args = CliArgs::from(cli);
assert_eq!(args.concurrent, 1);
assert_eq!(args.timeout, 1);
let cli = Cli::try_parse_from(["havn", "-c", "10", "-t", "0"]).unwrap();
let args = CliArgs::from(cli);
assert_eq!(args.concurrent, 10);
assert_eq!(args.timeout, 1);
let cli = Cli::try_parse_from(["havn", "-c", "0", "-t", "10"]).unwrap();
let args = CliArgs::from(cli);
assert_eq!(args.concurrent, 1);
assert_eq!(args.timeout, 10);
let cli = Cli::try_parse_from(["havn", "-c", "10", "-t", "10"]).unwrap();
let args = CliArgs::from(cli);
assert_eq!(args.concurrent, 10);
assert_eq!(args.timeout, 10);
}
}