core.rs 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use super::error::{Error, Result};
  5. use crate::api::{
  6. file::{ArchiveFormat, Extract, Move},
  7. http::{ClientBuilder, HttpRequestBuilder},
  8. version,
  9. };
  10. use base64::decode;
  11. use http::StatusCode;
  12. use minisign_verify::{PublicKey, Signature};
  13. use tauri_utils::{platform::current_exe, Env};
  14. use std::{
  15. collections::HashMap,
  16. env,
  17. io::{Cursor, Read, Seek},
  18. path::{Path, PathBuf},
  19. str::from_utf8,
  20. };
  21. #[cfg(not(target_os = "macos"))]
  22. use std::ffi::OsStr;
  23. #[cfg(not(target_os = "windows"))]
  24. use crate::api::file::Compression;
  25. #[cfg(target_os = "windows")]
  26. use std::{
  27. fs::read_dir,
  28. process::{exit, Command},
  29. };
  30. #[derive(Debug)]
  31. pub struct RemoteRelease {
  32. /// Version to install
  33. pub version: String,
  34. /// Release date
  35. pub date: String,
  36. /// Download URL for current platform
  37. pub download_url: String,
  38. /// Update short description
  39. pub body: Option<String>,
  40. /// Optional signature for the current platform
  41. pub signature: Option<String>,
  42. #[cfg(target_os = "windows")]
  43. /// Optional: Windows only try to use elevated task
  44. pub with_elevated_task: bool,
  45. }
  46. impl RemoteRelease {
  47. // Read JSON and confirm this is a valid Schema
  48. fn from_release(release: &serde_json::Value, target: &str) -> Result<RemoteRelease> {
  49. // Version or name is required for static and dynamic JSON
  50. // if `version` is not announced, we fallback to `name` (can be the tag name example v1.0.0)
  51. let version = match release.get("version") {
  52. Some(version) => version
  53. .as_str()
  54. .ok_or_else(|| {
  55. Error::RemoteMetadata("Unable to extract `version` from remote server".into())
  56. })?
  57. .trim_start_matches('v')
  58. .to_string(),
  59. None => release
  60. .get("name")
  61. .ok_or_else(|| Error::RemoteMetadata("Release missing `name` and `version`".into()))?
  62. .as_str()
  63. .ok_or_else(|| {
  64. Error::RemoteMetadata("Unable to extract `name` from remote server`".into())
  65. })?
  66. .trim_start_matches('v')
  67. .to_string(),
  68. };
  69. // pub_date is required default is: `N/A` if not provided by the remote JSON
  70. let date = release
  71. .get("pub_date")
  72. .and_then(|v| v.as_str())
  73. .unwrap_or("N/A")
  74. .to_string();
  75. // body is optional to build our update
  76. let body = release
  77. .get("notes")
  78. .map(|notes| notes.as_str().unwrap_or("").to_string());
  79. // signature is optional to build our update
  80. let mut signature = release
  81. .get("signature")
  82. .map(|signature| signature.as_str().unwrap_or("").to_string());
  83. let download_url;
  84. #[cfg(target_os = "windows")]
  85. let with_elevated_task;
  86. match release.get("platforms") {
  87. //
  88. // Did we have a platforms field?
  89. // If we did, that mean it's a static JSON.
  90. // The main difference with STATIC and DYNAMIC is static announce ALL platforms
  91. // and dynamic announce only the current platform.
  92. //
  93. // This could be used if you do NOT want an update server and use
  94. // a GIST, S3 or any static JSON file to announce your updates.
  95. //
  96. // Notes:
  97. // Dynamic help to reduce bandwidth usage or to intelligently update your clients
  98. // based on the request you give. The server can remotely drive behaviors like
  99. // rolling back or phased rollouts.
  100. //
  101. Some(platforms) => {
  102. // make sure we have our target available
  103. if let Some(current_target_data) = platforms.get(target) {
  104. // use provided signature if available
  105. signature = current_target_data
  106. .get("signature")
  107. .map(|found_signature| found_signature.as_str().unwrap_or("").to_string());
  108. // Download URL is required
  109. download_url = current_target_data
  110. .get("url")
  111. .ok_or_else(|| Error::RemoteMetadata("Release missing `url`".into()))?
  112. .as_str()
  113. .ok_or_else(|| {
  114. Error::RemoteMetadata("Unable to extract `url` from remote server`".into())
  115. })?
  116. .to_string();
  117. #[cfg(target_os = "windows")]
  118. {
  119. with_elevated_task = current_target_data
  120. .get("with_elevated_task")
  121. .and_then(|v| v.as_bool())
  122. .unwrap_or_default();
  123. }
  124. } else {
  125. // make sure we have an available platform from the static
  126. return Err(Error::RemoteMetadata("Platform not available".into()));
  127. }
  128. }
  129. // We don't have the `platforms` field announced, let's assume our
  130. // download URL is at the root of the JSON.
  131. None => {
  132. download_url = release
  133. .get("url")
  134. .ok_or_else(|| Error::RemoteMetadata("Release missing `url`".into()))?
  135. .as_str()
  136. .ok_or_else(|| {
  137. Error::RemoteMetadata("Unable to extract `url` from remote server`".into())
  138. })?
  139. .to_string();
  140. #[cfg(target_os = "windows")]
  141. {
  142. with_elevated_task = match release.get("with_elevated_task") {
  143. Some(with_elevated_task) => with_elevated_task.as_bool().unwrap_or(false),
  144. None => false,
  145. };
  146. }
  147. }
  148. }
  149. // Return our formatted release
  150. Ok(RemoteRelease {
  151. version,
  152. date,
  153. download_url,
  154. body,
  155. signature,
  156. #[cfg(target_os = "windows")]
  157. with_elevated_task,
  158. })
  159. }
  160. }
  161. #[derive(Debug)]
  162. pub struct UpdateBuilder<'a> {
  163. /// Environment information.
  164. pub env: Env,
  165. /// Current version we are running to compare with announced version
  166. pub current_version: &'a str,
  167. /// The URLs to checks updates. We suggest at least one fallback on a different domain.
  168. pub urls: Vec<String>,
  169. /// The platform the updater will check and install the update. Default is from `get_updater_target`
  170. pub target: Option<String>,
  171. /// The current executable path. Default is automatically extracted.
  172. pub executable_path: Option<PathBuf>,
  173. }
  174. // Create new updater instance and return an Update
  175. impl<'a> UpdateBuilder<'a> {
  176. pub fn new(env: Env) -> Self {
  177. UpdateBuilder {
  178. env,
  179. urls: Vec::new(),
  180. target: None,
  181. executable_path: None,
  182. current_version: env!("CARGO_PKG_VERSION"),
  183. }
  184. }
  185. #[allow(dead_code)]
  186. pub fn url(mut self, url: String) -> Self {
  187. self.urls.push(
  188. percent_encoding::percent_decode(url.as_bytes())
  189. .decode_utf8_lossy()
  190. .to_string(),
  191. );
  192. self
  193. }
  194. /// Add multiple URLS at once inside a Vec for future reference
  195. pub fn urls(mut self, urls: &[String]) -> Self {
  196. let mut formatted_vec: Vec<String> = Vec::new();
  197. for url in urls {
  198. formatted_vec.push(
  199. percent_encoding::percent_decode(url.as_bytes())
  200. .decode_utf8_lossy()
  201. .to_string(),
  202. );
  203. }
  204. self.urls = formatted_vec;
  205. self
  206. }
  207. /// Set the current app version, used to compare against the latest available version.
  208. /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
  209. pub fn current_version(mut self, ver: &'a str) -> Self {
  210. self.current_version = ver;
  211. self
  212. }
  213. /// Set the target (os)
  214. /// win32, win64, darwin and linux are currently supported
  215. #[allow(dead_code)]
  216. pub fn target(mut self, target: &str) -> Self {
  217. self.target = Some(target.to_owned());
  218. self
  219. }
  220. /// Set the executable path
  221. #[allow(dead_code)]
  222. pub fn executable_path<A: AsRef<Path>>(mut self, executable_path: A) -> Self {
  223. self.executable_path = Some(PathBuf::from(executable_path.as_ref()));
  224. self
  225. }
  226. pub async fn build(self) -> Result<Update> {
  227. let mut remote_release: Option<RemoteRelease> = None;
  228. // make sure we have at least one url
  229. if self.urls.is_empty() {
  230. return Err(Error::Builder(
  231. "Unable to check update, `url` is required.".into(),
  232. ));
  233. };
  234. // set current version if not set
  235. let current_version = self.current_version;
  236. // If no executable path provided, we use current_exe from tauri_utils
  237. let executable_path = self.executable_path.unwrap_or(current_exe()?);
  238. // Did the target is provided by the config?
  239. // Should be: linux, darwin, win32 or win64
  240. let target = self
  241. .target
  242. .or_else(get_updater_target)
  243. .ok_or(Error::UnsupportedPlatform)?;
  244. // Get the extract_path from the provided executable_path
  245. let extract_path = extract_path_from_executable(&self.env, &executable_path);
  246. // Set SSL certs for linux if they aren't available.
  247. // We do not require to recheck in the download_and_install as we use
  248. // ENV variables, we can expect them to be set for the second call.
  249. #[cfg(target_os = "linux")]
  250. {
  251. if env::var_os("SSL_CERT_FILE").is_none() {
  252. env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
  253. }
  254. if env::var_os("SSL_CERT_DIR").is_none() {
  255. env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
  256. }
  257. }
  258. // Allow fallback if more than 1 urls is provided
  259. let mut last_error: Option<Error> = None;
  260. for url in &self.urls {
  261. // replace {{current_version}} and {{target}} in the provided URL
  262. // this is usefull if we need to query example
  263. // https://releases.myapp.com/update/{{target}}/{{current_version}}
  264. // will be transleted into ->
  265. // https://releases.myapp.com/update/darwin/1.0.0
  266. // The main objective is if the update URL is defined via the Cargo.toml
  267. // the URL will be generated dynamicly
  268. let fixed_link = str::replace(
  269. &str::replace(url, "{{current_version}}", current_version),
  270. "{{target}}",
  271. &target,
  272. );
  273. // we want JSON only
  274. let mut headers = HashMap::new();
  275. headers.insert("Accept".into(), "application/json".into());
  276. let resp = ClientBuilder::new()
  277. .build()?
  278. .send(
  279. HttpRequestBuilder::new("GET", &fixed_link)?
  280. .headers(headers)
  281. // wait 20sec for the firewall
  282. .timeout(20),
  283. )
  284. .await;
  285. // If we got a success, we stop the loop
  286. // and we set our remote_release variable
  287. if let Ok(res) = resp {
  288. let res = res.read().await?;
  289. // got status code 2XX
  290. if StatusCode::from_u16(res.status)
  291. .map_err(|e| Error::Builder(e.to_string()))?
  292. .is_success()
  293. {
  294. // if we got 204
  295. if StatusCode::NO_CONTENT.as_u16() == res.status {
  296. // return with `UpToDate` error
  297. // we should catch on the client
  298. return Err(Error::UpToDate);
  299. };
  300. // Convert the remote result to our local struct
  301. let built_release = RemoteRelease::from_release(&res.data, &target);
  302. // make sure all went well and the remote data is compatible
  303. // with what we need locally
  304. match built_release {
  305. Ok(release) => {
  306. last_error = None;
  307. remote_release = Some(release);
  308. break;
  309. }
  310. Err(err) => last_error = Some(err),
  311. }
  312. } // if status code is not 2XX we keep loopin' our urls
  313. }
  314. }
  315. // Last error is cleaned on success -- shouldn't be triggered if
  316. // we have a successful call
  317. if let Some(error) = last_error {
  318. return Err(Error::Network(error.to_string()));
  319. }
  320. // Extracted remote metadata
  321. let final_release = remote_release.ok_or_else(|| {
  322. Error::RemoteMetadata("Unable to extract update metadata from the remote server.".into())
  323. })?;
  324. // did the announced version is greated than our current one?
  325. let should_update =
  326. version::is_greater(current_version, &final_release.version).unwrap_or(false);
  327. // create our new updater
  328. Ok(Update {
  329. env: self.env,
  330. target,
  331. extract_path,
  332. should_update,
  333. version: final_release.version,
  334. date: final_release.date,
  335. current_version: self.current_version.to_owned(),
  336. download_url: final_release.download_url,
  337. body: final_release.body,
  338. signature: final_release.signature,
  339. #[cfg(target_os = "windows")]
  340. with_elevated_task: final_release.with_elevated_task,
  341. })
  342. }
  343. }
  344. pub fn builder<'a>(env: Env) -> UpdateBuilder<'a> {
  345. UpdateBuilder::new(env)
  346. }
  347. #[derive(Debug, Clone)]
  348. pub struct Update {
  349. /// Environment information.
  350. pub env: Env,
  351. /// Update description
  352. pub body: Option<String>,
  353. /// Should we update or not
  354. pub should_update: bool,
  355. /// Version announced
  356. pub version: String,
  357. /// Running version
  358. pub current_version: String,
  359. /// Update publish date
  360. pub date: String,
  361. /// Target
  362. #[allow(dead_code)]
  363. target: String,
  364. /// Extract path
  365. extract_path: PathBuf,
  366. /// Download URL announced
  367. download_url: String,
  368. /// Signature announced
  369. signature: Option<String>,
  370. #[cfg(target_os = "windows")]
  371. /// Optional: Windows only try to use elevated task
  372. /// Default to false
  373. with_elevated_task: bool,
  374. }
  375. impl Update {
  376. // Download and install our update
  377. // @todo(lemarier): Split into download and install (two step) but need to be thread safe
  378. pub async fn download_and_install(&self, pub_key: String) -> Result {
  379. // download url for selected release
  380. let url = self.download_url.as_str();
  381. // extract path
  382. let extract_path = &self.extract_path;
  383. // make sure we can install the update on linux
  384. // We fail here because later we can add more linux support
  385. // actually if we use APPIMAGE, our extract path should already
  386. // be set with our APPIMAGE env variable, we don't need to do
  387. // anythin with it yet
  388. #[cfg(target_os = "linux")]
  389. if self.env.appimage.is_none() {
  390. return Err(Error::UnsupportedPlatform);
  391. }
  392. // set our headers
  393. let mut headers = HashMap::new();
  394. headers.insert("Accept".into(), "application/octet-stream".into());
  395. headers.insert("User-Agent".into(), "tauri/updater".into());
  396. // Create our request
  397. let resp = ClientBuilder::new()
  398. .build()?
  399. .send(
  400. HttpRequestBuilder::new("GET", url)?
  401. .headers(headers)
  402. // wait 20sec for the firewall
  403. .timeout(20),
  404. )
  405. .await?
  406. .bytes()
  407. .await?;
  408. // make sure it's success
  409. if !StatusCode::from_u16(resp.status)
  410. .map_err(|e| Error::Network(e.to_string()))?
  411. .is_success()
  412. {
  413. return Err(Error::Network(format!(
  414. "Download request failed with status: {}",
  415. resp.status
  416. )));
  417. }
  418. // create memory buffer from our archive (Seek + Read)
  419. let mut archive_buffer = Cursor::new(resp.data);
  420. // We need an announced signature by the server
  421. // if there is no signature, bail out.
  422. if let Some(signature) = &self.signature {
  423. // we make sure the archive is valid and signed with the private key linked with the publickey
  424. verify_signature(&mut archive_buffer, signature, &pub_key)?;
  425. } else {
  426. // We have a public key inside our source file, but not announced by the server,
  427. // we assume this update is NOT valid.
  428. return Err(Error::MissingUpdaterSignature);
  429. }
  430. // we copy the files depending of the operating system
  431. // we run the setup, appimage re-install or overwrite the
  432. // macos .app
  433. #[cfg(target_os = "windows")]
  434. copy_files_and_run(archive_buffer, extract_path, self.with_elevated_task)?;
  435. #[cfg(not(target_os = "windows"))]
  436. copy_files_and_run(archive_buffer, extract_path)?;
  437. // We are done!
  438. Ok(())
  439. }
  440. }
  441. // Linux (AppImage)
  442. // ### Expected structure:
  443. // ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
  444. // │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
  445. // └── ...
  446. // We should have an AppImage already installed to be able to copy and install
  447. // the extract_path is the current AppImage path
  448. // tmp_dir is where our new AppImage is found
  449. #[cfg(target_os = "linux")]
  450. fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) -> Result {
  451. let tmp_dir = tempfile::Builder::new()
  452. .prefix("tauri_current_app")
  453. .tempdir()?;
  454. let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
  455. // create a backup of our current app image
  456. Move::from_source(extract_path).to_dest(tmp_app_image)?;
  457. // extract the buffer to the tmp_dir
  458. // we extract our signed archive into our final directory without any temp file
  459. let mut extractor =
  460. Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
  461. for file in extractor.files()? {
  462. if file.extension() == Some(OsStr::new("AppImage")) {
  463. // if something went wrong during the extraction, we should restore previous app
  464. if let Err(err) = extractor.extract_file(extract_path, &file) {
  465. Move::from_source(tmp_app_image).to_dest(extract_path)?;
  466. return Err(Error::Extract(err.to_string()));
  467. }
  468. // early finish we have everything we need here
  469. return Ok(());
  470. }
  471. }
  472. Ok(())
  473. }
  474. // Windows
  475. // ### Expected structure:
  476. // ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
  477. // │ └──[AppName]_[version]_x64.msi # Application MSI
  478. // └── ...
  479. // ## MSI
  480. // Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
  481. // To replace current version of the application. In later version we'll offer
  482. // incremental update to push specific binaries.
  483. // ## EXE
  484. // Update server can provide a custom EXE (installer) who can run any task.
  485. #[cfg(target_os = "windows")]
  486. #[allow(clippy::unnecessary_wraps)]
  487. fn copy_files_and_run<R: Read + Seek>(
  488. archive_buffer: R,
  489. _extract_path: &Path,
  490. with_elevated_task: bool,
  491. ) -> Result {
  492. // FIXME: We need to create a memory buffer with the MSI and then run it.
  493. // (instead of extracting the MSI to a temp path)
  494. //
  495. // The tricky part is the MSI need to be exposed and spawned so the memory allocation
  496. // shouldn't drop but we should be able to pass the reference so we can drop it once the installation
  497. // is done, otherwise we have a huge memory leak.
  498. let tmp_dir = tempfile::Builder::new().tempdir()?.into_path();
  499. // extract the buffer to the tmp_dir
  500. // we extract our signed archive into our final directory without any temp file
  501. let mut extractor = Extract::from_cursor(archive_buffer, ArchiveFormat::Zip);
  502. // extract the msi
  503. extractor.extract_into(&tmp_dir)?;
  504. let paths = read_dir(&tmp_dir)?;
  505. // This consumes the TempDir without deleting directory on the filesystem,
  506. // meaning that the directory will no longer be automatically deleted.
  507. for path in paths {
  508. let found_path = path?.path();
  509. // we support 2 type of files exe & msi for now
  510. // If it's an `exe` we expect an installer not a runtime.
  511. if found_path.extension() == Some(OsStr::new("exe")) {
  512. // Run the EXE
  513. Command::new(found_path)
  514. .spawn()
  515. .expect("installer failed to start");
  516. exit(0);
  517. } else if found_path.extension() == Some(OsStr::new("msi")) {
  518. if with_elevated_task {
  519. if let Some(bin_name) = current_exe()
  520. .ok()
  521. .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
  522. .and_then(|s| s.into_string().ok())
  523. {
  524. let product_name = bin_name.replace(".exe", "");
  525. // Check if there is a task that enables the updater to skip the UAC prompt
  526. let update_task_name = format!("Update {} - Skip UAC", product_name);
  527. if let Ok(status) = Command::new("schtasks")
  528. .arg("/QUERY")
  529. .arg("/TN")
  530. .arg(update_task_name.clone())
  531. .status()
  532. {
  533. if status.success() {
  534. // Rename the MSI to the match file name the Skip UAC task is expecting it to be
  535. let temp_msi = tmp_dir.with_file_name(bin_name).with_extension("msi");
  536. Move::from_source(&found_path)
  537. .to_dest(&temp_msi)
  538. .expect("Unable to move update MSI");
  539. let exit_status = Command::new("schtasks")
  540. .arg("/RUN")
  541. .arg("/TN")
  542. .arg(update_task_name)
  543. .status()
  544. .expect("failed to start updater task");
  545. if exit_status.success() {
  546. // Successfully launched task that skips the UAC prompt
  547. exit(0);
  548. }
  549. }
  550. // Failed to run update task. Following UAC Path
  551. }
  552. }
  553. }
  554. // restart should be handled by WIX as we exit the process
  555. Command::new("msiexec.exe")
  556. .arg("/i")
  557. .arg(found_path)
  558. // quiet basic UI with prompt at the end
  559. .arg("/qb+")
  560. .spawn()
  561. .expect("installer failed to start");
  562. exit(0);
  563. }
  564. }
  565. Ok(())
  566. }
  567. // MacOS
  568. // ### Expected structure:
  569. // ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
  570. // │ └──[AppName].app # Main application
  571. // │ └── Contents # Application contents...
  572. // │ └── ...
  573. // └── ...
  574. #[cfg(target_os = "macos")]
  575. fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) -> Result {
  576. let mut extracted_files: Vec<PathBuf> = Vec::new();
  577. // extract the buffer to the tmp_dir
  578. // we extract our signed archive into our final directory without any temp file
  579. let mut extractor =
  580. Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
  581. // the first file in the tar.gz will always be
  582. // <app_name>/Contents
  583. let all_files = extractor.files()?;
  584. let tmp_dir = tempfile::Builder::new()
  585. .prefix("tauri_current_app")
  586. .tempdir()?;
  587. // create backup of our current app
  588. Move::from_source(extract_path).to_dest(tmp_dir.path())?;
  589. // extract all the files
  590. for file in all_files {
  591. // skip the first folder (should be the app name)
  592. let collected_path: PathBuf = file.iter().skip(1).collect();
  593. let extraction_path = extract_path.join(collected_path);
  594. // if something went wrong during the extraction, we should restore previous app
  595. if let Err(err) = extractor.extract_file(&extraction_path, &file) {
  596. for file in extracted_files {
  597. // delete all the files we extracted
  598. if file.is_dir() {
  599. std::fs::remove_dir(file)?;
  600. } else {
  601. std::fs::remove_file(file)?;
  602. }
  603. }
  604. Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
  605. return Err(Error::Extract(err.to_string()));
  606. }
  607. extracted_files.push(extraction_path);
  608. }
  609. Ok(())
  610. }
  611. /// Returns a target os
  612. /// We do not use a helper function like the target_triple
  613. /// from tauri-utils because this function return `None` if
  614. /// the updater do not support the platform.
  615. ///
  616. /// Available target: `linux, darwin, win32, win64`
  617. pub fn get_updater_target() -> Option<String> {
  618. if cfg!(target_os = "linux") {
  619. Some("linux".into())
  620. } else if cfg!(target_os = "macos") {
  621. Some("darwin".into())
  622. } else if cfg!(target_os = "windows") {
  623. if cfg!(target_pointer_width = "32") {
  624. Some("win32".into())
  625. } else {
  626. Some("win64".into())
  627. }
  628. } else {
  629. None
  630. }
  631. }
  632. /// Get the extract_path from the provided executable_path
  633. #[allow(unused_variables)]
  634. pub fn extract_path_from_executable(env: &Env, executable_path: &Path) -> PathBuf {
  635. // Return the path of the current executable by default
  636. // Example C:\Program Files\My App\
  637. let extract_path = executable_path
  638. .parent()
  639. .map(PathBuf::from)
  640. .expect("Can't determine extract path");
  641. // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
  642. // We need to get /Applications/<app>.app
  643. // todo(lemarier): Need a better way here
  644. // Maybe we could search for <*.app> to get the right path
  645. #[cfg(target_os = "macos")]
  646. if extract_path
  647. .display()
  648. .to_string()
  649. .contains("Contents/MacOS")
  650. {
  651. return extract_path
  652. .parent()
  653. .map(PathBuf::from)
  654. .expect("Unable to find the extract path")
  655. .parent()
  656. .map(PathBuf::from)
  657. .expect("Unable to find the extract path");
  658. }
  659. // We should use APPIMAGE exposed env variable
  660. // This is where our APPIMAGE should sit and should be replaced
  661. #[cfg(target_os = "linux")]
  662. if let Some(app_image_path) = &env.appimage {
  663. return PathBuf::from(app_image_path);
  664. }
  665. extract_path
  666. }
  667. // Convert base64 to string and prevent failing
  668. fn base64_to_string(base64_string: &str) -> Result<String> {
  669. let decoded_string = &decode(base64_string)?;
  670. let result = from_utf8(decoded_string)?.to_string();
  671. Ok(result)
  672. }
  673. // Validate signature
  674. // need to be public because its been used
  675. // by our tests in the bundler
  676. //
  677. // NOTE: The buffer position is not reset.
  678. pub fn verify_signature<R>(
  679. archive_reader: &mut R,
  680. release_signature: &str,
  681. pub_key: &str,
  682. ) -> Result<bool>
  683. where
  684. R: Read,
  685. {
  686. // we need to convert the pub key
  687. let pub_key_decoded = base64_to_string(pub_key)?;
  688. let public_key = PublicKey::decode(&pub_key_decoded)?;
  689. let signature_base64_decoded = base64_to_string(release_signature)?;
  690. let signature = Signature::decode(&signature_base64_decoded)?;
  691. // read all bytes until EOF in the buffer
  692. let mut data = Vec::new();
  693. archive_reader.read_to_end(&mut data)?;
  694. // Validate signature or bail out
  695. public_key.verify(&data, &signature, true)?;
  696. Ok(true)
  697. }
  698. #[cfg(test)]
  699. mod test {
  700. use super::*;
  701. #[cfg(target_os = "macos")]
  702. use std::fs::File;
  703. macro_rules! block {
  704. ($e:expr) => {
  705. tokio_test::block_on($e)
  706. };
  707. }
  708. fn generate_sample_raw_json() -> String {
  709. r#"{
  710. "version": "v2.0.0",
  711. "notes": "Test version !",
  712. "pub_date": "2020-06-22T19:25:57Z",
  713. "platforms": {
  714. "darwin": {
  715. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
  716. "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz"
  717. },
  718. "linux": {
  719. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K",
  720. "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
  721. },
  722. "win64": {
  723. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K",
  724. "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
  725. }
  726. }
  727. }"#.into()
  728. }
  729. fn generate_sample_platform_json(
  730. version: &str,
  731. public_signature: &str,
  732. download_url: &str,
  733. ) -> String {
  734. format!(
  735. r#"
  736. {{
  737. "name": "v{}",
  738. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  739. "pub_date": "2020-06-25T14:14:19Z",
  740. "signature": "{}",
  741. "url": "{}"
  742. }}
  743. "#,
  744. version, public_signature, download_url
  745. )
  746. }
  747. fn generate_sample_with_elevated_task_platform_json(
  748. version: &str,
  749. public_signature: &str,
  750. download_url: &str,
  751. with_elevated_task: bool,
  752. ) -> String {
  753. format!(
  754. r#"
  755. {{
  756. "name": "v{}",
  757. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  758. "pub_date": "2020-06-25T14:14:19Z",
  759. "signature": "{}",
  760. "url": "{}",
  761. "with_elevated_task": "{}"
  762. }}
  763. "#,
  764. version, public_signature, download_url, with_elevated_task
  765. )
  766. }
  767. fn generate_sample_bad_json() -> String {
  768. r#"{
  769. "version": "v0.0.3",
  770. "notes": "Blablaa",
  771. "date": "2020-02-20T15:41:00Z",
  772. "download_link": "https://github.com/lemarier/tauri-test/releases/download/v0.0.1/update3.tar.gz"
  773. }"#.into()
  774. }
  775. #[test]
  776. fn simple_http_updater() {
  777. let _m = mockito::mock("GET", "/")
  778. .with_status(200)
  779. .with_header("content-type", "application/json")
  780. .with_body(generate_sample_raw_json())
  781. .create();
  782. let check_update = block!(builder(Default::default())
  783. .current_version("0.0.0")
  784. .url(mockito::server_url())
  785. .build());
  786. assert!(check_update.is_ok());
  787. let updater = check_update.expect("Can't check update");
  788. assert!(updater.should_update);
  789. }
  790. #[test]
  791. fn simple_http_updater_raw_json() {
  792. let _m = mockito::mock("GET", "/")
  793. .with_status(200)
  794. .with_header("content-type", "application/json")
  795. .with_body(generate_sample_raw_json())
  796. .create();
  797. let check_update = block!(builder(Default::default())
  798. .current_version("0.0.0")
  799. .url(mockito::server_url())
  800. .build());
  801. assert!(check_update.is_ok());
  802. let updater = check_update.expect("Can't check update");
  803. assert!(updater.should_update);
  804. }
  805. #[test]
  806. fn simple_http_updater_raw_json_win64() {
  807. let _m = mockito::mock("GET", "/")
  808. .with_status(200)
  809. .with_header("content-type", "application/json")
  810. .with_body(generate_sample_raw_json())
  811. .create();
  812. let check_update = block!(builder(Default::default())
  813. .current_version("0.0.0")
  814. .target("win64")
  815. .url(mockito::server_url())
  816. .build());
  817. assert!(check_update.is_ok());
  818. let updater = check_update.expect("Can't check update");
  819. assert!(updater.should_update);
  820. assert_eq!(updater.version, "2.0.0");
  821. assert_eq!(updater.signature, Some("dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K".into()));
  822. assert_eq!(
  823. updater.download_url,
  824. "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
  825. );
  826. }
  827. #[test]
  828. fn simple_http_updater_raw_json_uptodate() {
  829. let _m = mockito::mock("GET", "/")
  830. .with_status(200)
  831. .with_header("content-type", "application/json")
  832. .with_body(generate_sample_raw_json())
  833. .create();
  834. let check_update = block!(builder(Default::default())
  835. .current_version("10.0.0")
  836. .url(mockito::server_url())
  837. .build());
  838. assert!(check_update.is_ok());
  839. let updater = check_update.expect("Can't check update");
  840. assert!(!updater.should_update);
  841. }
  842. #[test]
  843. fn simple_http_updater_without_version() {
  844. let _m = mockito::mock("GET", "/darwin/1.0.0")
  845. .with_status(200)
  846. .with_header("content-type", "application/json")
  847. .with_body(generate_sample_platform_json(
  848. "2.0.0",
  849. "SampleTauriKey",
  850. "https://tauri.studio",
  851. ))
  852. .create();
  853. let check_update = block!(builder(Default::default())
  854. .current_version("1.0.0")
  855. .url(format!(
  856. "{}/darwin/{{{{current_version}}}}",
  857. mockito::server_url()
  858. ))
  859. .build());
  860. assert!(check_update.is_ok());
  861. let updater = check_update.expect("Can't check update");
  862. assert!(updater.should_update);
  863. }
  864. #[test]
  865. fn simple_http_updater_percent_decode() {
  866. let _m = mockito::mock("GET", "/darwin/1.0.0")
  867. .with_status(200)
  868. .with_header("content-type", "application/json")
  869. .with_body(generate_sample_platform_json(
  870. "2.0.0",
  871. "SampleTauriKey",
  872. "https://tauri.studio",
  873. ))
  874. .create();
  875. let check_update = block!(builder(Default::default())
  876. .current_version("1.0.0")
  877. .url(
  878. url::Url::parse(&format!(
  879. "{}/darwin/{{{{current_version}}}}",
  880. mockito::server_url()
  881. ))
  882. .unwrap()
  883. .to_string()
  884. )
  885. .build());
  886. assert!(check_update.is_ok());
  887. let updater = check_update.expect("Can't check update");
  888. assert!(updater.should_update);
  889. let check_update = block!(builder(Default::default())
  890. .current_version("1.0.0")
  891. .urls(&[url::Url::parse(&format!(
  892. "{}/darwin/{{{{current_version}}}}",
  893. mockito::server_url()
  894. ))
  895. .unwrap()
  896. .to_string()])
  897. .build());
  898. assert!(check_update.is_ok());
  899. let updater = check_update.expect("Can't check update");
  900. assert!(updater.should_update);
  901. }
  902. #[test]
  903. fn simple_http_updater_with_elevated_task() {
  904. let _m = mockito::mock("GET", "/win64/1.0.0")
  905. .with_status(200)
  906. .with_header("content-type", "application/json")
  907. .with_body(generate_sample_with_elevated_task_platform_json(
  908. "2.0.0",
  909. "SampleTauriKey",
  910. "https://tauri.studio",
  911. true,
  912. ))
  913. .create();
  914. let check_update = block!(builder(Default::default())
  915. .current_version("1.0.0")
  916. .url(format!(
  917. "{}/win64/{{{{current_version}}}}",
  918. mockito::server_url()
  919. ))
  920. .build());
  921. assert!(check_update.is_ok());
  922. let updater = check_update.expect("Can't check update");
  923. assert!(updater.should_update);
  924. }
  925. #[test]
  926. fn http_updater_uptodate() {
  927. let _m = mockito::mock("GET", "/darwin/10.0.0")
  928. .with_status(200)
  929. .with_header("content-type", "application/json")
  930. .with_body(generate_sample_platform_json(
  931. "2.0.0",
  932. "SampleTauriKey",
  933. "https://tauri.studio",
  934. ))
  935. .create();
  936. let check_update = block!(builder(Default::default())
  937. .current_version("10.0.0")
  938. .url(format!(
  939. "{}/darwin/{{{{current_version}}}}",
  940. mockito::server_url()
  941. ))
  942. .build());
  943. assert!(check_update.is_ok());
  944. let updater = check_update.expect("Can't check update");
  945. assert!(!updater.should_update);
  946. }
  947. #[test]
  948. fn http_updater_fallback_urls() {
  949. let _m = mockito::mock("GET", "/")
  950. .with_status(200)
  951. .with_header("content-type", "application/json")
  952. .with_body(generate_sample_raw_json())
  953. .create();
  954. let check_update = block!(builder(Default::default())
  955. .url("http://badurl.www.tld/1".into())
  956. .url(mockito::server_url())
  957. .current_version("0.0.1")
  958. .build());
  959. assert!(check_update.is_ok());
  960. let updater = check_update.expect("Can't check remote update");
  961. assert!(updater.should_update);
  962. }
  963. #[test]
  964. fn http_updater_fallback_urls_withs_array() {
  965. let _m = mockito::mock("GET", "/")
  966. .with_status(200)
  967. .with_header("content-type", "application/json")
  968. .with_body(generate_sample_raw_json())
  969. .create();
  970. let check_update = block!(builder(Default::default())
  971. .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),])
  972. .current_version("0.0.1")
  973. .build());
  974. assert!(check_update.is_ok());
  975. let updater = check_update.expect("Can't check remote update");
  976. assert!(updater.should_update);
  977. }
  978. #[test]
  979. fn http_updater_missing_remote_data() {
  980. let _m = mockito::mock("GET", "/")
  981. .with_status(200)
  982. .with_header("content-type", "application/json")
  983. .with_body(generate_sample_bad_json())
  984. .create();
  985. let check_update = block!(builder(Default::default())
  986. .url(mockito::server_url())
  987. .current_version("0.0.1")
  988. .build());
  989. assert!(check_update.is_err());
  990. }
  991. // run complete process on mac only for now as we don't have
  992. // server (api) that we can use to test
  993. #[test]
  994. #[cfg(target_os = "macos")]
  995. fn http_updater_complete_process() {
  996. #[cfg(target_os = "macos")]
  997. let archive_file = "archive.macos.tar.gz";
  998. #[cfg(target_os = "linux")]
  999. let archive_file = "archive.linux.tar.gz";
  1000. #[cfg(target_os = "windows")]
  1001. let archive_file = "archive.windows.zip";
  1002. let good_archive_url = format!("{}/{}", mockito::server_url(), archive_file);
  1003. let mut signature_file = File::open(format!(
  1004. "./test/updater/fixture/archives/{}.sig",
  1005. archive_file
  1006. ))
  1007. .expect("Unable to open signature");
  1008. let mut signature = String::new();
  1009. signature_file
  1010. .read_to_string(&mut signature)
  1011. .expect("Unable to read signature as string");
  1012. let mut pubkey_file = File::open("./test/updater/fixture/good_signature/update.key.pub")
  1013. .expect("Unable to open pubkey");
  1014. let mut pubkey = String::new();
  1015. pubkey_file
  1016. .read_to_string(&mut pubkey)
  1017. .expect("Unable to read signature as string");
  1018. // add sample file
  1019. let _m = mockito::mock("GET", format!("/{}", archive_file).as_str())
  1020. .with_status(200)
  1021. .with_header("content-type", "application/octet-stream")
  1022. .with_body_from_file(format!("./test/updater/fixture/archives/{}", archive_file))
  1023. .create();
  1024. // sample mock for update file
  1025. let _m = mockito::mock("GET", "/")
  1026. .with_status(200)
  1027. .with_header("content-type", "application/json")
  1028. .with_body(generate_sample_platform_json(
  1029. "2.0.1",
  1030. signature.as_ref(),
  1031. good_archive_url.as_ref(),
  1032. ))
  1033. .create();
  1034. // Build a tmpdir so we can test our extraction inside
  1035. // We dont want to overwrite our current executable or the directory
  1036. // Otherwise tests are failing...
  1037. let executable_path = current_exe().expect("Can't extract executable path");
  1038. let parent_path = executable_path
  1039. .parent()
  1040. .expect("Can't find the parent path");
  1041. let tmp_dir = tempfile::Builder::new()
  1042. .prefix("tauri_updater_test")
  1043. .tempdir_in(parent_path);
  1044. assert!(tmp_dir.is_ok());
  1045. let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir");
  1046. let tmp_dir_path = tmp_dir_unwrap.path();
  1047. #[cfg(target_os = "linux")]
  1048. let my_executable = &tmp_dir_path.join("updater-example_0.1.0_amd64.AppImage");
  1049. #[cfg(target_os = "macos")]
  1050. let my_executable = &tmp_dir_path.join("my_app");
  1051. #[cfg(target_os = "windows")]
  1052. let my_executable = &tmp_dir_path.join("my_app.exe");
  1053. // configure the updater
  1054. let check_update = block!(builder(Default::default())
  1055. .url(mockito::server_url())
  1056. // It should represent the executable path, that's why we add my_app.exe in our
  1057. // test path -- in production you shouldn't have to provide it
  1058. .executable_path(my_executable)
  1059. // make sure we force an update
  1060. .current_version("1.0.0")
  1061. .build());
  1062. #[cfg(target_os = "linux")]
  1063. {
  1064. env::set_var("APPIMAGE", my_executable);
  1065. }
  1066. // make sure the process worked
  1067. assert!(check_update.is_ok());
  1068. // unwrap our results
  1069. let updater = check_update.expect("Can't check remote update");
  1070. // make sure we need to update
  1071. assert!(updater.should_update);
  1072. // make sure we can read announced version
  1073. assert_eq!(updater.version, "2.0.1");
  1074. // download, install and validate signature
  1075. let install_process = block!(updater.download_and_install(pubkey));
  1076. assert!(install_process.is_ok());
  1077. // make sure the extraction went well (it should have skipped the main app.app folder)
  1078. // as we can't extract in /Applications directly
  1079. #[cfg(target_os = "macos")]
  1080. let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app");
  1081. #[cfg(target_os = "linux")]
  1082. // linux should extract at same place as the executable path
  1083. let bin_file = my_executable;
  1084. #[cfg(target_os = "windows")]
  1085. let bin_file = tmp_dir_path.join("with").join("long").join("path.json");
  1086. assert!(bin_file.exists());
  1087. }
  1088. }