slicc-discovery 1.42.2

Discovery engine for agent-facing web standards
Documentation
pub struct IgnoreList {
    rules: Vec<IgnoreRule>,
}

struct IgnoreRule {
    standard: Option<String>,
    pattern: String,
}

impl IgnoreList {
    #[must_use]
    pub fn new() -> Self {
        Self { rules: Vec::new() }
    }

    pub fn parse(text: &str) -> Self {
        let mut rules = Vec::new();
        for line in text.lines() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }
            let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
            if parts.len() == 2 {
                let standard = parts[0].trim();
                let glob = parts[1].trim().to_ascii_lowercase();
                if standard == "*" {
                    rules.push(IgnoreRule {
                        standard: None,
                        pattern: glob,
                    });
                } else {
                    rules.push(IgnoreRule {
                        standard: Some(standard.to_ascii_lowercase()),
                        pattern: glob,
                    });
                }
            } else {
                rules.push(IgnoreRule {
                    standard: None,
                    pattern: trimmed.to_ascii_lowercase(),
                });
            }
        }
        Self { rules }
    }

    #[must_use]
    pub fn is_ignored(&self, hostname: &str, standard: Option<&str>) -> bool {
        let host_lower = hostname.to_ascii_lowercase();
        for rule in &self.rules {
            if !glob_match(&rule.pattern, &host_lower) {
                continue;
            }
            match (&rule.standard, standard) {
                (None, _) => return true,
                (Some(rs), Some(s)) if rs == &s.to_ascii_lowercase() => return true,
                _ => {}
            }
        }
        false
    }
}

impl Default for IgnoreList {
    fn default() -> Self {
        Self::new()
    }
}

fn glob_match(pattern: &str, text: &str) -> bool {
    let pat: Vec<char> = pattern.chars().collect();
    let txt: Vec<char> = text.chars().collect();
    glob_match_inner(&pat, &txt)
}

fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
    let mut pi = 0;
    let mut ti = 0;
    let mut star_pi = usize::MAX;
    let mut star_ti = 0;

    while ti < txt.len() {
        if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
            pi += 1;
            ti += 1;
        } else if pi < pat.len() && pat[pi] == '*' {
            star_pi = pi;
            star_ti = ti;
            pi += 1;
        } else if star_pi != usize::MAX {
            pi = star_pi + 1;
            star_ti += 1;
            ti = star_ti;
        } else {
            return false;
        }
    }

    while pi < pat.len() && pat[pi] == '*' {
        pi += 1;
    }

    pi == pat.len()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_list_ignores_nothing() {
        let list = IgnoreList::new();
        assert!(!list.is_ignored("example.com", None));
    }

    #[test]
    fn exact_hostname() {
        let list = IgnoreList::parse("* example.com");
        assert!(list.is_ignored("example.com", None));
        assert!(!list.is_ignored("other.com", None));
    }

    #[test]
    fn wildcard_subdomain() {
        let list = IgnoreList::parse("* *.example.com");
        assert!(list.is_ignored("sub.example.com", None));
        assert!(!list.is_ignored("example.com", None));
    }

    #[test]
    fn case_insensitive() {
        let list = IgnoreList::parse("* Example.COM");
        assert!(list.is_ignored("EXAMPLE.com", None));
    }

    #[test]
    fn comments_and_blanks() {
        let list = IgnoreList::parse("# a comment\n\n* example.com\n");
        assert!(list.is_ignored("example.com", None));
    }

    #[test]
    fn standard_specific_rule() {
        let list = IgnoreList::parse("ard example.com");
        assert!(list.is_ignored("example.com", Some("ard")));
        assert!(!list.is_ignored("example.com", Some("other")));
        assert!(!list.is_ignored("example.com", None));
    }

    #[test]
    fn bare_hostname_ignores_all_standards() {
        let list = IgnoreList::parse("example.com");
        assert!(list.is_ignored("example.com", None));
        assert!(list.is_ignored("example.com", Some("ard")));
    }

    #[test]
    fn glob_match_star() {
        assert!(glob_match("*", "anything"));
        assert!(glob_match("*.com", "example.com"));
        assert!(!glob_match("*.com", "example.org"));
    }

    #[test]
    fn glob_match_question() {
        assert!(glob_match("a?c", "abc"));
        assert!(!glob_match("a?c", "abbc"));
    }
}