1use ipnet::Ipv4Net;
2#[cfg(not(windows))]
3use ipnetwork::IpNetwork;
4
5use crate::error::{Error, Result};
6
7pub const DEFAULT_SUBNET: &str = "192.168.1.0/24";
8pub const DEFAULT_PORTS: &[u16] = &[22, 80, 443];
9pub const DEFAULT_CONCURRENCY: usize = 256;
10pub const DEFAULT_SCAN_TIMEOUT_MS: u64 = 500;
11pub const DEFAULT_PING_TIMEOUT_MS: u64 = 1000;
12pub const DEFAULT_DNS_TIMEOUT_MS: u64 = 1500;
13
14pub fn parse_ports(input: Option<&str>) -> Option<Vec<u16>> {
17 input.map(|s| {
18 s.split(',')
19 .filter_map(|part| parse_port_token(part).ok())
20 .flatten()
21 .collect()
22 })
23}
24
25fn parse_port_token(part: &str) -> Result<Vec<u16>> {
26 let part = part.trim();
27 if part.is_empty() {
28 return Ok(Vec::new());
29 }
30
31 if let Some((start, end)) = part.split_once('-') {
32 let start_trim = start.trim();
33 let end_trim = end.trim();
34 let s: u16 = start_trim
35 .parse()
36 .map_err(|_| Error::invalid_input(format!("invalid port in range: '{start_trim}'")))?;
37 let e: u16 = end_trim
38 .parse()
39 .map_err(|_| Error::invalid_input(format!("invalid port in range: '{end_trim}'")))?;
40 if s > e {
41 return Err(Error::invalid_input(format!(
42 "inverted port range: {s}-{e}"
43 )));
44 }
45 Ok((s..=e).collect())
46 } else {
47 let port: u16 = part
48 .parse()
49 .map_err(|_| Error::invalid_input(format!("invalid port: '{part}'")))?;
50 Ok(vec![port])
51 }
52}
53
54pub fn parse_ports_checked(input: Option<&str>) -> Result<Option<Vec<u16>>> {
59 let Some(raw) = input else {
60 return Ok(None);
61 };
62
63 let raw = raw.trim();
64 if raw.is_empty() {
65 return Ok(None);
66 }
67
68 let mut ports = Vec::new();
69 for part in raw.split(',') {
70 let mut expanded = parse_port_token(part).map_err(|e| {
71 Error::invalid_input(format!("Invalid port list '{raw}': {e}"))
74 })?;
75 ports.append(&mut expanded);
76 }
77 if ports.is_empty() {
78 return Err(Error::invalid_input(format!("Invalid port list: {raw}")));
79 }
80 ports.sort_unstable();
82 ports.dedup();
83 Ok(Some(ports))
84}
85
86#[cfg(not(windows))]
87pub fn detect_default_ipv4_subnet() -> Option<Ipv4Net> {
88 let mut interfaces: Vec<_> = pnet_datalink::interfaces()
94 .into_iter()
95 .filter(|i| i.is_up() && !i.is_loopback())
96 .collect();
97 interfaces.sort_by_key(|i| interface_preference_rank(&i.name));
98
99 for iface in interfaces {
100 for ip in iface.ips {
101 if let IpNetwork::V4(v4) = ip {
102 if let Ok(net) = Ipv4Net::new(v4.ip(), v4.prefix()) {
103 return Some(net.trunc());
106 }
107 }
108 }
109 }
110
111 None
112}
113
114#[cfg(not(windows))]
117fn interface_preference_rank(name: &str) -> u8 {
118 const TUNNEL_PREFIXES: &[&str] = &["tun", "tap", "wg", "utun", "zt", "ppp", "gif", "ipsec"];
119 let lowered = name.to_ascii_lowercase();
121 if TUNNEL_PREFIXES.iter().any(|p| lowered.starts_with(p)) {
122 2
123 } else {
124 0
125 }
126}
127
128#[cfg(windows)]
129pub fn detect_default_ipv4_subnet() -> Option<Ipv4Net> {
130 use ipconfig::{IfType, OperStatus};
131 use std::net::IpAddr;
132
133 let adapters = ipconfig::get_adapters().ok()?;
134 for adapter in adapters {
135 if adapter.oper_status() != OperStatus::IfOperStatusUp {
136 continue;
137 }
138 if adapter.if_type() == IfType::SoftwareLoopback {
139 continue;
140 }
141 for (prefix, len) in adapter.prefixes() {
142 if let IpAddr::V4(v4) = *prefix {
143 let prefix_len = (*len).min(32) as u8;
144 if let Ok(net) = Ipv4Net::new(v4, prefix_len) {
145 return Some(net);
146 }
147 }
148 }
149 }
150
151 None
152}
153
154#[cfg(not(windows))]
155pub fn detect_default_ipv4_addr() -> Option<std::net::Ipv4Addr> {
156 let iface = pnet_datalink::interfaces()
157 .into_iter()
158 .find(|i| i.is_up() && !i.is_loopback())?;
159
160 for ip in iface.ips {
161 if let IpNetwork::V4(v4) = ip {
162 return Some(v4.ip());
163 }
164 }
165
166 None
167}
168
169#[cfg(windows)]
170pub fn detect_default_ipv4_addr() -> Option<std::net::Ipv4Addr> {
171 use ipconfig::{IfType, OperStatus};
172 use std::net::IpAddr;
173
174 let adapters = ipconfig::get_adapters().ok()?;
175 for adapter in adapters {
176 if adapter.oper_status() != OperStatus::IfOperStatusUp {
177 continue;
178 }
179 if adapter.if_type() == IfType::SoftwareLoopback {
180 continue;
181 }
182 for ip in adapter.ip_addresses() {
183 if let IpAddr::V4(v4) = *ip {
184 return Some(v4);
185 }
186 }
187 }
188
189 None
190}
191
192pub fn default_ipv4_subnet_string() -> String {
193 detect_default_ipv4_subnet()
194 .map(|n| format!("{}/{}", n.network(), n.prefix_len()))
195 .unwrap_or_else(|| DEFAULT_SUBNET.to_string())
196}
197
198pub fn default_ports() -> Vec<u16> {
199 DEFAULT_PORTS.to_vec()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_parse_ports_single() {
208 assert_eq!(parse_ports(Some("80")), Some(vec![80]));
209 assert_eq!(parse_ports(Some("443")), Some(vec![443]));
210 }
211
212 #[test]
213 fn test_parse_ports_comma_separated() {
214 assert_eq!(parse_ports(Some("80,443,8080")), Some(vec![80, 443, 8080]));
215 assert_eq!(parse_ports(Some("22,80,443")), Some(vec![22, 80, 443]));
216 }
217
218 #[test]
219 fn test_parse_ports_range() {
220 assert_eq!(parse_ports(Some("80-82")), Some(vec![80, 81, 82]));
221 assert_eq!(parse_ports(Some("22-24")), Some(vec![22, 23, 24]));
222 }
223
224 #[test]
225 fn test_parse_ports_mixed() {
226 assert_eq!(
227 parse_ports(Some("80,443,8080-8082")),
228 Some(vec![80, 443, 8080, 8081, 8082])
229 );
230 }
231
232 #[test]
233 fn test_parse_ports_with_spaces() {
234 assert_eq!(
235 parse_ports(Some("80, 443, 8080")),
236 Some(vec![80, 443, 8080])
237 );
238 assert_eq!(parse_ports(Some("80 - 82")), Some(vec![80, 81, 82]));
239 }
240
241 #[test]
242 fn test_parse_ports_none() {
243 assert_eq!(parse_ports(None), None);
244 }
245
246 #[test]
247 fn test_parse_ports_invalid_lenient() {
248 assert_eq!(parse_ports(Some("invalid")), Some(vec![]));
252 assert_eq!(parse_ports(Some("80,invalid,443")), Some(vec![80, 443]));
253 }
254
255 #[test]
256 fn test_parse_ports_checked_invalid() {
257 let err = parse_ports_checked(Some("invalid")).unwrap_err();
258 assert!(err.to_string().contains("Invalid port list"));
259 }
260
261 #[test]
262 fn test_parse_ports_checked_rejects_typo() {
263 let err = parse_ports_checked(Some("80,invalid,443")).unwrap_err();
265 assert!(
266 err.to_string().contains("Invalid port list"),
267 "expected strict rejection, got: {err}"
268 );
269 }
270
271 #[test]
272 fn test_parse_ports_checked_rejects_out_of_range() {
273 let err = parse_ports_checked(Some("22,99999")).unwrap_err();
274 assert!(err.to_string().contains("Invalid port list"));
275 }
276
277 #[test]
278 fn test_parse_ports_checked_rejects_inverted_range() {
279 let err = parse_ports_checked(Some("90-80")).unwrap_err();
280 assert!(err.to_string().contains("Invalid port list"));
281 }
282
283 #[test]
284 fn test_parse_ports_checked_empty() {
285 assert_eq!(parse_ports_checked(Some(" ")).unwrap(), None);
286 }
287
288 #[test]
289 fn test_parse_ports_checked_sorts_and_dedupes() {
290 assert_eq!(
291 parse_ports_checked(Some("443,80,22,80,22")).unwrap(),
292 Some(vec![22, 80, 443])
293 );
294 }
295
296 #[test]
297 fn test_parse_ports_checked_valid() {
298 assert_eq!(
299 parse_ports_checked(Some("80,443")).unwrap(),
300 Some(vec![80, 443])
301 );
302 }
303}