nvda_url/
lib.rs

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
13/// Direct download link for NVDA 2017.3 (Windows XP).
14pub const XP_URL: &str = "https://download.nvaccess.org/releases/2017.3/nvda_2017.3.exe";
15/// SHA1 hash for NVDA 2017.3 (Windows XP).
16pub const XP_HASH: &str = "386e7acb8cc3ecaabc8005894cf783b51a8ac7f6";
17/// Direct download link for NVDA 2023.3.4 (Windows 7).
18pub const WIN7_URL: &str = "https://download.nvaccess.org/releases/2023.3.4/nvda_2023.3.4.exe";
19/// SHA1 hash for NVDA 2023.3.4 (Windows 7).
20pub const WIN7_HASH: &str = "985a6deab01edb55fbedc9b056956e30120db290";
21
22/// NV Access has their own custom format for NVDA's update API, this lets us parse only the fields we care about out of it.
23#[derive(Debug)]
24struct UpdateInfo {
25	pub launcher_url: Option<String>,
26	/// SHA1 hash of the file pointed to by `launcher_url`.
27	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/// Represents the different NVDA release channels.
48#[derive(Clone, Copy, Eq, Hash, PartialEq, Debug)]
49pub enum VersionType {
50	/// Official stable releases.
51	Stable,
52	/// Pre-release beta versions.
53	Beta,
54	/// Snapshot alpha builds.
55	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/// Fetches and caches NVDA download URLs.
69#[derive(Default)]
70pub struct NvdaUrl {
71	client: Client,
72	/// url, hash, timestamp
73	cache: Mutex<HashMap<VersionType, (String, String, Instant)>>,
74}
75
76impl NvdaUrl {
77	/// Retrieves the latest download URL for the specified NVDA version type.
78	///
79	/// Thin wrapper around `NvdaUrl::get_details`.
80	///
81	/// # Arguments
82	///
83	/// * `version_type` - The type of NVDA version to fetch.
84	///  
85	/// # Returns
86	///
87	/// An `Option<String>` containing the URL if successful, or `None` if an error occurs.
88	pub async fn get_url(&self, version_type: VersionType) -> Option<String> {
89		Some(self.get_details(version_type).await?.0)
90	}
91
92	/// Retrieves the latest download URL and launcher hash for the specified NVDA version type.
93	///
94	/// If a cached URL is still valid, it is returned. Otherwise, a new request is made.
95	///
96	/// # Arguments
97	///
98	/// * `version_type` - The type of NVDA version to fetch.
99	///  
100	/// # Returns
101	///
102	/// An `Option<(String, String)>` containing the URL and SHA1 hash if successful, or `None` if an error occurs.
103	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}