1use crate::config::{Flavor, SelfUpdateChannel};
2use crate::error::DownloadError;
3#[cfg(target_os = "macos")]
4use crate::error::FilesystemError;
5use crate::network::{download_file, request_async};
6
7use isahc::AsyncReadResponseExt;
8use regex::Regex;
9use retry::delay::Fibonacci;
10use retry::{retry, Error as RetryError, OperationResult};
11use serde::Deserialize;
12
13use std::ffi::OsStr;
14use std::fs;
15use std::io;
16use std::path::{Path, PathBuf};
17
18pub fn format_interface_into_game_version(interface: &str) -> String {
23 if interface.len() == 5 {
24 let major = interface[..1].parse::<u8>();
25 let minor = interface[1..3].parse::<u8>();
26 let patch = interface[3..5].parse::<u8>();
27 if let (Ok(major), Ok(minor), Ok(patch)) = (major, minor, patch) {
28 return format!("{}.{}.{}", major, minor, patch);
29 }
30 }
31
32 interface.to_owned()
33}
34
35pub(crate) fn strip_non_digits(string: &str) -> String {
41 let re = Regex::new(r"[\D]").unwrap();
42 let stripped = re.replace_all(string, "").to_string();
43 stripped
44}
45
46#[derive(Debug, Deserialize, Clone)]
47pub struct Release {
48 pub tag_name: String,
49 pub prerelease: bool,
50 pub assets: Vec<ReleaseAsset>,
51 pub body: String,
52}
53
54#[derive(Debug, Deserialize, Clone)]
55pub struct ReleaseAsset {
56 pub name: String,
57 #[serde(rename = "browser_download_url")]
58 pub download_url: String,
59}
60
61pub async fn get_latest_release(channel: SelfUpdateChannel) -> Option<Release> {
62 log::debug!("checking for application update");
63
64 let mut resp = request_async(
65 "https://api.github.com/repos/ajour/ajour/releases",
66 vec![],
67 None,
68 )
69 .await
70 .ok()?;
71
72 let releases: Vec<Release> = resp.json().await.ok()?;
73
74 releases.into_iter().find(|r| {
75 if channel == SelfUpdateChannel::Beta {
76 true
78 } else {
79 !r.prerelease
81 }
82 })
83}
84
85pub async fn download_update_to_temp_file(
89 bin_name: String,
90 release: Release,
91) -> Result<(PathBuf, PathBuf), DownloadError> {
92 #[cfg(not(target_os = "linux"))]
93 let current_bin_path = std::env::current_exe()?;
94
95 #[cfg(target_os = "linux")]
96 let current_bin_path = PathBuf::from(
97 std::env::var("APPIMAGE").map_err(|_| DownloadError::SelfUpdateLinuxNonAppImage)?,
98 );
99
100 let download_path = current_bin_path
102 .parent()
103 .unwrap()
104 .join(&format!("tmp_{}", bin_name));
105
106 let tmp_path = current_bin_path
110 .parent()
111 .unwrap()
112 .join(&format!("tmp2_{}", bin_name));
113
114 #[cfg(target_os = "macos")]
117 {
118 let asset_name = format!("{}-macos.tar.gz", bin_name);
119
120 let asset = release
121 .assets
122 .iter()
123 .find(|a| a.name == asset_name)
124 .cloned()
125 .ok_or(DownloadError::MissingSelfUpdateRelease { bin_name })?;
126
127 let archive_path = current_bin_path.parent().unwrap().join(&asset_name);
128
129 download_file(&asset.download_url, &archive_path).await?;
130
131 extract_binary_from_tar(&archive_path, &download_path, "ajour")?;
132
133 std::fs::remove_file(&archive_path)?;
134 }
135
136 #[cfg(not(target_os = "macos"))]
138 {
139 let asset = release
140 .assets
141 .iter()
142 .find(|a| a.name == bin_name)
143 .cloned()
144 .ok_or(DownloadError::MissingSelfUpdateRelease { bin_name })?;
145
146 download_file(&asset.download_url, &download_path).await?;
147 }
148
149 #[cfg(not(target_os = "windows"))]
151 {
152 use async_std::fs;
153 use std::os::unix::fs::PermissionsExt;
154
155 let mut permissions = fs::metadata(&download_path).await?.permissions();
156 permissions.set_mode(0o755);
157 fs::set_permissions(&download_path, permissions).await?;
158 }
159
160 rename(¤t_bin_path, &tmp_path)?;
161
162 rename(&download_path, ¤t_bin_path)?;
163
164 Ok((current_bin_path, tmp_path))
165}
166
167#[cfg(target_os = "macos")]
169fn extract_binary_from_tar(
170 archive_path: &Path,
171 temp_file: &Path,
172 bin_name: &str,
173) -> Result<(), FilesystemError> {
174 use flate2::read::GzDecoder;
175 use std::fs::File;
176 use std::io::copy;
177 use tar::Archive;
178
179 let mut archive = Archive::new(GzDecoder::new(File::open(&archive_path)?));
180
181 let mut temp_file = File::create(temp_file)?;
182
183 for file in archive.entries()? {
184 let mut file = file?;
185
186 let path = file.path()?;
187
188 if let Some(name) = path.to_str() {
189 if name == bin_name {
190 copy(&mut file, &mut temp_file)?;
191
192 return Ok(());
193 }
194 }
195 }
196
197 Err(FilesystemError::BinMissingFromTar {
198 bin_name: bin_name.to_owned(),
199 })
200}
201
202pub fn wow_path_resolution(path: Option<PathBuf>) -> Option<PathBuf> {
204 if let Some(path) = path {
205 let known_folders = Flavor::ALL
207 .iter()
208 .map(|f| f.folder_name())
209 .collect::<Vec<String>>();
210
211 for folder in known_folders.iter() {
213 if path.join(folder).exists() {
214 return Some(path);
215 }
216 }
217
218 for ancestor in path.as_path().ancestors() {
220 if let Some(file_name) = ancestor.file_name() {
221 for folder in known_folders.iter() {
222 if file_name == OsStr::new(folder) {
223 return ancestor.parent().map(|p| p.to_path_buf());
224 }
225 }
226 }
227 }
228 }
229
230 None
231}
232
233pub fn rename<F, T>(from: F, to: T) -> io::Result<()>
238where
239 F: AsRef<Path>,
240 T: AsRef<Path>,
241{
242 let from = from.as_ref();
246 let to = to.as_ref();
247
248 retry(Fibonacci::from_millis(1).take(21), || {
249 match fs::rename(from, to) {
250 Ok(_) => OperationResult::Ok(()),
251 Err(e) => match e.kind() {
252 io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
253 _ => OperationResult::Err(e),
254 },
255 }
256 })
257 .map_err(|e| match e {
258 RetryError::Operation { error, .. } => error,
259 RetryError::Internal(message) => io::Error::new(io::ErrorKind::Other, message),
260 })
261}
262
263pub fn remove_file<P>(path: P) -> io::Result<()>
268where
269 P: AsRef<Path>,
270{
271 let path = path.as_ref();
275
276 retry(
277 Fibonacci::from_millis(1).take(21),
278 || match fs::remove_file(path) {
279 Ok(_) => OperationResult::Ok(()),
280 Err(e) => match e.kind() {
281 io::ErrorKind::PermissionDenied => OperationResult::Retry(e),
282 _ => OperationResult::Err(e),
283 },
284 },
285 )
286 .map_err(|e| match e {
287 RetryError::Operation { error, .. } => error,
288 RetryError::Internal(message) => io::Error::new(io::ErrorKind::Other, message),
289 })
290}
291
292pub(crate) fn truncate(s: &str, max_chars: usize) -> &str {
293 match s.char_indices().nth(max_chars) {
294 None => s,
295 Some((idx, _)) => &s[..idx],
296 }
297}
298
299pub(crate) fn regex_html_tags_to_newline() -> Regex {
300 regex::Regex::new(r"<br ?/?>|#.\s").unwrap()
301}
302
303pub(crate) fn regex_html_tags_to_space() -> Regex {
304 regex::Regex::new(r"<[^>]*>|&#?\w+;|[gl]t;").unwrap()
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_wow_path_resolution() {
313 let classic_addon_path =
314 PathBuf::from(r"/Applications/World of Warcraft/_classic_/Interface/Addons");
315 let retail_addon_path =
316 PathBuf::from(r"/Applications/World of Warcraft/_retail_/Interface/Addons");
317 let retail_interface_path =
318 PathBuf::from(r"/Applications/World of Warcraft/_retail_/Interface");
319 let classic_interface_path =
320 PathBuf::from(r"/Applications/World of Warcraft/_classic_/Interface");
321 let classic_alternate_path = PathBuf::from(r"/Applications/Wow/_classic_");
322
323 let root_alternate_path = PathBuf::from(r"/Applications/Wow");
324 let root_path = PathBuf::from(r"/Applications/World of Warcraft");
325
326 assert_eq!(
327 root_path.eq(&wow_path_resolution(Some(classic_addon_path)).unwrap()),
328 true
329 );
330 assert_eq!(
331 root_path.eq(&wow_path_resolution(Some(retail_addon_path)).unwrap()),
332 true
333 );
334 assert_eq!(
335 root_path.eq(&wow_path_resolution(Some(retail_interface_path)).unwrap()),
336 true
337 );
338 assert_eq!(
339 root_path.eq(&wow_path_resolution(Some(classic_interface_path)).unwrap()),
340 true
341 );
342 assert_eq!(
343 root_alternate_path.eq(&wow_path_resolution(Some(classic_alternate_path)).unwrap()),
344 true
345 );
346 }
347
348 #[test]
349 fn test_interface() {
350 let interface = "90001";
351 assert_eq!("9.0.1", format_interface_into_game_version(interface));
352
353 let interface = "11305";
354 assert_eq!("1.13.5", format_interface_into_game_version(interface));
355
356 let interface = "100000";
357 assert_eq!("100000", format_interface_into_game_version(interface));
358
359 let interface = "9.0.1";
360 assert_eq!("9.0.1", format_interface_into_game_version(interface));
361 }
362}