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