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,
}
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))
};
let mut ports = (start..=end).collect::<Vec<_>>();
ports.reverse();
Self { start, end, ports }
}
}
#[derive(Debug, Clone)]
pub struct PortRange {
pub start: u16,
pub end: u16,
ports: Vec<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,
}
impl CliArgs {
pub fn new() -> Self {
let cli = Cli::parse();
let port_range = PortRange::from(&cli);
Self {
address: cli.address,
concurrent: cli.concurrent,
ip6: cli.ip_v6,
monochrome: cli.monochrome,
port_range,
retry: cli.retry,
timeout: cli.timeout,
}
}
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()
}
pub fn ports_split(&mut self) -> Vec<u16> {
let concurrent = usize::from(self.concurrent);
if self.port_range.ports.len() >= concurrent {
self.port_range.ports.split_off(concurrent)
} else {
vec![]
}
}
}
#[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,
};
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,
}
}
}
#[cfg(test)]
mod tests {
use crate::parse_arg::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,
});
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,
});
assert_eq!(result.start, 1);
assert_eq!(result.end, 65535);
assert_eq!(result.ports.len(), 65535);
}
}