1use std::sync::{
10 Arc, Mutex, OnceLock,
11 atomic::{AtomicU64, Ordering},
12};
13
14use sha2::{Digest, Sha256};
15
16use crate::registry::ServiceRegistry;
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
20pub enum DigestAlgorithm {
21 #[default]
23 Sha256,
24}
25
26impl DigestAlgorithm {
27 pub fn name(self) -> &'static str {
30 match self {
31 DigestAlgorithm::Sha256 => "sha256",
32 }
33 }
34
35 pub fn parse(name: &str) -> Option<Self> {
39 match name.trim().to_ascii_lowercase().as_str() {
40 "sha256" | "sha-256" => Some(DigestAlgorithm::Sha256),
41 _ => None,
42 }
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Hash)]
48pub struct PackageDigest {
49 pub algorithm: DigestAlgorithm,
50 pub value: String,
52}
53
54impl PackageDigest {
55 pub fn sha256(value: impl AsRef<str>) -> Self {
57 Self {
58 algorithm: DigestAlgorithm::Sha256,
59 value: value.as_ref().trim().to_ascii_lowercase(),
60 }
61 }
62
63 pub fn parse(value: &str) -> Option<Self> {
65 let (algorithm, digest) = value.split_once(':')?;
66 let algorithm = DigestAlgorithm::parse(algorithm)?;
67 let digest = digest.trim().to_ascii_lowercase();
68 (!digest.is_empty()).then_some(Self {
69 algorithm,
70 value: digest,
71 })
72 }
73
74 pub fn is_well_formed(&self) -> bool {
80 let expected = match self.algorithm {
81 DigestAlgorithm::Sha256 => 64,
82 };
83 self.value.len() == expected && self.value.bytes().all(|byte| byte.is_ascii_hexdigit())
84 }
85
86 pub fn to_feed_string(&self) -> String {
88 format!("{}:{}", self.algorithm.name(), self.value)
89 }
90}
91
92pub fn sha256_hex(bytes: &[u8]) -> String {
94 let mut hasher = Sha256::new();
95 hasher.update(bytes);
96 hex(&hasher.finalize())
97}
98
99fn hex(bytes: &[u8]) -> String {
100 let mut out = String::with_capacity(bytes.len() * 2);
101 for byte in bytes {
102 out.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
103 out.push(char::from_digit((byte & 0x0f) as u32, 16).unwrap_or('0'));
104 }
105 out
106}
107
108pub struct DigestVerifier {
115 expected: PackageDigest,
116 hasher: Sha256,
117 len: u64,
118}
119
120impl DigestVerifier {
121 pub fn new(expected: PackageDigest) -> Result<Self, AppUpdateError> {
123 if !expected.is_well_formed() {
124 return Err(AppUpdateError::MalformedDigest(expected.to_feed_string()));
125 }
126 Ok(Self {
127 expected,
128 hasher: Sha256::new(),
129 len: 0,
130 })
131 }
132
133 pub fn update(&mut self, chunk: &[u8]) {
135 self.hasher.update(chunk);
136 self.len += chunk.len() as u64;
137 }
138
139 pub fn len(&self) -> u64 {
141 self.len
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.len == 0
147 }
148
149 pub fn finish(self) -> Result<(), AppUpdateError> {
151 let actual = hex(&self.hasher.finalize());
152 if actual == self.expected.value {
153 Ok(())
154 } else {
155 Err(AppUpdateError::VerificationFailed {
156 expected: self.expected.value,
157 actual,
158 })
159 }
160 }
161}
162
163pub fn verify_package(bytes: &[u8], digest: &PackageDigest) -> Result<(), AppUpdateError> {
165 let mut verifier = DigestVerifier::new(digest.clone())?;
166 verifier.update(bytes);
167 verifier.finish()
168}
169
170#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
172pub struct UpdatePackage {
173 pub version: String,
175 pub download_url: String,
177 pub size: Option<u64>,
179 pub digest: Option<PackageDigest>,
187 pub notes: Option<String>,
189}
190
191impl UpdatePackage {
192 pub fn new(version: impl Into<String>, download_url: impl Into<String>) -> Self {
194 Self {
195 version: version.into(),
196 download_url: download_url.into(),
197 ..Self::default()
198 }
199 }
200
201 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 {
207 self.digest = Some(digest);
208 self
209 }
210
211 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
212 self.notes = Some(notes.into());
213 self
214 }
215
216 pub fn is_verifiable(&self) -> bool {
218 self.digest
219 .as_ref()
220 .is_some_and(PackageDigest::is_well_formed)
221 }
222}
223
224#[derive(Clone, Debug, PartialEq, Eq, Hash)]
226pub struct GitHubReleaseUpdate {
227 pub repository: String,
229 pub current_version: String,
231 pub asset_suffix: String,
233}
234
235impl GitHubReleaseUpdate {
236 pub fn new(
238 repository: impl Into<String>,
239 current_version: impl Into<String>,
240 asset_suffix: impl Into<String>,
241 ) -> Self {
242 Self {
243 repository: repository.into(),
244 current_version: current_version.into(),
245 asset_suffix: asset_suffix.into(),
246 }
247 }
248}
249
250#[derive(Clone, Debug, Default, PartialEq, Eq)]
252pub enum AppUpdateStatus {
253 #[default]
255 Idle,
256 Checking,
258 UpToDate,
260 Available {
262 package: UpdatePackage,
265 },
266 Downloading {
268 downloaded: u64,
270 total: Option<u64>,
272 },
273 Verifying,
276 AwaitingConfirmation,
278 Installing,
280 Error(String),
282}
283
284#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
286pub enum AppUpdateError {
287 #[error("application updates are unavailable on this platform")]
289 Unsupported,
290 #[error("application update request failed: {0}")]
292 Request(String),
293 #[error("the release feed published no digest for this package, so it cannot be checked")]
300 Unverifiable,
301 #[error("the release feed published a digest that cannot be checked: {0}")]
306 MalformedDigest(String),
307 #[error("the downloaded package does not match its digest (expected {expected}, got {actual})")]
309 VerificationFailed {
310 expected: String,
312 actual: String,
314 },
315}
316
317pub trait AppUpdater: Send + Sync {
319 fn capabilities(&self) -> AppUpdateCapabilities {
326 AppUpdateCapabilities::default()
327 }
328
329 fn check(&self, source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
332 let _ = source;
333 Err(AppUpdateError::Unsupported)
334 }
335
336 fn install(&self, package: &UpdatePackage) -> Result<(), AppUpdateError> {
342 let _ = package;
343 Err(AppUpdateError::Unsupported)
344 }
345}
346
347#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
356pub struct AppUpdateCapabilities {
357 pub check: bool,
359 pub install: bool,
361}
362
363pub type AppUpdaterRef = Arc<dyn AppUpdater>;
365
366static PLATFORM_UPDATER: ServiceRegistry<dyn AppUpdater> = ServiceRegistry::new();
367
368pub fn set_platform_app_updater(updater: AppUpdaterRef) {
370 PLATFORM_UPDATER.set(updater);
371}
372
373pub fn clear_platform_app_updater() {
375 PLATFORM_UPDATER.clear();
376}
377
378pub fn app_update_capabilities() -> AppUpdateCapabilities {
380 PLATFORM_UPDATER
381 .get()
382 .map(|updater| updater.capabilities())
383 .unwrap_or_default()
384}
385
386pub fn app_updates_supported() -> bool {
388 app_update_capabilities().install
389}
390
391pub fn app_update_checks_supported() -> bool {
397 app_update_capabilities().check
398}
399
400fn publish_failure(error: AppUpdateError) -> Result<(), AppUpdateError> {
401 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
402 Err(error)
403}
404
405pub fn check_for_app_update(source: &GitHubReleaseUpdate) -> Result<(), AppUpdateError> {
407 let Some(updater) = PLATFORM_UPDATER.get() else {
408 return publish_failure(AppUpdateError::Unsupported);
409 };
410 if !updater.capabilities().check {
411 return publish_failure(AppUpdateError::Unsupported);
412 }
413 set_app_update_status(AppUpdateStatus::Checking);
414 updater.check(source).inspect_err(|error| {
415 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
416 })
417}
418
419pub fn install_app_update(package: &UpdatePackage) -> Result<(), AppUpdateError> {
426 let Some(updater) = PLATFORM_UPDATER.get() else {
427 return publish_failure(AppUpdateError::Unsupported);
428 };
429 if !updater.capabilities().install {
430 return publish_failure(AppUpdateError::Unsupported);
431 }
432 let error = match &package.digest {
433 None => Some(AppUpdateError::Unverifiable),
434 Some(digest) if !digest.is_well_formed() => {
435 Some(AppUpdateError::MalformedDigest(digest.to_feed_string()))
436 }
437 Some(_) => None,
438 };
439 if let Some(error) = error {
440 return publish_failure(error);
441 }
442 set_app_update_status(AppUpdateStatus::Downloading {
443 downloaded: 0,
444 total: package.size,
445 });
446 updater.install(package).inspect_err(|error| {
447 set_app_update_status(AppUpdateStatus::Error(error.to_string()));
448 })
449}
450
451fn status_slot() -> &'static Mutex<AppUpdateStatus> {
452 static STATUS: OnceLock<Mutex<AppUpdateStatus>> = OnceLock::new();
453 STATUS.get_or_init(|| Mutex::new(AppUpdateStatus::Idle))
454}
455
456pub fn app_update_status() -> AppUpdateStatus {
458 status_slot().lock().map_or_else(
459 |poisoned| poisoned.into_inner().clone(),
460 |status| status.clone(),
461 )
462}
463
464#[cfg(not(target_arch = "wasm32"))]
465type Observer = Arc<dyn Fn(AppUpdateStatus) + Send + Sync>;
466#[cfg(target_arch = "wasm32")]
467type Observer = std::rc::Rc<dyn Fn(AppUpdateStatus)>;
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
475#[cfg(target_arch = "wasm32")]
476thread_local! {
477 static OBSERVERS: std::cell::RefCell<Vec<(u64, Observer)>> = const { std::cell::RefCell::new(Vec::new()) };
478}
479
480static NEXT_OBSERVER_ID: AtomicU64 = AtomicU64::new(1);
481
482pub struct AppUpdateObserver {
484 id: u64,
485}
486
487impl Drop for AppUpdateObserver {
488 fn drop(&mut self) {
489 #[cfg(not(target_arch = "wasm32"))]
490 if let Ok(mut observers) = observers().lock() {
491 observers.retain(|(id, _)| *id != self.id);
492 }
493 #[cfg(target_arch = "wasm32")]
494 OBSERVERS.with(|observers| observers.borrow_mut().retain(|(id, _)| *id != self.id));
495 }
496}
497
498#[cfg(not(target_arch = "wasm32"))]
500pub fn observe_app_update_status(
501 observer: impl Fn(AppUpdateStatus) + Send + Sync + 'static,
502) -> AppUpdateObserver {
503 let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed);
504 let observer: Observer = Arc::new(observer);
505 if let Ok(mut observers) = observers().lock() {
506 observers.push((id, Arc::clone(&observer)));
507 }
508 observer(app_update_status());
509 AppUpdateObserver { id }
510}
511
512#[cfg(target_arch = "wasm32")]
514pub fn observe_app_update_status(
515 observer: impl Fn(AppUpdateStatus) + 'static,
516) -> AppUpdateObserver {
517 let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed);
518 let observer: Observer = std::rc::Rc::new(observer);
519 OBSERVERS.with(|observers| {
520 observers
521 .borrow_mut()
522 .push((id, std::rc::Rc::clone(&observer)));
523 });
524 observer(app_update_status());
525 AppUpdateObserver { id }
526}
527
528pub fn set_app_update_status(status: AppUpdateStatus) {
530 if let Ok(mut current) = status_slot().lock() {
531 if *current == status {
532 return;
533 }
534 *current = status.clone();
535 }
536 #[cfg(not(target_arch = "wasm32"))]
537 let observers = observers()
538 .lock()
539 .map(|observers| {
540 observers
541 .iter()
542 .map(|(_, observer)| Arc::clone(observer))
543 .collect::<Vec<_>>()
544 })
545 .unwrap_or_default();
546 #[cfg(target_arch = "wasm32")]
547 let observers = OBSERVERS.with(|observers| {
548 observers
549 .borrow()
550 .iter()
551 .map(|(_, observer)| std::rc::Rc::clone(observer))
552 .collect::<Vec<_>>()
553 });
554 for observer in observers {
555 observer(status.clone());
556 }
557}
558
559#[cfg(test)]
560#[path = "tests/app_update_tests.rs"]
561mod tests;