appcore_update/
provider.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct UpdateRequest {
33 pub application_id: ApplicationId,
35 pub current_version: String,
37 pub channel: String,
39}
40
41pub trait UpdateProvider: Send + Sync {
43 fn latest(&self, request: &UpdateRequest) -> UpdateResult<Option<ArtifactDescriptor>>;
45
46 fn fetch(&self, artifact: &ArtifactDescriptor, max_bytes: usize) -> UpdateResult<Vec<u8>>;
48}
49
50pub type SharedUpdateProvider = Arc<dyn UpdateProvider>;
52
53pub const FILE_UPDATE_PROVIDER_ID: &str = "file-update";
55
56#[derive(Debug, Clone)]
58pub struct FileUpdateProvider {
59 index_path: PathBuf,
60}
61
62impl FileUpdateProvider {
63 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, ¤t)
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#[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}