use std::process::Command;
fn script() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("scripts/release-object-audit.sh")
}
const TARGETS: &[&str] = &[
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
];
const LINUX_TARGETS: &[&str] = &[
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
];
fn digest_for(name: &str) -> String {
let mut out = String::new();
let mut acc: u8 = 7;
for b in name.bytes() {
acc = acc.wrapping_mul(31).wrapping_add(b);
if out.len() < 64 {
out.push_str(&format!("{acc:02x}"));
}
}
while out.len() < 64 {
out.push('0');
}
out.truncate(64);
out
}
fn tarballs(version: &str, targets: &[&str]) -> Vec<String> {
targets
.iter()
.map(|t| format!("forjar-{version}-{t}.tar.gz"))
.collect()
}
struct Fixture {
dir: tempfile::TempDir,
}
impl Fixture {
fn new() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("sidecars")).expect("sidecars dir");
Fixture { dir }
}
fn assets(&self, names: &[String]) -> &Self {
std::fs::write(self.dir.path().join("assets.txt"), names.join("\n") + "\n")
.expect("assets.txt");
self
}
fn sums(&self, lines: &[(String, String)]) -> &Self {
let body: String = lines
.iter()
.map(|(d, n)| format!("{d} {n}\n"))
.collect::<Vec<_>>()
.join("");
std::fs::write(self.dir.path().join("SHA256SUMS"), body).expect("SHA256SUMS");
self
}
fn sidecar(&self, name: &str, digest: &str) -> &Self {
std::fs::write(
self.dir
.path()
.join("sidecars")
.join(format!("{name}.sha256")),
format!("{digest} {name}\n"),
)
.expect("sidecar");
self
}
fn check(&self, tag: &str) -> (bool, String) {
let p = self.dir.path();
let out = Command::new("bash")
.arg(script())
.arg("check")
.arg(tag)
.env("FORJAR_AUDIT_ASSETS_FILE", p.join("assets.txt"))
.env("FORJAR_AUDIT_SUMS_FILE", p.join("SHA256SUMS"))
.env("FORJAR_AUDIT_SIDECAR_DIR", p.join("sidecars"))
.output()
.expect("the auditor script must be runnable with bash");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(out.status.success(), text)
}
}
fn contaminated_v1_20_1() -> Fixture {
let own = tarballs("1.20.1", TARGETS);
let mut strays: Vec<String> = Vec::new();
for v in ["1.17.0", "1.18.0", "1.19.0", "1.20.0"] {
let targets = if v == "1.17.0" {
LINUX_TARGETS
} else {
TARGETS
};
strays.extend(tarballs(v, targets));
}
assert_eq!(strays.len(), 22, "the recorded stray count");
let mut assets: Vec<String> = strays.clone();
assets.extend(own.clone());
for t in LINUX_TARGETS {
assets.push(format!("forjar-1.20.1-{t}.tar.gz.sha256"));
}
assets.push("SHA256SUMS".to_string());
let sums: Vec<(String, String)> = own
.iter()
.chain(strays.iter())
.map(|n| (digest_for(n), n.clone()))
.collect();
assert_eq!(sums.len(), 28, "the recorded SHA256SUMS line count");
let f = Fixture::new();
f.assets(&assets).sums(&sums);
for t in LINUX_TARGETS {
let name = format!("forjar-1.20.1-{t}.tar.gz");
f.sidecar(&name, &digest_for(&name));
}
f.sidecar(
"forjar-1.20.1-x86_64-unknown-linux-gnu.tar.gz",
"e17903a9e87e8e562ec0055e51d28e76e3c8d707ced959b89a0789a308aee775",
);
f
}
fn clean_v1_18_0() -> Fixture {
let own = tarballs("1.18.0", TARGETS);
let mut assets = own.clone();
assets.extend(own.iter().map(|n| format!("{n}.sha256")));
assets.push("SHA256SUMS".to_string());
let sums: Vec<(String, String)> = own.iter().map(|n| (digest_for(n), n.clone())).collect();
let f = Fixture::new();
f.assets(&assets).sums(&sums);
for n in &own {
f.sidecar(n, &digest_for(n));
}
f
}
#[test]
fn contaminated_release_object_is_rejected() {
let (ok, out) = contaminated_v1_20_1().check("v1.20.1");
assert!(
!ok,
"the auditor accepted a release carrying 22 assets from four other \
versions (#325). Output:\n{out}"
);
for expected in [
"stray: forjar-1.17.0-x86_64-unknown-linux-gnu.tar.gz",
"SHA256SUMS describes 28 archives, release has 6",
"sums-extra: forjar-1.20.0-x86_64-apple-darwin.tar.gz",
"sidecar-missing: forjar-1.20.1-x86_64-apple-darwin.tar.gz.sha256",
"sidecar-disagrees: forjar-1.20.1-x86_64-unknown-linux-gnu.tar.gz",
] {
assert!(
out.contains(expected),
"the auditor never reported {expected:?}. Output:\n{out}"
);
}
}
#[test]
fn every_violation_is_reported_not_just_the_first() {
let (_, out) = contaminated_v1_20_1().check("v1.20.1");
let strays = out.lines().filter(|l| l.starts_with("stray: ")).count();
assert_eq!(
strays, 22,
"expected all 22 strays named, got {strays}. Output:\n{out}"
);
}
#[test]
fn clean_release_object_passes() {
let (ok, out) = clean_v1_18_0().check("v1.18.0");
assert!(ok, "the auditor rejected a clean release. Output:\n{out}");
let violations: Vec<&str> = out
.lines()
.filter(|l| {
l.starts_with("stray:")
|| l.starts_with("sums-")
|| l.starts_with("sidecar-")
|| l.starts_with("no-archives:")
})
.collect();
assert!(
violations.is_empty(),
"clean release reported violations {violations:?}. Output:\n{out}"
);
}
#[test]
fn an_empty_release_object_does_not_pass_vacuously() {
let f = Fixture::new();
f.assets(&["SHA256SUMS".to_string()]).sums(&[]);
let (ok, out) = f.check("v9.9.9");
assert!(!ok, "an assetless release passed the audit. Output:\n{out}");
assert!(
out.contains("no-archives:"),
"the auditor did not say the release carries no archives. Output:\n{out}"
);
}