use super::*;
use crate::output::{availability_label, lookup_source};
impl HumanFormatter {
pub(super) fn format_lookup(&self, result: &LookupResult) -> String {
let domain = result
.domain_name()
.unwrap_or_else(|| "Unknown".to_string());
let header_suffix = match result {
LookupResult::Rdap { .. } => "via RDAP".to_string(),
LookupResult::Whois { .. } => "via WHOIS".to_string(),
LookupResult::Available { data, .. } => {
availability_label(data.verdict()).0.to_lowercase()
}
};
let mut output = vec![self.header(&format!(
"Lookup: {} ({})",
sanitize_line(&domain),
header_suffix
))];
let mut rows = self.rows(&mut output, " ");
let source = lookup_source(result);
let source = match result {
LookupResult::Rdap { .. } => self.success(source),
_ => self.warning(source),
};
rows.kv("Source", source);
match result {
LookupResult::Rdap {
data,
whois_fallback,
} => {
rows.opt("Registrar", &data.get_registrar());
rows.opt("Registrant", &data.get_registrant());
rows.opt("Organization", &data.get_registrant_organization());
let infos = contact::rdap_contacts(data);
let rdap_contacts = contact::rdap_detail_views(&infos);
rows.contacts(rdap_contacts);
rows.date("Created", data.creation_date());
rows.expires(data.expiration_date());
rows.list("Status", &data.status);
rows.list("Nameservers", &data.nameserver_names());
if data.is_dnssec_signed() {
rows.kv("DNSSEC", self.success("signed"));
}
if let Some(whois) = whois_fallback {
let mut extra = Vec::new();
let mut fill = self.rows(&mut extra, " ");
if data.get_registrant().is_none() {
fill.opt("Registrant", &whois.registrant);
}
if data.get_registrant_organization().is_none() {
fill.opt("Organization", &whois.organization);
}
let fallback = contact::ROLES
.into_iter()
.zip(rdap_contacts)
.zip(whois.contacts());
for ((role, rdap), whois_contact) in fallback {
if rdap.is_empty() {
fill.contact(role, whois_contact);
}
}
fill.date("Updated", whois.updated_date);
if !data.is_dnssec_signed() {
fill.opt("DNSSEC", &whois.dnssec);
}
if !whois.whois_server.is_empty() {
fill.text("WHOIS Server", &whois.whois_server);
}
if !extra.is_empty() {
rows.push(format!("\n {}", self.label("Additional WHOIS Data:")));
rows.extend(extra);
}
}
}
LookupResult::Whois {
data, rdap_error, ..
} => {
if let Some(error) = rdap_error {
rows.kv("RDAP Error", self.error(&sanitize_line(error)));
}
rows.opt("Registrar", &data.registrar);
rows.opt("Registrant", &data.registrant);
rows.opt("Organization", &data.organization);
rows.contacts(data.contacts());
rows.date("Created", data.creation_date);
rows.expires(data.expiration_date);
rows.list("Status", &data.status);
rows.list("Nameservers", &data.nameservers);
rows.opt("DNSSEC", &data.dnssec);
}
LookupResult::Available {
data,
rdap_error,
whois_error,
whois_data,
} => {
let (verdict, emphasis) = availability_label(data.verdict());
rows.kv("Verdict", self.emphasize(verdict, emphasis));
rows.kv("Confidence", self.confidence(&data.confidence));
rows.text("Method", &data.method);
rows.opt("Details", &data.details);
if !rdap_error.is_empty() {
rows.kv("RDAP Error", self.error(&sanitize_line(rdap_error)));
}
if !whois_error.is_empty() {
rows.kv("WHOIS Error", self.error(&sanitize_line(whois_error)));
}
if let Some(w) = whois_data {
let mut extra = Vec::new();
let mut fill = self.rows(&mut extra, " ");
if !w.nameservers.is_empty() {
fill.text("Nameservers", &w.nameservers.join(", "));
}
if !w.status.is_empty() {
fill.text("Status", &w.status.join(", "));
}
fill.opt("DNSSEC", &w.dnssec);
if !w.whois_server.is_empty() {
fill.text("WHOIS Server", &w.whois_server);
}
if !extra.is_empty() {
rows.push(format!("\n {}", self.label("Additional WHOIS Data:")));
rows.extend(extra);
}
}
}
}
output.join("\n")
}
pub(super) fn format_availability(
&self,
result: &crate::availability::AvailabilityResult,
) -> String {
let status = if result.available {
self.success("AVAILABLE")
} else {
self.error("TAKEN")
};
let mut output = vec![format!("{}: {}", sanitize_line(&result.domain), status)];
let mut rows = self.rows(&mut output, " ");
rows.kv("Confidence", self.confidence(&result.confidence));
rows.text("Method", &result.method);
rows.opt("Details", &result.details);
output.join("\n")
}
fn confidence(&self, confidence: &str) -> String {
match confidence {
"high" => self.success(confidence),
"medium" => self.warning(confidence),
_ => self.error(confidence),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lookup::LookupResult;
fn formatter() -> HumanFormatter {
HumanFormatter::new().without_colors()
}
fn availability_lookup(available: bool, confidence: &str) -> LookupResult {
LookupResult::Available {
data: Box::new(crate::availability::AvailabilityResult {
domain: "myroyalcanin.lv".to_string(),
available,
confidence: confidence.to_string(),
method: "whois".to_string(),
details: None,
}),
rdap_error: "bootstrap failed".to_string(),
whois_error: String::new(),
whois_data: None,
}
}
#[test]
fn format_lookup_registered_high_confidence_does_not_say_available() {
let f = formatter();
let out = f.format_lookup(&availability_lookup(false, "high"));
assert!(
!out.contains("AVAILABLE"),
"must not claim available:\n{}",
out
);
assert!(
out.contains("REGISTERED"),
"must render REGISTERED:\n{}",
out
);
assert!(
out.contains("(registered)"),
"header suffix must say registered:\n{}",
out
);
}
#[test]
fn format_lookup_available_high_confidence_still_says_available() {
let f = formatter();
let out = f.format_lookup(&availability_lookup(true, "high"));
assert!(
out.contains("AVAILABLE"),
"high-confidence available:\n{}",
out
);
assert!(out.contains("(available)"), "header suffix:\n{}", out);
}
#[test]
fn format_lookup_likely_registered_medium_confidence() {
let f = formatter();
let out = f.format_lookup(&availability_lookup(false, "medium"));
assert!(out.contains("LIKELY REGISTERED"), "medium reg:\n{}", out);
assert!(
!out.contains("MAY BE AVAILABLE"),
"must not say MAY BE AVAILABLE:\n{}",
out
);
}
#[test]
fn format_lookup_sanitizes_protocol_error_strings() {
let evil = "connect to whois.evil\x1b]52;c;AAAA\x07\x1b[2J failed";
let whois = WhoisResponse::parse("example.com", "whois.test", "Registrar: R\n");
let whois_variant = LookupResult::Whois {
data: whois,
rdap_error: Some(evil.to_string()),
rdap_fallback: None,
};
let available_variant = LookupResult::Available {
data: Box::new(crate::availability::AvailabilityResult {
domain: "example.com".to_string(),
available: false,
confidence: "low".to_string(),
method: "none".to_string(),
details: None,
}),
rdap_error: evil.to_string(),
whois_error: evil.to_string(),
whois_data: None,
};
for result in [whois_variant, available_variant] {
let out = formatter().format_lookup(&result);
assert!(!out.contains('\x1b'), "ESC reached terminal: {out:?}");
assert!(!out.contains('\x07'), "BEL reached terminal: {out:?}");
assert!(
out.contains("connect to whois.evil failed"),
"error text kept: {out:?}"
);
}
}
#[test]
fn format_lookup_whois_fallback_keeps_organization_only_contacts() {
let mut whois = WhoisResponse::parse("example.com", "whois.test", "Registrar: R\n");
whois.admin_organization = Some("Admin Org LLC".to_string());
whois.tech_organization = Some("Tech Org LLC".to_string());
let rdap: RdapResponse =
serde_json::from_value(serde_json::json!({"ldhName": "example.com"})).unwrap();
let result = LookupResult::Rdap {
data: Box::new(rdap),
whois_fallback: Some(whois),
};
let out = formatter().format_lookup(&result);
for needle in [
"Admin Contact",
"Admin Org LLC",
"Tech Contact",
"Tech Org LLC",
] {
assert!(out.contains(needle), "missing {needle:?}:\n{out}");
}
}
fn rdap_registrant_identity_only() -> RdapResponse {
serde_json::from_value(serde_json::json!({
"ldhName": "example.com",
"entities": [{
"objectClassName": "entity",
"roles": ["registrant"],
"vcardArray": ["vcard", [
["fn", {}, "text", "Jane Registrant"],
["org", {}, "text", "Example LLC"]
]]
}]
}))
.unwrap()
}
#[test]
fn rdap_registrant_identity_only_opens_no_empty_contact_heading() {
let out = formatter().format_rdap(&rdap_registrant_identity_only());
assert!(out.contains("Registrant: Jane Registrant"), "got:\n{out}");
assert!(
!out.contains("Registrant Contact"),
"empty heading rendered:\n{out}"
);
}
#[test]
fn format_lookup_falls_back_to_whois_registrant_details() {
let mut whois = WhoisResponse::parse("example.com", "whois.test", "Registrar: R\n");
whois.registrant_email = Some("owner@example.com".to_string());
let result = LookupResult::Rdap {
data: Box::new(rdap_registrant_identity_only()),
whois_fallback: Some(whois),
};
let out = formatter().format_lookup(&result);
assert_eq!(out.matches("Registrant Contact").count(), 1, "got:\n{out}");
assert!(out.contains("Email: owner@example.com"), "got:\n{out}");
}
#[test]
fn format_lookup_rdap_admin_and_tech_show_postal_details() {
let entities = ["administrative", "technical"].map(|role| {
serde_json::json!({
"objectClassName": "entity",
"roles": [role],
"vcardArray": ["vcard", [
["fn", {}, "text", format!("{role} person")],
["adr", {}, "text", ["", "", "1 Main St", "Springfield", "", "", "US"]]
]]
})
});
let rdap: RdapResponse = serde_json::from_value(serde_json::json!({
"ldhName": "example.com",
"entities": entities,
}))
.unwrap();
let result = LookupResult::Rdap {
data: Box::new(rdap),
whois_fallback: None,
};
let out = formatter().format_lookup(&result);
assert_eq!(
out.matches("Address: 1 Main St, Springfield, US").count(),
2,
"got:\n{out}"
);
assert_eq!(out.matches("Country: US").count(), 2, "got:\n{out}");
}
#[test]
fn domain_info_verdict_registered_for_high_confidence_unavailable() {
let lookup = availability_lookup(false, "high");
let info = crate::domain_info::DomainInfo::from_lookup_result(&lookup);
assert_eq!(info.availability_verdict.as_deref(), Some("registered"));
}
}