Skip to main content

tauri_plugin_updater/
updater.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use 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    /// Download URL for the platform
74    pub url: Url,
75    /// Signature for the platform
76    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/// Information about a release returned by the remote update server.
89///
90/// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format.
91#[derive(Debug, Clone)]
92pub struct RemoteRelease {
93    /// Version to install.
94    pub version: Version,
95    /// Release notes.
96    pub notes: Option<String>,
97    /// Release date.
98    pub pub_date: Option<OffsetDateTime>,
99    /// Release data.
100    pub data: RemoteReleaseInner,
101}
102
103impl RemoteRelease {
104    /// The release's download URL for the given target.
105    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    /// The release's signature for the given target.
117    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    /// Clear all proxies. See [`reqwest::ClientBuilder::no_proxy`](https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.no_proxy).
248    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    /// Adds an argument to pass to the Windows installer.
259    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    /// Adds multiple arguments to pass to the Windows installer.
268    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    /// Removes all the additional arguments to pass to the Windows installer.
278    ///
279    /// Note: this only removes the additional arguments added through
280    /// [`Self::installer_arg`], [`crate::Builder::installer_arg`]
281    /// and the `plugins > updater > windows > installerArgs` config,
282    /// not the ones managed by us (e.g. `/UPDATER` flag passed to the NSIS installer)
283    pub fn clear_installer_args(mut self) -> Self {
284        self.installer_args.clear();
285        self
286    }
287
288    /// Function to run before we run the installer and exit the app through `std::process::exit(0)` on Windows
289    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    /// Allows you to modify the `reqwest` client builder before the HTTP request is sent.
295    ///
296    /// Note that `reqwest` crate may be updated in minor releases of tauri-plugin-updater.
297    /// Therefore it's recommended to pin the plugin to at least a minor version when you're using `configure_client`.
298    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        // Get the extract_path from the provided executable_path
320        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    // The `{{target}}` variable we replace in the endpoint and serach for in the JSON,
373    // this is either the user provided target or the current operating system by default
374    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        // we want JSON only
388        let mut headers = self.headers.clone();
389        if !headers.contains_key(ACCEPT) {
390            headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
391        }
392
393        // Set SSL certs for linux if they aren't available.
394        #[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            // replace {{current_version}}, {{target}}, {{arch}} and {{bundle_type}} in the provided URL
414            // this is useful if we need to query example
415            // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}}
416            // will be translated into ->
417            // https://releases.myapp.com/update/darwin/aarch64/1.0.0
418            // The main objective is if the update URL is defined via the Cargo.toml
419            // the URL will be generated dynamically
420            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                // url::Url automatically url-encodes the path components
432                .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                // but not query parameters
437                .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                // This can only fail if there is already a default provider which we checked for already.
448                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                        // no updates found!
485                        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                                // we found a release, break the loop
501                                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        // Last error is cleaned on success.
522        // Shouldn't be triggered if we had a successfull call
523        if let Some(error) = last_error {
524            return Err(error);
525        }
526
527        // Extracted remote metadata
528        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        // Use the user provided target
574        if let Some(target) = &self.target {
575            return Ok((release.download_url(target)?, release.signature(target)?));
576        }
577
578        // Or else we search for [`{os}-{arch}-{installer}`, `{os}-{arch}`] in order
579        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    /// Update description
609    pub body: Option<String>,
610    /// Version used to check for update
611    pub current_version: String,
612    /// Version announced
613    pub version: String,
614    /// Update publish date
615    pub date: Option<OffsetDateTime>,
616    /// The `{{target}}` variable we replace in the endpoint and search for in the JSON,
617    /// this is either the user provided target or the current operating system by default
618    pub target: String,
619    /// Download URL announced
620    pub download_url: Url,
621    /// Signature announced
622    pub signature: String,
623    /// The raw version of server's JSON response. Useful if the response contains additional fields that the updater doesn't handle.
624    pub raw_json: serde_json::Value,
625    /// Request timeout
626    pub timeout: Option<Duration>,
627    /// Request proxy
628    pub proxy: Option<Url>,
629    /// Disable system proxy
630    pub no_proxy: bool,
631    /// Request headers
632    pub headers: HeaderMap,
633    /// Extract path
634    #[allow(unused)]
635    extract_path: PathBuf,
636    /// App name, used for creating named tempfiles on Windows
637    #[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    /// Downloads the updater package, verifies it then return it as bytes.
650    ///
651    /// Use [`Update::install`] to install it
652    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        // set our headers
658        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    /// Installs the updater package downloaded by [`Update::download`]
718    pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
719        self.install_inner(bytes.as_ref())
720    }
721
722    /// Downloads and installs the updater package
723    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/// Windows
777#[cfg(windows)]
778impl Update {
779    /// ### Expected structure:
780    /// ├── [AppName]_[version]_x64.msi              # Application MSI
781    /// ├── [AppName]_[version]_x64-setup.exe        # NSIS installer
782    /// ├── [AppName]_[version]_x64.msi.zip          # ZIP generated by tauri-bundler
783    /// │   └──[AppName]_[version]_x64.msi           # Application MSI
784    /// ├── [AppName]_[version]_x64-setup.exe.zip          # ZIP generated by tauri-bundler
785    /// │   └──[AppName]_[version]_x64-setup.exe           # NSIS installer
786    /// └── ...
787    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/// Linux (AppImage and Deb)
953#[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    /// ### Expected structure:
962    /// ├── [AppName]_[version]_amd64.AppImage.tar.gz    # GZ generated by tauri-bundler
963    /// │   └──[AppName]_[version]_amd64.AppImage        # Application AppImage
964    /// ├── [AppName]_[version]_amd64.deb                # Debian package
965    /// └── ...
966    ///
967    fn install_inner(&self, bytes: &[u8]) -> Result<()> {
968        match installer_for_bundle_type(bundle_type()) {
969            Some(Installer::Deb) => self.install_deb(bytes),
970            Some(Installer::Rpm) => self.install_rpm(bytes),
971            _ => self.install_appimage(bytes),
972        }
973    }
974
975    fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
976        use std::os::unix::fs::{MetadataExt, PermissionsExt};
977        let extract_path_metadata = self.extract_path.metadata()?;
978
979        let tmp_dir_locations = vec![
980            Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
981            Box::new(dirs::cache_dir),
982            Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
983        ];
984
985        for tmp_dir_location in tmp_dir_locations {
986            if let Some(tmp_dir_location) = tmp_dir_location() {
987                let tmp_dir = tempfile::Builder::new()
988                    .prefix("tauri_current_app")
989                    .tempdir_in(tmp_dir_location)?;
990                let tmp_dir_metadata = tmp_dir.path().metadata()?;
991
992                if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
993                    let mut perms = tmp_dir_metadata.permissions();
994                    perms.set_mode(0o700);
995                    std::fs::set_permissions(tmp_dir.path(), perms)?;
996
997                    let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
998
999                    let permissions = std::fs::metadata(&self.extract_path)?.permissions();
1000
1001                    // create a backup of our current app image
1002                    std::fs::rename(&self.extract_path, tmp_app_image)?;
1003
1004                    #[cfg(feature = "zip")]
1005                    if infer::archive::is_gz(bytes) {
1006                        log::debug!("extracting AppImage");
1007                        // extract the buffer to the tmp_dir
1008                        // we extract our signed archive into our final directory without any temp file
1009                        let archive = Cursor::new(bytes);
1010                        let decoder = flate2::read::GzDecoder::new(archive);
1011                        let mut archive = tar::Archive::new(decoder);
1012                        for mut entry in archive.entries()?.flatten() {
1013                            if let Ok(path) = entry.path() {
1014                                if path.extension() == Some(OsStr::new("AppImage")) {
1015                                    // if something went wrong during the extraction, we should restore previous app
1016                                    if let Err(err) = entry.unpack(&self.extract_path) {
1017                                        std::fs::rename(tmp_app_image, &self.extract_path)?;
1018                                        return Err(err.into());
1019                                    }
1020                                    // early finish we have everything we need here
1021                                    return Ok(());
1022                                }
1023                            }
1024                        }
1025                        // if we have not returned early we should restore the backup
1026                        std::fs::rename(tmp_app_image, &self.extract_path)?;
1027                        return Err(Error::BinaryNotFoundInArchive);
1028                    }
1029
1030                    log::debug!("rewriting AppImage");
1031                    return match std::fs::write(&self.extract_path, bytes)
1032                        .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
1033                    {
1034                        Err(err) => {
1035                            // if something went wrong during the extraction, we should restore previous app
1036                            std::fs::rename(tmp_app_image, &self.extract_path)?;
1037                            Err(err.into())
1038                        }
1039                        Ok(_) => Ok(()),
1040                    };
1041                }
1042            }
1043        }
1044
1045        Err(Error::TempDirNotOnSameMountPoint)
1046    }
1047
1048    fn install_deb(&self, bytes: &[u8]) -> Result<()> {
1049        // First verify the bytes are actually a .deb package
1050        if !infer::archive::is_deb(bytes) {
1051            log::warn!("update is not a valid deb package");
1052            return Err(Error::InvalidUpdaterFormat);
1053        }
1054
1055        self.try_tmp_locations(bytes, "dpkg", "-i")
1056    }
1057
1058    fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
1059        // First verify the bytes are actually a .rpm package
1060        if !infer::archive::is_rpm(bytes) {
1061            return Err(Error::InvalidUpdaterFormat);
1062        }
1063        self.try_tmp_locations(bytes, "rpm", "-U")
1064    }
1065
1066    fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> {
1067        // Try different temp directories
1068        let tmp_dir_locations = vec![
1069            Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1070            Box::new(dirs::cache_dir),
1071            Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1072        ];
1073
1074        // Try writing to multiple temp locations until one succeeds
1075        for tmp_dir_location in tmp_dir_locations {
1076            if let Some(path) = tmp_dir_location() {
1077                if let Ok(tmp_dir) = tempfile::Builder::new()
1078                    .prefix("tauri_rpm_update")
1079                    .tempdir_in(path)
1080                {
1081                    let pkg_path = tmp_dir.path().join("package.rpm");
1082
1083                    // Try writing the .deb file
1084                    if std::fs::write(&pkg_path, bytes).is_ok() {
1085                        // If write succeeds, proceed with installation
1086                        return self.try_install_with_privileges(
1087                            &pkg_path,
1088                            install_cmd,
1089                            install_arg,
1090                        );
1091                    }
1092                    // If write fails, continue to next temp location
1093                }
1094            }
1095        }
1096
1097        // If we get here, all temp locations failed
1098        Err(Error::TempDirNotFound)
1099    }
1100
1101    fn try_install_with_privileges(
1102        &self,
1103        pkg_path: &Path,
1104        install_cmd: &str,
1105        install_arg: &str,
1106    ) -> Result<()> {
1107        // 1. First try using pkexec (graphical sudo prompt)
1108        if let Ok(status) = std::process::Command::new("pkexec")
1109            .arg(install_cmd)
1110            .arg(install_arg)
1111            .arg(pkg_path)
1112            .status()
1113        {
1114            if status.success() {
1115                log::debug!("installed deb with pkexec");
1116                return Ok(());
1117            }
1118        }
1119
1120        // 2. Try zenity or kdialog for a graphical sudo experience
1121        if let Ok(password) = self.get_password_graphically() {
1122            if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
1123                log::debug!("installed deb with GUI sudo");
1124                return Ok(());
1125            }
1126        }
1127
1128        // 3. Final fallback: terminal sudo
1129        let status = std::process::Command::new("sudo")
1130            .arg(install_cmd)
1131            .arg(install_arg)
1132            .arg(pkg_path)
1133            .status()?;
1134
1135        if status.success() {
1136            log::debug!("installed deb with sudo");
1137            Ok(())
1138        } else {
1139            Err(Error::PackageInstallFailed)
1140        }
1141    }
1142
1143    fn get_password_graphically(&self) -> Result<String> {
1144        // Try zenity first
1145        let zenity_result = std::process::Command::new("zenity")
1146            .args([
1147                "--password",
1148                "--title=Authentication Required",
1149                "--text=Enter your password to install the update:",
1150            ])
1151            .output();
1152
1153        if let Ok(output) = zenity_result {
1154            if output.status.success() {
1155                return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1156            }
1157        }
1158
1159        // Fall back to kdialog if zenity fails or isn't available
1160        let kdialog_result = std::process::Command::new("kdialog")
1161            .args(["--password", "Enter your password to install the update:"])
1162            .output();
1163
1164        if let Ok(output) = kdialog_result {
1165            if output.status.success() {
1166                return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1167            }
1168        }
1169
1170        Err(Error::AuthenticationFailed)
1171    }
1172
1173    fn install_with_sudo(
1174        &self,
1175        pkg_path: &Path,
1176        password: &str,
1177        install_cmd: &str,
1178        install_arg: &str,
1179    ) -> Result<bool> {
1180        use std::io::Write;
1181        use std::process::{Command, Stdio};
1182
1183        let mut child = Command::new("sudo")
1184            .arg("-S") // read password from stdin
1185            .arg(install_cmd)
1186            .arg(install_arg)
1187            .arg(pkg_path)
1188            .stdin(Stdio::piped())
1189            .stdout(Stdio::piped())
1190            .stderr(Stdio::piped())
1191            .spawn()?;
1192
1193        if let Some(mut stdin) = child.stdin.take() {
1194            // Write password to stdin
1195            writeln!(stdin, "{password}")?;
1196        }
1197
1198        let status = child.wait()?;
1199        Ok(status.success())
1200    }
1201}
1202
1203/// MacOS
1204#[cfg(target_os = "macos")]
1205impl Update {
1206    /// ### Expected structure:
1207    /// ├── [AppName]_[version]_x64.app.tar.gz       # GZ generated by tauri-bundler
1208    /// │   └──[AppName].app                         # Main application
1209    /// │      └── Contents                          # Application contents...
1210    /// │          └── ...
1211    /// └── ...
1212    fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1213        use flate2::read::GzDecoder;
1214
1215        let cursor = Cursor::new(bytes);
1216        let mut extracted_files: Vec<PathBuf> = Vec::new();
1217
1218        // Create temp directories for backup and extraction
1219        let tmp_backup_dir = tempfile::Builder::new()
1220            .prefix("tauri_current_app")
1221            .tempdir()?;
1222
1223        let tmp_extract_dir = tempfile::Builder::new()
1224            .prefix("tauri_updated_app")
1225            .tempdir()?;
1226
1227        let decoder = GzDecoder::new(cursor);
1228        let mut archive = tar::Archive::new(decoder);
1229
1230        // Extract files to temporary directory
1231        for entry in archive.entries()? {
1232            let mut entry = entry?;
1233            let collected_path: PathBuf = entry.path()?.iter().skip(1).collect();
1234            let extraction_path = tmp_extract_dir.path().join(&collected_path);
1235
1236            // Ensure parent directories exist
1237            if let Some(parent) = extraction_path.parent() {
1238                std::fs::create_dir_all(parent)?;
1239            }
1240
1241            if let Err(err) = entry.unpack(&extraction_path) {
1242                // Cleanup on error
1243                std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1244                return Err(err.into());
1245            }
1246            extracted_files.push(extraction_path);
1247        }
1248
1249        // Try to move the current app to backup
1250        let move_result = std::fs::rename(
1251            &self.extract_path,
1252            tmp_backup_dir.path().join("current_app"),
1253        );
1254        let need_authorization = if let Err(err) = move_result {
1255            if err.kind() == std::io::ErrorKind::PermissionDenied {
1256                true
1257            } else {
1258                std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1259                return Err(err.into());
1260            }
1261        } else {
1262            false
1263        };
1264
1265        if need_authorization {
1266            log::debug!("app installation needs admin privileges");
1267            // Use AppleScript to perform moves with admin privileges
1268            let apple_script = format!(
1269                "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges",
1270                src = self.extract_path.display(),
1271                new = tmp_extract_dir.path().display()
1272            );
1273
1274            let (tx, rx) = std::sync::mpsc::channel();
1275            let res = (self.run_on_main_thread)(Box::new(move || {
1276                let mut script =
1277                    osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script);
1278                script.compile().expect("invalid AppleScript");
1279                let r = script.execute();
1280                tx.send(r).unwrap();
1281            }));
1282            let result = rx.recv().unwrap();
1283
1284            if res.is_err() || result.is_err() {
1285                std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1286                return Err(Error::Io(std::io::Error::new(
1287                    std::io::ErrorKind::PermissionDenied,
1288                    "Failed to move the new app into place",
1289                )));
1290            }
1291        } else {
1292            // Remove existing directory if it exists
1293            if self.extract_path.exists() {
1294                std::fs::remove_dir_all(&self.extract_path)?;
1295            }
1296            // Move the new app to the target path
1297            std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?;
1298        }
1299
1300        let _ = std::process::Command::new("touch")
1301            .arg(&self.extract_path)
1302            .status();
1303
1304        Ok(())
1305    }
1306}
1307
1308/// Gets the base target string used by the updater. If bundle type is available it
1309/// will be added to this string when selecting the download URL and signature.
1310/// `tauri::utils::platform::bundle_type` method is used to obtain current bundle type.
1311pub fn target() -> Option<String> {
1312    if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
1313        Some(format!("{target}-{arch}"))
1314    } else {
1315        None
1316    }
1317}
1318
1319fn updater_os() -> Option<&'static str> {
1320    if cfg!(target_os = "linux") {
1321        Some("linux")
1322    } else if cfg!(target_os = "macos") {
1323        // TODO shouldn't this be macos instead?
1324        Some("darwin")
1325    } else if cfg!(target_os = "windows") {
1326        Some("windows")
1327    } else {
1328        None
1329    }
1330}
1331
1332fn updater_arch() -> Option<&'static str> {
1333    if cfg!(target_arch = "x86") {
1334        Some("i686")
1335    } else if cfg!(target_arch = "x86_64") {
1336        Some("x86_64")
1337    } else if cfg!(target_arch = "arm") {
1338        Some("armv7")
1339    } else if cfg!(target_arch = "aarch64") {
1340        Some("aarch64")
1341    } else if cfg!(target_arch = "riscv64") {
1342        Some("riscv64")
1343    } else {
1344        None
1345    }
1346}
1347
1348pub fn extract_path_from_executable(executable_path: &Path) -> Result<PathBuf> {
1349    // Return the path of the current executable by default
1350    // Example C:\Program Files\My App\
1351    let extract_path = executable_path
1352        .parent()
1353        .map(PathBuf::from)
1354        .ok_or(Error::FailedToDetermineExtractPath)?;
1355
1356    // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
1357    // We need to get /Applications/<app>.app
1358    // TODO(lemarier): Need a better way here
1359    // Maybe we could search for <*.app> to get the right path
1360    #[cfg(target_os = "macos")]
1361    if extract_path
1362        .display()
1363        .to_string()
1364        .contains("Contents/MacOS")
1365    {
1366        return extract_path
1367            .parent()
1368            .map(PathBuf::from)
1369            .ok_or(Error::FailedToDetermineExtractPath)?
1370            .parent()
1371            .map(PathBuf::from)
1372            .ok_or(Error::FailedToDetermineExtractPath);
1373    }
1374
1375    Ok(extract_path)
1376}
1377
1378impl<'de> Deserialize<'de> for RemoteRelease {
1379    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1380    where
1381        D: Deserializer<'de>,
1382    {
1383        #[derive(Deserialize)]
1384        struct InnerRemoteRelease {
1385            #[serde(alias = "name", deserialize_with = "parse_version")]
1386            version: Version,
1387            notes: Option<String>,
1388            pub_date: Option<String>,
1389            platforms: Option<HashMap<String, ReleaseManifestPlatform>>,
1390            // dynamic platform response
1391            url: Option<Url>,
1392            signature: Option<String>,
1393        }
1394
1395        let release = InnerRemoteRelease::deserialize(deserializer)?;
1396
1397        let pub_date = if let Some(date) = release.pub_date {
1398            Some(
1399                OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
1400                    .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
1401            )
1402        } else {
1403            None
1404        };
1405
1406        Ok(RemoteRelease {
1407            version: release.version,
1408            notes: release.notes,
1409            pub_date,
1410            data: if let Some(platforms) = release.platforms {
1411                RemoteReleaseInner::Static { platforms }
1412            } else {
1413                RemoteReleaseInner::Dynamic(ReleaseManifestPlatform {
1414                    url: release.url.ok_or_else(|| {
1415                        DeError::custom("the `url` field was not set on the updater response")
1416                    })?,
1417                    signature: release.signature.ok_or_else(|| {
1418                        DeError::custom("the `signature` field was not set on the updater response")
1419                    })?,
1420                })
1421            },
1422        })
1423    }
1424}
1425
1426fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
1427    match bundle? {
1428        BundleType::Deb => Some(Installer::Deb),
1429        BundleType::Rpm => Some(Installer::Rpm),
1430        BundleType::AppImage => Some(Installer::AppImage),
1431        BundleType::Msi => Some(Installer::Msi),
1432        BundleType::Nsis => Some(Installer::Nsis),
1433        BundleType::App => Some(Installer::App), // App is also returned for Dmg type
1434        _ => None,
1435    }
1436}
1437
1438fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
1439where
1440    D: serde::Deserializer<'de>,
1441{
1442    let str = String::deserialize(deserializer)?;
1443
1444    Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom)
1445}
1446
1447// Validate signature
1448fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
1449    // we need to convert the pub key
1450    let pub_key_decoded = base64_to_string(pub_key)?;
1451    let public_key = PublicKey::decode(&pub_key_decoded)?;
1452    let signature_base64_decoded = base64_to_string(release_signature)?;
1453    let signature = Signature::decode(&signature_base64_decoded)?;
1454
1455    // Validate signature or bail out
1456    public_key.verify(data, &signature, true)?;
1457    Ok(true)
1458}
1459
1460fn base64_to_string(base64_string: &str) -> Result<String> {
1461    let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
1462    let result = std::str::from_utf8(decoded_string)
1463        .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
1464        .to_string();
1465    Ok(result)
1466}
1467
1468#[cfg(windows)]
1469fn encode_wide(string: impl AsRef<OsStr>) -> Vec<u16> {
1470    use std::os::windows::ffi::OsStrExt;
1471
1472    string
1473        .as_ref()
1474        .encode_wide()
1475        .chain(std::iter::once(0))
1476        .collect()
1477}
1478
1479#[cfg(windows)]
1480trait PathExt {
1481    fn wrap_in_quotes(&self) -> Self;
1482}
1483
1484#[cfg(windows)]
1485impl PathExt for PathBuf {
1486    fn wrap_in_quotes(&self) -> Self {
1487        let mut msi_path = OsString::from("\"");
1488        msi_path.push(self.as_os_str());
1489        msi_path.push("\"");
1490        PathBuf::from(msi_path)
1491    }
1492}
1493
1494// adapted from https://github.com/rust-lang/rust/blob/1c047506f94cd2d05228eb992b0a6bbed1942349/library/std/src/sys/args/windows.rs#L174
1495#[cfg(windows)]
1496fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String {
1497    let arg = arg.to_string_lossy();
1498    let mut cmd: Vec<char> = Vec::new();
1499
1500    // compared to std we additionally escape `/` so that nsis won't interpret them as a beginning of an nsis argument.
1501    let quote = arg.chars().any(|c| c == ' ' || c == '\t' || c == '/') || arg.is_empty();
1502    let escape = true;
1503    if quote {
1504        cmd.push('"');
1505    }
1506    let mut backslashes: usize = 0;
1507    for x in arg.chars() {
1508        if escape {
1509            if x == '\\' {
1510                backslashes += 1;
1511            } else {
1512                if x == '"' {
1513                    // Add n+1 backslashes to total 2n+1 before internal '"'.
1514                    cmd.extend((0..=backslashes).map(|_| '\\'));
1515                }
1516                backslashes = 0;
1517            }
1518        }
1519        cmd.push(x);
1520    }
1521    if quote {
1522        // Add n backslashes to total 2n before ending '"'.
1523        cmd.extend((0..backslashes).map(|_| '\\'));
1524        cmd.push('"');
1525    }
1526    cmd.into_iter().collect()
1527}
1528
1529#[cfg(windows)]
1530fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
1531    let mut arg = arg.as_ref().to_string_lossy().to_string();
1532
1533    // Otherwise this argument will get lost in ShellExecute
1534    if arg.is_empty() {
1535        return "\"\"\"\"".to_string();
1536    } else if !arg.contains(' ') && !arg.contains('"') {
1537        return arg;
1538    }
1539
1540    if arg.contains('"') {
1541        arg = arg.replace('"', r#""""""#);
1542    }
1543
1544    if arg.starts_with('-') {
1545        if let Some((a1, a2)) = arg.split_once('=') {
1546            format!("{a1}=\"\"{a2}\"\"")
1547        } else {
1548            format!("\"\"{arg}\"\"")
1549        }
1550    } else {
1551        format!("\"\"{arg}\"\"")
1552    }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557
1558    #[test]
1559    #[cfg(windows)]
1560    fn it_wraps_correctly() {
1561        use super::PathExt;
1562        use std::path::PathBuf;
1563
1564        assert_eq!(
1565            PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(),
1566            PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
1567        )
1568    }
1569
1570    #[test]
1571    #[cfg(windows)]
1572    fn it_escapes_correctly_for_msi() {
1573        use crate::updater::escape_msi_property_arg;
1574
1575        // Explanation for quotes:
1576        // The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level.
1577        // To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later.
1578        // This means that the escaped strings can't ever have a single quotation mark!
1579        // Now there are 3 major things to look out for to not break the msiexec call:
1580        //   1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument.
1581        //   2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored.
1582        //   3) Escape emtpy args in quotation marks, otherwise the argument will get lost.
1583        let cases = [
1584            "something",
1585            "--flag",
1586            "--empty=",
1587            "--arg=value",
1588            "some space",                     // This simulates `./my-app "some string"`.
1589            "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
1590            "--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
1591            "--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
1592            "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
1593            "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
1594            "",            // `./my-app '""'`
1595        ];
1596        let cases_escaped = [
1597            "something",
1598            "--flag",
1599            "--empty=",
1600            "--arg=value",
1601            "\"\"some space\"\"",
1602            "\"\"--arg value\"\"",
1603            "--arg=\"\"unwrapped space\"\"",
1604            r#"--arg=""""""wrapped"""""""#,
1605            r#"--arg=""""""wrapped space"""""""#,
1606            r#"--arg=""midword""""wrapped space"""""""#,
1607            "\"\"\"\"",
1608        ];
1609
1610        // Just to be sure we didn't mess that up
1611        assert_eq!(cases.len(), cases_escaped.len());
1612
1613        for (orig, escaped) in cases.iter().zip(cases_escaped) {
1614            assert_eq!(escape_msi_property_arg(orig), escaped);
1615        }
1616    }
1617
1618    #[test]
1619    #[cfg(windows)]
1620    fn it_escapes_correctly_for_nsis() {
1621        use crate::updater::escape_nsis_current_exe_arg;
1622        use std::ffi::OsStr;
1623
1624        let cases = [
1625            "something",
1626            "--flag",
1627            "--empty=",
1628            "--arg=value",
1629            "some space",                     // This simulates `./my-app "some string"`.
1630            "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
1631            "--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
1632            "--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
1633            "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
1634            "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
1635            "",            // `./my-app '""'`
1636        ];
1637        // Note: These may not be the results we actually want (monitor this!).
1638        // We only make sure the implementation doesn't unintentionally change.
1639        let cases_escaped = [
1640            "something",
1641            "--flag",
1642            "--empty=",
1643            "--arg=value",
1644            "\"some space\"",
1645            "\"--arg value\"",
1646            "\"--arg=unwrapped space\"",
1647            "--arg=\\\"wrapped\\\"",
1648            "\"--arg=\\\"wrapped space\\\"\"",
1649            "\"--arg=midword\\\"wrapped space\\\"\"",
1650            "\"\"",
1651        ];
1652
1653        // Just to be sure we didn't mess that up
1654        assert_eq!(cases.len(), cases_escaped.len());
1655
1656        for (orig, escaped) in cases.iter().zip(cases_escaped) {
1657            assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped);
1658        }
1659    }
1660}