cargo-allow 0.1.7

Source-tree exception ledger and policy scanner for Rust repositories.
use super::PruneCandidate;
use allow_core::{AllowConfig, MatchOutcome, MatchStatus};
use std::collections::BTreeSet;

pub(super) fn prune_stale_candidates(
    cfg: &AllowConfig,
    outcomes: &[MatchOutcome],
) -> Vec<PruneCandidate> {
    outcomes
        .iter()
        .filter(|outcome| outcome.status == MatchStatus::Stale)
        .filter_map(|outcome| {
            let id = outcome.allow_id.as_deref()?;
            let entry = cfg.allow.iter().find(|entry| entry.id == id)?;
            Some(PruneCandidate {
                id: entry.id.clone(),
                kind: entry.kind,
                family: entry.family.clone(),
                owner: entry.owner.clone(),
                classification: entry.classification.clone(),
                scope: entry.path_or_glob(),
                reason: entry.reason.clone(),
            })
        })
        .collect()
}

pub(super) fn config_without_prune_candidates(
    cfg: &AllowConfig,
    candidates: &[PruneCandidate],
) -> AllowConfig {
    let mut pruned = cfg.clone();
    pruned
        .allow
        .retain(|entry| !candidates.iter().any(|candidate| candidate.id == entry.id));
    pruned
}

pub(super) fn removed_toml_blocks(
    rendered_policy: &str,
    candidates: &[PruneCandidate],
) -> Vec<String> {
    let ids = candidates
        .iter()
        .map(|candidate| candidate.id.as_str())
        .collect::<BTreeSet<_>>();
    allow_blocks(rendered_policy)
        .into_iter()
        .filter(|block| ids.iter().any(|id| block_contains_allow_id(block, id)))
        .map(str::to_string)
        .collect()
}

fn allow_blocks(rendered_policy: &str) -> Vec<&str> {
    let mut blocks = Vec::new();
    let mut start = None;
    let mut offset = 0;
    for line in rendered_policy.split_inclusive('\n') {
        let line_text = line.trim_end_matches('\n').trim_end_matches('\r');
        if line_text == "[[allow]]" {
            if let Some(previous) = start.replace(offset) {
                if let Some(block) = rendered_policy.get(previous..offset) {
                    blocks.push(block.trim_end());
                }
            }
        }
        offset += line.len();
    }
    if let Some(previous) = start {
        if let Some(block) = rendered_policy.get(previous..) {
            blocks.push(block.trim_end());
        }
    }
    blocks
}

fn block_contains_allow_id(block: &str, id: &str) -> bool {
    block
        .lines()
        .any(|line| line.trim() == format!("id = \"{}\"", escape_toml_basic(id)))
}

fn escape_toml_basic(value: &str) -> String {
    let mut out = String::new();
    for ch in value.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            _ => out.push(ch),
        }
    }
    out
}