use std::path::Path;
use crate::install::VerifyError;
use crate::layer::LayerId;
use crate::store::manifest_digest;
use crate::verify::sign_layer_manifest;
fn is_rfc3339(s: &str) -> bool {
let Some((date, time)) = s.split_once('T') else {
return false;
};
if crate::rollback::epoch_days(date).is_none() {
return false;
}
let b = time.as_bytes();
if b.len() < 9 || b[2] != b':' || b[5] != b':' {
return false;
}
if !b[..8].iter().enumerate().all(|(i, c)| {
if i == 2 || i == 5 {
*c == b':'
} else {
c.is_ascii_digit()
}
}) {
return false;
}
let rest = &time[8..];
rest == "Z" || rest.ends_with('Z') || rest.contains('+') || rest.matches('-').count() == 1
}
#[derive(Debug, Clone)]
pub struct DepositSpec {
pub layer: LayerId,
pub channel: String,
pub counter: u64,
pub issued_at: String,
pub tools: Vec<DepositTool>,
pub includes: Vec<DepositInclude>,
}
#[derive(Debug, Clone, Default)]
pub struct DepositInclude {
pub digest: String,
pub realm: Option<String>,
pub layer: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DepositTool {
pub name: String,
pub version: String,
pub platform: Option<String>,
pub bytes: Vec<u8>,
pub source: Option<ToolSource>,
pub runner: Option<RunnerSpec>,
pub kind: Option<crate::kind::PayloadKind>,
pub sdk_prefix: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunnerSpec {
pub tool: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(rename = "arg-prefix", default)]
pub arg_prefix: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolSource {
pub repo: String,
pub release: String,
pub asset: String,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepositOutcome {
pub digest: String,
pub layer: LayerId,
pub counter: u64,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DepositFileSpec {
pub layer: String,
pub channel: String,
pub counter: u64,
#[serde(default, rename = "tool")]
pub tools: Vec<SpecTool>,
#[serde(default, rename = "include")]
pub includes: Vec<SpecInclude>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SpecInclude {
pub digest: String,
#[serde(default)]
pub realm: Option<String>,
#[serde(default)]
pub layer: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SpecTool {
pub name: String,
pub version: String,
#[serde(default)]
pub platform: Option<String>,
pub path: String,
#[serde(default)]
pub source: Option<ToolSource>,
#[serde(default)]
pub runner: Option<RunnerSpec>,
#[serde(default)]
pub kind: Option<String>,
#[serde(rename = "sdk-prefix", default)]
pub sdk_prefix: Option<String>,
}
pub fn parse_deposit_spec(toml_text: &str) -> Result<DepositFileSpec, DepositError> {
toml::from_str(toml_text).map_err(|e| DepositError::Spec(e.to_string()))
}
#[derive(Debug, thiserror::Error)]
pub enum DepositError {
#[error(
"the envelope this deposit just signed does not verify against the key that signed \
it — refusing to publish an artifact no consumer could accept"
)]
SelfVerifyFailed,
#[error(
"channel {channel:?} is not one a pin can name — use `qualified` or `rolling`. \
Signing it would produce a layer no varve.toml could ever select."
)]
BadChannel { channel: String },
#[error(
"issued-at {issued_at:?} is not an RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z). \
It is signed into the manifest and drives staleness and the SBOM timestamp, so it \
cannot be corrected afterwards."
)]
BadIssuedAt { issued_at: String },
#[error("deposit spec is not valid: {0}")]
Spec(String),
#[error("deposit has no tools — an empty layer is not a toolchain")]
NoTools,
#[error(
"tool '{name}' is deposited twice for platform {platform} (versions {first} and \
{second}) — a tool is DISPATCHED BY NAME (`varve run {name}`, `varve which {name}`, the \
argv[0] shims), so one name must resolve to exactly one binary per platform. Deposit the \
other version as a separate layer, or give it a distinct name."
)]
DuplicateTool {
name: String,
platform: String,
first: String,
second: String,
},
#[error(
"payload '{name}' version {version} is deposited twice for platform {platform} — a layer \
may hold several VERSIONS of one name, but not one version twice: the two would be the \
same payload, and only one set of bytes could land."
)]
DuplicatePayload {
name: String,
version: String,
platform: String,
},
#[error(
"payload '{name}' is kind {kind} and carries `sdk-prefix` — only a tree payload (sdk) is \
relocated, so the prefix would be signed into the manifest, ignored by every adapter, \
and believed. Drop it, or deposit this payload as kind = \"sdk\"."
)]
SdkPrefixOnNonTree { name: String, kind: String },
#[error(
"sdk '{name}' version {version} declares no `sdk-prefix` — the absolute path it was \
BUILT for. Without it `varve export-sdk` has no relocation budget and no path to \
patch, so the layer would install and verify and could never be exported. Add \
`sdk-prefix = \"/opt/poky/4.0\"` (the path the SDK was built for) to the [[tool]] table."
)]
SdkPrefixMissing { name: String, version: String },
#[error(
"sdk '{name}' declares sdk-prefix {prefix:?}, which is not absolute — the prefix is the \
path PATCHED INTO the SDK's binaries, and a relative one there would resolve against \
whatever directory a compiler happens to run in"
)]
SdkPrefixNotAbsolute { name: String, prefix: String },
#[error(transparent)]
Sign(#[from] VerifyError),
#[error("io error at {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
}
fn platform_label(platform: Option<&String>) -> String {
platform
.cloned()
.unwrap_or_else(|| "any (unstamped)".to_string())
}
fn check_identities(tools: &[&DepositTool]) -> Result<(), DepositError> {
let mut dispatched: std::collections::BTreeMap<(&str, Option<&String>), &str> =
std::collections::BTreeMap::new();
let mut held: std::collections::BTreeSet<(&str, &str, Option<&String>)> =
std::collections::BTreeSet::new();
for tool in tools {
let dispatchable = tool.kind.unwrap_or_default().is_dispatchable();
let platform = tool.platform.as_ref();
if dispatchable {
if let Some(first) = dispatched.insert((&tool.name, platform), &tool.version) {
return Err(DepositError::DuplicateTool {
name: tool.name.clone(),
platform: platform_label(platform),
first: first.to_string(),
second: tool.version.clone(),
});
}
} else if !held.insert((&tool.name, &tool.version, platform)) {
return Err(DepositError::DuplicatePayload {
name: tool.name.clone(),
version: tool.version.clone(),
platform: platform_label(platform),
});
}
}
Ok(())
}
fn check_sdk_prefixes(tools: &[&DepositTool]) -> Result<(), DepositError> {
for tool in tools {
let is_tree = tool.kind == Some(crate::kind::PayloadKind::Sdk);
match (&tool.sdk_prefix, is_tree) {
(Some(_), false) => {
return Err(DepositError::SdkPrefixOnNonTree {
name: tool.name.clone(),
kind: tool.kind.unwrap_or_default().as_str().to_string(),
});
}
(None, true) => {
return Err(DepositError::SdkPrefixMissing {
name: tool.name.clone(),
version: tool.version.clone(),
});
}
(Some(prefix), true) if !prefix.starts_with('/') => {
return Err(DepositError::SdkPrefixNotAbsolute {
name: tool.name.clone(),
prefix: prefix.clone(),
});
}
_ => {}
}
}
Ok(())
}
pub fn deposit(
spec: &DepositSpec,
signing_key: &[u8],
key_id: &str,
dest: &Path,
) -> Result<DepositOutcome, DepositError> {
if spec.tools.is_empty() {
return Err(DepositError::NoTools);
}
let mut tools: Vec<&DepositTool> = spec.tools.iter().collect();
tools.sort_by(|a, b| {
(&a.name, &a.version, &a.platform).cmp(&(&b.name, &b.version, &b.platform))
});
check_identities(&tools)?;
check_sdk_prefixes(&tools)?;
let entries: Vec<serde_json::Value> = tools
.iter()
.map(|tool| {
let mut annotations = serde_json::Map::new();
annotations.insert("eu.pulseengine.tool".into(), tool.name.clone().into());
annotations.insert(
"eu.pulseengine.tool.version".into(),
tool.version.clone().into(),
);
if let Some(platform) = &tool.platform {
annotations.insert(
crate::platform::ANN_PLATFORM.into(),
platform.clone().into(),
);
}
if let Some(source) = &tool.source {
annotations.insert(
crate::bazel::ANN_SRC_REPO.into(),
source.repo.clone().into(),
);
annotations.insert(
crate::bazel::ANN_SRC_RELEASE.into(),
source.release.clone().into(),
);
annotations.insert(
crate::bazel::ANN_SRC_ASSET.into(),
source.asset.clone().into(),
);
annotations.insert(
crate::bazel::ANN_SRC_SHA256.into(),
source.sha256.clone().into(),
);
}
if let Some(kind) = tool.kind
&& kind != crate::kind::PayloadKind::Tool
{
annotations.insert(crate::kind::ANN_KIND.into(), kind.as_str().into());
}
if let Some(prefix) = &tool.sdk_prefix {
annotations.insert(
crate::sdkexport::ANN_SDK_PREFIX.into(),
prefix.clone().into(),
);
}
if let Some(runner) = &tool.runner {
annotations.insert(crate::bazel::ANN_RUNNER.into(), runner.tool.clone().into());
if !runner.args.is_empty() {
annotations.insert(
crate::bazel::ANN_RUNNER_ARGS.into(),
runner.args.join(" ").into(),
);
}
if let Some(prefix) = &runner.arg_prefix {
annotations.insert(
crate::bazel::ANN_RUNNER_ARG_PREFIX.into(),
prefix.clone().into(),
);
}
}
serde_json::json!({
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": manifest_digest(&tool.bytes),
"size": tool.bytes.len(),
"annotations": annotations,
})
})
.collect();
let mut entries = entries;
for inc in &spec.includes {
let mut annotations = serde_json::Map::new();
annotations.insert(
crate::kind::ANN_KIND.into(),
crate::kind::PayloadKind::Layer.as_str().into(),
);
if let Some(realm) = &inc.realm {
annotations.insert(
crate::compose::ANN_INCLUDE_REALM.into(),
realm.clone().into(),
);
}
if let Some(layer) = &inc.layer {
annotations.insert(
crate::compose::ANN_INCLUDE_LAYER.into(),
layer.clone().into(),
);
}
entries.push(serde_json::json!({
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": inc.digest,
"size": 0,
"annotations": annotations,
}));
}
if spec.channel.parse::<crate::pin::Channel>().is_err() {
return Err(DepositError::BadChannel {
channel: spec.channel.clone(),
});
}
if !is_rfc3339(&spec.issued_at) {
return Err(DepositError::BadIssuedAt {
issued_at: spec.issued_at.clone(),
});
}
let payload_json = serde_json::json!({
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
"annotations": {
"eu.pulseengine.varve.layer": spec.layer.to_string(),
"eu.pulseengine.varve.line": spec.layer.line().to_string(),
"eu.pulseengine.varve.channel": spec.channel,
"eu.pulseengine.varve.counter": spec.counter.to_string(),
"org.opencontainers.image.created": spec.issued_at,
},
"manifests": entries,
});
let payload = serde_json::to_vec_pretty(&payload_json).expect("payload serializes");
let envelope = sign_layer_manifest(&payload, signing_key, key_id)?;
let public = &signing_key[32..];
match crate::verify::dsse_verify_typed(
envelope.as_bytes(),
crate::verify::LAYER_PAYLOAD_TYPE,
public,
) {
Ok(back) if back == payload => {}
_ => return Err(DepositError::SelfVerifyFailed),
}
let blobs: Vec<(String, Vec<u8>)> = tools
.iter()
.map(|tool| (manifest_digest(&tool.bytes), tool.bytes.clone()))
.collect();
crate::archive::write_oci_layout(
&payload,
envelope.as_bytes(),
&blobs,
&spec.layer.to_string(),
&spec.channel,
None,
dest,
)
.map_err(|(path, source)| DepositError::Io { path, source })?;
Ok(DepositOutcome {
digest: manifest_digest(&payload),
layer: spec.layer.clone(),
counter: spec.counter,
})
}
#[cfg(test)]
mod producer_tests {
use super::*;
#[test]
fn a_channel_no_pin_could_name_is_refused_before_signing() {
assert!("qualified".parse::<crate::pin::Channel>().is_ok());
assert!("rolling".parse::<crate::pin::Channel>().is_ok());
for bad in ["stable", "Qualified", "", "beta"] {
assert!(
bad.parse::<crate::pin::Channel>().is_err(),
"{bad} must be refused"
);
}
}
#[test]
fn issued_at_must_be_a_timestamp_not_merely_a_date() {
assert!(is_rfc3339("2026-08-01T00:00:00Z"));
assert!(is_rfc3339("2026-08-01T12:34:56.789Z"));
assert!(is_rfc3339("2026-08-01T12:34:56+02:00"));
for bad in [
"2026-10-01",
"not-a-date",
"",
"2026-08-01T",
"2026-13-01T00:00:00Z",
] {
assert!(!is_rfc3339(bad), "{bad} must be refused");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive::OciLayoutSource;
use crate::install::{InstallPolicy, install};
use crate::pin::Pin;
use crate::rollback::HighWaterMarks;
use crate::store::Store;
use crate::verify::{PinnedKeyVerifier, generate_root_keypair};
fn spec() -> DepositSpec {
DepositSpec {
includes: Vec::new(),
layer: "2026.08.0".parse().unwrap(),
channel: "qualified".into(),
counter: 1,
issued_at: "2026-08-07T00:00:00Z".into(),
tools: vec![
DepositTool {
name: "synth".into(),
version: "0.45.0".into(),
platform: None,
bytes: b"synth-bytes".to_vec(),
source: None,
runner: None,
kind: None,
sdk_prefix: None,
},
DepositTool {
name: "rivet".into(),
version: "0.32.0".into(),
platform: None,
bytes: b"rivet-bytes".to_vec(),
source: None,
runner: None,
kind: None,
sdk_prefix: None,
},
],
}
}
#[test]
fn a_deposit_round_trips_through_the_standard_install_pipeline() {
let (sk, pk) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let dest = tmp.path().join("deposit");
let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
assert_eq!(outcome.layer.to_string(), "2026.08.0");
assert!(dest.join("oci-layout").is_file());
assert!(dest.join("index.json").is_file());
let pin = Pin::parse(
"manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\n",
"varve.toml",
)
.unwrap();
let root = tmp.path().join("fresh");
let store = Store::at(&root);
let mut marks = HighWaterMarks::load(&root).unwrap();
let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
let policy = InstallPolicy {
index: None,
now: "2026-08-07T00:00:00Z",
staleness_threshold_days: 90,
platform: "test-platform",
};
let installed = install(
&pin,
&OciLayoutSource::at(&dest),
&verifier,
&store,
&mut marks,
&policy,
)
.unwrap();
assert_eq!(installed.digest, outcome.digest);
assert_eq!(installed.counter, 1);
let entry = store.get(&installed.digest).unwrap().unwrap();
let checked =
crate::reverify::verify_installed(&store, &entry, &verifier, "test-platform").unwrap();
assert_eq!(checked, 2);
}
#[test]
fn the_payload_records_layer_line_channel_counter_and_tool_versions() {
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let dest = tmp.path().join("deposit");
let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
let hex = outcome.digest.strip_prefix("sha256:").unwrap();
let payload = std::fs::read(dest.join("blobs/sha256").join(hex)).unwrap();
let json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
let ann = &json["annotations"];
assert_eq!(ann["eu.pulseengine.varve.layer"], "2026.08.0");
assert_eq!(ann["eu.pulseengine.varve.line"], "2026.08");
assert_eq!(ann["eu.pulseengine.varve.channel"], "qualified");
assert_eq!(ann["eu.pulseengine.varve.counter"], "1");
assert_eq!(
ann["org.opencontainers.image.created"],
"2026-08-07T00:00:00Z"
);
let entries = json["manifests"].as_array().unwrap();
assert_eq!(entries.len(), 2);
assert!(entries.iter().any(|e| {
e["annotations"]["eu.pulseengine.tool"] == "synth"
&& e["annotations"]["eu.pulseengine.tool.version"] == "0.45.0"
}));
}
#[test]
fn identical_specs_deposit_identical_digests() {
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let a = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("a")).unwrap();
let b = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("b")).unwrap();
assert_eq!(
a.digest, b.digest,
"the payload is deterministic — the digest IS the identity"
);
}
#[test]
fn empty_and_duplicate_tool_lists_are_refused() {
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let mut empty = spec();
empty.tools.clear();
assert!(matches!(
deposit(&empty, &sk, "k", &tmp.path().join("x")).unwrap_err(),
DepositError::NoTools
));
let mut dup = spec();
dup.tools[1].name = "synth".into();
let err = deposit(&dup, &sk, "k", &tmp.path().join("y")).unwrap_err();
assert!(
matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
"got: {err}"
);
}
fn payload(
name: &str,
version: &str,
kind: Option<crate::kind::PayloadKind>,
platform: Option<&str>,
) -> DepositTool {
DepositTool {
name: name.into(),
version: version.into(),
platform: platform.map(str::to_string),
bytes: format!("{name}-{version}-bytes").into_bytes(),
source: None,
runner: None,
kind,
sdk_prefix: None,
}
}
#[test]
fn two_versions_of_one_crate_are_a_layer_not_a_duplicate() {
use crate::kind::PayloadKind;
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let mut spec = spec();
spec.tools = vec![
payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
];
let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
let hex = outcome.digest.strip_prefix("sha256:").unwrap();
let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
let entries = json["manifests"].as_array().unwrap();
assert_eq!(entries.len(), 2);
let versions: Vec<&str> = entries
.iter()
.map(|e| {
e["annotations"]["eu.pulseengine.tool.version"]
.as_str()
.unwrap()
})
.collect();
assert_eq!(versions, vec!["1.0.200", "1.0.210"]);
assert_ne!(
entries[0]["digest"], entries[1]["digest"],
"two versions are two artifacts"
);
}
#[test]
fn extensions_deposit_as_vsix_entries_at_several_versions() {
use crate::kind::PayloadKind;
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let mut spec = spec();
spec.tools = vec![
payload(
"rust-lang.rust-analyzer",
"0.3.2260",
Some(PayloadKind::Vsix),
None,
),
payload(
"rust-lang.rust-analyzer",
"0.3.2300",
Some(PayloadKind::Vsix),
None,
),
payload(
"vadimcn.vscode-lldb",
"1.11.4",
Some(PayloadKind::Vsix),
None,
),
];
let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
let hex = outcome.digest.strip_prefix("sha256:").unwrap();
let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
let entries = json["manifests"].as_array().unwrap();
assert_eq!(entries.len(), 3, "all three extensions must be signed in");
for e in entries {
assert_eq!(
e["annotations"][crate::kind::ANN_KIND],
"vsix",
"every entry must carry the vsix kind in the SIGNED payload"
);
}
let ids: Vec<(&str, &str)> = entries
.iter()
.map(|e| {
(
e["annotations"]["eu.pulseengine.tool"].as_str().unwrap(),
e["annotations"]["eu.pulseengine.tool.version"]
.as_str()
.unwrap(),
)
})
.collect();
assert_eq!(
ids,
vec![
("rust-lang.rust-analyzer", "0.3.2260"),
("rust-lang.rust-analyzer", "0.3.2300"),
("vadimcn.vscode-lldb", "1.11.4"),
]
);
assert_ne!(
entries[0]["digest"], entries[1]["digest"],
"two versions of one extension are two artifacts"
);
let mut dup = spec;
dup.tools = vec![
payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
];
let err = deposit(&dup, &sk, "k", &tmp.path().join("e")).unwrap_err();
assert!(
matches!(&err, DepositError::DuplicatePayload { name, version, .. }
if name == "pub.ext" && version == "1.0.0"),
"got: {err}"
);
}
#[test]
fn a_tool_may_not_appear_twice_under_one_name_however_its_versions_differ() {
use crate::kind::PayloadKind;
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let mut spec = spec();
spec.tools = vec![
payload("synth", "0.45.0", Some(PayloadKind::Tool), None),
payload("synth", "0.46.0", None, None), ];
let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
let msg = err.to_string();
assert!(
matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
"got: {err}"
);
assert!(msg.contains("0.45.0") && msg.contains("0.46.0"), "{msg}");
assert!(
msg.contains("any"),
"an unstamped entry is any-platform: {msg}"
);
let mut stamped = spec;
for tool in &mut stamped.tools {
tool.platform = Some("x86_64-unknown-linux-gnu".into());
}
let msg = deposit(&stamped, &sk, "k", &tmp.path().join("e"))
.unwrap_err()
.to_string();
assert!(msg.contains("x86_64-unknown-linux-gnu"), "{msg}");
}
#[test]
fn one_version_deposited_twice_is_still_refused_and_the_error_names_it() {
use crate::kind::PayloadKind;
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let mut spec = spec();
spec.tools = vec![
payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
];
let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
let msg = err.to_string();
assert!(
matches!(&err, DepositError::DuplicatePayload { name, version, .. }
if name == "serde" && version == "1.0.200"),
"got: {err}"
);
assert!(msg.contains("serde") && msg.contains("1.0.200"), "{msg}");
}
#[test]
fn platform_still_separates_identities_for_both_rules() {
use crate::kind::PayloadKind;
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let mut spec = spec();
spec.tools = vec![
payload("synth", "0.45.0", None, Some("aarch64-apple-darwin")),
payload("synth", "0.45.0", None, Some("x86_64-unknown-linux-gnu")),
payload(
"serde",
"1.0.200",
Some(PayloadKind::Crate),
Some("aarch64-apple-darwin"),
),
payload(
"serde",
"1.0.200",
Some(PayloadKind::Crate),
Some("x86_64-unknown-linux-gnu"),
),
];
deposit(&spec, &sk, "k", &tmp.path().join("d")).expect(
"distinct platforms, distinct
identities",
);
}
#[test]
fn the_signed_digest_does_not_depend_on_the_order_versions_are_listed_in() {
use crate::kind::PayloadKind;
let (sk, _) = generate_root_keypair();
let tmp = tempfile::tempdir().unwrap();
let a_first = vec![
payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
];
let b_first: Vec<DepositTool> = a_first.iter().rev().cloned().collect();
let mut s1 = spec();
s1.tools = a_first;
let mut s2 = spec();
s2.tools = b_first;
assert_eq!(
deposit(&s1, &sk, "k", &tmp.path().join("a"))
.unwrap()
.digest,
deposit(&s2, &sk, "k", &tmp.path().join("b"))
.unwrap()
.digest,
);
}
}