use indicatif::{ProgressBar, ProgressStyle};
use ipnetwork::{IpNetwork, NetworkSize};
use std::collections::HashSet;
use std::io::BufRead;
use std::net::IpAddr;
pub const MAX_CONCURRENCY: usize = 1024;
pub fn init_scan_pool(concurrency: usize) {
let threads = concurrency.clamp(1, MAX_CONCURRENCY);
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.stack_size(512 * 1024)
.build_global()
.expect("failed to initialize the scan thread pool");
}
pub fn progress_bar(total: u64, suffix: &str) -> ProgressBar {
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::with_template(&format!(
"[{{elapsed_precise}}] {{bar:40.cyan/blue}} {{pos}}/{{len}} {suffix}"
))
.unwrap()
.progress_chars("=> "),
);
pb
}
const MAX_TARGET_CIDR_HOSTS: u128 = 1 << 16;
pub fn read_targets_from_stdin() -> Vec<String> {
read_targets(std::io::stdin().lock())
}
pub fn read_targets_from_file(path: &std::path::Path) -> std::io::Result<Vec<String>> {
let file = std::fs::File::open(path)?;
Ok(read_targets(std::io::BufReader::new(file)))
}
pub fn read_targets<R: BufRead>(reader: R) -> Vec<String> {
let mut raw: Vec<String> = Vec::new();
for line in reader.lines() {
let Ok(line) = line else { continue };
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.as_bytes().first() {
Some(b'{') | Some(b'[') => extract_ips(trimmed, &mut raw),
_ if trimmed.contains('/') => expand_cidr(trimmed, &mut raw),
_ => raw.push(trimmed.to_string()),
}
}
let mut seen = HashSet::new();
raw.into_iter()
.filter(|host| seen.insert(host.clone()))
.collect()
}
fn expand_cidr(token: &str, out: &mut Vec<String>) {
let Ok(network) = token.parse::<IpNetwork>() else {
out.push(token.to_string());
return;
};
let size: u128 = match network.size() {
NetworkSize::V4(n) => u128::from(n),
NetworkSize::V6(n) => n,
};
if size > MAX_TARGET_CIDR_HOSTS {
eprintln!(
"Not expanding {} ({} addresses, limit {}); passing it through as-is",
token, size, MAX_TARGET_CIDR_HOSTS
);
out.push(token.to_string());
return;
}
for ip in network.iter() {
out.push(ip.to_string());
}
}
fn extract_ips(json: &str, out: &mut Vec<String>) {
let Ok(value) = serde_json::from_str::<serde_json::Value>(json) else {
return;
};
match value {
serde_json::Value::Array(items) => {
for item in items {
if let Some(ip) = item.get("ip").and_then(|v| v.as_str()) {
out.push(ip.to_string());
}
}
}
serde_json::Value::Object(_) => {
if let Some(ip) = value.get("ip").and_then(|v| v.as_str()) {
out.push(ip.to_string());
}
}
_ => {}
}
}
pub fn parse_ports(s: &str) -> Result<Vec<u16>, String> {
s.split(',')
.map(|p| {
p.parse::<u16>()
.map_err(|_| format!("Invalid port number: {}", p))
})
.collect()
}
pub fn parse_ip(ip: &str) -> Result<IpAddr, String> {
ip.parse::<IpAddr>()
.map_err(|_| format!("Invalid IP address: {}", ip))
}
pub fn parse_subnet(subnet: &str) -> Result<IpNetwork, String> {
subnet
.parse::<IpNetwork>()
.map_err(|_| format!("Invalid subnet format: {}", subnet))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn targets(input: &str) -> Vec<String> {
read_targets(Cursor::new(input))
}
#[test]
fn reads_bare_hosts_and_skips_blanks() {
assert_eq!(
targets("example.com\n\n 10.0.0.1 \n"),
vec!["example.com".to_string(), "10.0.0.1".to_string()]
);
}
#[test]
fn extracts_ip_from_json_and_jsonl_lines() {
let input = "{\"ip\":\"127.0.0.1\",\"status\":\"up\"}\n[{\"ip\":\"10.0.0.2\"}]\n";
assert_eq!(
targets(input),
vec!["127.0.0.1".to_string(), "10.0.0.2".to_string()]
);
}
#[test]
fn expands_a_cidr_line_into_addresses() {
let out = targets("192.168.1.0/30\n");
assert_eq!(
out,
vec![
"192.168.1.0".to_string(),
"192.168.1.1".to_string(),
"192.168.1.2".to_string(),
"192.168.1.3".to_string(),
]
);
}
#[test]
fn deduplicates_while_preserving_first_seen_order() {
assert_eq!(
targets("10.0.0.1\n10.0.0.2\n10.0.0.1\n"),
vec!["10.0.0.1".to_string(), "10.0.0.2".to_string()]
);
}
#[test]
fn oversized_cidr_is_passed_through_not_expanded() {
assert_eq!(targets("10.0.0.0/8\n"), vec!["10.0.0.0/8".to_string()]);
}
}