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 pub sdk_prefix: Option<String>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
104#[serde(deny_unknown_fields)]
105pub struct RunnerSpec {
106 pub tool: String,
107 #[serde(default)]
108 pub args: Vec<String>,
109 #[serde(rename = "arg-prefix", default)]
110 pub arg_prefix: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct ToolSource {
117 pub repo: String,
119 pub release: String,
121 pub asset: String,
123 pub sha256: String,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct DepositOutcome {
131 pub digest: String,
133 pub layer: LayerId,
134 pub counter: u64,
135}
136
137#[derive(Debug, serde::Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct DepositFileSpec {
142 pub layer: String,
143 pub channel: String,
144 pub counter: u64,
145 #[serde(default, rename = "tool")]
146 pub tools: Vec<SpecTool>,
147 #[serde(default, rename = "include")]
150 pub includes: Vec<SpecInclude>,
151}
152
153#[derive(Debug, serde::Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct SpecInclude {
158 pub digest: String,
160 #[serde(default)]
162 pub realm: Option<String>,
163 #[serde(default)]
166 pub layer: Option<String>,
167}
168
169#[derive(Debug, serde::Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct SpecTool {
172 pub name: String,
173 pub version: String,
174 #[serde(default)]
175 pub platform: Option<String>,
176 pub path: String,
178 #[serde(default)]
179 pub source: Option<ToolSource>,
180 #[serde(default)]
181 pub runner: Option<RunnerSpec>,
182 #[serde(default)]
185 pub kind: Option<String>,
186 #[serde(rename = "sdk-prefix", default)]
190 pub sdk_prefix: Option<String>,
191}
192
193pub fn parse_deposit_spec(toml_text: &str) -> Result<DepositFileSpec, DepositError> {
194 toml::from_str(toml_text).map_err(|e| DepositError::Spec(e.to_string()))
195}
196
197#[derive(Debug, thiserror::Error)]
198pub enum DepositError {
199 #[error(
200 "the envelope this deposit just signed does not verify against the key that signed \
201 it — refusing to publish an artifact no consumer could accept"
202 )]
203 SelfVerifyFailed,
204 #[error(
205 "channel {channel:?} is not one a pin can name — use `qualified` or `rolling`. \
206 Signing it would produce a layer no varve.toml could ever select."
207 )]
208 BadChannel { channel: String },
209 #[error(
210 "issued-at {issued_at:?} is not an RFC 3339 timestamp (e.g. 2026-08-01T00:00:00Z). \
211 It is signed into the manifest and drives staleness and the SBOM timestamp, so it \
212 cannot be corrected afterwards."
213 )]
214 BadIssuedAt { issued_at: String },
215 #[error("deposit spec is not valid: {0}")]
216 Spec(String),
217 #[error("deposit has no tools — an empty layer is not a toolchain")]
218 NoTools,
219 #[error(
220 "tool '{name}' is deposited twice for platform {platform} (versions {first} and \
221 {second}) — a tool is DISPATCHED BY NAME (`varve run {name}`, `varve which {name}`, the \
222 argv[0] shims), so one name must resolve to exactly one binary per platform. Deposit the \
223 other version as a separate layer, or give it a distinct name."
224 )]
225 DuplicateTool {
226 name: String,
227 platform: String,
228 first: String,
229 second: String,
230 },
231 #[error(
232 "payload '{name}' version {version} is deposited twice for platform {platform} — a layer \
233 may hold several VERSIONS of one name, but not one version twice: the two would be the \
234 same payload, and only one set of bytes could land."
235 )]
236 DuplicatePayload {
237 name: String,
238 version: String,
239 platform: String,
240 },
241 #[error(
242 "payload '{name}' is kind {kind} and carries `sdk-prefix` — only a tree payload (sdk) is \
243 relocated, so the prefix would be signed into the manifest, ignored by every adapter, \
244 and believed. Drop it, or deposit this payload as kind = \"sdk\"."
245 )]
246 SdkPrefixOnNonTree { name: String, kind: String },
247 #[error(
248 "sdk '{name}' version {version} declares no `sdk-prefix` — the absolute path it was \
249 BUILT for. Without it `varve export-sdk` has no relocation budget and no path to \
250 patch, so the layer would install and verify and could never be exported. Add \
251 `sdk-prefix = \"/opt/poky/4.0\"` (the path the SDK was built for) to the [[tool]] table."
252 )]
253 SdkPrefixMissing { name: String, version: String },
254 #[error(
255 "sdk '{name}' declares sdk-prefix {prefix:?}, which is not absolute — the prefix is the \
256 path PATCHED INTO the SDK's binaries, and a relative one there would resolve against \
257 whatever directory a compiler happens to run in"
258 )]
259 SdkPrefixNotAbsolute { name: String, prefix: String },
260 #[error(transparent)]
261 Sign(#[from] VerifyError),
262 #[error("io error at {path}: {source}")]
263 Io {
264 path: String,
265 #[source]
266 source: std::io::Error,
267 },
268}
269
270fn platform_label(platform: Option<&String>) -> String {
272 platform
273 .cloned()
274 .unwrap_or_else(|| "any (unstamped)".to_string())
275}
276
277fn check_identities(tools: &[&DepositTool]) -> Result<(), DepositError> {
288 let mut dispatched: std::collections::BTreeMap<(&str, Option<&String>), &str> =
290 std::collections::BTreeMap::new();
291 let mut held: std::collections::BTreeSet<(&str, &str, Option<&String>)> =
293 std::collections::BTreeSet::new();
294 for tool in tools {
295 let dispatchable = tool.kind.unwrap_or_default().is_dispatchable();
296 let platform = tool.platform.as_ref();
297 if dispatchable {
298 if let Some(first) = dispatched.insert((&tool.name, platform), &tool.version) {
299 return Err(DepositError::DuplicateTool {
300 name: tool.name.clone(),
301 platform: platform_label(platform),
302 first: first.to_string(),
303 second: tool.version.clone(),
304 });
305 }
306 } else if !held.insert((&tool.name, &tool.version, platform)) {
307 return Err(DepositError::DuplicatePayload {
308 name: tool.name.clone(),
309 version: tool.version.clone(),
310 platform: platform_label(platform),
311 });
312 }
313 }
314 Ok(())
315}
316
317fn check_sdk_prefixes(tools: &[&DepositTool]) -> Result<(), DepositError> {
326 for tool in tools {
327 let is_tree = tool.kind == Some(crate::kind::PayloadKind::Sdk);
328 match (&tool.sdk_prefix, is_tree) {
329 (Some(_), false) => {
330 return Err(DepositError::SdkPrefixOnNonTree {
331 name: tool.name.clone(),
332 kind: tool.kind.unwrap_or_default().as_str().to_string(),
333 });
334 }
335 (None, true) => {
336 return Err(DepositError::SdkPrefixMissing {
337 name: tool.name.clone(),
338 version: tool.version.clone(),
339 });
340 }
341 (Some(prefix), true) if !prefix.starts_with('/') => {
342 return Err(DepositError::SdkPrefixNotAbsolute {
343 name: tool.name.clone(),
344 prefix: prefix.clone(),
345 });
346 }
347 _ => {}
348 }
349 }
350 Ok(())
351}
352
353pub fn deposit(
355 spec: &DepositSpec,
356 signing_key: &[u8],
357 key_id: &str,
358 dest: &Path,
359) -> Result<DepositOutcome, DepositError> {
360 if spec.tools.is_empty() {
361 return Err(DepositError::NoTools);
362 }
363 let mut tools: Vec<&DepositTool> = spec.tools.iter().collect();
364 tools.sort_by(|a, b| {
368 (&a.name, &a.version, &a.platform).cmp(&(&b.name, &b.version, &b.platform))
369 });
370 check_identities(&tools)?;
371 check_sdk_prefixes(&tools)?;
372
373 let entries: Vec<serde_json::Value> = tools
378 .iter()
379 .map(|tool| {
380 let mut annotations = serde_json::Map::new();
381 annotations.insert("eu.pulseengine.tool".into(), tool.name.clone().into());
382 annotations.insert(
383 "eu.pulseengine.tool.version".into(),
384 tool.version.clone().into(),
385 );
386 if let Some(platform) = &tool.platform {
387 annotations.insert(
388 crate::platform::ANN_PLATFORM.into(),
389 platform.clone().into(),
390 );
391 }
392 if let Some(source) = &tool.source {
393 annotations.insert(
394 crate::bazel::ANN_SRC_REPO.into(),
395 source.repo.clone().into(),
396 );
397 annotations.insert(
398 crate::bazel::ANN_SRC_RELEASE.into(),
399 source.release.clone().into(),
400 );
401 annotations.insert(
402 crate::bazel::ANN_SRC_ASSET.into(),
403 source.asset.clone().into(),
404 );
405 annotations.insert(
406 crate::bazel::ANN_SRC_SHA256.into(),
407 source.sha256.clone().into(),
408 );
409 }
410 if let Some(kind) = tool.kind
414 && kind != crate::kind::PayloadKind::Tool
415 {
416 annotations.insert(crate::kind::ANN_KIND.into(), kind.as_str().into());
417 }
418 if let Some(prefix) = &tool.sdk_prefix {
423 annotations.insert(
424 crate::sdkexport::ANN_SDK_PREFIX.into(),
425 prefix.clone().into(),
426 );
427 }
428 if let Some(runner) = &tool.runner {
429 annotations.insert(crate::bazel::ANN_RUNNER.into(), runner.tool.clone().into());
430 if !runner.args.is_empty() {
431 annotations.insert(
432 crate::bazel::ANN_RUNNER_ARGS.into(),
433 runner.args.join(" ").into(),
434 );
435 }
436 if let Some(prefix) = &runner.arg_prefix {
437 annotations.insert(
438 crate::bazel::ANN_RUNNER_ARG_PREFIX.into(),
439 prefix.clone().into(),
440 );
441 }
442 }
443 serde_json::json!({
444 "mediaType": "application/vnd.oci.image.manifest.v1+json",
445 "digest": manifest_digest(&tool.bytes),
446 "size": tool.bytes.len(),
447 "annotations": annotations,
448 })
449 })
450 .collect();
451 let mut entries = entries;
456 for inc in &spec.includes {
457 let mut annotations = serde_json::Map::new();
458 annotations.insert(
459 crate::kind::ANN_KIND.into(),
460 crate::kind::PayloadKind::Layer.as_str().into(),
461 );
462 if let Some(realm) = &inc.realm {
463 annotations.insert(
464 crate::compose::ANN_INCLUDE_REALM.into(),
465 realm.clone().into(),
466 );
467 }
468 if let Some(layer) = &inc.layer {
469 annotations.insert(
470 crate::compose::ANN_INCLUDE_LAYER.into(),
471 layer.clone().into(),
472 );
473 }
474 entries.push(serde_json::json!({
475 "mediaType": "application/vnd.oci.image.index.v1+json",
476 "digest": inc.digest,
477 "size": 0,
478 "annotations": annotations,
479 }));
480 }
481 if spec.channel.parse::<crate::pin::Channel>().is_err() {
485 return Err(DepositError::BadChannel {
486 channel: spec.channel.clone(),
487 });
488 }
489 if !is_rfc3339(&spec.issued_at) {
490 return Err(DepositError::BadIssuedAt {
491 issued_at: spec.issued_at.clone(),
492 });
493 }
494 let payload_json = serde_json::json!({
495 "schemaVersion": 2,
496 "mediaType": "application/vnd.oci.image.index.v1+json",
497 "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
498 "annotations": {
499 "eu.pulseengine.varve.layer": spec.layer.to_string(),
500 "eu.pulseengine.varve.line": spec.layer.line().to_string(),
501 "eu.pulseengine.varve.channel": spec.channel,
502 "eu.pulseengine.varve.counter": spec.counter.to_string(),
503 "org.opencontainers.image.created": spec.issued_at,
504 },
505 "manifests": entries,
506 });
507 let payload = serde_json::to_vec_pretty(&payload_json).expect("payload serializes");
508 let envelope = sign_layer_manifest(&payload, signing_key, key_id)?;
509 let public = &signing_key[32..];
513 match crate::verify::dsse_verify_typed(
514 envelope.as_bytes(),
515 crate::verify::LAYER_PAYLOAD_TYPE,
516 public,
517 ) {
518 Ok(back) if back == payload => {}
519 _ => return Err(DepositError::SelfVerifyFailed),
520 }
521
522 let blobs: Vec<(String, Vec<u8>)> = tools
523 .iter()
524 .map(|tool| (manifest_digest(&tool.bytes), tool.bytes.clone()))
525 .collect();
526 crate::archive::write_oci_layout(
527 &payload,
528 envelope.as_bytes(),
529 &blobs,
530 &spec.layer.to_string(),
531 &spec.channel,
532 None,
535 dest,
536 )
537 .map_err(|(path, source)| DepositError::Io { path, source })?;
538
539 Ok(DepositOutcome {
540 digest: manifest_digest(&payload),
541 layer: spec.layer.clone(),
542 counter: spec.counter,
543 })
544}
545
546#[cfg(test)]
547mod producer_tests {
548 use super::*;
549
550 #[test]
552 fn a_channel_no_pin_could_name_is_refused_before_signing() {
553 assert!("qualified".parse::<crate::pin::Channel>().is_ok());
557 assert!("rolling".parse::<crate::pin::Channel>().is_ok());
558 for bad in ["stable", "Qualified", "", "beta"] {
559 assert!(
560 bad.parse::<crate::pin::Channel>().is_err(),
561 "{bad} must be refused"
562 );
563 }
564 }
565
566 #[test]
568 fn issued_at_must_be_a_timestamp_not_merely_a_date() {
569 assert!(is_rfc3339("2026-08-01T00:00:00Z"));
573 assert!(is_rfc3339("2026-08-01T12:34:56.789Z"));
574 assert!(is_rfc3339("2026-08-01T12:34:56+02:00"));
575 for bad in [
576 "2026-10-01",
577 "not-a-date",
578 "",
579 "2026-08-01T",
580 "2026-13-01T00:00:00Z",
581 ] {
582 assert!(!is_rfc3339(bad), "{bad} must be refused");
583 }
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::archive::OciLayoutSource;
591 use crate::install::{InstallPolicy, install};
592 use crate::pin::Pin;
593 use crate::rollback::HighWaterMarks;
594 use crate::store::Store;
595 use crate::verify::{PinnedKeyVerifier, generate_root_keypair};
596
597 fn spec() -> DepositSpec {
598 DepositSpec {
599 includes: Vec::new(),
600 layer: "2026.08.0".parse().unwrap(),
601 channel: "qualified".into(),
602 counter: 1,
603 issued_at: "2026-08-07T00:00:00Z".into(),
604 tools: vec![
605 DepositTool {
606 name: "synth".into(),
607 version: "0.45.0".into(),
608 platform: None,
609 bytes: b"synth-bytes".to_vec(),
610 source: None,
611 runner: None,
612 kind: None,
613 sdk_prefix: None,
614 },
615 DepositTool {
616 name: "rivet".into(),
617 version: "0.32.0".into(),
618 platform: None,
619 bytes: b"rivet-bytes".to_vec(),
620 source: None,
621 runner: None,
622 kind: None,
623 sdk_prefix: None,
624 },
625 ],
626 }
627 }
628
629 #[test]
631 fn a_deposit_round_trips_through_the_standard_install_pipeline() {
632 let (sk, pk) = generate_root_keypair();
633 let tmp = tempfile::tempdir().unwrap();
634 let dest = tmp.path().join("deposit");
635 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
636 assert_eq!(outcome.layer.to_string(), "2026.08.0");
637
638 assert!(dest.join("oci-layout").is_file());
640 assert!(dest.join("index.json").is_file());
641
642 let pin = Pin::parse(
644 "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\n",
645 "varve.toml",
646 )
647 .unwrap();
648 let root = tmp.path().join("fresh");
649 let store = Store::at(&root);
650 let mut marks = HighWaterMarks::load(&root).unwrap();
651 let verifier = PinnedKeyVerifier::from_public_key_bytes(&pk).unwrap();
652 let policy = InstallPolicy {
653 index: None,
654 now: "2026-08-07T00:00:00Z",
655 staleness_threshold_days: 90,
656 platform: "test-platform",
657 };
658 let installed = install(
659 &pin,
660 &OciLayoutSource::at(&dest),
661 &verifier,
662 &store,
663 &mut marks,
664 &policy,
665 )
666 .unwrap();
667 assert_eq!(installed.digest, outcome.digest);
668 assert_eq!(installed.counter, 1);
669 let entry = store.get(&installed.digest).unwrap().unwrap();
670 let checked =
671 crate::reverify::verify_installed(&store, &entry, &verifier, "test-platform").unwrap();
672 assert_eq!(checked, 2);
673 }
674
675 #[test]
677 fn the_payload_records_layer_line_channel_counter_and_tool_versions() {
678 let (sk, _) = generate_root_keypair();
679 let tmp = tempfile::tempdir().unwrap();
680 let dest = tmp.path().join("deposit");
681 let outcome = deposit(&spec(), &sk, "varve-root-1", &dest).unwrap();
682 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
683 let payload = std::fs::read(dest.join("blobs/sha256").join(hex)).unwrap();
684 let json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
685 let ann = &json["annotations"];
686 assert_eq!(ann["eu.pulseengine.varve.layer"], "2026.08.0");
687 assert_eq!(ann["eu.pulseengine.varve.line"], "2026.08");
688 assert_eq!(ann["eu.pulseengine.varve.channel"], "qualified");
689 assert_eq!(ann["eu.pulseengine.varve.counter"], "1");
690 assert_eq!(
691 ann["org.opencontainers.image.created"],
692 "2026-08-07T00:00:00Z"
693 );
694 let entries = json["manifests"].as_array().unwrap();
695 assert_eq!(entries.len(), 2);
696 assert!(entries.iter().any(|e| {
697 e["annotations"]["eu.pulseengine.tool"] == "synth"
698 && e["annotations"]["eu.pulseengine.tool.version"] == "0.45.0"
699 }));
700 }
701
702 #[test]
704 fn identical_specs_deposit_identical_digests() {
705 let (sk, _) = generate_root_keypair();
706 let tmp = tempfile::tempdir().unwrap();
707 let a = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("a")).unwrap();
708 let b = deposit(&spec(), &sk, "varve-root-1", &tmp.path().join("b")).unwrap();
709 assert_eq!(
710 a.digest, b.digest,
711 "the payload is deterministic — the digest IS the identity"
712 );
713 }
714
715 #[test]
717 fn empty_and_duplicate_tool_lists_are_refused() {
718 let (sk, _) = generate_root_keypair();
719 let tmp = tempfile::tempdir().unwrap();
720 let mut empty = spec();
721 empty.tools.clear();
722 assert!(matches!(
723 deposit(&empty, &sk, "k", &tmp.path().join("x")).unwrap_err(),
724 DepositError::NoTools
725 ));
726 let mut dup = spec();
727 dup.tools[1].name = "synth".into();
728 let err = deposit(&dup, &sk, "k", &tmp.path().join("y")).unwrap_err();
729 assert!(
730 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
731 "got: {err}"
732 );
733 }
734
735 fn payload(
737 name: &str,
738 version: &str,
739 kind: Option<crate::kind::PayloadKind>,
740 platform: Option<&str>,
741 ) -> DepositTool {
742 DepositTool {
743 name: name.into(),
744 version: version.into(),
745 platform: platform.map(str::to_string),
746 bytes: format!("{name}-{version}-bytes").into_bytes(),
747 source: None,
748 runner: None,
749 kind,
750 sdk_prefix: None,
751 }
752 }
753
754 #[test]
756 fn two_versions_of_one_crate_are_a_layer_not_a_duplicate() {
757 use crate::kind::PayloadKind;
763 let (sk, _) = generate_root_keypair();
764 let tmp = tempfile::tempdir().unwrap();
765 let mut spec = spec();
766 spec.tools = vec![
767 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
768 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
769 ];
770 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
771
772 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
774 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
775 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
776 let entries = json["manifests"].as_array().unwrap();
777 assert_eq!(entries.len(), 2);
778 let versions: Vec<&str> = entries
779 .iter()
780 .map(|e| {
781 e["annotations"]["eu.pulseengine.tool.version"]
782 .as_str()
783 .unwrap()
784 })
785 .collect();
786 assert_eq!(versions, vec!["1.0.200", "1.0.210"]);
787 assert_ne!(
788 entries[0]["digest"], entries[1]["digest"],
789 "two versions are two artifacts"
790 );
791 }
792
793 #[test]
795 fn extensions_deposit_as_vsix_entries_at_several_versions() {
796 use crate::kind::PayloadKind;
804 let (sk, _) = generate_root_keypair();
805 let tmp = tempfile::tempdir().unwrap();
806 let mut spec = spec();
807 spec.tools = vec![
808 payload(
809 "rust-lang.rust-analyzer",
810 "0.3.2260",
811 Some(PayloadKind::Vsix),
812 None,
813 ),
814 payload(
815 "rust-lang.rust-analyzer",
816 "0.3.2300",
817 Some(PayloadKind::Vsix),
818 None,
819 ),
820 payload(
821 "vadimcn.vscode-lldb",
822 "1.11.4",
823 Some(PayloadKind::Vsix),
824 None,
825 ),
826 ];
827 let outcome = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap();
828
829 let hex = outcome.digest.strip_prefix("sha256:").unwrap();
830 let payload_bytes = std::fs::read(tmp.path().join("d/blobs/sha256").join(hex)).unwrap();
831 let json: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
832 let entries = json["manifests"].as_array().unwrap();
833 assert_eq!(entries.len(), 3, "all three extensions must be signed in");
834 for e in entries {
835 assert_eq!(
836 e["annotations"][crate::kind::ANN_KIND],
837 "vsix",
838 "every entry must carry the vsix kind in the SIGNED payload"
839 );
840 }
841 let ids: Vec<(&str, &str)> = entries
842 .iter()
843 .map(|e| {
844 (
845 e["annotations"]["eu.pulseengine.tool"].as_str().unwrap(),
846 e["annotations"]["eu.pulseengine.tool.version"]
847 .as_str()
848 .unwrap(),
849 )
850 })
851 .collect();
852 assert_eq!(
853 ids,
854 vec![
855 ("rust-lang.rust-analyzer", "0.3.2260"),
856 ("rust-lang.rust-analyzer", "0.3.2300"),
857 ("vadimcn.vscode-lldb", "1.11.4"),
858 ]
859 );
860 assert_ne!(
861 entries[0]["digest"], entries[1]["digest"],
862 "two versions of one extension are two artifacts"
863 );
864
865 let mut dup = spec;
867 dup.tools = vec![
868 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
869 payload("pub.ext", "1.0.0", Some(PayloadKind::Vsix), None),
870 ];
871 let err = deposit(&dup, &sk, "k", &tmp.path().join("e")).unwrap_err();
872 assert!(
873 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
874 if name == "pub.ext" && version == "1.0.0"),
875 "got: {err}"
876 );
877 }
878
879 #[test]
881 fn a_tool_may_not_appear_twice_under_one_name_however_its_versions_differ() {
882 use crate::kind::PayloadKind;
888 let (sk, _) = generate_root_keypair();
889 let tmp = tempfile::tempdir().unwrap();
890 let mut spec = spec();
891 spec.tools = vec![
892 payload("synth", "0.45.0", Some(PayloadKind::Tool), None),
893 payload("synth", "0.46.0", None, None), ];
895 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
896 let msg = err.to_string();
897 assert!(
898 matches!(&err, DepositError::DuplicateTool { name, .. } if name == "synth"),
899 "got: {err}"
900 );
901 assert!(msg.contains("0.45.0") && msg.contains("0.46.0"), "{msg}");
902 assert!(
906 msg.contains("any"),
907 "an unstamped entry is any-platform: {msg}"
908 );
909 let mut stamped = spec;
910 for tool in &mut stamped.tools {
911 tool.platform = Some("x86_64-unknown-linux-gnu".into());
912 }
913 let msg = deposit(&stamped, &sk, "k", &tmp.path().join("e"))
914 .unwrap_err()
915 .to_string();
916 assert!(msg.contains("x86_64-unknown-linux-gnu"), "{msg}");
917 }
918
919 #[test]
921 fn one_version_deposited_twice_is_still_refused_and_the_error_names_it() {
922 use crate::kind::PayloadKind;
927 let (sk, _) = generate_root_keypair();
928 let tmp = tempfile::tempdir().unwrap();
929 let mut spec = spec();
930 spec.tools = vec![
931 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
932 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
933 ];
934 let err = deposit(&spec, &sk, "k", &tmp.path().join("d")).unwrap_err();
935 let msg = err.to_string();
936 assert!(
937 matches!(&err, DepositError::DuplicatePayload { name, version, .. }
938 if name == "serde" && version == "1.0.200"),
939 "got: {err}"
940 );
941 assert!(msg.contains("serde") && msg.contains("1.0.200"), "{msg}");
942 }
943
944 #[test]
946 fn platform_still_separates_identities_for_both_rules() {
947 use crate::kind::PayloadKind;
952 let (sk, _) = generate_root_keypair();
953 let tmp = tempfile::tempdir().unwrap();
954 let mut spec = spec();
955 spec.tools = vec![
956 payload("synth", "0.45.0", None, Some("aarch64-apple-darwin")),
957 payload("synth", "0.45.0", None, Some("x86_64-unknown-linux-gnu")),
958 payload(
959 "serde",
960 "1.0.200",
961 Some(PayloadKind::Crate),
962 Some("aarch64-apple-darwin"),
963 ),
964 payload(
965 "serde",
966 "1.0.200",
967 Some(PayloadKind::Crate),
968 Some("x86_64-unknown-linux-gnu"),
969 ),
970 ];
971 deposit(&spec, &sk, "k", &tmp.path().join("d")).expect(
972 "distinct platforms, distinct
973 identities",
974 );
975 }
976
977 #[test]
979 fn the_signed_digest_does_not_depend_on_the_order_versions_are_listed_in() {
980 use crate::kind::PayloadKind;
986 let (sk, _) = generate_root_keypair();
987 let tmp = tempfile::tempdir().unwrap();
988 let a_first = vec![
989 payload("serde", "1.0.200", Some(PayloadKind::Crate), None),
990 payload("serde", "1.0.210", Some(PayloadKind::Crate), None),
991 ];
992 let b_first: Vec<DepositTool> = a_first.iter().rev().cloned().collect();
993 let mut s1 = spec();
994 s1.tools = a_first;
995 let mut s2 = spec();
996 s2.tools = b_first;
997 assert_eq!(
998 deposit(&s1, &sk, "k", &tmp.path().join("a"))
999 .unwrap()
1000 .digest,
1001 deposit(&s2, &sk, "k", &tmp.path().join("b"))
1002 .unwrap()
1003 .digest,
1004 );
1005 }
1006}