weavatrix-scan 0.1.0

Deterministic, safe repository scanner for code intelligence
Documentation
use crate::glob;

#[derive(Debug, Clone)]
pub(super) enum RuleMatcher {
    Literal,
    Prefix(String),
    Suffix(String),
    Glob(Option<String>),
}

impl RuleMatcher {
    pub(super) fn new(pattern: &str) -> Self {
        if !has_meta(pattern) {
            Self::Literal
        } else if let Some(suffix) = pattern.strip_prefix('*').filter(|value| !has_meta(value)) {
            Self::Suffix(suffix.to_owned())
        } else if let Some(prefix) = pattern.strip_suffix('*').filter(|value| !has_meta(value)) {
            Self::Prefix(prefix.to_owned())
        } else {
            Self::Glob(longest_literal(pattern))
        }
    }

    pub(super) const fn is_literal(&self) -> bool {
        matches!(self, Self::Literal)
    }

    pub(super) fn matches(&self, pattern: &str, value: &str) -> bool {
        match self {
            Self::Literal => pattern == value,
            Self::Prefix(prefix) => value.starts_with(prefix),
            Self::Suffix(suffix) => value.ends_with(suffix),
            Self::Glob(needle) => {
                needle.as_ref().is_none_or(|needle| value.contains(needle))
                    && glob::matches(pattern, value)
            }
        }
    }
}

fn has_meta(value: &str) -> bool {
    value
        .bytes()
        .any(|byte| matches!(byte, b'*' | b'?' | b'[' | b'\\'))
}

fn longest_literal(pattern: &str) -> Option<String> {
    let mut longest = Vec::new();
    let mut current = Vec::new();
    let mut bytes = pattern.bytes();
    while let Some(byte) = bytes.next() {
        match byte {
            b'\\' => {
                if let Some(escaped) = bytes.next() {
                    current.push(escaped);
                }
            }
            b'[' => {
                retain_longest(&mut current, &mut longest);
                for member in bytes.by_ref() {
                    if member == b']' {
                        break;
                    }
                }
            }
            b'*' | b'?' => retain_longest(&mut current, &mut longest),
            literal => current.push(literal),
        }
    }
    retain_longest(&mut current, &mut longest);
    (longest.len() >= 2)
        .then(|| String::from_utf8(longest).ok())
        .flatten()
}

fn retain_longest(current: &mut Vec<u8>, longest: &mut Vec<u8>) {
    if current.len() > longest.len() {
        std::mem::swap(current, longest);
    }
    current.clear();
}

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

    #[test]
    fn specializes_literal_prefix_suffix_and_complex_patterns() {
        assert!(RuleMatcher::new("Cargo.toml").is_literal());
        assert!(RuleMatcher::new("target*").matches("target*", "target-debug"));
        assert!(RuleMatcher::new("*.rs").matches("*.rs", "lib.rs"));
        let complex = RuleMatcher::new("report.[0-9]*.json");
        assert!(complex.matches("report.[0-9]*.json", "report.1.json"));
        assert!(!complex.matches("report.[0-9]*.json", "notes.json"));
    }
}