1use std::path::Path;
12
13use crate::install::VerifyError;
14use crate::layer::LayerId;
15use crate::store::manifest_digest;
16use crate::verify::sign_layer_manifest;
17
18fn is_rfc3339(s: &str) -> bool {
23 let Some((date, time)) = s.split_once('T') else {
24 return false;
25 };
26 if crate::rollback::epoch_days(date).is_none() {
27 return false;
28 }
29 let b = time.as_bytes();
31 if b.len() < 9 || b[2] != b':' || b[5] != b':' {
32 return false;
33 }
34 if !b[..8].iter().enumerate().all(|(i, c)| {
35 if i == 2 || i == 5 {
36 *c == b':'
37 } else {
38 c.is_ascii_digit()
39 }
40 }) {
41 return false;
42 }
43 let rest = &time[8..];
44 rest == "Z" || rest.ends_with('Z') || rest.contains('+') || rest.matches('-').count() == 1
45}
46
47#[derive(Debug, Clone)]
49pub struct DepositSpec {
50 pub layer: LayerId,
51 pub channel: String,
53 pub counter: u64,
56 pub issued_at: String,
59 pub tools: Vec<DepositTool>,
61 pub includes: Vec<DepositInclude>,
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct DepositInclude {
68 pub digest: String,
69 pub realm: Option<String>,
70 pub layer: Option<String>,
71}
72
73#[derive(Debug, Clone)]
74pub struct DepositTool {
75 pub name: String,
76 pub version: String,
77 pub platform: Option<String>,
80 pub bytes: Vec<u8>,
81 pub source: Option<ToolSource>,
85 pub runner: Option<RunnerSpec>,
89 pub kind: Option<crate::kind::PayloadKind>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct RunnerSpec {
97 pub tool: String,
98 #[serde(default)]
99 pub args: Vec<String>,
100 #[serde(rename = "arg-prefix", default)]
101 pub arg_prefix: Option<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct ToolSource {
108 pub repo: String,
110 pub release: String,
112 pub asset: String,
114 pub sha256: String,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct DepositOutcome {
122 pub digest: String,
124 pub layer: LayerId,
125 pub counter: u64,
126}
127
128#[derive(Debug, serde::Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct DepositFileSpec {
133 pub layer: String,
134 pub channel: String,
135 pub counter: u64,
136 #[serde(default, rename = "tool")]
137 pub tools: Vec<SpecTool>,
138 #[serde(default, rename = "include")]
141 pub includes: Vec<SpecInclude>,
142}
143
144#[derive(Debug, serde::Deserialize)]
147#[serde(deny_unknown_fields)]
148pub struct SpecInclude {
149 pub digest: String,
151 #[serde(default)]
153 pub realm: Option<String>,
154 #[serde(default)]
157 pub layer: Option<String>,
158}
159
160#[derive(Debug, serde::Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct SpecTool {
163 pub name: String,
164 pub version: String,
165 #[serde(default)]
166 pub platform: Option<String>,
167 pub path: String,
169 #[serde(default)]
170 pub source: Option<ToolSource>,
171 #[serde(default)]
172 pub runner: Option<RunnerSpec>,
173 #[serde(default)]
176 pub kind: Option<String>,
177}
178
179pub fn parse_deposit_spec(toml_text: &str) -> Result<DepositFileSpec, DepositError> {
180 toml::from_str(toml_text).map_err(|e| DepositError::Spec(e.to_string()))
181}
182
183#[derive(Debug, thiserror::Error)]
184pub enum DepositError {
185 #[error(
186 "the envelope this deposit just signed does not verify against the key that signed \
187 it — refusing to publish an artifact no consumer could accept"
188 )]
189 SelfVerifyFailed,
190 #[error(
191 "channel {channel:?} is not one a pin can name — use `qualified` or `rolling`. \
192 Signing it would produce a layer no varve.toml could ever select."
193 )]
194 BadChannel { channel: String },
195 #[error(
196 "issued-at {issued_at:?} is not an RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z). \
197 It is signed into the manifest and drives staleness and the SBOM timestamp, so it \
198 cannot be corrected afterwards."
199 )]
200 BadIssuedAt { issued_at: String },
201 #[error("deposit spec is not valid: {0}")]
202 Spec(String),
203 #[error("deposit has no tools — an empty layer is not a toolchain")]
204 NoTools,
205 #[error(
206 "tool '{name}' is deposited twice for platform {platform} (versions {first} and \
207 {second}) — a tool is DISPATCHED BY NAME (`varve run {name}`, `varve which {name}`, the \
208 argv[0] shims), so one name must resolve to exactly one binary per platform. Deposit the \
209 other version as a separate layer, or give it a distinct name."
210 )]
211 DuplicateTool {
212 name: String,
213 platform: String,
214 first: String,
215 second: String,
216 },
217 #[error(
218 "payload '{name}' version {version} is deposited twice for platform {platform} — a layer \
219 may hold several VERSIONS of one name, but not one version twice: the two would be the \
220 same payload, and only one set of bytes could land."
221 )]
222 DuplicatePayload {
223 name: String,
224 version: String,
225 platform: String,
226 },
227 #[error(transparent)]
228 Sign(#[from] VerifyError),
229 #[error("io error at {path}: {source}")]
230 Io {
231 path: String,
232 #[source]
233 source: std::io::Error,
234 },
235}
236
237fn platform_label(platform: Option<&String>) -> String {
239 platform
240 .cloned()
241 .unwrap_or_else(|| "any (unstamped)".to_string())
242}
243
244fn check_identities(tools: &[&DepositTool]) -> Result<(), DepositError> {
255 let mut dispatched: std::collections::BTreeMap<(&str, Option<&String>), &str> =
257 std::collections::BTreeMap::new();
258 let mut held: std::collections::BTreeSet<(&str, &str, Option<&String>)> =
260 std::collections::BTreeSet::new();
261 for tool in tools {
262 let dispatchable = tool.kind.unwrap_or_default().is_dispatchable();
263 let platform = tool.platform.as_ref();
264 if dispatchable {
265 if let Some(first) = dispatched.insert((&tool.name, platform), &tool.version) {
266 return Err(DepositError::DuplicateTool {
267 name: tool.name.clone(),
268 platform: platform_label(platform),
269 first: first.to_string(),
270 second: tool.version.clone(),
271 });
272 }
273 } else if !held.insert((&tool.name, &tool.version, platform)) {
274 return Err(DepositError::DuplicatePayload {
275 name: tool.name.clone(),
276 version: tool.version.clone(),
277 platform: platform_label(platform),
278 });
279 }
280 }
281 Ok(())
282}
283
284pub fn deposit(
286 spec: &DepositSpec,
287 signing_key: &[u8],
288 key_id: &str,
289 dest: &Path,
290) -> Result<DepositOutcome, DepositError> {
291 if spec.tools.is_empty() {
292 return Err(DepositError::NoTools);
293 }
294 let mut tools: Vec<&DepositTool> = spec.tools.iter().collect();
295 tools.sort_by(|a, b| {
299 (&a.name, &a.version, &a.platform).cmp(&(&b.name, &b.version, &b.platform))
300 });
301 check_identities(&tools)?;
302
303 let entries: Vec<serde_json::Value> = tools
308 .iter()
309 .map(|tool| {
310 let mut annotations = serde_json::Map::new();
311 annotations.insert("eu.pulseengine.tool".into(), tool.name.clone().into());
312 annotations.insert(
313 "eu.pulseengine.tool.version".into(),
314 tool.version.clone().into(),
315 );
316 if let Some(platform) = &tool.platform {
317 annotations.insert(
318 crate::platform::ANN_PLATFORM.into(),
319 platform.clone().into(),
320 );
321 }
322 if let Some(source) = &tool.source {
323 annotations.insert(
324 crate::bazel::ANN_SRC_REPO.into(),
325 source.repo.clone().into(),
326 );
327 annotations.insert(
328 crate::bazel::ANN_SRC_RELEASE.into(),
329 source.release.clone().into(),
330 );
331 annotations.insert(
332 crate::bazel::ANN_SRC_ASSET.into(),
333 source.asset.clone().into(),
334 );
335 annotations.insert(
336 crate::bazel::ANN_SRC_SHA256.into(),
337 source.sha256.clone().into(),
338 );
339 }
340 if let Some(kind) = tool.kind
344 && kind != crate::kind::PayloadKind::Tool
345 {
346 annotations.insert(crate::kind::ANN_KIND.into(), kind.as_str().into());
347 }
348 if let Some(runner) = &tool.runner {
349 annotations.insert(crate::bazel::ANN_RUNNER.into(), runner.tool.clone().into());
350 if !runner.args.is_empty() {
351 annotations.insert(
352 crate::bazel::ANN_RUNNER_ARGS.into(),
353 runner.args.join(" ").into(),
354 );
355 }
356 if let Some(prefix) = &runner.arg_prefix {
357 annotations.insert(
358 crate::bazel::ANN_RUNNER_ARG_PREFIX.into(),
359 prefix.clone().into(),
360 );
361 }
362 }
363 serde_json::json!({
364 "mediaType": "application/vnd.oci.image.manifest.v1+json",
365 "digest": manifest_digest(&tool.bytes),
366 "size": tool.bytes.len(),
367 "annotations": annotations,
368 })
369 })
370 .collect();
371 let mut entries = entries;
376 for inc in &spec.includes {
377 let mut annotations = serde_json::Map::new();
378 annotations.insert(
379 crate::kind::ANN_KIND.into(),
380 crate::kind::PayloadKind::Layer.as_str().into(),
381 );
382 if let Some(realm) = &inc.realm {
383 annotations.insert(
384 crate::compose::ANN_INCLUDE_REALM.into(),
385 realm.clone().into(),
386 );
387 }
388 if let Some(layer) = &inc.layer {
389 annotations.insert(
390 crate::compose::ANN_INCLUDE_LAYER.into(),
391 layer.clone().into(),
392 );
393 }
394 entries.push(serde_json::json!({
395 "mediaType": "application/vnd.oci.image.index.v1+json",
396 "digest": inc.digest,
397 "size": 0,
398 "annotations": annotations,
399 }));
400 }
401 if spec.channel.parse::<crate::pin::Channel>().is_err() {
405 return Err(DepositError::BadChannel {
406 channel: spec.channel.clone(),
407 });
408 }
409 if !is_rfc3339(&spec.issued_at) {
410 return Err(DepositError::BadIssuedAt {
411 issued_at: spec.issued_at.clone(),
412 });
413 }
414 let payload_json = serde_json::json!({
415 "schemaVersion": 2,
416 "mediaType": "application/vnd.oci.image.index.v1+json",
417 "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
418 "annotations": {
419 "eu.pulseengine.varve.layer": spec.layer.to_string(),
420 "eu.pulseengine.varve.line": spec.layer.line().to_string(),
421 "eu.pulseengine.varve.channel": spec.channel,
422 "eu.pulseengine.varve.counter": spec.counter.to_string(),
423 "org.opencontainers.image.created": spec.issued_at,
424 },
425 "manifests": entries,
426 });
427 let payload = serde_json::to_vec_pretty(&payload_json).expect("payload serializes");
428 let envelope = sign_layer_manifest(&payload, signing_key, key_id)?;
429 let public = &signing_key[32..];
433 match crate::verify::dsse_verify_typed(
434 envelope.as_bytes(),
435 crate::verify::LAYER_PAYLOAD_TYPE,
436 public,
437 ) {
438 Ok(back) if back == payload => {}
439 _ => return Err(DepositError::SelfVerifyFailed),
440 }
441
442 let blobs: Vec<(String, Vec<u8>)> = tools
443 .iter()
444 .map(|tool| (manifest_digest(&tool.bytes), tool.bytes.clone()))
445 .collect();
446 crate::archive::write_oci_layout(
447 &payload,
448 envelope.as_bytes(),
449 &blobs,
450 &spec.layer.to_string(),
451 &spec.channel,
452 None,
455 dest,
456 )
457 .map_err(|(path, source)| DepositError::Io { path, source })?;
458
459 Ok(DepositOutcome {
460 digest: manifest_digest(&payload),
461 layer: spec.layer.clone(),
462 counter: spec.counter,
463 })
464}
465
466#[cfg(test)]
467mod producer_tests {
468 use super::*;
469
470 #[test]
472 fn a_channel_no_pin_could_name_is_refused_before_signing() {
473 assert!("qualified".parse::<crate::pin::Channel>().is_ok());
477 assert!("rolling".parse::<crate::pin::Channel>().is_ok());
478 for bad in ["stable", "Qualified", "", "beta"] {
479 assert!(
480 bad.parse::<crate::pin::Channel>().is_err(),
481 "{bad} must be refused"
482 );
483 }
484 }
485
486 #[test]
488 fn issued_at_must_be_a_timestamp_not_merely_a_date() {
489 assert!(is_rfc3339("2026-08-01T00:00:00Z"));
493 assert!(is_rfc3339("2026-08-01T12:34:56.789Z"));
494 assert!(is_rfc3339("2026-08-01T12:34:56+02:00"));
495 for bad in [
496 "2026-10-01",
497 "not-a-date",
498 "",
499 "2026-08-01T",
500 "2026-13-01T00:00:00Z",
501 ] {
502 assert!(!is_rfc3339(bad), "{bad} must be refused");
503 }
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::archive::OciLayoutSource;
511 use crate::install::{InstallPolicy, install};
512 use crate::pin::Pin;
513 use crate::rollback::HighWaterMarks;
514 use crate::store::Store;
515 use crate::verify::{PinnedKeyVerifier, generate_root_keypair};
516
517 fn spec() -> DepositSpec {
518 DepositSpec {
519 includes: Vec::new(),
520 layer: "2026.08.0".parse().unwrap(),
521 channel: "qualified".into(),
522 counter: 1,
523 issued_at: "2026-08-07T00:00:00Z".into(),
524 tools: vec![
525 DepositTool {
526 name: "synth".into(),
527 version: "0.45.0".into(),
528 platform: None,
529 bytes: b"synth-bytes".to_vec(),
530 source: None,
531 runner: None,
532 kind: None,
533 },
534 DepositTool {
535 name: "rivet".into(),
536 version: "0.32.0".into(),
537 platform: None,
538 bytes: b"rivet-bytes".to_vec(),
539 source: None,
540 runner: None,
541 kind: None,
542 },
543 ],
544 }
545 }
546
547 #[test]
549 fn a_deposit_round_trips_through_the_standard_install_pipeline() {
550 let (sk, pk) = generate_root_keypair();
551 let tmp = tempfile::tempdir().unwrap();
552 let dest = tmp.path().join("deposit");
553 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
554 assert_eq!(outcome.layer.to_string(), "2026.08.0");
555
556 assert!(dest.join("oci-layout").is_file());
558 assert!(dest.join("index.json").is_file());
559
560 let pin = Pin::parse(
562 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\n",
563 "varve.toml",
564 )
565 .unwrap();
566 let root = tmp.path().join("fresh");
567 let store = Store::at(&root);
568 let mut marks = HighWaterMarks::load(&root).unwrap();
569 let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
570 let policy = InstallPolicy {
571 index: None,
572 now: "2026-08-07T00:00:00Z",
573 staleness_threshold_days: 90,
574 platform: "test-platform",
575 };
576 let installed = install(
577 &pin,
578 &OciLayoutSource::at(&dest),
579 &verifier,
580 &store,
581 &mut marks,
582 &policy,
583 )
584 .unwrap();
585 assert_eq!(installed.digest, outcome.digest);
586 assert_eq!(installed.counter, 1);
587 let entry = store.get(&installed.digest).unwrap().unwrap();
588 let checked =
589 crate::reverify::verify_installed(&store, &entry, &verifier, "test-platform").unwrap();
590 assert_eq!(checked, 2);
591 }
592
593 #[test]
595 fn the_payload_records_layer_line_channel_counter_and_tool_versions() {
596 let (sk, _) = generate_root_keypair();
597 let tmp = tempfile::tempdir().unwrap();
598 let dest = tmp.path().join("deposit");
599 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
600 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
601 let payload = std::fs::read(dest.join("blobs/sha256").join(hex)).unwrap();
602 let json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
603 let ann = &json["annotations"];
604 assert_eq!(ann["eu.pulseengine.varve.layer"], "2026.08.0");
605 assert_eq!(ann["eu.pulseengine.varve.line"], "2026.08");
606 assert_eq!(ann["eu.pulseengine.varve.channel"], "qualified");
607 assert_eq!(ann["eu.pulseengine.varve.counter"], "1");
608 assert_eq!(
609 ann["org.opencontainers.image.created"],
610 "2026-08-07T00:00:00Z"
611 );
612 let entries = json["manifests"].as_array().unwrap();
613 assert_eq!(entries.len(), 2);
614 assert!(entries.iter().any(|e| {
615 e["annotations"]["eu.pulseengine.tool"] == "synth"
616 && e["annotations"]["eu.pulseengine.tool.version"] == "0.45.0"
617 }));
618 }
619
620 #[test]
622 fn identical_specs_deposit_identical_digests() {
623 let (sk, _) = generate_root_keypair();
624 let tmp = tempfile::tempdir().unwrap();
625 let a = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("a")).unwrap();
626 let b = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("b")).unwrap();
627 assert_eq!(
628 a.digest, b.digest,
629 "the payload is deterministic — the digest IS the identity"
630 );
631 }
632
633 #[test]
635 fn empty_and_duplicate_tool_lists_are_refused() {
636 let (sk, _) = generate_root_keypair();
637 let tmp = tempfile::tempdir().unwrap();
638 let mut empty = spec();
639 empty.tools.clear();
640 assert!(matches!(
641 deposit(&empty, &sk, "k", &tmp.path().join("x")).unwrap_err(),
642 DepositError::NoTools
643 ));
644 let mut dup = spec();
645 dup.tools[1].name = "synth".into();
646 let err = deposit(&dup, &sk, "k", &tmp.path().join("y")).unwrap_err();
647 assert!(
648 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
649 "got: {err}"
650 );
651 }
652
653 fn payload(
655 name: &str,
656 version: &str,
657 kind: Option<crate::kind::PayloadKind>,
658 platform: Option<&str>,
659 ) -> DepositTool {
660 DepositTool {
661 name: name.into(),
662 version: version.into(),
663 platform: platform.map(str::to_string),
664 bytes: format!("{name}-{version}-bytes").into_bytes(),
665 source: None,
666 runner: None,
667 kind,
668 }
669 }
670
671 #[test]
673 fn two_versions_of_one_crate_are_a_layer_not_a_duplicate() {
674 use crate::kind::PayloadKind;
680 let (sk, _) = generate_root_keypair();
681 let tmp = tempfile::tempdir().unwrap();
682 let mut spec = spec();
683 spec.tools = vec![
684 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
685 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
686 ];
687 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
688
689 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
691 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
692 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
693 let entries = json["manifests"].as_array().unwrap();
694 assert_eq!(entries.len(), 2);
695 let versions: Vec<&str> = entries
696 .iter()
697 .map(|e| {
698 e["annotations"]["eu.pulseengine.tool.version"]
699 .as_str()
700 .unwrap()
701 })
702 .collect();
703 assert_eq!(versions, vec!["1.0.200", "1.0.210"]);
704 assert_ne!(
705 entries[0]["digest"], entries[1]["digest"],
706 "two versions are two artifacts"
707 );
708 }
709
710 #[test]
712 fn extensions_deposit_as_vsix_entries_at_several_versions() {
713 use crate::kind::PayloadKind;
721 let (sk, _) = generate_root_keypair();
722 let tmp = tempfile::tempdir().unwrap();
723 let mut spec = spec();
724 spec.tools = vec![
725 payload(
726 "rust-lang.rust-analyzer",
727 "0.3.2260",
728 Some(PayloadKind::Vsix),
729 None,
730 ),
731 payload(
732 "rust-lang.rust-analyzer",
733 "0.3.2300",
734 Some(PayloadKind::Vsix),
735 None,
736 ),
737 payload(
738 "vadimcn.vscode-lldb",
739 "1.11.4",
740 Some(PayloadKind::Vsix),
741 None,
742 ),
743 ];
744 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
745
746 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
747 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
748 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
749 let entries = json["manifests"].as_array().unwrap();
750 assert_eq!(entries.len(), 3, "all three extensions must be signed in");
751 for e in entries {
752 assert_eq!(
753 e["annotations"][crate::kind::ANN_KIND],
754 "vsix",
755 "every entry must carry the vsix kind in the SIGNED payload"
756 );
757 }
758 let ids: Vec<(&str, &str)> = entries
759 .iter()
760 .map(|e| {
761 (
762 e["annotations"]["eu.pulseengine.tool"].as_str().unwrap(),
763 e["annotations"]["eu.pulseengine.tool.version"]
764 .as_str()
765 .unwrap(),
766 )
767 })
768 .collect();
769 assert_eq!(
770 ids,
771 vec![
772 ("rust-lang.rust-analyzer", "0.3.2260"),
773 ("rust-lang.rust-analyzer", "0.3.2300"),
774 ("vadimcn.vscode-lldb", "1.11.4"),
775 ]
776 );
777 assert_ne!(
778 entries[0]["digest"], entries[1]["digest"],
779 "two versions of one extension are two artifacts"
780 );
781
782 let mut dup = spec;
784 dup.tools = vec![
785 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
786 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
787 ];
788 let err = deposit(&dup, &sk, "k", &tmp.path().join("e")).unwrap_err();
789 assert!(
790 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
791 if name == "pub.ext" && version == "1.0.0"),
792 "got: {err}"
793 );
794 }
795
796 #[test]
798 fn a_tool_may_not_appear_twice_under_one_name_however_its_versions_differ() {
799 use crate::kind::PayloadKind;
805 let (sk, _) = generate_root_keypair();
806 let tmp = tempfile::tempdir().unwrap();
807 let mut spec = spec();
808 spec.tools = vec![
809 payload("synth", "0.45.0", Some(PayloadKind::Tool), None),
810 payload("synth", "0.46.0", None, None), ];
812 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
813 let msg = err.to_string();
814 assert!(
815 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
816 "got: {err}"
817 );
818 assert!(msg.contains("0.45.0") && msg.contains("0.46.0"), "{msg}");
819 assert!(
823 msg.contains("any"),
824 "an unstamped entry is any-platform: {msg}"
825 );
826 let mut stamped = spec;
827 for tool in &mut stamped.tools {
828 tool.platform = Some("x86_64-unknown-linux-gnu".into());
829 }
830 let msg = deposit(&stamped, &sk, "k", &tmp.path().join("e"))
831 .unwrap_err()
832 .to_string();
833 assert!(msg.contains("x86_64-unknown-linux-gnu"), "{msg}");
834 }
835
836 #[test]
838 fn one_version_deposited_twice_is_still_refused_and_the_error_names_it() {
839 use crate::kind::PayloadKind;
844 let (sk, _) = generate_root_keypair();
845 let tmp = tempfile::tempdir().unwrap();
846 let mut spec = spec();
847 spec.tools = vec![
848 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
849 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
850 ];
851 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
852 let msg = err.to_string();
853 assert!(
854 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
855 if name == "serde" && version == "1.0.200"),
856 "got: {err}"
857 );
858 assert!(msg.contains("serde") && msg.contains("1.0.200"), "{msg}");
859 }
860
861 #[test]
863 fn platform_still_separates_identities_for_both_rules() {
864 use crate::kind::PayloadKind;
869 let (sk, _) = generate_root_keypair();
870 let tmp = tempfile::tempdir().unwrap();
871 let mut spec = spec();
872 spec.tools = vec![
873 payload("synth", "0.45.0", None, Some("aarch64-apple-darwin")),
874 payload("synth", "0.45.0", None, Some("x86_64-unknown-linux-gnu")),
875 payload(
876 "serde",
877 "1.0.200",
878 Some(PayloadKind::Crate),
879 Some("aarch64-apple-darwin"),
880 ),
881 payload(
882 "serde",
883 "1.0.200",
884 Some(PayloadKind::Crate),
885 Some("x86_64-unknown-linux-gnu"),
886 ),
887 ];
888 deposit(&spec, &sk, "k", &tmp.path().join("d")).expect(
889 "distinct platforms, distinct
890 identities",
891 );
892 }
893
894 #[test]
896 fn the_signed_digest_does_not_depend_on_the_order_versions_are_listed_in() {
897 use crate::kind::PayloadKind;
903 let (sk, _) = generate_root_keypair();
904 let tmp = tempfile::tempdir().unwrap();
905 let a_first = vec![
906 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
907 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
908 ];
909 let b_first: Vec<DepositTool> = a_first.iter().rev().cloned().collect();
910 let mut s1 = spec();
911 s1.tools = a_first;
912 let mut s2 = spec();
913 s2.tools = b_first;
914 assert_eq!(
915 deposit(&s1, &sk, "k", &tmp.path().join("a"))
916 .unwrap()
917 .digest,
918 deposit(&s2, &sk, "k", &tmp.path().join("b"))
919 .unwrap()
920 .digest,
921 );
922 }
923}