use std::process::Command;
fn packslip(cwd: &std::path::Path, args: &[&str]) -> (i32, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_packslip"))
.current_dir(cwd)
.args(args)
.output()
.unwrap();
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[test]
fn usage_spec_is_emitted_but_hidden_from_help() {
let dir = tempfile::tempdir().unwrap();
let (code, spec, err) = packslip(dir.path(), &["usage"]);
assert_eq!(code, 0, "{err}");
assert!(spec.starts_with("// @generated by `packslip usage`\n"));
assert!(spec.contains("name packslip"), "{spec}");
assert!(spec.contains("cmd create"), "{spec}");
let (code, help, err) = packslip(dir.path(), &["--help"]);
assert_eq!(code, 0, "{err}");
assert!(!help.contains("packslip usage"), "{help}");
}
#[test]
fn keygen_create_verify_show_and_list() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let (code, out, err) = packslip(d, &["keygen", "-o", "release.key"]);
assert_eq!(code, 0, "{err}");
assert!(out.contains("wrote release.key and release.pub"), "{out}");
let (code, _, err) = packslip(d, &["keygen", "-o", "release.key"]);
assert_ne!(code, 0, "never overwrite a key");
assert!(err.contains("not overwriting"), "{err}");
std::fs::write(d.join("occupied.pub"), "keep").unwrap();
let (code, _, _err) = packslip(d, &["keygen", "-o", "occupied.key"]);
assert_ne!(code, 0);
assert_eq!(
std::fs::read_to_string(d.join("occupied.pub")).unwrap(),
"keep"
);
assert!(!d.join("occupied.key").exists());
let (code, _, err) = packslip(d, &["keygen", "-o", "same.pub"]);
assert_ne!(code, 0);
assert!(err.contains("both resolve"), "{err}");
std::fs::write(d.join("tool-v1.2.3-linux-x64.tar.xz"), b"linux").unwrap();
std::fs::write(d.join("tool-v1.2.3-darwin-arm64.tar.xz"), b"mac").unwrap();
std::fs::write(d.join("weird.bin"), b"?").unwrap();
let (code, out, err) = packslip(
d,
&[
"create",
"--project",
"tool.example.com",
"--version",
"1.2.3",
"--key",
"release.key",
"--no-log",
"--out",
"dist",
"--url-base",
"https://dl.example.com/tool/1.2.3",
"--source-repo",
"https://github.com/example/tool",
"--tag",
"v1.2.3",
"--published-at",
"2026-09-01T00:00:00Z",
"--bin",
"tool",
"--format",
"weird.bin=raw",
"tool-v1.2.3-linux-x64.tar.xz",
"tool-v1.2.3-darwin-arm64.tar.xz",
"weird.bin:freebsd/riscv64",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("3 artifact(s), signed by "), "{out}");
assert!(out.contains(", unlogged)"), "{out}");
assert!(d.join("dist/packslip.sigstore.json").exists());
assert!(!d.join("dist/packslip.json").exists(), "one file");
let (code, out, err) = packslip(d, &["show", "dist/packslip.sigstore.json"]);
assert_eq!(code, 0, "{err}");
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(doc["predicateType"], "https://packslip.dev/release/v1");
assert_eq!(doc["predicate"]["artifacts"][0]["os"], "linux");
assert_eq!(doc["predicate"]["artifacts"][0]["arch"], "x86_64");
assert_eq!(doc["predicate"]["artifacts"][0]["bin"][0], "tool");
assert_eq!(
doc["predicate"]["artifacts"][0]["url"],
"https://dl.example.com/tool/1.2.3/tool-v1.2.3-linux-x64.tar.xz"
);
assert_eq!(doc["predicate"]["artifacts"][2]["os"], "freebsd");
assert_eq!(doc["predicate"]["artifacts"][2]["arch"], "riscv64");
assert_eq!(doc["predicate"]["identity"]["scheme"], "sigstore-key");
assert_eq!(doc["predicate"]["source"]["tag"], "v1.2.3");
let (code, raw, _) = packslip(d, &["show", "--raw", "dist/packslip.sigstore.json"]);
assert_eq!(code, 0);
assert!(
raw.starts_with("{\"_type\":\"https://in-toto.io/Statement/v1\""),
"{raw}"
);
let (code, _, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"release.pub",
],
);
assert_eq!(code, 1);
assert!(err.contains("no transparency log entry"), "{err}");
let (code, out, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"release.pub",
"--allow-unlogged",
"--artifact",
"tool-v1.2.3-linux-x64.tar.xz",
"--artifact",
"weird.bin",
],
);
assert_eq!(code, 0, "{err}");
assert!(
out.starts_with("ok: tool.example.com 1.2.3 published 2026-09-01T00:00:00Z signed by "),
"{out}"
);
assert!(out.contains("(sigstore-key) unlogged"), "{out}");
assert!(out.contains("(2 of 3 artifact(s) checked)"), "{out}");
let (_, out, _) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"release.pub",
"--allow-unlogged",
"--json",
],
);
let json: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(json["scheme"], "sigstore-key");
assert_eq!(json["provenance_linked"], false);
assert_eq!(json["artifact_count"], 3);
let (code, _, err) = packslip(
d,
&["verify", "dist/packslip.sigstore.json", "--allow-unlogged"],
);
assert_ne!(code, 0);
assert!(err.contains("no identity to verify against"), "{err}");
std::fs::write(d.join("tool-v1.2.3-linux-x64.tar.xz"), b"linux!").unwrap();
let (code, _, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"release.pub",
"--allow-unlogged",
"--artifact",
"tool-v1.2.3-linux-x64.tar.xz",
],
);
assert_eq!(code, 1);
assert!(err.contains("sha256 is"), "{err}");
packslip(d, &["keygen", "-o", "other.key"]);
let (code, _, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"other.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 1);
assert!(err.contains("does not verify with the pinned key"), "{err}");
let (code, out, err) = packslip(
d,
&[
"releases",
"--project",
"tool.example.com",
"--sequence",
"4",
"--valid-for",
"2w",
"--generated-at",
"2026-09-02T00:00:00Z",
"--release",
"https://dl.example.com/tool/1.2.3/packslip.sigstore.json=dist/packslip.sigstore.json",
"--key",
"release.key",
"--no-log",
"--out",
"site/.well-known/packslip.json",
],
);
assert_eq!(code, 0, "{err}");
assert!(
out.contains("1 release(s), sequence 4, expires 2026-09-16T00:00:00Z"),
"{out}"
);
let (code, out, err) = packslip(
d,
&[
"verify",
"site/.well-known/packslip.json",
"--pubkey",
"release.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 0, "{err}");
assert!(
out.starts_with(
"ok: release list for tool.example.com sequence 4 expires 2026-09-16T00:00:00Z signed by "
),
"{out}"
);
let (code, _, err) = packslip(
d,
&[
"releases",
"--project",
"other.example.com",
"--sequence",
"1",
"--release",
"https://x/packslip.sigstore.json=dist/packslip.sigstore.json",
"--key",
"release.key",
"--no-log",
],
);
assert_ne!(code, 0);
assert!(err.contains("the list is for"), "{err}");
}
#[test]
fn source_revision_requires_a_repository() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let (code, _, err) = packslip(d, &["keygen", "-o", "release.key"]);
assert_eq!(code, 0, "{err}");
std::fs::write(d.join("tool.tar.xz"), b"tool").unwrap();
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"tool.example.com",
"--version",
"1",
"--key",
"release.key",
"--no-log",
"--commit",
"abc123",
"tool.tar.xz",
],
);
assert_ne!(code, 0);
assert!(err.contains("require --source-repo"), "{err}");
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"tool.example.com",
"--version",
"1",
"--no-log",
"tool.tar.xz",
],
);
assert_ne!(code, 0);
assert!(err.contains("--no-log is for --key"), "{err}");
}
#[test]
fn schemas_are_json() {
let dir = tempfile::tempdir().unwrap();
let (code, out, _) = packslip(dir.path(), &["schema"]);
assert_eq!(code, 0);
let schema: serde_json::Value = serde_json::from_str(&out).unwrap();
assert!(schema["properties"]["predicate"].is_object(), "{out}");
let extensions = &schema["$defs"]["Predicate"]["properties"]["extensions"];
assert_eq!(extensions["type"], "object", "{extensions}");
assert!(
extensions
.get("additionalProperties")
.is_none_or(|p| p != false),
"{extensions}"
);
let (code, out, _) = packslip(dir.path(), &["schema", "--releases"]);
assert_eq!(code, 0);
let schema: serde_json::Value = serde_json::from_str(&out).unwrap();
assert!(
out.contains("sequence") && schema["properties"]["predicate"].is_object(),
"{out}"
);
}
#[test]
fn variants_urls_evidence_and_monorepo_names() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let (code, _, err) = packslip(d, &["keygen", "-o", "k.key"]);
assert_eq!(code, 0, "{err}");
std::fs::write(d.join("oxlint-linux-x64.tar.gz"), b"a").unwrap();
std::fs::write(d.join("oxlint-fips-linux-x64.tar.gz"), b"b").unwrap();
std::fs::write(d.join("oxlint-linux-arm64"), b"c").unwrap();
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"github.com/oxc-project/oxc/oxlint",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"oxlint-linux-x64.tar.gz",
"oxlint-fips-linux-x64.tar.gz",
],
);
assert_ne!(code, 0);
assert!(err.contains("give one a variant"), "{err}");
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"github.com/oxc-project/oxc/oxlint",
"--version",
"1.0",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"--bin",
"oxlint",
"oxlint-linux-x64.tar.gz",
],
);
assert_ne!(code, 0);
assert!(err.contains("semver"), "{err}");
std::fs::write(
d.join("release.toml"),
"[[artifact]]\npath = \"oxlint-linux-arm64\"\nbin = [\"oxlint\"]\n",
)
.unwrap();
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"github.com/oxc-project/oxc/oxlint",
"--version",
"1.0.0-beta.1",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"--bin",
"oxlint=bin/oxlint-x86_64",
"oxlint-linux-x64.tar.gz",
"oxlint-linux-arm64",
],
);
assert_ne!(code, 0);
assert!(err.contains("bare executable"), "{err}");
let (code, out, err) = packslip(
d,
&[
"create",
"--project",
"github.com/oxc-project/oxc/oxlint",
"--version",
"1.0.0-beta.1",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"--manifest",
"release.toml",
"--url-base",
"https://github.com/oxc-project/oxc/releases/download/oxlint_v1.0.0-beta.1",
"--url",
"oxlint-linux-arm64=https://cdn.example.com/oxlint-linux-arm64",
"--bin",
"oxlint=bin/oxlint-x86_64",
"--notes-url",
"https://github.com/oxc-project/oxc/releases/tag/oxlint_v1.0.0-beta.1",
"--source-repo",
"https://github.com/oxc-project/oxc",
"--tag",
"oxlint_v1.0.0-beta.1",
"--published-at",
"2026-09-01T00:00:00Z",
"--extension",
r#"example.com={"build_id":"20260901.3"}"#,
"--extension",
"mise=true",
"--provenance",
"oxlint-linux-arm64=https://api.example.com/attestations/sha256:abc",
"oxlint-linux-x64.tar.gz",
"oxlint-fips-linux-x64.tar.gz@fips",
"oxlint-linux-arm64",
],
);
assert_eq!(code, 0, "{err}");
assert!(!err.contains("warning"), "{err}");
assert!(
out.contains("wrote dist/packslip.oxlint.sigstore.json (3 artifact(s)"),
"{out}"
);
for bad in ["nojson", "=1", "mise=not json", "mise={}", "mise=1"] {
let args = [
"create",
"--project",
"github.com/oxc-project/oxc/oxlint",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"bad",
"--extension",
"mise={}",
"--extension",
bad,
"oxlint-linux-x64.tar.gz",
];
let (code, _, err) = packslip(d, &args);
assert_ne!(code, 0, "{bad}");
assert!(err.contains("--extension"), "{bad}: {err}");
}
let (code, out, err) = packslip(d, &["show", "dist/packslip.oxlint.sigstore.json"]);
assert_eq!(code, 0, "{err}");
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
let arts = &doc["predicate"]["artifacts"];
assert_eq!(arts[0]["name"], "oxlint-linux-arm64");
assert_eq!(arts[0]["format"], "raw");
assert_eq!(arts[0]["url"], "https://cdn.example.com/oxlint-linux-arm64");
assert_eq!(arts[0]["bin"][0]["path"], "oxlint-linux-arm64");
assert_eq!(arts[0]["bin"][0]["name"], "oxlint");
assert_eq!(
arts[0]["provenance"][0],
"https://api.example.com/attestations/sha256:abc"
);
assert_eq!(arts[1]["bin"][0]["name"], "oxlint");
assert_eq!(arts[1]["bin"][0]["path"], "bin/oxlint-x86_64");
assert!(arts[1].get("provenance").is_none());
assert_eq!(arts[2]["variant"], "fips");
assert_eq!(doc["predicate"]["version"], "1.0.0-beta.1");
assert!(doc["predicate"].get("prerelease").is_none());
assert!(doc["predicate"].get("channel").is_none());
assert_eq!(doc["predicate"]["source"]["tag"], "oxlint_v1.0.0-beta.1");
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"github.com/oxc-project/oxc/oxlint",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"warned",
"--source-repo",
"https://github.com/oxc-project/oxc",
"--tag",
"oxlint_v1.0.1",
"oxlint-linux-x64.tar.gz",
],
);
assert_eq!(code, 0, "{err}");
assert!(err.contains("does not name version 1.0.0"), "{err}");
assert_eq!(
doc["predicate"]["extensions"]["example.com"]["build_id"],
"20260901.3"
);
assert_eq!(doc["predicate"]["extensions"]["mise"], true);
assert_eq!(
doc["subject"][0]["digest"]["sha512"].as_str().map(str::len),
Some(128)
);
assert!(doc["predicate"].get("attested_by").is_none());
let (code, out, _) = packslip(d, &["show", "repack/packslip.sigstore.json"]);
let _ = (code, out);
let (code, out, err) = packslip(
d,
&[
"verify",
"dist/packslip.oxlint.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 0, "{err}");
assert!(
out.starts_with(
"ok: github.com/oxc-project/oxc/oxlint 1.0.0-beta.1 (beta prerelease) published"
),
"{out}"
);
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"packages.example.com/tool",
"--version",
"2.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"repack",
"--evidence",
"apt-release-gpg=3FEF9748",
"oxlint-linux-x64.tar.gz",
],
);
assert_ne!(code, 0, "evidence needs --attested-by repackager");
assert!(err.contains("--attested-by repackager"), "{err}");
let (code, out, err) = packslip(
d,
&[
"create",
"--project",
"packages.example.com/tool",
"--version",
"2.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"repack",
"--attested-by",
"repackager",
"--evidence",
"apt-release-gpg=3FEF9748",
"--evidence",
"pkgbuild-checksums",
"--no-sha512",
"oxlint-linux-x64.tar.gz",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("repackager-attested"), "{out}");
let (code, out, _) = packslip(d, &["show", "repack/packslip.sigstore.json"]);
assert_eq!(code, 0);
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(doc["predicate"]["attested_by"], "repackager");
assert_eq!(doc["predicate"]["evidence"][0]["kind"], "apt-release-gpg");
assert_eq!(doc["predicate"]["evidence"][0]["detail"], "3FEF9748");
assert!(doc["subject"][0]["digest"].get("sha512").is_none());
let (code, out, err) = packslip(
d,
&[
"verify",
"repack/packslip.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("repackager-attested"), "{out}");
let (code, out, err) = packslip(
d,
&[
"releases",
"--project",
"packages.example.com/tool",
"--latest",
"2.0.0",
"--sequence",
"1",
"--release",
"https://packages.example.com/2.0/packslip.sigstore.json=repack/packslip.sigstore.json",
"--yank",
"https://packages.example.com/2.0/packslip.sigstore.json=regression",
"--security",
"https://packages.example.com/2.0/packslip.sigstore.json",
"--key",
"k.key",
"--no-log",
"--out",
"list.sigstore.json",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("1 release(s), sequence 1"), "{out}");
let (code, out, err) = packslip(
d,
&[
"verify",
"list.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("listing 1 release(s), 1 yanked"), "{out}");
let (code, out, _) = packslip(d, &["show", "list.sigstore.json"]);
assert_eq!(code, 0);
let list: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(list["predicate"]["latest"], "2.0.0");
assert_eq!(list["predicate"]["releases"][0]["status"], "yanked");
assert_eq!(
list["predicate"]["releases"][0]["status_reason"],
"regression"
);
assert_eq!(list["predicate"]["releases"][0]["security"], true);
let (code, _, err) = packslip(
d,
&[
"releases",
"--project",
"packages.example.com/tool",
"--sequence",
"2",
"--release",
"https://x/a=repack/packslip.sigstore.json",
"--yank",
"https://x/other=why",
"--key",
"k.key",
"--no-log",
],
);
assert_ne!(code, 0);
assert!(err.contains("not among the --release entries"), "{err}");
let (code, _, err) = packslip(
d,
&[
"releases",
"--project",
"packages.example.com/tool",
"--sequence",
"2",
"--release",
"https://x/a=repack/packslip.sigstore.json",
"--evidence",
"https://x/a=scan=https://scans.example.com/1",
"--evidence",
"https://x/a=provenance-verified",
"--key",
"k.key",
"--no-log",
"--out",
"scanned.sigstore.json",
],
);
assert_eq!(code, 0, "{err}");
let (_, out, _) = packslip(d, &["show", "scanned.sigstore.json"]);
let list: serde_json::Value = serde_json::from_str(&out).unwrap();
let evidence = &list["predicate"]["releases"][0]["evidence"];
assert_eq!(evidence[0]["kind"], "scan");
assert_eq!(evidence[0]["detail"], "https://scans.example.com/1");
assert_eq!(evidence[1]["kind"], "provenance-verified");
}
#[test]
fn manifests_and_real_archives() {
use std::io::Write as _;
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let (code, _, err) = packslip(d, &["keygen", "-o", "k.key"]);
assert_eq!(code, 0, "{err}");
let tar_gz = |entries: &[(&str, &[u8])]| {
let mut builder = tar::Builder::new(Vec::new());
for (name, data) in entries {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o755);
header.set_cksum();
builder.append_data(&mut header, name, *data).unwrap();
}
let tar = builder.into_inner().unwrap();
let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
gz.write_all(&tar).unwrap();
gz.finish().unwrap()
};
std::fs::create_dir_all(d.join("dist")).unwrap();
std::fs::write(
d.join("dist/tool-1.2.3-linux-x64.tar.gz"),
tar_gz(&[
("tool-1.2.3-linux-x64/tool", b"bin"),
("tool-1.2.3-linux-x64/share/man/man1/tool.1", b"man"),
]),
)
.unwrap();
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
zip.start_file("tool.exe", zip::write::SimpleFileOptions::default())
.unwrap();
zip.write_all(b"exe").unwrap();
std::fs::write(
d.join("dist/tool-1.2.3-windows-x64.zip"),
zip.finish().unwrap().into_inner(),
)
.unwrap();
std::fs::write(d.join("dist/tool-1.2.3-windows-arm64.exe"), b"bare").unwrap();
std::fs::write(d.join("dist/tool-1.2.3-setup.exe"), b"installer").unwrap();
std::fs::write(d.join("dist/tool.cdx.json"), b"{}").unwrap();
std::fs::write(
d.join("release.toml"),
r#"
project = "github.com/example/tool"
version = "1.2.3"
url_base = "https://github.com/example/tool/releases/download/v1.2.3"
bin = ["tool"]
requires = { glibc_min = "2.31" }
[source]
repo = "https://github.com/example/tool"
tag = "v1.2.3"
[extensions."example.com"]
build_id = "20260901.3"
[extensions.mise]
postinstall = "from the manifest"
[[artifact]]
path = "dist/tool-1.2.3-setup.exe"
bin = []
requires = { os_min = "10.0.17763" }
[[resource]]
kind = "man"
archive = "tool-1.2.3-linux-x64/share/man/man1/tool.1"
os = "linux"
[[resource]]
kind = "sbom"
format = "cyclonedx"
asset = "dist/tool.cdx.json"
"#,
)
.unwrap();
let (code, out, err) = packslip(
d,
&[
"create",
"--manifest",
"release.toml",
"--key",
"k.key",
"--no-log",
"--out",
"out",
"--published-at",
"2026-09-01T00:00:00Z",
"--extension",
r#"mise={"postinstall":"from the flag"}"#,
"dist/tool-1.2.3-linux-x64.tar.gz",
"dist/tool-1.2.3-windows-x64.zip",
"dist/tool-1.2.3-windows-arm64.exe",
"dist/tool-1.2.3-setup.exe",
],
);
assert_eq!(code, 0, "{err}");
assert!(
out.contains("(4 artifact(s), 2 resource(s), signed by"),
"{out}"
);
let (code, out, err) = packslip(d, &["show", "out/packslip.sigstore.json"]);
assert_eq!(code, 0, "{err}");
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(doc["predicate"]["project"], "github.com/example/tool");
assert_eq!(doc["predicate"]["version"], "1.2.3");
assert_eq!(
doc["predicate"]["extensions"]["example.com"]["build_id"],
"20260901.3"
);
assert_eq!(
doc["predicate"]["extensions"]["mise"]["postinstall"],
"from the flag"
);
let by_name = |name: &str| {
doc["predicate"]["artifacts"]
.as_array()
.unwrap()
.iter()
.find(|a| a["name"] == name)
.unwrap()
.clone()
};
let setup = by_name("tool-1.2.3-setup.exe");
assert_eq!(setup["format"], "exe");
assert!(setup.get("bin").is_none());
assert_eq!(setup["requires"]["os_min"], "10.0.17763");
let linux = by_name("tool-1.2.3-linux-x64.tar.gz");
assert_eq!(linux["bin"][0], "tool-1.2.3-linux-x64/tool");
assert_eq!(linux["requires"]["glibc_min"], "2.31");
assert_eq!(
linux["url"],
"https://github.com/example/tool/releases/download/v1.2.3/tool-1.2.3-linux-x64.tar.gz"
);
let windows_zip = by_name("tool-1.2.3-windows-x64.zip");
assert_eq!(windows_zip["bin"][0], "tool.exe");
let bare = by_name("tool-1.2.3-windows-arm64.exe");
assert_eq!(bare["format"], "raw");
assert_eq!(bare["bin"][0]["path"], "tool-1.2.3-windows-arm64.exe");
assert_eq!(bare["bin"][0]["name"], "tool", "the command as typed");
let res = &doc["predicate"]["resources"];
assert_eq!(res[0]["kind"], "man");
assert_eq!(res[0]["os"], "linux");
assert_eq!(res[1]["kind"], "sbom");
assert_eq!(res[1]["format"], "cyclonedx");
assert_eq!(res[1]["asset"], "tool.cdx.json");
assert_eq!(
res[1]["url"],
"https://github.com/example/tool/releases/download/v1.2.3/tool.cdx.json"
);
assert!(doc["predicate"].get("sbom").is_none());
let (code, out, err) = packslip(
d,
&[
"verify",
"out/packslip.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
"--artifact",
"dist/tool.cdx.json",
"--artifact",
"dist/tool-1.2.3-linux-x64.tar.gz",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("2 of 4 artifact(s) checked"), "{out}");
assert!(out.contains("2 resource(s)"), "{out}");
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"github.com/example/tool",
"--version",
"1.2.3",
"--key",
"k.key",
"--no-log",
"--bin",
"other",
"dist/tool-1.2.3-linux-x64.tar.gz",
],
);
assert_ne!(code, 0);
assert!(err.contains("no file named \"other\""), "{err}");
std::fs::write(d.join("bare.toml"), "bin = [\"tool\"]\n").unwrap();
let (code, _, err) = packslip(
d,
&[
"create",
"--manifest",
"bare.toml",
"--key",
"k.key",
"--no-log",
"dist/tool-1.2.3-linux-x64.tar.gz",
],
);
assert_ne!(code, 0);
assert!(err.contains("--project is required"), "{err}");
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"github.com/example/tool",
"--version",
"1.2.3",
"--key",
"k.key",
"--no-log",
],
);
assert_ne!(code, 0);
assert!(err.contains("no artifacts"), "{err}");
}
#[test]
fn resources_and_assets() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let (code, _, err) = packslip(d, &["keygen", "-o", "k.key"]);
assert_eq!(code, 0, "{err}");
std::fs::write(d.join("tool-linux-x64.tar.gz"), b"a").unwrap();
std::fs::write(d.join("tool-windows-x64.zip"), b"w").unwrap();
std::fs::write(d.join("tool-skill.tar.gz"), b"s").unwrap();
let create = |extra: &[&str]| {
let mut args = vec![
"create",
"--project",
"tool.example.com",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"--url-base",
"https://dl.example.com/1.0.0",
"--source-repo",
"https://github.com/example/tool",
"--commit",
"0123456789abcdef0123456789abcdef01234567",
"--bin",
"tool",
];
args.extend_from_slice(extra);
args.extend_from_slice(&["tool-linux-x64.tar.gz", "tool-windows-x64.zip"]);
packslip(d, &args)
};
let (code, out, err) = create(&[
"--resource",
"completion/zsh=archive:share/zsh/site-functions/_tool",
"--resource",
"completion/bash,zsh,fish=exec:tool completion {shell}",
"--resource",
"man=repo:man/tool.1",
"--resource",
"cli-spec/usage=exec:tool usage",
"--resource",
"skill/tool=asset:tool-skill.tar.gz",
"--resource",
"desktop=archive:share/applications/tool.desktop",
"--url",
"tool-skill.tar.gz=https://cdn.example.com/tool-skill.tar.gz",
]);
assert_eq!(code, 0, "{err}");
assert!(
out.contains("(2 artifact(s), 6 resource(s), signed by"),
"{out}"
);
let (code, out, err) = packslip(d, &["show", "dist/packslip.sigstore.json"]);
assert_eq!(code, 0, "{err}");
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(doc["subject"][2]["name"], "tool-skill.tar.gz");
let res = &doc["predicate"]["resources"];
assert_eq!(res[0]["shell"], "zsh");
assert_eq!(res[1]["shells"][2], "fish");
assert_eq!(res[1]["exec"][2], "{shell}");
assert_eq!(res[2]["repo"], "man/tool.1");
assert_eq!(res[3]["bin"], "tool");
assert_eq!(res[3]["format"], "usage");
assert_eq!(res[4]["asset"], "tool-skill.tar.gz");
assert_eq!(res[4]["url"], "https://cdn.example.com/tool-skill.tar.gz");
assert_eq!(res[5]["archive"], "share/applications/tool.desktop");
let (code, out, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
"--artifact",
"tool-skill.tar.gz",
"--json",
],
);
assert_eq!(code, 0, "{err}");
let verified: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(verified["checked_artifacts"][0], "tool-skill.tar.gz");
assert_eq!(verified["resources"][4], "skill/tool (asset)");
let (code, out, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("6 resource(s))"), "{out}");
let (code, out, err) = packslip(
d,
&[
"create",
"--project",
"tool.example.com",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"swept",
"--bin",
"tool",
"--resource",
"skill/tool=asset:tool-skill.tar.gz",
"tool-linux-x64.tar.gz",
"tool-skill.tar.gz",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("(1 artifact(s), 1 resource(s)"), "{out}");
let (code, _, err) = create(&["--resource", "man=exec:other man"]);
assert_ne!(code, 0);
assert!(err.contains("not a bin name"), "{err}");
let (code, _, err) = create(&["--resource", "cli-spec/usage/other=repo:x.kdl"]);
assert_ne!(code, 0);
assert!(err.contains("not a bin name"), "{err}");
let (code, _, err) = create(&["--resource", "completion=archive:x"]);
assert_ne!(code, 0);
assert!(err.contains("completion/SHELL"), "{err}");
let (code, _, err) = create(&["--resource", "man=archive:../x"]);
assert_ne!(code, 0);
assert!(err.contains("relative"), "{err}");
}
type TarMember<'a> = (&'a str, Option<&'a [u8]>, Option<&'a str>);
fn tar_gz(path: &std::path::Path, members: &[TarMember<'_>]) {
let file = std::fs::File::create(path).unwrap();
let gz = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
let mut tar = tar::Builder::new(gz);
for (name, bytes, link) in members {
let mut header = tar::Header::new_gnu();
match (bytes, link) {
(Some(bytes), _) => {
header.set_size(bytes.len() as u64);
header.set_mode(0o755);
header.set_cksum();
tar.append_data(&mut header, name, *bytes).unwrap();
}
(None, Some(target)) => {
let (kind, target) = match target.strip_prefix("hard:") {
Some(target) => (tar::EntryType::Link, target),
None => (tar::EntryType::Symlink, *target),
};
header.set_entry_type(kind);
header.set_size(0);
header.set_mode(0o777);
header.set_cksum();
tar.append_link(&mut header, name, target).unwrap();
}
(None, None) => unreachable!(),
}
}
tar.into_inner().unwrap().finish().unwrap();
}
fn zip_file(path: &std::path::Path, members: &[(&str, &[u8])]) {
use std::io::Write as _;
let file = std::fs::File::create(path).unwrap();
let mut zip = zip::ZipWriter::new(file);
for (name, bytes) in members {
zip.start_file(*name, zip::write::SimpleFileOptions::default())
.unwrap();
zip.write_all(bytes).unwrap();
}
zip.finish().unwrap();
}
#[test]
fn host_requirements_are_read_and_declared() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let (code, _, err) = packslip(d, &["keygen", "-o", "k.key"]);
assert_eq!(code, 0, "{err}");
let elf = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/needs-z"
))
.unwrap();
tar_gz(
&d.join("tool-linux-x64.tar.gz"),
&[
("tool-1.0/bin/tool", None, Some("../libexec/tool-real")),
("tool-1.0/libexec/tool-real", Some(&elf), None),
],
);
tar_gz(
&d.join("tool-linux-arm64.tar.gz"),
&[
("./tool-1.0/bin/tool", Some(&elf), None),
("./tool-1.0/lib/libz.so.1", Some(b"not really"), None),
],
);
tar_gz(
&d.join("tool-freebsd-x64.tar.gz"),
&[
("tool-1.0/libexec/tool-real", Some(&elf), None),
(
"tool-1.0/bin/tool",
None,
Some("hard:tool-1.0/libexec/tool-real"),
),
],
);
tar_gz(
&d.join("tool-darwin-arm64.tar.gz"),
&[(
"tool-1.0/bin/tool",
Some(b"#!/bin/sh\nexec java -jar tool.jar\n"),
None,
)],
);
zip_file(
&d.join("tool-windows-x64.zip"),
&[("tool-1.0/bin/tool.exe", &elf)],
);
std::fs::write(d.join("tool-linux-riscv64"), &elf).unwrap();
let base = [
"create",
"--project",
"tool.example.com",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"--bin",
"tool",
];
let artifacts = [
"tool-linux-x64.tar.gz",
"tool-linux-arm64.tar.gz",
"tool-darwin-arm64.tar.gz",
"tool-windows-x64.zip",
"tool-linux-riscv64",
"tool-freebsd-x64.tar.gz",
];
let create = |extra: &[&str]| {
let mut args = base.to_vec();
args.extend_from_slice(extra);
args.extend_from_slice(&artifacts);
packslip(d, &args)
};
let (code, out, err) = create(&["--require", "bin:java@17", "--require", "bin:git"]);
assert_eq!(code, 0, "{err}");
assert!(
out.contains("requires tool-linux-x64.tar.gz: libs libz.so.1; bin java>=17 git"),
"{out}"
);
let (code, out, err) = packslip(d, &["show", "dist/packslip.sigstore.json"]);
assert_eq!(code, 0, "{err}");
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
let arts = &doc["predicate"]["artifacts"];
assert_eq!(
arts[0]["requires"]["libs"],
serde_json::json!(["libz.so.1"])
);
assert_eq!(
arts[0]["requires"]["bin"][0],
serde_json::json!({"name": "java", "min": "17"})
);
assert_eq!(
arts[0]["requires"]["bin"][1],
serde_json::json!({"name": "git"})
);
assert_eq!(
arts[1]["requires"]["libs"],
serde_json::json!([]),
"shipped libz is not required"
);
assert!(
arts[2]["requires"]["libs"].is_null(),
"a script records no libs: {}",
arts[2]
);
assert_eq!(arts[2]["requires"]["bin"][0]["name"], "java");
assert_eq!(arts[3]["bin"][0], "tool-1.0/bin/tool.exe");
assert_eq!(
arts[3]["requires"]["libs"],
serde_json::json!(["libz.so.1"])
);
assert_eq!(arts[4]["format"], "raw");
assert_eq!(
arts[4]["requires"]["libs"],
serde_json::json!(["libz.so.1"])
);
assert_eq!(arts[5]["os"], "freebsd");
assert_eq!(
arts[5]["requires"]["libs"],
serde_json::json!(["libz.so.1"])
);
let (code, out, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
"--json",
],
);
assert_eq!(code, 0, "{err}");
let verified: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(
verified["requires"][0],
"tool-linux-x64.tar.gz: libs libz.so.1; bin java>=17 git"
);
assert_eq!(
verified["requires"][1],
"tool-linux-arm64.tar.gz: libs none; bin java>=17 git"
);
assert_eq!(
verified["requires"][2],
"tool-darwin-arm64.tar.gz: bin java>=17 git"
);
let (code, out, err) = packslip(
d,
&[
"verify",
"dist/packslip.sigstore.json",
"--pubkey",
"k.pub",
"--allow-unlogged",
],
);
assert_eq!(code, 0, "{err}");
assert!(
out.contains("\n requires tool-linux-x64.tar.gz: libs libz.so.1; bin java>=17 git\n"),
"{out}"
);
let (code, _, err) = create(&["--no-libs"]);
assert_eq!(code, 0, "{err}");
let (_, out, _) = packslip(d, &["show", "dist/packslip.sigstore.json"]);
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
assert!(
doc["predicate"]["artifacts"][0]["requires"].is_null(),
"{out}"
);
let mut args = base.to_vec();
let last = args.len() - 1;
args[last] = "tool-1.0/bin/other";
args.push("tool-linux-x64.tar.gz");
let (code, _, err) = packslip(d, &args);
assert_ne!(code, 0);
assert!(
err.contains("no file named \"tool-1.0/bin/other\""),
"{err}"
);
std::fs::write(
d.join("wrong.toml"),
"bin = [\"tool\"]\n[[artifact]]\npath = \"tool-linux-x64.tar.gz\"\nrequires = { libs = [\"libfoo.so.1\"] }\n",
)
.unwrap();
std::fs::write(
d.join("right.toml"),
"bin = [\"tool\"]\n[[artifact]]\npath = \"tool-linux-x64.tar.gz\"\nrequires = { libs = [\"libz.so.1\"], bin = [{ name = \"java\", min = \"17\" }] }\n",
)
.unwrap();
let manifest = |file: &str| {
packslip(
d,
&[
"create",
"--project",
"tool.example.com",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--out",
"dist",
"--manifest",
file,
],
)
};
let (code, _, err) = manifest("wrong.toml");
assert_ne!(code, 0);
assert!(
err.contains("[libfoo.so.1], but they load [libz.so.1]"),
"{err}"
);
let (code, out, err) = manifest("right.toml");
assert_eq!(code, 0, "{err}");
assert!(
out.contains("requires tool-linux-x64.tar.gz: libs libz.so.1; bin java>=17"),
"{out}"
);
for (spec, expected) in [
("java", "bin:NAME"),
("bin:", "empty command name"),
("bin:java@", "empty minimum"),
("bin:java@latest", "starts with a digit"),
("bin:tool", "release itself provides"),
("bin:java.exe", "bare name"),
] {
let (code, _, err) = create(&["--require", spec]);
assert_ne!(code, 0, "{spec}");
assert!(err.contains(expected), "{spec}: {err}");
}
let (code, _, err) = create(&["--require", "bin:java", "--require", "bin:java@11"]);
assert_ne!(code, 0);
assert!(err.contains("twice"), "{err}");
let (code, _, err) = packslip(
d,
&[
"create",
"--project",
"tool.example.com",
"--version",
"1.0.0",
"--key",
"k.key",
"--no-log",
"--require",
"bin:java",
"tool-linux-riscv64",
],
);
assert_ne!(code, 0);
assert!(err.contains("--bin"), "{err}");
}