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