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