use cargo_metadata::{DependencyKind, Metadata, Node, PackageId, Version};
use core::fmt;
use futures_util::future::join_all;
use miette::IntoDiagnostic;
use serde::Serialize;
use serde_json::json;
use std::sync::Arc;
use tracing::{error, trace, trace_span, warn};
use crate::errors::SuggestError;
use crate::format::{
self, AuditKind, CriteriaName, CriteriaStr, Delta, DiffStat, ExemptedDependency, ImportName,
PackageName, PackageStr, PolicyEntry,
};
use crate::format::{FastMap, FastSet, SortedMap, SortedSet};
use crate::network::Network;
use crate::out::Out;
use crate::serialization::spanned::Spanned;
use crate::{
AuditEntry, Cache, Config, CriteriaEntry, DumpGraphArgs, GraphFilter, GraphFilterProperty,
GraphFilterQuery, PackageExt, Store,
};
#[derive(Debug, Clone)]
pub struct ResolveReport<'a> {
pub graph: DepGraph<'a>,
pub criteria_mapper: CriteriaMapper,
pub results: Vec<ResolveResult<'a>>,
pub conclusion: Conclusion,
}
#[derive(Debug, Clone)]
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: SortedMap<PackageIdx, Vec<ViolationConflict>>,
}
#[derive(Debug, Clone)]
pub struct FailForVet {
pub failures: SortedMap<PackageIdx, AuditFailure>,
pub suggest: Option<Suggest>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize)]
pub enum ViolationConflict {
UnauditedConflict {
violation_source: AuditSource,
violation: AuditEntry,
exemptions: ExemptedDependency,
},
AuditConflict {
violation_source: AuditSource,
violation: AuditEntry,
audit_source: AuditSource,
audit: AuditEntry,
},
}
#[derive(Debug, Clone, Serialize)]
pub enum AuditSource {
OwnAudits,
Foreign(ImportName),
}
#[derive(Debug, Clone, Default)]
pub struct Suggest {
pub suggestions: Vec<SuggestItem>,
pub suggestions_by_criteria: SortedMap<CriteriaName, Vec<SuggestItem>>,
pub total_lines: u64,
}
#[derive(Debug, Clone)]
pub struct SuggestItem {
pub package: PackageIdx,
pub suggested_criteria: CriteriaFailureSet,
pub suggested_diff: DiffRecommendation,
pub notable_parents: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DiffRecommendation {
pub from: Version,
pub to: Version,
pub diffstat: DiffStat,
}
#[derive(Clone, Default)]
pub struct CriteriaSet(u64);
const MAX_CRITERIA: usize = u64::BITS as usize;
#[derive(Debug, Clone, Default)]
pub struct CriteriaFailureSet {
pub confident: CriteriaSet,
pub all: CriteriaSet,
}
#[derive(Debug, Clone)]
pub struct CriteriaMapper {
pub list: Vec<(CriteriaName, CriteriaEntry)>,
pub index: FastMap<CriteriaName, usize>,
pub implied_criteria: Vec<CriteriaSet>,
}
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: &'a Version,
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<'a> {
pub validated_criteria: CriteriaSet,
pub fully_audited_criteria: CriteriaSet,
pub search_results: Vec<SearchResult<'a>>,
pub directly_exempted: bool,
pub needed_exemption: bool,
}
pub type PolicyFailures = SortedMap<PackageIdx, CriteriaSet>;
pub type RootFailures = Vec<(PackageIdx, PolicyFailures, bool)>;
#[derive(Default, Debug, Clone)]
pub struct AuditFailure {
pub criteria_failures: CriteriaFailureSet,
}
#[derive(Debug, Clone)]
pub enum SearchResult<'a> {
Connected {
fully_audited: bool,
},
PossiblyConnected {
failed_deps: SortedMap<PackageIdx, CriteriaSet>,
},
Disconnected {
reachable_from_root: SortedSet<&'a Version>,
reachable_from_target: SortedSet<&'a Version>,
},
}
pub type AuditGraph<'a> = SortedMap<&'a Version, Vec<DeltaEdge<'a>>>;
#[derive(Debug, Clone)]
pub struct DeltaEdge<'a> {
version: &'a Version,
criteria: CriteriaSet,
dependency_criteria: FastMap<PackageStr<'a>, CriteriaSet>,
is_exemption: bool,
}
fn builtin_criteria() -> SortedMap<CriteriaName, CriteriaEntry> {
[
(
"safe-to-run".to_string(),
CriteriaEntry {
description: Some("safe to run locally".to_string()),
description_url: None,
implies: vec![],
},
),
(
"safe-to-deploy".to_string(),
CriteriaEntry {
description: Some("safe to deploy to production".to_string()),
description_url: None,
implies: vec!["safe-to-run".to_string().into()],
},
),
]
.into_iter()
.collect()
}
impl CriteriaMapper {
pub fn new(criteria: &SortedMap<CriteriaName, CriteriaEntry>) -> CriteriaMapper {
let builtins = builtin_criteria();
let list = criteria
.iter()
.chain(builtins.iter())
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<Vec<_>>();
let index: FastMap<CriteriaName, usize> = list
.iter()
.enumerate()
.map(|(idx, v)| (v.0.clone(), idx))
.collect();
let mut implied_criteria = Vec::with_capacity(list.len());
for (idx, (_name, entry)) in list.iter().enumerate() {
let mut implied = CriteriaSet::none(list.len());
implied.set_criteria(idx);
recursive_implies(&mut implied, &entry.implies, &index, &list);
implied_criteria.push(implied);
fn recursive_implies(
result: &mut CriteriaSet,
implies: &[Spanned<CriteriaName>],
index: &FastMap<CriteriaName, usize>,
list: &[(CriteriaName, CriteriaEntry)],
) {
for implied in implies {
let idx = index[&**implied];
result.set_criteria(idx);
let further_implies = &list[idx].1.implies[..];
recursive_implies(result, further_implies, index, list);
}
}
}
Self {
list,
index,
implied_criteria,
}
}
pub fn criteria_from_entry(&self, entry: &AuditEntry) -> CriteriaSet {
self.criteria_from_list(&entry.criteria)
}
pub fn criteria_from_list<'b, S: AsRef<str> + 'b + ?Sized>(
&self,
list: impl IntoIterator<Item = &'b S>,
) -> CriteriaSet {
let mut result = self.no_criteria();
for criteria in list {
let idx = self.index[criteria.as_ref()];
result.unioned_with(&self.implied_criteria[idx]);
}
result
}
pub fn set_criteria(&self, set: &mut CriteriaSet, criteria: CriteriaStr) {
set.set_criteria(self.index[criteria])
}
pub fn clear_criteria(&self, set: &mut CriteriaFailureSet, criteria: CriteriaStr) {
set.clear_criteria(&self.implied_criteria[self.index[criteria]])
}
pub fn all_criteria_iter(&self) -> impl Iterator<Item = &CriteriaSet> {
self.implied_criteria.iter()
}
pub fn len(&self) -> usize {
self.list.len()
}
pub fn is_empty(&self) -> bool {
self.list.is_empty()
}
pub fn no_criteria(&self) -> CriteriaSet {
CriteriaSet::none(self.len())
}
pub fn all_criteria(&self) -> CriteriaSet {
CriteriaSet::_all(self.len())
}
pub fn no_criteria_failures(&self) -> CriteriaFailureSet {
CriteriaFailureSet::none(self.len())
}
pub fn criteria_names<'a>(
&'a self,
criteria: &'a CriteriaSet,
) -> impl Iterator<Item = CriteriaStr<'a>> + 'a {
criteria
.indices()
.filter(|&cur_idx| {
criteria.indices().all(|other_idx| {
cur_idx == other_idx || !self.implied_criteria[other_idx].has_criteria(cur_idx)
})
})
.map(|idx| &*self.list[idx].0)
}
pub fn all_criteria_names<'a>(
&'a self,
criteria: &'a CriteriaFailureSet,
) -> impl Iterator<Item = CriteriaStr<'a>> + 'a {
self.criteria_names(criteria.all())
}
pub fn confident_criteria_names<'a>(
&'a self,
criteria: &'a CriteriaFailureSet,
) -> impl Iterator<Item = CriteriaStr<'a>> + 'a {
self.criteria_names(criteria.confident())
}
pub fn unconfident_criteria_names<'a>(
&'a self,
criteria: &'a CriteriaFailureSet,
) -> impl Iterator<Item = CriteriaStr<'a>> + 'a {
criteria
.all()
.indices()
.filter(|&cur_idx| {
!criteria.confident().has_criteria(cur_idx)
&& criteria.all().indices().all(|other_idx| {
cur_idx == other_idx
|| !self.implied_criteria[other_idx].has_criteria(cur_idx)
})
})
.map(|idx| &*self.list[idx].0)
}
}
impl CriteriaSet {
pub fn none(count: usize) -> Self {
assert!(
count <= MAX_CRITERIA,
"{MAX_CRITERIA} was not Enough For Everyone ({count} criteria)"
);
CriteriaSet(0)
}
pub fn _all(count: usize) -> Self {
assert!(
count <= MAX_CRITERIA,
"{MAX_CRITERIA} was not Enough For Everyone ({count} criteria)"
);
if count == MAX_CRITERIA {
CriteriaSet(!0)
} else {
CriteriaSet((1 << count) - 1)
}
}
pub fn set_criteria(&mut self, idx: usize) {
self.0 |= 1 << idx;
}
pub fn clear_criteria(&mut self, other: &CriteriaSet) {
self.0 &= !other.0;
}
pub fn has_criteria(&self, idx: usize) -> bool {
(self.0 & (1 << idx)) != 0
}
pub fn _intersected_with(&mut self, other: &CriteriaSet) {
self.0 &= other.0;
}
pub fn unioned_with(&mut self, other: &CriteriaSet) {
self.0 |= other.0;
}
pub fn contains(&self, other: &CriteriaSet) -> bool {
(self.0 & other.0) == other.0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn indices(&self) -> impl Iterator<Item = usize> + '_ {
let mut raw = self.0;
std::iter::from_fn(move || {
if raw == 0 {
None
} else {
let next = raw.trailing_zeros() as usize;
raw &= !(1 << next);
Some(next)
}
})
}
}
impl fmt::Debug for CriteriaSet {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{:08b}", self.0)
}
}
impl CriteriaFailureSet {
pub fn none(count: usize) -> Self {
CriteriaFailureSet {
confident: CriteriaSet::none(count),
all: CriteriaSet::none(count),
}
}
pub fn from(criteria: &CriteriaSet, confident: bool) -> Self {
CriteriaFailureSet {
confident: if confident {
criteria.clone()
} else {
CriteriaSet::default()
},
all: criteria.clone(),
}
}
pub fn set_criteria(&mut self, idx: usize, confident: bool) {
self.all.set_criteria(idx);
if confident {
self.confident.set_criteria(idx);
}
}
pub fn clear_criteria(&mut self, other: &CriteriaSet) {
self.confident.clear_criteria(other);
self.all.clear_criteria(other);
}
pub fn unioned_with(&mut self, other: &CriteriaFailureSet) {
self.all.unioned_with(&other.all);
self.confident.unioned_with(&other.confident);
}
pub fn contains(&self, other: &CriteriaFailureSet) -> bool {
self.all.contains(&other.all) && self.confident.contains(&other.confident)
}
pub fn is_empty(&self) -> bool {
self.all.is_empty()
}
pub fn is_fully_confident(&self) -> bool {
self.confident.contains(&self.all)
}
pub fn is_fully_unconfident(&self) -> bool {
self.confident.is_empty()
}
pub fn all(&self) -> &CriteriaSet {
&self.all
}
pub fn confident(&self) -> &CriteriaSet {
&self.confident
}
}
impl ResolveResult<'_> {
fn with_no_criteria(empty: CriteriaSet) -> Self {
Self {
validated_criteria: empty.clone(),
fully_audited_criteria: empty,
search_results: vec![],
directly_exempted: false,
needed_exemption: false,
}
}
fn contains(&mut self, other: &CriteriaSet) -> bool {
if self.fully_audited_criteria.contains(other) {
true
} else if self.validated_criteria.contains(other) {
self.needed_exemption = true;
true
} else {
false
}
}
fn _has_criteria(&mut self, criteria_idx: usize) -> bool {
if self.fully_audited_criteria.has_criteria(criteria_idx) {
true
} else if self.validated_criteria.has_criteria(criteria_idx) {
self.needed_exemption = true;
true
} else {
false
}
}
}
impl<'a> DepGraph<'a> {
pub fn new(
metadata: &'a Metadata,
filter_graph: Option<&Vec<GraphFilter>>,
policy: Option<&SortedMap<PackageName, PolicyEntry>>,
) -> Self {
let empty_override = SortedMap::new();
let policy = policy.unwrap_or(&empty_override);
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.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,
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 static ROOT_VERSION: Version = Version::new(0, 0, 0);
static NO_AUDITS: Vec<AuditEntry> = Vec::new();
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ResolveDepth {
Deep,
Shallow,
}
pub fn resolve<'a>(
metadata: &'a Metadata,
filter_graph: Option<&Vec<GraphFilter>>,
store: &'a Store,
resolve_depth: ResolveDepth,
) -> ResolveReport<'a> {
let _resolve_span = trace_span!("validate").entered();
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 mut results =
vec![ResolveResult::with_no_criteria(criteria_mapper.no_criteria()); graph.nodes.len()];
let mut root_failures = RootFailures::new();
let mut violations = SortedMap::new();
for &pkgidx in &graph.topo_index {
let package = &graph.nodes[pkgidx];
trace!("resolving {}:{}", package.name, package.version,);
if package.is_third_party {
resolve_third_party(
store,
&graph,
&criteria_mapper,
&mut results,
&mut violations,
&mut root_failures,
pkgidx,
);
} else {
resolve_first_party(
store,
&graph,
&criteria_mapper,
&mut results,
&mut violations,
&mut root_failures,
pkgidx,
);
}
resolve_self_policy(
store,
&graph,
&criteria_mapper,
&mut results,
&mut violations,
&mut root_failures,
pkgidx,
);
}
for &pkgidx in &graph.topo_index {
let package = &graph.nodes[pkgidx];
trace!("resolving dev {}:{}", package.name, package.version,);
if package.is_workspace_member {
resolve_dev(
store,
&graph,
&criteria_mapper,
&mut results,
&mut violations,
&mut root_failures,
pkgidx,
);
} else {
assert!(
package.dev_deps.is_empty(),
"{}:{} isn't a workspace member but has dev-deps!",
package.name,
package.version
);
}
}
if !violations.is_empty() {
return ResolveReport {
graph,
criteria_mapper,
results,
conclusion: Conclusion::FailForViolationConflict(FailForViolationConflict {
violations,
}),
};
}
_resolve_span.exit();
let _blame_span = trace_span!("blame").entered();
let mut failures = SortedMap::<PackageIdx, AuditFailure>::new();
visit_failures(
&graph,
&criteria_mapper,
&results,
&root_failures,
resolve_depth,
|failure, depth, own_failure| {
if let Some(criteria_failures) = own_failure {
trace!(
" {:width$}blaming: {}:{} for {:?} + {:?}",
"",
graph.nodes[failure].name,
graph.nodes[failure].version,
criteria_mapper
.confident_criteria_names(criteria_failures)
.collect::<Vec<_>>(),
criteria_mapper
.unconfident_criteria_names(criteria_failures)
.collect::<Vec<_>>(),
width = depth
);
failures
.entry(failure)
.or_default()
.criteria_failures
.unioned_with(criteria_failures);
}
Ok::<(), ()>(())
},
)
.unwrap();
assert_eq!(
root_failures.is_empty(),
failures.is_empty(),
"failure blaming system bugged out"
);
if !failures.is_empty() {
return ResolveReport {
graph,
criteria_mapper,
results,
conclusion: Conclusion::FailForVet(FailForVet {
failures,
suggest: None,
}),
};
}
let mut vetted_with_exemptions = vec![];
let mut vetted_partially = vec![];
let mut vetted_fully = vec![];
for &pkgidx in &graph.topo_index {
let package = &graph.nodes[pkgidx];
if !package.is_third_party {
continue;
}
let result = &results[pkgidx];
if !result.needed_exemption {
vetted_fully.push(pkgidx);
} else if result.directly_exempted {
vetted_with_exemptions.push(pkgidx);
} else {
vetted_partially.push(pkgidx);
}
}
ResolveReport {
graph,
criteria_mapper,
results,
conclusion: Conclusion::Success(Success {
vetted_with_exemptions,
vetted_partially,
vetted_fully,
}),
}
}
fn resolve_third_party<'a>(
store: &'a Store,
graph: &DepGraph<'a>,
criteria_mapper: &CriteriaMapper,
results: &mut [ResolveResult<'a>],
violations: &mut SortedMap<PackageIdx, Vec<ViolationConflict>>,
_root_failures: &mut RootFailures,
pkgidx: PackageIdx,
) {
let package = &graph.nodes[pkgidx];
let exemptions = store.config.exemptions.get(package.name);
let own_audits = store.audits.audits.get(package.name).unwrap_or(&NO_AUDITS);
let mut forward_audits = AuditGraph::new();
let mut backward_audits = AuditGraph::new();
let mut violation_nodes = Vec::new();
for entry in own_audits.iter() {
let (from_ver, to_ver, dependency_criteria) = match &entry.kind {
AuditKind::Full {
version,
dependency_criteria,
} => (&ROOT_VERSION, version, dependency_criteria),
AuditKind::Delta {
delta,
dependency_criteria,
} => (&delta.from, &delta.to, dependency_criteria),
AuditKind::Violation { .. } => {
violation_nodes.push((AuditSource::OwnAudits, entry));
continue;
}
};
let criteria = criteria_mapper.criteria_from_entry(entry);
let dependency_criteria: FastMap<_, _> = dependency_criteria
.iter()
.map(|(pkg_name, criteria)| (&**pkg_name, criteria_mapper.criteria_from_list(criteria)))
.collect();
forward_audits.entry(from_ver).or_default().push(DeltaEdge {
version: to_ver,
criteria: criteria.clone(),
dependency_criteria: dependency_criteria.clone(),
is_exemption: false,
});
backward_audits.entry(to_ver).or_default().push(DeltaEdge {
version: from_ver,
criteria,
dependency_criteria,
is_exemption: false,
});
}
for (foreign_name, foreign_audits) in &store.imports.audits {
let foreign_criteria_mapper = CriteriaMapper::new(&foreign_audits.criteria);
let criteria_map = &store
.config
.imports
.get(foreign_name)
.expect("Foreign Import isn't in config file (imports.lock outdated?)")
.criteria_map;
let criteria_map: Vec<(CriteriaStr, CriteriaSet)> = criteria_map
.iter()
.map(|mapping| {
let set = foreign_criteria_mapper.criteria_from_list(&mapping.theirs);
(&*mapping.ours, set)
})
.collect();
for entry in foreign_audits
.audits
.get(package.name)
.unwrap_or(&NO_AUDITS)
{
let (from_ver, to_ver, dependency_criteria) = match &entry.kind {
AuditKind::Full {
version,
dependency_criteria,
} => (&ROOT_VERSION, version, dependency_criteria),
AuditKind::Delta {
delta,
dependency_criteria,
} => (&delta.from, &delta.to, dependency_criteria),
AuditKind::Violation { .. } => {
violation_nodes.push((AuditSource::Foreign(foreign_name.clone()), entry));
continue;
}
};
if !dependency_criteria.is_empty() {
warn!("discarding foreign audit with dependency_criteria (TODO)");
continue;
}
let mut local_criteria = criteria_mapper.no_criteria();
let foreign_criteria = foreign_criteria_mapper.criteria_from_entry(entry);
for (local_implied, foreign_required) in &criteria_map {
if foreign_criteria.contains(foreign_required) {
criteria_mapper.set_criteria(&mut local_criteria, local_implied);
}
}
forward_audits.entry(from_ver).or_default().push(DeltaEdge {
version: to_ver,
criteria: local_criteria.clone(),
dependency_criteria: Default::default(),
is_exemption: false,
});
backward_audits.entry(to_ver).or_default().push(DeltaEdge {
version: from_ver,
criteria: local_criteria,
dependency_criteria: Default::default(),
is_exemption: false,
});
}
}
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.entry(pkgidx).or_default().push(
ViolationConflict::UnauditedConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
exemptions: allowed.clone(),
},
);
}
}
}
for audit in own_audits {
let audit_criteria = criteria_mapper.criteria_from_entry(audit);
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.entry(pkgidx).or_default().push(
ViolationConflict::AuditConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
audit_source: AuditSource::OwnAudits,
audit: audit.clone(),
},
);
}
}
AuditKind::Delta { delta, .. } => {
if violation_range.matches(&delta.from) || violation_range.matches(&delta.to) {
violations.entry(pkgidx).or_default().push(
ViolationConflict::AuditConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
audit_source: AuditSource::OwnAudits,
audit: audit.clone(),
},
);
}
}
AuditKind::Violation { .. } => {
}
}
}
for (foreign_name, foreign_audits) in &store.imports.audits {
for audit in foreign_audits
.audits
.get(package.name)
.unwrap_or(&NO_AUDITS)
{
let audit_criteria = criteria_mapper.criteria_from_entry(audit);
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.entry(pkgidx).or_default().push(
ViolationConflict::AuditConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
audit_source: AuditSource::Foreign(foreign_name.clone()),
audit: audit.clone(),
},
);
}
}
AuditKind::Delta { delta, .. } => {
if violation_range.matches(&delta.from)
|| violation_range.matches(&delta.to)
{
violations.entry(pkgidx).or_default().push(
ViolationConflict::AuditConflict {
violation_source: violation_source.clone(),
violation: (*violation_entry).clone(),
audit_source: AuditSource::Foreign(foreign_name.clone()),
audit: audit.clone(),
},
);
}
}
AuditKind::Violation { .. } => {
}
}
}
}
}
let mut directly_exempted = false;
if let Some(alloweds) = exemptions {
for allowed in alloweds {
if &allowed.version == package.version {
directly_exempted = true;
}
let from_ver = &ROOT_VERSION;
let to_ver = &allowed.version;
let criteria = criteria_mapper.criteria_from_list(&allowed.criteria);
let dependency_criteria: FastMap<_, _> = allowed
.dependency_criteria
.iter()
.map(|(pkg_name, criteria)| {
(&**pkg_name, criteria_mapper.criteria_from_list(criteria))
})
.collect();
forward_audits.entry(from_ver).or_default().push(DeltaEdge {
version: to_ver,
criteria: criteria.clone(),
dependency_criteria: dependency_criteria.clone(),
is_exemption: true,
});
backward_audits.entry(to_ver).or_default().push(DeltaEdge {
version: from_ver,
criteria,
dependency_criteria,
is_exemption: true,
});
}
}
let mut validated_criteria = criteria_mapper.no_criteria();
let mut fully_audited_criteria = criteria_mapper.no_criteria();
let mut search_results = vec![];
for criteria in criteria_mapper.all_criteria_iter() {
let result = search_for_path(
criteria,
&ROOT_VERSION,
package.version,
&forward_audits,
graph,
criteria_mapper,
package,
results,
);
match result {
SearchResult::Connected { fully_audited } => {
if fully_audited {
fully_audited_criteria.unioned_with(criteria);
}
validated_criteria.unioned_with(criteria);
search_results.push(SearchResult::Connected { fully_audited });
}
SearchResult::PossiblyConnected { failed_deps } => {
search_results.push(SearchResult::PossiblyConnected { failed_deps });
}
SearchResult::Disconnected {
reachable_from_root,
..
} => {
let rev_result = search_for_path(
criteria,
package.version,
&ROOT_VERSION,
&backward_audits,
graph,
criteria_mapper,
package,
results,
);
if let SearchResult::Disconnected {
reachable_from_root: reachable_from_target,
..
} = rev_result
{
search_results.push(SearchResult::Disconnected {
reachable_from_root,
reachable_from_target,
})
} else {
unreachable!("We managed to find a path but only from one direction?!");
}
}
}
}
trace!(
" third-party validation: {:?}",
criteria_mapper
.criteria_names(&validated_criteria)
.collect::<Vec<_>>()
);
results[pkgidx] = ResolveResult {
validated_criteria,
fully_audited_criteria,
directly_exempted,
search_results,
needed_exemption: false,
};
}
#[allow(clippy::too_many_arguments)]
fn search_for_path<'a>(
cur_criteria: &CriteriaSet,
from_version: &'a Version,
to_version: &'a Version,
audit_graph: &AuditGraph<'a>,
dep_graph: &DepGraph<'a>,
criteria_mapper: &CriteriaMapper,
package: &PackageNode<'a>,
results: &mut [ResolveResult],
) -> SearchResult<'a> {
let mut found_path = false;
let mut needed_exemption = false;
let mut needed_failed_edges = false;
let mut failed_deps = SortedMap::<PackageIdx, CriteriaSet>::new();
let mut search_stack = vec![from_version];
let mut visited = SortedSet::new();
let mut deferred_exemptions_entries = vec![];
let mut deferred_failed_edges = vec![];
loop {
if let Some(node) = deferred_exemptions_entries.pop() {
if visited.contains(node) {
continue;
}
needed_exemption = true;
search_stack.push(node);
} else if let Some(node) = deferred_failed_edges.pop() {
if visited.contains(node) {
continue;
}
needed_failed_edges = true;
search_stack.push(node);
}
while let Some(cur_version) = search_stack.pop() {
visited.insert(cur_version);
if cur_version == to_version {
found_path = true;
break;
}
if let Some(edges) = audit_graph.get(cur_version) {
for edge in edges {
if !edge.criteria.contains(cur_criteria) {
continue;
}
if visited.contains(edge.version) {
continue;
}
let mut deps_satisfied = true;
for &dependency in &package.normal_and_build_deps {
let dep_package = &dep_graph.nodes[dependency];
let dep_vet_result = &mut results[dependency];
let dep_req = edge
.dependency_criteria
.get(&*dep_package.name)
.unwrap_or(cur_criteria);
if !dep_vet_result.contains(dep_req) {
failed_deps
.entry(dependency)
.or_insert_with(|| criteria_mapper.no_criteria())
.unioned_with(dep_req);
deps_satisfied = false;
}
}
if deps_satisfied {
if edge.is_exemption {
deferred_exemptions_entries.push(edge.version);
} else {
search_stack.push(edge.version);
}
} else {
deferred_failed_edges.push(edge.version);
}
}
}
}
if found_path
|| (deferred_exemptions_entries.is_empty() && deferred_failed_edges.is_empty())
{
break;
}
}
if found_path && !needed_failed_edges {
SearchResult::Connected {
fully_audited: !needed_exemption,
}
} else if found_path {
SearchResult::PossiblyConnected { failed_deps }
} else {
SearchResult::Disconnected {
reachable_from_root: visited,
reachable_from_target: Default::default(),
}
}
}
fn resolve_first_party<'a>(
store: &'a Store,
graph: &DepGraph<'a>,
criteria_mapper: &CriteriaMapper,
results: &mut [ResolveResult<'a>],
_violations: &mut SortedMap<PackageIdx, Vec<ViolationConflict>>,
_root_failures: &mut RootFailures,
pkgidx: PackageIdx,
) {
let package = &graph.nodes[pkgidx];
let dep_criteria = store
.config
.policy
.get(package.name)
.map(|policy| {
policy
.dependency_criteria
.iter()
.map(|(dep_name, criteria)| {
(&**dep_name, criteria_mapper.criteria_from_list(criteria))
})
.collect::<FastMap<_, _>>()
})
.unwrap_or_default();
let mut validated_criteria = criteria_mapper.no_criteria();
let mut search_results = vec![];
for criteria in criteria_mapper.all_criteria_iter() {
let mut failed_deps = SortedMap::new();
for &depidx in &package.normal_and_build_deps {
let dep_name = graph.nodes[depidx].name;
let required_criteria = dep_criteria.get(dep_name).unwrap_or(criteria);
if !results[depidx].contains(required_criteria) {
failed_deps
.entry(depidx)
.or_insert_with(|| criteria_mapper.no_criteria())
.unioned_with(required_criteria);
}
}
if failed_deps.is_empty() {
search_results.push(SearchResult::Connected {
fully_audited: true,
});
validated_criteria.unioned_with(criteria);
} else {
search_results.push(SearchResult::PossiblyConnected { failed_deps })
}
}
trace!(
" first-party validation: {:?}",
criteria_mapper
.criteria_names(&validated_criteria)
.collect::<Vec<_>>()
);
results[pkgidx].search_results = search_results;
results[pkgidx].validated_criteria = validated_criteria;
}
fn resolve_self_policy<'a>(
store: &'a Store,
graph: &DepGraph<'a>,
criteria_mapper: &CriteriaMapper,
results: &mut [ResolveResult<'a>],
_violations: &mut SortedMap<PackageIdx, Vec<ViolationConflict>>,
root_failures: &mut RootFailures,
pkgidx: PackageIdx,
) {
let package = &graph.nodes[pkgidx];
let entry = store.config.policy.get(package.name);
let own_policy = if let Some(c) = entry.and_then(|p| p.criteria.as_ref()) {
trace!(" explicit policy: {:?}", c);
criteria_mapper.criteria_from_list(c)
} else if package.is_root {
trace!(" root policy: {:?}", [format::DEFAULT_POLICY_CRITERIA]);
criteria_mapper.criteria_from_list([format::DEFAULT_POLICY_CRITERIA])
} else {
trace!(" has no policy, done");
return;
};
let mut policy_failures = PolicyFailures::new();
for criteria_idx in own_policy.indices() {
match &results[pkgidx].search_results[criteria_idx] {
SearchResult::PossiblyConnected { failed_deps } => {
for (&dep, failed_criteria) in failed_deps {
policy_failures
.entry(dep)
.or_insert_with(|| criteria_mapper.no_criteria())
.unioned_with(failed_criteria);
}
}
SearchResult::Disconnected { .. } => {
policy_failures
.entry(pkgidx)
.or_insert_with(|| criteria_mapper.no_criteria())
.set_criteria(criteria_idx)
}
SearchResult::Connected { .. } => {
}
}
}
if policy_failures.is_empty() {
trace!(" passed policy, all_criteria");
results[pkgidx].validated_criteria = criteria_mapper.all_criteria();
} else {
trace!(" failed policy, no_criteria");
results[pkgidx].validated_criteria = criteria_mapper.no_criteria();
root_failures.push((pkgidx, policy_failures, false));
}
}
fn resolve_dev<'a>(
store: &'a Store,
graph: &DepGraph<'a>,
criteria_mapper: &CriteriaMapper,
results: &mut [ResolveResult<'a>],
_violations: &mut SortedMap<PackageIdx, Vec<ViolationConflict>>,
root_failures: &mut RootFailures,
pkgidx: PackageIdx,
) {
let package = &graph.nodes[pkgidx];
let dep_criteria = store
.config
.policy
.get(package.name)
.map(|policy| {
policy
.dependency_criteria
.iter()
.map(|(dep_name, criteria)| {
(&**dep_name, criteria_mapper.criteria_from_list(criteria))
})
.collect::<FastMap<_, _>>()
})
.unwrap_or_default();
let mut validated_criteria = criteria_mapper.no_criteria();
let mut search_results = vec![];
for criteria in criteria_mapper.all_criteria_iter() {
let mut failed_deps = SortedMap::new();
for &depidx in &package.dev_deps {
let dep_name = graph.nodes[depidx].name;
let required_criteria = dep_criteria.get(dep_name).unwrap_or(criteria);
if !results[depidx].contains(required_criteria) {
failed_deps
.entry(depidx)
.or_insert_with(|| criteria_mapper.no_criteria())
.unioned_with(required_criteria);
}
}
if failed_deps.is_empty() {
search_results.push(SearchResult::Connected {
fully_audited: true,
});
validated_criteria.unioned_with(criteria);
} else {
search_results.push(SearchResult::PossiblyConnected { failed_deps })
}
}
trace!(
" dev validation: {:?}",
criteria_mapper
.criteria_names(&validated_criteria)
.collect::<Vec<_>>()
);
let entry = store.config.policy.get(package.name);
let own_policy = if let Some(c) = entry.and_then(|p| p.dev_criteria.as_ref()) {
trace!(" explicit policy: {:?}", c);
criteria_mapper.criteria_from_list(c)
} else {
trace!(" root policy: {:?}", [format::DEFAULT_POLICY_DEV_CRITERIA]);
criteria_mapper.criteria_from_list([format::DEFAULT_POLICY_DEV_CRITERIA])
};
let mut policy_failures = PolicyFailures::new();
for criteria_idx in own_policy.indices() {
match &search_results[criteria_idx] {
SearchResult::PossiblyConnected { failed_deps } => {
for (&dep, failed_criteria) in failed_deps {
policy_failures
.entry(dep)
.or_insert_with(|| criteria_mapper.no_criteria())
.unioned_with(failed_criteria);
}
}
SearchResult::Disconnected { .. } => {
policy_failures
.entry(pkgidx)
.or_insert_with(|| criteria_mapper.no_criteria())
.set_criteria(criteria_idx)
}
SearchResult::Connected { .. } => {
}
}
}
if policy_failures.is_empty() {
trace!(" passed dev policy");
} else {
trace!(" failed dev policy");
root_failures.push((pkgidx, policy_failures, true));
}
}
fn visit_failures<'a, T>(
graph: &DepGraph<'a>,
criteria_mapper: &CriteriaMapper,
results: &[ResolveResult<'a>],
root_failures: &RootFailures,
resolve_depth: ResolveDepth,
mut callback: impl FnMut(PackageIdx, usize, Option<&CriteriaFailureSet>) -> Result<(), T>,
) -> Result<(), T> {
trace!(" traversing blame tree");
let mut search_stack = Vec::new();
let mut immune_to_parent_demands = FastSet::new();
for (failed_idx, policy_failures, is_dev) in root_failures {
let failed_package = &graph.nodes[*failed_idx];
trace!(
" policy failure for {}:{}",
failed_package.name,
failed_package.version
);
for (&failed_dep_idx, failed_criteria) in policy_failures {
let failed_dep = &graph.nodes[failed_dep_idx];
trace!(
" {}:{} needed {:?}",
failed_dep.name,
failed_dep.version,
criteria_mapper
.criteria_names(failed_criteria)
.collect::<Vec<_>>()
);
search_stack.push((
failed_dep_idx,
0,
Some(CriteriaFailureSet::from(failed_criteria, true)),
));
}
if !is_dev {
immune_to_parent_demands.insert(failed_idx);
}
}
let mut visited = FastMap::<PackageIdx, CriteriaFailureSet>::new();
let no_criteria = criteria_mapper.no_criteria_failures();
while let Some((failure_idx, depth, cur_criteria)) = search_stack.pop() {
match visited.entry(failure_idx) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(cur_criteria.clone().unwrap_or_else(|| no_criteria.clone()));
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
let cur = cur_criteria.as_ref().unwrap_or(&no_criteria);
if entry.get().contains(cur) {
continue;
} else {
entry.get_mut().unioned_with(cur);
}
}
}
let result = &results[failure_idx];
let package = &graph.nodes[failure_idx];
trace!(
" {:width$}visiting {}:{} for {:?} + {:?}",
"",
package.name,
package.version,
cur_criteria.as_ref().map(|c| criteria_mapper
.confident_criteria_names(c)
.collect::<Vec<_>>()),
cur_criteria.as_ref().map(|c| criteria_mapper
.unconfident_criteria_names(c)
.collect::<Vec<_>>()),
width = depth
);
if let Some(failed_criteria) = cur_criteria {
let mut own_fault = no_criteria.clone();
let mut dep_faults = FastMap::<PackageIdx, CriteriaFailureSet>::new();
for criteria_idx in failed_criteria.all().indices() {
let confident = failed_criteria.confident().has_criteria(criteria_idx);
match &result.search_results[criteria_idx] {
SearchResult::Connected { .. } => {
}
SearchResult::PossiblyConnected { failed_deps } => {
for (&failed_dep, failed_criteria) in failed_deps {
dep_faults
.entry(failed_dep)
.or_insert_with(|| no_criteria.clone())
.unioned_with(&CriteriaFailureSet::from(
failed_criteria,
confident,
));
}
}
SearchResult::Disconnected { .. } => {
own_fault.set_criteria(criteria_idx, confident);
if resolve_depth != ResolveDepth::Shallow {
for &dep_idx in &package.normal_and_build_deps {
let dep_result = &results[dep_idx];
if !dep_result.validated_criteria.has_criteria(criteria_idx) {
dep_faults
.entry(dep_idx)
.or_insert_with(|| no_criteria.clone())
.set_criteria(criteria_idx, false);
}
}
}
}
}
}
if own_fault.is_empty() {
callback(failure_idx, depth, None)?;
} else {
callback(failure_idx, depth, Some(&own_fault))?;
}
for (failed_dep, failed_criteria) in dep_faults {
if immune_to_parent_demands.contains(&failed_dep) {
continue;
}
search_stack.push((failed_dep, depth + 1, Some(failed_criteria.clone())));
}
} else {
unreachable!("I don't think this should happen..?");
}
}
Ok(())
}
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,
network: Option<&Network>,
allow_deltas: bool,
) -> 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 mut suggestions = tokio::runtime::Handle::current()
.block_on(join_all(fail.failures.iter().map(
|(failure_idx, audit_failure)| async {
let failure_idx = *failure_idx;
let package = &self.graph.nodes[failure_idx];
let result = &self.results[failure_idx];
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 {} others", remainder));
}
reverse_deps.join(", ")
};
let mut from_root = None::<SortedSet<&Version>>;
let mut from_target = None::<SortedSet<&Version>>;
for criteria_idx in audit_failure.criteria_failures.all().indices() {
let search_result = &result.search_results[criteria_idx];
if let SearchResult::Disconnected {
reachable_from_root,
reachable_from_target,
} = search_result
{
if let (Some(from_root), Some(from_target)) =
(from_root.as_mut(), from_target.as_mut())
{
*from_root = &*from_root & reachable_from_root;
*from_target = &*from_target & reachable_from_target;
} else {
from_root = Some(reachable_from_root.clone());
from_target = Some(reachable_from_target.clone());
}
} else {
unreachable!("messed up suggest...");
}
}
let mut candidates = SortedSet::new();
if allow_deltas {
for &dest in from_target.as_ref().unwrap() {
let mut closest_above = None;
let mut closest_below = None;
for &src in from_root.as_ref().unwrap() {
if src < dest {
if let Some(closest) = closest_below {
if src > closest {
closest_below = Some(src);
}
} else {
closest_below = Some(src);
}
} else if let Some(closest) = closest_above {
if src < closest {
closest_above = Some(src);
}
} else {
closest_above = Some(src);
}
}
for closest in closest_below.into_iter().chain(closest_above) {
candidates.insert(Delta {
from: closest.clone(),
to: dest.clone(),
});
}
}
} else {
for &dest in from_target.as_ref().unwrap() {
candidates.insert(Delta {
from: ROOT_VERSION.clone(),
to: dest.clone(),
});
}
}
let diffstats = join_all(candidates.iter().map(|delta| async {
match cache
.fetch_and_diffstat_package(network, package.name, delta)
.await
{
Ok(diffstat) => Some(DiffRecommendation {
diffstat,
from: delta.from.clone(),
to: delta.to.clone(),
}),
Err(err) => {
error!(
"error diffing {}:{}: {:?}",
package.name, package.version, err
);
None
}
}
}))
.await;
Some(SuggestItem {
package: failure_idx,
suggested_diff: diffstats
.into_iter()
.flatten()
.min_by_key(|diff| diff.diffstat.count)?,
suggested_criteria: audit_failure.criteria_failures.clone(),
notable_parents,
})
},
)))
.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);
suggestions.sort_by_key(|item| item.suggested_criteria.is_fully_unconfident());
let mut suggestions_by_criteria = SortedMap::<CriteriaName, Vec<SuggestItem>>::new();
for s in suggestions.clone().into_iter() {
let criteria_names = self
.criteria_mapper
.all_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,
}))
}
pub fn compute_suggested_criteria(
&self,
package_name: PackageStr<'_>,
from: Option<&Version>,
to: &Version,
) -> Vec<CriteriaName> {
let fail = if let Conclusion::FailForVet(fail) = &self.conclusion {
fail
} else {
return Vec::new();
};
let mut criteria = self.criteria_mapper.no_criteria();
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];
for criteria_idx in audit_failure.criteria_failures.all().indices() {
let search_result = &result.search_results[criteria_idx];
if let SearchResult::Disconnected {
reachable_from_root,
reachable_from_target,
} = search_result
{
if reachable_from_target.contains(to)
&& from.map_or(true, |v| reachable_from_root.contains(v))
{
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>,
_cfg: &Config,
suggest: Option<&Suggest>,
) -> Result<(), miette::Report> {
let result = match &self.conclusion {
Conclusion::Success(success) => {
let json_package = |pkgidx: &PackageIdx| {
let package = &self.graph.nodes[*pkgidx];
json!({
"name": package.name,
"version": package.version,
})
};
json!({
"conclusion": "success",
"vetted_fully": success.vetted_fully.iter().map(json_package).collect::<Vec<_>>(),
"vetted_partially": success.vetted_partially.iter().map(json_package).collect::<Vec<_>>(),
"vetted_with_exemptions": success.vetted_with_exemptions.iter().map(json_package).collect::<Vec<_>>(),
})
}
Conclusion::FailForViolationConflict(fail) => json!({
"conclusion": "fail (violation)",
"violations": fail.violations.iter().map(|(pkgidx, violations)| {
let package = &self.graph.nodes[*pkgidx];
let key = format!("{}:{}", package.name, package.version);
(key, violations)
}).collect::<SortedMap<_,_>>(),
}),
Conclusion::FailForVet(fail) => {
let json_suggest_item = |item: &SuggestItem| {
let package = &self.graph.nodes[item.package];
json!({
"name": package.name,
"notable_parents": item.notable_parents,
"suggested_criteria": self.criteria_mapper.all_criteria_names(&item.suggested_criteria).collect::<Vec<_>>(),
"suggested_diff": item.suggested_diff,
})
};
json!({
"conclusion": "fail (vetting)",
"failures": fail.failures.iter().map(|(&pkgidx, audit_fail)| {
let package = &self.graph.nodes[pkgidx];
json!({
"name": package.name,
"version": package.version,
"missing_criteria": self.criteria_mapper.all_criteria_names(&audit_fail.criteria_failures).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
"suggest": suggest.map(|suggest| json!({
"suggestions": suggest.suggestions.iter().map(json_suggest_item).collect::<Vec<_>>(),
"suggest_by_criteria": suggest.suggestions_by_criteria.iter().map(|(criteria, items)| (criteria, items.iter().map(json_suggest_item).collect::<Vec<_>>())).collect::<SortedMap<_,_>>(),
"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", fully_audited_count);
count_count -= 1;
if count_count > 0 {
write!(out, ", ");
}
}
if partially_audited_count != 0 {
write!(out, "{} partially audited", partially_audited_count);
count_count -= 1;
if count_count > 0 {
write!(out, ", ");
}
}
if exemptions_count != 0 {
write!(out, "{} exempted", exemptions_count);
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 = if item.suggested_diff.from == ROOT_VERSION {
format!(
"cargo vet inspect {} {}",
package.name, item.suggested_diff.to
)
} else {
format!(
"cargo vet diff {} {} {}",
package.name, item.suggested_diff.from, item.suggested_diff.to
)
};
let parents = format!("(used by {})", item.notable_parents);
let diffstat = if item.suggested_diff.from == ROOT_VERSION {
format!("({} lines)", item.suggested_diff.diffstat.count)
} else {
format!("({})", item.suggested_diff.diffstat.raw.trim())
};
let style = if item.suggested_criteria.is_fully_unconfident() {
out.style().dim()
} else {
out.style()
};
(cmd, parents, diffstat, style)
})
.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, style) in strings {
write!(
out,
"{}",
style
.clone()
.cyan()
.bold()
.apply_to(format_args!(" {s0:width$}", width = max0))
);
writeln!(
out,
"{}",
style.apply_to(format_args!(" {s1:width$} {s2}", width = max1))
);
}
writeln!(out);
}
writeln!(out, "estimated audit backlog: {} lines", self.total_lines);
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);
failures.sort_by_key(|(_, failure)| failure.criteria_failures.is_fully_unconfident());
for (failed_package, failed_audit) in failures {
let confident_criteria = report
.criteria_mapper
.confident_criteria_names(&failed_audit.criteria_failures)
.collect::<Vec<_>>();
let unconfident_criteria = report
.criteria_mapper
.unconfident_criteria_names(&failed_audit.criteria_failures)
.collect::<Vec<_>>();
let label = format!(" {}:{}", failed_package.name, failed_package.version);
if !confident_criteria.is_empty() {
writeln!(out, "{} missing {:?}", label, confident_criteria);
}
if !unconfident_criteria.is_empty() {
writeln!(
out,
"{}",
out.style().dim().apply_to(format_args!(
"{} likely missing {:?}",
if confident_criteria.is_empty() {
label
} else {
format!("{:width$}", "", width = console::measure_text_width(&label))
},
unconfident_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: &AuditSource,
entry: &AuditEntry,
) -> Result<(), std::io::Error> {
match source {
AuditSource::OwnAudits => write!(out, "own "),
AuditSource::Foreign(name) => write!(out, "foreign ({name}) "),
}
match &entry.kind {
AuditKind::Full { version, .. } => {
writeln!(out, "audit {version}");
}
AuditKind::Delta { delta, .. } => {
writeln!(out, "audit {} -> {}", delta.from, delta.to);
}
AuditKind::Violation { violation } => {
writeln!(out, "violation against {violation}");
}
}
writeln!(out, " criteria: {:?}", entry.criteria);
if let Some(who) = &entry.who {
writeln!(out, " who: {who}");
}
if let Some(notes) = &entry.notes {
writeln!(out, " notes: {notes}");
}
Ok(())
}
Ok(())
}
}