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, Ipv4Range, IpRange};
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(s: &str, set: &mut IpSet, resolver: Option<ResolverFn>) -> Result<(), IpParseError> {
139 if s.contains('/') {
140 let range = parse_cidr(s)?;
141 set.insert_range(range);
142 return Ok(());
143 }
144
145 if s.contains('-') {
146 let range = parse_range(s)?;
147 set.insert_range(range);
148 return Ok(());
149 }
150
151 if s.eq_ignore_ascii_case(Keyword::Lan.as_str()) {
152 if let Some(r) = resolver {
153 return r(Keyword::Lan, set);
154 } else {
155 return Err(IpParseError::LanError("LAN keyword used but no resolver provided".into()));
156 }
157 }
158
159 let ip = s
160 .parse::<IpAddr>()
161 .map_err(|_| IpParseError::Malformed(s.to_string()))?;
162 set.insert(ip);
163
164 Ok(())
165}
166
167fn parse_range(s: &str) -> Result<IpRange, IpParseError> {
169 let (start_str, end_str) = s
170 .split_once('-')
171 .ok_or_else(|| IpParseError::Malformed(s.into()))?;
172
173 let start_addr = start_str
174 .parse::<IpAddr>()
175 .map_err(|_| IpParseError::Malformed(s.into()))?;
176
177 match start_addr {
178 IpAddr::V4(start_v4) => {
179 let end_v4 = if let Ok(addr) = end_str.parse::<Ipv4Addr>() {
180 addr
181 } else {
182 let mut octets = start_v4.octets();
183 let parts: Vec<u8> = end_str
184 .split('.')
185 .map(|p| p.parse::<u8>())
186 .collect::<Result<Vec<u8>, _>>()
187 .map_err(|_| IpParseError::Malformed(s.into()))?;
188
189 if parts.is_empty() || parts.len() > 4 {
190 return Err(IpParseError::Malformed(s.into()));
191 }
192
193 let offset = 4 - parts.len();
194 octets[offset..].copy_from_slice(&parts);
195 Ipv4Addr::from(octets)
196 };
197 Ipv4Range::new(start_v4, end_v4)
198 .map(IpRange::V4)
199 .map_err(map_range_error)
200 }
201 IpAddr::V6(start_v6) => {
202 let end_v6 = end_str
203 .parse::<Ipv6Addr>()
204 .map_err(|_| IpParseError::Malformed(s.into()))?;
205 crate::core::models::ip::range::Ipv6Range::new(start_v6, end_v6)
206 .map(IpRange::V6)
207 .map_err(map_range_error)
208 }
209 }
210}
211
212fn parse_cidr(s: &str) -> Result<IpRange, IpParseError> {
214 let (ip_str, prefix_str) = s
215 .split_once('/')
216 .ok_or_else(|| IpParseError::Malformed(s.into()))?;
217
218 let ip = ip_str
219 .parse::<IpAddr>()
220 .map_err(|_| IpParseError::Malformed(s.into()))?;
221
222 let prefix = prefix_str
223 .parse::<u8>()
224 .map_err(|_| IpParseError::Malformed(s.into()))?;
225
226 crate::core::models::ip::range::cidr_range(ip, prefix).map_err(map_range_error)
227}
228
229fn map_range_error(e: IpError) -> IpParseError {
230 match e {
231 IpError::InvalidRange(s, e) => IpParseError::InvalidRange(s, e),
232 IpError::InvalidPrefix(p) => IpParseError::InvalidPrefix(p),
233 IpError::NetworkError(msg) => IpParseError::NetworkError(msg),
234 _ => IpParseError::Malformed("Invalid IP range".into()),
235 }
236}
237
238#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::net::Ipv4Addr;
251
252 #[test]
253 fn to_set_basic_single() {
254 let input = vec!["192.168.1.1"];
255 let set = to_set(&input, None).expect("Should parse single IP");
256 assert_eq!(set.len(), 1);
257 assert!(set.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
258 }
259
260 #[test]
261 fn to_set_comma_separated() {
262 let input = vec!["10.0.0.1, 10.0.0.2, 10.0.0.5"];
263 let set = to_set(&input, None).expect("Should parse comma list");
264 assert_eq!(set.len(), 3);
265 assert!(set.contains(&IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
266 }
267
268 #[test]
269 fn parse_cidr_blocks() {
270 let input = vec!["172.16.0.0/24"];
271 let set = to_set(&input, None).expect("Should parse CIDR");
272 assert_eq!(set.len(), 256);
273 }
274
275 #[test]
276 fn parse_short_range_suffix() {
277 let input = vec!["192.168.1.250-2.10"];
278 let set = to_set(&input, None).unwrap();
279 assert_eq!(set.len(), 17);
280 }
281
282 #[test]
283 fn error_invalid_cidr() {
284 let input = vec!["192.168.1.1/33"];
285 let result = to_set(&input, None);
286 assert_eq!(result.unwrap_err(), IpParseError::InvalidPrefix(33));
287 }
288
289 #[test]
290 fn error_invalid_range_order() {
291 let input = vec!["10.0.0.10-1"];
292 let result = to_set(&input, None);
293 assert!(matches!(result, Err(IpParseError::InvalidRange(_, _))));
294 }
295
296 #[test]
297 fn empty_input_error() {
298 let input: Vec<&str> = vec!["", " "];
299 let result = to_set(&input, None);
300 assert_eq!(result.unwrap_err(), IpParseError::EmptySet);
301 }
302}