appcore_update/
artifact.rs1use crate::{UpdateError, UpdateResult};
2use appcore_contracts::{ApplicationId, BuildId};
3use semver::{Version, VersionReq};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct ArtifactDescriptor {
9 application_id: ApplicationId,
10 application_version: String,
11 build_id: BuildId,
12 channel: String,
13 runtime_requirement: String,
14 protocol_version: String,
15 artifact_reference: String,
16 sha256: String,
17 size_bytes: u64,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 signing_key_id: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 ed25519_signature: Option<String>,
22}
23
24impl ArtifactDescriptor {
25 #[allow(clippy::too_many_arguments)]
27 pub fn new(
28 application_id: ApplicationId,
29 application_version: impl Into<String>,
30 build_id: BuildId,
31 channel: impl Into<String>,
32 runtime_requirement: impl Into<String>,
33 protocol_version: impl Into<String>,
34 artifact_reference: impl Into<String>,
35 sha256: impl Into<String>,
36 size_bytes: u64,
37 ) -> UpdateResult<Self> {
38 let descriptor = Self {
39 application_id,
40 application_version: application_version.into(),
41 build_id,
42 channel: channel.into(),
43 runtime_requirement: runtime_requirement.into(),
44 protocol_version: protocol_version.into(),
45 artifact_reference: artifact_reference.into(),
46 sha256: sha256.into(),
47 size_bytes,
48 signing_key_id: None,
49 ed25519_signature: None,
50 };
51 descriptor.validate()?;
52 Ok(descriptor)
53 }
54
55 pub fn with_ed25519_signature(
57 mut self,
58 signing_key_id: impl Into<String>,
59 signature: impl Into<String>,
60 ) -> UpdateResult<Self> {
61 self.signing_key_id = Some(signing_key_id.into());
62 self.ed25519_signature = Some(signature.into());
63 self.validate()?;
64 Ok(self)
65 }
66
67 pub fn application_id(&self) -> &ApplicationId {
69 &self.application_id
70 }
71
72 pub fn application_version(&self) -> &str {
74 &self.application_version
75 }
76
77 pub fn build_id(&self) -> &BuildId {
79 &self.build_id
80 }
81
82 pub fn channel(&self) -> &str {
84 &self.channel
85 }
86
87 pub fn runtime_requirement(&self) -> &str {
89 &self.runtime_requirement
90 }
91
92 pub fn protocol_version(&self) -> &str {
94 &self.protocol_version
95 }
96
97 pub fn artifact_reference(&self) -> &str {
99 &self.artifact_reference
100 }
101
102 pub fn sha256(&self) -> &str {
104 &self.sha256
105 }
106
107 pub fn size_bytes(&self) -> u64 {
109 self.size_bytes
110 }
111
112 pub fn signing_key_id(&self) -> Option<&str> {
114 self.signing_key_id.as_deref()
115 }
116
117 pub fn ed25519_signature(&self) -> Option<&str> {
119 self.ed25519_signature.as_deref()
120 }
121
122 pub fn ensure_compatible(
124 &self,
125 runtime_version: &str,
126 protocol_version: &str,
127 ) -> UpdateResult<()> {
128 let runtime = Version::parse(runtime_version).map_err(|error| {
129 UpdateError::Incompatible(format!("invalid runtime version: {error}"))
130 })?;
131 let requirement = VersionReq::parse(&self.runtime_requirement).map_err(|error| {
132 UpdateError::InvalidArtifact(format!("invalid runtime requirement: {error}"))
133 })?;
134 if !requirement.matches(&runtime) {
135 return Err(UpdateError::Incompatible(format!(
136 "runtime {runtime} does not satisfy {}",
137 self.runtime_requirement
138 )));
139 }
140 if self.protocol_version != protocol_version {
141 return Err(UpdateError::Incompatible(format!(
142 "protocol {} is required, host provides {protocol_version}",
143 self.protocol_version
144 )));
145 }
146 Ok(())
147 }
148
149 pub(crate) fn validate(&self) -> UpdateResult<()> {
150 for (name, value, max) in [
151 ("application_version", self.application_version.as_str(), 64),
152 ("channel", self.channel.as_str(), 64),
153 (
154 "runtime_requirement",
155 self.runtime_requirement.as_str(),
156 128,
157 ),
158 ("protocol_version", self.protocol_version.as_str(), 64),
159 (
160 "artifact_reference",
161 self.artifact_reference.as_str(),
162 2_048,
163 ),
164 ] {
165 if value.trim().is_empty() || value.len() > max || value.chars().any(char::is_control) {
166 return Err(UpdateError::InvalidArtifact(format!(
167 "{name} is empty, too long or contains control characters"
168 )));
169 }
170 }
171 Version::parse(&self.application_version).map_err(|error| {
172 UpdateError::InvalidArtifact(format!("invalid application version: {error}"))
173 })?;
174 VersionReq::parse(&self.runtime_requirement).map_err(|error| {
175 UpdateError::InvalidArtifact(format!("invalid runtime requirement: {error}"))
176 })?;
177 if self.size_bytes == 0 {
178 return Err(UpdateError::InvalidArtifact(
179 "size_bytes must be greater than zero".to_string(),
180 ));
181 }
182 if self.sha256.len() != 64
183 || !self
184 .sha256
185 .bytes()
186 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
187 {
188 return Err(UpdateError::InvalidArtifact(
189 "sha256 must be 64 lowercase hexadecimal characters".to_string(),
190 ));
191 }
192 match (&self.signing_key_id, &self.ed25519_signature) {
193 (None, None) => {}
194 (Some(key_id), Some(signature)) => {
195 if key_id.trim().is_empty()
196 || key_id.len() > 128
197 || key_id.chars().any(char::is_control)
198 {
199 return Err(UpdateError::InvalidArtifact(
200 "signing_key_id is empty, too long or contains control characters"
201 .to_string(),
202 ));
203 }
204 if signature.len() != 128
205 || !signature
206 .bytes()
207 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
208 {
209 return Err(UpdateError::InvalidArtifact(
210 "ed25519_signature must be 128 lowercase hexadecimal characters"
211 .to_string(),
212 ));
213 }
214 }
215 _ => {
216 return Err(UpdateError::InvalidArtifact(
217 "signing_key_id and ed25519_signature must be provided together".to_string(),
218 ));
219 }
220 }
221 Ok(())
222 }
223}