1mod bootstrap;
2mod client;
3mod types;
4
5pub use client::RdapClient;
6pub use types::{ContactInfo, RdapResponse};
7
8use std::net::IpAddr;
9
10use crate::error::{Result, SeerError};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum RdapRoute {
18 Ip(IpAddr),
20 Asn(u32),
22 Domain(String),
24}
25
26pub fn classify(query: &str) -> Result<RdapRoute> {
41 let trimmed = query.trim();
42 if trimmed.is_empty() {
43 return Err(SeerError::InvalidInput("empty RDAP query".to_string()));
44 }
45
46 if let Ok(ip) = trimmed.parse::<IpAddr>() {
48 return Ok(RdapRoute::Ip(ip));
49 }
50
51 let upper = trimmed.to_uppercase();
55 if let Some(rest) = upper.strip_prefix("AS") {
56 if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) && !trimmed.contains('.') {
57 let asn: u32 = rest
58 .parse()
59 .map_err(|_| SeerError::InvalidInput(format!("invalid ASN: {query}")))?;
60 return Ok(RdapRoute::Asn(asn));
61 }
62 }
63
64 Ok(RdapRoute::Domain(trimmed.to_string()))
66}
67
68pub async fn auto_lookup(client: &RdapClient, query: &str) -> Result<RdapResponse> {
76 match classify(query)? {
77 RdapRoute::Ip(_ip) => client.lookup_ip(query.trim()).await,
78 RdapRoute::Asn(asn) => client.lookup_asn(asn).await,
79 RdapRoute::Domain(domain) => client.lookup_domain(&domain).await,
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn classifies_ipv4() {
89 assert!(matches!(classify("8.8.8.8").unwrap(), RdapRoute::Ip(_)));
90 assert!(matches!(classify("1.1.1.1").unwrap(), RdapRoute::Ip(_)));
91 }
92
93 #[test]
94 fn classifies_ipv6() {
95 assert!(matches!(
96 classify("2606:4700:4700::1111").unwrap(),
97 RdapRoute::Ip(_)
98 ));
99 }
100
101 #[test]
102 fn classifies_asn_upper() {
103 assert_eq!(classify("AS1234").unwrap(), RdapRoute::Asn(1234));
104 }
105
106 #[test]
107 fn classifies_asn_lower() {
108 assert_eq!(classify("as1234").unwrap(), RdapRoute::Asn(1234));
109 }
110
111 #[test]
112 fn classifies_asn_mixed_case() {
113 assert_eq!(classify("As15169").unwrap(), RdapRoute::Asn(15169));
114 }
115
116 #[test]
117 fn as_prefix_domain_routes_to_domain() {
118 assert_eq!(
121 classify("as1234.io").unwrap(),
122 RdapRoute::Domain("as1234.io".to_string())
123 );
124 assert_eq!(
125 classify("AS1234.IO").unwrap(),
126 RdapRoute::Domain("AS1234.IO".to_string())
127 );
128 }
129
130 #[test]
131 fn asn_with_trailing_junk_routes_to_domain() {
132 assert_eq!(
134 classify("AS1234x").unwrap(),
135 RdapRoute::Domain("AS1234x".to_string())
136 );
137 }
138
139 #[test]
140 fn bare_as_routes_to_domain() {
141 assert_eq!(classify("AS").unwrap(), RdapRoute::Domain("AS".to_string()));
143 }
144
145 #[test]
146 fn normal_domain_routes_to_domain() {
147 assert_eq!(
148 classify("example.com").unwrap(),
149 RdapRoute::Domain("example.com".to_string())
150 );
151 }
152
153 #[test]
154 fn trims_whitespace() {
155 assert_eq!(classify(" AS64500 ").unwrap(), RdapRoute::Asn(64500));
156 assert_eq!(
157 classify(" example.com ").unwrap(),
158 RdapRoute::Domain("example.com".to_string())
159 );
160 }
161
162 #[test]
163 fn empty_query_errors() {
164 assert!(matches!(
165 classify("").unwrap_err(),
166 SeerError::InvalidInput(_)
167 ));
168 assert!(matches!(
169 classify(" ").unwrap_err(),
170 SeerError::InvalidInput(_)
171 ));
172 }
173
174 #[test]
175 fn asn_overflow_errors() {
176 assert!(matches!(
178 classify("AS99999999999999").unwrap_err(),
179 SeerError::InvalidInput(_)
180 ));
181 }
182}