use crate::harvest::attribution_files;
use crate::policy::license_name;
use cargo_metadata::{Package, PackageId};
use spdx::detection::{Store, TextData};
use spdx::expression::ExprNode;
use std::collections::{BTreeMap, BTreeSet};
const THRESHOLD: f32 = 0.9;
pub fn run_audit(
deps: &BTreeSet<&PackageId>,
pkg_of: &BTreeMap<&PackageId, &Package>,
effective: &BTreeMap<&PackageId, &str>,
) -> String {
let mut store = Store::new();
for (id, text) in spdx::text::LICENSE_TEXTS {
store.add_license((*id).into(), TextData::new(text));
}
let mut findings = Vec::new();
let mut scanned = 0usize;
for id in deps {
let Some(&pkg) = pkg_of.get(id) else { continue };
let declared = effective.get(id).copied();
let leaves: BTreeSet<String> = declared
.and_then(|e| spdx::Expression::parse_mode(e, spdx::ParseMode::LAX).ok())
.map(|expr| {
expr.iter()
.filter_map(|n| match n {
ExprNode::Req(r) => Some(license_name(&r.req)),
ExprNode::Op(_) => None,
})
.collect()
})
.unwrap_or_default();
for (fname, text, is_notice) in attribution_files(pkg) {
if is_notice {
continue;
}
scanned += 1;
let data = TextData::new(&text);
let m = store.analyze(&data);
if m.score < THRESHOLD || leaves.contains(m.name) {
continue;
}
let declared_close = leaves
.iter()
.any(|l| store.get_original(l).is_some_and(|orig| data.match_score(orig) >= m.score - 0.05));
if declared_close {
continue;
}
findings.push(format!(
"{} {}: {fname} matches {} (score {:.2}) but the declared license is '{}'",
pkg.name,
pkg.version,
m.name,
m.score,
declared.unwrap_or("<none>")
));
}
}
if findings.is_empty() {
format!("audit: no mismatches ({} license files scanned across {} crates)", scanned, deps.len())
} else {
format!("audit: {} possible mismatches (advisory only):\n {}", findings.len(), findings.join("\n "))
}
}