rmap/parsers/
targets.rs

1use ipnet::IpNet;
2use std::net::IpAddr;
3use std::path::PathBuf;
4
5/// Represents a parsed target that can be used for scanning
6#[derive(Debug, Clone)]
7pub enum Target {
8    /// Single IP address
9    IpAddr(IpAddr),
10    /// IP network (CIDR notation)
11    IpNet(IpNet),
12    /// Hostname (DNS resolution will be handled separately)
13    Hostname(String),
14    /// File path containing targets
15    File(PathBuf),
16}
17
18/// Error types that can occur during target parsing
19#[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
34/// Parser for various target formats
35pub struct TargetParser {
36    /// Whether to allow hostname resolution
37    allow_hostnames: bool,
38    /// Whether to allow file paths
39    allow_files: bool,
40}
41
42impl TargetParser {
43    /// Create a new target parser with default settings
44    pub fn new() -> Self {
45        Self {
46            allow_hostnames: true,
47            allow_files: true,
48        }
49    }
50
51    /// Create a new target parser with custom settings
52    pub fn with_settings(allow_hostnames: bool, allow_files: bool) -> Self {
53        Self {
54            allow_hostnames,
55            allow_files,
56        }
57    }
58
59    /// Parse a target string into a Target enum
60    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        // Try to parse as IP address first
68        if let Ok(ip) = input.parse::<IpAddr>() {
69            return Ok(Target::IpAddr(ip));
70        }
71
72        // Try to parse as IP network (CIDR) - check this before file paths
73        if input.contains('/') {
74            if let Ok(net) = input.parse::<IpNet>() {
75                return Ok(Target::IpNet(net));
76            }
77        }
78
79        // Check if it's a file path (only if it contains path separators or has specific extensions)
80        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        // Try to parse as hostname
95        if self.allow_hostnames {
96            // Basic hostname validation
97            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    /// Parse multiple targets from a string (comma or space separated)
112    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    /// Parse targets from a file
128    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    /// Extract all IP addresses from a target
159    pub fn extract_ips(&self, target: &Target) -> Vec<IpAddr> {
160        match target {
161            Target::IpAddr(ip) => vec![*ip],
162            Target::IpNet(net) => {
163                // For now, return the network address
164                // In a real implementation, you might want to enumerate all addresses
165                vec![net.addr().into()]
166            }
167            Target::Hostname(_) => {
168                // Hostnames need DNS resolution to get IPs
169                vec![]
170            }
171            Target::File(_) => {
172                // Files need to be processed separately
173                vec![]
174            }
175        }
176    }
177
178    /// Check if a target is IPv6
179    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, // Need DNS resolution to determine
184            Target::File(_) => false,     // Need to process file contents
185        }
186    }
187
188    /// Get all targets from a file
189    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
206/// Convenience function to parse a single target
207pub fn parse_target(input: &str) -> Result<Target, TargetParseError> {
208    let parser = TargetParser::new();
209    parser.parse(input)
210}
211
212/// Convenience function to parse multiple targets
213pub 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}