use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::panic::panic_any;
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use std::{fs::File, io, panic, path::PathBuf};
use cargo_metadata::{Metadata, Package};
use clap::{CommandFactory, Parser};
use console::Term;
use errors::{
AggregateCriteriaDescription, AggregateCriteriaDescriptionMismatchError,
AggregateCriteriaImplies, AggregateError, AggregateErrors, AggregateImpliesMismatchError,
AuditAsError, AuditAsErrors, CacheAcquireError, CertifyError, CratePolicyError,
CratePolicyErrors, FetchAuditError, LoadTomlError, NeedsAuditAsErrors,
NeedsPolicyVersionErrors, PackageError, ShouldntBeAuditAsErrors, TomlParseError,
UnusedAuditAsErrors, UnusedPolicyVersionErrors, UserInfoError,
};
use format::{CriteriaName, CriteriaStr, PackageName, Policy, PolicyEntry, SortedSet, VetVersion};
use futures_util::future::{join_all, try_join_all};
use indicatif::ProgressDrawTarget;
use lazy_static::lazy_static;
use miette::{miette, Context, Diagnostic, IntoDiagnostic, SourceOffset};
use network::Network;
use out::{progress_bar, IncProgressOnDrop};
use reqwest::Url;
use serde::de::Deserialize;
use serialization::spanned::Spanned;
use storage::fetch_registry;
use thiserror::Error;
use tracing::{error, info, trace, warn};
use crate::cli::*;
use crate::criteria::CriteriaMapper;
use crate::errors::{
CommandError, DownloadError, FetchAndDiffError, FetchError, MetadataAcquireError, SourceFile,
};
use crate::format::{
AuditEntry, AuditKind, AuditsFile, ConfigFile, CratesUserId, CriteriaEntry, ExemptedDependency,
FetchCommand, MetaConfig, MetaConfigInstance, PackageStr, RemoteImport, SortedMap, StoreInfo,
WildcardEntry,
};
use crate::git_tool::Pager;
use crate::out::{indeterminate_spinner, Out, StderrLogWriter, MULTIPROGRESS};
use crate::storage::{Cache, Store};
mod cli;
mod criteria;
pub mod errors;
mod flock;
pub mod format;
mod git_tool;
pub mod network;
mod out;
pub mod resolver;
mod serialization;
pub mod storage;
#[cfg(test)]
mod tests;
pub struct Config {
pub metacfg: MetaConfig,
pub metadata: Metadata,
_rest: PartialConfig,
}
pub struct PartialConfig {
pub cli: Cli,
pub cache_dir: PathBuf,
pub mock_cache: bool,
}
impl Deref for Config {
type Target = PartialConfig;
fn deref(&self) -> &Self::Target {
&self._rest
}
}
pub trait PackageExt {
fn is_third_party(&self, policy: &Policy) -> bool;
fn is_crates_io(&self) -> bool;
fn policy_entry<'a>(&self, policy: &'a Policy) -> Option<&'a PolicyEntry>;
fn vet_version(&self) -> VetVersion;
}
impl PackageExt for Package {
fn is_third_party(&self, policy: &Policy) -> bool {
let forced_third_party = self
.policy_entry(policy)
.and_then(|policy| policy.audit_as_crates_io)
.unwrap_or(false);
forced_third_party || self.is_crates_io()
}
fn is_crates_io(&self) -> bool {
self.source
.as_ref()
.map(|s| s.is_crates_io())
.unwrap_or(false)
}
fn policy_entry<'a>(&self, policy: &'a Policy) -> Option<&'a PolicyEntry> {
policy.get(&self.name, &self.vet_version())
}
fn vet_version(&self) -> VetVersion {
VetVersion {
semver: self.version.clone(),
git_rev: self.source.as_ref().and_then(|s| {
let git_source = s.repr.strip_prefix("git+")?;
let source_url = Url::parse(git_source).ok()?;
Some(source_url.fragment()?.to_owned())
}),
}
}
}
const CACHE_DIR_SUFFIX: &str = "cargo-vet";
const CARGO_ENV: &str = "CARGO";
const PACKAGE_VET_CONFIG: &str = "vet";
const WORKSPACE_VET_CONFIG: &str = "vet";
const DURATION_DAY: Duration = Duration::from_secs(60 * 60 * 24);
struct ExitPanic(i32);
type ReportErrorFunc = dyn Fn(&miette::Report) + Send + Sync + 'static;
lazy_static! {
static ref REPORT_ERROR: Mutex<Option<Box<ReportErrorFunc>>> = Mutex::new(None);
}
fn set_report_errors_as_json(out: Arc<dyn Out>) {
*REPORT_ERROR.lock().unwrap() = Some(Box::new(move |error| {
let mut report = String::new();
miette::JSONReportHandler::new()
.render_report(&mut report, error.as_ref())
.unwrap();
writeln!(out, r#"{{"error": {report}}}"#);
}));
}
fn report_error(error: &miette::Report) {
{
let guard = REPORT_ERROR.lock().unwrap();
if let Some(do_report) = &*guard {
do_report(error);
return;
}
}
error!("{:?}", error);
}
fn main() -> Result<(), ()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.max_blocking_threads(128)
.enable_all()
.build()
.unwrap();
let _guard = runtime.enter();
let panic_result = std::panic::catch_unwind(real_main);
let main_result = match panic_result {
Ok(main_result) => main_result,
Err(e) => {
if let Some(ExitPanic(code)) = e.downcast_ref::<ExitPanic>() {
std::process::exit(*code);
} else {
std::panic::resume_unwind(e);
}
}
};
main_result.map_err(|e| {
report_error(&e);
std::process::exit(-1);
})
}
fn real_main() -> Result<(), miette::Report> {
use cli::Commands::*;
let fake_cli = cli::FakeCli::parse();
let cli::FakeCli::Vet(cli) = fake_cli;
if let Some(log_path) = &cli.log_file {
let log_file = File::create(log_path).unwrap();
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(log_file)
.init();
} else {
tracing_subscriber::fmt::fmt()
.with_max_level(cli.verbose)
.with_target(false)
.without_time()
.with_ansi(console::colors_enabled_stderr())
.with_writer(StderrLogWriter::new)
.init();
}
let using_log_file = cli.log_file.is_some();
miette::set_hook(Box::new(move |_| {
let graphical_theme = if console::colors_enabled_stderr() && !using_log_file {
miette::GraphicalTheme::unicode()
} else {
miette::GraphicalTheme::unicode_nocolor()
};
Box::new(
miette::MietteHandlerOpts::new()
.graphical_theme(graphical_theme)
.build(),
)
}))
.expect("failed to initialize error handler");
panic::set_hook(Box::new(move |panic_info| {
if panic_info.payload().is::<ExitPanic>() {
return;
}
let payload = panic_info.payload();
let message = if let Some(msg) = payload.downcast_ref::<&str>() {
msg
} else if let Some(msg) = payload.downcast_ref::<String>() {
&msg[..]
} else {
"something went wrong"
};
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
pub struct PanicError {
pub message: String,
#[help]
pub help: Option<String>,
}
report_error(
&miette::Report::from(PanicError {
message: message.to_owned(),
help: panic_info
.location()
.map(|loc| format!("at {}:{}:{}", loc.file(), loc.line(), loc.column())),
})
.wrap_err("cargo vet panicked"),
);
}));
MULTIPROGRESS.set_draw_target(ProgressDrawTarget::stderr());
let out: Arc<dyn Out> = if let Some(output_path) = &cli.output_file {
Arc::new(File::create(output_path).unwrap())
} else {
Arc::new(Term::stdout())
};
if cli.output_format == OutputFormat::Json {
set_report_errors_as_json(out.clone());
}
let cache_dir = cli.cache_dir.clone().unwrap_or_else(|| {
dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join(CACHE_DIR_SUFFIX)
});
let partial_cfg = PartialConfig {
cli,
cache_dir,
mock_cache: false,
};
match &partial_cfg.cli.command {
Some(Aggregate(sub_args)) => return cmd_aggregate(&out, &partial_cfg, sub_args),
Some(HelpMarkdown(sub_args)) => return cmd_help_md(&out, &partial_cfg, sub_args),
Some(Gc(sub_args)) => return cmd_gc(&out, &partial_cfg, sub_args),
_ => {
}
}
let cli = &partial_cfg.cli;
let cargo_path = std::env::var_os(CARGO_ENV).expect("Cargo failed to set $CARGO, how?");
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.cargo_path(cargo_path);
if let Some(manifest_path) = &cli.manifest_path {
cmd.manifest_path(manifest_path);
}
if !cli.no_all_features {
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
}
if cli.no_default_features {
cmd.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
}
if !cli.features.is_empty() {
cmd.features(cargo_metadata::CargoOpt::SomeFeatures(cli.features.clone()));
}
let mut other_options = Vec::new();
if cli.frozen {
other_options.push("--frozen".to_string());
} else {
other_options.push("--locked".to_string());
}
if !using_log_file
&& cli.output_format == OutputFormat::Human
&& console::colors_enabled_stderr()
{
other_options.push("--color=always".to_string());
}
cmd.other_options(other_options);
info!("Running: {:#?}", cmd.cargo_command());
let metadata = {
let _spinner = indeterminate_spinner("Running", "`cargo metadata`");
cmd.exec().map_err(MetadataAcquireError::from)?
};
trace!("Got Metadata!");
let default_config = MetaConfigInstance {
version: Some(1),
store: Some(StoreInfo {
path: Some(
metadata
.workspace_root
.join(storage::DEFAULT_STORE)
.into_std_path_buf(),
),
}),
};
let workspace_metacfg = metadata
.workspace_metadata
.get(WORKSPACE_VET_CONFIG)
.map(|cfg| {
MetaConfigInstance::deserialize(cfg)
.into_diagnostic()
.wrap_err("Workspace had [{WORKSPACE_VET_CONFIG}] but it was malformed")
})
.transpose()?;
let package_metacfg = metadata
.root_package()
.and_then(|r| r.metadata.get(PACKAGE_VET_CONFIG))
.map(|cfg| {
MetaConfigInstance::deserialize(cfg)
.into_diagnostic()
.wrap_err("Root package had [{PACKAGE_VET_CONFIG}] but it was malformed")
})
.transpose()?;
if workspace_metacfg.is_some() && package_metacfg.is_some() {
return Err(miette!("Both a workspace and a package defined [metadata.vet]! We don't know what that means, if you do, let us know!"));
}
let mut metacfgs = vec![default_config];
if let Some(metacfg) = workspace_metacfg {
metacfgs.push(metacfg);
}
if let Some(metacfg) = package_metacfg {
metacfgs.push(metacfg);
}
let metacfg = MetaConfig(metacfgs);
info!("Final Metadata Config: ");
info!(" - version: {}", metacfg.version());
info!(" - store.path: {:#?}", metacfg.store_path());
let init = Store::is_init(&metacfg);
if matches!(cli.command, Some(Commands::Init { .. })) {
if init {
return Err(miette!(
"'cargo vet' already initialized (store found at {})",
metacfg.store_path().display()
));
}
} else if !init {
return Err(miette!(
"You must run 'cargo vet init' (store not found at {})",
metacfg.store_path().display()
));
}
let cfg = Config {
metacfg,
metadata,
_rest: partial_cfg,
};
use RegenerateSubcommands::*;
match &cfg.cli.command {
None => cmd_check(&out, &cfg, &cfg.cli.check_args),
Some(Check(sub_args)) => cmd_check(&out, &cfg, sub_args),
Some(Init(sub_args)) => cmd_init(&out, &cfg, sub_args),
Some(Certify(sub_args)) => cmd_certify(&out, &cfg, sub_args),
Some(Import(sub_args)) => cmd_import(&out, &cfg, sub_args),
Some(AddExemption(sub_args)) => cmd_add_exemption(&out, &cfg, sub_args),
Some(RecordViolation(sub_args)) => cmd_record_violation(&out, &cfg, sub_args),
Some(Suggest(sub_args)) => cmd_suggest(&out, &cfg, sub_args),
Some(Fmt(sub_args)) => cmd_fmt(&out, &cfg, sub_args),
Some(Prune(sub_args)) => cmd_prune(&out, &cfg, sub_args),
Some(DumpGraph(sub_args)) => cmd_dump_graph(&out, &cfg, sub_args),
Some(Inspect(sub_args)) => cmd_inspect(&out, &cfg, sub_args),
Some(Diff(sub_args)) => cmd_diff(&out, &cfg, sub_args),
Some(Regenerate(Imports(sub_args))) => cmd_regenerate_imports(&out, &cfg, sub_args),
Some(Regenerate(Exemptions(sub_args))) => cmd_regenerate_exemptions(&out, &cfg, sub_args),
Some(Regenerate(AuditAsCratesIo(sub_args))) => {
cmd_regenerate_audit_as(&out, &cfg, sub_args)
}
Some(Aggregate(_)) | Some(HelpMarkdown(_)) | Some(Gc(_)) => unreachable!("handled earlier"),
}
}
fn cmd_init(_out: &Arc<dyn Out>, cfg: &Config, _sub_args: &InitArgs) -> Result<(), miette::Report> {
trace!("initializing...");
let network = Network::acquire(cfg);
let mut store = Store::create(cfg)?;
check_crate_policies(cfg, &store)?;
tokio::runtime::Handle::current().block_on(fix_audit_as(cfg, network.as_ref(), &mut store))?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::RegenerateExemptions,
prune_exemptions: true,
prune_imports: true,
});
store.commit()?;
Ok(())
}
fn cmd_inspect(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &InspectArgs,
) -> Result<(), miette::Report> {
let version = &sub_args.version;
let package = &*sub_args.package;
let fetched = {
let network = Network::acquire(cfg);
let store = Store::acquire(cfg, network.as_ref(), false)?;
let cache = Cache::acquire(cfg)?;
cache.set_last_fetch(FetchCommand::Inspect {
package: package.to_owned(),
version: version.clone(),
});
if sub_args.mode == FetchMode::Sourcegraph && version.git_rev.is_none() {
let url = format!("https://sourcegraph.com/crates/{package}@v{version}");
tokio::runtime::Handle::current()
.block_on(prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
None,
version,
Some(&url),
))
.into_diagnostic()?;
open::that(&url).into_diagnostic().wrap_err_with(|| {
format!("Couldn't open {url} in your browser, try --mode=local?")
})?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
return Ok(());
}
tokio::runtime::Handle::current().block_on(async {
let (pkg, eulas) = tokio::join!(
async {
if let Some(git_rev) = &version.git_rev {
storage::locate_local_checkout(&cfg.metadata, package, version).ok_or_else(
|| FetchError::UnknownGitRevision {
package: package.to_owned(),
git_rev: git_rev.to_owned(),
},
)
} else {
cache
.fetch_package(&cfg.metadata, network.as_ref(), package, version)
.await
}
},
prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
None,
version,
None,
),
);
eulas.into_diagnostic()?;
pkg.into_diagnostic()
})?
};
#[cfg(target_family = "unix")]
if let Some(shell) = std::env::var_os("SHELL") {
writeln!(out, "Opening nested shell in: {fetched:#?}");
writeln!(out, "Use `exit` or Ctrl-D to finish.",);
let status = std::process::Command::new(shell)
.current_dir(fetched.clone())
.env("PWD", fetched)
.status()
.map_err(CommandError::CommandFailed)
.into_diagnostic()?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
if let Some(code) = status.code() {
panic_any(ExitPanic(code));
}
return Ok(());
}
writeln!(out, " fetched to {fetched:#?}");
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
Ok(())
}
fn cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
) -> Result<(), miette::Report> {
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
let last_fetch = Cache::acquire(cfg)?.get_last_fetch();
let today = <chrono::DateTime<chrono::Utc>>::from(SystemTime::now()).date_naive();
do_cmd_certify(
out,
cfg,
sub_args,
&mut store,
network.as_ref(),
last_fetch,
today,
)?;
store.commit()?;
Ok(())
}
fn do_cmd_certify(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &CertifyArgs,
store: &mut Store,
network: Option<&Network>,
last_fetch: Option<FetchCommand>,
today: chrono::NaiveDate,
) -> Result<(), CertifyError> {
let package = if let Some(package) = &sub_args.package {
package.clone()
} else if let Some(last_fetch) = &last_fetch {
last_fetch.package().to_owned()
} else {
return Err(CertifyError::CouldntGuessPackage);
};
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,
},
}
let kind = if let Some(login) = &sub_args.wildcard {
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()))?;
let start = sub_args.start_date.unwrap_or(earliest.when);
let max_end = today + chrono::Months::new(12);
let end = sub_args.end_date.unwrap_or(max_end);
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,
}
} else if let Some(v1) = &sub_args.version1 {
if let Some(v2) = &sub_args.version2 {
CertifyKind::Delta {
from: v1.clone(),
to: v2.clone(),
}
} else {
CertifyKind::Full {
version: v1.clone(),
}
}
} else if let Some(fetch) = last_fetch.filter(|f| f.package() == package) {
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_mapper = CriteriaMapper::new(&store.audits.criteria);
let criteria_names = if sub_args.criteria.is_empty() {
let (mut chosen_criteria, version_suffix) = match &kind {
CertifyKind::Full { version } => (
guess_audit_criteria(cfg, store, &package, None, version),
version.to_string(),
),
CertifyKind::Delta { from, to } => (
guess_audit_criteria(cfg, store, &package, Some(from), to),
format!("{from} -> {to}"),
),
CertifyKind::Wildcard { .. } => {
(vec![format::SAFE_TO_DEPLOY.to_owned()], "*".to_owned())
}
};
loop {
out.clear_screen()?;
writeln!(
out,
"choose criteria to certify for {package}:{version_suffix}"
);
writeln!(out, " 0. <clear selections>");
let implied_criteria = criteria_mapper.criteria_from_list(&chosen_criteria);
for (criteria_idx, criteria_name) in criteria_mapper.all_criteria_names().enumerate() {
if chosen_criteria.iter().any(|s| s == criteria_name) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style().green().apply_to(criteria_name)
);
} else if implied_criteria.has_criteria(criteria_idx) {
writeln!(
out,
" {}. {}",
criteria_idx + 1,
out.style().yellow().apply_to(criteria_name)
);
} else {
writeln!(out, " {}. {}", criteria_idx + 1, criteria_name);
}
}
writeln!(out);
writeln!(
out,
"current selection: {:?}",
criteria_mapper
.criteria_names(&implied_criteria)
.collect::<Vec<_>>()
);
writeln!(out, "(press ENTER to accept the current criteria)");
let input = out.read_line_with_prompt("> ")?;
let input = input.trim();
if input.is_empty() {
if chosen_criteria.is_empty() {
return Err(CertifyError::NoCriteriaChosen);
}
break;
}
let answer = if let Ok(val) = input.parse::<usize>() {
val
} else {
writeln!(out, "error: not a valid integer");
continue;
};
if answer == 0 {
chosen_criteria.clear();
continue;
}
if answer > criteria_mapper.len() {
writeln!(out, "error: not a valid criteria");
continue;
}
chosen_criteria.push(criteria_mapper.criteria_name(answer - 1).to_owned());
}
chosen_criteria
} else {
sub_args.criteria.clone()
};
let criteria_set = criteria_mapper.criteria_from_list(&criteria_names);
let criteria_names = criteria_mapper
.criteria_names(&criteria_set)
.collect::<Vec<_>>();
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 {
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()?;
let new_notes = match editor_result.trim_start().strip_prefix(&statement) {
Some(notes) => notes.trim_start_matches('\n'),
None => {
return Err(CertifyError::CouldntFindCertifyStatement);
}
};
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
.iter()
.map(|s| s.to_string().into())
.collect();
match kind {
CertifyKind::Full { version } => {
store
.audits
.audits
.entry(package.clone())
.or_default()
.push(AuditEntry {
kind: AuditKind::Full { version },
criteria,
who,
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
CertifyKind::Delta { from, to } => {
store
.audits
.audits
.entry(package.clone())
.or_default()
.push(AuditEntry {
kind: AuditKind::Delta { from, to },
criteria,
who,
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
CertifyKind::Wildcard {
user_id,
start,
end,
..
} => {
store
.audits
.wildcard_audits
.entry(package.clone())
.or_default()
.push(WildcardEntry {
who,
criteria,
user_id,
start: start.into(),
end: end.into(),
notes,
aggregated_from: vec![],
is_fresh_import: false,
});
}
};
store
.validate(today, false)
.expect("the new audit entry made the store invalid?");
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_imports: false,
});
Ok(())
}
fn guess_audit_criteria(
cfg: &Config,
store: &Store,
package: PackageStr<'_>,
from: Option<&VetVersion>,
to: &VetVersion,
) -> Vec<String> {
let criteria = resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), store)
.compute_suggested_criteria(package, from, to);
if !criteria.is_empty() {
return criteria;
}
resolver::resolve(
&cfg.metadata,
cfg.cli.filter_graph.as_ref(),
&store.clone_for_suggest(true),
)
.compute_suggested_criteria(package, from, to)
}
#[allow(clippy::too_many_arguments)]
async fn prompt_criteria_eulas(
out: &Arc<dyn Out>,
cfg: &Config,
network: Option<&Network>,
store: &Store,
package: PackageStr<'_>,
from: Option<&VetVersion>,
to: &VetVersion,
url: Option<&str>,
) -> Result<(), io::Error> {
let description = if let Some(from) = from {
format!("You are about to diff versions {from} and {to} of '{package}'")
} else {
format!("You are about to inspect version {to} of '{package}'")
};
let criteria_names = guess_audit_criteria(cfg, store, package, from, to);
if criteria_names.is_empty() {
writeln!(out, "{}", out.style().bold().apply_to(description));
warn!("unable to determine likely criteria, this may not be a relevant audit for this project.");
} else {
let eulas = join_all(criteria_names.iter().map(|criteria| async {
(
&criteria[..],
eula_for_criteria(network, &store.audits.criteria, criteria).await,
)
}))
.await;
for (idx, (criteria, eula)) in eulas.into_iter().enumerate() {
let prompt = if idx == 0 {
format!("{description}, likely to certify it for {criteria:?}, which means:")
} else {
format!("... and for {criteria:?}, which means:")
};
writeln!(
out,
"{}\n\n {}",
out.style().bold().apply_to(prompt),
eula.replace('\n', "\n "),
);
}
writeln!(
out,
"{}",
out.style().bold().apply_to(
"Please read the above criteria and consider them when performing the audit."
)
);
}
writeln!(
out,
"{}",
out.style().bold().apply_to(
"Other software projects may rely on this audit. Ask for help if you're not sure.\n"
)
);
let final_prompt = if let Some(url) = url {
writeln!(
out,
"You can inspect the {} here: {}\n",
if from.is_some() { "diff" } else { "crate" },
url,
);
"(press ENTER to open in your browser, or re-run with --mode=local)"
} else {
"(press ENTER to inspect locally)"
};
let out_ = out.clone();
tokio::task::spawn_blocking(move || out_.read_line_with_prompt(final_prompt)).await??;
Ok(())
}
fn cmd_import(
_out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &ImportArgs,
) -> Result<(), miette::Report> {
let Some(network) = Network::acquire(cfg) else {
return Err(miette!("`cargo vet import` cannot be run while frozen"));
};
let registry_file;
let import_url = match &sub_args.url {
Some(url) => url,
None => {
registry_file = tokio::runtime::Handle::current().block_on(fetch_registry(&network))?;
registry_file
.registry
.get(&sub_args.name)
.ok_or_else(|| miette!("no peer named {} found in the registry", &sub_args.name))
.map(|entry| &entry.url)?
}
};
let mut store = Store::acquire_offline(cfg)?;
use std::collections::btree_map::Entry;
match store.config.imports.entry(sub_args.name.clone()) {
Entry::Occupied(_) => {
return Err(miette!(
"a peer named '{}', has already been imported",
&sub_args.name
))
}
Entry::Vacant(vacant) => {
vacant.insert(RemoteImport {
url: import_url.clone(),
exclude: Vec::new(),
criteria_map: SortedMap::new(),
});
}
}
let cache = Cache::acquire(cfg)?;
tokio::runtime::Handle::current().block_on(store.go_online(cfg, &network, &cache, false))?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferFreshImports,
prune_exemptions: true,
prune_imports: true,
});
store.commit()?;
Ok(())
}
fn cmd_record_violation(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &RecordViolationArgs,
) -> Result<(), miette::Report> {
let mut store = Store::acquire_offline(cfg)?;
let kind = AuditKind::Violation {
violation: sub_args.versions.clone(),
};
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 notes = sub_args.notes.clone();
let criteria = if sub_args.criteria.is_empty() {
vec![store.config.default_criteria.clone().into()]
} else {
sub_args
.criteria
.iter()
.map(|s| s.to_owned().into())
.collect()
};
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == sub_args.package)
{
return Err(miette!(
"'{}' isn't one of your foreign packages",
sub_args.package
));
}
let new_entry = AuditEntry {
kind,
criteria,
who,
notes,
aggregated_from: vec![],
is_fresh_import: false,
};
store
.audits
.audits
.entry(sub_args.package.clone())
.or_default()
.push(new_entry);
store.commit()?;
writeln!(out, "If you've identified a security vulnerability in {} please report it at https://github.com/rustsec/advisory-db#reporting-vulnerabilities", sub_args.package);
Ok(())
}
fn cmd_add_exemption(
_out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &AddExemptionArgs,
) -> Result<(), miette::Report> {
let mut store = Store::acquire_offline(cfg)?;
let notes = sub_args.notes.clone();
let criteria = if sub_args.criteria.is_empty() {
vec![store.config.default_criteria.clone().into()]
} else {
sub_args
.criteria
.iter()
.map(|s| s.to_owned().into())
.collect()
};
let suggest = !sub_args.no_suggest;
if !sub_args.force
&& !foreign_packages(&cfg.metadata, &store.config).any(|pkg| pkg.name == sub_args.package)
{
return Err(miette!(
"'{}' isn't one of your foreign packages",
sub_args.package
));
}
let new_entry = ExemptedDependency {
criteria,
notes,
version: sub_args.version.clone(),
suggest,
};
store
.config
.exemptions
.entry(sub_args.package.clone())
.or_default()
.push(new_entry);
store.commit()?;
Ok(())
}
fn cmd_suggest(
out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &SuggestArgs,
) -> Result<(), miette::Report> {
trace!("suggesting...");
let network = Network::acquire(cfg);
let suggest_store = Store::acquire(cfg, network.as_ref(), false)?.clone_for_suggest(true);
let report = resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), &suggest_store);
let suggest = report.compute_suggest(cfg, &suggest_store, network.as_ref())?;
match cfg.cli.output_format {
OutputFormat::Human => report
.print_suggest_human(out, cfg, suggest.as_ref())
.into_diagnostic()?,
OutputFormat::Json => report.print_json(out, suggest.as_ref())?,
}
Ok(())
}
fn cmd_regenerate_imports(
out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateImportsArgs,
) -> Result<(), miette::Report> {
trace!("regenerating imports...");
if cfg.cli.locked {
writeln!(
out,
"warning: ran `regenerate imports` with --locked, this won't do anything!"
);
return Ok(());
}
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), true)?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferFreshImports,
prune_exemptions: true,
prune_imports: true,
});
store.commit()?;
Ok(())
}
fn cmd_regenerate_audit_as(
_out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateAuditAsCratesIoArgs,
) -> Result<(), miette::Report> {
trace!("regenerating audit-as-crates-io...");
let network = Network::acquire(cfg);
let mut store = Store::acquire_offline(cfg)?;
tokio::runtime::Handle::current().block_on(fix_audit_as(cfg, network.as_ref(), &mut store))?;
store.commit()?;
Ok(())
}
async fn fix_audit_as(
cfg: &Config,
network: Option<&Network>,
store: &mut Store,
) -> Result<(), CacheAcquireError> {
let _spinner = indeterminate_spinner("Fetching", "crate metadata");
let mut cache = Cache::acquire(cfg)?;
let third_party_packages = foreign_packages_strict(&cfg.metadata, &store.config)
.map(|p| &p.name)
.collect::<SortedSet<_>>();
let issues = check_audit_as_crates_io(cfg, store, &mut cache);
if let Err(AuditAsErrors { errors }) = issues {
fn get_policy_entry<'a>(
store: &'a mut Store,
cfg: &Config,
third_party_packages: &SortedSet<&String>,
error: &PackageError,
) -> &'a mut PolicyEntry {
let is_third_party = third_party_packages.contains(&error.package);
let all_versions = || {
cfg.metadata
.packages
.iter()
.filter_map(|p| (p.name == error.package).then(|| p.vet_version()))
.collect()
};
store
.config
.policy
.get_mut_or_default(
error.package.clone(),
is_third_party.then_some(error.version.as_ref()).flatten(),
all_versions,
)
.expect("unexpected crate policy state")
}
for error in errors {
match error {
AuditAsError::NeedsAuditAs(NeedsAuditAsErrors { errors }) => {
for err in errors {
let default_audit_as = if let Some(network) = network {
let url = Url::parse(&format!(
"https://crates.io/api/v1/crates/{}",
err.package
))
.expect("invalid crate name");
let crates_api_metadata = network
.download(url)
.await
.ok()
.and_then(|response| {
serde_json::from_slice::<format::CratesAPICrate>(&response).ok()
})
.map(|result| result.crate_data);
let crates_api_description = crates_api_metadata
.as_ref()
.and_then(|crate_data| crate_data.description.as_ref());
let crates_api_repository = crates_api_metadata
.as_ref()
.and_then(|crate_data| crate_data.repository.as_ref());
cfg.metadata.packages.iter().any(|p| {
p.name == err.package
&& err
.version
.as_ref()
.map(|v| v == &p.vet_version())
.unwrap_or(true)
&& ((crates_api_description.is_some()
&& p.description.as_ref() == crates_api_description)
|| (crates_api_repository.is_some()
&& p.repository.as_ref() == crates_api_repository))
})
} else {
false
};
get_policy_entry(store, cfg, &third_party_packages, &err)
.audit_as_crates_io = Some(default_audit_as);
}
}
AuditAsError::ShouldntBeAuditAs(ShouldntBeAuditAsErrors { errors }) => {
for err in errors {
get_policy_entry(store, cfg, &third_party_packages, &err)
.audit_as_crates_io = Some(false);
}
}
AuditAsError::UnusedAuditAs(unuseds) => {
for err in unuseds.errors {
if let Some(policy) = store
.config
.policy
.get_mut(&err.package, err.version.as_ref())
{
policy.audit_as_crates_io = None;
}
}
}
}
}
}
Ok(())
}
fn cmd_regenerate_exemptions(
_out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &RegenerateExemptionsArgs,
) -> Result<(), miette::Report> {
trace!("regenerating exemptions...");
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::RegenerateExemptions,
prune_exemptions: true,
prune_imports: true,
});
store.commit()?;
Ok(())
}
fn cmd_diff(out: &Arc<dyn Out>, cfg: &Config, sub_args: &DiffArgs) -> Result<(), miette::Report> {
let version1 = &sub_args.version1;
let version2 = &sub_args.version2;
let package = &*sub_args.package;
let to_compare = {
let network = Network::acquire(cfg);
let store = Store::acquire(cfg, network.as_ref(), false)?;
let cache = Cache::acquire(cfg)?;
cache.set_last_fetch(FetchCommand::Diff {
package: package.to_owned(),
version1: version1.clone(),
version2: version2.clone(),
});
if sub_args.mode == FetchMode::Sourcegraph
&& version1.git_rev.is_none()
&& version2.git_rev.is_none()
{
let url = format!(
"https://sourcegraph.com/crates/{package}/-/compare/v{version1}...v{version2}?visible=1000000"
);
tokio::runtime::Handle::current()
.block_on(prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
Some(version1),
version2,
Some(&url),
))
.into_diagnostic()?;
open::that(&url).into_diagnostic().wrap_err_with(|| {
format!("Couldn't open {url} in your browser, try --mode=local?")
})?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
return Ok(());
}
tokio::runtime::Handle::current().block_on(async {
let (to_compare, eulas) = tokio::join!(
async {
let (pkg1, pkg2) = tokio::try_join!(
cache.fetch_package(&cfg.metadata, network.as_ref(), package, version1),
cache.fetch_package(&cfg.metadata, network.as_ref(), package, version2)
)?;
let (_, to_compare) = cache
.diffstat_package(
&pkg1,
&pkg2,
version1.git_rev.is_some() || version2.git_rev.is_some(),
)
.await?;
Ok::<_, FetchAndDiffError>(to_compare)
},
prompt_criteria_eulas(
out,
cfg,
network.as_ref(),
&store,
package,
Some(version1),
version2,
None,
)
);
eulas.into_diagnostic()?;
to_compare.into_diagnostic()
})?
};
writeln!(out);
let mut pager = Pager::new(&**out).into_diagnostic()?;
for (from, to) in to_compare {
let output = std::process::Command::new("git")
.arg("-c")
.arg("core.safecrlf=false")
.arg("diff")
.arg(if pager.use_color() {
"--color=always"
} else {
"--color=never"
})
.arg("--no-index")
.arg("--ignore-cr-at-eol")
.arg(&from)
.arg(&to)
.stdout(Stdio::piped())
.output()
.map_err(CommandError::CommandFailed)
.into_diagnostic()?;
io::Write::write_all(&mut pager, &output.stdout).into_diagnostic()?;
}
pager.wait().into_diagnostic()?;
writeln!(out, "\nUse |cargo vet certify| to record your audit.");
Ok(())
}
fn cmd_check(
out: &Arc<dyn Out>,
cfg: &Config,
_sub_args: &CheckArgs,
) -> Result<(), miette::Report> {
trace!("vetting...");
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
if !cfg.cli.locked {
let mut cache = Cache::acquire(cfg).into_diagnostic()?;
check_crate_policies(cfg, &store)?;
check_audit_as_crates_io(cfg, &store, &mut cache)?;
}
let report = resolver::resolve(&cfg.metadata, cfg.cli.filter_graph.as_ref(), &store);
let suggest = if !cfg.cli.locked {
report.compute_suggest(cfg, &store, network.as_ref())?
} else {
None
};
match cfg.cli.output_format {
OutputFormat::Human => report
.print_human(out, cfg, suggest.as_ref())
.into_diagnostic()?,
OutputFormat::Json => report.print_json(out, suggest.as_ref())?,
}
if report.has_errors() {
panic_any(ExitPanic(-1));
} else {
if !cfg.cli.locked {
let (pruned_imports, pruned_exemptions) =
resolver::get_store_updates(cfg, &store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferFreshImports,
prune_exemptions: true,
prune_imports: true,
});
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: resolver::SearchMode::PreferExemptions,
prune_exemptions: false,
prune_imports: false,
});
if store.config.exemptions != pruned_exemptions {
warn!("Your supply-chain has unnecessary exemptions which could be relaxed or pruned.");
warn!("Consider running `cargo vet prune` to prune unnecessary exemptions and imports.");
} else if store.imports != pruned_imports {
warn!("Your supply-chain has unnecessary imports which could be pruned.");
warn!("Consider running `cargo vet prune` to prune unnecessary imports.");
}
}
store.commit()?;
}
Ok(())
}
fn cmd_prune(
_out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &PruneArgs,
) -> Result<(), miette::Report> {
let network = Network::acquire(cfg);
let mut store = Store::acquire(cfg, network.as_ref(), false)?;
let _spinner = indeterminate_spinner("Pruning", "unnecessary imports and exemptions");
resolver::update_store(cfg, &mut store, |_| resolver::UpdateMode {
search_mode: if sub_args.no_exemptions {
resolver::SearchMode::PreferExemptions
} else {
resolver::SearchMode::PreferFreshImports
},
prune_exemptions: !sub_args.no_exemptions,
prune_imports: !sub_args.no_imports,
});
store.commit()?;
Ok(())
}
fn cmd_aggregate(
out: &Arc<dyn Out>,
cfg: &PartialConfig,
sub_args: &AggregateArgs,
) -> Result<(), miette::Report> {
let network =
Network::acquire(cfg).ok_or_else(|| miette!("cannot aggregate imports when --frozen"))?;
let mut urls = Vec::new();
{
let sources_file = BufReader::new(
File::open(&sub_args.sources)
.into_diagnostic()
.wrap_err("failed to open sources file")?,
);
for line_result in sources_file.lines() {
let line = line_result
.into_diagnostic()
.wrap_err("failed to read sources file")?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
urls.push(
Url::parse(trimmed)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse url: {trimmed:?}"))?,
);
}
}
let progress_bar = progress_bar("Fetching", "source audits", urls.len() as u64);
let sources = tokio::runtime::Handle::current()
.block_on(try_join_all(urls.into_iter().map(|url| async {
let _guard = IncProgressOnDrop(&progress_bar, 1);
let url_string = url.to_string();
let audit_bytes = network.download(url).await?;
let audit_string = String::from_utf8(audit_bytes).map_err(LoadTomlError::from)?;
let audit_source = SourceFile::new(&url_string, audit_string.clone());
let audit_file: AuditsFile = toml::de::from_str(&audit_string)
.map_err(|error| {
let (line, col) = error.line_col().unwrap_or((0, 0));
TomlParseError {
source_code: audit_source,
span: SourceOffset::from_location(&audit_string, line + 1, col + 1),
error,
}
})
.map_err(LoadTomlError::from)?;
Ok::<_, FetchAuditError>((url_string, audit_file))
})))
.into_diagnostic()?;
let merged_audits = do_aggregate_audits(sources).into_diagnostic()?;
let document = serialization::to_formatted_toml(merged_audits).into_diagnostic()?;
write!(out, "{document}");
Ok(())
}
fn do_aggregate_audits(sources: Vec<(String, AuditsFile)>) -> Result<AuditsFile, AggregateErrors> {
let mut errors = Vec::new();
let mut aggregate = AuditsFile {
criteria: SortedMap::new(),
wildcard_audits: SortedMap::new(),
audits: SortedMap::new(),
};
for (source, audit_file) in sources {
for (criteria_name, mut criteria_entry) in audit_file.criteria {
match aggregate.criteria.entry(criteria_name) {
std::collections::btree_map::Entry::Vacant(vacant) => {
criteria_entry.aggregated_from.push(source.clone().into());
vacant.insert(criteria_entry);
}
std::collections::btree_map::Entry::Occupied(occupied) => {
let prev_source = occupied
.get()
.aggregated_from
.last()
.map(|s| s.to_string())
.unwrap_or_default();
if occupied.get().description != criteria_entry.description
|| occupied.get().description_url != criteria_entry.description_url
{
errors.push(AggregateError::CriteriaDescriptionMismatch(
AggregateCriteriaDescriptionMismatchError {
criteria_name: occupied.key().to_owned(),
first: AggregateCriteriaDescription {
source: prev_source.clone(),
description: occupied.get().description.clone(),
description_url: occupied.get().description_url.clone(),
},
second: AggregateCriteriaDescription {
source: source.clone(),
description: criteria_entry.description.clone(),
description_url: criteria_entry.description_url.clone(),
},
},
))
}
if occupied.get().implies != criteria_entry.implies {
errors.push(AggregateError::ImpliesMismatch(
AggregateImpliesMismatchError {
criteria_name: occupied.key().to_owned(),
first: AggregateCriteriaImplies {
source: prev_source.clone(),
implies: occupied
.get()
.implies
.iter()
.map(|c| c.to_string())
.collect(),
},
second: AggregateCriteriaImplies {
source: source.clone(),
implies: criteria_entry
.implies
.iter()
.map(|c| c.to_string())
.collect(),
},
},
));
}
}
}
}
for (package_name, audits) in audit_file.audits {
aggregate
.audits
.entry(package_name)
.or_default()
.extend(audits.into_iter().map(|mut audit_entry| {
audit_entry.aggregated_from.push(source.clone().into());
audit_entry
}));
}
for (package_name, audits) in audit_file.wildcard_audits {
aggregate
.wildcard_audits
.entry(package_name)
.or_default()
.extend(audits.into_iter().map(|mut wildcard_entry| {
wildcard_entry.aggregated_from.push(source.clone().into());
wildcard_entry
}));
}
}
if errors.is_empty() {
Ok(aggregate)
} else {
Err(AggregateErrors { errors })
}
}
fn cmd_dump_graph(
out: &Arc<dyn Out>,
cfg: &Config,
sub_args: &DumpGraphArgs,
) -> Result<(), miette::Report> {
trace!("dumping...");
let graph = resolver::DepGraph::new(&cfg.metadata, cfg.cli.filter_graph.as_ref(), None);
match cfg.cli.output_format {
OutputFormat::Human => graph.print_mermaid(out, sub_args).into_diagnostic()?,
OutputFormat::Json => {
serde_json::to_writer_pretty(&**out, &graph.nodes).into_diagnostic()?
}
}
Ok(())
}
fn cmd_fmt(_out: &Arc<dyn Out>, cfg: &Config, _sub_args: &FmtArgs) -> Result<(), miette::Report> {
trace!("formatting...");
let store = Store::acquire_offline(cfg)?;
store.commit()?;
Ok(())
}
fn cmd_help_md(
out: &Arc<dyn Out>,
_cfg: &PartialConfig,
_sub_args: &HelpMarkdownArgs,
) -> Result<(), miette::Report> {
let app_name = "cargo-vet";
let pretty_app_name = "cargo vet";
writeln!(out, "# {pretty_app_name} CLI manual");
writeln!(out);
writeln!(
out,
"> This manual can be regenerated with `{pretty_app_name} help-markdown`"
);
writeln!(out);
let mut fake_cli = FakeCli::command().term_width(0);
let full_command = fake_cli.get_subcommands_mut().next().unwrap();
full_command.build();
let mut todo = vec![full_command];
let mut is_full_command = true;
while let Some(command) = todo.pop() {
let mut help_buf = Vec::new();
command.write_long_help(&mut help_buf).unwrap();
let help = String::from_utf8(help_buf).unwrap();
let mut lines = help.lines();
let version_line = lines.next().unwrap();
let subcommand_name = command.get_name();
if is_full_command {
writeln!(out, "Version: `{version_line}`");
writeln!(out);
} else {
writeln!(out, "<br><br><br>");
writeln!(out, "## {pretty_app_name} {subcommand_name}");
}
let mut in_subcommands_listing = false;
let mut in_usage = false;
let mut in_global_options = false;
for line in lines {
if let Some(heading) = line.strip_suffix(':') {
if !line.starts_with(' ') {
if heading.to_ascii_uppercase() == heading {
in_subcommands_listing = heading == "SUBCOMMANDS";
in_usage = heading == "USAGE";
in_global_options = heading == "GLOBAL OPTIONS";
writeln!(out, "### {heading}");
if in_global_options && !is_full_command {
writeln!(
out,
"This subcommand accepts all the [global options](#global-options)"
);
}
} else {
writeln!(out, "### {heading}");
}
continue;
}
}
if in_global_options && !is_full_command {
continue;
}
if in_subcommands_listing && !line.starts_with(" ") {
let own_subcommand_name = line.trim();
write!(
out,
"* [{own_subcommand_name}](#{app_name}-{own_subcommand_name}): "
);
continue;
}
let line = line.trim();
if in_usage && line.starts_with(pretty_app_name) {
writeln!(out, "```");
writeln!(out, "{line}");
writeln!(out, "```");
continue;
}
if line.starts_with('-') || line.starts_with('<') {
writeln!(out, "#### `{line}`");
continue;
}
if line.starts_with('[') {
writeln!(out, "\\{line} ");
continue;
}
writeln!(out, "{line}");
}
writeln!(out);
todo.extend(
command
.get_subcommands_mut()
.filter(|cmd| !cmd.is_hide_set())
.collect::<Vec<_>>()
.into_iter()
.rev(),
);
is_full_command = false;
}
Ok(())
}
fn cmd_gc(
out: &Arc<dyn Out>,
cfg: &PartialConfig,
sub_args: &GcArgs,
) -> Result<(), miette::Report> {
let cache = Cache::acquire(cfg)?;
if sub_args.clean {
writeln!(
out,
"cleaning entire contents of cache directory: {}",
cfg.cache_dir.display()
);
cache.clean_sync().into_diagnostic()?;
return Ok(());
}
if sub_args.max_package_age_days.is_nan() {
return Err(miette!("max package age cannot be NaN"));
}
if sub_args.max_package_age_days < 0.0 {
return Err(miette!("max package age cannot be negative"));
}
cache.gc_sync(DURATION_DAY.mul_f64(sub_args.max_package_age_days));
Ok(())
}
struct UserInfo {
username: String,
email: String,
}
fn get_user_info() -> Result<UserInfo, UserInfoError> {
fn get_git_config(value_name: &str) -> Result<String, CommandError> {
let out = std::process::Command::new("git")
.arg("config")
.arg("--get")
.arg(value_name)
.output()
.map_err(CommandError::CommandFailed)?;
if !out.status.success() {
return Err(CommandError::BadStatus(out.status.code().unwrap()));
}
String::from_utf8(out.stdout)
.map(|s| s.trim().to_string())
.map_err(CommandError::BadOutput)
}
let username = get_git_config("user.name").map_err(UserInfoError::UserCommandFailed)?;
let email = get_git_config("user.email").map_err(UserInfoError::EmailCommandFailed)?;
Ok(UserInfo { username, email })
}
async fn eula_for_criteria(
network: Option<&Network>,
criteria_map: &SortedMap<CriteriaName, CriteriaEntry>,
criteria: CriteriaStr<'_>,
) -> String {
let builtin_eulas = [
(
format::SAFE_TO_DEPLOY,
include_str!("criteria/safe-to-deploy.txt"),
),
(
format::SAFE_TO_RUN,
include_str!("criteria/safe-to-run.txt"),
),
]
.into_iter()
.collect::<HashMap<_, _>>();
let builtin = builtin_eulas.get(criteria).map(|s| s.to_string());
if let Some(eula) = builtin {
return eula;
}
let criteria_entry = criteria_map
.get(criteria)
.unwrap_or_else(|| panic!("no entry for the criteria {criteria}"));
assert!(
criteria_entry.description.is_some() || criteria_entry.description_url.is_some(),
"entry for criteria {criteria} is corrupt!"
);
if let Some(eula) = criteria_entry.description.clone() {
return eula;
}
let url = Url::parse(criteria_entry.description_url.as_ref().unwrap()).unwrap();
if let Some(network) = network {
if let Ok(eula) = network.download(url.clone()).await.and_then(|bytes| {
String::from_utf8(bytes).map_err(|error| DownloadError::InvalidText {
url: Box::new(url.clone()),
error,
})
}) {
return eula;
}
}
format!("Could not download criteria description, it should be available at {url}")
}
fn foreign_packages<'a>(
metadata: &'a Metadata,
config: &'a ConfigFile,
) -> impl Iterator<Item = &'a Package> + 'a {
metadata
.packages
.iter()
.filter(|package| package.is_third_party(&config.policy))
}
fn first_party_packages_strict<'a>(
metadata: &'a Metadata,
_config: &'a ConfigFile,
) -> impl Iterator<Item = &'a Package> + 'a {
metadata
.packages
.iter()
.filter(move |package| !package.is_crates_io())
}
fn foreign_packages_strict<'a>(
metadata: &'a Metadata,
_config: &ConfigFile,
) -> impl Iterator<Item = &'a Package> + 'a {
metadata
.packages
.iter()
.filter(move |package| package.is_crates_io())
}
fn check_audit_as_crates_io(
cfg: &Config,
store: &Store,
cache: &mut Cache,
) -> Result<(), AuditAsErrors> {
if !cache.has_registry() {
return Ok(());
}
let mut needs_audit_as_entry = vec![];
let mut shouldnt_be_audit_as = vec![];
let mut unused_audit_as: SortedSet<(PackageName, Option<VetVersion>)> = store
.config
.policy
.iter()
.filter(|(_, _, policy)| policy.audit_as_crates_io.is_some())
.map(|(name, version, _)| (name.clone(), version.cloned()))
.collect();
for package in first_party_packages_strict(&cfg.metadata, &store.config) {
unused_audit_as.remove(&(package.name.clone(), Some(package.vet_version())));
unused_audit_as.remove(&(package.name.clone(), None));
let audit_policy = package
.policy_entry(&store.config.policy)
.and_then(|policy| policy.audit_as_crates_io);
if audit_policy == Some(false) {
continue;
}
if let Some(index_entry) = cache.query_package_from_index(&package.name) {
if storage::exact_version(&index_entry, &package.version).is_some() {
if audit_policy.is_none() {
needs_audit_as_entry.push(PackageError {
package: package.name.clone(),
version: Some(package.vet_version()),
});
}
continue;
}
}
if audit_policy == Some(true) {
if cache.ensure_index_up_to_date() {
return check_audit_as_crates_io(cfg, store, cache);
}
shouldnt_be_audit_as.push(PackageError {
package: package.name.clone(),
version: Some(package.vet_version()),
});
}
}
if !needs_audit_as_entry.is_empty()
|| !shouldnt_be_audit_as.is_empty()
|| !unused_audit_as.is_empty()
{
let mut errors = vec![];
if !needs_audit_as_entry.is_empty() {
errors.push(AuditAsError::NeedsAuditAs(NeedsAuditAsErrors {
errors: needs_audit_as_entry,
}));
}
if !shouldnt_be_audit_as.is_empty() {
errors.push(AuditAsError::ShouldntBeAuditAs(ShouldntBeAuditAsErrors {
errors: shouldnt_be_audit_as,
}));
}
if !unused_audit_as.is_empty() {
errors.push(AuditAsError::UnusedAuditAs(UnusedAuditAsErrors {
errors: unused_audit_as
.into_iter()
.map(|(package, version)| PackageError { package, version })
.collect(),
}))
}
return Err(AuditAsErrors { errors });
}
Ok(())
}
fn check_crate_policies(cfg: &Config, store: &Store) -> Result<(), CratePolicyErrors> {
let mut policy_crates: SortedSet<&PackageName> = store.config.policy.package.keys().collect();
let mut versioned_policy_crates: SortedSet<(PackageName, VetVersion)> = store
.config
.policy
.iter()
.filter_map(|(name, version, _)| version.map(|version| (name.clone(), version.clone())))
.collect();
let third_party_packages = foreign_packages_strict(&cfg.metadata, &store.config)
.map(|p| &p.name)
.collect::<SortedSet<_>>();
let dependency_criteria_packages = store
.config
.policy
.iter()
.filter_map(|(name, _, entry)| (!entry.dependency_criteria.is_empty()).then_some(name))
.collect::<SortedSet<_>>();
let mut needs_policy_version_errors = Vec::new();
for package in &cfg.metadata.packages {
policy_crates.remove(&package.name);
let versioned_policy_exists =
versioned_policy_crates.remove(&(package.name.clone(), package.vet_version()));
if third_party_packages.contains(&package.name)
&& dependency_criteria_packages.contains(&package.name)
&& !versioned_policy_exists
{
needs_policy_version_errors.push(PackageError {
package: package.name.clone(),
version: Some(package.vet_version()),
});
}
}
let unused_policy_version_errors: Vec<_> = policy_crates
.into_iter()
.map(|name| PackageError {
package: name.clone(),
version: None,
})
.chain(
versioned_policy_crates
.into_iter()
.map(|(package, version)| PackageError {
package,
version: Some(version),
}),
)
.collect();
if !needs_policy_version_errors.is_empty() || !unused_policy_version_errors.is_empty() {
let mut errors = Vec::new();
if !needs_policy_version_errors.is_empty() {
errors.push(CratePolicyError::NeedsVersion(NeedsPolicyVersionErrors {
errors: needs_policy_version_errors,
}));
}
if !unused_policy_version_errors.is_empty() {
errors.push(CratePolicyError::UnusedVersion(UnusedPolicyVersionErrors {
errors: unused_policy_version_errors,
}));
}
Err(CratePolicyErrors { errors })
} else {
Ok(())
}
}