allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use allow_core::{Finding, FindingKind, glob_matches, normalize_path};

#[derive(Debug, Clone)]
pub(crate) struct LegacyNonRustRule {
    pub(crate) id: String,
    pub(crate) pattern: String,
    pub(crate) is_path: bool,
    pub(crate) owner: String,
    pub(crate) classification: String,
    pub(crate) reason: String,
    pub(crate) evidence: Vec<String>,
    pub(crate) created: Option<String>,
    pub(crate) review_after: Option<String>,
    pub(crate) expires: Option<String>,
}

impl LegacyNonRustRule {
    pub(crate) fn matches(&self, finding: &Finding) -> bool {
        if !matches!(
            finding.kind,
            FindingKind::NonRustFile | FindingKind::GeneratedCode
        ) {
            return false;
        }
        if self.is_path {
            normalize_path(&self.pattern) == normalize_path(&finding.path)
        } else {
            glob_matches(&self.pattern, &finding.path)
        }
    }

    pub(crate) fn specificity(&self) -> usize {
        let literal_chars = self
            .pattern
            .chars()
            .filter(|ch| !matches!(ch, '*' | '?' | '[' | ']' | '{' | '}' | ',' | '!'))
            .count();
        literal_chars + if self.is_path { 10_000 } else { 0 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use allow_core::{Span, StructuralIdentity};
    use std::path::PathBuf;

    #[test]
    fn non_rust_rule_matches_file_findings_by_path_or_glob() {
        let path_rule = legacy_rule("README.md", true);
        let glob_rule = legacy_rule("docs/**", false);

        assert!(path_rule.matches(&finding(FindingKind::NonRustFile, "README.md")));
        assert!(glob_rule.matches(&finding(FindingKind::GeneratedCode, "docs/schema.json")));
        assert!(!path_rule.matches(&finding(FindingKind::NonRustFile, "docs/README.md")));
        assert!(!glob_rule.matches(&finding(FindingKind::Panic, "docs/panic.md")));
    }

    #[test]
    fn non_rust_rule_specificity_prefers_paths_over_equal_globs() {
        let path_specificity = legacy_rule("docs/guide.md", true).specificity();
        let glob_specificity = legacy_rule("docs/guide.md", false).specificity();
        let broader_glob_specificity = legacy_rule("docs/**", false).specificity();

        assert!(path_specificity > glob_specificity);
        assert!(glob_specificity > broader_glob_specificity);
    }

    fn legacy_rule(pattern: &str, is_path: bool) -> LegacyNonRustRule {
        LegacyNonRustRule {
            id: "non-rust-doc".to_string(),
            pattern: pattern.to_string(),
            is_path,
            owner: "docs".to_string(),
            classification: "documentation".to_string(),
            reason: "Documentation files are intentionally tracked.".to_string(),
            evidence: Vec::new(),
            created: Some("2026-05-09".to_string()),
            review_after: Some("2026-09-09".to_string()),
            expires: Some("never".to_string()),
        }
    }

    fn finding(kind: FindingKind, path: &str) -> Finding {
        Finding {
            kind,
            family: Some(kind.as_str().to_string()),
            path: PathBuf::from(path),
            span: Some(Span { line: 1, column: 1 }),
            identity: StructuralIdentity::new("file", "tracked_file"),
            message: format!("tracked file: {path}"),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct LegacyGeneratedRule {
    pub(crate) id: String,
    pub(crate) path: String,
    pub(crate) owner: String,
    pub(crate) reason: String,
    pub(crate) generator: Option<String>,
    pub(crate) regenerate_command: Option<String>,
    pub(crate) evidence: Vec<String>,
    pub(crate) created: Option<String>,
    pub(crate) expires: Option<String>,
}