1use ipnet::IpNet;
2use std::net::IpAddr;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone)]
7pub enum Target {
8 IpAddr(IpAddr),
10 IpNet(IpNet),
12 Hostname(String),
14 File(PathBuf),
16}
17
18#[derive(Debug, thiserror::Error)]
20pub enum TargetParseError {
21 #[error("Failed to parse IP address: {0}")]
22 IpAddrParse(std::net::AddrParseError),
23
24 #[error("Failed to parse IP network: {0}")]
25 IpNetParse(ipnet::AddrParseError),
26
27 #[error("File does not exist: {0}")]
28 FileNotFound(PathBuf),
29
30 #[error("Invalid target format: {0}")]
31 InvalidFormat(String),
32}
33
34pub struct TargetParser {
36 allow_hostnames: bool,
38 allow_files: bool,
40}
41
42impl TargetParser {
43 pub fn new() -> Self {
45 Self {
46 allow_hostnames: true,
47 allow_files: true,
48 }
49 }
50
51 pub fn with_settings(allow_hostnames: bool, allow_files: bool) -> Self {
53 Self {
54 allow_hostnames,
55 allow_files,
56 }
57 }
58
59 pub fn parse(&self, input: &str) -> Result<Target, TargetParseError> {
61 let input = input.trim();
62
63 if input.is_empty() {
64 return Err(TargetParseError::InvalidFormat("Empty target".to_string()));
65 }
66
67 if let Ok(ip) = input.parse::<IpAddr>() {
69 return Ok(Target::IpAddr(ip));
70 }
71
72 if input.contains('/') {
74 if let Ok(net) = input.parse::<IpNet>() {
75 return Ok(Target::IpNet(net));
76 }
77 }
78
79 if self.allow_files
81 && (input.contains('/')
82 || input.contains('\\')
83 || input.ends_with(".txt")
84 || input.ends_with(".csv"))
85 {
86 let path = PathBuf::from(input);
87 if path.exists() {
88 return Ok(Target::File(path));
89 } else {
90 return Err(TargetParseError::FileNotFound(path));
91 }
92 }
93
94 if self.allow_hostnames {
96 if input
98 .chars()
99 .all(|c| c.is_alphanumeric() || c == '.' || c == '-')
100 {
101 return Ok(Target::Hostname(input.to_string()));
102 }
103 }
104
105 Err(TargetParseError::InvalidFormat(format!(
106 "Could not parse target: {}",
107 input
108 )))
109 }
110
111 pub fn parse_multiple(&self, input: &str) -> Result<Vec<Target>, TargetParseError> {
113 let targets: Vec<&str> = input
114 .split(|c: char| c == ',' || c.is_whitespace())
115 .filter(|s| !s.is_empty())
116 .collect();
117
118 let mut results = Vec::new();
119 for target in targets {
120 let parsed = self.parse(target)?;
121 results.push(parsed);
122 }
123
124 Ok(results)
125 }
126
127 pub async fn parse_file(&self, path: &PathBuf) -> Result<Vec<Target>, TargetParseError> {
129 use tokio::fs::File;
130 use tokio::io::{AsyncBufReadExt, BufReader};
131
132 let file = File::open(path)
133 .await
134 .map_err(|_| TargetParseError::FileNotFound(path.clone()))?;
135
136 let reader = BufReader::new(file);
137 let mut lines = reader.lines();
138 let mut targets = Vec::new();
139
140 while let Some(line) = lines
141 .next_line()
142 .await
143 .map_err(|_| TargetParseError::InvalidFormat("Failed to read line".to_string()))?
144 {
145 let line = line.trim();
146
147 if line.is_empty() || line.starts_with('#') {
148 continue;
149 }
150
151 let target = self.parse(line)?;
152 targets.push(target);
153 }
154
155 Ok(targets)
156 }
157
158 pub fn extract_ips(&self, target: &Target) -> Vec<IpAddr> {
160 match target {
161 Target::IpAddr(ip) => vec![*ip],
162 Target::IpNet(net) => {
163 vec![net.addr().into()]
166 }
167 Target::Hostname(_) => {
168 vec![]
170 }
171 Target::File(_) => {
172 vec![]
174 }
175 }
176 }
177
178 pub fn is_ipv6(&self, target: &Target) -> bool {
180 match target {
181 Target::IpAddr(ip) => matches!(ip, IpAddr::V6(_)),
182 Target::IpNet(net) => matches!(net.addr(), IpAddr::V6(_)),
183 Target::Hostname(_) => false, Target::File(_) => false, }
186 }
187
188 pub async fn get_targets_from_file(
190 &self,
191 target: &Target,
192 ) -> Result<Vec<Target>, TargetParseError> {
193 match target {
194 Target::File(path) => self.parse_file(path).await,
195 _ => Ok(vec![target.clone()]),
196 }
197 }
198}
199
200impl Default for TargetParser {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206pub fn parse_target(input: &str) -> Result<Target, TargetParseError> {
208 let parser = TargetParser::new();
209 parser.parse(input)
210}
211
212pub fn parse_targets(input: &str) -> Result<Vec<Target>, TargetParseError> {
214 let parser = TargetParser::new();
215 parser.parse_multiple(input)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use std::net::Ipv4Addr;
222
223 #[test]
224 fn test_parse_ipv4() {
225 let parser = TargetParser::new();
226 let result = parser.parse("192.168.1.1").unwrap();
227
228 match result {
229 Target::IpAddr(ip) => {
230 assert!(matches!(ip, IpAddr::V4(_)));
231 assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
232 }
233 _ => panic!("Expected IpAddr variant"),
234 }
235 }
236
237 #[test]
238 fn test_parse_ipv6() {
239 let parser = TargetParser::new();
240 let result = parser.parse("2001:db8::1").unwrap();
241
242 match result {
243 Target::IpAddr(ip) => {
244 assert!(matches!(ip, IpAddr::V6(_)));
245 }
246 _ => panic!("Expected IpAddr variant"),
247 }
248 }
249
250 #[test]
251 fn test_parse_network() {
252 let parser = TargetParser::new();
253 let result = parser.parse("192.168.1.0/24").unwrap();
254
255 match result {
256 Target::IpNet(net) => {
257 assert!(matches!(net.addr(), IpAddr::V4(_)));
258 assert_eq!(net.prefix_len(), 24);
259 }
260 _ => panic!("Expected IpNet variant"),
261 }
262 }
263
264 #[test]
265 fn test_parse_hostname() {
266 let parser = TargetParser::new();
267 let result = parser.parse("example.com").unwrap();
268
269 match result {
270 Target::Hostname(name) => {
271 assert_eq!(name, "example.com");
272 }
273 _ => panic!("Expected Hostname variant"),
274 }
275 }
276
277 #[test]
278 fn test_parse_multiple() {
279 let parser = TargetParser::new();
280 let result = parser.parse_multiple("192.168.1.1, 10.0.0.1").unwrap();
281
282 assert_eq!(result.len(), 2);
283 assert!(matches!(result[0], Target::IpAddr(_)));
284 assert!(matches!(result[1], Target::IpAddr(_)));
285 }
286}