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<()> {
969 match installer_for_bundle_type(bundle_type()) {
970 Some(Installer::Deb) => self.install_deb(bytes),
971 Some(Installer::Rpm) => self.install_rpm(bytes),
972 _ => self.install_appimage(bytes),
973 }
974 }
975
976 fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
977 use std::os::unix::fs::{MetadataExt, PermissionsExt};
978 let extract_path_metadata = self.extract_path.metadata()?;
979
980 let tmp_dir_locations = vec![
981 Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
982 Box::new(dirs::cache_dir),
983 Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
984 ];
985
986 for tmp_dir_location in tmp_dir_locations {
987 if let Some(tmp_dir_location) = tmp_dir_location() {
988 let tmp_dir = tempfile::Builder::new()
989 .prefix("tauri_current_app")
990 .tempdir_in(tmp_dir_location)?;
991 let tmp_dir_metadata = tmp_dir.path().metadata()?;
992
993 if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
994 let mut perms = tmp_dir_metadata.permissions();
995 perms.set_mode(0o700);
996 std::fs::set_permissions(tmp_dir.path(), perms)?;
997
998 let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
999
1000 let permissions = std::fs::metadata(&self.extract_path)?.permissions();
1001
1002 std::fs::rename(&self.extract_path, tmp_app_image)?;
1004
1005 #[cfg(feature = "zip")]
1006 if infer::archive::is_gz(bytes) {
1007 log::debug!("extracting AppImage");
1008 let archive = Cursor::new(bytes);
1011 let decoder = flate2::read::GzDecoder::new(archive);
1012 let mut archive = tar::Archive::new(decoder);
1013 for mut entry in archive.entries()?.flatten() {
1014 if let Ok(path) = entry.path() {
1015 if path.extension() == Some(OsStr::new("AppImage")) {
1016 if let Err(err) = entry.unpack(&self.extract_path) {
1018 std::fs::rename(tmp_app_image, &self.extract_path)?;
1019 return Err(err.into());
1020 }
1021 return Ok(());
1023 }
1024 }
1025 }
1026 std::fs::rename(tmp_app_image, &self.extract_path)?;
1028 return Err(Error::BinaryNotFoundInArchive);
1029 }
1030
1031 log::debug!("rewriting AppImage");
1032 return match std::fs::write(&self.extract_path, bytes)
1033 .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
1034 {
1035 Err(err) => {
1036 std::fs::rename(tmp_app_image, &self.extract_path)?;
1038 Err(err.into())
1039 }
1040 Ok(_) => Ok(()),
1041 };
1042 }
1043 }
1044 }
1045
1046 Err(Error::TempDirNotOnSameMountPoint)
1047 }
1048
1049 fn install_deb(&self, bytes: &[u8]) -> Result<()> {
1050 if !infer::archive::is_deb(bytes) {
1052 log::warn!("update is not a valid deb package");
1053 return Err(Error::InvalidUpdaterFormat);
1054 }
1055
1056 self.try_tmp_locations(bytes, "dpkg", "-i", "deb")
1057 }
1058
1059 fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
1060 if !infer::archive::is_rpm(bytes) {
1062 return Err(Error::InvalidUpdaterFormat);
1063 }
1064 self.try_tmp_locations(bytes, "rpm", "-U", "rpm")
1065 }
1066
1067 fn try_tmp_locations(
1068 &self,
1069 bytes: &[u8],
1070 install_cmd: &str,
1071 install_arg: &str,
1072 package_extension: &str,
1073 ) -> Result<()> {
1074 let tmp_dir_locations = vec![
1076 Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1077 Box::new(dirs::cache_dir),
1078 Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1079 ];
1080
1081 for tmp_dir_location in tmp_dir_locations {
1083 if let Some(path) = tmp_dir_location() {
1084 let prefix = format!("tauri_{package_extension}_update");
1085 if let Ok(tmp_dir) = tempfile::Builder::new().prefix(&prefix).tempdir_in(path) {
1086 let pkg_path = tmp_dir.path().join(format!("package.{package_extension}"));
1087
1088 if std::fs::write(&pkg_path, bytes).is_ok() {
1090 return self.try_install_with_privileges(
1092 &pkg_path,
1093 install_cmd,
1094 install_arg,
1095 );
1096 }
1097 }
1099 }
1100 }
1101
1102 Err(Error::TempDirNotFound)
1104 }
1105
1106 fn try_install_with_privileges(
1107 &self,
1108 pkg_path: &Path,
1109 install_cmd: &str,
1110 install_arg: &str,
1111 ) -> Result<()> {
1112 if let Ok(status) = std::process::Command::new("pkexec")
1114 .arg(install_cmd)
1115 .arg(install_arg)
1116 .arg(pkg_path)
1117 .status()
1118 {
1119 if status.success() {
1120 log::debug!("installed {pkg_path:?} with pkexec");
1121 return Ok(());
1122 }
1123 }
1124
1125 if let Ok(password) = self.get_password_graphically() {
1127 if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
1128 log::debug!("installed {pkg_path:?} with GUI sudo");
1129 return Ok(());
1130 }
1131 }
1132
1133 let status = std::process::Command::new("sudo")
1135 .arg(install_cmd)
1136 .arg(install_arg)
1137 .arg(pkg_path)
1138 .status()?;
1139
1140 if status.success() {
1141 log::debug!("installed {pkg_path:?} with sudo");
1142 Ok(())
1143 } else {
1144 Err(Error::PackageInstallFailed)
1145 }
1146 }
1147
1148 fn get_password_graphically(&self) -> Result<String> {
1149 let zenity_result = std::process::Command::new("zenity")
1151 .args([
1152 "--password",
1153 "--title=Authentication Required",
1154 "--text=Enter your password to install the update:",
1155 ])
1156 .output();
1157
1158 if let Ok(output) = zenity_result {
1159 if output.status.success() {
1160 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1161 }
1162 }
1163
1164 let kdialog_result = std::process::Command::new("kdialog")
1166 .args(["--password", "Enter your password to install the update:"])
1167 .output();
1168
1169 if let Ok(output) = kdialog_result {
1170 if output.status.success() {
1171 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1172 }
1173 }
1174
1175 Err(Error::AuthenticationFailed)
1176 }
1177
1178 fn install_with_sudo(
1179 &self,
1180 pkg_path: &Path,
1181 password: &str,
1182 install_cmd: &str,
1183 install_arg: &str,
1184 ) -> Result<bool> {
1185 use std::io::Write;
1186 use std::process::{Command, Stdio};
1187
1188 let mut child = Command::new("sudo")
1189 .arg("-S") .arg(install_cmd)
1191 .arg(install_arg)
1192 .arg(pkg_path)
1193 .stdin(Stdio::piped())
1194 .stdout(Stdio::piped())
1195 .stderr(Stdio::piped())
1196 .spawn()?;
1197
1198 if let Some(mut stdin) = child.stdin.take() {
1199 writeln!(stdin, "{password}")?;
1201 }
1202
1203 let status = child.wait()?;
1204 Ok(status.success())
1205 }
1206}
1207
1208#[cfg(target_os = "macos")]
1210impl Update {
1211 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1218 use flate2::read::GzDecoder;
1219
1220 let cursor = Cursor::new(bytes);
1221 let mut extracted_files: Vec<PathBuf> = Vec::new();
1222
1223 let tmp_backup_dir = tempfile::Builder::new()
1225 .prefix("tauri_current_app")
1226 .tempdir()?;
1227
1228 let tmp_extract_dir = tempfile::Builder::new()
1229 .prefix("tauri_updated_app")
1230 .tempdir()?;
1231
1232 let decoder = GzDecoder::new(cursor);
1233 let mut archive = tar::Archive::new(decoder);
1234
1235 for entry in archive.entries()? {
1237 let mut entry = entry?;
1238 let collected_path: PathBuf = entry.path()?.iter().skip(1).collect();
1239 let extraction_path = tmp_extract_dir.path().join(&collected_path);
1240
1241 if let Some(parent) = extraction_path.parent() {
1243 std::fs::create_dir_all(parent)?;
1244 }
1245
1246 if let Err(err) = entry.unpack(&extraction_path) {
1247 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1249 return Err(err.into());
1250 }
1251 extracted_files.push(extraction_path);
1252 }
1253
1254 let move_result = std::fs::rename(
1256 &self.extract_path,
1257 tmp_backup_dir.path().join("current_app"),
1258 );
1259 let need_authorization = if let Err(err) = move_result {
1260 if err.kind() == std::io::ErrorKind::PermissionDenied {
1261 true
1262 } else {
1263 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1264 return Err(err.into());
1265 }
1266 } else {
1267 false
1268 };
1269
1270 if need_authorization {
1271 log::debug!("app installation needs admin privileges");
1272 let apple_script = format!(
1274 "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges",
1275 src = self.extract_path.display(),
1276 new = tmp_extract_dir.path().display()
1277 );
1278
1279 let (tx, rx) = std::sync::mpsc::channel();
1280 let res = (self.run_on_main_thread)(Box::new(move || {
1281 let mut script =
1282 osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script);
1283 script.compile().expect("invalid AppleScript");
1284 let r = script.execute();
1285 tx.send(r).unwrap();
1286 }));
1287 let result = rx.recv().unwrap();
1288
1289 if res.is_err() || result.is_err() {
1290 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1291 return Err(Error::Io(std::io::Error::new(
1292 std::io::ErrorKind::PermissionDenied,
1293 "Failed to move the new app into place",
1294 )));
1295 }
1296 } else {
1297 if self.extract_path.exists() {
1299 std::fs::remove_dir_all(&self.extract_path)?;
1300 }
1301 std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?;
1303 }
1304
1305 let _ = std::process::Command::new("touch")
1306 .arg(&self.extract_path)
1307 .status();
1308
1309 Ok(())
1310 }
1311}
1312
1313pub fn target() -> Option<String> {
1317 if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
1318 Some(format!("{target}-{arch}"))
1319 } else {
1320 None
1321 }
1322}
1323
1324fn updater_os() -> Option<&'static str> {
1325 if cfg!(target_os = "linux") {
1326 Some("linux")
1327 } else if cfg!(target_os = "macos") {
1328 Some("darwin")
1330 } else if cfg!(target_os = "windows") {
1331 Some("windows")
1332 } else {
1333 None
1334 }
1335}
1336
1337fn updater_arch() -> Option<&'static str> {
1338 if cfg!(target_arch = "x86") {
1339 Some("i686")
1340 } else if cfg!(target_arch = "x86_64") {
1341 Some("x86_64")
1342 } else if cfg!(target_arch = "arm") {
1343 Some("armv7")
1344 } else if cfg!(target_arch = "aarch64") {
1345 Some("aarch64")
1346 } else if cfg!(target_arch = "riscv64") {
1347 Some("riscv64")
1348 } else {
1349 None
1350 }
1351}
1352
1353pub fn extract_path_from_executable(executable_path: &Path) -> Result<PathBuf> {
1354 let extract_path = executable_path
1357 .parent()
1358 .map(PathBuf::from)
1359 .ok_or(Error::FailedToDetermineExtractPath)?;
1360
1361 #[cfg(target_os = "macos")]
1366 if extract_path
1367 .display()
1368 .to_string()
1369 .contains("Contents/MacOS")
1370 {
1371 return extract_path
1372 .parent()
1373 .map(PathBuf::from)
1374 .ok_or(Error::FailedToDetermineExtractPath)?
1375 .parent()
1376 .map(PathBuf::from)
1377 .ok_or(Error::FailedToDetermineExtractPath);
1378 }
1379
1380 Ok(extract_path)
1381}
1382
1383impl<'de> Deserialize<'de> for RemoteRelease {
1384 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1385 where
1386 D: Deserializer<'de>,
1387 {
1388 #[derive(Deserialize)]
1389 struct InnerRemoteRelease {
1390 #[serde(alias = "name", deserialize_with = "parse_version")]
1391 version: Version,
1392 notes: Option<String>,
1393 pub_date: Option<String>,
1394 platforms: Option<HashMap<String, ReleaseManifestPlatform>>,
1395 url: Option<Url>,
1397 signature: Option<String>,
1398 }
1399
1400 let release = InnerRemoteRelease::deserialize(deserializer)?;
1401
1402 let pub_date = if let Some(date) = release.pub_date {
1403 Some(
1404 OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
1405 .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
1406 )
1407 } else {
1408 None
1409 };
1410
1411 Ok(RemoteRelease {
1412 version: release.version,
1413 notes: release.notes,
1414 pub_date,
1415 data: if let Some(platforms) = release.platforms {
1416 RemoteReleaseInner::Static { platforms }
1417 } else {
1418 RemoteReleaseInner::Dynamic(ReleaseManifestPlatform {
1419 url: release.url.ok_or_else(|| {
1420 DeError::custom("the `url` field was not set on the updater response")
1421 })?,
1422 signature: release.signature.ok_or_else(|| {
1423 DeError::custom("the `signature` field was not set on the updater response")
1424 })?,
1425 })
1426 },
1427 })
1428 }
1429}
1430
1431fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
1432 match bundle? {
1433 BundleType::Deb => Some(Installer::Deb),
1434 BundleType::Rpm => Some(Installer::Rpm),
1435 BundleType::AppImage => Some(Installer::AppImage),
1436 BundleType::Msi => Some(Installer::Msi),
1437 BundleType::Nsis => Some(Installer::Nsis),
1438 BundleType::App => Some(Installer::App), _ => None,
1440 }
1441}
1442
1443fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
1444where
1445 D: serde::Deserializer<'de>,
1446{
1447 let str = String::deserialize(deserializer)?;
1448
1449 Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom)
1450}
1451
1452fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
1454 let pub_key_decoded = base64_to_string(pub_key)?;
1456 let public_key = PublicKey::decode(&pub_key_decoded)?;
1457 let signature_base64_decoded = base64_to_string(release_signature)?;
1458 let signature = Signature::decode(&signature_base64_decoded)?;
1459
1460 public_key.verify(data, &signature, true)?;
1462 Ok(true)
1463}
1464
1465fn base64_to_string(base64_string: &str) -> Result<String> {
1466 let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
1467 let result = std::str::from_utf8(decoded_string)
1468 .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
1469 .to_string();
1470 Ok(result)
1471}
1472
1473#[cfg(windows)]
1474fn encode_wide(string: impl AsRef<OsStr>) -> Vec<u16> {
1475 use std::os::windows::ffi::OsStrExt;
1476
1477 string
1478 .as_ref()
1479 .encode_wide()
1480 .chain(std::iter::once(0))
1481 .collect()
1482}
1483
1484#[cfg(windows)]
1485trait PathExt {
1486 fn wrap_in_quotes(&self) -> Self;
1487}
1488
1489#[cfg(windows)]
1490impl PathExt for PathBuf {
1491 fn wrap_in_quotes(&self) -> Self {
1492 let mut msi_path = OsString::from("\"");
1493 msi_path.push(self.as_os_str());
1494 msi_path.push("\"");
1495 PathBuf::from(msi_path)
1496 }
1497}
1498
1499#[cfg(windows)]
1501fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String {
1502 let arg = arg.to_string_lossy();
1503 let mut cmd: Vec<char> = Vec::new();
1504
1505 let quote = arg.chars().any(|c| c == ' ' || c == '\t' || c == '/') || arg.is_empty();
1507 let escape = true;
1508 if quote {
1509 cmd.push('"');
1510 }
1511 let mut backslashes: usize = 0;
1512 for x in arg.chars() {
1513 if escape {
1514 if x == '\\' {
1515 backslashes += 1;
1516 } else {
1517 if x == '"' {
1518 cmd.extend((0..=backslashes).map(|_| '\\'));
1520 }
1521 backslashes = 0;
1522 }
1523 }
1524 cmd.push(x);
1525 }
1526 if quote {
1527 cmd.extend((0..backslashes).map(|_| '\\'));
1529 cmd.push('"');
1530 }
1531 cmd.into_iter().collect()
1532}
1533
1534#[cfg(windows)]
1535fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
1536 let mut arg = arg.as_ref().to_string_lossy().to_string();
1537
1538 if arg.is_empty() {
1540 return "\"\"\"\"".to_string();
1541 } else if !arg.contains(' ') && !arg.contains('"') {
1542 return arg;
1543 }
1544
1545 if arg.contains('"') {
1546 arg = arg.replace('"', r#""""""#);
1547 }
1548
1549 if arg.starts_with('-') {
1550 if let Some((a1, a2)) = arg.split_once('=') {
1551 format!("{a1}=\"\"{a2}\"\"")
1552 } else {
1553 format!("\"\"{arg}\"\"")
1554 }
1555 } else {
1556 format!("\"\"{arg}\"\"")
1557 }
1558}
1559
1560#[cfg(test)]
1561mod tests {
1562
1563 #[test]
1564 #[cfg(windows)]
1565 fn it_wraps_correctly() {
1566 use super::PathExt;
1567 use std::path::PathBuf;
1568
1569 assert_eq!(
1570 PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(),
1571 PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
1572 )
1573 }
1574
1575 #[test]
1576 #[cfg(windows)]
1577 fn it_escapes_correctly_for_msi() {
1578 use crate::updater::escape_msi_property_arg;
1579
1580 let cases = [
1589 "something",
1590 "--flag",
1591 "--empty=",
1592 "--arg=value",
1593 "some space", "--arg value", "--arg=unwrapped space", "--arg=\"wrapped\"", "--arg=\"wrapped space\"", "--arg=midword\"wrapped space\"", "", ];
1601 let cases_escaped = [
1602 "something",
1603 "--flag",
1604 "--empty=",
1605 "--arg=value",
1606 "\"\"some space\"\"",
1607 "\"\"--arg value\"\"",
1608 "--arg=\"\"unwrapped space\"\"",
1609 r#"--arg=""""""wrapped"""""""#,
1610 r#"--arg=""""""wrapped space"""""""#,
1611 r#"--arg=""midword""""wrapped space"""""""#,
1612 "\"\"\"\"",
1613 ];
1614
1615 assert_eq!(cases.len(), cases_escaped.len());
1617
1618 for (orig, escaped) in cases.iter().zip(cases_escaped) {
1619 assert_eq!(escape_msi_property_arg(orig), escaped);
1620 }
1621 }
1622
1623 #[test]
1624 #[cfg(windows)]
1625 fn it_escapes_correctly_for_nsis() {
1626 use crate::updater::escape_nsis_current_exe_arg;
1627 use std::ffi::OsStr;
1628
1629 let cases = [
1630 "something",
1631 "--flag",
1632 "--empty=",
1633 "--arg=value",
1634 "some space", "--arg value", "--arg=unwrapped space", "--arg=\"wrapped\"", "--arg=\"wrapped space\"", "--arg=midword\"wrapped space\"", "", ];
1642 let cases_escaped = [
1645 "something",
1646 "--flag",
1647 "--empty=",
1648 "--arg=value",
1649 "\"some space\"",
1650 "\"--arg value\"",
1651 "\"--arg=unwrapped space\"",
1652 "--arg=\\\"wrapped\\\"",
1653 "\"--arg=\\\"wrapped space\\\"\"",
1654 "\"--arg=midword\\\"wrapped space\\\"\"",
1655 "\"\"",
1656 ];
1657
1658 assert_eq!(cases.len(), cases_escaped.len());
1660
1661 for (orig, escaped) in cases.iter().zip(cases_escaped) {
1662 assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped);
1663 }
1664 }
1665}