1use crate::{ContentDigest, DependencyGroup, RegistryOrigin, RegistrySourceId};
2use semver::VersionReq;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase", deny_unknown_fields)]
8pub struct RegistryPackageReference {
9 pub registry: RegistryOrigin,
10 pub namespace: String,
11 pub name: String,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct RegistryReleaseDependency {
17 pub alias: String,
18 pub package: RegistryPackageReference,
19 pub requirement: String,
20 pub group: DependencyGroup,
21 pub target: Option<String>,
22 pub optional: bool,
23 pub default_features: bool,
24 pub features: Vec<String>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct RegistryReleaseMetadata {
30 pub singleton: bool,
31 pub runmat_requirement: Option<String>,
32 pub dependencies: Vec<RegistryReleaseDependency>,
33 pub features: BTreeMap<String, Vec<String>>,
34 pub required_capabilities: Vec<String>,
35 pub optional_capabilities: Vec<String>,
36 pub readme_digest: Option<String>,
37 pub license: Option<String>,
38 #[serde(default)]
39 pub encryption: Option<super::EncryptedArtifactMetadata>,
40 #[serde(default)]
41 pub supply_chain: Option<super::RegistryReleaseSupplyChain>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46pub struct RegistryCandidateRecord {
47 pub source: RegistrySourceId,
48 pub metadata: RegistryReleaseMetadata,
49 pub yanked: bool,
50}
51
52impl RegistryReleaseMetadata {
53 pub fn compute_digest(&self, source: &RegistrySourceId) -> Result<ContentDigest, String> {
54 self.validate_semantics()?;
55 #[derive(Serialize)]
56 #[serde(rename_all = "camelCase")]
57 struct CanonicalPackage<'a> {
58 registry: &'a str,
59 namespace: &'a str,
60 name: &'a str,
61 }
62 #[derive(Serialize)]
63 #[serde(rename_all = "camelCase")]
64 struct CanonicalMetadata<'a> {
65 singleton: bool,
66 runmat_requirement: &'a Option<String>,
67 dependencies: &'a [RegistryReleaseDependency],
68 features: &'a BTreeMap<String, Vec<String>>,
69 required_capabilities: &'a [String],
70 optional_capabilities: &'a [String],
71 readme_digest: &'a Option<String>,
72 license: &'a Option<String>,
73 }
74 #[derive(Serialize)]
75 #[serde(rename_all = "camelCase")]
76 struct Canonical<'a> {
77 format: &'static str,
78 package: CanonicalPackage<'a>,
79 version: String,
80 artifact_digest: String,
81 tree_digest: String,
82 metadata: CanonicalMetadata<'a>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 encryption: Option<&'a super::EncryptedArtifactMetadata>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 supply_chain: Option<&'a super::RegistryReleaseSupplyChain>,
87 }
88 if let Some(encryption) = &self.encryption {
89 encryption.validate(source)?;
90 }
91 let bytes = serde_json::to_vec(&Canonical {
92 format: if self.encryption.is_some() {
93 "runmat-registry-release-v3"
94 } else if self.supply_chain.is_some() {
95 "runmat-registry-release-v2"
96 } else {
97 "runmat-registry-release-v1"
98 },
99 package: CanonicalPackage {
100 registry: source.registry_origin.as_str(),
101 namespace: source.package.organization(),
102 name: source.package.name(),
103 },
104 version: source.version.to_string(),
105 artifact_digest: source.artifact_digest.to_string(),
106 tree_digest: source.tree_digest.to_string(),
107 metadata: CanonicalMetadata {
108 singleton: self.singleton,
109 runmat_requirement: &self.runmat_requirement,
110 dependencies: &self.dependencies,
111 features: &self.features,
112 required_capabilities: &self.required_capabilities,
113 optional_capabilities: &self.optional_capabilities,
114 readme_digest: &self.readme_digest,
115 license: &self.license,
116 },
117 encryption: self.encryption.as_ref(),
118 supply_chain: self.supply_chain.as_ref(),
119 })
120 .map_err(|error| error.to_string())?;
121 Ok(ContentDigest::sha256(bytes))
122 }
123
124 pub fn validate_source(&self, source: &RegistrySourceId) -> Result<(), String> {
125 source.validate().map_err(|error| error.to_string())?;
126 if self.compute_digest(source)? != source.release_digest {
127 return Err(
128 "registry release metadata digest differs from the exact source".to_string(),
129 );
130 }
131 Ok(())
132 }
133
134 pub fn verify_supply_chain(
135 &self,
136 package_id: &str,
137 source: &RegistrySourceId,
138 ) -> Result<(), String> {
139 self.validate_source(source)?;
140 if let Some(supply_chain) = &self.supply_chain {
141 supply_chain.verify(package_id)?;
142 }
143 Ok(())
144 }
145
146 fn validate_semantics(&self) -> Result<(), String> {
147 if self.dependencies.len() > 512
148 || self.features.len() > 512
149 || self.required_capabilities.len() > 128
150 || self.optional_capabilities.len() > 128
151 {
152 return Err("registry release metadata exceeds semantic limits".to_string());
153 }
154 if self
155 .runmat_requirement
156 .as_deref()
157 .map(VersionReq::parse)
158 .transpose()
159 .is_err()
160 || self.dependencies.iter().any(|dependency| {
161 dependency.alias.is_empty()
162 || dependency.alias.len() > 64
163 || VersionReq::parse(&dependency.requirement).is_err()
164 || dependency
165 .target
166 .as_ref()
167 .is_some_and(|target| target.is_empty() || target.len() > 256)
168 || dependency.features.len() > 128
169 })
170 || self.features.iter().any(|(feature, requests)| {
171 feature.is_empty()
172 || feature.len() > 64
173 || requests.len() > 512
174 || requests
175 .iter()
176 .any(|request| request.is_empty() || request.len() > 129)
177 })
178 || self
179 .required_capabilities
180 .iter()
181 .chain(&self.optional_capabilities)
182 .any(|capability| capability.is_empty() || capability.len() > 64)
183 {
184 return Err("registry release metadata contains invalid semantic fields".to_string());
185 }
186 Ok(())
187 }
188}