use crate::support::dns;
#[cfg(any(test, feature = "dev-tools"))]
mod compile {
use super::*;
struct Node {
label: String,
kind: NodeKind,
children: Vec<Node>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum NodeKind {
Branch,
OrgParent,
OrgGrandparent,
}
pub(super) fn compile(psl: &str) -> String {
let tree = build_tree(psl);
let mut data = String::new();
for node in tree {
to_text(&mut data, &node);
}
data
}
fn build_tree(psl: &str) -> Vec<Node> {
#[derive(Clone, Copy, PartialEq, Eq)]
enum LineKind {
Normal,
Wildcard,
Exception,
}
let mut nodes = Vec::<Node>::new();
for line in psl.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") {
continue;
}
let (unicode_name, line_kind) = if line.starts_with('!') {
(&line[1..], LineKind::Exception)
} else if line.starts_with("*.") {
(&line[2..], LineKind::Wildcard)
} else {
(line, LineKind::Normal)
};
let domain = dns::Name::from_utf8(unicode_name)
.unwrap()
.to_ascii()
.to_lowercase();
let num_labels = domain.split('.').count();
let mut search = &mut nodes;
for (i, label) in domain.rsplit('.').enumerate() {
let new_node_kind = if i + 1 < num_labels {
NodeKind::Branch
} else {
match line_kind {
LineKind::Normal => NodeKind::OrgParent,
LineKind::Wildcard => NodeKind::OrgGrandparent,
LineKind::Exception => NodeKind::Branch,
}
};
let existing_ix =
match search.iter_mut().position(|n| n.label == label) {
Some(ix) => ix,
None => {
let ix = search.len();
search.push(Node {
label: label.to_owned(),
kind: new_node_kind,
children: Vec::new(),
});
ix
},
};
search = &mut search[existing_ix].children;
}
}
sort_nodes(&mut nodes);
nodes
}
fn sort_nodes(nodes: &mut [Node]) {
nodes.sort_unstable_by(|a, b| a.label.cmp(&b.label));
for node in nodes {
sort_nodes(&mut node.children);
}
}
fn to_text(s: &mut String, node: &Node) {
use std::fmt::Write as _;
s.push_str(&node.label);
if !node.children.is_empty() || NodeKind::OrgGrandparent == node.kind {
let mut children = String::new();
for child in &node.children {
to_text(&mut children, child);
}
children.push('\n');
s.push(' ');
match node.kind {
NodeKind::Branch => {},
NodeKind::OrgParent => s.push('.'),
NodeKind::OrgGrandparent => s.push('*'),
}
s.push(' ');
let _ = write!(s, "{}\n", children.len());
s.push_str(&children);
} else {
s.push('\n');
}
}
}
#[cfg(feature = "dev-tools")]
pub mod cli {
use std::fs;
use std::path::Path;
use super::*;
pub fn compile_psl(infile: &Path, outfile: &Path) {
use chrono::prelude::*;
let src_data = fs::read(infile).unwrap();
let compiled = compile::compile(&String::from_utf8(src_data).unwrap());
let compiled = format!(
"\
# This is a compiled representation of the Mozilla Public Suffix
# List, which can be found here:
# https://publicsuffix.org/list/public_suffix_list.dat
#
# This Data is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
# Compiled on {date}.
{compiled}",
date = Utc::now(),
);
fs::write(outfile, compiled.as_bytes()).unwrap();
}
}
static PSL_DATA: &str = include_str!("psl.txt");
pub fn organisational_domain(domain: &dns::Name) -> dns::Name {
eval(PSL_DATA, domain)
}
fn eval(mut psl_data: &str, domain: &dns::Name) -> dns::Name {
let domain_str = domain.to_ascii().to_lowercase();
#[cfg(test)]
{
if "smtpin.sptest.lin.gl" == domain_str {
return domain.clone();
}
}
let mut best_match = None::<dns::Name>;
while psl_data.starts_with('#') {
psl_data = psl_data.split_once('\n').unwrap().1;
}
let mut under_wildcard = false;
for (i, label) in domain_str.rsplit('.').enumerate() {
let found_match = loop {
let Some((line, rest)) = psl_data.split_once('\n') else {
break None;
};
psl_data = rest;
if line.is_empty() {
break None;
}
let (branch_label, info) =
line.split_once(' ').unwrap_or((line, ""));
if branch_label > label {
break None;
}
if branch_label < label {
if let Some((_, len_str)) = info.split_once(' ') {
let len = len_str.parse::<usize>().unwrap();
psl_data = &psl_data[len..];
}
continue;
}
break Some(info);
};
let Some(info) = found_match else {
break;
};
let match_labels = if info.starts_with('.') || info.is_empty() {
if under_wildcard {
Some(i + 1)
} else {
Some(i + 2)
}
} else if info.starts_with('*') {
under_wildcard = true;
Some(i + 3)
} else {
None
};
if let Some(match_labels) = match_labels {
if match_labels <= usize::from(domain.num_labels()) {
best_match = Some(domain.trim_to(match_labels));
}
}
if info.is_empty() {
break;
}
}
best_match.unwrap_or_else(|| {
domain.trim_to(2usize.min(usize::from(domain.num_labels())))
})
}
#[cfg(test)]
mod test {
use super::*;
fn dn(s: &str) -> dns::Name {
dns::Name::from_ascii(s).unwrap()
}
#[test]
fn psl_examples() {
let psl = compile::compile(
"com\n\
*.foo.com\n\
*.jp\n\
*.hokkaido.jp\n\
*.tokyo.jp\n\
!pref.hokkaido.jp\n\
!metro.tokyo.jp\n",
);
println!("{}", psl);
assert_eq!(dn("bar.com"), eval(&psl, &dn("bar.com")));
assert_eq!(dn("bar.com"), eval(&psl, &dn("mail.bar.com")));
assert_eq!(dn("foo.com"), eval(&psl, &dn("foo.com")));
assert_eq!(dn("foo.com"), eval(&psl, &dn("bar.foo.com")));
assert_eq!(
dn("example.bar.foo.com"),
eval(&psl, &dn("example.bar.foo.com")),
);
assert_eq!(
dn("foo.bar.hokkaido.jp"),
eval(&psl, &dn("foo.bar.hokkaido.jp")),
);
assert_eq!(
dn("foo.bar.hokkaido.jp"),
eval(&psl, &dn("mail.foo.bar.hokkaido.jp")),
);
assert_eq!(dn("pref.hokkaido.jp"), eval(&psl, &dn("pref.hokkaido.jp")));
assert_eq!(
dn("pref.hokkaido.jp"),
eval(&psl, &dn("mail.pref.hokkaido.jp")),
);
assert_eq!(dn("foo.xyz"), eval(&psl, &dn("foo.xyz")));
assert_eq!(dn("foo.xyz"), eval(&psl, &dn("mail.foo.xyz")));
assert_eq!(dn("xyz"), eval(&psl, &dn("xyz")));
}
#[test]
fn test_organisational_domain() {
assert_eq!(dn("foo.com"), organisational_domain(&dn("foo.com")));
assert_eq!(dn("foo.com"), organisational_domain(&dn("BAR.FOO.com")));
assert_eq!(dn("foo.co.uk"), organisational_domain(&dn("foo.co.uk")));
assert_eq!(
dn("foo.co.uk"),
organisational_domain(&dn("mail.foo.co.uk"))
);
assert_eq!(dn("z.zzz"), organisational_domain(&dn("z.z.zzz")));
assert_eq!(
dn("foo.kyoto.jp"),
organisational_domain(&dn("mail.foo.kyoto.jp")),
);
assert_eq!(
dn("foo.xii.jp"),
organisational_domain(&dn("mail.foo.xii.jp")),
);
assert_eq!(
dns::Name::from_utf8("foo.京都.jp").unwrap(),
organisational_domain(
&dns::Name::from_utf8("mail.foo.京都.jp").unwrap()
),
);
}
}