zond_engine/core/parse/
ip.rs1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
29use std::sync::atomic::AtomicBool;
30use thiserror::Error;
31
32use crate::core::models::ip::range::{IpError, IpRange, Ipv4Range};
33use crate::core::models::ip::set::IpSet;
34use crate::success;
35
36pub static IS_LAN_SCAN: AtomicBool = AtomicBool::new(false);
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Keyword {
41 Lan,
42 Vpn,
43}
44
45impl Keyword {
46 pub fn as_str(&self) -> &'static str {
47 match self {
48 Keyword::Lan => "lan",
49 Keyword::Vpn => "vpn",
50 }
51 }
52}
53
54#[derive(Debug, Error, PartialEq, Eq)]
56pub enum IpParseError {
57 #[error("Invalid CIDR prefix: {0} (must be 0-32)")]
59 InvalidPrefix(u8),
60
61 #[error("Invalid range: start address {0} is greater than end address {1}")]
63 InvalidRange(IpAddr, IpAddr),
64
65 #[error("Malformed IP or range string: '{0}'")]
67 Malformed(String),
68
69 #[error("Could not resolve LAN interface: {0}")]
71 LanError(String),
72
73 #[error("Network error: {0}")]
75 NetworkError(String),
76
77 #[error("Target input resulted in an empty set")]
79 EmptySet,
80}
81
82pub type ResolverFn = fn(Keyword, &mut IpSet) -> Result<(), IpParseError>;
108
109pub fn to_set<S>(ips: &[S], resolver: Option<ResolverFn>) -> Result<IpSet, IpParseError>
110where
111 S: AsRef<str>,
112{
113 let mut set = IpSet::new();
114
115 for ip in ips {
116 let s = ip.as_ref().trim();
117 if s.is_empty() {
118 continue;
119 }
120
121 for part in s.split(',').map(|p| p.trim()).filter(|p| !p.is_empty()) {
122 parse_and_insert(part, &mut set, resolver)?;
123 }
124 }
125
126 if set.is_empty() {
127 return Err(IpParseError::EmptySet);
128 }
129
130 let len = set.len();
131 let suffix = if len == 1 { "" } else { "es" };
132 success!("{len} IP address{suffix} resolved successfully");
133
134 Ok(set)
135}
136
137fn parse_and_insert(
139 s: &str,
140 set: &mut IpSet,
141 resolver: Option<ResolverFn>,
142) -> Result<(), IpParseError> {
143 if s.contains('/') {
144 let range = parse_cidr(s)?;
145 set.insert_range(range);
146 return Ok(());
147 }
148
149 if s.contains('-') {
150 let range = parse_range(s)?;
151 set.insert_range(range);
152 return Ok(());
153 }
154
155 if s.eq_ignore_ascii_case(Keyword::Lan.as_str()) {
156 if let Some(r) = resolver {
157 return r(Keyword::Lan, set);
158 } else {
159 return Err(IpParseError::LanError(
160 "LAN keyword used but no resolver provided".into(),
161 ));
162 }
163 }
164
165 let ip = s
166 .parse::<IpAddr>()
167 .map_err(|_| IpParseError::Malformed(s.to_string()))?;
168 set.insert(ip);
169
170 Ok(())
171}
172
173fn parse_range(s: &str) -> Result<IpRange, IpParseError> {
175 let (start_str, end_str) = s
176 .split_once('-')
177 .ok_or_else(|| IpParseError::Malformed(s.into()))?;
178
179 let start_addr = start_str
180 .parse::<IpAddr>()
181 .map_err(|_| IpParseError::Malformed(s.into()))?;
182
183 match start_addr {
184 IpAddr::V4(start_v4) => {
185 let end_v4 = if let Ok(addr) = end_str.parse::<Ipv4Addr>() {
186 addr
187 } else {
188 let mut octets = start_v4.octets();
189 let parts: Vec<u8> = end_str
190 .split('.')
191 .map(|p| p.parse::<u8>())
192 .collect::<Result<Vec<u8>, _>>()
193 .map_err(|_| IpParseError::Malformed(s.into()))?;
194
195 if parts.is_empty() || parts.len() > 4 {
196 return Err(IpParseError::Malformed(s.into()));
197 }
198
199 let offset = 4 - parts.len();
200 octets[offset..].copy_from_slice(&parts);
201 Ipv4Addr::from(octets)
202 };
203 Ipv4Range::new(start_v4, end_v4)
204 .map(IpRange::V4)
205 .map_err(map_range_error)
206 }
207 IpAddr::V6(start_v6) => {
208 let end_v6 = end_str
209 .parse::<Ipv6Addr>()
210 .map_err(|_| IpParseError::Malformed(s.into()))?;
211 crate::core::models::ip::range::Ipv6Range::new(start_v6, end_v6)
212 .map(IpRange::V6)
213 .map_err(map_range_error)
214 }
215 }
216}
217
218fn parse_cidr(s: &str) -> Result<IpRange, IpParseError> {
220 let (ip_str, prefix_str) = s
221 .split_once('/')
222 .ok_or_else(|| IpParseError::Malformed(s.into()))?;
223
224 let ip = ip_str
225 .parse::<IpAddr>()
226 .map_err(|_| IpParseError::Malformed(s.into()))?;
227
228 let prefix = prefix_str
229 .parse::<u8>()
230 .map_err(|_| IpParseError::Malformed(s.into()))?;
231
232 crate::core::models::ip::range::cidr_range(ip, prefix).map_err(map_range_error)
233}
234
235fn map_range_error(e: IpError) -> IpParseError {
236 match e {
237 IpError::InvalidRange(s, e) => IpParseError::InvalidRange(s, e),
238 IpError::InvalidPrefix(p) => IpParseError::InvalidPrefix(p),
239 IpError::NetworkError(msg) => IpParseError::NetworkError(msg),
240 _ => IpParseError::Malformed("Invalid IP range".into()),
241 }
242}
243
244#[cfg(test)]
254mod tests {
255 use super::*;
256 use std::net::Ipv4Addr;
257
258 #[test]
259 fn to_set_basic_single() {
260 let input = vec!["192.168.1.1"];
261 let set = to_set(&input, None).expect("Should parse single IP");
262 assert_eq!(set.len(), 1);
263 assert!(set.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
264 }
265
266 #[test]
267 fn to_set_comma_separated() {
268 let input = vec!["10.0.0.1, 10.0.0.2, 10.0.0.5"];
269 let set = to_set(&input, None).expect("Should parse comma list");
270 assert_eq!(set.len(), 3);
271 assert!(set.contains(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
272 }
273
274 #[test]
275 fn parse_cidr_blocks() {
276 let input = vec!["172.16.0.0/24"];
277 let set = to_set(&input, None).expect("Should parse CIDR");
278 assert_eq!(set.len(), 256);
279 }
280
281 #[test]
282 fn parse_short_range_suffix() {
283 let input = vec!["192.168.1.250-2.10"];
284 let set = to_set(&input, None).unwrap();
285 assert_eq!(set.len(), 17);
286 }
287
288 #[test]
289 fn error_invalid_cidr() {
290 let input = vec!["192.168.1.1/33"];
291 let result = to_set(&input, None);
292 assert_eq!(result.unwrap_err(), IpParseError::InvalidPrefix(33));
293 }
294
295 #[test]
296 fn error_invalid_range_order() {
297 let input = vec!["10.0.0.10-1"];
298 let result = to_set(&input, None);
299 assert!(matches!(result, Err(IpParseError::InvalidRange(_, _))));
300 }
301
302 #[test]
303 fn empty_input_error() {
304 let input: Vec<&str> = vec!["", " "];
305 let result = to_set(&input, None);
306 assert_eq!(result.unwrap_err(), IpParseError::EmptySet);
307 }
308}