use crate::domain::Tld;
use crate::registry::Registry;
use crate::transport::{RawResponse, ResponseKind};
const COMMENT_PREFIXES: [&str; 6] = ["%", "#", ";", ">>>", "---", "--"];
const BOILERPLATE: [&str; 8] = [
"available on web at",
"find the terms and conditions",
"terms of use",
"by submitting",
"this whois information is provided",
"for more information",
"please see",
"the data in this",
];
#[derive(Debug, Clone)]
pub struct Evidence<'a> {
text: &'a str,
lowercase: String,
significant: Vec<String>,
comments: Vec<String>,
tld: &'a Tld,
registry: Option<&'a Registry>,
kind: ResponseKind,
}
impl<'a> Evidence<'a> {
pub fn new(
text: &'a str,
kind: ResponseKind,
tld: &'a Tld,
registry: Option<&'a Registry>,
) -> Self {
let lowercase = text.to_lowercase();
let is_comment = |line: &&str| {
COMMENT_PREFIXES
.iter()
.any(|prefix| line.starts_with(prefix))
};
let significant = lowercase
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.filter(|line| !is_comment(line))
.filter(|line| !BOILERPLATE.iter().any(|phrase| line.contains(phrase)))
.map(str::to_string)
.collect();
let comments = lowercase
.lines()
.map(str::trim)
.filter(is_comment)
.map(|line| {
line.trim_start_matches(['%', '#', ';', '>', '-'])
.trim()
.to_string()
})
.filter(|line| !line.is_empty())
.collect();
Evidence {
text,
lowercase,
significant,
comments,
tld,
registry,
kind,
}
}
pub fn from_response(
response: &'a RawResponse,
tld: &'a Tld,
registry: Option<&'a Registry>,
) -> Self {
Evidence::new(response.text(), response.kind(), tld, registry)
}
pub fn text(&self) -> &str {
self.text
}
pub fn lowercase(&self) -> &str {
&self.lowercase
}
pub fn significant_lines(&self) -> &[String] {
&self.significant
}
pub fn comment_lines(&self) -> &[String] {
&self.comments
}
pub fn head(&self, lines: usize) -> String {
self.lowercase
.lines()
.take(lines)
.collect::<Vec<_>>()
.join("\n")
}
pub fn tld(&self) -> &Tld {
self.tld
}
pub fn registry(&self) -> Option<&Registry> {
self.registry
}
pub fn kind(&self) -> ResponseKind {
self.kind
}
pub fn is_rdap(&self) -> bool {
self.kind == ResponseKind::RdapJson
}
pub fn contains(&self, needle: &str) -> bool {
self.lowercase.contains(needle)
}
pub fn contains_any(&self, needles: &[&str]) -> bool {
needles.iter().any(|needle| self.lowercase.contains(needle))
}
pub fn any_significant_line(&self, needle: &str) -> bool {
self.significant.iter().any(|line| line.contains(needle))
}
pub fn significant_text(&self) -> String {
self.significant.join(" ")
}
pub fn len(&self) -> usize {
self.text.trim().len()
}
pub fn is_empty(&self) -> bool {
self.text.trim().is_empty()
}
pub fn preview(&self, limit: usize) -> String {
let trimmed = self.text.trim();
let taken: String = trimmed.chars().take(limit).collect();
if trimmed.chars().count() > limit {
format!("{taken}…")
} else {
taken
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn evidence<'a>(text: &'a str, tld: &'a Tld) -> Evidence<'a> {
Evidence::new(text, ResponseKind::WhoisText, tld, None)
}
#[test]
fn comments_and_boilerplate_are_not_significant() {
let tld = Tld::parse("com").unwrap();
let record = "\
% This is a banner
# so is this
>>> Last update of WHOIS database <<<
--- separator
Domain Name: EXAMPLE.COM
This WHOIS information is provided for free by someone
Registrar: Example LLC
";
let evidence = evidence(record, &tld);
assert_eq!(
evidence.significant_lines(),
["domain name: example.com", "registrar: example llc"]
);
}
#[test]
fn banner_text_is_visible_to_contains_but_not_to_lines() {
let tld = Tld::parse("mc").unwrap();
let record = "% Lookup available on web at http://www.nic.mc\nStatus: registered\n";
let evidence = evidence(record, &tld);
assert!(evidence.contains("available on web"));
assert!(!evidence.any_significant_line("available"));
assert!(evidence.any_significant_line("registered"));
}
#[test]
fn wrapped_sentences_are_searchable_as_one_string() {
let tld = Tld::parse("hk").unwrap();
let record = "The domain has not\nbeen registered.\n";
let evidence = evidence(record, &tld);
assert!(!evidence.any_significant_line("has not been registered"));
assert!(evidence
.significant_text()
.contains("has not been registered"));
}
#[test]
fn everything_is_lowercased_once() {
let tld = Tld::parse("com").unwrap();
let evidence = evidence("No Match FOR example.COM", &tld);
assert!(evidence.contains("no match for"));
assert_eq!(
evidence.text(),
"No Match FOR example.COM",
"raw text is preserved"
);
}
#[test]
fn preview_is_truncated_on_character_boundaries() {
let tld = Tld::parse("de").unwrap();
let evidence = evidence("Müncheners are here", &tld);
assert_eq!(evidence.preview(3), "Mün…");
assert_eq!(evidence.preview(1000), "Müncheners are here");
}
#[test]
fn length_ignores_surrounding_whitespace() {
let tld = Tld::parse("com").unwrap();
assert_eq!(evidence(" abc ", &tld).len(), 3);
assert!(evidence(" \n\t ", &tld).is_empty());
}
}