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