use std::fmt;
use crate::error::{Error, Result};
use crate::transport::RawResponse;
mod dates;
mod keyvalue;
mod record;
#[cfg(feature = "rdap")]
mod rdap;
pub use dates::parse_datetime;
pub use keyvalue::KeyValueParser;
pub use record::{Contact, WhoisRecord};
#[cfg(feature = "rdap")]
pub use rdap::RdapParser;
pub trait RecordParser: fmt::Debug + Send + Sync {
fn name(&self) -> &'static str;
fn can_parse(&self, response: &RawResponse) -> bool;
fn parse(&self, response: &RawResponse) -> Result<WhoisRecord>;
}
impl<T: RecordParser + ?Sized> RecordParser for std::sync::Arc<T> {
fn name(&self) -> &'static str {
(**self).name()
}
fn can_parse(&self, response: &RawResponse) -> bool {
(**self).can_parse(response)
}
fn parse(&self, response: &RawResponse) -> Result<WhoisRecord> {
(**self).parse(response)
}
}
#[derive(Debug, Default)]
pub struct CompositeParser {
parsers: Vec<Box<dyn RecordParser>>,
}
impl CompositeParser {
pub fn new() -> Self {
CompositeParser {
parsers: Vec::new(),
}
}
pub fn standard() -> Self {
let composite = CompositeParser::new();
#[cfg(feature = "rdap")]
let composite = composite.parser(RdapParser::new());
composite.parser(KeyValueParser::new())
}
pub fn parser(mut self, parser: impl RecordParser + 'static) -> Self {
self.parsers.push(Box::new(parser));
self
}
pub fn boxed_parser(mut self, parser: Box<dyn RecordParser>) -> Self {
self.parsers.push(parser);
self
}
pub fn len(&self) -> usize {
self.parsers.len()
}
pub fn is_empty(&self) -> bool {
self.parsers.is_empty()
}
pub fn parser_names(&self) -> Vec<&'static str> {
self.parsers.iter().map(|parser| parser.name()).collect()
}
pub fn parse_all(&self, responses: &[RawResponse]) -> Result<WhoisRecord> {
let mut merged = WhoisRecord::new();
let mut parsed_any = false;
let mut last_error = None;
for response in responses {
match self.parse(response) {
Ok(record) => {
parsed_any = true;
merge_into(&mut merged, record);
}
Err(error) => last_error = Some(error),
}
}
match (parsed_any, last_error) {
(true, _) => Ok(merged),
(false, Some(error)) => Err(error),
(false, None) => Ok(merged),
}
}
}
impl RecordParser for CompositeParser {
fn name(&self) -> &'static str {
"composite"
}
fn can_parse(&self, response: &RawResponse) -> bool {
self.parsers.iter().any(|parser| parser.can_parse(response))
}
fn parse(&self, response: &RawResponse) -> Result<WhoisRecord> {
for parser in &self.parsers {
if parser.can_parse(response) {
return parser.parse(response);
}
}
Err(Error::Definitions(format!(
"no parser handles a {:?} response from {}",
response.kind(),
response.endpoint().address()
)))
}
}
fn merge_into(target: &mut WhoisRecord, source: WhoisRecord) {
fn fill<T>(target: &mut Option<T>, source: Option<T>) {
if target.is_none() {
*target = source;
}
}
fill(&mut target.domain, source.domain);
fill(&mut target.registry_id, source.registry_id);
fill(&mut target.registrar, source.registrar);
fill(&mut target.registrar_iana_id, source.registrar_iana_id);
fill(
&mut target.registrar_whois_server,
source.registrar_whois_server,
);
fill(&mut target.registrar_url, source.registrar_url);
fill(&mut target.abuse_contact_email, source.abuse_contact_email);
fill(&mut target.abuse_contact_phone, source.abuse_contact_phone);
fill(&mut target.created, source.created);
fill(&mut target.updated, source.updated);
fill(&mut target.expires, source.expires);
fill(&mut target.dnssec, source.dnssec);
fill(&mut target.registrant, source.registrant);
fill(&mut target.admin, source.admin);
fill(&mut target.tech, source.tech);
fill(&mut target.billing, source.billing);
for status in source.statuses {
if !target.statuses.contains(&status) {
target.statuses.push(status);
}
}
for server in source.name_servers {
if !target.name_servers.contains(&server) {
target.name_servers.push(server);
}
}
for (key, values) in source.extra {
target.extra.entry(key).or_insert(values);
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::registry::Endpoint;
use crate::transport::ResponseKind;
fn whois(text: &str) -> RawResponse {
RawResponse::new(
Endpoint::whois("whois.example"),
ResponseKind::WhoisText,
text,
Duration::ZERO,
)
}
#[cfg(feature = "rdap")]
fn rdap(json: &str) -> RawResponse {
RawResponse::new(
Endpoint::rdap("https://rdap.example/"),
ResponseKind::RdapJson,
json,
Duration::ZERO,
)
}
#[test]
fn the_standard_composite_handles_whois_text() {
let parser = CompositeParser::standard();
let response = whois("Domain Name: example.com\nRegistrar: Example LLC\n");
assert!(parser.can_parse(&response));
let record = parser.parse(&response).unwrap();
assert_eq!(record.registrar.as_deref(), Some("Example LLC"));
}
#[cfg(feature = "rdap")]
#[test]
fn the_standard_composite_handles_rdap_json() {
let parser = CompositeParser::standard();
let response = rdap(r#"{"objectClassName":"domain","ldhName":"example.com"}"#);
assert!(parser.can_parse(&response));
assert_eq!(
parser.parse(&response).unwrap().domain.unwrap().as_ascii(),
"example.com"
);
assert_eq!(parser.parser_names(), ["rdap", "key-value"]);
}
#[test]
fn an_empty_composite_claims_nothing() {
let parser = CompositeParser::new();
let response = whois("Domain Name: example.com\n");
assert!(parser.is_empty());
assert!(!parser.can_parse(&response));
assert!(parser.parse(&response).is_err());
}
#[test]
fn a_thin_registry_answer_is_completed_by_the_referral() {
let registry = whois(
"\
Domain Name: EXAMPLE.COM
Registrar: Example Registrar, LLC
Registrar WHOIS Server: whois.example-registrar.com
Registry Expiry Date: 2027-08-13T04:00:00Z
Name Server: NS1.EXAMPLE.COM
",
);
let registrar = whois(
"\
Domain Name: EXAMPLE.COM
Registrant Name: Ada Lovelace
Registrant Country: GB
Expiry Date: 2020-01-01T00:00:00Z
Name Server: NS2.EXAMPLE.COM
",
);
let merged = CompositeParser::standard()
.parse_all(&[registry, registrar])
.unwrap();
assert_eq!(merged.registrar.as_deref(), Some("Example Registrar, LLC"));
assert_eq!(
merged.registrant.as_ref().unwrap().name.as_deref(),
Some("Ada Lovelace")
);
assert_eq!(
merged.expires.unwrap().format("%Y").to_string(),
"2027",
"the registry's date is authoritative and must not be overwritten"
);
assert_eq!(
merged.name_servers,
["ns1.example.com", "ns2.example.com"],
"multi-valued fields accumulate"
);
}
#[test]
fn parse_all_of_nothing_is_an_empty_record() {
let merged = CompositeParser::standard().parse_all(&[]).unwrap();
assert!(merged.is_empty());
}
#[test]
fn parse_all_reports_a_failure_only_when_everything_failed() {
let parser = CompositeParser::new().parser(KeyValueParser::new());
let unparseable = RawResponse::new(
Endpoint::rdap("https://rdap.example/"),
ResponseKind::RdapJson,
"{}",
Duration::ZERO,
);
assert!(parser.parse_all(&[unparseable]).is_err());
let unparseable = RawResponse::new(
Endpoint::rdap("https://rdap.example/"),
ResponseKind::RdapJson,
"{}",
Duration::ZERO,
);
let good = whois("Domain Name: example.com\n");
let merged = parser.parse_all(&[unparseable, good]).unwrap();
assert!(merged.domain.is_some());
}
#[test]
fn unrecognised_extra_fields_merge_without_clobbering() {
let first = whois("Eligibility Type: Company\n");
let second = whois("Eligibility Type: Other\nEligibility Name: Example Pty\n");
let merged = CompositeParser::standard()
.parse_all(&[first, second])
.unwrap();
assert_eq!(merged.extra_field("eligibility type"), Some("Company"));
assert_eq!(merged.extra_field("eligibility name"), Some("Example Pty"));
}
}