Skip to main content

appcore_update/
artifact.rs

1use crate::{UpdateError, UpdateResult};
2use appcore_contracts::{ApplicationId, BuildId};
3use semver::{Version, VersionReq};
4use serde::{Deserialize, Serialize};
5
6/// Immutable application artifact published by an update provider.
7#[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    /// Creates and validates an immutable artifact descriptor.
26    #[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    /// Adds the Ed25519 signing key identity and detached signature.
56    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    /// Returns the application identity.
68    pub fn application_id(&self) -> &ApplicationId {
69        &self.application_id
70    }
71
72    /// Returns the semantic application version.
73    pub fn application_version(&self) -> &str {
74        &self.application_version
75    }
76
77    /// Returns the immutable build identity.
78    pub fn build_id(&self) -> &BuildId {
79        &self.build_id
80    }
81
82    /// Returns the publication channel.
83    pub fn channel(&self) -> &str {
84        &self.channel
85    }
86
87    /// Returns the runtime semantic-version requirement.
88    pub fn runtime_requirement(&self) -> &str {
89        &self.runtime_requirement
90    }
91
92    /// Returns the required distributed protocol version.
93    pub fn protocol_version(&self) -> &str {
94        &self.protocol_version
95    }
96
97    /// Returns the opaque provider reference used to fetch bytes.
98    pub fn artifact_reference(&self) -> &str {
99        &self.artifact_reference
100    }
101
102    /// Returns the lowercase SHA-256 checksum.
103    pub fn sha256(&self) -> &str {
104        &self.sha256
105    }
106
107    /// Returns the declared artifact size.
108    pub fn size_bytes(&self) -> u64 {
109        self.size_bytes
110    }
111
112    /// Returns the configured signing key identity.
113    pub fn signing_key_id(&self) -> Option<&str> {
114        self.signing_key_id.as_deref()
115    }
116
117    /// Returns the lowercase hexadecimal Ed25519 signature.
118    pub fn ed25519_signature(&self) -> Option<&str> {
119        self.ed25519_signature.as_deref()
120    }
121
122    /// Checks runtime and protocol compatibility.
123    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}