Skip to main content

appcore_update/
artifact.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: artifact.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/23 13:45:20 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use crate::{UpdateError, UpdateResult};
12use appcore_contracts::{ApplicationId, BuildId};
13use semver::{Version, VersionReq};
14use serde::{Deserialize, Serialize};
15
16/// Immutable application artifact published by an update provider.
17#[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    /// Creates and validates an immutable artifact descriptor.
36    #[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    /// Adds the Ed25519 signing key identity and detached signature.
66    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    /// Returns the application identity.
78    pub fn application_id(&self) -> &ApplicationId {
79        &self.application_id
80    }
81
82    /// Returns the semantic application version.
83    pub fn application_version(&self) -> &str {
84        &self.application_version
85    }
86
87    /// Returns the immutable build identity.
88    pub fn build_id(&self) -> &BuildId {
89        &self.build_id
90    }
91
92    /// Returns the publication channel.
93    pub fn channel(&self) -> &str {
94        &self.channel
95    }
96
97    /// Returns the runtime semantic-version requirement.
98    pub fn runtime_requirement(&self) -> &str {
99        &self.runtime_requirement
100    }
101
102    /// Returns the required distributed protocol version.
103    pub fn protocol_version(&self) -> &str {
104        &self.protocol_version
105    }
106
107    /// Returns the opaque provider reference used to fetch bytes.
108    pub fn artifact_reference(&self) -> &str {
109        &self.artifact_reference
110    }
111
112    /// Returns the lowercase SHA-256 checksum.
113    pub fn sha256(&self) -> &str {
114        &self.sha256
115    }
116
117    /// Returns the declared artifact size.
118    pub fn size_bytes(&self) -> u64 {
119        self.size_bytes
120    }
121
122    /// Returns the configured signing key identity.
123    pub fn signing_key_id(&self) -> Option<&str> {
124        self.signing_key_id.as_deref()
125    }
126
127    /// Returns the lowercase hexadecimal Ed25519 signature.
128    pub fn ed25519_signature(&self) -> Option<&str> {
129        self.ed25519_signature.as_deref()
130    }
131
132    /// Checks runtime and protocol compatibility.
133    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}