1use std::{
6 collections::HashMap,
7 ffi::OsString,
8 io::Cursor,
9 path::{Path, PathBuf},
10 str::FromStr,
11 sync::Arc,
12 time::Duration,
13};
14
15#[cfg(not(target_os = "macos"))]
16use std::ffi::OsStr;
17
18use base64::Engine;
19use futures_util::StreamExt;
20use http::{header::ACCEPT, HeaderName};
21use minisign_verify::{PublicKey, Signature};
22use percent_encoding::{AsciiSet, CONTROLS};
23use reqwest::{
24 header::{HeaderMap, HeaderValue},
25 ClientBuilder, StatusCode,
26};
27use semver::Version;
28use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
29use tauri::{
30 utils::{
31 config::BundleType,
32 platform::{bundle_type, current_exe},
33 },
34 AppHandle, Resource, Runtime,
35};
36use time::OffsetDateTime;
37use url::Url;
38
39use crate::{
40 error::{Error, Result},
41 Config,
42};
43
44const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
45
46#[derive(Copy, Clone)]
47pub enum Installer {
48 AppImage,
49 Deb,
50 Rpm,
51
52 App,
53
54 Msi,
55 Nsis,
56}
57
58impl Installer {
59 fn name(self) -> &'static str {
60 match self {
61 Self::AppImage => "appimage",
62 Self::Deb => "deb",
63 Self::Rpm => "rpm",
64 Self::App => "app",
65 Self::Msi => "msi",
66 Self::Nsis => "nsis",
67 }
68 }
69}
70
71#[derive(Debug, Deserialize, Serialize, Clone)]
72pub struct ReleaseManifestPlatform {
73 pub url: Url,
75 pub signature: String,
77}
78
79#[derive(Debug, Deserialize, Serialize, Clone)]
80#[serde(untagged)]
81pub enum RemoteReleaseInner {
82 Dynamic(ReleaseManifestPlatform),
83 Static {
84 platforms: HashMap<String, ReleaseManifestPlatform>,
85 },
86}
87
88#[derive(Debug, Clone)]
92pub struct RemoteRelease {
93 pub version: Version,
95 pub notes: Option<String>,
97 pub pub_date: Option<OffsetDateTime>,
99 pub data: RemoteReleaseInner,
101}
102
103impl RemoteRelease {
104 pub fn download_url(&self, target: &str) -> Result<&Url> {
106 match self.data {
107 RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url),
108 RemoteReleaseInner::Static { ref platforms } => platforms
109 .get(target)
110 .map_or(Err(Error::TargetNotFound(target.to_string())), |p| {
111 Ok(&p.url)
112 }),
113 }
114 }
115
116 pub fn signature(&self, target: &str) -> Result<&String> {
118 match self.data {
119 RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature),
120 RemoteReleaseInner::Static { ref platforms } => platforms
121 .get(target)
122 .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| {
123 Ok(&platform.signature)
124 }),
125 }
126 }
127}
128
129pub type OnBeforeExit = Arc<dyn Fn() + Send + Sync + 'static>;
130pub type OnBeforeRequest = Arc<dyn Fn(ClientBuilder) -> ClientBuilder + Send + Sync + 'static>;
131pub type VersionComparator = Arc<dyn Fn(Version, RemoteRelease) -> bool + Send + Sync>;
132type MainThreadClosure = Box<dyn FnOnce() + Send + Sync + 'static>;
133type RunOnMainThread =
134 Box<dyn Fn(MainThreadClosure) -> std::result::Result<(), tauri::Error> + Send + Sync + 'static>;
135
136pub struct UpdaterBuilder {
137 #[allow(dead_code)]
138 run_on_main_thread: RunOnMainThread,
139 app_name: String,
140 current_version: Version,
141 config: Config,
142 pub(crate) version_comparator: Option<VersionComparator>,
143 executable_path: Option<PathBuf>,
144 target: Option<String>,
145 endpoints: Option<Vec<Url>>,
146 headers: HeaderMap,
147 timeout: Option<Duration>,
148 proxy: Option<Url>,
149 no_proxy: bool,
150 installer_args: Vec<OsString>,
151 current_exe_args: Vec<OsString>,
152 on_before_exit: Option<OnBeforeExit>,
153 configure_client: Option<OnBeforeRequest>,
154}
155
156impl UpdaterBuilder {
157 pub(crate) fn new<R: Runtime>(app: &AppHandle<R>, config: crate::Config) -> Self {
158 let app_ = app.clone();
159 let run_on_main_thread = move |f| app_.run_on_main_thread(f);
160 Self {
161 run_on_main_thread: Box::new(run_on_main_thread),
162 installer_args: config
163 .windows
164 .as_ref()
165 .map(|w| w.installer_args.clone())
166 .unwrap_or_default(),
167 current_exe_args: Vec::new(),
168 app_name: app.package_info().name.clone(),
169 current_version: app.package_info().version.clone(),
170 config,
171 version_comparator: None,
172 executable_path: None,
173 target: None,
174 endpoints: None,
175 headers: Default::default(),
176 timeout: None,
177 proxy: None,
178 no_proxy: false,
179 on_before_exit: None,
180 configure_client: None,
181 }
182 }
183
184 pub fn version_comparator<F: Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static>(
185 mut self,
186 f: F,
187 ) -> Self {
188 self.version_comparator = Some(Arc::new(f));
189 self
190 }
191
192 pub fn target(mut self, target: impl Into<String>) -> Self {
193 self.target.replace(target.into());
194 self
195 }
196
197 pub fn endpoints(mut self, endpoints: Vec<Url>) -> Result<Self> {
198 crate::config::validate_endpoints(
199 &endpoints,
200 self.config.dangerous_insecure_transport_protocol,
201 )?;
202
203 self.endpoints.replace(endpoints);
204 Ok(self)
205 }
206
207 pub fn executable_path<P: AsRef<Path>>(mut self, p: P) -> Self {
208 self.executable_path.replace(p.as_ref().into());
209 self
210 }
211
212 pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
213 where
214 HeaderName: TryFrom<K>,
215 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
216 HeaderValue: TryFrom<V>,
217 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
218 {
219 let key: std::result::Result<HeaderName, http::Error> = key.try_into().map_err(Into::into);
220 let value: std::result::Result<HeaderValue, http::Error> =
221 value.try_into().map_err(Into::into);
222 self.headers.insert(key?, value?);
223
224 Ok(self)
225 }
226
227 pub fn headers(mut self, headers: HeaderMap) -> Self {
228 self.headers = headers;
229 self
230 }
231
232 pub fn clear_headers(mut self) -> Self {
233 self.headers.clear();
234 self
235 }
236
237 pub fn timeout(mut self, timeout: Duration) -> Self {
238 self.timeout = Some(timeout);
239 self
240 }
241
242 pub fn proxy(mut self, proxy: Url) -> Self {
243 self.proxy.replace(proxy);
244 self
245 }
246
247 pub fn no_proxy(mut self) -> Self {
249 self.no_proxy = true;
250 self
251 }
252
253 pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
254 self.config.pubkey = pubkey.into();
255 self
256 }
257
258 pub fn installer_arg<S>(mut self, arg: S) -> Self
260 where
261 S: Into<OsString>,
262 {
263 self.installer_args.push(arg.into());
264 self
265 }
266
267 pub fn installer_args<I, S>(mut self, args: I) -> Self
269 where
270 I: IntoIterator<Item = S>,
271 S: Into<OsString>,
272 {
273 self.installer_args.extend(args.into_iter().map(Into::into));
274 self
275 }
276
277 pub fn clear_installer_args(mut self) -> Self {
284 self.installer_args.clear();
285 self
286 }
287
288 pub fn on_before_exit<F: Fn() + Send + Sync + 'static>(mut self, f: F) -> Self {
290 self.on_before_exit.replace(Arc::new(f));
291 self
292 }
293
294 pub fn configure_client<F: Fn(ClientBuilder) -> ClientBuilder + Send + Sync + 'static>(
299 mut self,
300 f: F,
301 ) -> Self {
302 self.configure_client.replace(Arc::new(f));
303 self
304 }
305
306 pub fn build(self) -> Result<Updater> {
307 let endpoints = self
308 .endpoints
309 .unwrap_or_else(|| self.config.endpoints.clone());
310
311 if endpoints.is_empty() {
312 return Err(Error::EmptyEndpoints);
313 };
314
315 let arch = updater_arch().ok_or(Error::UnsupportedArch)?;
316
317 let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
318
319 let extract_path = if cfg!(target_os = "linux") {
321 executable_path
322 } else {
323 extract_path_from_executable(&executable_path)?
324 };
325
326 Ok(Updater {
327 run_on_main_thread: Arc::new(self.run_on_main_thread),
328 config: self.config,
329 app_name: self.app_name,
330 current_version: self.current_version,
331 version_comparator: self.version_comparator,
332 timeout: self.timeout,
333 proxy: self.proxy,
334 no_proxy: self.no_proxy,
335 endpoints,
336 installer_args: self.installer_args,
337 current_exe_args: self.current_exe_args,
338 arch,
339 target: self.target,
340 headers: self.headers,
341 extract_path,
342 on_before_exit: self.on_before_exit,
343 configure_client: self.configure_client,
344 })
345 }
346}
347
348impl UpdaterBuilder {
349 pub(crate) fn current_exe_args<I, S>(mut self, args: I) -> Self
350 where
351 I: IntoIterator<Item = S>,
352 S: Into<OsString>,
353 {
354 self.current_exe_args
355 .extend(args.into_iter().map(Into::into));
356 self
357 }
358}
359
360pub struct Updater {
361 #[allow(dead_code)]
362 run_on_main_thread: Arc<RunOnMainThread>,
363 config: Config,
364 app_name: String,
365 current_version: Version,
366 version_comparator: Option<VersionComparator>,
367 timeout: Option<Duration>,
368 proxy: Option<Url>,
369 no_proxy: bool,
370 endpoints: Vec<Url>,
371 arch: &'static str,
372 target: Option<String>,
375 headers: HeaderMap,
376 extract_path: PathBuf,
377 on_before_exit: Option<OnBeforeExit>,
378 configure_client: Option<OnBeforeRequest>,
379 #[allow(unused)]
380 installer_args: Vec<OsString>,
381 #[allow(unused)]
382 current_exe_args: Vec<OsString>,
383}
384
385impl Updater {
386 pub async fn check(&self) -> Result<Option<Update>> {
387 let mut headers = self.headers.clone();
389 if !headers.contains_key(ACCEPT) {
390 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
391 }
392
393 #[cfg(target_os = "linux")]
395 {
396 if std::env::var_os("SSL_CERT_FILE").is_none() {
397 std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
398 }
399 if std::env::var_os("SSL_CERT_DIR").is_none() {
400 std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
401 }
402 }
403 let target = if let Some(target) = &self.target {
404 target
405 } else {
406 updater_os().ok_or(Error::UnsupportedOs)?
407 };
408
409 let mut remote_release: Option<RemoteRelease> = None;
410 let mut raw_json: Option<serde_json::Value> = None;
411 let mut last_error: Option<Error> = None;
412 for url in &self.endpoints {
413 let version = self.current_version.to_string();
421 let version = version.as_bytes();
422 const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+');
423 let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD);
424 let encoded_version = encoded_version.to_string();
425 let installer = installer_for_bundle_type(bundle_type())
426 .map(|i| i.name())
427 .unwrap_or("unknown");
428
429 let url: Url = url
430 .to_string()
431 .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version)
433 .replace("%7B%7Btarget%7D%7D", target)
434 .replace("%7B%7Barch%7D%7D", self.arch)
435 .replace("%7B%7Bbundle_type%7D%7D", installer)
436 .replace("{{current_version}}", &encoded_version)
438 .replace("{{target}}", target)
439 .replace("{{arch}}", self.arch)
440 .replace("{{bundle_type}}", installer)
441 .parse()?;
442
443 log::debug!("checking for updates {url}");
444
445 #[cfg(feature = "rustls-tls")]
446 if rustls::crypto::CryptoProvider::get_default().is_none() {
447 let _ = rustls::crypto::ring::default_provider().install_default();
449 }
450
451 let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
452 if self.config.dangerous_accept_invalid_certs {
453 request = request.danger_accept_invalid_certs(true);
454 }
455 if self.config.dangerous_accept_invalid_hostnames {
456 request = request.danger_accept_invalid_hostnames(true);
457 }
458 if let Some(timeout) = self.timeout {
459 request = request.timeout(timeout);
460 }
461 if self.no_proxy {
462 log::debug!("disabling proxy");
463 request = request.no_proxy();
464 } else if let Some(ref proxy) = self.proxy {
465 log::debug!("using proxy {proxy}");
466 let proxy = reqwest::Proxy::all(proxy.as_str())?;
467 request = request.proxy(proxy);
468 }
469
470 if let Some(ref configure_client) = self.configure_client {
471 request = configure_client(request);
472 }
473
474 let response = request
475 .build()?
476 .get(url)
477 .headers(headers.clone())
478 .send()
479 .await;
480
481 match response {
482 Ok(res) => {
483 if res.status().is_success() {
484 if StatusCode::NO_CONTENT == res.status() {
486 log::debug!("update endpoint returned 204 No Content");
487 return Ok(None);
488 };
489
490 let update_response: serde_json::Value = res.json().await?;
491 log::debug!("update response: {update_response:?}");
492 raw_json = Some(update_response.clone());
493 match serde_json::from_value::<RemoteRelease>(update_response)
494 .map_err(Into::into)
495 {
496 Ok(release) => {
497 log::debug!("parsed release response {release:?}");
498 last_error = None;
499 remote_release = Some(release);
500 break;
502 }
503 Err(err) => {
504 log::error!("failed to deserialize update response: {err}");
505 last_error = Some(err)
506 }
507 }
508 } else {
509 log::error!(
510 "update endpoint did not respond with a successful status code"
511 );
512 }
513 }
514 Err(err) => {
515 log::error!("failed to check for updates: {err}");
516 last_error = Some(err.into())
517 }
518 }
519 }
520
521 if let Some(error) = last_error {
524 return Err(error);
525 }
526
527 let release = remote_release.ok_or(Error::ReleaseNotFound)?;
529
530 let should_update = match self.version_comparator.as_ref() {
531 Some(comparator) => comparator(self.current_version.clone(), release.clone()),
532 None => release.version > self.current_version,
533 };
534
535 let installer = installer_for_bundle_type(bundle_type());
536 let (download_url, signature) = self.get_urls(&release, &installer)?;
537
538 let update = if should_update {
539 Some(Update {
540 run_on_main_thread: self.run_on_main_thread.clone(),
541 config: self.config.clone(),
542 on_before_exit: self.on_before_exit.clone(),
543 app_name: self.app_name.clone(),
544 current_version: self.current_version.to_string(),
545 target: target.to_owned(),
546 extract_path: self.extract_path.clone(),
547 version: release.version.to_string(),
548 date: release.pub_date,
549 download_url: download_url.clone(),
550 signature: signature.to_owned(),
551 body: release.notes,
552 raw_json: raw_json.unwrap(),
553 timeout: None,
554 proxy: self.proxy.clone(),
555 no_proxy: self.no_proxy,
556 headers: self.headers.clone(),
557 installer_args: self.installer_args.clone(),
558 current_exe_args: self.current_exe_args.clone(),
559 configure_client: self.configure_client.clone(),
560 })
561 } else {
562 None
563 };
564
565 Ok(update)
566 }
567
568 fn get_urls<'a>(
569 &self,
570 release: &'a RemoteRelease,
571 installer: &Option<Installer>,
572 ) -> Result<(&'a Url, &'a String)> {
573 if let Some(target) = &self.target {
575 return Ok((release.download_url(target)?, release.signature(target)?));
576 }
577
578 let os = updater_os().ok_or(Error::UnsupportedOs)?;
580 let arch = self.arch;
581 let mut targets = Vec::new();
582 if let Some(installer) = installer {
583 let installer = installer.name();
584 targets.push(format!("{os}-{arch}-{installer}"));
585 }
586 targets.push(format!("{os}-{arch}"));
587
588 for target in &targets {
589 log::debug!("Searching for updater target '{target}' in release data");
590 if let (Ok(download_url), Ok(signature)) =
591 (release.download_url(target), release.signature(target))
592 {
593 return Ok((download_url, signature));
594 };
595 }
596
597 Err(Error::TargetsNotFound(targets))
598 }
599}
600
601#[derive(Clone)]
602pub struct Update {
603 #[allow(dead_code)]
604 run_on_main_thread: Arc<RunOnMainThread>,
605 config: Config,
606 #[allow(unused)]
607 on_before_exit: Option<OnBeforeExit>,
608 pub body: Option<String>,
610 pub current_version: String,
612 pub version: String,
614 pub date: Option<OffsetDateTime>,
616 pub target: String,
619 pub download_url: Url,
621 pub signature: String,
623 pub raw_json: serde_json::Value,
625 pub timeout: Option<Duration>,
627 pub proxy: Option<Url>,
629 pub no_proxy: bool,
631 pub headers: HeaderMap,
633 #[allow(unused)]
635 extract_path: PathBuf,
636 #[allow(unused)]
638 app_name: String,
639 #[allow(unused)]
640 installer_args: Vec<OsString>,
641 #[allow(unused)]
642 current_exe_args: Vec<OsString>,
643 configure_client: Option<OnBeforeRequest>,
644}
645
646impl Resource for Update {}
647
648impl Update {
649 pub async fn download<C: FnMut(usize, Option<u64>), D: FnOnce()>(
653 &self,
654 mut on_chunk: C,
655 on_download_finish: D,
656 ) -> Result<Vec<u8>> {
657 let mut headers = self.headers.clone();
659 if !headers.contains_key(ACCEPT) {
660 headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
661 }
662
663 let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
664 if self.config.dangerous_accept_invalid_certs {
665 request = request.danger_accept_invalid_certs(true);
666 }
667 if self.config.dangerous_accept_invalid_hostnames {
668 request = request.danger_accept_invalid_hostnames(true);
669 }
670 if let Some(timeout) = self.timeout {
671 request = request.timeout(timeout);
672 }
673 if self.no_proxy {
674 request = request.no_proxy();
675 } else if let Some(ref proxy) = self.proxy {
676 let proxy = reqwest::Proxy::all(proxy.as_str())?;
677 request = request.proxy(proxy);
678 }
679 if let Some(ref configure_client) = self.configure_client {
680 request = configure_client(request);
681 }
682 let response = request
683 .build()?
684 .get(self.download_url.clone())
685 .headers(headers)
686 .send()
687 .await?;
688
689 if !response.status().is_success() {
690 return Err(Error::Network(format!(
691 "Download request failed with status: {}",
692 response.status()
693 )));
694 }
695
696 let content_length: Option<u64> = response
697 .headers()
698 .get("Content-Length")
699 .and_then(|value| value.to_str().ok())
700 .and_then(|value| value.parse().ok());
701
702 let mut buffer = Vec::new();
703
704 let mut stream = response.bytes_stream();
705 while let Some(chunk) = stream.next().await {
706 let chunk = chunk?;
707 on_chunk(chunk.len(), content_length);
708 buffer.extend(chunk);
709 }
710 on_download_finish();
711
712 verify_signature(&buffer, &self.signature, &self.config.pubkey)?;
713
714 Ok(buffer)
715 }
716
717 pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
719 self.install_inner(bytes.as_ref())
720 }
721
722 pub async fn download_and_install<C: FnMut(usize, Option<u64>), D: FnOnce()>(
724 &self,
725 on_chunk: C,
726 on_download_finish: D,
727 ) -> Result<()> {
728 let bytes = self.download(on_chunk, on_download_finish).await?;
729 self.install(bytes)
730 }
731
732 #[cfg(mobile)]
733 fn install_inner(&self, _bytes: &[u8]) -> Result<()> {
734 Ok(())
735 }
736}
737
738#[cfg(windows)]
739enum WindowsUpdaterType {
740 Nsis {
741 path: PathBuf,
742 #[allow(unused)]
743 temp: Option<tempfile::TempPath>,
744 },
745 Msi {
746 path: PathBuf,
747 #[allow(unused)]
748 temp: Option<tempfile::TempPath>,
749 },
750}
751
752#[cfg(windows)]
753impl WindowsUpdaterType {
754 fn nsis(path: PathBuf, temp: Option<tempfile::TempPath>) -> Self {
755 Self::Nsis { path, temp }
756 }
757
758 fn msi(path: PathBuf, temp: Option<tempfile::TempPath>) -> Self {
759 Self::Msi {
760 path: path.wrap_in_quotes(),
761 temp,
762 }
763 }
764}
765
766#[cfg(windows)]
767impl Config {
768 fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode {
769 self.windows
770 .as_ref()
771 .map(|w| w.install_mode.clone())
772 .unwrap_or_default()
773 }
774}
775
776#[cfg(windows)]
778impl Update {
779 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
788 use std::iter::once;
789 use windows_sys::{
790 w,
791 Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW},
792 };
793
794 let updater_type = self.extract(bytes)?;
795
796 let install_mode = self.config.install_mode();
797 let current_args = &self.current_exe_args()[1..];
798 let msi_args;
799 let nsis_args;
800
801 let installer_args: Vec<&OsStr> = match &updater_type {
802 WindowsUpdaterType::Nsis { .. } => {
803 nsis_args = current_args
804 .iter()
805 .map(escape_nsis_current_exe_arg)
806 .collect::<Vec<_>>();
807
808 install_mode
809 .nsis_args()
810 .iter()
811 .map(OsStr::new)
812 .chain(once(OsStr::new("/UPDATE")))
813 .chain(once(OsStr::new("/ARGS")))
814 .chain(nsis_args.iter().map(OsStr::new))
815 .chain(self.installer_args())
816 .collect()
817 }
818 WindowsUpdaterType::Msi { path, .. } => {
819 let escaped_args = current_args
820 .iter()
821 .map(escape_msi_property_arg)
822 .collect::<Vec<_>>()
823 .join(" ");
824 msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\""));
825
826 [OsStr::new("/i"), path.as_os_str()]
827 .into_iter()
828 .chain(install_mode.msiexec_args().iter().map(OsStr::new))
829 .chain(once(OsStr::new("/promptrestart")))
830 .chain(self.installer_args())
831 .chain(once(OsStr::new("AUTOLAUNCHAPP=True")))
832 .chain(once(msi_args.as_os_str()))
833 .collect()
834 }
835 };
836
837 if let Some(on_before_exit) = self.on_before_exit.as_ref() {
838 log::debug!("running on_before_exit hook");
839 on_before_exit();
840 }
841
842 let file = match &updater_type {
843 WindowsUpdaterType::Nsis { path, .. } => path.as_os_str().to_os_string(),
844 WindowsUpdaterType::Msi { .. } => std::env::var("SYSTEMROOT").as_ref().map_or_else(
845 |_| OsString::from("msiexec.exe"),
846 |p| OsString::from(format!("{p}\\System32\\msiexec.exe")),
847 ),
848 };
849 let file = encode_wide(file);
850
851 let parameters = installer_args.join(OsStr::new(" "));
852 let parameters = encode_wide(parameters);
853
854 unsafe {
855 ShellExecuteW(
856 std::ptr::null_mut(),
857 w!("open"),
858 file.as_ptr(),
859 parameters.as_ptr(),
860 std::ptr::null(),
861 SW_SHOW,
862 )
863 };
864
865 std::process::exit(0);
866 }
867
868 fn installer_args(&self) -> Vec<&OsStr> {
869 self.installer_args
870 .iter()
871 .map(OsStr::new)
872 .collect::<Vec<_>>()
873 }
874
875 fn current_exe_args(&self) -> Vec<&OsStr> {
876 self.current_exe_args
877 .iter()
878 .map(OsStr::new)
879 .collect::<Vec<_>>()
880 }
881
882 fn extract(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
883 #[cfg(feature = "zip")]
884 if infer::archive::is_zip(bytes) {
885 return self.extract_zip(bytes);
886 }
887
888 self.extract_exe(bytes)
889 }
890
891 fn make_temp_dir(&self) -> Result<PathBuf> {
892 Ok(tempfile::Builder::new()
893 .prefix(&format!("{}-{}-updater-", self.app_name, self.version))
894 .tempdir()?
895 .keep())
896 }
897
898 #[cfg(feature = "zip")]
899 fn extract_zip(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
900 let temp_dir = self.make_temp_dir()?;
901
902 let archive = Cursor::new(bytes);
903 let mut extractor = zip::ZipArchive::new(archive)?;
904 extractor.extract(&temp_dir)?;
905
906 let paths = std::fs::read_dir(&temp_dir)?;
907 for path in paths {
908 let path = path?.path();
909 let ext = path.extension();
910 if ext == Some(OsStr::new("exe")) {
911 return Ok(WindowsUpdaterType::nsis(path, None));
912 } else if ext == Some(OsStr::new("msi")) {
913 return Ok(WindowsUpdaterType::msi(path, None));
914 }
915 }
916
917 Err(crate::Error::BinaryNotFoundInArchive)
918 }
919
920 fn extract_exe(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
921 if infer::app::is_exe(bytes) {
922 let (path, temp) = self.write_to_temp(bytes, ".exe")?;
923 Ok(WindowsUpdaterType::nsis(path, temp))
924 } else if infer::archive::is_msi(bytes) {
925 let (path, temp) = self.write_to_temp(bytes, ".msi")?;
926 Ok(WindowsUpdaterType::msi(path, temp))
927 } else {
928 Err(crate::Error::InvalidUpdaterFormat)
929 }
930 }
931
932 fn write_to_temp(
933 &self,
934 bytes: &[u8],
935 ext: &str,
936 ) -> Result<(PathBuf, Option<tempfile::TempPath>)> {
937 use std::io::Write;
938
939 let temp_dir = self.make_temp_dir()?;
940 let mut temp_file = tempfile::Builder::new()
941 .prefix(&format!("{}-{}-installer", self.app_name, self.version))
942 .suffix(ext)
943 .rand_bytes(0)
944 .tempfile_in(temp_dir)?;
945 temp_file.write_all(bytes)?;
946
947 let temp = temp_file.into_temp_path();
948 Ok((temp.to_path_buf(), Some(temp)))
949 }
950}
951
952#[cfg(any(
954 target_os = "linux",
955 target_os = "dragonfly",
956 target_os = "freebsd",
957 target_os = "netbsd",
958 target_os = "openbsd"
959))]
960impl Update {
961 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
968 match installer_for_bundle_type(bundle_type()) {
969 Some(Installer::Deb) => self.install_deb(bytes),
970 Some(Installer::Rpm) => self.install_rpm(bytes),
971 _ => self.install_appimage(bytes),
972 }
973 }
974
975 fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
976 use std::os::unix::fs::{MetadataExt, PermissionsExt};
977 let extract_path_metadata = self.extract_path.metadata()?;
978
979 let tmp_dir_locations = vec![
980 Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
981 Box::new(dirs::cache_dir),
982 Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
983 ];
984
985 for tmp_dir_location in tmp_dir_locations {
986 if let Some(tmp_dir_location) = tmp_dir_location() {
987 let tmp_dir = tempfile::Builder::new()
988 .prefix("tauri_current_app")
989 .tempdir_in(tmp_dir_location)?;
990 let tmp_dir_metadata = tmp_dir.path().metadata()?;
991
992 if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
993 let mut perms = tmp_dir_metadata.permissions();
994 perms.set_mode(0o700);
995 std::fs::set_permissions(tmp_dir.path(), perms)?;
996
997 let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
998
999 let permissions = std::fs::metadata(&self.extract_path)?.permissions();
1000
1001 std::fs::rename(&self.extract_path, tmp_app_image)?;
1003
1004 #[cfg(feature = "zip")]
1005 if infer::archive::is_gz(bytes) {
1006 log::debug!("extracting AppImage");
1007 let archive = Cursor::new(bytes);
1010 let decoder = flate2::read::GzDecoder::new(archive);
1011 let mut archive = tar::Archive::new(decoder);
1012 for mut entry in archive.entries()?.flatten() {
1013 if let Ok(path) = entry.path() {
1014 if path.extension() == Some(OsStr::new("AppImage")) {
1015 if let Err(err) = entry.unpack(&self.extract_path) {
1017 std::fs::rename(tmp_app_image, &self.extract_path)?;
1018 return Err(err.into());
1019 }
1020 return Ok(());
1022 }
1023 }
1024 }
1025 std::fs::rename(tmp_app_image, &self.extract_path)?;
1027 return Err(Error::BinaryNotFoundInArchive);
1028 }
1029
1030 log::debug!("rewriting AppImage");
1031 return match std::fs::write(&self.extract_path, bytes)
1032 .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
1033 {
1034 Err(err) => {
1035 std::fs::rename(tmp_app_image, &self.extract_path)?;
1037 Err(err.into())
1038 }
1039 Ok(_) => Ok(()),
1040 };
1041 }
1042 }
1043 }
1044
1045 Err(Error::TempDirNotOnSameMountPoint)
1046 }
1047
1048 fn install_deb(&self, bytes: &[u8]) -> Result<()> {
1049 if !infer::archive::is_deb(bytes) {
1051 log::warn!("update is not a valid deb package");
1052 return Err(Error::InvalidUpdaterFormat);
1053 }
1054
1055 self.try_tmp_locations(bytes, "dpkg", "-i")
1056 }
1057
1058 fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
1059 if !infer::archive::is_rpm(bytes) {
1061 return Err(Error::InvalidUpdaterFormat);
1062 }
1063 self.try_tmp_locations(bytes, "rpm", "-U")
1064 }
1065
1066 fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> {
1067 let tmp_dir_locations = vec![
1069 Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1070 Box::new(dirs::cache_dir),
1071 Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1072 ];
1073
1074 for tmp_dir_location in tmp_dir_locations {
1076 if let Some(path) = tmp_dir_location() {
1077 if let Ok(tmp_dir) = tempfile::Builder::new()
1078 .prefix("tauri_rpm_update")
1079 .tempdir_in(path)
1080 {
1081 let pkg_path = tmp_dir.path().join("package.rpm");
1082
1083 if std::fs::write(&pkg_path, bytes).is_ok() {
1085 return self.try_install_with_privileges(
1087 &pkg_path,
1088 install_cmd,
1089 install_arg,
1090 );
1091 }
1092 }
1094 }
1095 }
1096
1097 Err(Error::TempDirNotFound)
1099 }
1100
1101 fn try_install_with_privileges(
1102 &self,
1103 pkg_path: &Path,
1104 install_cmd: &str,
1105 install_arg: &str,
1106 ) -> Result<()> {
1107 if let Ok(status) = std::process::Command::new("pkexec")
1109 .arg(install_cmd)
1110 .arg(install_arg)
1111 .arg(pkg_path)
1112 .status()
1113 {
1114 if status.success() {
1115 log::debug!("installed deb with pkexec");
1116 return Ok(());
1117 }
1118 }
1119
1120 if let Ok(password) = self.get_password_graphically() {
1122 if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
1123 log::debug!("installed deb with GUI sudo");
1124 return Ok(());
1125 }
1126 }
1127
1128 let status = std::process::Command::new("sudo")
1130 .arg(install_cmd)
1131 .arg(install_arg)
1132 .arg(pkg_path)
1133 .status()?;
1134
1135 if status.success() {
1136 log::debug!("installed deb with sudo");
1137 Ok(())
1138 } else {
1139 Err(Error::PackageInstallFailed)
1140 }
1141 }
1142
1143 fn get_password_graphically(&self) -> Result<String> {
1144 let zenity_result = std::process::Command::new("zenity")
1146 .args([
1147 "--password",
1148 "--title=Authentication Required",
1149 "--text=Enter your password to install the update:",
1150 ])
1151 .output();
1152
1153 if let Ok(output) = zenity_result {
1154 if output.status.success() {
1155 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1156 }
1157 }
1158
1159 let kdialog_result = std::process::Command::new("kdialog")
1161 .args(["--password", "Enter your password to install the update:"])
1162 .output();
1163
1164 if let Ok(output) = kdialog_result {
1165 if output.status.success() {
1166 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1167 }
1168 }
1169
1170 Err(Error::AuthenticationFailed)
1171 }
1172
1173 fn install_with_sudo(
1174 &self,
1175 pkg_path: &Path,
1176 password: &str,
1177 install_cmd: &str,
1178 install_arg: &str,
1179 ) -> Result<bool> {
1180 use std::io::Write;
1181 use std::process::{Command, Stdio};
1182
1183 let mut child = Command::new("sudo")
1184 .arg("-S") .arg(install_cmd)
1186 .arg(install_arg)
1187 .arg(pkg_path)
1188 .stdin(Stdio::piped())
1189 .stdout(Stdio::piped())
1190 .stderr(Stdio::piped())
1191 .spawn()?;
1192
1193 if let Some(mut stdin) = child.stdin.take() {
1194 writeln!(stdin, "{password}")?;
1196 }
1197
1198 let status = child.wait()?;
1199 Ok(status.success())
1200 }
1201}
1202
1203#[cfg(target_os = "macos")]
1205impl Update {
1206 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1213 use flate2::read::GzDecoder;
1214
1215 let cursor = Cursor::new(bytes);
1216 let mut extracted_files: Vec<PathBuf> = Vec::new();
1217
1218 let tmp_backup_dir = tempfile::Builder::new()
1220 .prefix("tauri_current_app")
1221 .tempdir()?;
1222
1223 let tmp_extract_dir = tempfile::Builder::new()
1224 .prefix("tauri_updated_app")
1225 .tempdir()?;
1226
1227 let decoder = GzDecoder::new(cursor);
1228 let mut archive = tar::Archive::new(decoder);
1229
1230 for entry in archive.entries()? {
1232 let mut entry = entry?;
1233 let collected_path: PathBuf = entry.path()?.iter().skip(1).collect();
1234 let extraction_path = tmp_extract_dir.path().join(&collected_path);
1235
1236 if let Some(parent) = extraction_path.parent() {
1238 std::fs::create_dir_all(parent)?;
1239 }
1240
1241 if let Err(err) = entry.unpack(&extraction_path) {
1242 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1244 return Err(err.into());
1245 }
1246 extracted_files.push(extraction_path);
1247 }
1248
1249 let move_result = std::fs::rename(
1251 &self.extract_path,
1252 tmp_backup_dir.path().join("current_app"),
1253 );
1254 let need_authorization = if let Err(err) = move_result {
1255 if err.kind() == std::io::ErrorKind::PermissionDenied {
1256 true
1257 } else {
1258 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1259 return Err(err.into());
1260 }
1261 } else {
1262 false
1263 };
1264
1265 if need_authorization {
1266 log::debug!("app installation needs admin privileges");
1267 let apple_script = format!(
1269 "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges",
1270 src = self.extract_path.display(),
1271 new = tmp_extract_dir.path().display()
1272 );
1273
1274 let (tx, rx) = std::sync::mpsc::channel();
1275 let res = (self.run_on_main_thread)(Box::new(move || {
1276 let mut script =
1277 osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script);
1278 script.compile().expect("invalid AppleScript");
1279 let r = script.execute();
1280 tx.send(r).unwrap();
1281 }));
1282 let result = rx.recv().unwrap();
1283
1284 if res.is_err() || result.is_err() {
1285 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1286 return Err(Error::Io(std::io::Error::new(
1287 std::io::ErrorKind::PermissionDenied,
1288 "Failed to move the new app into place",
1289 )));
1290 }
1291 } else {
1292 if self.extract_path.exists() {
1294 std::fs::remove_dir_all(&self.extract_path)?;
1295 }
1296 std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?;
1298 }
1299
1300 let _ = std::process::Command::new("touch")
1301 .arg(&self.extract_path)
1302 .status();
1303
1304 Ok(())
1305 }
1306}
1307
1308pub fn target() -> Option<String> {
1312 if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
1313 Some(format!("{target}-{arch}"))
1314 } else {
1315 None
1316 }
1317}
1318
1319fn updater_os() -> Option<&'static str> {
1320 if cfg!(target_os = "linux") {
1321 Some("linux")
1322 } else if cfg!(target_os = "macos") {
1323 Some("darwin")
1325 } else if cfg!(target_os = "windows") {
1326 Some("windows")
1327 } else {
1328 None
1329 }
1330}
1331
1332fn updater_arch() -> Option<&'static str> {
1333 if cfg!(target_arch = "x86") {
1334 Some("i686")
1335 } else if cfg!(target_arch = "x86_64") {
1336 Some("x86_64")
1337 } else if cfg!(target_arch = "arm") {
1338 Some("armv7")
1339 } else if cfg!(target_arch = "aarch64") {
1340 Some("aarch64")
1341 } else if cfg!(target_arch = "riscv64") {
1342 Some("riscv64")
1343 } else {
1344 None
1345 }
1346}
1347
1348pub fn extract_path_from_executable(executable_path: &Path) -> Result<PathBuf> {
1349 let extract_path = executable_path
1352 .parent()
1353 .map(PathBuf::from)
1354 .ok_or(Error::FailedToDetermineExtractPath)?;
1355
1356 #[cfg(target_os = "macos")]
1361 if extract_path
1362 .display()
1363 .to_string()
1364 .contains("Contents/MacOS")
1365 {
1366 return extract_path
1367 .parent()
1368 .map(PathBuf::from)
1369 .ok_or(Error::FailedToDetermineExtractPath)?
1370 .parent()
1371 .map(PathBuf::from)
1372 .ok_or(Error::FailedToDetermineExtractPath);
1373 }
1374
1375 Ok(extract_path)
1376}
1377
1378impl<'de> Deserialize<'de> for RemoteRelease {
1379 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1380 where
1381 D: Deserializer<'de>,
1382 {
1383 #[derive(Deserialize)]
1384 struct InnerRemoteRelease {
1385 #[serde(alias = "name", deserialize_with = "parse_version")]
1386 version: Version,
1387 notes: Option<String>,
1388 pub_date: Option<String>,
1389 platforms: Option<HashMap<String, ReleaseManifestPlatform>>,
1390 url: Option<Url>,
1392 signature: Option<String>,
1393 }
1394
1395 let release = InnerRemoteRelease::deserialize(deserializer)?;
1396
1397 let pub_date = if let Some(date) = release.pub_date {
1398 Some(
1399 OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
1400 .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
1401 )
1402 } else {
1403 None
1404 };
1405
1406 Ok(RemoteRelease {
1407 version: release.version,
1408 notes: release.notes,
1409 pub_date,
1410 data: if let Some(platforms) = release.platforms {
1411 RemoteReleaseInner::Static { platforms }
1412 } else {
1413 RemoteReleaseInner::Dynamic(ReleaseManifestPlatform {
1414 url: release.url.ok_or_else(|| {
1415 DeError::custom("the `url` field was not set on the updater response")
1416 })?,
1417 signature: release.signature.ok_or_else(|| {
1418 DeError::custom("the `signature` field was not set on the updater response")
1419 })?,
1420 })
1421 },
1422 })
1423 }
1424}
1425
1426fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
1427 match bundle? {
1428 BundleType::Deb => Some(Installer::Deb),
1429 BundleType::Rpm => Some(Installer::Rpm),
1430 BundleType::AppImage => Some(Installer::AppImage),
1431 BundleType::Msi => Some(Installer::Msi),
1432 BundleType::Nsis => Some(Installer::Nsis),
1433 BundleType::App => Some(Installer::App), _ => None,
1435 }
1436}
1437
1438fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
1439where
1440 D: serde::Deserializer<'de>,
1441{
1442 let str = String::deserialize(deserializer)?;
1443
1444 Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom)
1445}
1446
1447fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
1449 let pub_key_decoded = base64_to_string(pub_key)?;
1451 let public_key = PublicKey::decode(&pub_key_decoded)?;
1452 let signature_base64_decoded = base64_to_string(release_signature)?;
1453 let signature = Signature::decode(&signature_base64_decoded)?;
1454
1455 public_key.verify(data, &signature, true)?;
1457 Ok(true)
1458}
1459
1460fn base64_to_string(base64_string: &str) -> Result<String> {
1461 let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
1462 let result = std::str::from_utf8(decoded_string)
1463 .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
1464 .to_string();
1465 Ok(result)
1466}
1467
1468#[cfg(windows)]
1469fn encode_wide(string: impl AsRef<OsStr>) -> Vec<u16> {
1470 use std::os::windows::ffi::OsStrExt;
1471
1472 string
1473 .as_ref()
1474 .encode_wide()
1475 .chain(std::iter::once(0))
1476 .collect()
1477}
1478
1479#[cfg(windows)]
1480trait PathExt {
1481 fn wrap_in_quotes(&self) -> Self;
1482}
1483
1484#[cfg(windows)]
1485impl PathExt for PathBuf {
1486 fn wrap_in_quotes(&self) -> Self {
1487 let mut msi_path = OsString::from("\"");
1488 msi_path.push(self.as_os_str());
1489 msi_path.push("\"");
1490 PathBuf::from(msi_path)
1491 }
1492}
1493
1494#[cfg(windows)]
1496fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String {
1497 let arg = arg.to_string_lossy();
1498 let mut cmd: Vec<char> = Vec::new();
1499
1500 let quote = arg.chars().any(|c| c == ' ' || c == '\t' || c == '/') || arg.is_empty();
1502 let escape = true;
1503 if quote {
1504 cmd.push('"');
1505 }
1506 let mut backslashes: usize = 0;
1507 for x in arg.chars() {
1508 if escape {
1509 if x == '\\' {
1510 backslashes += 1;
1511 } else {
1512 if x == '"' {
1513 cmd.extend((0..=backslashes).map(|_| '\\'));
1515 }
1516 backslashes = 0;
1517 }
1518 }
1519 cmd.push(x);
1520 }
1521 if quote {
1522 cmd.extend((0..backslashes).map(|_| '\\'));
1524 cmd.push('"');
1525 }
1526 cmd.into_iter().collect()
1527}
1528
1529#[cfg(windows)]
1530fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
1531 let mut arg = arg.as_ref().to_string_lossy().to_string();
1532
1533 if arg.is_empty() {
1535 return "\"\"\"\"".to_string();
1536 } else if !arg.contains(' ') && !arg.contains('"') {
1537 return arg;
1538 }
1539
1540 if arg.contains('"') {
1541 arg = arg.replace('"', r#""""""#);
1542 }
1543
1544 if arg.starts_with('-') {
1545 if let Some((a1, a2)) = arg.split_once('=') {
1546 format!("{a1}=\"\"{a2}\"\"")
1547 } else {
1548 format!("\"\"{arg}\"\"")
1549 }
1550 } else {
1551 format!("\"\"{arg}\"\"")
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557
1558 #[test]
1559 #[cfg(windows)]
1560 fn it_wraps_correctly() {
1561 use super::PathExt;
1562 use std::path::PathBuf;
1563
1564 assert_eq!(
1565 PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(),
1566 PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
1567 )
1568 }
1569
1570 #[test]
1571 #[cfg(windows)]
1572 fn it_escapes_correctly_for_msi() {
1573 use crate::updater::escape_msi_property_arg;
1574
1575 let cases = [
1584 "something",
1585 "--flag",
1586 "--empty=",
1587 "--arg=value",
1588 "some space", "--arg value", "--arg=unwrapped space", "--arg=\"wrapped\"", "--arg=\"wrapped space\"", "--arg=midword\"wrapped space\"", "", ];
1596 let cases_escaped = [
1597 "something",
1598 "--flag",
1599 "--empty=",
1600 "--arg=value",
1601 "\"\"some space\"\"",
1602 "\"\"--arg value\"\"",
1603 "--arg=\"\"unwrapped space\"\"",
1604 r#"--arg=""""""wrapped"""""""#,
1605 r#"--arg=""""""wrapped space"""""""#,
1606 r#"--arg=""midword""""wrapped space"""""""#,
1607 "\"\"\"\"",
1608 ];
1609
1610 assert_eq!(cases.len(), cases_escaped.len());
1612
1613 for (orig, escaped) in cases.iter().zip(cases_escaped) {
1614 assert_eq!(escape_msi_property_arg(orig), escaped);
1615 }
1616 }
1617
1618 #[test]
1619 #[cfg(windows)]
1620 fn it_escapes_correctly_for_nsis() {
1621 use crate::updater::escape_nsis_current_exe_arg;
1622 use std::ffi::OsStr;
1623
1624 let cases = [
1625 "something",
1626 "--flag",
1627 "--empty=",
1628 "--arg=value",
1629 "some space", "--arg value", "--arg=unwrapped space", "--arg=\"wrapped\"", "--arg=\"wrapped space\"", "--arg=midword\"wrapped space\"", "", ];
1637 let cases_escaped = [
1640 "something",
1641 "--flag",
1642 "--empty=",
1643 "--arg=value",
1644 "\"some space\"",
1645 "\"--arg value\"",
1646 "\"--arg=unwrapped space\"",
1647 "--arg=\\\"wrapped\\\"",
1648 "\"--arg=\\\"wrapped space\\\"\"",
1649 "\"--arg=midword\\\"wrapped space\\\"\"",
1650 "\"\"",
1651 ];
1652
1653 assert_eq!(cases.len(), cases_escaped.len());
1655
1656 for (orig, escaped) in cases.iter().zip(cases_escaped) {
1657 assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped);
1658 }
1659 }
1660}