util.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. fs::{create_dir_all, File},
  6. io::{Cursor, Read, Write},
  7. path::{Path, PathBuf},
  8. };
  9. use regex::Regex;
  10. use sha2::Digest;
  11. use url::Url;
  12. use zip::ZipArchive;
  13. pub const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
  14. pub const WEBVIEW2_OFFLINE_INSTALLER_X86_URL: &str =
  15. "https://go.microsoft.com/fwlink/?linkid=2099617";
  16. pub const WEBVIEW2_OFFLINE_INSTALLER_X64_URL: &str =
  17. "https://go.microsoft.com/fwlink/?linkid=2124701";
  18. pub const WEBVIEW2_URL_PREFIX: &str =
  19. "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/";
  20. pub const NSIS_OUTPUT_FOLDER_NAME: &str = "nsis";
  21. pub const NSIS_UPDATER_OUTPUT_FOLDER_NAME: &str = "nsis-updater";
  22. pub const WIX_OUTPUT_FOLDER_NAME: &str = "msi";
  23. pub const WIX_UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater";
  24. pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> {
  25. let agent = ureq::AgentBuilder::new().try_proxy_from_env(true).build();
  26. let response = agent.head(url).call().map_err(Box::new)?;
  27. let final_url = response.get_url();
  28. let remaining_url = final_url.strip_prefix(WEBVIEW2_URL_PREFIX).ok_or_else(|| {
  29. anyhow::anyhow!(
  30. "WebView2 URL prefix mismatch. Expected `{}`, found `{}`.",
  31. WEBVIEW2_URL_PREFIX,
  32. final_url
  33. )
  34. })?;
  35. let (guid, filename) = remaining_url.split_once('/').ok_or_else(|| {
  36. anyhow::anyhow!(
  37. "WebView2 URL format mismatch. Expected `<GUID>/<FILENAME>`, found `{}`.",
  38. remaining_url
  39. )
  40. })?;
  41. Ok((guid.into(), filename.into()))
  42. }
  43. pub fn download_webview2_bootstrapper(base_path: &Path) -> crate::Result<PathBuf> {
  44. let file_path = base_path.join("MicrosoftEdgeWebview2Setup.exe");
  45. if !file_path.exists() {
  46. std::fs::write(&file_path, download(WEBVIEW2_BOOTSTRAPPER_URL)?)?;
  47. }
  48. Ok(file_path)
  49. }
  50. pub fn download_webview2_offline_installer(base_path: &Path, arch: &str) -> crate::Result<PathBuf> {
  51. let url = if arch == "x64" {
  52. WEBVIEW2_OFFLINE_INSTALLER_X64_URL
  53. } else {
  54. WEBVIEW2_OFFLINE_INSTALLER_X86_URL
  55. };
  56. let (guid, filename) = webview2_guid_path(url)?;
  57. let dir_path = base_path.join(guid);
  58. let file_path = dir_path.join(filename);
  59. if !file_path.exists() {
  60. create_dir_all(dir_path)?;
  61. std::fs::write(&file_path, download(url)?)?;
  62. }
  63. Ok(file_path)
  64. }
  65. fn generate_github_mirror_url_from_template(github_url: &str) -> Option<String> {
  66. std::env::var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE")
  67. .ok()
  68. .and_then(|template| {
  69. let re =
  70. Regex::new(r"https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(.*)").unwrap();
  71. re.captures(github_url).map(|caps| {
  72. template
  73. .replace("<owner>", &caps[1])
  74. .replace("<repo>", &caps[2])
  75. .replace("<version>", &caps[3])
  76. .replace("<asset>", &caps[4])
  77. })
  78. })
  79. }
  80. fn generate_github_mirror_url_from_base(github_url: &str) -> Option<String> {
  81. std::env::var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR")
  82. .ok()
  83. .and_then(|cdn| Url::parse(&cdn).ok())
  84. .map(|mut cdn| {
  85. cdn.set_path(github_url);
  86. cdn.to_string()
  87. })
  88. }
  89. fn generate_github_alternative_url(url: &str) -> Option<(ureq::Agent, String)> {
  90. if !url.starts_with("https://github.com/") {
  91. return None;
  92. }
  93. generate_github_mirror_url_from_template(url)
  94. .or_else(|| generate_github_mirror_url_from_base(url))
  95. .map(|alt_url| (ureq::AgentBuilder::new().build(), alt_url))
  96. }
  97. fn create_agent_and_url(url: &str) -> (ureq::Agent, String) {
  98. generate_github_alternative_url(url).unwrap_or((
  99. ureq::AgentBuilder::new().try_proxy_from_env(true).build(),
  100. url.to_owned(),
  101. ))
  102. }
  103. pub fn download(url: &str) -> crate::Result<Vec<u8>> {
  104. let (agent, final_url) = create_agent_and_url(url);
  105. log::info!(action = "Downloading"; "{}", final_url);
  106. let response = agent.get(&final_url).call().map_err(Box::new)?;
  107. let mut bytes = Vec::new();
  108. response.into_reader().read_to_end(&mut bytes)?;
  109. Ok(bytes)
  110. }
  111. #[derive(Clone, Copy)]
  112. pub enum HashAlgorithm {
  113. #[cfg(target_os = "windows")]
  114. Sha256,
  115. Sha1,
  116. }
  117. /// Function used to download a file and checks SHA256 to verify the download.
  118. pub fn download_and_verify(
  119. url: &str,
  120. hash: &str,
  121. hash_algorithm: HashAlgorithm,
  122. ) -> crate::Result<Vec<u8>> {
  123. let data = download(url)?;
  124. log::info!("validating hash");
  125. verify_hash(&data, hash, hash_algorithm)?;
  126. Ok(data)
  127. }
  128. pub fn verify_hash(data: &[u8], hash: &str, hash_algorithm: HashAlgorithm) -> crate::Result<()> {
  129. match hash_algorithm {
  130. #[cfg(target_os = "windows")]
  131. HashAlgorithm::Sha256 => {
  132. let hasher = sha2::Sha256::new();
  133. verify_data_with_hasher(data, hash, hasher)
  134. }
  135. HashAlgorithm::Sha1 => {
  136. let hasher = sha1::Sha1::new();
  137. verify_data_with_hasher(data, hash, hasher)
  138. }
  139. }
  140. }
  141. fn verify_data_with_hasher(data: &[u8], hash: &str, mut hasher: impl Digest) -> crate::Result<()> {
  142. hasher.update(data);
  143. let url_hash = hasher.finalize().to_vec();
  144. let expected_hash = hex::decode(hash)?;
  145. if expected_hash == url_hash {
  146. Ok(())
  147. } else {
  148. Err(crate::Error::HashError)
  149. }
  150. }
  151. pub fn verify_file_hash<P: AsRef<Path>>(
  152. path: P,
  153. hash: &str,
  154. hash_algorithm: HashAlgorithm,
  155. ) -> crate::Result<()> {
  156. let data = std::fs::read(path)?;
  157. verify_hash(&data, hash, hash_algorithm)
  158. }
  159. /// Extracts the zips from memory into a usable path.
  160. #[allow(dead_code)]
  161. pub fn extract_zip(data: &[u8], path: &Path) -> crate::Result<()> {
  162. let cursor = Cursor::new(data);
  163. let mut zipa = ZipArchive::new(cursor)?;
  164. for i in 0..zipa.len() {
  165. let mut file = zipa.by_index(i)?;
  166. if let Some(name) = file.enclosed_name() {
  167. let dest_path = path.join(name);
  168. if file.is_dir() {
  169. create_dir_all(&dest_path)?;
  170. continue;
  171. }
  172. let parent = dest_path.parent().expect("Failed to get parent");
  173. if !parent.exists() {
  174. create_dir_all(parent)?;
  175. }
  176. let mut buff: Vec<u8> = Vec::new();
  177. file.read_to_end(&mut buff)?;
  178. let mut fileout = File::create(dest_path).expect("Failed to open file");
  179. fileout.write_all(&buff)?;
  180. }
  181. }
  182. Ok(())
  183. }
  184. #[cfg(target_os = "windows")]
  185. pub fn os_bitness<'a>() -> Option<&'a str> {
  186. use windows_sys::Win32::System::SystemInformation::{
  187. GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO,
  188. };
  189. let mut system_info: SYSTEM_INFO = unsafe { std::mem::zeroed() };
  190. unsafe { GetNativeSystemInfo(&mut system_info) };
  191. match unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture } {
  192. PROCESSOR_ARCHITECTURE_INTEL => Some("x86"),
  193. PROCESSOR_ARCHITECTURE_AMD64 => Some("x64"),
  194. _ => None,
  195. }
  196. }
  197. #[cfg(test)]
  198. mod tests {
  199. use super::generate_github_mirror_url_from_template;
  200. use std::env;
  201. const GITHUB_ASSET_URL: &str =
  202. "https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip";
  203. const NON_GITHUB_ASSET_URL: &str = "https://someotherwebsite.com/somefile.zip";
  204. #[test]
  205. fn test_generate_mirror_url_no_env_var() {
  206. env::remove_var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE");
  207. assert!(generate_github_mirror_url_from_template(GITHUB_ASSET_URL).is_none());
  208. }
  209. #[test]
  210. fn test_generate_mirror_url_non_github_url() {
  211. env::set_var(
  212. "TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE",
  213. "https://mirror.example.com/<owner>/<repo>/releases/download/<version>/<asset>",
  214. );
  215. assert!(generate_github_mirror_url_from_template(NON_GITHUB_ASSET_URL).is_none());
  216. }
  217. struct TestCase {
  218. template: &'static str,
  219. expected_url: &'static str,
  220. }
  221. #[test]
  222. fn test_generate_mirror_url_correctly() {
  223. let test_cases = vec![
  224. TestCase {
  225. template: "https://mirror.example.com/<owner>/<repo>/releases/download/<version>/<asset>",
  226. expected_url: "https://mirror.example.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip",
  227. },
  228. TestCase {
  229. template: "https://mirror.example.com/<asset>",
  230. expected_url: "https://mirror.example.com/wix311-binaries.zip",
  231. },
  232. ];
  233. for case in test_cases {
  234. env::set_var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE", case.template);
  235. assert_eq!(
  236. generate_github_mirror_url_from_template(GITHUB_ASSET_URL),
  237. Some(case.expected_url.to_string())
  238. );
  239. }
  240. }
  241. }