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>;
132#[cfg(target_os = "macos")]
133type MainThreadClosure = Box<dyn FnOnce() + Send + Sync + 'static>;
134#[cfg(target_os = "macos")]
135type RunOnMainThread = Arc<dyn Fn(MainThreadClosure) -> tauri::Result<()> + Send + Sync + 'static>;
136
137#[derive(Clone)]
140struct UpdaterContext {
141 config: Config,
142 configure_client: Option<OnBeforeRequest>,
143 #[cfg(target_os = "macos")]
144 run_on_main_thread: RunOnMainThread,
145 #[cfg(windows)]
147 app_name: String,
148 #[cfg(windows)]
149 installer_args: Vec<OsString>,
150 #[cfg(windows)]
151 current_exe_args: Vec<OsString>,
152 #[cfg(windows)]
153 on_before_exit: Option<OnBeforeExit>,
154 #[cfg(windows)]
155 restart_after_install: bool,
156}
157
158pub struct UpdaterBuilder {
159 current_version: Version,
160 pub(crate) version_comparator: Option<VersionComparator>,
161 executable_path: Option<PathBuf>,
162 target: Option<String>,
163 endpoints: Option<Vec<Url>>,
164 headers: HeaderMap,
165 timeout: Option<Duration>,
166 proxy: Option<Url>,
167 no_proxy: bool,
168 context: UpdaterContext,
169}
170
171impl UpdaterBuilder {
172 pub(crate) fn new<R: Runtime>(app: &AppHandle<R>, config: crate::Config) -> Self {
173 #[cfg(target_os = "macos")]
174 let run_on_main_thread = {
175 let app_ = app.clone();
176 Arc::new(move |f| app_.run_on_main_thread(f))
177 };
178 Self {
179 context: UpdaterContext {
180 #[cfg(windows)]
181 installer_args: config
182 .windows
183 .as_ref()
184 .map(|w| w.installer_args.clone())
185 .unwrap_or_default(),
186 config,
187 configure_client: None,
188 #[cfg(target_os = "macos")]
189 run_on_main_thread,
190 #[cfg(windows)]
191 app_name: app.package_info().name.clone(),
192 #[cfg(windows)]
193 current_exe_args: Vec::new(),
194 #[cfg(windows)]
195 on_before_exit: None,
196 #[cfg(windows)]
197 restart_after_install: true,
198 },
199 current_version: app.package_info().version.clone(),
200 version_comparator: None,
201 executable_path: None,
202 target: None,
203 endpoints: None,
204 headers: Default::default(),
205 timeout: None,
206 proxy: None,
207 no_proxy: false,
208 }
209 }
210
211 pub fn version_comparator<F: Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static>(
212 mut self,
213 f: F,
214 ) -> Self {
215 self.version_comparator = Some(Arc::new(f));
216 self
217 }
218
219 pub fn target(mut self, target: impl Into<String>) -> Self {
220 self.target.replace(target.into());
221 self
222 }
223
224 pub fn endpoints(mut self, endpoints: Vec<Url>) -> Result<Self> {
225 crate::config::validate_endpoints(
226 &endpoints,
227 self.context.config.dangerous_insecure_transport_protocol,
228 )?;
229
230 self.endpoints.replace(endpoints);
231 Ok(self)
232 }
233
234 pub fn executable_path<P: AsRef<Path>>(mut self, p: P) -> Self {
235 self.executable_path.replace(p.as_ref().into());
236 self
237 }
238
239 pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
240 where
241 HeaderName: TryFrom<K>,
242 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
243 HeaderValue: TryFrom<V>,
244 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
245 {
246 let key: std::result::Result<HeaderName, http::Error> = key.try_into().map_err(Into::into);
247 let value: std::result::Result<HeaderValue, http::Error> =
248 value.try_into().map_err(Into::into);
249 self.headers.insert(key?, value?);
250
251 Ok(self)
252 }
253
254 pub fn headers(mut self, headers: HeaderMap) -> Self {
255 self.headers = headers;
256 self
257 }
258
259 pub fn clear_headers(mut self) -> Self {
260 self.headers.clear();
261 self
262 }
263
264 pub fn timeout(mut self, timeout: Duration) -> Self {
265 self.timeout = Some(timeout);
266 self
267 }
268
269 pub fn proxy(mut self, proxy: Url) -> Self {
270 self.proxy.replace(proxy);
271 self
272 }
273
274 pub fn no_proxy(mut self) -> Self {
276 self.no_proxy = true;
277 self
278 }
279
280 pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
281 self.context.config.pubkey = pubkey.into();
282 self
283 }
284
285 #[cfg_attr(not(windows), allow(unused))]
289 pub fn installer_arg<S>(mut self, arg: S) -> Self
290 where
291 S: Into<OsString>,
292 {
293 #[cfg(windows)]
294 {
295 self.context.installer_args.push(arg.into());
296 }
297 self
298 }
299
300 #[cfg_attr(not(windows), allow(unused))]
304 pub fn installer_args<I, S>(mut self, args: I) -> Self
305 where
306 I: IntoIterator<Item = S>,
307 S: Into<OsString>,
308 {
309 #[cfg(windows)]
310 {
311 self.context
312 .installer_args
313 .extend(args.into_iter().map(Into::into));
314 }
315 self
316 }
317
318 #[cfg_attr(not(windows), allow(unused))]
325 pub fn clear_installer_args(mut self) -> Self {
326 #[cfg(windows)]
327 {
328 self.context.installer_args.clear();
329 }
330 self
331 }
332
333 #[cfg_attr(not(windows), allow(unused))]
335 pub fn on_before_exit<F: Fn() + Send + Sync + 'static>(mut self, f: F) -> Self {
336 #[cfg(windows)]
337 {
338 self.context.on_before_exit.replace(Arc::new(f));
339 }
340 self
341 }
342
343 #[cfg_attr(not(windows), allow(unused))]
345 pub fn restart_after_install(mut self, restart_after_install: bool) -> Self {
346 #[cfg(windows)]
347 {
348 self.context.restart_after_install = restart_after_install;
349 }
350 self
351 }
352
353 pub fn configure_client<F: Fn(ClientBuilder) -> ClientBuilder + Send + Sync + 'static>(
358 mut self,
359 f: F,
360 ) -> Self {
361 self.context.configure_client.replace(Arc::new(f));
362 self
363 }
364
365 pub fn build(self) -> Result<Updater> {
366 let endpoints = self
367 .endpoints
368 .unwrap_or_else(|| self.context.config.endpoints.clone());
369
370 if endpoints.is_empty() {
371 return Err(Error::EmptyEndpoints);
372 };
373
374 let arch = updater_arch().ok_or(Error::UnsupportedArch)?;
375
376 let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
377
378 let extract_path = if cfg!(target_os = "linux") {
380 executable_path
381 } else {
382 extract_path_from_executable(&executable_path)?
383 };
384
385 Ok(Updater {
386 current_version: self.current_version,
387 version_comparator: self.version_comparator,
388 timeout: self.timeout,
389 proxy: self.proxy,
390 no_proxy: self.no_proxy,
391 endpoints,
392 arch,
393 target: self.target,
394 headers: self.headers,
395 extract_path,
396 context: self.context.clone(),
397 })
398 }
399}
400
401#[cfg(windows)]
402impl UpdaterBuilder {
403 pub(crate) fn current_exe_args<I, S>(mut self, args: I) -> Self
404 where
405 I: IntoIterator<Item = S>,
406 S: Into<OsString>,
407 {
408 self.context
409 .current_exe_args
410 .extend(args.into_iter().map(Into::into));
411 self
412 }
413}
414
415pub struct Updater {
416 current_version: Version,
417 version_comparator: Option<VersionComparator>,
418 timeout: Option<Duration>,
419 proxy: Option<Url>,
420 no_proxy: bool,
421 endpoints: Vec<Url>,
422 arch: &'static str,
423 target: Option<String>,
426 headers: HeaderMap,
427 extract_path: PathBuf,
428 context: UpdaterContext,
429}
430
431impl Updater {
432 pub async fn check(&self) -> Result<Option<Update>> {
433 let mut headers = self.headers.clone();
435 if !headers.contains_key(ACCEPT) {
436 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
437 }
438
439 #[cfg(target_os = "linux")]
441 {
442 if std::env::var_os("SSL_CERT_FILE").is_none() {
443 std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
444 }
445 if std::env::var_os("SSL_CERT_DIR").is_none() {
446 std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
447 }
448 }
449 let target = if let Some(target) = &self.target {
450 target
451 } else {
452 updater_os().ok_or(Error::UnsupportedOs)?
453 };
454
455 let mut remote_release: Option<RemoteRelease> = None;
456 let mut raw_json: Option<serde_json::Value> = None;
457 let mut last_error: Option<Error> = None;
458 for url in &self.endpoints {
459 let version = self.current_version.to_string();
467 let version = version.as_bytes();
468 const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+');
469 let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD);
470 let encoded_version = encoded_version.to_string();
471 let installer = installer_for_bundle_type(bundle_type())
472 .map(|i| i.name())
473 .unwrap_or("unknown");
474
475 let url: Url = url
476 .to_string()
477 .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version)
479 .replace("%7B%7Btarget%7D%7D", target)
480 .replace("%7B%7Barch%7D%7D", self.arch)
481 .replace("%7B%7Bbundle_type%7D%7D", installer)
482 .replace("{{current_version}}", &encoded_version)
484 .replace("{{target}}", target)
485 .replace("{{arch}}", self.arch)
486 .replace("{{bundle_type}}", installer)
487 .parse()?;
488
489 log::debug!("checking for updates {url}");
490
491 #[cfg(feature = "rustls-tls")]
492 if rustls::crypto::CryptoProvider::get_default().is_none() {
493 let _ = rustls::crypto::ring::default_provider().install_default();
495 }
496
497 let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
498 if self.context.config.dangerous_accept_invalid_certs {
499 request = request.danger_accept_invalid_certs(true);
500 }
501 if self.context.config.dangerous_accept_invalid_hostnames {
502 request = request.danger_accept_invalid_hostnames(true);
503 }
504 if let Some(timeout) = self.timeout {
505 request = request.timeout(timeout);
506 }
507 if self.no_proxy {
508 log::debug!("disabling proxy");
509 request = request.no_proxy();
510 } else if let Some(ref proxy) = self.proxy {
511 log::debug!("using proxy {proxy}");
512 let proxy = reqwest::Proxy::all(proxy.as_str())?;
513 request = request.proxy(proxy);
514 }
515
516 if let Some(ref configure_client) = self.context.configure_client {
517 request = configure_client(request);
518 }
519
520 let response = request
521 .build()?
522 .get(url)
523 .headers(headers.clone())
524 .send()
525 .await;
526
527 match response {
528 Ok(res) => {
529 if res.status().is_success() {
530 if StatusCode::NO_CONTENT == res.status() {
532 log::debug!("update endpoint returned 204 No Content");
533 return Ok(None);
534 };
535
536 let update_response: serde_json::Value = res.json().await?;
537 log::debug!("update response: {update_response:?}");
538 raw_json = Some(update_response.clone());
539 match serde_json::from_value::<RemoteRelease>(update_response)
540 .map_err(Into::into)
541 {
542 Ok(release) => {
543 log::debug!("parsed release response {release:?}");
544 last_error = None;
545 remote_release = Some(release);
546 break;
548 }
549 Err(err) => {
550 log::error!("failed to deserialize update response: {err}");
551 last_error = Some(err)
552 }
553 }
554 } else {
555 log::error!(
556 "update endpoint did not respond with a successful status code"
557 );
558 }
559 }
560 Err(err) => {
561 log::error!("failed to check for updates: {err}");
562 last_error = Some(err.into())
563 }
564 }
565 }
566
567 if let Some(error) = last_error {
570 return Err(error);
571 }
572
573 let release = remote_release.ok_or(Error::ReleaseNotFound)?;
575
576 let should_update = match self.version_comparator.as_ref() {
577 Some(comparator) => comparator(self.current_version.clone(), release.clone()),
578 None => release.version > self.current_version,
579 };
580
581 let installer = installer_for_bundle_type(bundle_type());
582 let (download_url, signature) = self.get_urls(&release, &installer)?;
583
584 let update = if should_update {
585 Some(Update {
586 current_version: self.current_version.to_string(),
587 target: target.to_owned(),
588 extract_path: self.extract_path.clone(),
589 version: release.version.to_string(),
590 date: release.pub_date,
591 download_url: download_url.clone(),
592 signature: signature.to_owned(),
593 body: release.notes,
594 raw_json: raw_json.unwrap(),
595 timeout: None,
596 proxy: self.proxy.clone(),
597 no_proxy: self.no_proxy,
598 headers: self.headers.clone(),
599 context: self.context.clone(),
600 })
601 } else {
602 None
603 };
604
605 Ok(update)
606 }
607
608 fn get_urls<'a>(
609 &self,
610 release: &'a RemoteRelease,
611 installer: &Option<Installer>,
612 ) -> Result<(&'a Url, &'a String)> {
613 if let Some(target) = &self.target {
615 return Ok((release.download_url(target)?, release.signature(target)?));
616 }
617
618 let os = updater_os().ok_or(Error::UnsupportedOs)?;
620 let arch = self.arch;
621 let mut targets = Vec::new();
622 if let Some(installer) = installer {
623 let installer = installer.name();
624 targets.push(format!("{os}-{arch}-{installer}"));
625 }
626 targets.push(format!("{os}-{arch}"));
627
628 for target in &targets {
629 log::debug!("Searching for updater target '{target}' in release data");
630 if let (Ok(download_url), Ok(signature)) =
631 (release.download_url(target), release.signature(target))
632 {
633 return Ok((download_url, signature));
634 };
635 }
636
637 Err(Error::TargetsNotFound(targets))
638 }
639}
640
641#[derive(Clone)]
642pub struct Update {
643 pub body: Option<String>,
645 pub current_version: String,
647 pub version: String,
649 pub date: Option<OffsetDateTime>,
651 pub target: String,
654 pub download_url: Url,
656 pub signature: String,
658 pub raw_json: serde_json::Value,
660 pub timeout: Option<Duration>,
662 pub proxy: Option<Url>,
664 pub no_proxy: bool,
666 pub headers: HeaderMap,
668 #[allow(unused)]
670 extract_path: PathBuf,
671 context: UpdaterContext,
672}
673
674impl Resource for Update {}
675
676impl Update {
677 pub async fn download<C: FnMut(usize, Option<u64>), D: FnOnce()>(
681 &self,
682 mut on_chunk: C,
683 on_download_finish: D,
684 ) -> Result<Vec<u8>> {
685 let mut headers = self.headers.clone();
687 if !headers.contains_key(ACCEPT) {
688 headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
689 }
690
691 let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
692 if self.context.config.dangerous_accept_invalid_certs {
693 request = request.danger_accept_invalid_certs(true);
694 }
695 if self.context.config.dangerous_accept_invalid_hostnames {
696 request = request.danger_accept_invalid_hostnames(true);
697 }
698 if let Some(timeout) = self.timeout {
699 request = request.timeout(timeout);
700 }
701 if self.no_proxy {
702 request = request.no_proxy();
703 } else if let Some(ref proxy) = self.proxy {
704 let proxy = reqwest::Proxy::all(proxy.as_str())?;
705 request = request.proxy(proxy);
706 }
707 if let Some(ref configure_client) = self.context.configure_client {
708 request = configure_client(request);
709 }
710 let response = request
711 .build()?
712 .get(self.download_url.clone())
713 .headers(headers)
714 .send()
715 .await?;
716
717 if !response.status().is_success() {
718 return Err(Error::Network(format!(
719 "Download request failed with status: {}",
720 response.status()
721 )));
722 }
723
724 let content_length: Option<u64> = response
725 .headers()
726 .get("Content-Length")
727 .and_then(|value| value.to_str().ok())
728 .and_then(|value| value.parse().ok());
729
730 let mut buffer = Vec::new();
731
732 let mut stream = response.bytes_stream();
733 while let Some(chunk) = stream.next().await {
734 let chunk = chunk?;
735 on_chunk(chunk.len(), content_length);
736 buffer.extend(chunk);
737 }
738 on_download_finish();
739
740 verify_signature(
741 &buffer,
742 &self.signature,
743 &self.context.config.pubkey,
744 &self.version,
745 self.context.config.require_signed_version,
746 )?;
747
748 Ok(buffer)
749 }
750
751 pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
758 self.install_inner(bytes.as_ref())
759 }
760
761 pub async fn download_and_install<C: FnMut(usize, Option<u64>), D: FnOnce()>(
768 &self,
769 on_chunk: C,
770 on_download_finish: D,
771 ) -> Result<()> {
772 let bytes = self.download(on_chunk, on_download_finish).await?;
773 self.install(bytes)
774 }
775
776 #[cfg(mobile)]
777 fn install_inner(&self, _bytes: &[u8]) -> Result<()> {
778 Ok(())
779 }
780
781 #[cfg_attr(not(windows), allow(unused))]
783 pub fn restart_after_install(mut self, restart_after_install: bool) -> Self {
784 #[cfg(windows)]
785 {
786 self.context.restart_after_install = restart_after_install;
787 }
788 self
789 }
790}
791
792#[cfg(windows)]
793enum WindowsUpdaterType {
794 Nsis {
795 path: PathBuf,
796 #[allow(unused)]
797 temp: Option<tempfile::TempPath>,
798 },
799 Msi {
800 path: PathBuf,
801 #[allow(unused)]
802 temp: Option<tempfile::TempPath>,
803 },
804}
805
806#[cfg(windows)]
807impl WindowsUpdaterType {
808 fn nsis(path: PathBuf, temp: Option<tempfile::TempPath>) -> Self {
809 Self::Nsis { path, temp }
810 }
811
812 fn msi(path: PathBuf, temp: Option<tempfile::TempPath>) -> Self {
813 Self::Msi {
814 path: path.wrap_in_quotes(),
815 temp,
816 }
817 }
818}
819
820#[cfg(windows)]
821impl Config {
822 fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode {
823 self.windows
824 .as_ref()
825 .map(|w| w.install_mode.clone())
826 .unwrap_or_default()
827 }
828}
829
830#[cfg(windows)]
832impl Update {
833 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
842 use windows_sys::{
843 w,
844 Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW},
845 };
846
847 let updater_type = self.extract(bytes)?;
848
849 if let Some(on_before_exit) = self.context.on_before_exit.as_ref() {
850 log::debug!("running on_before_exit hook");
851 on_before_exit();
852 }
853
854 let file = match &updater_type {
855 WindowsUpdaterType::Nsis { path, .. } => path.as_os_str().to_os_string(),
856 WindowsUpdaterType::Msi { .. } => std::env::var("SYSTEMROOT").as_ref().map_or_else(
857 |_| OsString::from("msiexec.exe"),
858 |p| OsString::from(format!("{p}\\System32\\msiexec.exe")),
859 ),
860 };
861 let parameters = self.updater_parameters(&updater_type);
862
863 log::debug!("Executing updater {file:?} with parameters: {parameters:?}");
864
865 let file = encode_wide(file);
866 let parameters = encode_wide(parameters);
867
868 let result = unsafe {
869 ShellExecuteW(
870 std::ptr::null_mut(),
871 w!("open"),
872 file.as_ptr(),
873 parameters.as_ptr(),
874 std::ptr::null(),
875 SW_SHOW,
876 )
877 };
878 if result as isize <= 32 {
879 return Err(crate::Error::Io(std::io::Error::last_os_error()));
880 }
881
882 std::process::exit(0);
883 }
884
885 fn updater_parameters(&self, updater_type: &WindowsUpdaterType) -> OsString {
886 let install_mode = self.context.config.install_mode();
887 let current_args = &self.context.current_exe_args[1..];
888
889 match updater_type {
890 WindowsUpdaterType::Nsis { .. } => {
891 let mut installer_args: Vec<&OsStr> = Vec::new();
892 installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new));
893 installer_args.push(OsStr::new("/UPDATE"));
894
895 let nsis_current_exe_arg;
896 if self.context.restart_after_install {
897 nsis_current_exe_arg = current_args
898 .iter()
899 .map(escape_nsis_current_exe_arg)
900 .collect::<Vec<_>>();
901
902 installer_args.extend(
903 install_mode
904 .nsis_restart_after_install_args()
905 .iter()
906 .map(OsStr::new),
907 );
908 installer_args.push(OsStr::new("/ARGS"));
909 installer_args.extend(nsis_current_exe_arg.iter().map(OsStr::new));
910 }
911
912 installer_args.extend(self.installer_args());
913
914 installer_args.join(OsStr::new(" "))
915 }
916 WindowsUpdaterType::Msi { path, .. } => {
917 let mut installer_args: Vec<&OsStr> = vec![OsStr::new("/i"), path.as_os_str()];
918 installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new));
919 installer_args.push(OsStr::new("/promptrestart"));
920 installer_args.extend(self.installer_args());
921
922 let msi_current_exe_arg;
923 if self.context.restart_after_install {
924 msi_current_exe_arg = format!(
925 "LAUNCHAPPARGS=\"{}\"",
926 current_args
927 .iter()
928 .map(escape_msi_property_arg)
929 .collect::<Vec<_>>()
930 .join(" ")
931 );
932
933 installer_args.extend(
934 install_mode
935 .msi_restart_after_install_args()
936 .iter()
937 .map(OsStr::new),
938 );
939 installer_args.push(OsStr::new(&msi_current_exe_arg));
940 }
941
942 installer_args.join(OsStr::new(" "))
943 }
944 }
945 }
946
947 fn installer_args(
948 &self,
949 ) -> std::iter::Map<std::slice::Iter<'_, OsString>, fn(&OsString) -> &OsStr> {
950 self.context.installer_args.iter().map(OsStr::new)
951 }
952
953 fn extract(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
954 #[cfg(feature = "zip")]
955 if infer::archive::is_zip(bytes) {
956 return self.extract_zip(bytes);
957 }
958
959 self.extract_exe(bytes)
960 }
961
962 fn make_temp_dir(&self) -> Result<PathBuf> {
963 Ok(tempfile::Builder::new()
964 .prefix(&format!(
965 "{}-{}-updater-",
966 self.context.app_name, self.version
967 ))
968 .tempdir()?
969 .keep())
970 }
971
972 #[cfg(feature = "zip")]
973 fn extract_zip(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
974 let temp_dir = self.make_temp_dir()?;
975
976 let archive = Cursor::new(bytes);
977 let mut extractor = zip::ZipArchive::new(archive)?;
978 extractor.extract(&temp_dir)?;
979
980 let paths = std::fs::read_dir(&temp_dir)?;
981 for path in paths {
982 let path = path?.path();
983 let ext = path.extension();
984 if ext == Some(OsStr::new("exe")) {
985 return Ok(WindowsUpdaterType::nsis(path, None));
986 } else if ext == Some(OsStr::new("msi")) {
987 return Ok(WindowsUpdaterType::msi(path, None));
988 }
989 }
990
991 Err(crate::Error::BinaryNotFoundInArchive)
992 }
993
994 fn extract_exe(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
995 if infer::app::is_exe(bytes) {
996 let (path, temp) = self.write_to_temp(bytes, ".exe")?;
997 Ok(WindowsUpdaterType::nsis(path, temp))
998 } else if infer::archive::is_msi(bytes) {
999 let (path, temp) = self.write_to_temp(bytes, ".msi")?;
1000 Ok(WindowsUpdaterType::msi(path, temp))
1001 } else {
1002 Err(crate::Error::InvalidUpdaterFormat)
1003 }
1004 }
1005
1006 fn write_to_temp(
1007 &self,
1008 bytes: &[u8],
1009 ext: &str,
1010 ) -> Result<(PathBuf, Option<tempfile::TempPath>)> {
1011 use std::io::Write;
1012
1013 let temp_dir = self.make_temp_dir()?;
1014 let mut temp_file = tempfile::Builder::new()
1015 .prefix(&format!(
1016 "{}-{}-installer",
1017 self.context.app_name, self.version
1018 ))
1019 .suffix(ext)
1020 .rand_bytes(0)
1021 .tempfile_in(temp_dir)?;
1022 temp_file.write_all(bytes)?;
1023
1024 let temp = temp_file.into_temp_path();
1025 Ok((temp.to_path_buf(), Some(temp)))
1026 }
1027}
1028
1029#[cfg(any(
1031 target_os = "linux",
1032 target_os = "dragonfly",
1033 target_os = "freebsd",
1034 target_os = "netbsd",
1035 target_os = "openbsd"
1036))]
1037impl Update {
1038 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1046 match installer_for_bundle_type(bundle_type()) {
1047 Some(Installer::Deb) => self.install_deb(bytes),
1048 Some(Installer::Rpm) => self.install_rpm(bytes),
1049 _ => self.install_appimage(bytes),
1050 }
1051 }
1052
1053 fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
1054 use std::os::unix::fs::{MetadataExt, PermissionsExt};
1055 let extract_path_metadata = self.extract_path.metadata()?;
1056
1057 let tmp_dir_locations = vec![
1058 Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1059 Box::new(dirs::cache_dir),
1060 Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1061 ];
1062
1063 for tmp_dir_location in tmp_dir_locations {
1064 if let Some(tmp_dir_location) = tmp_dir_location() {
1065 let tmp_dir = tempfile::Builder::new()
1066 .prefix("tauri_current_app")
1067 .tempdir_in(tmp_dir_location)?;
1068 let tmp_dir_metadata = tmp_dir.path().metadata()?;
1069
1070 if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
1071 let mut perms = tmp_dir_metadata.permissions();
1072 perms.set_mode(0o700);
1073 std::fs::set_permissions(tmp_dir.path(), perms)?;
1074
1075 let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
1076
1077 let permissions = std::fs::metadata(&self.extract_path)?.permissions();
1078
1079 std::fs::rename(&self.extract_path, tmp_app_image)?;
1081
1082 #[cfg(feature = "zip")]
1083 if infer::archive::is_gz(bytes) {
1084 log::debug!("extracting AppImage");
1085 let archive = Cursor::new(bytes);
1088 let decoder = flate2::read::GzDecoder::new(archive);
1089 let mut archive = tar::Archive::new(decoder);
1090 for mut entry in archive.entries()?.flatten() {
1091 if let Ok(path) = entry.path() {
1092 if path.extension() == Some(OsStr::new("AppImage")) {
1093 if let Err(err) = entry.unpack(&self.extract_path) {
1095 std::fs::rename(tmp_app_image, &self.extract_path)?;
1096 return Err(err.into());
1097 }
1098 return Ok(());
1100 }
1101 }
1102 }
1103 std::fs::rename(tmp_app_image, &self.extract_path)?;
1105 return Err(Error::BinaryNotFoundInArchive);
1106 }
1107
1108 log::debug!("rewriting AppImage");
1109 return match std::fs::write(&self.extract_path, bytes)
1110 .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
1111 {
1112 Err(err) => {
1113 std::fs::rename(tmp_app_image, &self.extract_path)?;
1115 Err(err.into())
1116 }
1117 Ok(_) => Ok(()),
1118 };
1119 }
1120 }
1121 }
1122
1123 Err(Error::TempDirNotOnSameMountPoint)
1124 }
1125
1126 fn install_deb(&self, bytes: &[u8]) -> Result<()> {
1127 if !infer::archive::is_deb(bytes) {
1129 log::warn!("update is not a valid deb package");
1130 return Err(Error::InvalidUpdaterFormat);
1131 }
1132
1133 self.try_tmp_locations(bytes, "dpkg", "-i", "deb")
1134 }
1135
1136 fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
1137 if !infer::archive::is_rpm(bytes) {
1139 return Err(Error::InvalidUpdaterFormat);
1140 }
1141 self.try_tmp_locations(bytes, "rpm", "-U", "rpm")
1142 }
1143
1144 fn try_tmp_locations(
1145 &self,
1146 bytes: &[u8],
1147 install_cmd: &str,
1148 install_arg: &str,
1149 package_extension: &str,
1150 ) -> Result<()> {
1151 let tmp_dir_locations = vec![
1153 Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1154 Box::new(dirs::cache_dir),
1155 Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1156 ];
1157
1158 for tmp_dir_location in tmp_dir_locations {
1160 if let Some(path) = tmp_dir_location() {
1161 let prefix = format!("tauri_{package_extension}_update");
1162 if let Ok(tmp_dir) = tempfile::Builder::new().prefix(&prefix).tempdir_in(path) {
1163 let pkg_path = tmp_dir.path().join(format!("package.{package_extension}"));
1164
1165 if std::fs::write(&pkg_path, bytes).is_ok() {
1167 return self.try_install_with_privileges(
1169 &pkg_path,
1170 install_cmd,
1171 install_arg,
1172 );
1173 }
1174 }
1176 }
1177 }
1178
1179 Err(Error::TempDirNotFound)
1181 }
1182
1183 fn try_install_with_privileges(
1184 &self,
1185 pkg_path: &Path,
1186 install_cmd: &str,
1187 install_arg: &str,
1188 ) -> Result<()> {
1189 if let Ok(status) = std::process::Command::new("pkexec")
1191 .arg(install_cmd)
1192 .arg(install_arg)
1193 .arg(pkg_path)
1194 .status()
1195 {
1196 if status.success() {
1197 log::debug!("installed {pkg_path:?} with pkexec");
1198 return Ok(());
1199 }
1200 }
1201
1202 if let Ok(password) = self.get_password_graphically() {
1204 if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
1205 log::debug!("installed {pkg_path:?} with GUI sudo");
1206 return Ok(());
1207 }
1208 }
1209
1210 let status = std::process::Command::new("sudo")
1212 .arg(install_cmd)
1213 .arg(install_arg)
1214 .arg(pkg_path)
1215 .status()?;
1216
1217 if status.success() {
1218 log::debug!("installed {pkg_path:?} with sudo");
1219 Ok(())
1220 } else {
1221 Err(Error::PackageInstallFailed)
1222 }
1223 }
1224
1225 fn get_password_graphically(&self) -> Result<String> {
1226 let zenity_result = std::process::Command::new("zenity")
1228 .args([
1229 "--password",
1230 "--title=Authentication Required",
1231 "--text=Enter your password to install the update:",
1232 ])
1233 .output();
1234
1235 if let Ok(output) = zenity_result {
1236 if output.status.success() {
1237 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1238 }
1239 }
1240
1241 let kdialog_result = std::process::Command::new("kdialog")
1243 .args(["--password", "Enter your password to install the update:"])
1244 .output();
1245
1246 if let Ok(output) = kdialog_result {
1247 if output.status.success() {
1248 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1249 }
1250 }
1251
1252 Err(Error::AuthenticationFailed)
1253 }
1254
1255 fn install_with_sudo(
1256 &self,
1257 pkg_path: &Path,
1258 password: &str,
1259 install_cmd: &str,
1260 install_arg: &str,
1261 ) -> Result<bool> {
1262 use std::io::Write;
1263 use std::process::{Command, Stdio};
1264
1265 let mut child = Command::new("sudo")
1266 .arg("-S") .arg(install_cmd)
1268 .arg(install_arg)
1269 .arg(pkg_path)
1270 .stdin(Stdio::piped())
1271 .stdout(Stdio::piped())
1272 .stderr(Stdio::piped())
1273 .spawn()?;
1274
1275 if let Some(mut stdin) = child.stdin.take() {
1276 writeln!(stdin, "{password}")?;
1278 }
1279
1280 let status = child.wait()?;
1281 Ok(status.success())
1282 }
1283}
1284
1285#[cfg(target_os = "macos")]
1287impl Update {
1288 fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1295 use flate2::read::GzDecoder;
1296
1297 let cursor = Cursor::new(bytes);
1298 let mut extracted_files: Vec<PathBuf> = Vec::new();
1299
1300 let tmp_backup_dir = tempfile::Builder::new()
1302 .prefix("tauri_current_app")
1303 .tempdir()?;
1304
1305 let tmp_extract_dir = tempfile::Builder::new()
1306 .prefix("tauri_updated_app")
1307 .tempdir()?;
1308
1309 let decoder = GzDecoder::new(cursor);
1310 let mut archive = tar::Archive::new(decoder);
1311
1312 for entry in archive.entries()? {
1314 let mut entry = entry?;
1315 let collected_path: PathBuf = entry.path()?.iter().skip(1).collect();
1316 let extraction_path = tmp_extract_dir.path().join(&collected_path);
1317
1318 if let Some(parent) = extraction_path.parent() {
1320 std::fs::create_dir_all(parent)?;
1321 }
1322
1323 if let Err(err) = entry.unpack(&extraction_path) {
1324 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1326 return Err(err.into());
1327 }
1328 extracted_files.push(extraction_path);
1329 }
1330
1331 let move_result = std::fs::rename(
1333 &self.extract_path,
1334 tmp_backup_dir.path().join("current_app"),
1335 );
1336 let need_authorization = if let Err(err) = move_result {
1337 if err.kind() == std::io::ErrorKind::PermissionDenied {
1338 true
1339 } else {
1340 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1341 return Err(err.into());
1342 }
1343 } else {
1344 false
1345 };
1346
1347 if need_authorization {
1348 log::debug!("app installation needs admin privileges");
1349 let apple_script = format!(
1351 "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges",
1352 src = self.extract_path.display(),
1353 new = tmp_extract_dir.path().display()
1354 );
1355
1356 let (tx, rx) = std::sync::mpsc::channel();
1357 let res = (self.context.run_on_main_thread)(Box::new(move || {
1358 let mut script =
1359 osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script);
1360 script.compile().expect("invalid AppleScript");
1361 let r = script.execute();
1362 tx.send(r).unwrap();
1363 }));
1364 let result = rx.recv().unwrap();
1365
1366 if res.is_err() || result.is_err() {
1367 std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1368 return Err(Error::Io(std::io::Error::new(
1369 std::io::ErrorKind::PermissionDenied,
1370 "Failed to move the new app into place",
1371 )));
1372 }
1373 } else {
1374 if self.extract_path.exists() {
1376 std::fs::remove_dir_all(&self.extract_path)?;
1377 }
1378 std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?;
1380 }
1381
1382 let _ = std::process::Command::new("touch")
1383 .arg(&self.extract_path)
1384 .status();
1385
1386 Ok(())
1387 }
1388}
1389
1390pub fn target() -> Option<String> {
1394 if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
1395 Some(format!("{target}-{arch}"))
1396 } else {
1397 None
1398 }
1399}
1400
1401fn updater_os() -> Option<&'static str> {
1402 if cfg!(target_os = "linux") {
1403 Some("linux")
1404 } else if cfg!(target_os = "macos") {
1405 Some("darwin")
1407 } else if cfg!(target_os = "windows") {
1408 Some("windows")
1409 } else {
1410 None
1411 }
1412}
1413
1414fn updater_arch() -> Option<&'static str> {
1415 if cfg!(target_arch = "x86") {
1416 Some("i686")
1417 } else if cfg!(target_arch = "x86_64") {
1418 Some("x86_64")
1419 } else if cfg!(target_arch = "arm") {
1420 Some("armv7")
1421 } else if cfg!(target_arch = "aarch64") {
1422 Some("aarch64")
1423 } else if cfg!(target_arch = "riscv64") {
1424 Some("riscv64")
1425 } else {
1426 None
1427 }
1428}
1429
1430pub fn extract_path_from_executable(executable_path: &Path) -> Result<PathBuf> {
1431 let extract_path = executable_path
1434 .parent()
1435 .map(PathBuf::from)
1436 .ok_or(Error::FailedToDetermineExtractPath)?;
1437
1438 #[cfg(target_os = "macos")]
1443 if extract_path
1444 .display()
1445 .to_string()
1446 .contains("Contents/MacOS")
1447 {
1448 return extract_path
1449 .parent()
1450 .map(PathBuf::from)
1451 .ok_or(Error::FailedToDetermineExtractPath)?
1452 .parent()
1453 .map(PathBuf::from)
1454 .ok_or(Error::FailedToDetermineExtractPath);
1455 }
1456
1457 Ok(extract_path)
1458}
1459
1460impl<'de> Deserialize<'de> for RemoteRelease {
1461 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1462 where
1463 D: Deserializer<'de>,
1464 {
1465 #[derive(Deserialize)]
1466 struct InnerRemoteRelease {
1467 #[serde(alias = "name", deserialize_with = "parse_version")]
1468 version: Version,
1469 notes: Option<String>,
1470 pub_date: Option<String>,
1471 platforms: Option<HashMap<String, ReleaseManifestPlatform>>,
1472 url: Option<Url>,
1474 signature: Option<String>,
1475 }
1476
1477 let release = InnerRemoteRelease::deserialize(deserializer)?;
1478
1479 let pub_date = if let Some(date) = release.pub_date {
1480 Some(
1481 OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
1482 .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
1483 )
1484 } else {
1485 None
1486 };
1487
1488 Ok(RemoteRelease {
1489 version: release.version,
1490 notes: release.notes,
1491 pub_date,
1492 data: if let Some(platforms) = release.platforms {
1493 RemoteReleaseInner::Static { platforms }
1494 } else {
1495 RemoteReleaseInner::Dynamic(ReleaseManifestPlatform {
1496 url: release.url.ok_or_else(|| {
1497 DeError::custom("the `url` field was not set on the updater response")
1498 })?,
1499 signature: release.signature.ok_or_else(|| {
1500 DeError::custom("the `signature` field was not set on the updater response")
1501 })?,
1502 })
1503 },
1504 })
1505 }
1506}
1507
1508fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
1509 match bundle? {
1510 BundleType::Deb => Some(Installer::Deb),
1511 BundleType::Rpm => Some(Installer::Rpm),
1512 BundleType::AppImage => Some(Installer::AppImage),
1513 BundleType::Msi => Some(Installer::Msi),
1514 BundleType::Nsis => Some(Installer::Nsis),
1515 BundleType::App => Some(Installer::App), _ => None,
1517 }
1518}
1519
1520fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
1521where
1522 D: serde::Deserializer<'de>,
1523{
1524 let str = String::deserialize(deserializer)?;
1525
1526 Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom)
1527}
1528
1529fn verify_signature(
1531 data: &[u8],
1532 release_signature: &str,
1533 pub_key: &str,
1534 announced_version: &str,
1535 require_signed_version: bool,
1536) -> Result<()> {
1537 let pub_key_decoded = base64_to_string(pub_key)?;
1539 let public_key = PublicKey::decode(&pub_key_decoded)?;
1540 let signature_base64_decoded = base64_to_string(release_signature)?;
1541 let signature = Signature::decode(&signature_base64_decoded)?;
1542
1543 public_key.verify(data, &signature, true)?;
1545
1546 verify_signed_version(
1550 signature.trusted_comment(),
1551 announced_version,
1552 require_signed_version,
1553 )
1554}
1555
1556fn verify_signed_version(
1563 trusted_comment: &str,
1564 announced_version: &str,
1565 require_signed_version: bool,
1566) -> Result<()> {
1567 let Some(signed_version) = signed_version(trusted_comment) else {
1568 return if require_signed_version {
1572 Err(Error::MissingSignedVersion)
1573 } else {
1574 Ok(())
1575 };
1576 };
1577
1578 let matches = match (
1581 Version::from_str(signed_version.trim_start_matches('v')),
1582 Version::from_str(announced_version.trim_start_matches('v')),
1583 ) {
1584 (Ok(signed), Ok(announced)) => signed == announced,
1585 _ => signed_version == announced_version,
1586 };
1587
1588 if matches {
1589 Ok(())
1590 } else {
1591 Err(Error::SignedVersionMismatch {
1592 signed: signed_version.to_string(),
1593 announced: announced_version.to_string(),
1594 })
1595 }
1596}
1597
1598fn signed_version(trusted_comment: &str) -> Option<&str> {
1601 trusted_comment
1602 .split('\t')
1603 .find_map(|field| field.strip_prefix("version:"))
1604}
1605
1606fn base64_to_string(base64_string: &str) -> Result<String> {
1607 let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
1608 let result = std::str::from_utf8(decoded_string)
1609 .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
1610 .to_string();
1611 Ok(result)
1612}
1613
1614#[cfg(windows)]
1615fn encode_wide(string: impl AsRef<OsStr>) -> Vec<u16> {
1616 use std::os::windows::ffi::OsStrExt;
1617
1618 string
1619 .as_ref()
1620 .encode_wide()
1621 .chain(std::iter::once(0))
1622 .collect()
1623}
1624
1625#[cfg(windows)]
1626trait PathExt {
1627 fn wrap_in_quotes(&self) -> Self;
1628}
1629
1630#[cfg(windows)]
1631impl PathExt for PathBuf {
1632 fn wrap_in_quotes(&self) -> Self {
1633 let mut msi_path = OsString::from("\"");
1634 msi_path.push(self.as_os_str());
1635 msi_path.push("\"");
1636 PathBuf::from(msi_path)
1637 }
1638}
1639
1640#[cfg(windows)]
1642fn escape_nsis_current_exe_arg(arg: impl AsRef<OsStr>) -> OsString {
1643 use std::os::windows::ffi::{OsStrExt, OsStringExt};
1644
1645 let arg = arg.as_ref();
1646 let mut cmd: Vec<u16> = Vec::new();
1647
1648 let quote = arg
1650 .as_encoded_bytes()
1651 .iter()
1652 .any(|c| *c == b' ' || *c == b'\t' || *c == b'/')
1653 || arg.is_empty();
1654 let escape = true;
1655 if quote {
1656 cmd.push('"' as u16);
1657 }
1658 let mut backslashes: usize = 0;
1659 for x in arg.encode_wide() {
1660 if escape {
1661 if x == '\\' as u16 {
1662 backslashes += 1;
1663 } else {
1664 if x == '"' as u16 {
1665 cmd.extend((0..=backslashes).map(|_| '\\' as u16));
1667 }
1668 backslashes = 0;
1669 }
1670 }
1671 cmd.push(x);
1672 }
1673 if quote {
1674 cmd.extend((0..backslashes).map(|_| '\\' as u16));
1676 cmd.push('"' as u16);
1677 }
1678 OsString::from_wide(&cmd)
1679}
1680
1681#[cfg(windows)]
1682fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
1683 let mut arg = arg.as_ref().to_string_lossy().to_string();
1684
1685 if arg.is_empty() {
1687 return "\"\"\"\"".to_string();
1688 } else if !arg.contains(' ') && !arg.contains('"') {
1689 return arg;
1690 }
1691
1692 if arg.contains('"') {
1693 arg = arg.replace('"', r#""""""#);
1694 }
1695
1696 if arg.starts_with('-') {
1697 if let Some((a1, a2)) = arg.split_once('=') {
1698 format!("{a1}=\"\"{a2}\"\"")
1699 } else {
1700 format!("\"\"{arg}\"\"")
1701 }
1702 } else {
1703 format!("\"\"{arg}\"\"")
1704 }
1705}
1706
1707#[cfg(test)]
1708mod tests {
1709 use super::{signed_version, verify_signed_version};
1710 use crate::error::Error;
1711
1712 const CURRENT: &str = "timestamp:1700000000\tfile:app_1.2.3_x64.msi.zip\tversion:1.2.3";
1713 const LEGACY: &str = "timestamp:1600000000\tfile:app_1.0.0_x64.msi.zip";
1715
1716 #[test]
1717 fn reads_the_signed_version() {
1718 assert_eq!(signed_version(CURRENT), Some("1.2.3"));
1719 assert_eq!(signed_version(LEGACY), None);
1720 assert_eq!(signed_version("timestamp:1\tfile:app-version:2.zip"), None);
1722 }
1723
1724 #[test]
1725 fn accepts_a_matching_version() {
1726 assert!(verify_signed_version(CURRENT, "1.2.3", true).is_ok());
1727 assert!(verify_signed_version(CURRENT, "1.2.3", false).is_ok());
1728 assert!(verify_signed_version(CURRENT, "v1.2.3", true).is_ok());
1730 }
1731
1732 #[test]
1733 fn rejects_a_version_the_artifact_was_not_signed_for() {
1734 let err = verify_signed_version(CURRENT, "9.9.9", false).unwrap_err();
1736 assert!(
1737 matches!(err, Error::SignedVersionMismatch { ref signed, ref announced }
1738 if signed == "1.2.3" && announced == "9.9.9"),
1739 "unexpected error: {err}"
1740 );
1741 assert!(verify_signed_version(CURRENT, "9.9.9", true).is_err());
1744 assert!(verify_signed_version(CURRENT, "1.2.4", true).is_err());
1745 }
1746
1747 #[test]
1748 fn only_requires_a_signed_version_when_configured() {
1749 assert!(verify_signed_version(LEGACY, "9.9.9", false).is_ok());
1750 assert!(matches!(
1751 verify_signed_version(LEGACY, "9.9.9", true).unwrap_err(),
1752 Error::MissingSignedVersion
1753 ));
1754 }
1755
1756 #[test]
1757 fn compares_non_semver_versions_literally() {
1758 let comment = "timestamp:1700000000\tfile:app.zip\tversion:2024-01-01";
1759 assert!(verify_signed_version(comment, "2024-01-01", true).is_ok());
1760 assert!(verify_signed_version(comment, "2024-01-02", true).is_err());
1761 }
1762
1763 #[test]
1764 #[cfg(windows)]
1765 fn it_wraps_correctly() {
1766 use super::PathExt;
1767 use std::path::PathBuf;
1768
1769 assert_eq!(
1770 PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(),
1771 PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
1772 )
1773 }
1774
1775 #[test]
1776 #[cfg(windows)]
1777 fn it_escapes_correctly_for_msi() {
1778 use crate::updater::escape_msi_property_arg;
1779
1780 let cases = [
1789 "something",
1790 "--flag",
1791 "--empty=",
1792 "--arg=value",
1793 "some space", "--arg value", "--arg=unwrapped space", "--arg=\"wrapped\"", "--arg=\"wrapped space\"", "--arg=midword\"wrapped space\"", "", ];
1801 let cases_escaped = [
1802 "something",
1803 "--flag",
1804 "--empty=",
1805 "--arg=value",
1806 "\"\"some space\"\"",
1807 "\"\"--arg value\"\"",
1808 "--arg=\"\"unwrapped space\"\"",
1809 r#"--arg=""""""wrapped"""""""#,
1810 r#"--arg=""""""wrapped space"""""""#,
1811 r#"--arg=""midword""""wrapped space"""""""#,
1812 "\"\"\"\"",
1813 ];
1814
1815 assert_eq!(cases.len(), cases_escaped.len());
1817
1818 for (orig, escaped) in cases.iter().zip(cases_escaped) {
1819 assert_eq!(escape_msi_property_arg(orig), escaped);
1820 }
1821 }
1822
1823 #[test]
1824 #[cfg(windows)]
1825 fn it_escapes_correctly_for_nsis() {
1826 use crate::updater::escape_nsis_current_exe_arg;
1827 use std::ffi::OsStr;
1828
1829 let cases = [
1830 "something",
1831 "--flag",
1832 "--empty=",
1833 "--arg=value",
1834 "some space", "--arg value", "--arg=unwrapped space", "--arg=\"wrapped\"", "--arg=\"wrapped space\"", "--arg=midword\"wrapped space\"", "", ];
1842 let cases_escaped = [
1845 "something",
1846 "--flag",
1847 "--empty=",
1848 "--arg=value",
1849 "\"some space\"",
1850 "\"--arg value\"",
1851 "\"--arg=unwrapped space\"",
1852 "--arg=\\\"wrapped\\\"",
1853 "\"--arg=\\\"wrapped space\\\"\"",
1854 "\"--arg=midword\\\"wrapped space\\\"\"",
1855 "\"\"",
1856 ];
1857
1858 assert_eq!(cases.len(), cases_escaped.len());
1860
1861 for (orig, escaped) in cases.iter().zip(cases_escaped) {
1862 assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped);
1863 }
1864 }
1865}