in src/main.rs [689:1084]
fn do_cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
store: &mut Store,
network: Option<&Network>,
last_fetch: Option<FetchCommand>,
) -> Result<(), CertifyError> {
// Before setting up magic, we need to agree on a package
let package = if let Some(package) = &sub_args.package {
package.clone()
} else if let Some(last_fetch) = &last_fetch {
// If we just fetched a package, assume we want to certify it
last_fetch.package().to_owned()
} else {
return Err(CertifyError::CouldntGuessPackage);
};
// FIXME: can/should we check if the version makes sense..?
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == *package)
{
return Err(CertifyError::NotAPackage(package));
}
#[derive(Debug)]
enum CertifyKind {
Delta {
from: VetVersion,
to: VetVersion,
},
Full {
version: VetVersion,
},
Wildcard {
user_login: String,
user_id: CratesUserId,
start: chrono::NaiveDate,
end: chrono::NaiveDate,
set_renew_false: bool,
},
}
let kind = if let Some(login) = &sub_args.wildcard {
// Fetch publisher information for relevant versions of `package`.
let publishers = store.ensure_publisher_versions(cfg, network, &package)?;
let published_versions = publishers
.iter()
.filter(|publisher| &publisher.user_login == login);
let earliest = published_versions
.min_by_key(|p| p.when)
.ok_or_else(|| CertifyError::NotAPublisher(login.to_owned(), package.to_owned()))?;
// Get the from and to dates, defaulting to a from date of the earliest
// published package by the user, and a to date of 12 months from today.
let start = sub_args.start_date.unwrap_or(earliest.when);
let max_end = cfg.today() + chrono::Months::new(12);
let end = sub_args.end_date.unwrap_or(max_end);
let set_renew_false = sub_args.end_date.is_some();
if end > max_end {
return Err(CertifyError::BadWildcardEndDate(end));
}
CertifyKind::Wildcard {
user_login: earliest.user_login.to_owned(),
user_id: earliest.user_id,
start,
end,
set_renew_false,
}
} else if let Some(v1) = &sub_args.version1 {
// If explicit versions were provided, use those
if let Some(v2) = &sub_args.version2 {
// This is a delta audit
CertifyKind::Delta {
from: v1.clone(),
to: v2.clone(),
}
} else {
// This is a full audit
CertifyKind::Full {
version: v1.clone(),
}
}
} else if let Some(fetch) = last_fetch.filter(|f| f.package() == package) {
// Otherwise, is we just fetched this package, use the version(s) we fetched
match fetch {
FetchCommand::Inspect { version, .. } => CertifyKind::Full { version },
FetchCommand::Diff {
version1, version2, ..
} => CertifyKind::Delta {
from: version1,
to: version2,
},
}
} else {
return Err(CertifyError::CouldntGuessVersion(package));
};
let (username, who) = if sub_args.who.is_empty() {
let user_info = get_user_info()?;
let who = format!("{} <{}>", user_info.username, user_info.email);
(user_info.username, vec![Spanned::from(who)])
} else {
(
sub_args.who.join(", "),
sub_args
.who
.iter()
.map(|w| Spanned::from(w.clone()))
.collect(),
)
};
let (criteria_guess, prompt) = if sub_args.criteria.is_empty() {
// If we don't have explicit cli criteria, guess the criteria
//
// * Check what would cause `cargo vet` to encounter fewer errors
// * Otherwise check what would cause `cargo vet suggest` to suggest fewer audits
// * Otherwise guess nothing
//
// Regardless of the guess, prompt the user to confirm (just needs to mash enter)
match &kind {
CertifyKind::Full { version } => (
guess_audit_criteria(cfg, store, &package, None, version),
Some(format!(
"choose criteria to certify for {package}:{version}"
)),
),
CertifyKind::Delta { from, to } => (
guess_audit_criteria(cfg, store, &package, Some(from), to),
Some(format!(
"choose criteria to certify for {package}:{from} -> {to}"
)),
),
CertifyKind::Wildcard { .. } => {
// FIXME: Consider predicting the criteria better for wildcard
// audits in the future.
(
vec![format::SAFE_TO_DEPLOY.to_owned()],
Some(format!("choose criteria to certify for {package}:*")),
)
}
}
} else {
// If we do have explcit criteria, don't prompt, but still pass through
// prompt_pick_criteria to simplify and validate.
(sub_args.criteria.clone(), None)
};
let criteria_names =
criteria_picker(out, &store.audits.criteria, criteria_guess, prompt.as_ref())?;
let statement = match &kind {
CertifyKind::Full { version } => {
format!(
"I, {username}, certify that I have audited version {version} of {package} in accordance with the above criteria.",
)
}
CertifyKind::Delta { from, to } => {
format!(
"I, {username}, certify that I have audited the changes from version {from} to {to} of {package} in accordance with the above criteria.",
)
}
CertifyKind::Wildcard {
user_login,
start,
end,
..
} => {
format!(
"I, {username}, certify that any version of {package} published by '{user_login}' between {start} and {end} will satisfy the above criteria.",
)
}
};
let mut notes = sub_args.notes.clone();
if !sub_args.accept_all {
// Get all the EULAs at once
let eulas = tokio::runtime::Handle::current().block_on(join_all(
criteria_names.iter().map(|criteria| async {
(
&criteria[..],
eula_for_criteria(network, &store.audits.criteria, criteria).await,
)
}),
));
let mut editor = out.editor("VET_CERTIFY")?;
if let Some(notes) = ¬es {
editor.select_comment_char(notes);
}
editor.add_comments(
"Please read the following criteria and then follow the instructions below:",
)?;
editor.add_text("")?;
for (criteria, eula) in &eulas {
editor.add_comments(&format!("=== BEGIN CRITERIA {criteria:?} ==="))?;
editor.add_comments("")?;
editor.add_comments(eula)?;
editor.add_comments("")?;
editor.add_comments("=== END CRITERIA ===")?;
editor.add_comments("")?;
}
editor.add_comments("Uncomment the following statement:")?;
editor.add_text("")?;
editor.add_comments(&statement)?;
editor.add_text("")?;
editor.add_comments("Add any notes about your audit below this line:")?;
editor.add_text("")?;
if let Some(notes) = ¬es {
editor.add_text(notes)?;
}
let editor_result = editor.edit()?;
// Check to make sure that the statement was uncommented as the first
// line in the parsed file, and remove blank lines between the statement
// and notes.
let new_notes = match editor_result.trim_start().strip_prefix(&statement) {
Some(notes) => notes.trim_start_matches('\n'),
None => {
// FIXME: Might be nice to try to save any notes the user typed
// in and re-try the prompt if the user asks for it, in case
// they wrote some nice notes, but forgot to uncomment the
// statement.
return Err(CertifyError::CouldntFindCertifyStatement);
}
};
// Strip trailing newline if notes would otherwise contain no newlines.
let new_notes = new_notes
.strip_suffix('\n')
.filter(|s| !s.contains('\n'))
.unwrap_or(new_notes);
notes = if new_notes.is_empty() {
None
} else {
Some(new_notes.to_owned())
};
}
let criteria = criteria_names.into_iter().map(|s| s.into()).collect();
match kind {
CertifyKind::Full { version } => {
let kind = AuditKind::Full { version };
let importable = kind.default_importable();
store
.audits
.audits
.entry(package.clone())
.or_default()
.push(AuditEntry {
kind,
criteria,
who,
importable,
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
CertifyKind::Delta { from, to } => {
let from_is_git_version = from.git_rev.is_some();
let kind = AuditKind::Delta { from, to };
let importable = kind.default_importable();
let mut entry = AuditEntry {
kind,
criteria,
who,
importable,
notes,
aggregated_from: vec![],
is_fresh_import: false,
};
// Collapse a delta audit with a git `from` version with a prior audit that is
// non-importable and has identical and satisfied criteria.
//
// We merge an adjacent audit for a prior version with the new audit (updating the new
// audit). The later `update_store` call will remove the now-unused prior audit.
if from_is_git_version && !sub_args.no_collapse {
// A closure which returns whether the given audit entry satisfies the criteria
// being certified.
let is_rooted_for_criteria = {
let mapper = CriteriaMapper::new(&store.audits.criteria);
let criteria = mapper.criteria_from_list(&entry.criteria);
// If the audit graph fails to load, we always return `false` and thus don't
// make any changes.
let audit_graph = match resolver::AuditGraph::build(
store, &mapper, &package, None,
) {
Ok(graph) => Some(graph),
Err(_) => {
warn!(
"failed to build audit graph to determine audit collapse validity, so not collapsing any audits"
);
None
}
};
move |audit: &AuditEntry| {
let Some(audit_graph) = &audit_graph else {
return false;
};
let version = match &audit.kind {
AuditKind::Delta { from, .. } => from,
AuditKind::Full { .. } => return true,
AuditKind::Violation { .. } => return false,
};
// NOTE we use `criteria` of the certification rather than the target audit
// to check root accessibility, which is okay since later in
// `try_collapse_with_prior` we verify that the criteria of the audit is
// identical to that of the certification.
mapper.minimal_indices(&criteria).all(|idx| {
audit_graph
.search(idx, version, resolver::SearchMode::PreferExemptions)
.is_ok()
})
}
};
for audit in store
.audits
.audits
.get(&package)
.into_iter()
.flatten()
.filter(|a| !a.importable && is_rooted_for_criteria(a))
{
if let Some(new_entry) = entry.try_collapse_with_prior(audit) {
entry = new_entry;
break;
}
}
}
store
.audits
.audits
.entry(package.clone())
.or_default()
.push(entry);
}
CertifyKind::Wildcard {
user_id,
start,
end,
set_renew_false,
..
} => {
store
.audits
.wildcard_audits
.entry(package.clone())
.or_default()
.push(WildcardEntry {
who,
criteria,
user_id,
start: start.into(),
end: end.into(),
renew: set_renew_false.then_some(false),
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
};
store
.validate(cfg.today(), false)
.expect("the new audit entry made the store invalid?");
// Minimize exemptions after adding the new audit. This will be used to potentially update
// imports, and remove now-unnecessary exemptions and audits for the target package. We only
// prefer fresh imports and prune exemptions for the package we certified, to avoid unrelated
// changes.
resolver::update_store(cfg, store, |name| resolver::UpdateMode {
search_mode: if name == &package[..] {
resolver::SearchMode::PreferFreshImports
} else {
resolver::SearchMode::PreferExemptions
},
prune_exemptions: name == &package[..],
prune_non_importable_audits: name == &package[..],
prune_imports: false,
});
Ok(())
}