Skip to content

Commit 6a6fd7d

Browse files
Showfomclaude
andcommitted
Release v1.0.0 with clippy fixes and duplicate query fix
- Bump version to 1.0.0 - Add clippy pedantic and nursery lints configuration - Fix all clippy warnings (format string inlining, Self usage, etc.) - Fix duplicate RDAP query when referral points to same server - Skip registrar referral if host matches registry server Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 77d0282 commit 6a6fd7d

18 files changed

Lines changed: 176 additions & 155 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
[package]
22
name = "rdap"
3-
version = "0.2.1"
3+
version = "1.0.0"
44
edition = "2024"
55
authors = ["xTom", "Akaere Networks"]
66
description = "A modern RDAP (Registration Data Access Protocol) client"
77
license = "MIT"
88
homepage = "https://github.com/xtomcom/rdap"
99
repository = "https://github.com/xtomcom/rdap"
1010

11+
[lints.clippy]
12+
pedantic = { level = "warn", priority = -1 }
13+
nursery = { level = "warn", priority = -1 }
14+
# Allow these common patterns
15+
module_name_repetitions = "allow"
16+
must_use_candidate = "allow"
17+
return_self_not_must_use = "allow"
18+
missing_errors_doc = "allow"
19+
missing_panics_doc = "allow"
20+
redundant_pub_crate = "allow"
21+
too_many_lines = "allow"
22+
struct_excessive_bools = "allow"
23+
unused_self = "allow"
24+
cast_possible_truncation = "allow"
25+
items_after_statements = "allow"
26+
if_not_else = "allow"
27+
branches_sharing_code = "allow"
28+
assigning_clones = "allow"
29+
1130
[dependencies]
1231
# HTTP client
1332
reqwest = { version = "0.13", features = ["json", "rustls"] }

src/bootstrap.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,20 @@ impl BootstrapClient {
4545
QueryType::Tld => {
4646
// TLD queries always go to IANA RDAP
4747
let url = Url::parse(config::IANA_RDAP_URL)
48-
.map_err(|e| RdapError::Bootstrap(format!("Invalid IANA RDAP URL: {}", e)))?;
48+
.map_err(|e| RdapError::Bootstrap(format!("Invalid IANA RDAP URL: {e}")))?;
4949
Ok(vec![url])
5050
}
5151
QueryType::Domain => {
5252
// Priority: tlds.json first, then bootstrap
5353
if let Some(url) = config::lookup_tld_override(&self.tld_overrides, &request.query)
5454
{
55-
log::debug!("Found TLD override for {}: {}", request.query, url);
55+
log::debug!("Found TLD override for {}: {url}", request.query);
5656
return Ok(vec![url]);
5757
}
5858

5959
// Fall back to IANA bootstrap
6060
let registry = self.fetch_registry(&self.config.bootstrap.dns).await?;
61-
self.match_domain(&registry, &request.query)
61+
Ok(self.match_domain(&registry, &request.query))
6262
}
6363
QueryType::Ip => {
6464
let bootstrap_url = if request.query.contains(':') {
@@ -74,24 +74,24 @@ impl BootstrapClient {
7474
self.match_asn(&registry, &request.query)
7575
}
7676
QueryType::Entity => Err(RdapError::Bootstrap(
77-
"Entity queries require explicit server (-s/--server)".to_string(),
77+
"Entity queries require explicit server (-s/--server)".to_owned(),
7878
)),
7979
_ => Err(RdapError::Bootstrap(
80-
"This query type requires explicit server (-s/--server)".to_string(),
80+
"This query type requires explicit server (-s/--server)".to_owned(),
8181
)),
8282
}
8383
}
8484

8585
/// Fetch bootstrap registry file from URL
8686
async fn fetch_registry(&self, url: &str) -> Result<BootstrapRegistry> {
87-
log::debug!("Fetching bootstrap registry: {}", url);
87+
log::debug!("Fetching bootstrap registry: {url}");
8888

8989
let response = self.http_client.get(url).send().await?;
9090

9191
if !response.status().is_success() {
92+
let status = response.status();
9293
return Err(RdapError::Bootstrap(format!(
93-
"Failed to fetch registry: HTTP {}",
94-
response.status()
94+
"Failed to fetch registry: HTTP {status}"
9595
)));
9696
}
9797

@@ -100,7 +100,7 @@ impl BootstrapClient {
100100
}
101101

102102
/// Match domain name
103-
fn match_domain(&self, registry: &BootstrapRegistry, domain: &str) -> Result<Vec<Url>> {
103+
fn match_domain(&self, registry: &BootstrapRegistry, domain: &str) -> Vec<Url> {
104104
let domain = domain.trim_end_matches('.').to_lowercase();
105105

106106
// Build lookup map
@@ -111,7 +111,7 @@ impl BootstrapClient {
111111
{
112112
let url_strings: Vec<String> = urls
113113
.iter()
114-
.filter_map(|v| v.as_str().map(|s| s.to_string()))
114+
.filter_map(|v| v.as_str().map(std::borrow::ToOwned::to_owned))
115115
.collect();
116116

117117
for entry in entries {
@@ -128,34 +128,34 @@ impl BootstrapClient {
128128
while !parts.is_empty() {
129129
let test_domain = parts.join(".");
130130
if let Some(urls) = map.get(&test_domain) {
131-
return Ok(urls.iter().filter_map(|s| Url::parse(s).ok()).collect());
131+
return urls.iter().filter_map(|s| Url::parse(s).ok()).collect();
132132
}
133133
parts.remove(0);
134134
}
135135

136-
Ok(vec![])
136+
vec![]
137137
}
138138

139139
/// Match IP address (supports standard IPs, shorthand IPs, and CIDR)
140140
fn match_ip(&self, registry: &BootstrapRegistry, ip_query: &str) -> Result<Vec<Url>> {
141141
// Normalize the IP (handles shorthand like 1.1 -> 1.0.0.1)
142142
let normalized = ip::normalize_ip(ip_query)
143-
.ok_or_else(|| RdapError::InvalidQuery(format!("Invalid IP address: {}", ip_query)))?;
143+
.ok_or_else(|| RdapError::InvalidQuery(format!("Invalid IP address: {ip_query}")))?;
144144

145145
// Extract the IP part (for CIDR queries like 8.8.8.0/24, we match using the network address)
146146
let ip_str = ip::extract_ip_from_cidr(&normalized);
147147

148148
let addr: IpAddr = ip_str
149149
.parse()
150-
.map_err(|_| RdapError::InvalidQuery(format!("Invalid IP address: {}", ip_str)))?;
150+
.map_err(|_| RdapError::InvalidQuery(format!("Invalid IP address: {ip_str}")))?;
151151

152152
for service in &registry.services {
153153
if service.len() >= 2
154154
&& let (Some(entries), Some(urls)) = (service[0].as_array(), service[1].as_array())
155155
{
156156
for entry in entries {
157157
if let Some(cidr) = entry.as_str()
158-
&& self.ip_in_network(&addr, cidr)
158+
&& Self::ip_in_network(&addr, cidr)
159159
{
160160
let url_list: Vec<Url> = urls
161161
.iter()
@@ -171,7 +171,7 @@ impl BootstrapClient {
171171
}
172172

173173
/// Check if IP is in CIDR network using ipnet
174-
fn ip_in_network(&self, addr: &IpAddr, cidr: &str) -> bool {
174+
fn ip_in_network(addr: &IpAddr, cidr: &str) -> bool {
175175
if let Ok(network) = cidr.parse::<IpNet>() {
176176
return network.contains(addr);
177177
}
@@ -188,15 +188,15 @@ impl BootstrapClient {
188188
};
189189
let asn: u32 = asn_str
190190
.parse()
191-
.map_err(|_| RdapError::InvalidQuery(format!("Invalid AS number: {}", asn_str)))?;
191+
.map_err(|_| RdapError::InvalidQuery(format!("Invalid AS number: {asn_str}")))?;
192192

193193
for service in &registry.services {
194194
if service.len() >= 2
195195
&& let (Some(entries), Some(urls)) = (service[0].as_array(), service[1].as_array())
196196
{
197197
for entry in entries {
198198
if let Some(range_str) = entry.as_str()
199-
&& self.asn_in_range(asn, range_str)
199+
&& Self::asn_in_range(asn, range_str)
200200
{
201201
let url_list: Vec<Url> = urls
202202
.iter()
@@ -212,7 +212,7 @@ impl BootstrapClient {
212212
}
213213

214214
/// Check if AS number is in range
215-
fn asn_in_range(&self, asn: u32, range_str: &str) -> bool {
215+
fn asn_in_range(asn: u32, range_str: &str) -> bool {
216216
if let Some(dash_pos) = range_str.find('-') {
217217
// Range: "1000-2000"
218218
if let (Ok(start), Ok(end)) = (

src/cache.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ pub struct Cache {
1414
impl Cache {
1515
/// Create a new cache (~/.cache/rdap/ on all platforms)
1616
pub fn new() -> Result<Self> {
17-
let cache_dir = std::env::var("HOME")
18-
.map(|h| PathBuf::from(h).join(".cache/rdap"))
19-
.unwrap_or_else(|_| PathBuf::from(".cache/rdap"));
17+
let cache_dir = std::env::var("HOME").map_or_else(|_| PathBuf::from(".cache/rdap"), |h| PathBuf::from(h).join(".cache/rdap"));
2018

2119
fs::create_dir_all(&cache_dir)?;
2220

@@ -27,7 +25,7 @@ impl Cache {
2725
}
2826

2927
/// Set cache TTL
30-
pub fn with_ttl(mut self, ttl: Duration) -> Self {
28+
pub const fn with_ttl(mut self, ttl: Duration) -> Self {
3129
self.ttl = ttl;
3230
self
3331
}
@@ -46,7 +44,7 @@ impl Cache {
4644
&& let Ok(elapsed) = SystemTime::now().duration_since(modified)
4745
&& elapsed > self.ttl
4846
{
49-
log::debug!("Cache expired for {}", key);
47+
log::debug!("Cache expired for {key}");
5048
let _ = fs::remove_file(&path);
5149
return None;
5250
}

src/client.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ impl RdapClient {
4848
}
4949

5050
/// Set timeout
51-
pub fn with_timeout(mut self, timeout: Duration) -> Self {
51+
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
5252
self.timeout = timeout;
5353
self
5454
}
5555

5656
/// Enable or disable following registrar referrals (default: enabled)
57-
pub fn with_follow_referral(mut self, follow: bool) -> Self {
57+
pub const fn with_follow_referral(mut self, follow: bool) -> Self {
5858
self.follow_referral = follow;
5959
self
6060
}
@@ -85,7 +85,7 @@ impl RdapClient {
8585
for base_url in &urls {
8686
let url = request.build_url(base_url)?;
8787

88-
log::debug!("Querying RDAP server: {}", url);
88+
log::debug!("Querying RDAP server: {url}");
8989

9090
match self.fetch_rdap(&url).await {
9191
Ok(obj) => {
@@ -95,7 +95,21 @@ impl RdapClient {
9595
&& let RdapObject::Domain(ref domain) = obj
9696
&& let Some(registrar_rdap_url) = self.extract_registrar_rdap_url(domain)
9797
{
98-
log::debug!("Following registrar referral: {}", registrar_rdap_url);
98+
// Skip if referral points to the same server (same host)
99+
if Self::is_same_server(&url, &registrar_rdap_url) {
100+
log::debug!(
101+
"Skipping referral: same server as registry ({})",
102+
registrar_rdap_url.host_str().unwrap_or("unknown")
103+
);
104+
return Ok(RdapQueryResult {
105+
registry: obj,
106+
registry_url: url,
107+
registrar: None,
108+
registrar_url: None,
109+
});
110+
}
111+
112+
log::debug!("Following registrar referral: {registrar_rdap_url}");
99113
match self.fetch_rdap(&registrar_rdap_url).await {
100114
Ok(registrar_obj) => {
101115
return Ok(RdapQueryResult {
@@ -106,7 +120,7 @@ impl RdapClient {
106120
});
107121
}
108122
Err(e) => {
109-
log::warn!("Failed to fetch registrar data: {}", e);
123+
log::warn!("Failed to fetch registrar data: {e}");
110124
// Continue with registry-only result
111125
}
112126
}
@@ -120,7 +134,7 @@ impl RdapClient {
120134
}
121135
Err(RdapError::NotFound) => return Err(RdapError::NotFound),
122136
Err(e) => {
123-
log::warn!("Server {} failed: {}", url, e);
137+
log::warn!("Server {url} failed: {e}");
124138
last_error = Some(e);
125139
}
126140
}
@@ -129,6 +143,11 @@ impl RdapClient {
129143
Err(last_error.unwrap_or(RdapError::NoWorkingServers))
130144
}
131145

146+
/// Check if two URLs point to the same server (same host)
147+
fn is_same_server(url1: &Url, url2: &Url) -> bool {
148+
url1.host() == url2.host()
149+
}
150+
132151
/// Extract registrar RDAP URL from domain response
133152
fn extract_registrar_rdap_url(&self, domain: &Domain) -> Option<Url> {
134153
// Look for a link with rel="related" and type containing "rdap"
@@ -200,7 +219,7 @@ impl RdapClient {
200219
description: err_obj.description,
201220
})
202221
} else {
203-
Err(RdapError::Other(format!("HTTP error: {}", status)))
222+
Err(RdapError::Other(format!("HTTP error: {status}")))
204223
}
205224
}
206225
}

src/config.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ impl Default for CacheConfig {
7878

7979
/// Get the user config directory path (~/.config/rdap/)
8080
pub fn user_config_dir() -> Result<PathBuf> {
81-
let dir = std::env::var("HOME")
82-
.map(|h| PathBuf::from(h).join(".config/rdap"))
83-
.unwrap_or_else(|_| PathBuf::from(".config/rdap"));
81+
let dir = std::env::var("HOME").map_or_else(|_| PathBuf::from(".config/rdap"), |h| PathBuf::from(h).join(".config/rdap"));
8482
Ok(dir)
8583
}
8684

@@ -90,7 +88,7 @@ pub fn system_config_dir() -> PathBuf {
9088
}
9189

9290
impl Config {
93-
/// Get the config directory path (alias for user_config_dir)
91+
/// Get the config directory path (alias for `user_config_dir`)
9492
pub fn config_dir() -> Result<PathBuf> {
9593
user_config_dir()
9694
}
@@ -132,7 +130,7 @@ impl Config {
132130

133131
// Fall back to built-in default
134132
log::debug!("Using built-in config");
135-
let config: Config = serde_json::from_str(BUILTIN_CONFIG)?;
133+
let config: Self = serde_json::from_str(BUILTIN_CONFIG)?;
136134
Ok(config)
137135
}
138136

@@ -265,11 +263,11 @@ pub async fn update_configs() -> Result<UpdateResult> {
265263
result.config_error = Some("Invalid config.json format".to_string());
266264
}
267265
}
268-
Err(e) => result.config_error = Some(format!("Failed to read response: {}", e)),
266+
Err(e) => result.config_error = Some(format!("Failed to read response: {e}")),
269267
}
270268
}
271269
Ok(response) => result.config_error = Some(format!("HTTP {}", response.status())),
272-
Err(e) => result.config_error = Some(format!("Request failed: {}", e)),
270+
Err(e) => result.config_error = Some(format!("Request failed: {e}")),
273271
}
274272

275273
// Update tlds.json
@@ -287,11 +285,11 @@ pub async fn update_configs() -> Result<UpdateResult> {
287285
result.tlds_error = Some("Invalid tlds.json format".to_string());
288286
}
289287
}
290-
Err(e) => result.tlds_error = Some(format!("Failed to read response: {}", e)),
288+
Err(e) => result.tlds_error = Some(format!("Failed to read response: {e}")),
291289
}
292290
}
293291
Ok(response) => result.tlds_error = Some(format!("HTTP {}", response.status())),
294-
Err(e) => result.tlds_error = Some(format!("Request failed: {}", e)),
292+
Err(e) => result.tlds_error = Some(format!("Request failed: {e}")),
295293
}
296294

297295
// Update tlds.txt (IANA TLD list)
@@ -314,11 +312,11 @@ pub async fn update_configs() -> Result<UpdateResult> {
314312
result.tld_list_error = Some("Invalid tlds.txt format".to_string());
315313
}
316314
}
317-
Err(e) => result.tld_list_error = Some(format!("Failed to read response: {}", e)),
315+
Err(e) => result.tld_list_error = Some(format!("Failed to read response: {e}")),
318316
}
319317
}
320318
Ok(response) => result.tld_list_error = Some(format!("HTTP {}", response.status())),
321-
Err(e) => result.tld_list_error = Some(format!("Request failed: {}", e)),
319+
Err(e) => result.tld_list_error = Some(format!("Request failed: {e}")),
322320
}
323321

324322
Ok(result)

0 commit comments

Comments
 (0)