///|
fn parse_domain(
input : String,
) -> Result[(Array[String], Bool, String), DomainError] {
if input.length() == 0 {
return Err(EmptyDomain)
}
let trailing_dot = input.has_suffix(".")
let body = if trailing_dot { input[:input.length() - 1] } else { input[:] }
if body.length() == 0 {
return Err(EmptyDomain)
}
let labels : Array[String] = []
let mut label_index = 0
for part in body.split(".") {
if part.length() == 0 {
return Err(EmptyLabel(label_index))
}
for ch in part {
if is_rule_whitespace(ch) || ch == '*' || ch == '!' || ch == '/' {
return Err(InvalidDomainCharacter(label_index, ch))
}
}
labels.push(part.to_lower().to_owned())
label_index = label_index + 1
}
let normalized = labels.join(".") + (if trailing_dot { "." } else { "" })
Ok((labels, trailing_dot, normalized))
}
///|
fn joined_suffix(
labels : Array[String],
count : Int,
trailing_dot : Bool,
) -> String {
let text = labels[labels.length() - count:].join(".")
text + (if trailing_dot { "." } else { "" })
}
///|
fn exact_rule_text(labels : Array[String], depth : Int) -> String {
labels[labels.length() - depth:].join(".")
}
///|
fn eligible_section(
sections : Array[RuleSection],
scope : SectionScope,
) -> RuleSection? {
for section in sections {
if section == IcannSection {
return Some(IcannSection)
}
}
if scope == IcannAndPrivate {
for section in sections {
if section == PrivateSection {
return Some(PrivateSection)
}
}
}
for section in sections {
if section == UnsectionedRule {
return Some(UnsectionedRule)
}
}
None
}
///|
/// Apply the PSL prevailing-rule algorithm to a hostname.
///
/// This method performs case normalization, not IDNA conversion. The hostname
/// and parsed list must already use the same U-label or A-label representation.
pub fn SuffixList::lookup(
self : SuffixList,
domain : String,
) -> Result[Lookup, DomainError] {
self.lookup_with_options(domain, LookupOptions::browser_default())
}
///|
/// Apply the PSL prevailing-rule algorithm with explicit section and
/// unknown-suffix policies.
///
/// Unsectioned rules remain eligible in both scopes so compact and custom rule
/// sets keep their expected behavior. This method performs case normalization,
/// not IDNA conversion.
pub fn SuffixList::lookup_with_options(
self : SuffixList,
domain : String,
options : LookupOptions,
) -> Result[Lookup, DomainError] {
let (labels, trailing_dot, normalized_domain) = match parse_domain(domain) {
Ok(value) => value
Err(error) => return Err(error)
}
let mut node_index = 0
let mut depth = 0
let mut best_length = 1
let mut best_kind = DefaultRule
let mut best_rule = "*"
let mut best_section : RuleSection? = None
let mut exception_length = 0
let mut exception_rule = ""
let mut exception_section : RuleSection? = None
for i = labels.length() - 1; i >= 0; i = i - 1 {
let next_index = match self.children[node_index].get(labels[i]) {
Some(index) => index
None => break
}
node_index = next_index
depth = depth + 1
match eligible_section(self.exact_sections[node_index], options.scope) {
Some(section) =>
if depth >= best_length {
best_length = depth
best_kind = ExactRule
best_rule = exact_rule_text(labels, depth)
best_section = Some(section)
}
None => ()
}
match eligible_section(self.wildcard_sections[node_index], options.scope) {
Some(section) =>
if i > 0 && depth + 1 > best_length {
best_length = depth + 1
best_kind = WildcardRule
best_rule = "*." + exact_rule_text(labels, depth)
best_section = Some(section)
}
None => ()
}
match eligible_section(self.exception_sections[node_index], options.scope) {
Some(section) =>
if depth > exception_length {
exception_length = depth
exception_rule = "!" + exact_rule_text(labels, depth)
exception_section = Some(section)
}
None => ()
}
}
if best_kind == DefaultRule &&
exception_length == 0 &&
options.unknown_suffix == RequireListedSuffix {
return Err(UnlistedSuffix(normalized_domain))
}
let (suffix_length, rule_kind, matched_rule, rule_section) = if exception_length >
0 {
(exception_length - 1, ExceptionRule, exception_rule, exception_section)
} else {
(best_length, best_kind, best_rule, best_section)
}
let public_suffix = joined_suffix(labels, suffix_length, trailing_dot)
let registrable_domain = if labels.length() > suffix_length {
Some(joined_suffix(labels, suffix_length + 1, trailing_dot))
} else {
None
}
Ok({
normalized_domain,
public_suffix,
registrable_domain,
matched_rule,
rule_kind,
rule_section,
})
}
///|
pub fn SuffixList::public_suffix(
self : SuffixList,
domain : String,
) -> Result[String, DomainError] {
match self.lookup(domain) {
Ok(result) => Ok(result.public_suffix())
Err(error) => Err(error)
}
}
///|
pub fn SuffixList::registrable_domain(
self : SuffixList,
domain : String,
) -> Result[String?, DomainError] {
match self.lookup(domain) {
Ok(result) => Ok(result.registrable_domain())
Err(error) => Err(error)
}
}
///|
pub fn SuffixList::is_public_suffix(
self : SuffixList,
domain : String,
) -> Result[Bool, DomainError] {
match self.lookup(domain) {
Ok(result) => Ok(result.registrable_domain() is None)
Err(error) => Err(error)
}
}