blob: adfff5921747ee6d6c44d15dbb2009c055ac9a85 [file] [log] [blame]
use std::{
collections::BTreeMap,
ffi::OsString,
fmt, fs, io,
path::PathBuf,
sync::{Arc, Mutex},
};
use cargo_metadata::{semver, Metadata};
use clap::Parser;
use serde_json::{json, Value};
use crate::{
format::{
AuditEntry, AuditKind, AuditsFile, ConfigFile, CratesAPICrate, CratesAPICrateMetadata,
CratesAPIUser, CratesAPIVersion, CratesPublisher, CratesUserId, CriteriaEntry, CriteriaMap,
CriteriaName, CriteriaStr, ExemptedDependency, FastMap, ImportsFile, MetaConfig,
PackageName, PackagePolicyEntry, PackageStr, PolicyEntry, SortedMap, SortedSet, TrustEntry,
VersionReq, VetVersion, WildcardEntry, SAFE_TO_DEPLOY, SAFE_TO_RUN,
},
git_tool::Editor,
network::Network,
out::Out,
resolver::ResolveReport,
storage::Store,
Config, PackageExt, PartialConfig,
};
/// Helper for performing an `assert_snapshot!` for the report output of a
/// resolver invocation. This will generate both human and JSON reports for the
/// given resolve report, and snapshot both. The JSON reports will have the
/// suffix `.json`.
///
/// Unlike a normal `assert_snapshot!` the snapshot name isn't inferred by this
/// macro, as multiple snapshots with different names need to be generated.
macro_rules! assert_report_snapshot {
($name:expr, $metadata:expr, $store:expr) => {
assert_report_snapshot!($name, $metadata, $store, None);
};
($name:expr, $metadata:expr, $store:expr, $network:expr) => {{
let report = $crate::resolver::resolve(&$metadata, None, &$store);
let (human, json) = $crate::tests::get_reports(&$metadata, report, &$store, $network);
insta::assert_snapshot!($name, human);
insta::assert_snapshot!(concat!($name, ".json"), json);
}};
}
mod aggregate;
mod audit_as_crates_io;
mod certify;
mod crate_policies;
mod import;
mod regenerate_unaudited;
mod registry;
mod renew;
mod store_parsing;
mod trusted;
mod unpublished;
mod vet;
mod violations;
mod wildcard;
// Some room above and below
const DEFAULT_VER: u64 = 10;
const DEFAULT_CRIT: CriteriaStr = "reviewed";
// Some strings for imports
const FOREIGN: &str = "peer-company";
const FOREIGN_URL: &str = "https://peercompany.co.uk";
const OTHER_FOREIGN: &str = "rival-company";
const OTHER_FOREIGN_URL: &str = "https://rivalcompany.ca";
lazy_static::lazy_static! {
static ref TEST_RUNTIME: tokio::runtime::Runtime = {
let error_colors_enabled = false;
miette::set_hook(Box::new(move |_| {
let graphical_theme = if error_colors_enabled {
miette::GraphicalTheme::unicode()
} else {
miette::GraphicalTheme::unicode_nocolor()
};
Box::new(
miette::MietteHandlerOpts::new()
.graphical_theme(graphical_theme)
.width(80)
.build()
)
})).expect("Failed to initialize error handler");
tracing_subscriber::fmt::fmt()
.with_max_level(tracing::level_filters::LevelFilter::TRACE)
.with_target(false)
.without_time()
.with_writer(tracing_subscriber::fmt::writer::TestWriter::new())
.init();
tokio::runtime::Runtime::new().unwrap()
};
}
struct MockMetadata {
packages: Vec<MockPackage>,
pkgids: Vec<String>,
idx_by_name_and_ver: BTreeMap<PackageStr<'static>, BTreeMap<VetVersion, usize>>,
}
struct MockPackage {
name: &'static str,
version: VetVersion,
deps: Vec<MockDependency>,
dev_deps: Vec<MockDependency>,
build_deps: Vec<MockDependency>,
targets: Vec<&'static str>,
is_workspace: bool,
is_first_party: bool,
}
struct MockDependency {
name: &'static str,
version: VetVersion,
}
impl Default for MockPackage {
fn default() -> Self {
Self {
name: "",
version: ver(DEFAULT_VER),
deps: vec![],
dev_deps: vec![],
build_deps: vec![],
targets: vec!["lib"],
is_workspace: false,
is_first_party: false,
}
}
}
fn ver(major: u64) -> VetVersion {
VetVersion {
semver: semver::Version {
major,
minor: 0,
patch: 0,
pre: Default::default(),
build: Default::default(),
},
git_rev: None,
}
}
fn dep(name: &'static str) -> MockDependency {
dep_ver(name, DEFAULT_VER)
}
fn dep_ver(name: &'static str, version: u64) -> MockDependency {
MockDependency {
name,
version: ver(version),
}
}
#[allow(dead_code)]
fn default_exemptions(version: VetVersion, config: &ConfigFile) -> ExemptedDependency {
ExemptedDependency {
version,
criteria: vec![config.default_criteria.clone().into()],
notes: None,
suggest: true,
}
}
fn exemptions(version: VetVersion, criteria: CriteriaStr) -> ExemptedDependency {
ExemptedDependency {
version,
criteria: vec![criteria.to_string().into()],
notes: None,
suggest: true,
}
}
fn delta_audit(from: VetVersion, to: VetVersion, criteria: CriteriaStr) -> AuditEntry {
AuditEntry {
who: vec![],
notes: None,
criteria: vec![criteria.to_string().into()],
kind: AuditKind::Delta { from, to },
aggregated_from: vec![],
is_fresh_import: false,
}
}
fn full_audit(version: VetVersion, criteria: CriteriaStr) -> AuditEntry {
AuditEntry {
who: vec![],
notes: None,
criteria: vec![criteria.to_string().into()],
kind: AuditKind::Full { version },
aggregated_from: vec![],
is_fresh_import: false,
}
}
fn full_audit_m(
version: VetVersion,
criteria: impl IntoIterator<Item = impl Into<CriteriaName>>,
) -> AuditEntry {
AuditEntry {
who: vec![],
notes: None,
criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
kind: AuditKind::Full { version },
aggregated_from: vec![],
is_fresh_import: false,
}
}
fn violation_hard(version: VersionReq) -> AuditEntry {
AuditEntry {
who: vec![],
notes: None,
criteria: vec![SAFE_TO_RUN.to_string().into()],
kind: AuditKind::Violation { violation: version },
aggregated_from: vec![],
is_fresh_import: false,
}
}
#[allow(dead_code)]
fn violation(version: VersionReq, criteria: CriteriaStr) -> AuditEntry {
AuditEntry {
who: vec![],
notes: None,
criteria: vec![criteria.to_string().into()],
kind: AuditKind::Violation { violation: version },
aggregated_from: vec![],
is_fresh_import: false,
}
}
#[allow(dead_code)]
fn violation_m(
version: VersionReq,
criteria: impl IntoIterator<Item = impl Into<CriteriaName>>,
) -> AuditEntry {
AuditEntry {
who: vec![],
notes: None,
criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
kind: AuditKind::Violation { violation: version },
aggregated_from: vec![],
is_fresh_import: false,
}
}
fn wildcard_audit(user_id: u64, criteria: CriteriaStr) -> WildcardEntry {
WildcardEntry {
who: vec![],
notes: None,
criteria: vec![criteria.to_string().into()],
user_id,
start: chrono::NaiveDate::from_ymd_opt(2022, 12, 1).unwrap().into(),
end: chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap().into(),
renew: None,
aggregated_from: vec![],
is_fresh_import: false,
}
}
fn wildcard_audit_m(
user_id: u64,
criteria: impl IntoIterator<Item = impl Into<CriteriaName>>,
) -> WildcardEntry {
WildcardEntry {
who: vec![],
notes: None,
criteria: criteria.into_iter().map(|s| s.into().into()).collect(),
user_id,
start: chrono::NaiveDate::from_ymd_opt(2022, 12, 1).unwrap().into(),
end: chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap().into(),
renew: None,
aggregated_from: vec![],
is_fresh_import: false,
}
}
fn trusted_entry(user_id: u64, criteria: CriteriaStr) -> TrustEntry {
TrustEntry {
notes: None,
criteria: vec![criteria.to_string().into()],
user_id,
start: chrono::NaiveDate::from_ymd_opt(2022, 12, 1).unwrap().into(),
end: chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap().into(),
aggregated_from: vec![],
}
}
fn publisher_entry(version: VetVersion, user_id: u64) -> CratesPublisher {
CratesPublisher {
version,
when: chrono::NaiveDate::from_ymd_opt(2022, 12, 15).unwrap(),
user_id,
user_login: format!("user{user_id}"),
user_name: None,
is_fresh_import: false,
}
}
fn publisher_entry_named(
version: VetVersion,
user_id: u64,
login: &str,
name: &str,
) -> CratesPublisher {
CratesPublisher {
version,
when: chrono::NaiveDate::from_ymd_opt(2022, 12, 15).unwrap(),
user_id,
user_login: login.to_owned(),
user_name: Some(name.to_owned()),
is_fresh_import: false,
}
}
fn default_policy() -> PolicyEntry {
PolicyEntry {
audit_as_crates_io: None,
criteria: None,
dev_criteria: None,
dependency_criteria: SortedMap::new(),
notes: None,
}
}
fn audit_as_policy(audit_as_crates_io: Option<bool>) -> PackagePolicyEntry {
PackagePolicyEntry::Unversioned(PolicyEntry {
audit_as_crates_io,
..default_policy()
})
}
fn audit_as_policy_with<F: Fn(&mut PolicyEntry)>(
audit_as_crates_io: Option<bool>,
alter: F,
) -> PackagePolicyEntry {
let mut entry = PolicyEntry {
audit_as_crates_io,
..default_policy()
};
alter(&mut entry);
PackagePolicyEntry::Unversioned(entry)
}
fn self_policy(criteria: impl IntoIterator<Item = impl Into<CriteriaName>>) -> PackagePolicyEntry {
PackagePolicyEntry::Unversioned(PolicyEntry {
criteria: Some(criteria.into_iter().map(|s| s.into().into()).collect()),
..default_policy()
})
}
fn dep_policy(
dependency_criteria: impl IntoIterator<
Item = (
impl Into<PackageName>,
impl IntoIterator<Item = impl Into<CriteriaName>>,
),
>,
) -> PackagePolicyEntry {
PackagePolicyEntry::Unversioned(PolicyEntry {
dependency_criteria: dependency_criteria
.into_iter()
.map(|(k, v)| {
(
k.into().into(),
v.into_iter().map(|s| s.into().into()).collect::<Vec<_>>(),
)
})
.collect(),
..default_policy()
})
}
fn criteria(description: &str) -> CriteriaEntry {
CriteriaEntry {
description: Some(description.to_owned()),
description_url: None,
implies: vec![],
aggregated_from: vec![],
}
}
fn criteria_implies(
description: &str,
implies: impl IntoIterator<Item = impl Into<CriteriaName>>,
) -> CriteriaEntry {
CriteriaEntry {
description: Some(description.to_owned()),
description_url: None,
implies: implies.into_iter().map(|s| s.into().into()).collect(),
aggregated_from: vec![],
}
}
impl MockMetadata {
fn simple() -> Self {
// A simple dependency tree to test basic functionality on.
//
// Graph
// =======================================================================================
//
// root-package
// |
// first-party
// / \
// third-party1 third-party2
// |
// transitive-third-party1
//
MockMetadata::new(vec![
MockPackage {
name: "root-package",
is_workspace: true,
is_first_party: true,
deps: vec![dep("first-party")],
..Default::default()
},
MockPackage {
name: "first-party",
is_first_party: true,
deps: vec![dep("third-party1"), dep("third-party2")],
..Default::default()
},
MockPackage {
name: "third-party1",
deps: vec![dep("transitive-third-party1")],
..Default::default()
},
MockPackage {
name: "third-party2",
..Default::default()
},
MockPackage {
name: "transitive-third-party1",
..Default::default()
},
])
}
fn complex() -> Self {
// A Complex dependency tree to test more weird interactions and corner cases:
//
// * firstAB: first-party shared between two roots
// * firstB-nodeps: first-party with no third-parties
// * third-core: third-party used by everything, has two versions in-tree
//
// Graph
// =======================================================================================
//
// rootA rootB
// ------- ---------------------
// / \ / | \
// / \ / | \
// firstA firstAB firstB firstB-nodeps
// / \ \ |
// / \ \ |
// / thirdA thirdAB +
// / \ | /
// / \ | /
// third-core:v5 third-core:v10
//
MockMetadata::new(vec![
MockPackage {
name: "rootA",
is_workspace: true,
is_first_party: true,
deps: vec![dep("firstA"), dep("firstAB")],
..Default::default()
},
MockPackage {
name: "rootB",
is_workspace: true,
is_first_party: true,
deps: vec![dep("firstB"), dep("firstAB"), dep("firstB-nodeps")],
..Default::default()
},
MockPackage {
name: "firstA",
is_first_party: true,
deps: vec![dep("thirdA"), dep_ver("third-core", 5)],
..Default::default()
},
MockPackage {
name: "firstAB",
is_first_party: true,
deps: vec![dep("thirdAB")],
..Default::default()
},
MockPackage {
name: "firstB",
is_first_party: true,
deps: vec![dep("third-core")],
..Default::default()
},
MockPackage {
name: "firstB-nodeps",
is_first_party: true,
..Default::default()
},
MockPackage {
name: "thirdA",
deps: vec![dep("third-core")],
..Default::default()
},
MockPackage {
name: "thirdAB",
deps: vec![dep("third-core")],
..Default::default()
},
MockPackage {
name: "third-core",
..Default::default()
},
MockPackage {
name: "third-core",
version: ver(5),
..Default::default()
},
])
}
/// The `third-party` crate is used as both a first- and third-party crate (with different
/// versions).
fn overlapping() -> Self {
MockMetadata::new(vec![
MockPackage {
name: "root-package",
is_workspace: true,
is_first_party: true,
deps: vec![dep("first-party"), dep_ver("third-party", 1)],
..Default::default()
},
MockPackage {
name: "first-party",
is_first_party: true,
deps: vec![dep_ver("third-party", 2)],
..Default::default()
},
MockPackage {
name: "third-party",
is_first_party: true,
version: ver(1),
..Default::default()
},
MockPackage {
name: "third-party",
version: ver(2),
..Default::default()
},
])
}
fn simple_deps() -> Self {
// Different dependency cases
MockMetadata::new(vec![
MockPackage {
name: "root",
is_workspace: true,
is_first_party: true,
deps: vec![dep("normal"), dep("proc-macro")],
dev_deps: vec![dep("dev"), dep("dev-proc-macro")],
build_deps: vec![dep("build"), dep("build-proc-macro")],
..Default::default()
},
MockPackage {
name: "normal",
..Default::default()
},
MockPackage {
name: "dev",
..Default::default()
},
MockPackage {
name: "build",
..Default::default()
},
MockPackage {
name: "proc-macro",
targets: vec!["proc-macro"],
..Default::default()
},
MockPackage {
name: "dev-proc-macro",
targets: vec!["proc-macro"],
..Default::default()
},
MockPackage {
name: "build-proc-macro",
targets: vec!["proc-macro"],
..Default::default()
},
])
}
fn cycle() -> Self {
// Different dependency cases
MockMetadata::new(vec![
MockPackage {
name: "root",
is_workspace: true,
is_first_party: true,
deps: vec![dep("normal")],
dev_deps: vec![dep("dev-cycle")],
..Default::default()
},
MockPackage {
name: "normal",
..Default::default()
},
MockPackage {
name: "dev-cycle",
deps: vec![dep("root")],
..Default::default()
},
])
}
fn dev_detection() -> Self {
MockMetadata::new(vec![
MockPackage {
name: "root",
is_workspace: true,
is_first_party: true,
deps: vec![dep("normal"), dep("both")],
dev_deps: vec![dep("dev-cycle-direct"), dep("both"), dep("simple-dev")],
..Default::default()
},
MockPackage {
name: "normal",
..Default::default()
},
MockPackage {
name: "both",
..Default::default()
},
MockPackage {
name: "simple-dev",
deps: vec![dep("simple-dev-indirect")],
..Default::default()
},
MockPackage {
name: "simple-dev-indirect",
..Default::default()
},
MockPackage {
name: "dev-cycle-direct",
deps: vec![dep("dev-cycle-indirect")],
..Default::default()
},
MockPackage {
name: "dev-cycle-indirect",
deps: vec![dep("root")],
..Default::default()
},
])
}
fn haunted_tree() -> Self {
MockMetadata::new(vec![
MockPackage {
name: "root",
is_workspace: true,
is_first_party: true,
deps: vec![dep("first")],
..Default::default()
},
MockPackage {
name: "first",
is_workspace: true,
is_first_party: true,
deps: vec![dep("third-normal")],
dev_deps: vec![dep("third-dev")],
..Default::default()
},
MockPackage {
name: "third-normal",
..Default::default()
},
MockPackage {
name: "third-dev",
..Default::default()
},
])
}
fn descriptive() -> Self {
MockMetadata::new(vec![MockPackage {
name: "descriptive",
is_workspace: true,
is_first_party: true,
..Default::default()
}])
}
fn new(packages: Vec<MockPackage>) -> Self {
let mut pkgids = vec![];
let mut idx_by_name_and_ver = BTreeMap::<PackageStr, BTreeMap<VetVersion, usize>>::new();
for (idx, package) in packages.iter().enumerate() {
let pkgid = if package.is_first_party {
format!(
"{} {} (path+file:///C:/FAKE/{})",
package.name, package.version, package.name
)
} else {
format!(
"{} {} (registry+https://github.com/rust-lang/crates.io-index)",
package.name, package.version
)
};
pkgids.push(pkgid);
let old = idx_by_name_and_ver
.entry(package.name)
.or_default()
.insert(package.version.clone(), idx);
assert!(
old.is_none(),
"duplicate version {} {}",
package.name,
package.version
);
}
Self {
packages,
pkgids,
idx_by_name_and_ver,
}
}
fn pkgid(&self, package: &MockPackage) -> &str {
self.pkgid_by(package.name, &package.version)
}
fn pkgid_by(&self, name: PackageStr, version: &VetVersion) -> &str {
&self.pkgids[self.idx_by_name_and_ver[name][version]]
}
fn package_by(&self, name: PackageStr, version: &VetVersion) -> &MockPackage {
&self.packages[self.idx_by_name_and_ver[name][version]]
}
fn source(&self, package: &MockPackage) -> Value {
if package.is_first_party {
json!(null)
} else {
json!("registry+https://github.com/rust-lang/crates.io-index")
}
}
fn metadata(&self) -> Metadata {
let meta_json = json!({
"packages": self.packages.iter().map(|package| json!({
"name": package.name,
"version": package.version.to_string(),
"id": self.pkgid(package),
"license": "MIT",
"license_file": null,
"description": "whatever",
"source": self.source(package),
"dependencies": package.deps.iter().chain(&package.dev_deps).chain(&package.build_deps).map(|dep| json!({
"name": dep.name,
"source": self.source(self.package_by(dep.name, &dep.version)),
"req": format!("={}", dep.version),
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
})).collect::<Vec<_>>(),
"targets": package.targets.iter().map(|target| json!({
"kind": [
target
],
"crate_types": [
target
],
"name": package.name,
"src_path": "C:\\Users\\fake_user\\.cargo\\registry\\src\\github.com-1ecc6299db9ec823\\DUMMY\\src\\lib.rs",
"edition": "2015",
"doc": true,
"doctest": true,
"test": true
})).collect::<Vec<_>>(),
"features": {},
"manifest_path": "C:\\Users\\fake_user\\.cargo\\registry\\src\\github.com-1ecc6299db9ec823\\DUMMY\\Cargo.toml",
"metadata": null,
"publish": null,
"authors": [],
"categories": [],
"keywords": [],
"readme": "README.md",
"repository": null,
"homepage": null,
"documentation": null,
"edition": "2015",
"links": null,
"default_run": null,
"rust_version": null
})).collect::<Vec<_>>(),
"workspace_members": self.packages.iter().filter_map(|package| {
if package.is_workspace {
Some(self.pkgid(package))
} else {
None
}
}).collect::<Vec<_>>(),
"resolve": {
"nodes": self.packages.iter().map(|package| {
let mut all_deps = BTreeMap::<(PackageStr, &VetVersion), Vec<Option<&str>>>::new();
for dep in &package.deps {
all_deps.entry((dep.name, &dep.version)).or_default().push(None);
}
for dep in &package.build_deps {
all_deps.entry((dep.name, &dep.version)).or_default().push(Some("build"));
}
for dep in &package.dev_deps {
all_deps.entry((dep.name, &dep.version)).or_default().push(Some("dev"));
}
json!({
"id": self.pkgid(package),
"dependencies": all_deps.keys().map(|(name, version)| self.pkgid_by(name, version)).collect::<Vec<_>>(),
"deps": all_deps.iter().map(|((name, version), kinds)| json!({
"name": name,
"pkg": self.pkgid_by(name, version),
"dep_kinds": kinds.iter().map(|kind| json!({
"kind": kind,
"target": null,
})).collect::<Vec<_>>(),
})).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
"root": null,
},
"target_directory": "C:\\FAKE\\target",
"version": 1,
"workspace_root": "C:\\FAKE\\",
"metadata": null,
});
serde_json::from_value(meta_json).unwrap()
}
}
fn init_files(
metadata: &Metadata,
criteria: impl IntoIterator<Item = (CriteriaName, CriteriaEntry)>,
default_criteria: &str,
) -> (ConfigFile, AuditsFile, ImportsFile) {
let mut config = ConfigFile {
cargo_vet: Default::default(),
default_criteria: default_criteria.to_owned(),
imports: Default::default(),
policy: Default::default(),
exemptions: Default::default(),
};
let audits = AuditsFile {
criteria: criteria.into_iter().collect(),
wildcard_audits: SortedMap::new(),
audits: SortedMap::new(),
trusted: SortedMap::new(),
};
let imports = ImportsFile {
unpublished: SortedMap::new(),
publisher: SortedMap::new(),
audits: SortedMap::new(),
};
// Make the root packages use our custom criteria instead of the builtins
if default_criteria != SAFE_TO_DEPLOY {
for pkgid in &metadata.workspace_members {
for package in &metadata.packages {
if package.id == *pkgid {
config.policy.insert(
package.name.clone(),
PackagePolicyEntry::Unversioned(PolicyEntry {
audit_as_crates_io: None,
criteria: Some(vec![default_criteria.to_string().into()]),
dev_criteria: Some(vec![default_criteria.to_string().into()]),
dependency_criteria: CriteriaMap::new(),
notes: None,
}),
);
}
}
}
}
// Use `update_store` to generate exemptions which would allow the tree to
// be mocked, then deconstruct the store again. Callers may want to
// initialize the store differently during their tests.
let mut store = Store::mock(config, audits, imports);
crate::resolver::update_store(&mock_cfg(metadata), &mut store, |_| {
crate::resolver::UpdateMode {
search_mode: crate::resolver::SearchMode::RegenerateExemptions,
prune_exemptions: true,
prune_imports: true,
}
});
(store.config, store.audits, store.imports)
}
fn files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
// Criteria hierarchy:
//
// * strong-reviewed
// * reviewed (default)
// * weak-reviewed
// * fuzzed
//
// This lets use mess around with "strong reqs", "weaker reqs", and "unrelated reqs"
// with "reviewed" as the implicit default everything cares about.
init_files(
metadata,
[
(
"strong-reviewed".to_string(),
criteria_implies("strongly reviewed", ["reviewed"]),
),
(
"reviewed".to_string(),
criteria_implies("reviewed", ["weak-reviewed"]),
),
("weak-reviewed".to_string(), criteria("weakly reviewed")),
("fuzzed".to_string(), criteria("fuzzed")),
],
DEFAULT_CRIT,
)
}
fn files_no_exemptions(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
let (mut config, audits, imports) = files_inited(metadata);
// Just clear all the exemptions out
config.exemptions.clear();
(config, audits, imports)
}
fn files_full_audited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
let (config, mut audits, imports) = files_no_exemptions(metadata);
let mut audited = SortedMap::<PackageName, Vec<AuditEntry>>::new();
for package in &metadata.packages {
if package.is_third_party(&config.policy) {
audited
.entry(package.name.clone())
.or_default()
.push(full_audit(package.vet_version(), DEFAULT_CRIT));
}
}
audits.audits = audited;
(config, audits, imports)
}
fn builtin_files_inited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
init_files(metadata, [], SAFE_TO_DEPLOY)
}
fn builtin_files_no_exemptions(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
let (mut config, audits, imports) = builtin_files_inited(metadata);
// Just clear all the exemptions out
config.exemptions.clear();
(config, audits, imports)
}
fn builtin_files_full_audited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
let (config, mut audits, imports) = builtin_files_no_exemptions(metadata);
let mut audited = SortedMap::<PackageName, Vec<AuditEntry>>::new();
for package in &metadata.packages {
if package.is_third_party(&config.policy) {
audited
.entry(package.name.clone())
.or_default()
.push(full_audit(package.vet_version(), SAFE_TO_DEPLOY));
}
}
audits.audits = audited;
(config, audits, imports)
}
fn builtin_files_minimal_audited(metadata: &Metadata) -> (ConfigFile, AuditsFile, ImportsFile) {
let (mut config, mut audits, imports) = builtin_files_inited(metadata);
let mut audited = SortedMap::<PackageName, Vec<AuditEntry>>::new();
for (name, entries) in std::mem::take(&mut config.exemptions) {
for entry in entries {
audited.entry(name.clone()).or_default().push(full_audit_m(
entry.version,
entry.criteria.iter().map(|s| &**s).collect::<Vec<_>>(),
));
}
}
audits.audits = audited;
(config, audits, imports)
}
/// Returns a fixed datetime that should be considered `now`: 2023-01-01 12:00 UTC.
fn mock_now() -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::from_utc(
chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
),
chrono::Utc,
)
}
/// Returns a fixed datetime that should be considered `today`: 2023-01-01.
///
/// This is derived from `mock_now()`.
fn mock_today() -> chrono::NaiveDate {
mock_now().date_naive()
}
fn mock_cfg(metadata: &Metadata) -> Config {
mock_cfg_args(metadata, ["cargo", "vet"])
}
fn mock_cfg_args<I, T>(metadata: &Metadata, itr: I) -> Config
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let crate::cli::FakeCli::Vet(cli) =
crate::cli::FakeCli::try_parse_from(itr).expect("Parsing arguments for mock_cfg failed!");
Config {
metacfg: MetaConfig(vec![]),
metadata: metadata.clone(),
_rest: PartialConfig {
cli,
now: mock_now(),
cache_dir: PathBuf::new(),
mock_cache: true,
},
}
}
fn get_reports(
metadata: &Metadata,
report: ResolveReport,
store: &Store,
network: Option<&Network>,
) -> (String, String) {
// FIXME: Figure out how to handle disabling output colours better in tests.
console::set_colors_enabled(false);
console::set_colors_enabled_stderr(false);
let cfg = mock_cfg(metadata);
let suggest = report.compute_suggest(&cfg, store, network).unwrap();
let human_output = BasicTestOutput::new();
report
.print_human(&human_output.clone().as_dyn(), &cfg, suggest.as_ref())
.unwrap();
let json_output = BasicTestOutput::new();
report
.print_json(&json_output.clone().as_dyn(), suggest.as_ref())
.unwrap();
(human_output.to_string(), json_output.to_string())
}
#[allow(clippy::type_complexity)]
struct BasicTestOutput {
output: Mutex<Vec<u8>>,
on_read_line: Option<Box<dyn Fn(&str) -> io::Result<String> + Send + Sync + 'static>>,
on_edit: Option<Box<dyn Fn(String) -> io::Result<String> + Send + Sync + 'static>>,
}
impl BasicTestOutput {
fn new() -> Arc<Self> {
Arc::new(BasicTestOutput {
output: Mutex::new(Vec::new()),
on_read_line: None,
on_edit: None,
})
}
fn with_callbacks(
on_read_line: impl Fn(&str) -> io::Result<String> + Send + Sync + 'static,
on_edit: impl Fn(String) -> io::Result<String> + Send + Sync + 'static,
) -> Arc<Self> {
Arc::new(BasicTestOutput {
output: Mutex::new(Vec::new()),
on_read_line: Some(Box::new(on_read_line)),
on_edit: Some(Box::new(on_edit)),
})
}
fn as_dyn(self: Arc<Self>) -> Arc<dyn Out> {
self
}
}
impl fmt::Display for BasicTestOutput {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::str::from_utf8(&self.output.lock().unwrap())
.unwrap()
.fmt(f)
}
}
impl Out for BasicTestOutput {
fn write(&self, buf: &[u8]) -> io::Result<usize> {
self.output.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn clear_screen(&self) -> io::Result<()> {
writeln!(self, "<<<CLEAR SCREEN>>>");
Ok(())
}
fn read_line_with_prompt(&self, initial: &str) -> io::Result<String> {
write!(self, "{initial}");
if let Some(on_read_line) = &self.on_read_line {
let response = on_read_line(initial)?;
writeln!(self, "{response}");
Ok(response)
} else {
Err(io::ErrorKind::Unsupported.into())
}
}
fn editor<'b>(&'b self, name: &'b str) -> io::Result<Editor<'b>> {
if let Some(on_edit) = &self.on_edit {
let mut editor = Editor::new(name)?;
editor.set_run_editor(move |path| {
let original = fs::read_to_string(path)?;
writeln!(self, "<<<EDITING {name}>>>\n{original}");
match on_edit(original) {
Ok(contents) => {
writeln!(self, "<<<EDIT OK>>>\n{contents}\n<<<END EDIT>>>");
fs::write(path, contents)?;
Ok(true)
}
Err(err) => {
writeln!(self, "<<<EDIT ERROR>>>");
Err(err)
}
}
});
Ok(editor)
} else {
panic!("Unexpected editor call without on_edit configured!");
}
}
}
/// Format a diff between the old and new strings for reporting.
fn generate_diff(old: &str, new: &str) -> String {
similar::utils::diff_lines(similar::Algorithm::Myers, old, new)
.into_iter()
.map(|(tag, line)| format!("{tag}{line}"))
.collect()
}
/// Generate a diff between two values returned from `Store::mock_commit`.
fn diff_store_commits(old: &SortedMap<String, String>, new: &SortedMap<String, String>) -> String {
use std::fmt::Write;
let mut result = String::new();
let keys = old.keys().chain(new.keys()).collect::<SortedSet<&String>>();
for key in keys {
let old = old.get(key).map(|s| &s[..]).unwrap_or("");
let new = new.get(key).map(|s| &s[..]).unwrap_or("");
if old == new {
writeln!(&mut result, "{key}: (unchanged)").unwrap();
continue;
}
let diff = generate_diff(old, new);
writeln!(&mut result, "{key}:\n{diff}").unwrap();
}
result
}
#[derive(Clone)]
struct MockRegistryVersion {
version: semver::Version,
published_by: Option<CratesUserId>,
created_at: chrono::DateTime<chrono::Utc>,
}
fn reg_published_by(
version: VetVersion,
published_by: Option<CratesUserId>,
when: &str,
) -> MockRegistryVersion {
assert!(
version.git_rev.is_none(),
"cannot publish a git version to registry"
);
MockRegistryVersion {
version: version.semver,
published_by,
created_at: chrono::DateTime::from_utc(
chrono::NaiveDateTime::new(
when.parse::<chrono::NaiveDate>().unwrap(),
chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
),
chrono::Utc,
),
}
}
struct MockRegistryPackage {
versions: Vec<MockRegistryVersion>,
metadata: CratesAPICrateMetadata,
}
#[derive(Default)]
struct MockRegistryBuilder {
users: FastMap<CratesUserId, CratesAPIUser>,
packages: FastMap<PackageName, MockRegistryPackage>,
}
impl MockRegistryBuilder {
fn new() -> Self {
Default::default()
}
fn user(&mut self, id: CratesUserId, login: &str, name: &str) -> &mut Self {
self.users.insert(
id,
CratesAPIUser {
id,
login: login.to_owned(),
name: Some(name.to_owned()),
},
);
self
}
fn package(&mut self, name: PackageStr<'_>, versions: &[MockRegistryVersion]) -> &mut Self {
self.package_m(
name,
CratesAPICrateMetadata {
description: None,
repository: None,
},
versions,
)
}
fn package_m(
&mut self,
name: PackageStr<'_>,
metadata: CratesAPICrateMetadata,
versions: &[MockRegistryVersion],
) -> &mut Self {
// To keep things simple, only handle the URL for 4+ characters in package names for now.
assert!(name.len() >= 4);
self.packages.insert(
name.to_owned(),
MockRegistryPackage {
metadata,
versions: versions.to_owned(),
},
);
self
}
fn serve(&self, network: &mut Network) {
for (name, pkg) in &self.packages {
// Serve the index entry as part of the http index.
network.mock_serve(
format!(
"https://index.crates.io/{}/{}/{name}",
&name[0..2],
&name[2..4]
).to_ascii_lowercase(),
pkg.versions
.iter()
.map(|v| {
serde_json::to_string(&json!({
"name": name,
"vers": &v.version,
"deps": [],
"cksum": "90527ab4abff2f0608cdb1a78e2349180e1d92059f59b5a65ce2a1a15a499b73",
"features": {},
"yanked": false
}))
.unwrap()
})
.collect::<Vec<_>>()
.join("\n"),
);
// Serve the crates.io API to match the http index and host extra metadata.
//
// NOTE: crates.io actually serves the API case-insensitively,
// unlike the http index, which is case-sensitive (and lowercase).
// Preserving case here matches how we currently construct the API
// url internally, but may need to be changed in the future.
network.mock_serve_json(
format!("https://crates.io/api/v1/crates/{name}"),
&CratesAPICrate {
crate_data: pkg.metadata.clone(),
versions: pkg
.versions
.iter()
.map(|v| CratesAPIVersion {
created_at: v.created_at,
num: v.version.clone(),
published_by: v.published_by.map(|id| {
let user = &self.users[&id];
CratesAPIUser {
id,
login: user.login.clone(),
name: user.name.clone(),
}
}),
})
.collect(),
},
)
}
}
}
// TESTING BACKLOG:
//
// * custom policies
// * basic
// * custom criteria to third-party
// * custom criteria to first-party
// * two first-parties depending on the same thing
// * which is itself first-party
// * which is a third-party
// * with different policies
// * where only the weaker one is satisfied (fail but give good diagnostic)
//
// * misc
// * git deps are first party but not in workspace
// * path deps are first party but not in workspace
// * multiple root packages
// * weird workspaces
// * running from weird directories
// * a node explicitly setting all its dependency_criteria to "no reqs"
// * ...should this just be an error? that feels wrong to do. otherwise:
// * with perfectly fine children
// * with children that fail to validate at all
//
// * malformed inputs:
// * no default criteria specified
// * referring to non-existent criteria
// * referring to non-existent crates (in crates.io? or just in our dep graph?)
// * referring to non-existent versions?
// * Bad delta syntax
// * Bad version syntax
// * entries in tomls that don't map to anything (at least warn to catch typos?)
// * might be running an old version of cargo-vet on a newer repo?