1#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
2
3use std::{
4 collections::HashMap,
5 time::{Duration, Instant},
6};
7
8use reqwest::Client;
9use tokio::sync::Mutex;
10
11const CACHE_TTL: Duration = Duration::from_secs(30);
12
13pub const XP_URL: &str = "https://download.nvaccess.org/releases/2017.3/nvda_2017.3.exe";
15pub const XP_HASH: &str = "386e7acb8cc3ecaabc8005894cf783b51a8ac7f6";
17pub const WIN7_URL: &str = "https://download.nvaccess.org/releases/2023.3.4/nvda_2023.3.4.exe";
19pub const WIN7_HASH: &str = "985a6deab01edb55fbedc9b056956e30120db290";
21
22#[derive(Debug)]
24struct UpdateInfo {
25 pub launcher_url: Option<String>,
26 pub launcher_hash: Option<String>,
28}
29
30impl UpdateInfo {
31 #[must_use]
32 fn parse(data: &str) -> Self {
33 let mut launcher_url = None;
34 let mut launcher_hash = None;
35 for line in data.lines() {
36 let Some((key, value)) = line.split_once(": ") else { continue };
37 match key {
38 "launcherUrl" => launcher_url = Some(value.to_owned()),
39 "launcherHash" => launcher_hash = Some(value.to_owned()),
40 _ => (),
41 }
42 }
43 Self { launcher_url, launcher_hash }
44 }
45}
46
47#[derive(Clone, Copy, Eq, Hash, PartialEq, Debug)]
49pub enum VersionType {
50 Stable,
52 Beta,
54 Alpha,
56}
57
58impl VersionType {
59 const fn as_str(self) -> &'static str {
60 match self {
61 Self::Alpha => "snapshot:alpha",
62 Self::Beta => "beta",
63 Self::Stable => "stable",
64 }
65 }
66}
67
68#[derive(Default)]
70pub struct NvdaUrl {
71 client: Client,
72 cache: Mutex<HashMap<VersionType, (String, String, Instant)>>,
74}
75
76impl NvdaUrl {
77 pub async fn get_url(&self, version_type: VersionType) -> Option<String> {
89 Some(self.get_details(version_type).await?.0)
90 }
91
92 pub async fn get_details(&self, version_type: VersionType) -> Option<(String, String)> {
104 let mut cache = self.cache.lock().await;
105 if let Some((url, sha1_hash, timestamp)) = cache.get(&version_type)
106 && timestamp.elapsed() < CACHE_TTL
107 {
108 return Some((url.clone(), sha1_hash.clone()));
109 }
110 let (url, sha1_hash) = self.fetch_url(&version_type).await?;
111 cache.insert(version_type, (url.clone(), sha1_hash.clone(), Instant::now()));
112 drop(cache);
113 Some((url, sha1_hash))
114 }
115
116 async fn fetch_url(&self, version_type: &VersionType) -> Option<(String, String)> {
117 let url = format!("https://api.nvaccess.org/nvdaUpdateCheck?versionType={}", version_type.as_str());
118 let body = self.client.get(&url).send().await.ok()?.text().await.ok()?;
119 let info = UpdateInfo::parse(&body);
120 Some((info.launcher_url?, info.launcher_hash?))
121 }
122}