use std::{collections::BTreeSet, error::Error, fmt};
use dnspls_domain::DomainName;
use serde::{Deserialize, Serialize};
use crate::{
DnsOutcome, EvidenceFact, Observation, ObservationId, ProviderAuthority, ProviderStatus,
RdapOutcome, TemporalState, UnixMillis, ZoneMembership,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerdictStatus {
Registrable,
RegistryPremium,
AftermarketFixed,
AftermarketOffer,
Auction,
Reserved,
Registered,
Unsupported,
Pending,
Unknown,
Disputed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceTier {
None,
LocalSnapshot,
NetworkSignal,
PublicRegistrationData,
ProviderReported,
RegistrationCapable,
RegistryEpp,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionReason {
FreshRegistryResult,
FreshRegistrationCapableResult,
ProviderReportOnly,
RdapObjectFound,
ZonePresenceObserved,
DnsDelegationObserved,
AbsenceRequiresPreciseVerification,
NoDecisiveEvidence,
ContradictoryEvidence,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContradictionKind {
RegistrationVsRegistrable,
PreciseProviderDisagreement,
ReportedProviderDisagreement,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Contradiction {
kind: ContradictionKind,
observation_ids: Vec<ObservationId>,
}
impl Contradiction {
pub const fn kind(&self) -> ContradictionKind {
self.kind
}
pub fn observation_ids(&self) -> &[ObservationId] {
&self.observation_ids
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Verdict {
domain: DomainName,
status: VerdictStatus,
evidence_tier: EvidenceTier,
reason: DecisionReason,
can_attempt_registration: bool,
considered_observation_ids: Vec<ObservationId>,
decisive_observation_ids: Vec<ObservationId>,
contradictions: Vec<Contradiction>,
}
impl Verdict {
pub const fn domain(&self) -> &DomainName {
&self.domain
}
pub const fn status(&self) -> VerdictStatus {
self.status
}
pub const fn evidence_tier(&self) -> EvidenceTier {
self.evidence_tier
}
pub const fn reason(&self) -> DecisionReason {
self.reason
}
pub const fn can_attempt_registration(&self) -> bool {
self.can_attempt_registration
}
pub fn considered_observation_ids(&self) -> &[ObservationId] {
&self.considered_observation_ids
}
pub fn decisive_observation_ids(&self) -> &[ObservationId] {
&self.decisive_observation_ids
}
pub fn contradictions(&self) -> &[Contradiction] {
&self.contradictions
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReductionError {
observation_id: ObservationId,
}
impl ReductionError {
pub const fn observation_id(&self) -> &ObservationId {
&self.observation_id
}
}
impl fmt::Display for ReductionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"observation {} belongs to a different domain",
self.observation_id
)
}
}
impl Error for ReductionError {}
pub fn reduce(
domain: &DomainName,
observations: &[Observation],
now: UnixMillis,
) -> Result<Verdict, ReductionError> {
validate_domains(domain, observations)?;
let considered = observations
.iter()
.map(|observation| observation.id().clone())
.collect::<Vec<_>>();
let evidence = FreshEvidence::collect(observations, now);
if let Some(verdict) = find_contradiction(
domain,
&considered,
&evidence.precise,
&evidence.reported,
&evidence.rdap_found,
) {
return Ok(verdict);
}
Ok(reduce_consistent(domain, &considered, &evidence))
}
struct FreshEvidence<'observation> {
all: Vec<&'observation Observation>,
precise: Vec<(&'observation Observation, ProviderAuthority, ProviderStatus)>,
reported: Vec<(&'observation Observation, ProviderStatus)>,
rdap_found: Vec<&'observation Observation>,
}
impl<'observation> FreshEvidence<'observation> {
fn collect(observations: &'observation [Observation], now: UnixMillis) -> Self {
let all = observations
.iter()
.filter(|observation| observation.window().state_at(now) == TemporalState::Fresh)
.collect::<Vec<_>>();
let precise = all
.iter()
.copied()
.filter_map(|observation| match observation.fact() {
EvidenceFact::ProviderCheck {
authority, status, ..
} if *authority >= ProviderAuthority::RegistrationCapable => {
Some((observation, *authority, *status))
}
_ => None,
})
.collect();
let reported = all
.iter()
.copied()
.filter_map(|observation| match observation.fact() {
EvidenceFact::ProviderCheck {
authority: ProviderAuthority::Reported,
status,
..
} => Some((observation, *status)),
_ => None,
})
.collect();
let rdap_found = all
.iter()
.copied()
.filter(|observation| {
matches!(
observation.fact(),
EvidenceFact::Rdap {
outcome: RdapOutcome::ObjectFound
}
)
})
.collect();
Self {
all,
precise,
reported,
rdap_found,
}
}
}
fn validate_domains(
domain: &DomainName,
observations: &[Observation],
) -> Result<(), ReductionError> {
if let Some(observation) = observations
.iter()
.find(|observation| observation.domain() != domain)
{
return Err(ReductionError {
observation_id: observation.id().clone(),
});
}
Ok(())
}
fn reduce_consistent(
domain: &DomainName,
considered: &[ObservationId],
evidence: &FreshEvidence<'_>,
) -> Verdict {
precise_verdict(domain, considered, &evidence.precise)
.or_else(|| rdap_verdict(domain, considered, &evidence.rdap_found))
.or_else(|| reported_verdict(domain, considered, &evidence.reported))
.or_else(|| presence_verdict(domain, considered, &evidence.all))
.unwrap_or_else(|| unknown_verdict(domain, considered, &evidence.all))
}
fn precise_verdict(
domain: &DomainName,
considered: &[ObservationId],
evidence: &[(&Observation, ProviderAuthority, ProviderStatus)],
) -> Option<Verdict> {
let (observation, authority, status) = strongest_precise(evidence)?;
let reason = if authority == ProviderAuthority::RegistryEpp {
DecisionReason::FreshRegistryResult
} else {
DecisionReason::FreshRegistrationCapableResult
};
Some(make_verdict(
domain,
status.into(),
authority_to_tier(authority),
reason,
is_registration_attempt(status),
considered.to_vec(),
vec![observation.id().clone()],
Vec::new(),
))
}
fn rdap_verdict(
domain: &DomainName,
considered: &[ObservationId],
evidence: &[&Observation],
) -> Option<Verdict> {
(!evidence.is_empty()).then(|| {
make_verdict(
domain,
VerdictStatus::Registered,
EvidenceTier::PublicRegistrationData,
DecisionReason::RdapObjectFound,
false,
considered.to_vec(),
ids(evidence),
Vec::new(),
)
})
}
fn reported_verdict(
domain: &DomainName,
considered: &[ObservationId],
evidence: &[(&Observation, ProviderStatus)],
) -> Option<Verdict> {
let (observation, status) = evidence.first().copied()?;
let status = if is_registration_attempt(status) {
VerdictStatus::Unknown
} else {
status.into()
};
Some(make_verdict(
domain,
status,
EvidenceTier::ProviderReported,
DecisionReason::ProviderReportOnly,
false,
considered.to_vec(),
vec![observation.id().clone()],
Vec::new(),
))
}
fn presence_verdict(
domain: &DomainName,
considered: &[ObservationId],
evidence: &[&Observation],
) -> Option<Verdict> {
let (observation, tier, reason) =
evidence
.iter()
.copied()
.find_map(|observation| match observation.fact() {
EvidenceFact::ZoneSnapshot {
membership: ZoneMembership::Present,
} => Some((
observation,
EvidenceTier::LocalSnapshot,
DecisionReason::ZonePresenceObserved,
)),
EvidenceFact::Dns {
outcome: DnsOutcome::Delegated,
..
} => Some((
observation,
EvidenceTier::NetworkSignal,
DecisionReason::DnsDelegationObserved,
)),
_ => None,
})?;
Some(make_verdict(
domain,
VerdictStatus::Registered,
tier,
reason,
false,
considered.to_vec(),
vec![observation.id().clone()],
Vec::new(),
))
}
fn unknown_verdict(
domain: &DomainName,
considered: &[ObservationId],
evidence: &[&Observation],
) -> Verdict {
let has_absence = evidence.iter().any(|observation| {
matches!(
observation.fact(),
EvidenceFact::ZoneSnapshot {
membership: ZoneMembership::Absent
} | EvidenceFact::Dns {
outcome: DnsOutcome::NameError,
..
} | EvidenceFact::Rdap {
outcome: RdapOutcome::ObjectNotFound
}
)
});
let (tier, reason) = if has_absence {
(
EvidenceTier::NetworkSignal,
DecisionReason::AbsenceRequiresPreciseVerification,
)
} else {
(EvidenceTier::None, DecisionReason::NoDecisiveEvidence)
};
make_verdict(
domain,
VerdictStatus::Unknown,
tier,
reason,
false,
considered.to_vec(),
Vec::new(),
Vec::new(),
)
}
fn find_contradiction(
domain: &DomainName,
considered: &[ObservationId],
precise: &[(&Observation, ProviderAuthority, ProviderStatus)],
reported: &[(&Observation, ProviderStatus)],
rdap_found: &[&Observation],
) -> Option<Verdict> {
let precise_statuses = precise
.iter()
.map(|(_, _, status)| *status)
.collect::<BTreeSet<_>>();
if precise_statuses.len() > 1 {
let evidence = precise
.iter()
.map(|(observation, _, _)| *observation)
.collect::<Vec<_>>();
return Some(disputed(
domain,
considered,
ContradictionKind::PreciseProviderDisagreement,
precise
.iter()
.map(|(_, authority, _)| authority_to_tier(*authority))
.max_by_key(|tier| evidence_tier_rank(*tier))
.unwrap_or(EvidenceTier::RegistrationCapable),
&evidence,
));
}
let reported_statuses = reported
.iter()
.map(|(_, status)| status_category(*status))
.collect::<BTreeSet<_>>();
if reported_statuses.len() > 1 {
let evidence = reported
.iter()
.map(|(observation, _)| *observation)
.collect::<Vec<_>>();
return Some(disputed(
domain,
considered,
ContradictionKind::ReportedProviderDisagreement,
EvidenceTier::ProviderReported,
&evidence,
));
}
let availability_claims = precise
.iter()
.filter_map(|(observation, _, status)| {
is_registration_attempt(*status).then_some(*observation)
})
.chain(reported.iter().filter_map(|(observation, status)| {
is_registration_attempt(*status).then_some(*observation)
}))
.collect::<Vec<_>>();
if !rdap_found.is_empty() && !availability_claims.is_empty() {
let mut evidence = availability_claims;
evidence.extend(rdap_found.iter().copied());
return Some(disputed(
domain,
considered,
ContradictionKind::RegistrationVsRegistrable,
precise
.iter()
.map(|(_, authority, _)| authority_to_tier(*authority))
.max_by_key(|tier| evidence_tier_rank(*tier))
.unwrap_or(EvidenceTier::PublicRegistrationData),
&evidence,
));
}
None
}
fn disputed(
domain: &DomainName,
considered: &[ObservationId],
kind: ContradictionKind,
evidence_tier: EvidenceTier,
evidence: &[&Observation],
) -> Verdict {
let decisive = ids(evidence);
make_verdict(
domain,
VerdictStatus::Disputed,
evidence_tier,
DecisionReason::ContradictoryEvidence,
false,
considered.to_vec(),
decisive.clone(),
vec![Contradiction {
kind,
observation_ids: decisive,
}],
)
}
const fn authority_to_tier(authority: ProviderAuthority) -> EvidenceTier {
match authority {
ProviderAuthority::Reported => EvidenceTier::ProviderReported,
ProviderAuthority::RegistrationCapable => EvidenceTier::RegistrationCapable,
ProviderAuthority::RegistryEpp => EvidenceTier::RegistryEpp,
}
}
const fn evidence_tier_rank(tier: EvidenceTier) -> u8 {
match tier {
EvidenceTier::None => 0,
EvidenceTier::LocalSnapshot => 1,
EvidenceTier::NetworkSignal => 2,
EvidenceTier::ProviderReported => 3,
EvidenceTier::PublicRegistrationData => 4,
EvidenceTier::RegistrationCapable => 5,
EvidenceTier::RegistryEpp => 6,
}
}
fn strongest_precise<'observation>(
precise: &[(&'observation Observation, ProviderAuthority, ProviderStatus)],
) -> Option<(&'observation Observation, ProviderAuthority, ProviderStatus)> {
precise
.iter()
.copied()
.max_by_key(|(_, authority, _)| *authority)
}
fn ids(observations: &[&Observation]) -> Vec<ObservationId> {
observations
.iter()
.map(|observation| observation.id().clone())
.collect()
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum StatusCategory {
Acquisition,
Unavailable,
Indeterminate,
}
const fn status_category(status: ProviderStatus) -> StatusCategory {
match status {
ProviderStatus::Registrable
| ProviderStatus::RegistryPremium
| ProviderStatus::AftermarketFixed
| ProviderStatus::AftermarketOffer
| ProviderStatus::Auction => StatusCategory::Acquisition,
ProviderStatus::Reserved | ProviderStatus::Registered => StatusCategory::Unavailable,
ProviderStatus::Unsupported | ProviderStatus::Pending | ProviderStatus::Unknown => {
StatusCategory::Indeterminate
}
}
}
const fn is_registration_attempt(status: ProviderStatus) -> bool {
matches!(
status,
ProviderStatus::Registrable | ProviderStatus::RegistryPremium
)
}
impl From<ProviderStatus> for VerdictStatus {
fn from(value: ProviderStatus) -> Self {
match value {
ProviderStatus::Registrable => Self::Registrable,
ProviderStatus::RegistryPremium => Self::RegistryPremium,
ProviderStatus::AftermarketFixed => Self::AftermarketFixed,
ProviderStatus::AftermarketOffer => Self::AftermarketOffer,
ProviderStatus::Auction => Self::Auction,
ProviderStatus::Reserved => Self::Reserved,
ProviderStatus::Registered => Self::Registered,
ProviderStatus::Unsupported => Self::Unsupported,
ProviderStatus::Pending => Self::Pending,
ProviderStatus::Unknown => Self::Unknown,
}
}
}
#[allow(clippy::too_many_arguments)]
fn make_verdict(
domain: &DomainName,
status: VerdictStatus,
evidence_tier: EvidenceTier,
reason: DecisionReason,
can_attempt_registration: bool,
considered_observation_ids: Vec<ObservationId>,
decisive_observation_ids: Vec<ObservationId>,
contradictions: Vec<Contradiction>,
) -> Verdict {
Verdict {
domain: domain.clone(),
status,
evidence_tier,
reason,
can_attempt_registration,
considered_observation_ids,
decisive_observation_ids,
contradictions,
}
}