use std::collections::BTreeSet;
use std::path::Path;
type Pin = (String, String);
fn patched(manifest: &toml::Value) -> BTreeSet<Pin> {
let Some(table) = manifest
.get("patch")
.and_then(|patch| patch.get("crates-io"))
else {
return BTreeSet::new();
};
let table = table
.as_table()
.expect("[patch.crates-io] is a table of patched crates");
table
.iter()
.map(|(name, entry)| {
let git = entry
.get("git")
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("the patch of `{name}` is not a git source"));
let rev = entry
.get("rev")
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("the patch of `{name}` names no `rev`"));
(git.to_owned(), commit(rev, name))
})
.collect()
}
fn allowed(deny: &toml::Value) -> BTreeSet<Pin> {
let Some(list) = deny
.get("sources")
.and_then(|sources| sources.get("allow-git"))
else {
return BTreeSet::new();
};
list.as_array()
.expect("`allow-git` is a list of permitted git sources")
.iter()
.map(|entry| {
let url = entry
.as_str()
.unwrap_or_else(|| panic!("an `allow-git` entry is not a string: {entry}"));
let (repository, rev) = url
.split_once("?rev=")
.unwrap_or_else(|| panic!("the `allow-git` entry `{url}` pins no revision"));
(repository.to_owned(), commit(rev, url))
})
.collect()
}
fn commit(rev: &str, whose: &str) -> String {
assert!(
rev.len() >= 7 && rev.chars().all(|c| c.is_ascii_hexdigit()),
"`{whose}` names `{rev}`, which is not a commit"
);
rev.to_owned()
}
fn read(name: &str) -> toml::Value {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(name);
let text =
std::fs::read_to_string(&path).unwrap_or_else(|error| panic!("{name} reads: {error}"));
toml::from_str(&text).unwrap_or_else(|error| panic!("{name} parses: {error}"))
}
#[test]
fn the_audit_permits_exactly_the_revisions_the_manifest_patches() {
let patched = patched(&read("Cargo.toml"));
let allowed = allowed(&read("deny.toml"));
assert_eq!(
patched, allowed,
"`Cargo.toml`'s [patch.crates-io] links {patched:?} and `deny.toml`'s allow-git permits \
{allowed:?}; move both in one change, and delete both together"
);
}
#[test]
fn every_patched_member_of_one_repository_comes_from_one_revision() {
let pins = patched(&read("Cargo.toml"));
let repositories: BTreeSet<&String> = pins.iter().map(|(repository, _)| repository).collect();
assert_eq!(
pins.len(),
repositories.len(),
"[patch.crates-io] links one repository at more than one revision: {pins:?}"
);
}
#[test]
fn a_manifest_that_patches_nothing_wants_an_audit_that_permits_nothing() {
let manifest: toml::Value = toml::from_str("[dependencies]\nserde = \"1\"\n").expect("parses");
let deny: toml::Value = toml::from_str("[sources]\nallow-git = []\n").expect("parses");
assert!(patched(&manifest).is_empty());
assert!(allowed(&deny).is_empty());
}
#[test]
fn a_pin_is_read_out_of_each_files_own_spelling_of_it() {
let manifest: toml::Value = toml::from_str(
"[patch.crates-io]\n\
onevcs = { git = \"https://example.invalid/x\", rev = \"abc1234\" }\n\
onevcs-testing = { git = \"https://example.invalid/x\", rev = \"abc1234\" }\n\
\n[profile.release]\nrev = \"not-a-patch\"\n",
)
.expect("parses");
let deny: toml::Value =
toml::from_str("[sources]\nallow-git = [\"https://example.invalid/x?rev=abc1234\"]\n")
.expect("parses");
assert_eq!(patched(&manifest), allowed(&deny));
}
#[test]
#[should_panic(expected = "pins no revision")]
fn an_allow_git_entry_that_pins_no_revision_is_refused_rather_than_ignored() {
let deny: toml::Value =
toml::from_str("[sources]\nallow-git = [\"https://example.invalid/x\"]\n").expect("parses");
let _ = allowed(&deny);
}
#[test]
#[should_panic(expected = "is not a commit")]
fn a_revision_that_is_not_a_commit_is_refused_rather_than_compared() {
let manifest: toml::Value = toml::from_str(
"[patch.crates-io]\nonevcs = { git = \"https://example.invalid/x\", rev = \"\" }\n",
)
.expect("parses");
let _ = patched(&manifest);
}
#[test]
#[should_panic(expected = "names no `rev`")]
fn a_patch_entry_with_no_revision_is_refused_rather_than_ignored() {
let manifest: toml::Value = toml::from_str(
"[patch.crates-io]\nonevcs = { git = \"https://example.invalid/x\", branch = \"main\" }\n",
)
.expect("parses");
let _ = patched(&manifest);
}