1use std::sync::{Arc, Mutex, OnceLock, atomic::Ordering};
10
11use portable_atomic::AtomicU64;
12use sha2::{Digest, Sha256};
13
14use crate::registry::ServiceRegistry;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum DigestAlgorithm {
19 #[default]
21 Sha256,
22}
23
24impl DigestAlgorithm {
25 pub fn name(self) -> &'static str {
28 match self {
29 DigestAlgorithm::Sha256 => "sha256",
30 }
31 }
32
33 pub fn parse(name: &str) -> Option<Self> {
37 match name.trim().to_ascii_lowercase().as_str() {
38 "sha256" | "sha-256" => Some(DigestAlgorithm::Sha256),
39 _ => None,
40 }
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub struct PackageDigest {
47 pub algorithm: DigestAlgorithm,
49 pub value: String,
51}
52
53impl PackageDigest {
54 pub fn sha256(value: impl AsRef<str>) -> Self {
56 Self {
57 algorithm: DigestAlgorithm::Sha256,
58 value: value.as_ref().trim().to_ascii_lowercase(),
59 }
60 }
61
62 pub fn parse(value: &str) -> Option<Self> {
64 let (algorithm, digest) = value.split_once(':')?;
65 let algorithm = DigestAlgorithm::parse(algorithm)?;
66 let digest = digest.trim().to_ascii_lowercase();
67 (!digest.is_empty()).then_some(Self {
68 algorithm,
69 value: digest,
70 })
71 }
72
73 pub fn is_well_formed(&self) -> bool {
79 let expected = match self.algorithm {
80 DigestAlgorithm::Sha256 => 64,
81 };
82 self.value.len() == expected && self.value.bytes().all(|byte| byte.is_ascii_hexdigit())
83 }
84
85 pub fn to_feed_string(&self) -> String {
87 format!("{}:{}", self.algorithm.name(), self.value)
88 }
89}
90
91pub fn sha256_hex(bytes: &[u8]) -> String {
93 let mut hasher = Sha256::new();
94 hasher.update(bytes);
95 hex(&hasher.finalize())
96}
97
98fn hex(bytes: &[u8]) -> String {
99 let mut out = String::with_capacity(bytes.len() * 2);
100 for byte in bytes {
101 out.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
102 out.push(char::from_digit((byte & 0x0f) as u32, 16).unwrap_or('0'));
103 }
104 out
105}
106
107pub struct DigestVerifier {
114 expected: PackageDigest,
115 hasher: Sha256,
116 len: u64,
117}
118
119impl DigestVerifier {
120 pub fn new(expected: PackageDigest) -> Result<Self, AppUpdateError> {
122 if !expected.is_well_formed() {
123 return Err(AppUpdateError::MalformedDigest(expected.to_feed_string()));
124 }
125 Ok(Self {
126 expected,
127 hasher: Sha256::new(),
128 len: 0,
129 })
130 }
131
132 pub fn update(&mut self, chunk: &[u8]) {
134 self.hasher.update(chunk);
135 self.len += chunk.len() as u64;
136 }
137
138 pub fn len(&self) -> u64 {
140 self.len
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.len == 0
146 }
147
148 pub fn finish(self) -> Result<(), AppUpdateError> {
150 let actual = hex(&self.hasher.finalize());
151 if actual == self.expected.value {
152 Ok(())
153 } else {
154 Err(AppUpdateError::VerificationFailed {
155 expected: self.expected.value,
156 actual,
157 })
158 }
159 }
160}
161
162pub fn verify_package(bytes: &[u8], digest: &PackageDigest) -> Result<(), AppUpdateError> {
164 let mut verifier = DigestVerifier::new(digest.clone())?;
165 verifier.update(bytes);
166 verifier.finish()
167}
168
169#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
171pub struct UpdatePackage {
172 pub version: String,
174 pub download_url: String,
176 pub size: Option<u64>,
178 pub digest: Option<PackageDigest>,
186 pub notes: Option<String>,
188}
189
190impl UpdatePackage {
191 pub fn new(version: impl Into<String>, download_url: impl Into<String>) -> Self {
193 Self {
194 version: version.into(),
195 download_url: download_url.into(),
196 ..Self::default()
197 }
198 }
199
200 pub fn with_size(mut self, size: u64) -> Self {
202 self.size = Some(size);
203 self
204 }
205
206 pub fn with_digest(mut self, digest: PackageDigest) -> Self {
208 self.digest = Some(digest);
209 self
210 }
211
212 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
214 self.notes = Some(notes.into());
215 self
216 }
217
218 pub fn is_verifiable(&self) -> bool {
220 self.digest
221 .as_ref()
222 .is_some_and(PackageDigest::is_well_formed)
223 }
224}
225
226#[derive(Clone, Debug, PartialEq, Eq, Hash)]
228pub struct GitHubReleaseUpdate {
229 pub repository: String,
231 pub current_version: String,
233 pub asset_suffix: String,
235}
236
237impl GitHubReleaseUpdate {
238 pub fn new(
240 repository: impl Into<String>,
241 current_version: impl Into<String>,
242 asset_suffix: impl Into<String>,
243 ) -> Self {
244 Self {
245 repository: repository.into(),
246 current_version: current_version.into(),
247 asset_suffix: asset_suffix.into(),
248 }
249 }
250}
251
252#[derive(Clone, Debug, Default, PartialEq, Eq)]
254pub enum AppUpdateStatus {
255 #[default]
257 Idle,
258 Checking,
260 UpToDate,
262 Available {
264 package: UpdatePackage,
267 },
268 Downloading {
270 downloaded: u64,
272 total: Option<u64>,
274 },
275 Verifying,
278 AwaitingConfirmation,
280 Installing,
282 Error(String),
284}
285
286#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
288pub enum AppUpdateError {
289 #[error("application updates are unavailable on this platform")]
291 Unsupported,
292 #[error("application update request failed: {0}")]
294 Request(String),
295 #[error("the release feed published no digest for this package, so it cannot be checked")]
302 Unverifiable,
303 #[error("the release feed published a digest that cannot be checked: {0}")]
308 MalformedDigest(String),
309 #[error("the downloaded package does not match its digest (expected {expected}, got {actual})")]
311 VerificationFailed {
312 expected: String,
314 actual: String,
316 },
317}
318
319pub trait AppUpdater: Send + Sync {
321 fn capabilities(&self) -> AppUpdateCapabilities {
328 AppUpdateCapabilities::default()
329 }
330
331 fn check(&self, source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
334 let _ = source;
335 Err(AppUpdateError::Unsupported)
336 }
337
338 fn install(&self, package: &UpdatePackage) -> Result<(), AppUpdateError> {
344 let _ = package;
345 Err(AppUpdateError::Unsupported)
346 }
347}
348
349#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
358pub struct AppUpdateCapabilities {
359 pub check: bool,
361 pub install: bool,
363}
364
365pub type AppUpdaterRef = Arc<dyn AppUpdater>;
367
368static PLATFORM_UPDATER: ServiceRegistry<dyn AppUpdater> = ServiceRegistry::new();
369
370pub fn set_platform_app_updater(updater: AppUpdaterRef) {
372 PLATFORM_UPDATER.set(updater);
373}
374
375pub fn clear_platform_app_updater() {
377 PLATFORM_UPDATER.clear();
378}
379
380pub fn app_update_capabilities() -> AppUpdateCapabilities {
382 PLATFORM_UPDATER
383 .get()
384 .map(|updater| updater.capabilities())
385 .unwrap_or_default()
386}
387
388pub fn app_updates_supported() -> bool {
390 app_update_capabilities().install
391}
392
393pub fn app_update_checks_supported() -> bool {
399 app_update_capabilities().check
400}
401
402fn publish_failure(error: AppUpdateError) -> Result<(), AppUpdateError> {
403 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
404 Err(error)
405}
406
407pub fn check_for_app_update(source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
409 let Some(updater) = PLATFORM_UPDATER.get() else {
410 return publish_failure(AppUpdateError::Unsupported);
411 };
412 if !updater.capabilities().check {
413 return publish_failure(AppUpdateError::Unsupported);
414 }
415 set_app_update_status(AppUpdateStatus::Checking);
416 updater.check(source).inspect_err(|error| {
417 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
418 })
419}
420
421pub fn install_app_update(package: &UpdatePackage) -> Result<(), AppUpdateError> {
428 let Some(updater) = PLATFORM_UPDATER.get() else {
429 return publish_failure(AppUpdateError::Unsupported);
430 };
431 if !updater.capabilities().install {
432 return publish_failure(AppUpdateError::Unsupported);
433 }
434 let error = match &package.digest {
435 None => Some(AppUpdateError::Unverifiable),
436 Some(digest) if !digest.is_well_formed() => {
437 Some(AppUpdateError::MalformedDigest(digest.to_feed_string()))
438 }
439 Some(_) => None,
440 };
441 if let Some(error) = error {
442 return publish_failure(error);
443 }
444 set_app_update_status(AppUpdateStatus::Downloading {
445 downloaded: 0,
446 total: package.size,
447 });
448 updater.install(package).inspect_err(|error| {
449 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
450 })
451}
452
453fn status_slot() -> &'static Mutex<AppUpdateStatus> {
454 static STATUS: OnceLock<Mutex<AppUpdateStatus>> = OnceLock::new();
455 STATUS.get_or_init(|| Mutex::new(AppUpdateStatus::Idle))
456}
457
458pub fn app_update_status() -> AppUpdateStatus {
460 status_slot().lock().map_or_else(
461 |poisoned| poisoned.into_inner().clone(),
462 |status| status.clone(),
463 )
464}
465
466#[cfg(not(target_arch = "wasm32"))]
467type Observer = Arc<dyn Fn(AppUpdateStatus) + Send + Sync>;
468
469#[cfg(not(target_arch = "wasm32"))]
470fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
471 static OBSERVERS: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
472 OBSERVERS.get_or_init(|| Mutex::new(Vec::new()))
473}
474
475static NEXT_OBSERVER_ID: AtomicU64 = AtomicU64::new(1);
476
477pub struct AppUpdateObserver {
479 id: u64,
480}
481
482impl Drop for AppUpdateObserver {
483 fn drop(&mut self) {
484 #[cfg(not(target_arch = "wasm32"))]
485 if let Ok(mut observers) = observers().lock() {
486 observers.retain(|(id, _)| *id != self.id);
487 }
488 #[cfg(target_arch = "wasm32")]
489 crate::thread_state::with(|state| state.app_update_observers.remove(self.id));
490 }
491}
492
493#[cfg(not(target_arch = "wasm32"))]
495pub fn observe_app_update_status(
496 observer: impl Fn(AppUpdateStatus) + Send + Sync + 'static,
497) -> AppUpdateObserver {
498 let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed);
499 let observer: Observer = Arc::new(observer);
500 if let Ok(mut observers) = observers().lock() {
501 observers.push((id, Arc::clone(&observer)));
502 }
503 observer(app_update_status());
504 AppUpdateObserver { id }
505}
506
507#[cfg(target_arch = "wasm32")]
509pub fn observe_app_update_status(
510 observer: impl Fn(AppUpdateStatus) + 'static,
511) -> AppUpdateObserver {
512 let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed);
513 let observer: std::rc::Rc<dyn Fn(AppUpdateStatus)> = std::rc::Rc::new(observer);
514 crate::thread_state::with(|state| {
515 state
516 .app_update_observers
517 .add(id, std::rc::Rc::clone(&observer));
518 });
519 observer(app_update_status());
520 AppUpdateObserver { id }
521}
522
523pub fn set_app_update_status(status: AppUpdateStatus) {
525 if let Ok(mut current) = status_slot().lock() {
526 if *current == status {
527 return;
528 }
529 *current = status.clone();
530 }
531 #[cfg(not(target_arch = "wasm32"))]
532 let observers = observers()
533 .lock()
534 .map(|observers| {
535 observers
536 .iter()
537 .map(|(_, observer)| Arc::clone(observer))
538 .collect::<Vec<_>>()
539 })
540 .unwrap_or_default();
541 #[cfg(target_arch = "wasm32")]
542 let observers = crate::thread_state::with(|state| state.app_update_observers.all());
543 for observer in observers {
544 observer(status.clone());
545 }
546}
547
548#[cfg(test)]
549#[path = "tests/app_update_tests.rs"]
550mod tests;