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,
48 pub value: String,
50}
51
52impl PackageDigest {
53 pub fn sha256(value: impl AsRef<str>) -> Self {
55 Self {
56 algorithm: DigestAlgorithm::Sha256,
57 value: value.as_ref().trim().to_ascii_lowercase(),
58 }
59 }
60
61 pub fn parse(value: &str) -> Option<Self> {
63 let (algorithm, digest) = value.split_once(':')?;
64 let algorithm = DigestAlgorithm::parse(algorithm)?;
65 let digest = digest.trim().to_ascii_lowercase();
66 (!digest.is_empty()).then_some(Self {
67 algorithm,
68 value: digest,
69 })
70 }
71
72 pub fn is_well_formed(&self) -> bool {
78 let expected = match self.algorithm {
79 DigestAlgorithm::Sha256 => 64,
80 };
81 self.value.len() == expected && self.value.bytes().all(|byte| byte.is_ascii_hexdigit())
82 }
83
84 pub fn to_feed_string(&self) -> String {
86 format!("{}:{}", self.algorithm.name(), self.value)
87 }
88}
89
90pub fn sha256_hex(bytes: &[u8]) -> String {
92 let mut hasher = Sha256::new();
93 hasher.update(bytes);
94 hex(&hasher.finalize())
95}
96
97fn hex(bytes: &[u8]) -> String {
98 let mut out = String::with_capacity(bytes.len() * 2);
99 for byte in bytes {
100 out.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
101 out.push(char::from_digit((byte & 0x0f) as u32, 16).unwrap_or('0'));
102 }
103 out
104}
105
106pub struct DigestVerifier {
113 expected: PackageDigest,
114 hasher: Sha256,
115 len: u64,
116}
117
118impl DigestVerifier {
119 pub fn new(expected: PackageDigest) -> Result<Self, AppUpdateError> {
121 if !expected.is_well_formed() {
122 return Err(AppUpdateError::MalformedDigest(expected.to_feed_string()));
123 }
124 Ok(Self {
125 expected,
126 hasher: Sha256::new(),
127 len: 0,
128 })
129 }
130
131 pub fn update(&mut self, chunk: &[u8]) {
133 self.hasher.update(chunk);
134 self.len += chunk.len() as u64;
135 }
136
137 pub fn len(&self) -> u64 {
139 self.len
140 }
141
142 pub fn is_empty(&self) -> bool {
144 self.len == 0
145 }
146
147 pub fn finish(self) -> Result<(), AppUpdateError> {
149 let actual = hex(&self.hasher.finalize());
150 if actual == self.expected.value {
151 Ok(())
152 } else {
153 Err(AppUpdateError::VerificationFailed {
154 expected: self.expected.value,
155 actual,
156 })
157 }
158 }
159}
160
161pub fn verify_package(bytes: &[u8], digest: &PackageDigest) -> Result<(), AppUpdateError> {
163 let mut verifier = DigestVerifier::new(digest.clone())?;
164 verifier.update(bytes);
165 verifier.finish()
166}
167
168#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
170pub struct UpdatePackage {
171 pub version: String,
173 pub download_url: String,
175 pub size: Option<u64>,
177 pub digest: Option<PackageDigest>,
185 pub notes: Option<String>,
187}
188
189impl UpdatePackage {
190 pub fn new(version: impl Into<String>, download_url: impl Into<String>) -> Self {
192 Self {
193 version: version.into(),
194 download_url: download_url.into(),
195 ..Self::default()
196 }
197 }
198
199 pub fn with_size(mut self, size: u64) -> Self {
200 self.size = Some(size);
201 self
202 }
203
204 pub fn with_digest(mut self, digest: PackageDigest) -> Self {
205 self.digest = Some(digest);
206 self
207 }
208
209 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
210 self.notes = Some(notes.into());
211 self
212 }
213
214 pub fn is_verifiable(&self) -> bool {
216 self.digest
217 .as_ref()
218 .is_some_and(PackageDigest::is_well_formed)
219 }
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Hash)]
224pub struct GitHubReleaseUpdate {
225 pub repository: String,
227 pub current_version: String,
229 pub asset_suffix: String,
231}
232
233impl GitHubReleaseUpdate {
234 pub fn new(
236 repository: impl Into<String>,
237 current_version: impl Into<String>,
238 asset_suffix: impl Into<String>,
239 ) -> Self {
240 Self {
241 repository: repository.into(),
242 current_version: current_version.into(),
243 asset_suffix: asset_suffix.into(),
244 }
245 }
246}
247
248#[derive(Clone, Debug, Default, PartialEq, Eq)]
250pub enum AppUpdateStatus {
251 #[default]
253 Idle,
254 Checking,
256 UpToDate,
258 Available {
260 package: UpdatePackage,
263 },
264 Downloading {
266 downloaded: u64,
268 total: Option<u64>,
270 },
271 Verifying,
274 AwaitingConfirmation,
276 Installing,
278 Error(String),
280}
281
282#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
284pub enum AppUpdateError {
285 #[error("application updates are unavailable on this platform")]
287 Unsupported,
288 #[error("application update request failed: {0}")]
290 Request(String),
291 #[error("the release feed published no digest for this package, so it cannot be checked")]
298 Unverifiable,
299 #[error("the release feed published a digest that cannot be checked: {0}")]
304 MalformedDigest(String),
305 #[error("the downloaded package does not match its digest (expected {expected}, got {actual})")]
307 VerificationFailed {
308 expected: String,
310 actual: String,
312 },
313}
314
315pub trait AppUpdater: Send + Sync {
317 fn capabilities(&self) -> AppUpdateCapabilities {
324 AppUpdateCapabilities::default()
325 }
326
327 fn check(&self, source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
330 let _ = source;
331 Err(AppUpdateError::Unsupported)
332 }
333
334 fn install(&self, package: &UpdatePackage) -> Result<(), AppUpdateError> {
340 let _ = package;
341 Err(AppUpdateError::Unsupported)
342 }
343}
344
345#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
354pub struct AppUpdateCapabilities {
355 pub check: bool,
357 pub install: bool,
359}
360
361pub type AppUpdaterRef = Arc<dyn AppUpdater>;
363
364static PLATFORM_UPDATER: ServiceRegistry<dyn AppUpdater> = ServiceRegistry::new();
365
366pub fn set_platform_app_updater(updater: AppUpdaterRef) {
368 PLATFORM_UPDATER.set(updater);
369}
370
371pub fn clear_platform_app_updater() {
373 PLATFORM_UPDATER.clear();
374}
375
376pub fn app_update_capabilities() -> AppUpdateCapabilities {
378 PLATFORM_UPDATER
379 .get()
380 .map(|updater| updater.capabilities())
381 .unwrap_or_default()
382}
383
384pub fn app_updates_supported() -> bool {
386 app_update_capabilities().install
387}
388
389pub fn app_update_checks_supported() -> bool {
395 app_update_capabilities().check
396}
397
398fn publish_failure(error: AppUpdateError) -> Result<(), AppUpdateError> {
399 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
400 Err(error)
401}
402
403pub fn check_for_app_update(source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
405 let Some(updater) = PLATFORM_UPDATER.get() else {
406 return publish_failure(AppUpdateError::Unsupported);
407 };
408 if !updater.capabilities().check {
409 return publish_failure(AppUpdateError::Unsupported);
410 }
411 set_app_update_status(AppUpdateStatus::Checking);
412 updater.check(source).inspect_err(|error| {
413 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
414 })
415}
416
417pub fn install_app_update(package: &UpdatePackage) -> Result<(), AppUpdateError> {
424 let Some(updater) = PLATFORM_UPDATER.get() else {
425 return publish_failure(AppUpdateError::Unsupported);
426 };
427 if !updater.capabilities().install {
428 return publish_failure(AppUpdateError::Unsupported);
429 }
430 let error = match &package.digest {
431 None => Some(AppUpdateError::Unverifiable),
432 Some(digest) if !digest.is_well_formed() => {
433 Some(AppUpdateError::MalformedDigest(digest.to_feed_string()))
434 }
435 Some(_) => None,
436 };
437 if let Some(error) = error {
438 return publish_failure(error);
439 }
440 set_app_update_status(AppUpdateStatus::Downloading {
441 downloaded: 0,
442 total: package.size,
443 });
444 updater.install(package).inspect_err(|error| {
445 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
446 })
447}
448
449fn status_slot() -> &'static Mutex<AppUpdateStatus> {
450 static STATUS: OnceLock<Mutex<AppUpdateStatus>> = OnceLock::new();
451 STATUS.get_or_init(|| Mutex::new(AppUpdateStatus::Idle))
452}
453
454pub fn app_update_status() -> AppUpdateStatus {
456 status_slot().lock().map_or_else(
457 |poisoned| poisoned.into_inner().clone(),
458 |status| status.clone(),
459 )
460}
461
462#[cfg(not(target_arch = "wasm32"))]
463type Observer = Arc<dyn Fn(AppUpdateStatus) + Send + Sync>;
464
465#[cfg(not(target_arch = "wasm32"))]
466fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
467 static OBSERVERS: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
468 OBSERVERS.get_or_init(|| Mutex::new(Vec::new()))
469}
470
471static NEXT_OBSERVER_ID: AtomicU64 = AtomicU64::new(1);
472
473pub struct AppUpdateObserver {
475 id: u64,
476}
477
478impl Drop for AppUpdateObserver {
479 fn drop(&mut self) {
480 #[cfg(not(target_arch = "wasm32"))]
481 if let Ok(mut observers) = observers().lock() {
482 observers.retain(|(id, _)| *id != self.id);
483 }
484 #[cfg(target_arch = "wasm32")]
485 crate::thread_state::with(|state| state.app_update_observers.remove(self.id));
486 }
487}
488
489#[cfg(not(target_arch = "wasm32"))]
491pub fn observe_app_update_status(
492 observer: impl Fn(AppUpdateStatus) + Send + Sync + 'static,
493) -> AppUpdateObserver {
494 let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed);
495 let observer: Observer = Arc::new(observer);
496 if let Ok(mut observers) = observers().lock() {
497 observers.push((id, Arc::clone(&observer)));
498 }
499 observer(app_update_status());
500 AppUpdateObserver { id }
501}
502
503#[cfg(target_arch = "wasm32")]
505pub fn observe_app_update_status(
506 observer: impl Fn(AppUpdateStatus) + 'static,
507) -> AppUpdateObserver {
508 let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed);
509 let observer: std::rc::Rc<dyn Fn(AppUpdateStatus)> = std::rc::Rc::new(observer);
510 crate::thread_state::with(|state| {
511 state
512 .app_update_observers
513 .add(id, std::rc::Rc::clone(&observer));
514 });
515 observer(app_update_status());
516 AppUpdateObserver { id }
517}
518
519pub fn set_app_update_status(status: AppUpdateStatus) {
521 if let Ok(mut current) = status_slot().lock() {
522 if *current == status {
523 return;
524 }
525 *current = status.clone();
526 }
527 #[cfg(not(target_arch = "wasm32"))]
528 let observers = observers()
529 .lock()
530 .map(|observers| {
531 observers
532 .iter()
533 .map(|(_, observer)| Arc::clone(observer))
534 .collect::<Vec<_>>()
535 })
536 .unwrap_or_default();
537 #[cfg(target_arch = "wasm32")]
538 let observers = crate::thread_state::with(|state| state.app_update_observers.all());
539 for observer in observers {
540 observer(status.clone());
541 }
542}
543
544#[cfg(test)]
545#[path = "tests/app_update_tests.rs"]
546mod tests;