use cargo_metadata::{DependencyKind, Metadata, Node, PackageId};
use futures_util::future::join_all;
use miette::IntoDiagnostic;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::sync::Arc;
use tracing::{trace, trace_span, warn};
use crate::cli::{DumpGraphArgs, GraphFilter, GraphFilterProperty, GraphFilterQuery, OutputFormat};
use crate::criteria::{CriteriaMapper, CriteriaSet};
use crate::errors::SuggestError;
use crate::format::{
self, AuditEntry, AuditKind, AuditsFile, CratesPublisher, CriteriaName, Delta, DiffStat,
ExemptedDependency, FastMap, FastSet, ImportName, ImportsFile, JsonPackage, JsonReport,
JsonReportConclusion, JsonReportFailForVet, JsonReportFailForViolationConflict,
JsonReportSuccess, JsonSuggest, JsonSuggestItem, JsonVetFailure, PackageName, PackageStr,
Policy, VetVersion, WildcardEntry,
};
use crate::format::{SortedMap, SortedSet};
use crate::network::Network;
use crate::out::{progress_bar, IncProgressOnDrop, Out};
use crate::storage::Cache;
use crate::{Config, PackageExt, Store};
pub struct ResolveReport<'a> {
pub graph: DepGraph<'a>,
pub criteria_mapper: CriteriaMapper,
pub results: Vec<Option<ResolveResult>>,
pub conclusion: Conclusion,
}
#[derive(Debug)]
pub enum Conclusion {
Success(Success),
FailForViolationConflict(FailForViolationConflict),
FailForVet(FailForVet),
}
#[derive(Debug, Clone)]
pub struct Success {
pub vetted_with_exemptions: Vec<PackageIdx>,
pub vetted_partially: Vec<PackageIdx>,
pub vetted_fully: Vec<PackageIdx>,
}
#[derive(Debug, Clone)]
pub struct FailForViolationConflict {
pub violations: Vec<(PackageIdx, Vec<ViolationConflict>)>,
}
#[derive(Debug)]
pub struct FailForVet {
pub failures: Vec<(PackageIdx, AuditFailure)>,
pub suggest: Option<Suggest>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ViolationConflict {
UnauditedConflict {
violation_source: Option<ImportName>,
violation: AuditEntry,
exemptions: ExemptedDependency,
},
AuditConflict {
violation_source: Option<ImportName>,
violation: AuditEntry,
audit_source: Option<ImportName>,
audit: AuditEntry,
},
}
#[derive(Debug, Default)]
pub struct Suggest {
pub suggestions: Vec<SuggestItem>,
pub suggestions_by_criteria: SortedMap<CriteriaName, Vec<SuggestItem>>,
pub total_lines: u64,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SuggestItem {
pub package: PackageIdx,
pub suggested_criteria: CriteriaSet,
pub suggested_diff: DiffRecommendation,
pub notable_parents: String,
pub registry_suggestion: Vec<RegistrySuggestion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffRecommendation {
pub from: Option<VetVersion>,
pub to: VetVersion,
pub diffstat: DiffStat,
}
#[derive(Debug, Clone)]
pub struct RegistrySuggestion {
pub name: ImportName,
pub url: String,
pub diff: DiffRecommendation,
}
pub type PackageIdx = usize;
#[derive(Debug, Clone, Serialize)]
pub struct PackageNode<'a> {
#[serde(skip_serializing_if = "pkgid_unstable")]
pub package_id: &'a PackageId,
pub name: PackageStr<'a>,
pub version: VetVersion,
pub normal_deps: Vec<PackageIdx>,
pub build_deps: Vec<PackageIdx>,
pub dev_deps: Vec<PackageIdx>,
pub normal_and_build_deps: Vec<PackageIdx>,
pub all_deps: Vec<PackageIdx>,
pub reverse_deps: SortedSet<PackageIdx>,
pub is_workspace_member: bool,
pub is_third_party: bool,
pub is_root: bool,
pub is_dev_only: bool,
}
fn pkgid_unstable(pkgid: &PackageId) -> bool {
pkgid.repr.contains("(path+file:/")
}
#[derive(Debug, Clone)]
pub struct DepGraph<'a> {
pub nodes: Vec<PackageNode<'a>>,
pub interner_by_pkgid: SortedMap<&'a PackageId, PackageIdx>,
pub topo_index: Vec<PackageIdx>,
}
#[derive(Debug, Clone)]
pub struct ResolveResult {
pub search_results: Vec<Result<Vec<DeltaEdgeOrigin>, SearchFailure>>,
}
#[derive(Debug, Clone)]
pub struct AuditFailure {
pub criteria_failures: CriteriaSet,
}
#[derive(Debug, Clone)]
pub struct SearchFailure {
pub reachable_from_root: SortedSet<Option<VetVersion>>,
pub reachable_from_target: SortedSet<Option<VetVersion>>,
}
type DirectedAuditGraph<'a> = SortedMap<Option<&'a VetVersion>, Vec<DeltaEdge<'a>>>;
#[derive(Debug, Clone)]
pub struct AuditGraph<'a> {
forward_audits: DirectedAuditGraph<'a>,
backward_audits: DirectedAuditGraph<'a>,
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum DeltaEdgeOrigin {
StoredLocalAudit { audit_index: usize },
ImportedAudit {
import_index: usize,
audit_index: usize,
},
WildcardAudit {
import_index: Option<usize>,
audit_index: usize,
publisher_index: usize,
},
Exemption { exemption_index: usize },
FreshExemption { version: VetVersion },
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum RequiredEntry {
Audit {
import_index: usize,
audit_index: usize,
},
WildcardAudit {
import_index: usize,
audit_index: usize,
},
Publisher {
publisher_index: usize,
},
Exemption {
exemption_index: usize,
},
FreshExemption {
version: VetVersion,
},
}
#[derive(Debug, Clone)]
struct DeltaEdge<'a> {
version: Option<&'a VetVersion>,
criteria: CriteriaSet,
origin: DeltaEdgeOrigin,
is_fresh_import: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum SearchMode {
PreferExemptions,
PreferFreshImports,
RegenerateExemptions,
}
impl<'a> DepGraph<'a> {
pub fn new(
metadata: &'a Metadata,
filter_graph: Option<&Vec<GraphFilter>>,
policy: Option<&Policy>,
) -> Self {
let default_policy = Policy::default();
let policy = policy.unwrap_or(&default_policy);
let package_list = &*metadata.packages;
let resolve_list = &*metadata
.resolve
.as_ref()
.expect("cargo metadata did not yield resolve!")
.nodes;
let package_index_by_pkgid = package_list
.iter()
.enumerate()
.map(|(idx, pkg)| (&pkg.id, idx))
.collect::<SortedMap<_, _>>();
let resolve_index_by_pkgid = resolve_list
.iter()
.enumerate()
.map(|(idx, pkg)| (&pkg.id, idx))
.collect();
let mut interner_by_pkgid = SortedMap::<&PackageId, PackageIdx>::new();
let mut nodes = vec![];
for resolve_node in resolve_list {
let package = &package_list[package_index_by_pkgid[&resolve_node.id]];
nodes.push(PackageNode {
package_id: &resolve_node.id,
name: &package.name,
version: package.vet_version(),
is_third_party: package.is_third_party(policy),
normal_deps: vec![],
build_deps: vec![],
dev_deps: vec![],
normal_and_build_deps: vec![],
all_deps: vec![],
reverse_deps: SortedSet::new(),
is_workspace_member: false,
is_root: false,
is_dev_only: true,
});
}
nodes.sort_by_key(|k| k.package_id);
for (idx, node) in nodes.iter_mut().enumerate() {
assert!(interner_by_pkgid.insert(node.package_id, idx).is_none());
}
let mut topo_index = vec![];
{
let mut visited = FastMap::new();
for pkgid in &metadata.workspace_members {
let node_idx = interner_by_pkgid[pkgid];
nodes[node_idx].is_workspace_member = true;
visit_node(
&mut nodes,
&mut topo_index,
&mut visited,
&interner_by_pkgid,
&resolve_index_by_pkgid,
resolve_list,
node_idx,
);
}
for (&node_idx, ()) in &visited {
nodes[node_idx].is_dev_only = false;
}
for pkgid in &metadata.workspace_members {
let node = &mut nodes[interner_by_pkgid[pkgid]];
node.is_root = node.reverse_deps.is_empty();
}
for pkgid in &metadata.workspace_members {
let node_idx = interner_by_pkgid[pkgid];
let resolve_node = &resolve_list[resolve_index_by_pkgid[pkgid]];
let dev_deps = deps(
resolve_node,
&[DependencyKind::Development],
&interner_by_pkgid,
);
for &child in &dev_deps {
visit_node(
&mut nodes,
&mut topo_index,
&mut visited,
&interner_by_pkgid,
&resolve_index_by_pkgid,
resolve_list,
child,
);
nodes[child].reverse_deps.insert(node_idx);
}
let node = &mut nodes[node_idx];
node.dev_deps = dev_deps;
}
fn visit_node<'a>(
nodes: &mut Vec<PackageNode<'a>>,
topo_index: &mut Vec<PackageIdx>,
visited: &mut FastMap<PackageIdx, ()>,
interner_by_pkgid: &SortedMap<&'a PackageId, PackageIdx>,
resolve_index_by_pkgid: &SortedMap<&'a PackageId, usize>,
resolve_list: &'a [cargo_metadata::Node],
normal_idx: PackageIdx,
) {
let query = visited.entry(normal_idx);
if matches!(query, std::collections::hash_map::Entry::Vacant(..)) {
query.or_insert(());
let resolve_node =
&resolve_list[resolve_index_by_pkgid[nodes[normal_idx].package_id]];
let all_deps = resolve_node
.dependencies
.iter()
.map(|pkgid| interner_by_pkgid[pkgid])
.collect::<Vec<_>>();
let build_deps =
deps(resolve_node, &[DependencyKind::Build], interner_by_pkgid);
let normal_deps =
deps(resolve_node, &[DependencyKind::Normal], interner_by_pkgid);
let normal_and_build_deps = deps(
resolve_node,
&[DependencyKind::Normal, DependencyKind::Build],
interner_by_pkgid,
);
for &child in &normal_and_build_deps {
visit_node(
nodes,
topo_index,
visited,
interner_by_pkgid,
resolve_index_by_pkgid,
resolve_list,
child,
);
nodes[child].reverse_deps.insert(normal_idx);
}
topo_index.push(normal_idx);
let cur_node = &mut nodes[normal_idx];
cur_node.build_deps = build_deps;
cur_node.normal_deps = normal_deps;
cur_node.normal_and_build_deps = normal_and_build_deps;
cur_node.all_deps = all_deps;
}
}
fn deps(
resolve_node: &Node,
kinds: &[DependencyKind],
interner_by_pkgid: &SortedMap<&PackageId, PackageIdx>,
) -> Vec<PackageIdx> {
resolve_node
.deps
.iter()
.filter(|dep| {
dep.dep_kinds
.iter()
.any(|dep_kind| kinds.contains(&dep_kind.kind))
})
.map(|dep| interner_by_pkgid[&dep.pkg])
.collect()
}
}
let result = Self {
interner_by_pkgid,
nodes,
topo_index,
};
if let Some(filters) = filter_graph {
result.filter(filters)
} else {
result
}
}
pub fn filter(self, filters: &[GraphFilter]) -> Self {
use GraphFilter::*;
use GraphFilterProperty::*;
use GraphFilterQuery::*;
fn matches_query(package: &PackageNode, query: &GraphFilterQuery) -> bool {
match query {
All(queries) => queries.iter().all(|q| matches_query(package, q)),
Any(queries) => queries.iter().any(|q| matches_query(package, q)),
Not(query) => !matches_query(package, query),
Prop(property) => matches_property(package, property),
}
}
fn matches_property(package: &PackageNode, property: &GraphFilterProperty) -> bool {
match property {
Name(val) => package.name == val,
Version(val) => &package.version == val,
IsRoot(val) => &package.is_root == val,
IsWorkspaceMember(val) => &package.is_workspace_member == val,
IsThirdParty(val) => &package.is_third_party == val,
IsDevOnly(val) => &package.is_dev_only == val,
}
}
let mut passed_filters = FastSet::new();
'nodes: for (idx, package) in self.nodes.iter().enumerate() {
for filter in filters {
match filter {
Include(query) => {
if !matches_query(package, query) {
continue 'nodes;
}
}
Exclude(query) => {
if matches_query(package, query) {
continue 'nodes;
}
}
}
}
passed_filters.insert(idx);
}
let mut reachable = FastMap::new();
for (idx, package) in self.nodes.iter().enumerate() {
if package.is_workspace_member {
visit(&mut reachable, &self, &passed_filters, idx);
}
fn visit(
visited: &mut FastMap<PackageIdx, ()>,
graph: &DepGraph,
passed_filters: &FastSet<PackageIdx>,
node_idx: PackageIdx,
) {
if !passed_filters.contains(&node_idx) {
return;
}
let query = visited.entry(node_idx);
if matches!(query, std::collections::hash_map::Entry::Vacant(..)) {
query.or_insert(());
for &child in &graph.nodes[node_idx].all_deps {
visit(visited, graph, passed_filters, child);
}
}
}
}
let mut old_to_new = FastMap::new();
let mut nodes = Vec::new();
let mut interner_by_pkgid = SortedMap::new();
let mut topo_index = Vec::new();
for (old_idx, package) in self.nodes.iter().enumerate() {
if !reachable.contains_key(&old_idx) {
continue;
}
let new_idx = nodes.len();
old_to_new.insert(old_idx, new_idx);
nodes.push(PackageNode {
package_id: package.package_id,
name: package.name,
version: package.version.clone(),
normal_deps: vec![],
build_deps: vec![],
dev_deps: vec![],
normal_and_build_deps: vec![],
all_deps: vec![],
reverse_deps: SortedSet::new(),
is_workspace_member: package.is_workspace_member,
is_third_party: package.is_third_party,
is_root: package.is_root,
is_dev_only: package.is_dev_only,
});
interner_by_pkgid.insert(package.package_id, new_idx);
}
for old_idx in &self.topo_index {
if let Some(&new_idx) = old_to_new.get(old_idx) {
topo_index.push(new_idx);
}
}
for (old_idx, old_package) in self.nodes.iter().enumerate() {
if let Some(&new_idx) = old_to_new.get(&old_idx) {
let new_package = &mut nodes[new_idx];
for old_dep in &old_package.normal_deps {
if let Some(&new_dep) = old_to_new.get(old_dep) {
new_package.normal_deps.push(new_dep);
}
}
for old_dep in &old_package.build_deps {
if let Some(&new_dep) = old_to_new.get(old_dep) {
new_package.build_deps.push(new_dep);
}
}
for old_dep in &old_package.dev_deps {
if let Some(&new_dep) = old_to_new.get(old_dep) {
new_package.dev_deps.push(new_dep);
}
}
for old_dep in &old_package.normal_and_build_deps {
if let Some(&new_dep) = old_to_new.get(old_dep) {
new_package.normal_and_build_deps.push(new_dep);
}
}
for old_dep in &old_package.all_deps {
if let Some(&new_dep) = old_to_new.get(old_dep) {
new_package.all_deps.push(new_dep);
}
}
for old_dep in &old_package.reverse_deps {
if let Some(&new_dep) = old_to_new.get(old_dep) {
new_package.reverse_deps.insert(new_dep);
}
}
}
}
Self {
nodes,
interner_by_pkgid,
topo_index,
}
}
pub fn print_mermaid(
&self,
out: &Arc<dyn Out>,
sub_args: &DumpGraphArgs,
) -> Result<(), std::io::Error> {
use crate::DumpGraphDepth::*;
let depth = sub_args.depth;
let mut visible_nodes = SortedSet::new();
let mut nodes_with_children = SortedSet::new();
let mut shown = SortedSet::new();
for (idx, package) in self.nodes.iter().enumerate() {
if (package.is_root && depth >= Roots)
|| (package.is_workspace_member && depth >= Workspace)
|| (!package.is_third_party && depth >= FirstParty)
|| depth >= Full
{
visible_nodes.insert(idx);
nodes_with_children.insert(idx);
if depth >= FirstPartyAndDirects {
for &dep in &package.all_deps {
visible_nodes.insert(dep);
}
}
}
}
writeln!(out, "graph LR");
writeln!(out, " subgraph roots");
for &idx in &visible_nodes {
let package = &self.nodes[idx];
if package.is_root && shown.insert(idx) {
writeln!(
out,
" node{idx}{{{}:{}}}",
package.name, package.version
);
}
}
writeln!(out, " end");
writeln!(out, " subgraph workspace-members");
for &idx in &visible_nodes {
let package = &self.nodes[idx];
if package.is_workspace_member && shown.insert(idx) {
writeln!(
out,
" node{idx}[/{}:{}/]",
package.name, package.version
);
}
}
writeln!(out, " end");
writeln!(out, " subgraph first-party");
for &idx in &visible_nodes {
let package = &self.nodes[idx];
if !package.is_third_party && shown.insert(idx) {
writeln!(
out,
" node{idx}[{}:{}]",
package.name, package.version
);
}
}
writeln!(out, " end");
writeln!(out, " subgraph third-party");
for &idx in &visible_nodes {
let package = &self.nodes[idx];
if shown.insert(idx) {
writeln!(
out,
" node{idx}({}:{})",
package.name, package.version
);
}
}
writeln!(out, " end");
for &idx in &nodes_with_children {
let package = &self.nodes[idx];
for &dep_idx in &package.all_deps {
if visible_nodes.contains(&dep_idx) {
writeln!(out, " node{idx} --> node{dep_idx}");
}
}
}
Ok(())
}
}
pub fn resolve<'a>(
metadata: &'a Metadata,
filter_graph: Option<&Vec<GraphFilter>>,
store: &Store,
) -> ResolveReport<'a> {
let graph = DepGraph::new(metadata, filter_graph, Some(&store.config.policy));
trace!("built DepGraph!");
let criteria_mapper = CriteriaMapper::new(&store.audits.criteria);
trace!("built CriteriaMapper!");
let requirements = resolve_requirements(&graph, &store.config.policy, &criteria_mapper);
let (results, conclusion) = resolve_audits(&graph, store, &criteria_mapper, &requirements);
ResolveReport {
graph,
criteria_mapper,
results,
conclusion,
}
}
fn resolve_requirements(
graph: &DepGraph<'_>,
policy: &Policy,
criteria_mapper: &CriteriaMapper,
) -> Vec<CriteriaSet> {
let _resolve_requirements = trace_span!("resolve_requirements").entered();
let mut requirements = vec![criteria_mapper.no_criteria(); graph.nodes.len()];
for package in &graph.nodes {
if package.dev_deps.is_empty() {
continue;
}
let policy = policy.get(package.name, &package.version);
let dev_criteria = if let Some(c) = policy.and_then(|p| p.dev_criteria.as_ref()) {
criteria_mapper.criteria_from_list(c)
} else {
criteria_mapper.criteria_from_list([format::DEFAULT_POLICY_DEV_CRITERIA])
};
for &depidx in &package.dev_deps {
let dep_package = &graph.nodes[depidx];
let dependency_criteria = policy
.and_then(|policy| policy.dependency_criteria.get(dep_package.name))
.map(|criteria| criteria_mapper.criteria_from_list(criteria));
requirements[depidx]
.unioned_with(dependency_criteria.as_ref().unwrap_or(&dev_criteria));
}
}
for &pkgidx in graph.topo_index.iter().rev() {
let package = &graph.nodes[pkgidx];
let policy = policy.get(package.name, &package.version);
if let Some(c) = policy.and_then(|p| p.criteria.as_ref()) {
requirements[pkgidx] = criteria_mapper.criteria_from_list(c);
} else if package.is_root {
requirements[pkgidx].unioned_with(
&criteria_mapper.criteria_from_list([format::DEFAULT_POLICY_CRITERIA]),
);
}
let normal_criteria = requirements[pkgidx].clone();
for &depidx in &package.normal_and_build_deps {
let dep_package = &graph.nodes[depidx];
let dependency_criteria = policy
.and_then(|policy| policy.dependency_criteria.get(dep_package.name))
.map(|criteria| criteria_mapper.criteria_from_list(criteria));
requirements[depidx]
.unioned_with(dependency_criteria.as_ref().unwrap_or(&normal_criteria));
}
}
requirements
}
fn resolve_audits(
graph: &DepGraph<'_>,
store: &Store,
criteria_mapper: &CriteriaMapper,
requirements: &[CriteriaSet],
) -> (Vec<Option<ResolveResult>>, Conclusion) {
let _resolve_audits = trace_span!("resolve_audits").entered();
let mut violations = Vec::new();
let mut failures = Vec::new();
let mut vetted_with_exemptions = Vec::new();
let mut vetted_partially = Vec::new();
let mut vetted_fully = Vec::new();
let results: Vec<_> = requirements
.iter()
.enumerate()
.map(|(pkgidx, required_criteria)| {
let package = &graph.nodes[pkgidx];
if !package.is_third_party {
return None; }
let audit_graph = AuditGraph::build(store, criteria_mapper, package.name, None)
.map_err(|v| violations.push((pkgidx, v)))
.ok()?;
let search_results: Vec<_> = (0..criteria_mapper.len())
.map(|criteria_idx| {
audit_graph.search(criteria_idx, &package.version, SearchMode::PreferExemptions)
})
.collect();
let mut needed_exemptions = false;
let mut directly_exempted = false;
let mut criteria_failures = criteria_mapper.no_criteria();
for criteria_idx in required_criteria.indices() {
match &search_results[criteria_idx] {
Ok(path) => {
needed_exemptions |= path
.iter()
.any(|o| matches!(o, DeltaEdgeOrigin::Exemption { .. }));
directly_exempted |=
path.len() == 1 && matches!(path[0], DeltaEdgeOrigin::Exemption { .. });
}
Err(_) => criteria_failures.set_criteria(criteria_idx),
}
}
if !criteria_failures.is_empty() {
failures.push((pkgidx, AuditFailure { criteria_failures }));
}
if !needed_exemptions {
vetted_fully.push(pkgidx);
} else if directly_exempted {
vetted_with_exemptions.push(pkgidx);
} else {
vetted_partially.push(pkgidx);
}
Some(ResolveResult { search_results })
})
.collect();
let conclusion = if !violations.is_empty() {
Conclusion::FailForViolationConflict(FailForViolationConflict { violations })
} else if !failures.is_empty() {
Conclusion::FailForVet(FailForVet {
failures,
suggest: None,
})
} else {
Conclusion::Success(Success {
vetted_with_exemptions,
vetted_partially,
vetted_fully,
})
};
(results, conclusion)
}
impl<'a> AuditGraph<'a> {
pub fn build(
store: &'a Store,
criteria_mapper: &CriteriaMapper,
package: PackageStr<'_>,
extra_audits_file: Option<&'a AuditsFile>,
) -> Result<Self, Vec<ViolationConflict>> {
let foreign_namespaces: Vec<Option<ImportName>> = store
.imported_audits()
.keys()
.map(|import_name| Some(import_name.clone()))
.collect();
let all_audits_files = store
.imported_audits()
.values()
.enumerate()
.map(|(import_index, audits_file)| {
(
Some(import_index),
&foreign_namespaces[import_index],
audits_file,
)
})
.chain([(None, &None, &store.audits)])
.chain(
extra_audits_file
.iter()
.map(|&audits_file| (None, &None, audits_file)),
);
let all_audits =
all_audits_files
.clone()
.flat_map(|(import_index, namespace, audits_file)| {
audits_file
.audits
.get(package)
.map(|v| &v[..])
.unwrap_or(&[])
.iter()
.enumerate()
.map(move |(audit_index, audit)| {
(
namespace,
match import_index {
Some(import_index) => DeltaEdgeOrigin::ImportedAudit {
import_index,
audit_index,
},
None => DeltaEdgeOrigin::StoredLocalAudit { audit_index },
},
audit,
)
})
});
let all_wildcard_audits =
all_audits_files
.clone()
.flat_map(|(import_index, namespace, audits_file)| {
audits_file
.wildcard_audits
.get(package)
.map(|v| &v[..])
.unwrap_or(&[])
.iter()
.enumerate()
.map(move |(audit_index, audit)| {
(namespace, import_index, audit_index, audit)
})
});
let publishers = store
.publishers()
.get(package)
.map(|v| &v[..])
.unwrap_or(&[]);
let exemptions = store.config.exemptions.get(package);
let mut forward_audits = DirectedAuditGraph::new();
let mut backward_audits = DirectedAuditGraph::new();
let mut violation_nodes = Vec::new();
for (namespace, origin, entry) in all_audits.clone() {
let (from_ver, to_ver) = match &entry.kind {
AuditKind::Full { version } => (None, version),
AuditKind::Delta { from, to } => (Some(from), to),
AuditKind::Violation { .. } => {
violation_nodes.push((namespace.clone(), entry));
continue;
}
};
let criteria = criteria_mapper.criteria_from_list(&entry.criteria);
forward_audits.entry(from_ver).or_default().push(DeltaEdge {
version: Some(to_ver),
criteria: criteria.clone(),
origin: origin.clone(),
is_fresh_import: entry.is_fresh_import,
});
backward_audits
.entry(Some(to_ver))
.or_default()
.push(DeltaEdge {
version: from_ver,
criteria,
origin,
is_fresh_import: entry.is_fresh_import,
});
}
for (publisher_index, publisher) in publishers.iter().enumerate() {
for (_, import_index, audit_index, entry) in all_wildcard_audits.clone() {
if entry.user_id == publisher.user_id
&& *entry.start <= publisher.when
&& publisher.when < *entry.end
{
let from_ver = None;
let to_ver = Some(&publisher.version);
let criteria = criteria_mapper.criteria_from_list(&entry.criteria);
let origin = DeltaEdgeOrigin::WildcardAudit {
import_index,
audit_index,
publisher_index,
};
let is_fresh_import = entry.is_fresh_import || publisher.is_fresh_import;
forward_audits.entry(from_ver).or_default().push(DeltaEdge {
version: to_ver,
criteria: criteria.clone(),
origin: origin.clone(),
is_fresh_import,
});
backward_audits.entry(to_ver).or_default().push(DeltaEdge {
version: from_ver,
criteria,
origin,
is_fresh_import,
});
}
}
}
if let Some(alloweds) = exemptions {
for (exemption_index, allowed) in alloweds.iter().enumerate() {
let from_ver = None;
let to_ver = Some(&allowed.version);
let criteria = criteria_mapper.criteria_from_list(&allowed.criteria);
let origin = DeltaEdgeOrigin::Exemption { exemption_index };
forward_audits.entry(from_ver).or_default().push(DeltaEdge {
version: to_ver,
criteria: criteria.clone(),
origin: origin.clone(),
is_fresh_import: false,
});
backward_audits.entry(to_ver).or_default().push(DeltaEdge {
version: from_ver,
criteria,
origin,
is_fresh_import: false,
});
}
}
let mut violations = Vec::new();
for (violation_source, violation_entry) in &violation_nodes {
let violation_criterias = violation_entry
.criteria
.iter()
.map(|c| criteria_mapper.criteria_from_list([&c]))
.collect::<Vec<_>>();
let violation_range = if let AuditKind::Violation { violation } = &violation_entry.kind
{
violation
} else {
unreachable!("violation_entry wasn't a Violation?");
};
if let Some(alloweds) = exemptions {
for allowed in alloweds {
let audit_criteria = criteria_mapper.criteria_from_list(&allowed.criteria);
let has_violation = violation_criterias
.iter()
.any(|v| audit_criteria.contains(v));
if !has_violation {
continue;
}
if violation_range.matches(&allowed.version) {
violations.push(ViolationConflict::UnauditedConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
exemptions: allowed.clone(),
});
}
}
}
for (namespace, _origin, audit) in all_audits.clone() {
let audit_criteria = criteria_mapper.criteria_from_list(&audit.criteria);
let has_violation = violation_criterias
.iter()
.any(|v| audit_criteria.contains(v));
if !has_violation {
continue;
}
match &audit.kind {
AuditKind::Full { version, .. } => {
if violation_range.matches(version) {
violations.push(ViolationConflict::AuditConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
audit_source: namespace.clone(),
audit: audit.clone(),
});
}
}
AuditKind::Delta { from, to, .. } => {
if violation_range.matches(from) || violation_range.matches(to) {
violations.push(ViolationConflict::AuditConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
audit_source: namespace.clone(),
audit: audit.clone(),
});
}
}
AuditKind::Violation { .. } => {
}
}
}
}
if !violations.is_empty() {
return Err(violations);
}
Ok(AuditGraph {
forward_audits,
backward_audits,
})
}
pub fn search(
&self,
criteria_idx: usize,
version: &VetVersion,
mode: SearchMode,
) -> Result<Vec<DeltaEdgeOrigin>, SearchFailure> {
search_for_path(
&self.backward_audits,
criteria_idx,
Some(version),
None,
mode,
)
.map_err(|reachable_from_target| {
assert!(
mode != SearchMode::RegenerateExemptions,
"RegenerateExemptions search mode cannot fail"
);
let reachable_from_root = search_for_path(
&self.forward_audits,
criteria_idx,
None,
Some(version),
mode,
)
.unwrap_err();
SearchFailure {
reachable_from_root,
reachable_from_target,
}
})
}
}
fn search_for_path(
audit_graph: &DirectedAuditGraph<'_>,
criteria_idx: usize,
from_version: Option<&VetVersion>,
to_version: Option<&VetVersion>,
mode: SearchMode,
) -> Result<Vec<DeltaEdgeOrigin>, SortedSet<Option<VetVersion>>> {
assert!(
mode != SearchMode::RegenerateExemptions || to_version.is_none(),
"RegenerateExemptions requires searching towards root"
);
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum CaveatLevel {
None,
PreferredExemption,
FreshImport,
Exemption,
FreshExemption,
}
#[derive(Debug)]
struct Node<'a> {
version: Option<&'a VetVersion>,
origin_version: Option<&'a VetVersion>,
path: Vec<DeltaEdgeOrigin>,
caveat_level: CaveatLevel,
}
impl Node<'_> {
fn key(&self) -> impl Ord + '_ {
Reverse((
self.caveat_level,
self.version,
self.exemption_origin_version(),
self.path.len(),
self.path.last(),
))
}
fn exemption_origin_version(&self) -> Option<&VetVersion> {
if matches!(
self.caveat_level,
CaveatLevel::PreferredExemption
| CaveatLevel::Exemption
| CaveatLevel::FreshExemption
) {
self.origin_version
} else {
None
}
}
}
impl<'a> PartialEq for Node<'a> {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key()
}
}
impl<'a> Eq for Node<'a> {}
impl<'a> PartialOrd for Node<'a> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<'a> Ord for Node<'a> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.key().cmp(&other.key())
}
}
let mut queue = BinaryHeap::new();
queue.push(Node {
version: from_version,
origin_version: from_version,
path: Vec::new(),
caveat_level: CaveatLevel::None,
});
let mut visited = SortedSet::new();
while let Some(Node {
version,
origin_version: _,
path,
caveat_level,
}) = queue.pop()
{
if !visited.insert(version) {
continue;
}
if version == to_version {
return Ok(path);
}
let edges = audit_graph.get(&version).map(|v| &v[..]).unwrap_or(&[]);
for edge in edges {
let allow_any_criteria = mode == SearchMode::RegenerateExemptions
&& matches!(edge.origin, DeltaEdgeOrigin::Exemption { .. });
if !allow_any_criteria && !edge.criteria.has_criteria(criteria_idx) {
continue;
}
if visited.contains(&edge.version) {
continue;
}
let edge_caveat_level = match &edge.origin {
DeltaEdgeOrigin::Exemption { .. } if mode == SearchMode::PreferExemptions => {
CaveatLevel::PreferredExemption
}
DeltaEdgeOrigin::Exemption { .. } => CaveatLevel::Exemption,
DeltaEdgeOrigin::FreshExemption { .. } => unreachable!(),
_ if edge.is_fresh_import => CaveatLevel::FreshImport,
_ => CaveatLevel::None,
};
queue.push(Node {
version: edge.version,
origin_version: version,
path: path.iter().cloned().chain([edge.origin.clone()]).collect(),
caveat_level: caveat_level.max(edge_caveat_level),
});
}
if mode == SearchMode::RegenerateExemptions {
queue.push(Node {
version: None,
origin_version: version,
path: path
.iter()
.cloned()
.chain([DeltaEdgeOrigin::FreshExemption {
version: version
.expect("RegenerateExemptions requires searching towards None")
.clone(),
}])
.collect(),
caveat_level: caveat_level.max(CaveatLevel::FreshExemption),
})
}
}
Err(visited.into_iter().map(|v| v.cloned()).collect())
}
impl<'a> ResolveReport<'a> {
pub fn has_errors(&self) -> bool {
!matches!(self.conclusion, Conclusion::Success(_))
}
pub fn _has_warnings(&self) -> bool {
false
}
pub fn compute_suggest(
&self,
cfg: &Config,
store: &Store,
network: Option<&Network>,
) -> Result<Option<Suggest>, SuggestError> {
let _suggest_span = trace_span!("suggest").entered();
let fail = if let Conclusion::FailForVet(fail) = &self.conclusion {
fail
} else {
return Ok(None);
};
let cache = Cache::acquire(cfg)?;
let warnings = RefCell::new(Vec::new());
let mut store = store.clone_for_suggest(false);
let registry = if let (false, OutputFormat::Human, Some(network)) = (
cfg.cli.no_registry_suggestions,
cfg.cli.output_format,
network,
) {
tokio::runtime::Handle::current()
.block_on(store.fetch_registry_audits(cfg, network, &cache))
.map_err(|error| warnings.borrow_mut().push(error.to_string()))
.ok()
} else {
None
};
let suggest_progress =
progress_bar("Suggesting", "relevant audits", fail.failures.len() as u64);
let mut suggestions = tokio::runtime::Handle::current()
.block_on(join_all(fail.failures.iter().map(
|(failure_idx, audit_failure)| async {
let _guard = IncProgressOnDrop(&suggest_progress, 1);
let failure_idx = *failure_idx;
let package = &self.graph.nodes[failure_idx];
let result = self.results[failure_idx]
.as_ref()
.expect("failed package without ResolveResults?");
let notable_parents = {
let mut reverse_deps = self.graph.nodes[failure_idx]
.reverse_deps
.iter()
.map(|&parent| self.graph.nodes[parent].name.to_string())
.collect::<Vec<_>>();
reverse_deps.sort();
reverse_deps.sort_by_key(|item| item.len());
let cutoff_index = reverse_deps
.iter()
.scan(0, |sum, s| {
*sum += s.len();
Some(*sum)
})
.position(|count| count > 20);
let remainder = cutoff_index.map(|i| reverse_deps.len() - i).unwrap_or(0);
if remainder > 1 {
reverse_deps.truncate(cutoff_index.unwrap());
reverse_deps.push(format!("and {remainder} others"));
}
reverse_deps.join(", ")
};
let suggested_diff = suggest_delta(
&cfg.metadata,
network,
&cache,
package,
audit_failure
.criteria_failures
.indices()
.map(|criteria_idx| {
result.search_results[criteria_idx].as_ref().unwrap_err()
}),
&warnings,
)
.await?;
let mut registry_suggestion: Vec<_> = join_all(registry.iter().flatten().map(
|(name, entry, audits)| async {
let audit_graph = AuditGraph::build(
&store,
&self.criteria_mapper,
package.name,
Some(audits),
)
.ok()?;
let failures: Vec<_> = audit_failure
.criteria_failures
.indices()
.filter_map(|criteria_idx| {
audit_graph
.search(
criteria_idx,
&package.version,
SearchMode::PreferExemptions,
)
.err()
})
.collect();
let registry_suggested_diff = suggest_delta(
&cfg.metadata,
network,
&cache,
package,
failures.iter(),
&warnings,
)
.await?;
if registry_suggested_diff.diffstat.count()
< suggested_diff.diffstat.count()
{
Some(RegistrySuggestion {
name: name.clone(),
url: entry.url.clone(),
diff: registry_suggested_diff,
})
} else {
None
}
},
))
.await
.into_iter()
.flatten()
.collect();
registry_suggestion.sort_by_key(|suggestion| suggestion.diff.diffstat.count());
Some(SuggestItem {
package: failure_idx,
suggested_diff,
suggested_criteria: audit_failure.criteria_failures.clone(),
notable_parents,
registry_suggestion,
})
},
)))
.into_iter()
.flatten()
.collect::<Vec<_>>();
let total_lines = suggestions
.iter()
.map(|s| s.suggested_diff.diffstat.count())
.sum();
suggestions.sort_by_key(|item| &self.graph.nodes[item.package].version);
suggestions.sort_by_key(|item| self.graph.nodes[item.package].name);
suggestions.sort_by_key(|item| item.suggested_diff.diffstat.count());
let mut suggestions_by_criteria = SortedMap::<CriteriaName, Vec<SuggestItem>>::new();
for s in suggestions.clone().into_iter() {
let criteria_names = self
.criteria_mapper
.criteria_names(&s.suggested_criteria)
.collect::<Vec<_>>()
.join(", ");
suggestions_by_criteria
.entry(criteria_names)
.or_default()
.push(s);
}
Ok(Some(Suggest {
suggestions,
suggestions_by_criteria,
total_lines,
warnings: warnings.into_inner(),
}))
}
pub fn compute_suggested_criteria(
&self,
package_name: PackageStr<'_>,
from: Option<&VetVersion>,
to: &VetVersion,
) -> Vec<CriteriaName> {
let fail = if let Conclusion::FailForVet(fail) = &self.conclusion {
fail
} else {
return Vec::new();
};
let mut criteria = self.criteria_mapper.no_criteria();
let from = from.cloned();
let to = Some(to.clone());
for (failure_idx, audit_failure) in &fail.failures {
let package = &self.graph.nodes[*failure_idx];
if package.name != package_name {
continue;
}
let result = &self.results[*failure_idx]
.as_ref()
.expect("failure without ResolveResults?");
for criteria_idx in audit_failure.criteria_failures.indices() {
let search_result = &result.search_results[criteria_idx];
if let Err(SearchFailure {
reachable_from_root,
reachable_from_target,
}) = search_result
{
if reachable_from_target.contains(&to) && reachable_from_root.contains(&from) {
criteria.set_criteria(criteria_idx);
}
}
}
}
self.criteria_mapper
.criteria_names(&criteria)
.map(str::to_owned)
.collect()
}
pub fn print_human(
&self,
out: &Arc<dyn Out>,
cfg: &Config,
suggest: Option<&Suggest>,
) -> Result<(), std::io::Error> {
match &self.conclusion {
Conclusion::Success(res) => res.print_human(out, self, cfg),
Conclusion::FailForViolationConflict(res) => res.print_human(out, self, cfg),
Conclusion::FailForVet(res) => res.print_human(out, self, cfg, suggest),
}
}
pub fn print_suggest_human(
&self,
out: &Arc<dyn Out>,
_cfg: &Config,
suggest: Option<&Suggest>,
) -> Result<(), std::io::Error> {
if let Some(suggest) = suggest {
suggest.print_human(out, self)?;
} else {
writeln!(out, "Nothing to suggest, you're fully audited!");
}
Ok(())
}
pub fn print_json(
&self,
out: &Arc<dyn Out>,
suggest: Option<&Suggest>,
) -> Result<(), miette::Report> {
let result = JsonReport {
conclusion: match &self.conclusion {
Conclusion::Success(success) => {
let json_package = |pkgidx: &PackageIdx| {
let package = &self.graph.nodes[*pkgidx];
JsonPackage {
name: package.name.to_owned(),
version: package.version.clone(),
}
};
JsonReportConclusion::Success(JsonReportSuccess {
vetted_fully: success.vetted_fully.iter().map(json_package).collect(),
vetted_partially: success
.vetted_partially
.iter()
.map(json_package)
.collect(),
vetted_with_exemptions: success
.vetted_with_exemptions
.iter()
.map(json_package)
.collect(),
})
}
Conclusion::FailForViolationConflict(fail) => {
JsonReportConclusion::FailForViolationConflict(
JsonReportFailForViolationConflict {
violations: fail
.violations
.iter()
.map(|(pkgidx, violations)| {
let package = &self.graph.nodes[*pkgidx];
let key = format!("{}:{}", package.name, package.version);
(key, violations.clone())
})
.collect(),
},
)
}
Conclusion::FailForVet(fail) => {
let json_suggest_item = |item: &SuggestItem| {
let package = &self.graph.nodes[item.package];
JsonSuggestItem {
name: package.name.to_owned(),
notable_parents: item.notable_parents.to_owned(),
suggested_criteria: self
.criteria_mapper
.criteria_names(&item.suggested_criteria)
.map(|s| s.to_owned())
.collect(),
suggested_diff: item.suggested_diff.clone(),
}
};
JsonReportConclusion::FailForVet(JsonReportFailForVet {
failures: fail
.failures
.iter()
.map(|(pkgidx, audit_fail)| {
let package = &self.graph.nodes[*pkgidx];
JsonVetFailure {
name: package.name.to_owned(),
version: package.version.clone(),
missing_criteria: self
.criteria_mapper
.criteria_names(&audit_fail.criteria_failures)
.map(|s| s.to_owned())
.collect(),
}
})
.collect(),
suggest: suggest.as_ref().map(|suggest| JsonSuggest {
suggestions: suggest
.suggestions
.iter()
.map(json_suggest_item)
.collect(),
suggest_by_criteria: suggest
.suggestions_by_criteria
.iter()
.map(|(criteria, items)| {
(
criteria.to_owned(),
items.iter().map(json_suggest_item).collect::<Vec<_>>(),
)
})
.collect(),
total_lines: suggest.total_lines,
}),
})
}
},
};
serde_json::to_writer_pretty(&**out, &result).into_diagnostic()?;
Ok(())
}
}
impl Success {
pub fn print_human(
&self,
out: &Arc<dyn Out>,
_report: &ResolveReport<'_>,
_cfg: &Config,
) -> Result<(), std::io::Error> {
let fully_audited_count = self.vetted_fully.len();
let partially_audited_count: usize = self.vetted_partially.len();
let exemptions_count = self.vetted_with_exemptions.len();
let mut count_count = (fully_audited_count != 0) as usize
+ (partially_audited_count != 0) as usize
+ (exemptions_count != 0) as usize;
if count_count == 0 {
writeln!(
out,
"Vetting Succeeded (because you have no third-party dependencies)"
);
} else {
write!(out, "Vetting Succeeded (");
if fully_audited_count != 0 {
write!(out, "{fully_audited_count} fully audited");
count_count -= 1;
if count_count > 0 {
write!(out, ", ");
}
}
if partially_audited_count != 0 {
write!(out, "{partially_audited_count} partially audited");
count_count -= 1;
if count_count > 0 {
write!(out, ", ");
}
}
if exemptions_count != 0 {
write!(out, "{exemptions_count} exempted");
count_count -= 1;
if count_count > 0 {
write!(out, ", ");
}
}
writeln!(out, ")");
}
Ok(())
}
}
impl Suggest {
pub fn print_human(
&self,
out: &Arc<dyn Out>,
report: &ResolveReport<'_>,
) -> Result<(), std::io::Error> {
for (criteria, suggestions) in &self.suggestions_by_criteria {
writeln!(out, "recommended audits for {criteria}:");
let strings = suggestions
.iter()
.map(|item| {
let package = &report.graph.nodes[item.package];
let cmd = match &item.suggested_diff.from {
Some(from) => format!(
"cargo vet diff {} {} {}",
package.name, from, item.suggested_diff.to
),
None => format!(
"cargo vet inspect {} {}",
package.name, item.suggested_diff.to
),
};
let parents = format!("(used by {})", item.notable_parents);
let diffstat = match &item.suggested_diff.from {
Some(_) => format!("({})", item.suggested_diff.diffstat),
None => format!("({} lines)", item.suggested_diff.diffstat.count()),
};
(cmd, parents, diffstat, item)
})
.collect::<Vec<_>>();
let mut max0 = 0;
let mut max1 = 0;
for (s0, s1, ..) in &strings {
max0 = max0.max(console::measure_text_width(s0));
max1 = max1.max(console::measure_text_width(s1));
}
for (s0, s1, s2, item) in strings {
write!(
out,
"{}",
out.style()
.cyan()
.bold()
.apply_to(format_args!(" {s0:max0$}"))
);
writeln!(out, " {s1:max1$} {s2}");
let dim = out.style().dim();
for suggestion in &item.registry_suggestion {
writeln!(
out,
" {} {} {}",
dim.clone().apply_to("NOTE:"),
dim.clone()
.cyan()
.bold()
.apply_to(format_args!("cargo vet import {}", suggestion.name)),
dim.clone()
.apply_to(match suggestion.diff.diffstat.count() {
0 => "would eliminate this".to_owned(),
n => format!("would reduce this to a {n}-line diff"),
}),
);
}
}
writeln!(out);
}
writeln!(out, "estimated audit backlog: {} lines", self.total_lines);
if !self.warnings.is_empty() {
writeln!(out);
for warning in &self.warnings {
writeln!(
out,
"{}: {warning}",
out.style().yellow().apply_to("WARNING"),
);
}
}
writeln!(out);
writeln!(out, "Use |cargo vet certify| to record the audits.");
Ok(())
}
}
impl FailForVet {
fn print_human(
&self,
out: &Arc<dyn Out>,
report: &ResolveReport<'_>,
_cfg: &Config,
suggest: Option<&Suggest>,
) -> Result<(), std::io::Error> {
writeln!(out, "Vetting Failed!");
writeln!(out);
writeln!(out, "{} unvetted dependencies:", self.failures.len());
let mut failures = self
.failures
.iter()
.map(|(failed_idx, failure)| (&report.graph.nodes[*failed_idx], failure))
.collect::<Vec<_>>();
failures.sort_by_key(|(failed, _)| &failed.version);
failures.sort_by_key(|(failed, _)| failed.name);
for (failed_package, failed_audit) in failures {
let criteria = report
.criteria_mapper
.criteria_names(&failed_audit.criteria_failures)
.collect::<Vec<_>>();
let label = format!(" {}:{}", failed_package.name, failed_package.version);
writeln!(out, "{label} missing {criteria:?}");
}
if let Some(suggest) = suggest {
writeln!(out);
suggest.print_human(out, report)?;
}
Ok(())
}
}
impl FailForViolationConflict {
fn print_human(
&self,
out: &Arc<dyn Out>,
report: &ResolveReport<'_>,
_cfg: &Config,
) -> Result<(), std::io::Error> {
writeln!(out, "Violations Found!");
for (pkgidx, violations) in &self.violations {
let package = &report.graph.nodes[*pkgidx];
writeln!(out, " {}:{}", package.name, package.version);
for violation in violations {
match violation {
ViolationConflict::UnauditedConflict {
violation_source,
violation,
exemptions,
} => {
write!(out, " the ");
print_exemption(out, exemptions)?;
write!(out, " conflicts with ");
print_entry(out, violation_source, violation)?;
}
ViolationConflict::AuditConflict {
violation_source,
violation,
audit_source,
audit,
} => {
write!(out, " the ");
print_entry(out, audit_source, audit)?;
write!(out, " conflicts with ");
print_entry(out, violation_source, violation)?;
}
}
writeln!(out);
}
}
fn print_exemption(
out: &Arc<dyn Out>,
entry: &ExemptedDependency,
) -> Result<(), std::io::Error> {
writeln!(out, "exemption {}", entry.version);
writeln!(out, " criteria: {:?}", entry.criteria);
if let Some(notes) = &entry.notes {
writeln!(out, " notes: {notes}");
}
Ok(())
}
fn print_entry(
out: &Arc<dyn Out>,
source: &Option<ImportName>,
entry: &AuditEntry,
) -> Result<(), std::io::Error> {
match source {
None => write!(out, "own "),
Some(name) => write!(out, "foreign ({name}) "),
}
match &entry.kind {
AuditKind::Full { version, .. } => {
writeln!(out, "audit {version}");
}
AuditKind::Delta { from, to, .. } => {
writeln!(out, "audit {from} -> {to}");
}
AuditKind::Violation { violation } => {
writeln!(out, "violation against {violation}");
}
}
writeln!(out, " criteria: {:?}", entry.criteria);
for (idx, who) in entry.who.iter().enumerate() {
if idx == 0 {
write!(out, " who: {who}");
} else {
write!(out, ", {who}");
}
}
if !entry.who.is_empty() {
writeln!(out);
}
if let Some(notes) = &entry.notes {
writeln!(out, " notes: {notes}");
}
Ok(())
}
Ok(())
}
}
async fn suggest_delta(
metadata: &cargo_metadata::Metadata,
network: Option<&Network>,
cache: &Cache,
package: &PackageNode<'_>,
failures: impl Iterator<Item = &SearchFailure>,
warnings: &RefCell<Vec<String>>,
) -> Option<DiffRecommendation> {
struct Reachable<'a> {
from_root: SortedSet<&'a Option<VetVersion>>,
from_target: SortedSet<&'a Option<VetVersion>>,
}
let mut reachable = None::<Reachable<'_>>;
for SearchFailure {
reachable_from_root,
reachable_from_target,
} in failures
{
if let Some(Reachable {
from_root,
from_target,
}) = reachable.as_mut()
{
from_root.retain(|ver| reachable_from_root.contains(ver));
from_target.retain(|ver| reachable_from_target.contains(ver));
} else {
let version_has_sources = |ver: &&Option<VetVersion>| -> bool {
ver.as_ref() == Some(&package.version)
|| !matches!(
ver,
Some(VetVersion {
git_rev: Some(_),
..
})
)
};
reachable = Some(Reachable {
from_root: reachable_from_root
.iter()
.filter(version_has_sources)
.collect(),
from_target: reachable_from_target
.iter()
.filter(version_has_sources)
.collect(),
});
}
}
let Some(Reachable { from_root, from_target }) = &reachable else {
return Some(DiffRecommendation {
from: Some(package.version.clone()),
to: package.version.clone(),
diffstat: DiffStat { insertions: 0, deletions: 0, files_changed: 0 },
});
};
let mut candidates = SortedSet::new();
for &dest in from_target {
let closest_above = from_root.range::<&Option<VetVersion>, _>(dest..).next();
let closest_below = from_root
.range::<&Option<VetVersion>, _>(..dest)
.next_back();
for &closest in closest_below.into_iter().chain(closest_above) {
candidates.insert(Delta {
from: closest.clone(),
to: dest.clone().unwrap(),
});
}
}
let diffstats = join_all(candidates.iter().map(|delta| async {
match cache
.fetch_and_diffstat_package(metadata, network, package.name, delta)
.await
{
Ok(diffstat) => Some(DiffRecommendation {
diffstat,
from: delta.from.clone(),
to: delta.to.clone(),
}),
Err(err) => {
warnings.borrow_mut().push(format!(
"error diffing {}:{}: {}",
package.name, package.version, err
));
None
}
}
}))
.await;
diffstats
.into_iter()
.flatten()
.min_by_key(|diff| diff.diffstat.count())
}
#[tracing::instrument(skip(graph, criteria_mapper, requirements, store))]
fn resolve_package_required_entries(
graph: &DepGraph<'_>,
criteria_mapper: &CriteriaMapper,
requirements: &[CriteriaSet],
store: &Store,
package_name: PackageStr<'_>,
search_mode: SearchMode,
) -> Option<SortedMap<RequiredEntry, CriteriaSet>> {
assert_eq!(graph.nodes.len(), requirements.len());
let packages: Vec<_> = graph
.nodes
.iter()
.zip(requirements)
.filter(|(package, _)| package.name == package_name && package.is_third_party)
.collect();
if packages.is_empty() {
return Some(SortedMap::new());
}
let Ok(audit_graph) = AuditGraph::build(store, criteria_mapper, package_name, None) else {
return None;
};
let mut required_entries = SortedMap::new();
for &(package, reqs) in &packages {
for criteria_idx in criteria_mapper.minimal_indices(reqs) {
let Ok(path) = audit_graph.search(criteria_idx, &package.version, search_mode) else {
return None;
};
let mut add_entry = |entry: RequiredEntry| {
required_entries
.entry(entry)
.or_insert_with(|| criteria_mapper.no_criteria())
.set_criteria(criteria_idx);
};
for origin in path {
match origin {
DeltaEdgeOrigin::Exemption { exemption_index } => {
add_entry(RequiredEntry::Exemption { exemption_index });
}
DeltaEdgeOrigin::FreshExemption { version } => {
add_entry(RequiredEntry::FreshExemption { version });
}
DeltaEdgeOrigin::ImportedAudit {
import_index,
audit_index,
} => {
add_entry(RequiredEntry::Audit {
import_index,
audit_index,
});
}
DeltaEdgeOrigin::WildcardAudit {
import_index,
audit_index,
publisher_index,
} => {
if let Some(import_index) = import_index {
add_entry(RequiredEntry::WildcardAudit {
import_index,
audit_index,
})
}
add_entry(RequiredEntry::Publisher { publisher_index })
}
DeltaEdgeOrigin::StoredLocalAudit { .. } => {}
}
}
}
continue;
}
Some(required_entries)
}
pub struct UpdateMode {
pub search_mode: SearchMode,
pub prune_exemptions: bool,
pub prune_imports: bool,
}
pub fn update_store(
cfg: &Config,
store: &mut Store,
mode: impl FnMut(PackageStr<'_>) -> UpdateMode,
) {
let (new_imports, new_exemptions) = get_store_updates(cfg, store, mode);
store.imports = new_imports;
store.config.exemptions = new_exemptions;
}
pub(crate) fn get_store_updates(
cfg: &Config,
store: &Store,
mut mode: impl FnMut(PackageStr<'_>) -> UpdateMode,
) -> (ImportsFile, SortedMap<PackageName, Vec<ExemptedDependency>>) {
let graph = DepGraph::new(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
Some(&store.config.policy),
);
let criteria_mapper = CriteriaMapper::new(&store.audits.criteria);
let requirements = resolve_requirements(&graph, &store.config.policy, &criteria_mapper);
let mut required_entries = SortedMap::new();
for package in &graph.nodes {
required_entries.entry(package.name).or_insert_with(|| {
resolve_package_required_entries(
&graph,
&criteria_mapper,
&requirements,
store,
package.name,
mode(package.name).search_mode,
)
});
}
let no_required_entries = Some(SortedMap::new());
let mut new_imports = ImportsFile {
publisher: SortedMap::new(),
audits: SortedMap::new(),
};
for (import_index, (import_name, live_audits_file)) in
store.imported_audits().iter().enumerate()
{
let new_audits_file = AuditsFile {
criteria: live_audits_file.criteria.clone(),
wildcard_audits: live_audits_file
.wildcard_audits
.iter()
.map(|(pkgname, wildcard_audits)| {
let prune_imports = mode(&pkgname[..]).prune_imports;
let required_entries = required_entries
.get(&pkgname[..])
.unwrap_or(&no_required_entries);
(
pkgname,
wildcard_audits
.iter()
.enumerate()
.filter(|&(audit_index, entry)| {
if !prune_imports && !entry.is_fresh_import {
return true;
}
if let Some(required_entries) = required_entries {
required_entries.contains_key(&RequiredEntry::WildcardAudit {
import_index,
audit_index,
})
} else {
!entry.is_fresh_import
}
})
.map(|(_, entry)| WildcardEntry {
is_fresh_import: false,
..entry.clone()
})
.collect::<Vec<_>>(),
)
})
.filter(|(_, l)| !l.is_empty())
.map(|(n, mut l)| {
l.sort();
(n.clone(), l)
})
.collect(),
audits: live_audits_file
.audits
.iter()
.map(|(pkgname, audits)| {
let prune_imports = mode(&pkgname[..]).prune_imports;
let required_entries = required_entries
.get(&pkgname[..])
.unwrap_or(&no_required_entries);
(
pkgname,
audits
.iter()
.enumerate()
.filter(|&(audit_index, entry)| {
if matches!(entry.kind, AuditKind::Violation { .. }) {
return true;
}
if !prune_imports && !entry.is_fresh_import {
return true;
}
if let Some(required_entries) = required_entries {
required_entries.contains_key(&RequiredEntry::Audit {
import_index,
audit_index,
})
} else {
!entry.is_fresh_import
}
})
.map(|(_, entry)| AuditEntry {
is_fresh_import: false,
..entry.clone()
})
.collect::<Vec<_>>(),
)
})
.filter(|(_, l)| !l.is_empty())
.map(|(n, mut l)| {
l.sort();
(n.clone(), l)
})
.collect(),
};
new_imports
.audits
.insert(import_name.clone(), new_audits_file);
}
for (pkgname, publishers) in store.publishers() {
let prune_imports = mode(&pkgname[..]).prune_imports;
let required_entries = required_entries
.get(&pkgname[..])
.unwrap_or(&no_required_entries);
let mut publishers: Vec<_> = publishers
.iter()
.enumerate()
.filter(|&(publisher_index, entry)| {
if !prune_imports && !entry.is_fresh_import {
return true;
}
if let Some(required_entries) = required_entries {
required_entries.contains_key(&RequiredEntry::Publisher { publisher_index })
} else {
!entry.is_fresh_import
}
})
.map(|(_, entry)| CratesPublisher {
is_fresh_import: false,
..entry.clone()
})
.collect();
publishers.sort();
if !publishers.is_empty() {
new_imports.publisher.insert(pkgname.clone(), publishers);
}
}
let mut all_new_exemptions = SortedMap::new();
for (pkgname, exemptions) in &store.config.exemptions {
let prune_exemptions = mode(pkgname).prune_exemptions;
let required_entries = required_entries
.get(&pkgname[..])
.unwrap_or(&no_required_entries);
let mut new_exemptions = Vec::with_capacity(exemptions.len());
for (exemption_index, entry) in exemptions.iter().enumerate() {
let original_criteria = criteria_mapper.criteria_from_list(&entry.criteria);
let mut useful_criteria = if let Some(required_entries) = required_entries {
required_entries
.get(&RequiredEntry::Exemption { exemption_index })
.cloned()
.unwrap_or_else(|| criteria_mapper.no_criteria())
} else {
original_criteria.clone()
};
if !prune_exemptions {
useful_criteria.unioned_with(&original_criteria);
}
if useful_criteria.is_empty() {
continue; }
if !entry.suggest && !original_criteria.contains(&useful_criteria) {
let mut extra_criteria = useful_criteria.clone();
extra_criteria.clear_criteria(&original_criteria);
new_exemptions.push(ExemptedDependency {
version: entry.version.clone(),
criteria: criteria_mapper
.criteria_names(&extra_criteria)
.map(|n| n.to_owned().into())
.collect(),
suggest: true,
notes: None,
});
useful_criteria = original_criteria;
}
new_exemptions.push(ExemptedDependency {
version: entry.version.clone(),
criteria: criteria_mapper
.criteria_names(&useful_criteria)
.map(|n| n.to_owned().into())
.collect(),
suggest: entry.suggest,
notes: entry.notes.clone(),
});
}
if !new_exemptions.is_empty() {
all_new_exemptions.insert(pkgname.clone(), new_exemptions);
}
}
for (&pkgname, required_entries) in &required_entries {
let Some(required_entries) = required_entries else { continue };
for (entry, criteria) in required_entries.iter().rev() {
let RequiredEntry::FreshExemption { version } = entry else {
break;
};
all_new_exemptions
.entry(pkgname.to_owned())
.or_default()
.push(ExemptedDependency {
version: version.clone(),
criteria: criteria_mapper
.criteria_names(criteria)
.map(|n| n.to_owned().into())
.collect(),
suggest: true,
notes: None,
});
}
}
for exemptions in all_new_exemptions.values_mut() {
exemptions.sort();
}
(new_imports, all_new_exemptions)
}