Skip to main content

appcore_update/
provider.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: provider.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 13:24:05 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Defines bounded provider contracts and behavior for this crate.
12
13use crate::filesystem::{open_regular_file, read_regular_file_bounded};
14use crate::{ArtifactDescriptor, UpdateError, UpdateResult};
15use appcore_contracts::ApplicationId;
16use appcore_provider::{
17    ProviderContext, ProviderError, ProviderFactory, ProviderResult, ProviderRole, SecretProvider,
18};
19use semver::Version;
20use serde::de::{SeqAccess, Visitor};
21use serde::Deserializer as _;
22use std::fmt;
23use std::io::{BufReader, Read};
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26
27pub(crate) const FILE_UPDATE_INDEX_MAX_BYTES: usize = 1_048_576;
28const FILE_UPDATE_INDEX_BUFFER_BYTES: usize = 16 * 1024;
29
30/// Query used to select an update candidate.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct UpdateRequest {
33    /// Installed application identity.
34    pub application_id: ApplicationId,
35    /// Current semantic application version.
36    pub current_version: String,
37    /// Selected update channel.
38    pub channel: String,
39}
40
41/// Provider contract for listing and fetching opaque application artifacts.
42pub trait UpdateProvider: Send + Sync {
43    /// Returns the newest eligible artifact, or `None` when no update exists.
44    fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
45
46    /// Fetches complete artifact bytes while respecting `max_bytes`.
47    fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
48}
49
50/// Shared update provider interface produced by deployment factories.
51pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
52
53/// Provider ID for the local-first JSON index and file artifact adapter.
54pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
55
56/// Local-first update provider backed by a bounded JSON artifact index.
57#[derive(Debug, Clone)]
58pub struct FileUpdateProvider {
59    index_path: PathBuf,
60}
61
62impl FileUpdateProvider {
63    /// Creates a provider from an installation-owned index path.
64    pub fn new(index_path: impl Into<PathBuf>) -> Self {
65        Self {
66            index_path: index_path.into(),
67        }
68    }
69
70    fn latest_from_index(
71        &self,
72        request: &UpdateRequest,
73        current: &Version,
74    ) -> UpdateResult<Option<ArtifactDescriptor>> {
75        let file = open_regular_file(&self.index_path)
76            .map_err(|error| UpdateError::Provider(error.to_string()))?;
77        let declared_length = file
78            .metadata()
79            .map_err(|error| UpdateError::Provider(error.to_string()))?
80            .len();
81        select_index(file, declared_length, request, current)
82    }
83}
84
85pub(crate) fn select_index(
86    reader: impl Read,
87    declared_length: u64,
88    request: &UpdateRequest,
89    current: &Version,
90) -> UpdateResult<Option<ArtifactDescriptor>> {
91    let max_bytes = FILE_UPDATE_INDEX_MAX_BYTES as u64;
92    if declared_length > max_bytes {
93        return Err(index_size_error());
94    }
95    let mut reader =
96        BufReader::with_capacity(FILE_UPDATE_INDEX_BUFFER_BYTES, reader.take(max_bytes + 1));
97    let mut deserializer = serde_json::Deserializer::from_reader(&mut reader);
98    let selection = deserializer
99        .deserialize_seq(LatestIndexVisitor { request, current })
100        .map_err(|error| UpdateError::Provider(error.to_string()))?;
101    deserializer
102        .end()
103        .map_err(|error| UpdateError::Provider(error.to_string()))?;
104    if reader.get_ref().limit() == 0 {
105        return Err(index_size_error());
106    }
107    selection
108}
109
110struct LatestIndexVisitor<'a> {
111    request: &'a UpdateRequest,
112    current: &'a Version,
113}
114
115impl<'de> Visitor<'de> for LatestIndexVisitor<'_> {
116    type Value = UpdateResult<Option<ArtifactDescriptor>>;
117
118    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str("an array of update artifact descriptors")
120    }
121
122    fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
123    where
124        A: SeqAccess<'de>,
125    {
126        let mut selected = None;
127        let mut validation_error = None;
128        while let Some(artifact) = sequence.next_element::<ArtifactDescriptor>()? {
129            if validation_error.is_some() {
130                continue;
131            }
132            if let Err(error) = artifact.validate() {
133                validation_error = Some(error);
134                continue;
135            }
136            consider_candidate(artifact, self.request, self.current, &mut selected);
137        }
138        if let Some(error) = validation_error {
139            return Ok(Err(error));
140        }
141        Ok(Ok(selected.map(|(_, artifact)| artifact)))
142    }
143}
144
145fn consider_candidate(
146    artifact: ArtifactDescriptor,
147    request: &UpdateRequest,
148    current: &Version,
149    selected: &mut Option<(Version, ArtifactDescriptor)>,
150) {
151    if artifact.application_id() != &request.application_id || artifact.channel() != request.channel
152    {
153        return;
154    }
155    let Ok(version) = Version::parse(artifact.application_version()) else {
156        return;
157    };
158    if version <= *current
159        || selected
160            .as_ref()
161            .is_some_and(|(selected_version, _)| version <= *selected_version)
162    {
163        return;
164    }
165    *selected = Some((version, artifact));
166}
167
168fn index_size_error() -> UpdateError {
169    UpdateError::Provider("update index exceeds configured read limit".to_string())
170}
171
172impl UpdateProvider for FileUpdateProvider {
173    fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>> {
174        let current = Version::parse(&request.current_version).map_err(|error| {
175            UpdateError::Provider(format!("invalid installed application version: {error}"))
176        })?;
177        self.latest_from_index(request, &current)
178    }
179
180    fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>> {
181        let path = artifact
182            .artifact_reference()
183            .strip_prefix("file:")
184            .ok_or_else(|| {
185                UpdateError::Provider("file-update artifact reference must use file:".to_string())
186            })?;
187        match read_regular_file_bounded(Path::new(path), max_bytes) {
188            Ok(bytes) => Ok(bytes),
189            Err(error) if error.kind() == std::io::ErrorKind::InvalidData => {
190                Err(UpdateError::ArtifactTooLarge { max_bytes })
191            }
192            Err(error) => Err(UpdateError::Provider(error.to_string())),
193        }
194    }
195}
196
197/// Factory for the local-first file update provider.
198#[derive(Debug, Clone, Copy, Default)]
199pub struct FileUpdateProviderFactory;
200
201impl ProviderFactory<SharedUpdateProvider> for FileUpdateProviderFactory {
202    fn role(&self) -> ProviderRole {
203        ProviderRole::Update
204    }
205
206    fn provider_id(&self) -> &'static str {
207        FILE_UPDATE_PROVIDER_ID
208    }
209
210    fn create(
211        &self,
212        config: &appcore_contracts::ProviderConfig,
213        _context: &ProviderContext,
214        _secrets: &dyn SecretProvider,
215    ) -> ProviderResult<SharedUpdateProvider> {
216        let endpoint = config.endpoint().ok_or_else(|| {
217            ProviderError::InvalidConfiguration(
218                "file-update provider requires an index endpoint".to_string(),
219            )
220        })?;
221        let path = endpoint.strip_prefix("file:").unwrap_or(endpoint);
222        if path.trim().is_empty() {
223            return Err(ProviderError::InvalidConfiguration(
224                "file-update index path is empty".to_string(),
225            ));
226        }
227        Ok(Arc::new(FileUpdateProvider::new(path)))
228    }
229}