1use crate::{UpdateError, UpdateResult};
14use appcore_contracts::{ApplicationId, BuildId};
15use semver::{Version, VersionReq};
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct ArtifactTarget {
21 os: String,
22 architecture: String,
23 format: String,
24}
25
26impl ArtifactTarget {
27 pub fn new(
29 os: impl Into<String>,
30 architecture: impl Into<String>,
31 format: impl Into<String>,
32 ) -> UpdateResult<Self> {
33 let target = Self {
34 os: os.into(),
35 architecture: architecture.into(),
36 format: format.into(),
37 };
38 target.validate()?;
39 Ok(target)
40 }
41
42 pub fn os(&self) -> &str {
44 &self.os
45 }
46
47 pub fn architecture(&self) -> &str {
49 &self.architecture
50 }
51
52 pub fn format(&self) -> &str {
54 &self.format
55 }
56
57 fn validate(&self) -> UpdateResult<()> {
58 for (name, value) in [
59 ("target.os", self.os.as_str()),
60 ("target.architecture", self.architecture.as_str()),
61 ("target.format", self.format.as_str()),
62 ] {
63 if value.trim().is_empty() || value.len() > 64 || value.chars().any(char::is_control) {
64 return Err(UpdateError::InvalidArtifact(format!(
65 "{name} is empty, too long or contains control characters"
66 )));
67 }
68 }
69 Ok(())
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct ArtifactDescriptor {
76 application_id: ApplicationId,
77 application_version: String,
78 build_id: BuildId,
79 channel: String,
80 runtime_requirement: String,
81 protocol_version: String,
82 artifact_reference: String,
83 sha256: String,
84 size_bytes: u64,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 signing_key_id: Option<String>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 ed25519_signature: Option<String>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 target: Option<ArtifactTarget>,
91}
92
93impl ArtifactDescriptor {
94 #[allow(clippy::too_many_arguments)]
96 pub fn new(
97 application_id: ApplicationId,
98 application_version: impl Into<String>,
99 build_id: BuildId,
100 channel: impl Into<String>,
101 runtime_requirement: impl Into<String>,
102 protocol_version: impl Into<String>,
103 artifact_reference: impl Into<String>,
104 sha256: impl Into<String>,
105 size_bytes: u64,
106 ) -> UpdateResult<Self> {
107 let descriptor = Self {
108 application_id,
109 application_version: application_version.into(),
110 build_id,
111 channel: channel.into(),
112 runtime_requirement: runtime_requirement.into(),
113 protocol_version: protocol_version.into(),
114 artifact_reference: artifact_reference.into(),
115 sha256: sha256.into(),
116 size_bytes,
117 signing_key_id: None,
118 ed25519_signature: None,
119 target: None,
120 };
121 descriptor.validate()?;
122 Ok(descriptor)
123 }
124
125 pub fn with_ed25519_signature(
127 mut self,
128 signing_key_id: impl Into<String>,
129 signature: impl Into<String>,
130 ) -> UpdateResult<Self> {
131 self.signing_key_id = Some(signing_key_id.into());
132 self.ed25519_signature = Some(signature.into());
133 self.validate()?;
134 Ok(self)
135 }
136
137 pub fn with_target(mut self, target: ArtifactTarget) -> UpdateResult<Self> {
139 if self.signing_key_id.is_some() || self.ed25519_signature.is_some() {
140 return Err(UpdateError::InvalidArtifact(
141 "target must be set before the descriptor is signed".to_string(),
142 ));
143 }
144 target.validate()?;
145 self.target = Some(target);
146 Ok(self)
147 }
148
149 pub fn application_id(&self) -> &ApplicationId {
151 &self.application_id
152 }
153
154 pub fn application_version(&self) -> &str {
156 &self.application_version
157 }
158
159 pub fn build_id(&self) -> &BuildId {
161 &self.build_id
162 }
163
164 pub fn channel(&self) -> &str {
166 &self.channel
167 }
168
169 pub fn runtime_requirement(&self) -> &str {
171 &self.runtime_requirement
172 }
173
174 pub fn protocol_version(&self) -> &str {
176 &self.protocol_version
177 }
178
179 pub fn artifact_reference(&self) -> &str {
181 &self.artifact_reference
182 }
183
184 pub fn sha256(&self) -> &str {
186 &self.sha256
187 }
188
189 pub fn size_bytes(&self) -> u64 {
191 self.size_bytes
192 }
193
194 pub fn signing_key_id(&self) -> Option<&str> {
196 self.signing_key_id.as_deref()
197 }
198
199 pub fn ed25519_signature(&self) -> Option<&str> {
201 self.ed25519_signature.as_deref()
202 }
203
204 pub fn target(&self) -> Option<&ArtifactTarget> {
206 self.target.as_ref()
207 }
208
209 pub fn ensure_compatible(
211 &self,
212 runtime_version: &str,
213 protocol_version: &str,
214 ) -> UpdateResult<()> {
215 let runtime = Version::parse(runtime_version).map_err(|error| {
216 UpdateError::Incompatible(format!("invalid runtime version: {error}"))
217 })?;
218 let requirement = VersionReq::parse(&self.runtime_requirement).map_err(|error| {
219 UpdateError::InvalidArtifact(format!("invalid runtime requirement: {error}"))
220 })?;
221 if !requirement.matches(&runtime) {
222 return Err(UpdateError::Incompatible(format!(
223 "runtime {runtime} does not satisfy {}",
224 self.runtime_requirement
225 )));
226 }
227 if self.protocol_version != protocol_version {
228 return Err(UpdateError::Incompatible(format!(
229 "protocol {} is required, host provides {protocol_version}",
230 self.protocol_version
231 )));
232 }
233 Ok(())
234 }
235
236 pub fn validate(&self) -> UpdateResult<()> {
238 for (name, value, max) in [
239 ("application_version", self.application_version.as_str(), 64),
240 ("channel", self.channel.as_str(), 64),
241 (
242 "runtime_requirement",
243 self.runtime_requirement.as_str(),
244 128,
245 ),
246 ("protocol_version", self.protocol_version.as_str(), 64),
247 (
248 "artifact_reference",
249 self.artifact_reference.as_str(),
250 2_048,
251 ),
252 ] {
253 if value.trim().is_empty() || value.len() > max || value.chars().any(char::is_control) {
254 return Err(UpdateError::InvalidArtifact(format!(
255 "{name} is empty, too long or contains control characters"
256 )));
257 }
258 }
259 Version::parse(&self.application_version).map_err(|error| {
260 UpdateError::InvalidArtifact(format!("invalid application version: {error}"))
261 })?;
262 VersionReq::parse(&self.runtime_requirement).map_err(|error| {
263 UpdateError::InvalidArtifact(format!("invalid runtime requirement: {error}"))
264 })?;
265 if self.size_bytes == 0 {
266 return Err(UpdateError::InvalidArtifact(
267 "size_bytes must be greater than zero".to_string(),
268 ));
269 }
270 if self.sha256.len() != 64
271 || !self
272 .sha256
273 .bytes()
274 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
275 {
276 return Err(UpdateError::InvalidArtifact(
277 "sha256 must be 64 lowercase hexadecimal characters".to_string(),
278 ));
279 }
280 match (&self.signing_key_id, &self.ed25519_signature) {
281 (None, None) => {}
282 (Some(key_id), Some(signature)) => {
283 if key_id.trim().is_empty()
284 || key_id.len() > 128
285 || key_id.chars().any(char::is_control)
286 {
287 return Err(UpdateError::InvalidArtifact(
288 "signing_key_id is empty, too long or contains control characters"
289 .to_string(),
290 ));
291 }
292 if signature.len() != 128
293 || !signature
294 .bytes()
295 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
296 {
297 return Err(UpdateError::InvalidArtifact(
298 "ed25519_signature must be 128 lowercase hexadecimal characters"
299 .to_string(),
300 ));
301 }
302 }
303 _ => {
304 return Err(UpdateError::InvalidArtifact(
305 "signing_key_id and ed25519_signature must be provided together".to_string(),
306 ));
307 }
308 }
309 if let Some(target) = &self.target {
310 target.validate()?;
311 }
312 Ok(())
313 }
314}