1use crate::{
14 ArtifactAuthenticityVerifier, ArtifactDescriptor, QuarantineKey, QuarantineReason,
15 QuarantineStore, UpdateError, UpdateResult,
16};
17use appcore_contracts::ApplicationId;
18use semver::Version;
19use serde::{Deserialize, Serialize};
20use std::collections::{BTreeMap, BTreeSet};
21use std::io::{BufReader, Read};
22use std::time::{SystemTime, UNIX_EPOCH};
23
24pub const RELEASE_CATALOG_MAX_BYTES: usize = 4 * 1024 * 1024;
26pub const RELEASE_CATALOG_MAX_ENTRIES: usize = 4_096;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct UpdateIdentity {
32 pub application_id: ApplicationId,
34 pub os: String,
36 pub architecture: String,
38 pub format: String,
40 pub channel: String,
42 pub current_version: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct LatestCompatibleUpdateIdentity {
49 pub application_id: ApplicationId,
51 pub os: String,
53 pub architecture: String,
55 pub format: String,
57 pub channel: String,
59 pub current_version: String,
61 pub runtime_version: String,
63 pub protocol_version: String,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct QuarantineExclusion {
70 pub key: QuarantineKey,
72 pub reason: QuarantineReason,
74 pub expires_at_ms: Option<u64>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct QuarantineSelection {
81 pub selected: Option<ArtifactDescriptor>,
83 pub exclusions: Vec<QuarantineExclusion>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct ReleaseCatalog {
90 entries: Vec<ArtifactDescriptor>,
91}
92
93impl ReleaseCatalog {
94 pub fn open<R: Read>(
96 reader: R,
97 verifier: &dyn ArtifactAuthenticityVerifier,
98 ) -> UpdateResult<Self> {
99 let mut reader = BufReader::new(reader.take(RELEASE_CATALOG_MAX_BYTES as u64 + 1));
100 let mut bytes = Vec::with_capacity(RELEASE_CATALOG_MAX_BYTES.min(64 * 1024));
101 reader
102 .read_to_end(&mut bytes)
103 .map_err(|error| UpdateError::Provider(error.to_string()))?;
104 if bytes.len() > RELEASE_CATALOG_MAX_BYTES {
105 return Err(UpdateError::Provider(
106 "release catalog exceeds configured read limit".to_string(),
107 ));
108 }
109 let entries: Vec<ArtifactDescriptor> = serde_json::from_slice(&bytes)
110 .map_err(|error| UpdateError::Provider(error.to_string()))?;
111 Self::from_entries(entries, verifier)
112 }
113
114 pub fn from_entries(
116 entries: Vec<ArtifactDescriptor>,
117 verifier: &dyn ArtifactAuthenticityVerifier,
118 ) -> UpdateResult<Self> {
119 if entries.len() > RELEASE_CATALOG_MAX_ENTRIES {
120 return Err(UpdateError::Provider(
121 "release catalog exceeds entry limit".to_string(),
122 ));
123 }
124 let mut keys = BTreeSet::new();
125 for entry in &entries {
126 entry.validate()?;
127 let target = entry.target().ok_or_else(|| {
128 UpdateError::InvalidArtifact(
129 "catalog entries require a platform target".to_string(),
130 )
131 })?;
132 let key = format!(
133 "{}\n{}\n{}\n{}\n{}",
134 entry.application_id(),
135 target.os(),
136 target.architecture(),
137 target.format(),
138 entry.channel(),
139 );
140 let version_key = format!("{key}\n{}", entry.application_version());
141 if !keys.insert(version_key) {
142 return Err(UpdateError::InvalidArtifact(
143 "catalog contains ambiguous duplicate release versions".to_string(),
144 ));
145 }
146 verifier.verify(entry)?;
147 }
148 Ok(Self { entries })
149 }
150
151 pub fn select(&self, identity: &UpdateIdentity) -> UpdateResult<Option<ArtifactDescriptor>> {
153 self.select_internal(identity, None)
154 }
155
156 pub fn select_latest_compatible(
159 &self,
160 identity: &LatestCompatibleUpdateIdentity,
161 ) -> UpdateResult<Option<ArtifactDescriptor>> {
162 let current = Version::parse(&identity.current_version).map_err(|error| {
163 UpdateError::Incompatible(format!("invalid installed version: {error}"))
164 })?;
165 validate_latest_identity(identity)?;
166 let mut selected: Option<(Version, ArtifactDescriptor)> = None;
167 for entry in &self.entries {
168 let Some(target) = entry.target() else {
169 continue;
170 };
171 if entry.application_id() != &identity.application_id
172 || target.os() != identity.os
173 || target.architecture() != identity.architecture
174 || target.format() != identity.format
175 || entry.channel() != identity.channel
176 {
177 continue;
178 }
179 let version = Version::parse(entry.application_version()).map_err(|error| {
180 UpdateError::InvalidArtifact(format!("invalid catalog version: {error}"))
181 })?;
182 if version <= current || selected.as_ref().is_some_and(|(best, _)| version <= *best) {
183 continue;
184 }
185 match entry.ensure_compatible(&identity.runtime_version, &identity.protocol_version) {
186 Ok(()) => selected = Some((version, entry.clone())),
187 Err(UpdateError::Incompatible(_)) => continue,
188 Err(error) => return Err(error),
189 }
190 }
191 Ok(selected.map(|(_, entry)| entry))
192 }
193
194 pub fn select_with_quarantine(
196 &self,
197 identity: &UpdateIdentity,
198 quarantine: &QuarantineStore,
199 ) -> UpdateResult<Option<ArtifactDescriptor>> {
200 Ok(self
201 .select_with_quarantine_at(identity, quarantine, current_time_ms())?
202 .selected)
203 }
204
205 pub fn select_with_quarantine_report(
207 &self,
208 identity: &UpdateIdentity,
209 quarantine: &QuarantineStore,
210 at_ms: u64,
211 ) -> UpdateResult<QuarantineSelection> {
212 let current = Version::parse(&identity.current_version).map_err(|error| {
213 UpdateError::Incompatible(format!("invalid installed version: {error}"))
214 })?;
215 validate_identity(identity)?;
216 let records = quarantine
217 .list()?
218 .into_iter()
219 .map(|record| (record.key.clone(), record))
220 .collect::<BTreeMap<_, _>>();
221 let mut selected: Option<(Version, ArtifactDescriptor)> = None;
222 let mut exclusions = Vec::new();
223 for entry in &self.entries {
224 let Some(target) = entry.target() else {
225 continue;
226 };
227 if entry.application_id() != &identity.application_id
228 || target.os() != identity.os
229 || target.architecture() != identity.architecture
230 || target.format() != identity.format
231 || entry.channel() != identity.channel
232 {
233 continue;
234 }
235 let version = Version::parse(entry.application_version()).map_err(|error| {
236 UpdateError::InvalidArtifact(format!("invalid catalog version: {error}"))
237 })?;
238 if version <= current {
239 continue;
240 }
241 let key = QuarantineKey::from_descriptor(entry);
242 if let Some(record) = records
243 .get(&key)
244 .filter(|record| record.is_active_at(at_ms))
245 {
246 exclusions.push(QuarantineExclusion {
247 key,
248 reason: record.reason.clone(),
249 expires_at_ms: record.expires_at_ms,
250 });
251 continue;
252 }
253 if selected.as_ref().is_some_and(|(best, _)| version <= *best) {
254 continue;
255 }
256 selected = Some((version, entry.clone()));
257 }
258 exclusions.sort_by(|left, right| left.key.cmp(&right.key));
259 Ok(QuarantineSelection {
260 selected: selected.map(|(_, entry)| entry),
261 exclusions,
262 })
263 }
264
265 pub fn select_with_quarantine_at(
267 &self,
268 identity: &UpdateIdentity,
269 quarantine: &QuarantineStore,
270 at_ms: u64,
271 ) -> UpdateResult<QuarantineSelection> {
272 self.select_with_quarantine_report(identity, quarantine, at_ms)
273 }
274
275 fn select_internal(
276 &self,
277 identity: &UpdateIdentity,
278 quarantine: Option<&QuarantineStore>,
279 ) -> UpdateResult<Option<ArtifactDescriptor>> {
280 let current = Version::parse(&identity.current_version).map_err(|error| {
281 UpdateError::Incompatible(format!("invalid installed version: {error}"))
282 })?;
283 validate_identity(identity)?;
284 let mut selected: Option<(Version, ArtifactDescriptor)> = None;
285 for entry in &self.entries {
286 let Some(target) = entry.target() else {
287 continue;
288 };
289 if entry.application_id() != &identity.application_id
290 || target.os() != identity.os
291 || target.architecture() != identity.architecture
292 || target.format() != identity.format
293 || entry.channel() != identity.channel
294 {
295 continue;
296 }
297 if let Some(store) = quarantine {
298 if store.is_quarantined(&QuarantineKey::from_descriptor(entry))? {
299 continue;
300 }
301 }
302 let version = Version::parse(entry.application_version()).map_err(|error| {
303 UpdateError::InvalidArtifact(format!("invalid catalog version: {error}"))
304 })?;
305 if version <= current || selected.as_ref().is_some_and(|(best, _)| version <= *best) {
306 continue;
307 }
308 selected = Some((version, entry.clone()));
309 }
310 Ok(selected.map(|(_, entry)| entry))
311 }
312
313 pub fn len(&self) -> usize {
315 self.entries.len()
316 }
317
318 pub fn is_empty(&self) -> bool {
320 self.entries.is_empty()
321 }
322}
323
324fn current_time_ms() -> u64 {
325 SystemTime::now()
326 .duration_since(UNIX_EPOCH)
327 .unwrap_or_default()
328 .as_millis()
329 .try_into()
330 .unwrap_or(u64::MAX)
331}
332
333fn validate_identity(identity: &UpdateIdentity) -> UpdateResult<()> {
334 for (name, value) in [
335 ("os", identity.os.as_str()),
336 ("architecture", identity.architecture.as_str()),
337 ("format", identity.format.as_str()),
338 ("channel", identity.channel.as_str()),
339 ] {
340 if value.trim().is_empty() || value.len() > 64 || value.chars().any(char::is_control) {
341 return Err(UpdateError::Incompatible(format!(
342 "update identity {name} is invalid"
343 )));
344 }
345 }
346 Ok(())
347}
348
349fn validate_latest_identity(identity: &LatestCompatibleUpdateIdentity) -> UpdateResult<()> {
350 validate_identity(&UpdateIdentity {
351 application_id: identity.application_id.clone(),
352 os: identity.os.clone(),
353 architecture: identity.architecture.clone(),
354 format: identity.format.clone(),
355 channel: identity.channel.clone(),
356 current_version: identity.current_version.clone(),
357 })?;
358 for (name, value, max) in [
359 ("runtime_version", identity.runtime_version.as_str(), 64),
360 ("protocol_version", identity.protocol_version.as_str(), 64),
361 ] {
362 if value.trim().is_empty() || value.len() > max || value.chars().any(char::is_control) {
363 return Err(UpdateError::Incompatible(format!(
364 "update identity {name} is invalid"
365 )));
366 }
367 }
368 Version::parse(&identity.runtime_version)
369 .map_err(|error| UpdateError::Incompatible(format!("invalid runtime version: {error}")))?;
370 Ok(())
371}