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>;
132#[cfg(target_os = "macos")]
133type MainThreadClosure = Box<dyn FnOnce() + Send + Sync + 'static>;
134#[cfg(target_os = "macos")]
135type RunOnMainThread = Arc<dyn Fn(MainThreadClosure) -> tauri::Result<()> + Send + Sync + 'static>;
136
137// TODO: Move more fields to this in v3 if we can mark those fields non `pub`
138/// Updater context shared between [`UpdaterBuilder`], [`Updater`] and [`Update`]
139#[derive(Clone)]
140struct UpdaterContext {
141    config: Config,
142    configure_client: Option<OnBeforeRequest>,
143    #[cfg(target_os = "macos")]
144    run_on_main_thread: RunOnMainThread,
145    /// App name, used for creating named tempfiles
146    #[cfg(windows)]
147    app_name: String,
148    #[cfg(windows)]
149    installer_args: Vec<OsString>,
150    #[cfg(windows)]
151    current_exe_args: Vec<OsString>,
152    #[cfg(windows)]
153    on_before_exit: Option<OnBeforeExit>,
154    #[cfg(windows)]
155    restart_after_install: bool,
156}
157
158pub struct UpdaterBuilder {
159    current_version: Version,
160    pub(crate) version_comparator: Option<VersionComparator>,
161    executable_path: Option<PathBuf>,
162    target: Option<String>,
163    endpoints: Option<Vec<Url>>,
164    headers: HeaderMap,
165    timeout: Option<Duration>,
166    proxy: Option<Url>,
167    no_proxy: bool,
168    context: UpdaterContext,
169}
170
171impl UpdaterBuilder {
172    pub(crate) fn new<R: Runtime>(app: &AppHandle<R>, config: crate::Config) -> Self {
173        #[cfg(target_os = "macos")]
174        let run_on_main_thread = {
175            let app_ = app.clone();
176            Arc::new(move |f| app_.run_on_main_thread(f))
177        };
178        Self {
179            context: UpdaterContext {
180                #[cfg(windows)]
181                installer_args: config
182                    .windows
183                    .as_ref()
184                    .map(|w| w.installer_args.clone())
185                    .unwrap_or_default(),
186                config,
187                configure_client: None,
188                #[cfg(target_os = "macos")]
189                run_on_main_thread,
190                #[cfg(windows)]
191                app_name: app.package_info().name.clone(),
192                #[cfg(windows)]
193                current_exe_args: Vec::new(),
194                #[cfg(windows)]
195                on_before_exit: None,
196                #[cfg(windows)]
197                restart_after_install: true,
198            },
199            current_version: app.package_info().version.clone(),
200            version_comparator: None,
201            executable_path: None,
202            target: None,
203            endpoints: None,
204            headers: Default::default(),
205            timeout: None,
206            proxy: None,
207            no_proxy: false,
208        }
209    }
210
211    pub fn version_comparator<F: Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static>(
212        mut self,
213        f: F,
214    ) -> Self {
215        self.version_comparator = Some(Arc::new(f));
216        self
217    }
218
219    pub fn target(mut self, target: impl Into<String>) -> Self {
220        self.target.replace(target.into());
221        self
222    }
223
224    pub fn endpoints(mut self, endpoints: Vec<Url>) -> Result<Self> {
225        crate::config::validate_endpoints(
226            &endpoints,
227            self.context.config.dangerous_insecure_transport_protocol,
228        )?;
229
230        self.endpoints.replace(endpoints);
231        Ok(self)
232    }
233
234    pub fn executable_path<P: AsRef<Path>>(mut self, p: P) -> Self {
235        self.executable_path.replace(p.as_ref().into());
236        self
237    }
238
239    pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
240    where
241        HeaderName: TryFrom<K>,
242        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
243        HeaderValue: TryFrom<V>,
244        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
245    {
246        let key: std::result::Result<HeaderName, http::Error> = key.try_into().map_err(Into::into);
247        let value: std::result::Result<HeaderValue, http::Error> =
248            value.try_into().map_err(Into::into);
249        self.headers.insert(key?, value?);
250
251        Ok(self)
252    }
253
254    pub fn headers(mut self, headers: HeaderMap) -> Self {
255        self.headers = headers;
256        self
257    }
258
259    pub fn clear_headers(mut self) -> Self {
260        self.headers.clear();
261        self
262    }
263
264    pub fn timeout(mut self, timeout: Duration) -> Self {
265        self.timeout = Some(timeout);
266        self
267    }
268
269    pub fn proxy(mut self, proxy: Url) -> Self {
270        self.proxy.replace(proxy);
271        self
272    }
273
274    /// Clear all proxies. See [`reqwest::ClientBuilder::no_proxy`](https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.no_proxy).
275    pub fn no_proxy(mut self) -> Self {
276        self.no_proxy = true;
277        self
278    }
279
280    pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
281        self.context.config.pubkey = pubkey.into();
282        self
283    }
284
285    /// Adds an argument to pass to the Windows installer.
286    ///
287    /// Note: this applies to both WiX and NSIS installers
288    #[cfg_attr(not(windows), allow(unused))]
289    pub fn installer_arg<S>(mut self, arg: S) -> Self
290    where
291        S: Into<OsString>,
292    {
293        #[cfg(windows)]
294        {
295            self.context.installer_args.push(arg.into());
296        }
297        self
298    }
299
300    /// Adds multiple arguments to pass to the Windows installer.
301    ///
302    /// Note: this applies to both WiX and NSIS installers
303    #[cfg_attr(not(windows), allow(unused))]
304    pub fn installer_args<I, S>(mut self, args: I) -> Self
305    where
306        I: IntoIterator<Item = S>,
307        S: Into<OsString>,
308    {
309        #[cfg(windows)]
310        {
311            self.context
312                .installer_args
313                .extend(args.into_iter().map(Into::into));
314        }
315        self
316    }
317
318    /// Removes all the additional arguments to pass to the Windows installer.
319    ///
320    /// Note: this only removes the additional arguments added through
321    /// [`Self::installer_arg`], [`crate::Builder::installer_arg`]
322    /// and the `plugins > updater > windows > installerArgs` config,
323    /// not the ones managed by us (e.g. `/UPDATER` flag passed to the NSIS installer)
324    #[cfg_attr(not(windows), allow(unused))]
325    pub fn clear_installer_args(mut self) -> Self {
326        #[cfg(windows)]
327        {
328            self.context.installer_args.clear();
329        }
330        self
331    }
332
333    /// Function to run before we run the installer and exit the app through `std::process::exit(0)` on Windows
334    #[cfg_attr(not(windows), allow(unused))]
335    pub fn on_before_exit<F: Fn() + Send + Sync + 'static>(mut self, f: F) -> Self {
336        #[cfg(windows)]
337        {
338            self.context.on_before_exit.replace(Arc::new(f));
339        }
340        self
341    }
342
343    /// If the Windows installer should restart the app after installed, default is `true`
344    #[cfg_attr(not(windows), allow(unused))]
345    pub fn restart_after_install(mut self, restart_after_install: bool) -> Self {
346        #[cfg(windows)]
347        {
348            self.context.restart_after_install = restart_after_install;
349        }
350        self
351    }
352
353    /// Allows you to modify the `reqwest` client builder before the HTTP request is sent.
354    ///
355    /// Note that `reqwest` crate may be updated in minor releases of tauri-plugin-updater.
356    /// Therefore it's recommended to pin the plugin to at least a minor version when you're using `configure_client`.
357    pub fn configure_client<F: Fn(ClientBuilder) -> ClientBuilder + Send + Sync + 'static>(
358        mut self,
359        f: F,
360    ) -> Self {
361        self.context.configure_client.replace(Arc::new(f));
362        self
363    }
364
365    pub fn build(self) -> Result<Updater> {
366        let endpoints = self
367            .endpoints
368            .unwrap_or_else(|| self.context.config.endpoints.clone());
369
370        if endpoints.is_empty() {
371            return Err(Error::EmptyEndpoints);
372        };
373
374        let arch = updater_arch().ok_or(Error::UnsupportedArch)?;
375
376        let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
377
378        // Get the extract_path from the provided executable_path
379        let extract_path = if cfg!(target_os = "linux") {
380            executable_path
381        } else {
382            extract_path_from_executable(&executable_path)?
383        };
384
385        Ok(Updater {
386            current_version: self.current_version,
387            version_comparator: self.version_comparator,
388            timeout: self.timeout,
389            proxy: self.proxy,
390            no_proxy: self.no_proxy,
391            endpoints,
392            arch,
393            target: self.target,
394            headers: self.headers,
395            extract_path,
396            context: self.context.clone(),
397        })
398    }
399}
400
401#[cfg(windows)]
402impl UpdaterBuilder {
403    pub(crate) fn current_exe_args<I, S>(mut self, args: I) -> Self
404    where
405        I: IntoIterator<Item = S>,
406        S: Into<OsString>,
407    {
408        self.context
409            .current_exe_args
410            .extend(args.into_iter().map(Into::into));
411        self
412    }
413}
414
415pub struct Updater {
416    current_version: Version,
417    version_comparator: Option<VersionComparator>,
418    timeout: Option<Duration>,
419    proxy: Option<Url>,
420    no_proxy: bool,
421    endpoints: Vec<Url>,
422    arch: &'static str,
423    // The `{{target}}` variable we replace in the endpoint and serach for in the JSON,
424    // this is either the user provided target or the current operating system by default
425    target: Option<String>,
426    headers: HeaderMap,
427    extract_path: PathBuf,
428    context: UpdaterContext,
429}
430
431impl Updater {
432    pub async fn check(&self) -> Result<Option<Update>> {
433        // we want JSON only
434        let mut headers = self.headers.clone();
435        if !headers.contains_key(ACCEPT) {
436            headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
437        }
438
439        // Set SSL certs for linux if they aren't available.
440        #[cfg(target_os = "linux")]
441        {
442            if std::env::var_os("SSL_CERT_FILE").is_none() {
443                std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
444            }
445            if std::env::var_os("SSL_CERT_DIR").is_none() {
446                std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
447            }
448        }
449        let target = if let Some(target) = &self.target {
450            target
451        } else {
452            updater_os().ok_or(Error::UnsupportedOs)?
453        };
454
455        let mut remote_release: Option<RemoteRelease> = None;
456        let mut raw_json: Option<serde_json::Value> = None;
457        let mut last_error: Option<Error> = None;
458        for url in &self.endpoints {
459            // replace {{current_version}}, {{target}}, {{arch}} and {{bundle_type}} in the provided URL
460            // this is useful if we need to query example
461            // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}}
462            // will be translated into ->
463            // https://releases.myapp.com/update/darwin/aarch64/1.0.0
464            // The main objective is if the update URL is defined via the Cargo.toml
465            // the URL will be generated dynamically
466            let version = self.current_version.to_string();
467            let version = version.as_bytes();
468            const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+');
469            let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD);
470            let encoded_version = encoded_version.to_string();
471            let installer = installer_for_bundle_type(bundle_type())
472                .map(|i| i.name())
473                .unwrap_or("unknown");
474
475            let url: Url = url
476                .to_string()
477                // url::Url automatically url-encodes the path components
478                .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version)
479                .replace("%7B%7Btarget%7D%7D", target)
480                .replace("%7B%7Barch%7D%7D", self.arch)
481                .replace("%7B%7Bbundle_type%7D%7D", installer)
482                // but not query parameters
483                .replace("{{current_version}}", &encoded_version)
484                .replace("{{target}}", target)
485                .replace("{{arch}}", self.arch)
486                .replace("{{bundle_type}}", installer)
487                .parse()?;
488
489            log::debug!("checking for updates {url}");
490
491            #[cfg(feature = "rustls-tls")]
492            if rustls::crypto::CryptoProvider::get_default().is_none() {
493                // This can only fail if there is already a default provider which we checked for already.
494                let _ = rustls::crypto::ring::default_provider().install_default();
495            }
496
497            let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
498            if self.context.config.dangerous_accept_invalid_certs {
499                request = request.danger_accept_invalid_certs(true);
500            }
501            if self.context.config.dangerous_accept_invalid_hostnames {
502                request = request.danger_accept_invalid_hostnames(true);
503            }
504            if let Some(timeout) = self.timeout {
505                request = request.timeout(timeout);
506            }
507            if self.no_proxy {
508                log::debug!("disabling proxy");
509                request = request.no_proxy();
510            } else if let Some(ref proxy) = self.proxy {
511                log::debug!("using proxy {proxy}");
512                let proxy = reqwest::Proxy::all(proxy.as_str())?;
513                request = request.proxy(proxy);
514            }
515
516            if let Some(ref configure_client) = self.context.configure_client {
517                request = configure_client(request);
518            }
519
520            let response = request
521                .build()?
522                .get(url)
523                .headers(headers.clone())
524                .send()
525                .await;
526
527            match response {
528                Ok(res) => {
529                    if res.status().is_success() {
530                        // no updates found!
531                        if StatusCode::NO_CONTENT == res.status() {
532                            log::debug!("update endpoint returned 204 No Content");
533                            return Ok(None);
534                        };
535
536                        let update_response: serde_json::Value = res.json().await?;
537                        log::debug!("update response: {update_response:?}");
538                        raw_json = Some(update_response.clone());
539                        match serde_json::from_value::<RemoteRelease>(update_response)
540                            .map_err(Into::into)
541                        {
542                            Ok(release) => {
543                                log::debug!("parsed release response {release:?}");
544                                last_error = None;
545                                remote_release = Some(release);
546                                // we found a release, break the loop
547                                break;
548                            }
549                            Err(err) => {
550                                log::error!("failed to deserialize update response: {err}");
551                                last_error = Some(err)
552                            }
553                        }
554                    } else {
555                        log::error!(
556                            "update endpoint did not respond with a successful status code"
557                        );
558                    }
559                }
560                Err(err) => {
561                    log::error!("failed to check for updates: {err}");
562                    last_error = Some(err.into())
563                }
564            }
565        }
566
567        // Last error is cleaned on success.
568        // Shouldn't be triggered if we had a successfull call
569        if let Some(error) = last_error {
570            return Err(error);
571        }
572
573        // Extracted remote metadata
574        let release = remote_release.ok_or(Error::ReleaseNotFound)?;
575
576        let should_update = match self.version_comparator.as_ref() {
577            Some(comparator) => comparator(self.current_version.clone(), release.clone()),
578            None => release.version > self.current_version,
579        };
580
581        let installer = installer_for_bundle_type(bundle_type());
582        let (download_url, signature) = self.get_urls(&release, &installer)?;
583
584        let update = if should_update {
585            Some(Update {
586                current_version: self.current_version.to_string(),
587                target: target.to_owned(),
588                extract_path: self.extract_path.clone(),
589                version: release.version.to_string(),
590                date: release.pub_date,
591                download_url: download_url.clone(),
592                signature: signature.to_owned(),
593                body: release.notes,
594                raw_json: raw_json.unwrap(),
595                timeout: None,
596                proxy: self.proxy.clone(),
597                no_proxy: self.no_proxy,
598                headers: self.headers.clone(),
599                context: self.context.clone(),
600            })
601        } else {
602            None
603        };
604
605        Ok(update)
606    }
607
608    fn get_urls<'a>(
609        &self,
610        release: &'a RemoteRelease,
611        installer: &Option<Installer>,
612    ) -> Result<(&'a Url, &'a String)> {
613        // Use the user provided target
614        if let Some(target) = &self.target {
615            return Ok((release.download_url(target)?, release.signature(target)?));
616        }
617
618        // Or else we search for [`{os}-{arch}-{installer}`, `{os}-{arch}`] in order
619        let os = updater_os().ok_or(Error::UnsupportedOs)?;
620        let arch = self.arch;
621        let mut targets = Vec::new();
622        if let Some(installer) = installer {
623            let installer = installer.name();
624            targets.push(format!("{os}-{arch}-{installer}"));
625        }
626        targets.push(format!("{os}-{arch}"));
627
628        for target in &targets {
629            log::debug!("Searching for updater target '{target}' in release data");
630            if let (Ok(download_url), Ok(signature)) =
631                (release.download_url(target), release.signature(target))
632            {
633                return Ok((download_url, signature));
634            };
635        }
636
637        Err(Error::TargetsNotFound(targets))
638    }
639}
640
641#[derive(Clone)]
642pub struct Update {
643    /// Update description
644    pub body: Option<String>,
645    /// Version used to check for update
646    pub current_version: String,
647    /// Version announced
648    pub version: String,
649    /// Update publish date
650    pub date: Option<OffsetDateTime>,
651    /// The `{{target}}` variable we replace in the endpoint and search for in the JSON,
652    /// this is either the user provided target or the current operating system by default
653    pub target: String,
654    /// Download URL announced
655    pub download_url: Url,
656    /// Signature announced
657    pub signature: String,
658    /// The raw version of server's JSON response. Useful if the response contains additional fields that the updater doesn't handle.
659    pub raw_json: serde_json::Value,
660    /// Request timeout
661    pub timeout: Option<Duration>,
662    /// Request proxy
663    pub proxy: Option<Url>,
664    /// Disable system proxy
665    pub no_proxy: bool,
666    /// Request headers
667    pub headers: HeaderMap,
668    /// Extract path
669    #[allow(unused)]
670    extract_path: PathBuf,
671    context: UpdaterContext,
672}
673
674impl Resource for Update {}
675
676impl Update {
677    /// Downloads the updater package, verifies it then return it as bytes.
678    ///
679    /// Use [`Update::install`] to install it
680    pub async fn download<C: FnMut(usize, Option<u64>), D: FnOnce()>(
681        &self,
682        mut on_chunk: C,
683        on_download_finish: D,
684    ) -> Result<Vec<u8>> {
685        // set our headers
686        let mut headers = self.headers.clone();
687        if !headers.contains_key(ACCEPT) {
688            headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
689        }
690
691        let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
692        if self.context.config.dangerous_accept_invalid_certs {
693            request = request.danger_accept_invalid_certs(true);
694        }
695        if self.context.config.dangerous_accept_invalid_hostnames {
696            request = request.danger_accept_invalid_hostnames(true);
697        }
698        if let Some(timeout) = self.timeout {
699            request = request.timeout(timeout);
700        }
701        if self.no_proxy {
702            request = request.no_proxy();
703        } else if let Some(ref proxy) = self.proxy {
704            let proxy = reqwest::Proxy::all(proxy.as_str())?;
705            request = request.proxy(proxy);
706        }
707        if let Some(ref configure_client) = self.context.configure_client {
708            request = configure_client(request);
709        }
710        let response = request
711            .build()?
712            .get(self.download_url.clone())
713            .headers(headers)
714            .send()
715            .await?;
716
717        if !response.status().is_success() {
718            return Err(Error::Network(format!(
719                "Download request failed with status: {}",
720                response.status()
721            )));
722        }
723
724        let content_length: Option<u64> = response
725            .headers()
726            .get("Content-Length")
727            .and_then(|value| value.to_str().ok())
728            .and_then(|value| value.parse().ok());
729
730        let mut buffer = Vec::new();
731
732        let mut stream = response.bytes_stream();
733        while let Some(chunk) = stream.next().await {
734            let chunk = chunk?;
735            on_chunk(chunk.len(), content_length);
736            buffer.extend(chunk);
737        }
738        on_download_finish();
739
740        verify_signature(
741            &buffer,
742            &self.signature,
743            &self.context.config.pubkey,
744            &self.version,
745            self.context.config.require_signed_version,
746        )?;
747
748        Ok(buffer)
749    }
750
751    /// Installs the updater package downloaded by [`Update::download`]
752    ///
753    /// ## Platform-specific:
754    ///
755    /// - **Windows:** This function exits the app after launching the updater installer successfully
756    /// - **macOS / Linux:** You need to relaunch the app to run the newly install version
757    pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
758        self.install_inner(bytes.as_ref())
759    }
760
761    /// Downloads and installs the updater package
762    ///
763    /// ## Platform-specific:
764    ///
765    /// - **Windows:** This function exits the app after launching the updater installer successfully
766    /// - **macOS / Linux:** You need to relaunch the app to run the newly install version
767    pub async fn download_and_install<C: FnMut(usize, Option<u64>), D: FnOnce()>(
768        &self,
769        on_chunk: C,
770        on_download_finish: D,
771    ) -> Result<()> {
772        let bytes = self.download(on_chunk, on_download_finish).await?;
773        self.install(bytes)
774    }
775
776    #[cfg(mobile)]
777    fn install_inner(&self, _bytes: &[u8]) -> Result<()> {
778        Ok(())
779    }
780
781    /// Whether the Windows installer should restart the app after installed, default is `true`
782    #[cfg_attr(not(windows), allow(unused))]
783    pub fn restart_after_install(mut self, restart_after_install: bool) -> Self {
784        #[cfg(windows)]
785        {
786            self.context.restart_after_install = restart_after_install;
787        }
788        self
789    }
790}
791
792#[cfg(windows)]
793enum WindowsUpdaterType {
794    Nsis {
795        path: PathBuf,
796        #[allow(unused)]
797        temp: Option<tempfile::TempPath>,
798    },
799    Msi {
800        path: PathBuf,
801        #[allow(unused)]
802        temp: Option<tempfile::TempPath>,
803    },
804}
805
806#[cfg(windows)]
807impl WindowsUpdaterType {
808    fn nsis(path: PathBuf, temp: Option<tempfile::TempPath>) -> Self {
809        Self::Nsis { path, temp }
810    }
811
812    fn msi(path: PathBuf, temp: Option<tempfile::TempPath>) -> Self {
813        Self::Msi {
814            path: path.wrap_in_quotes(),
815            temp,
816        }
817    }
818}
819
820#[cfg(windows)]
821impl Config {
822    fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode {
823        self.windows
824            .as_ref()
825            .map(|w| w.install_mode.clone())
826            .unwrap_or_default()
827    }
828}
829
830/// Windows
831#[cfg(windows)]
832impl Update {
833    /// ### Expected structure:
834    /// ├── [AppName]_[version]_x64.msi              # Application MSI
835    /// ├── [AppName]_[version]_x64-setup.exe        # NSIS installer
836    /// ├── [AppName]_[version]_x64.msi.zip          # ZIP generated by tauri-bundler
837    /// │   └──[AppName]_[version]_x64.msi           # Application MSI
838    /// ├── [AppName]_[version]_x64-setup.exe.zip          # ZIP generated by tauri-bundler
839    /// │   └──[AppName]_[version]_x64-setup.exe           # NSIS installer
840    /// └── ...
841    fn install_inner(&self, bytes: &[u8]) -> Result<()> {
842        use windows_sys::{
843            w,
844            Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW},
845        };
846
847        let updater_type = self.extract(bytes)?;
848
849        if let Some(on_before_exit) = self.context.on_before_exit.as_ref() {
850            log::debug!("running on_before_exit hook");
851            on_before_exit();
852        }
853
854        let file = match &updater_type {
855            WindowsUpdaterType::Nsis { path, .. } => path.as_os_str().to_os_string(),
856            WindowsUpdaterType::Msi { .. } => std::env::var("SYSTEMROOT").as_ref().map_or_else(
857                |_| OsString::from("msiexec.exe"),
858                |p| OsString::from(format!("{p}\\System32\\msiexec.exe")),
859            ),
860        };
861        let parameters = self.updater_parameters(&updater_type);
862
863        log::debug!("Executing updater {file:?} with parameters: {parameters:?}");
864
865        let file = encode_wide(file);
866        let parameters = encode_wide(parameters);
867
868        let result = unsafe {
869            ShellExecuteW(
870                std::ptr::null_mut(),
871                w!("open"),
872                file.as_ptr(),
873                parameters.as_ptr(),
874                std::ptr::null(),
875                SW_SHOW,
876            )
877        };
878        if result as isize <= 32 {
879            return Err(crate::Error::Io(std::io::Error::last_os_error()));
880        }
881
882        std::process::exit(0);
883    }
884
885    fn updater_parameters(&self, updater_type: &WindowsUpdaterType) -> OsString {
886        let install_mode = self.context.config.install_mode();
887        let current_args = &self.context.current_exe_args[1..];
888
889        match updater_type {
890            WindowsUpdaterType::Nsis { .. } => {
891                let mut installer_args: Vec<&OsStr> = Vec::new();
892                installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new));
893                installer_args.push(OsStr::new("/UPDATE"));
894
895                let nsis_current_exe_arg;
896                if self.context.restart_after_install {
897                    nsis_current_exe_arg = current_args
898                        .iter()
899                        .map(escape_nsis_current_exe_arg)
900                        .collect::<Vec<_>>();
901
902                    installer_args.extend(
903                        install_mode
904                            .nsis_restart_after_install_args()
905                            .iter()
906                            .map(OsStr::new),
907                    );
908                    installer_args.push(OsStr::new("/ARGS"));
909                    installer_args.extend(nsis_current_exe_arg.iter().map(OsStr::new));
910                }
911
912                installer_args.extend(self.installer_args());
913
914                installer_args.join(OsStr::new(" "))
915            }
916            WindowsUpdaterType::Msi { path, .. } => {
917                let mut installer_args: Vec<&OsStr> = vec![OsStr::new("/i"), path.as_os_str()];
918                installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new));
919                installer_args.push(OsStr::new("/promptrestart"));
920                installer_args.extend(self.installer_args());
921
922                let msi_current_exe_arg;
923                if self.context.restart_after_install {
924                    msi_current_exe_arg = format!(
925                        "LAUNCHAPPARGS=\"{}\"",
926                        current_args
927                            .iter()
928                            .map(escape_msi_property_arg)
929                            .collect::<Vec<_>>()
930                            .join(" ")
931                    );
932
933                    installer_args.extend(
934                        install_mode
935                            .msi_restart_after_install_args()
936                            .iter()
937                            .map(OsStr::new),
938                    );
939                    installer_args.push(OsStr::new(&msi_current_exe_arg));
940                }
941
942                installer_args.join(OsStr::new(" "))
943            }
944        }
945    }
946
947    fn installer_args(
948        &self,
949    ) -> std::iter::Map<std::slice::Iter<'_, OsString>, fn(&OsString) -> &OsStr> {
950        self.context.installer_args.iter().map(OsStr::new)
951    }
952
953    fn extract(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
954        #[cfg(feature = "zip")]
955        if infer::archive::is_zip(bytes) {
956            return self.extract_zip(bytes);
957        }
958
959        self.extract_exe(bytes)
960    }
961
962    fn make_temp_dir(&self) -> Result<PathBuf> {
963        Ok(tempfile::Builder::new()
964            .prefix(&format!(
965                "{}-{}-updater-",
966                self.context.app_name, self.version
967            ))
968            .tempdir()?
969            .keep())
970    }
971
972    #[cfg(feature = "zip")]
973    fn extract_zip(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
974        let temp_dir = self.make_temp_dir()?;
975
976        let archive = Cursor::new(bytes);
977        let mut extractor = zip::ZipArchive::new(archive)?;
978        extractor.extract(&temp_dir)?;
979
980        let paths = std::fs::read_dir(&temp_dir)?;
981        for path in paths {
982            let path = path?.path();
983            let ext = path.extension();
984            if ext == Some(OsStr::new("exe")) {
985                return Ok(WindowsUpdaterType::nsis(path, None));
986            } else if ext == Some(OsStr::new("msi")) {
987                return Ok(WindowsUpdaterType::msi(path, None));
988            }
989        }
990
991        Err(crate::Error::BinaryNotFoundInArchive)
992    }
993
994    fn extract_exe(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
995        if infer::app::is_exe(bytes) {
996            let (path, temp) = self.write_to_temp(bytes, ".exe")?;
997            Ok(WindowsUpdaterType::nsis(path, temp))
998        } else if infer::archive::is_msi(bytes) {
999            let (path, temp) = self.write_to_temp(bytes, ".msi")?;
1000            Ok(WindowsUpdaterType::msi(path, temp))
1001        } else {
1002            Err(crate::Error::InvalidUpdaterFormat)
1003        }
1004    }
1005
1006    fn write_to_temp(
1007        &self,
1008        bytes: &[u8],
1009        ext: &str,
1010    ) -> Result<(PathBuf, Option<tempfile::TempPath>)> {
1011        use std::io::Write;
1012
1013        let temp_dir = self.make_temp_dir()?;
1014        let mut temp_file = tempfile::Builder::new()
1015            .prefix(&format!(
1016                "{}-{}-installer",
1017                self.context.app_name, self.version
1018            ))
1019            .suffix(ext)
1020            .rand_bytes(0)
1021            .tempfile_in(temp_dir)?;
1022        temp_file.write_all(bytes)?;
1023
1024        let temp = temp_file.into_temp_path();
1025        Ok((temp.to_path_buf(), Some(temp)))
1026    }
1027}
1028
1029/// Linux (AppImage, Deb, RPM)
1030#[cfg(any(
1031    target_os = "linux",
1032    target_os = "dragonfly",
1033    target_os = "freebsd",
1034    target_os = "netbsd",
1035    target_os = "openbsd"
1036))]
1037impl Update {
1038    /// ### Expected structure:
1039    /// ├── [AppName]_[version]_amd64.AppImage.tar.gz    # GZ generated by tauri-bundler
1040    /// │   └──[AppName]_[version]_amd64.AppImage        # Application AppImage
1041    /// ├── [AppName]_[version]_amd64.deb                # Debian package
1042    /// ├── [AppName]_[version]_amd64.rpm                # RPM package
1043    /// └── ...
1044    ///
1045    fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1046        match installer_for_bundle_type(bundle_type()) {
1047            Some(Installer::Deb) => self.install_deb(bytes),
1048            Some(Installer::Rpm) => self.install_rpm(bytes),
1049            _ => self.install_appimage(bytes),
1050        }
1051    }
1052
1053    fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
1054        use std::os::unix::fs::{MetadataExt, PermissionsExt};
1055        let extract_path_metadata = self.extract_path.metadata()?;
1056
1057        let tmp_dir_locations = vec![
1058            Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1059            Box::new(dirs::cache_dir),
1060            Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1061        ];
1062
1063        for tmp_dir_location in tmp_dir_locations {
1064            if let Some(tmp_dir_location) = tmp_dir_location() {
1065                let tmp_dir = tempfile::Builder::new()
1066                    .prefix("tauri_current_app")
1067                    .tempdir_in(tmp_dir_location)?;
1068                let tmp_dir_metadata = tmp_dir.path().metadata()?;
1069
1070                if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
1071                    let mut perms = tmp_dir_metadata.permissions();
1072                    perms.set_mode(0o700);
1073                    std::fs::set_permissions(tmp_dir.path(), perms)?;
1074
1075                    let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
1076
1077                    let permissions = std::fs::metadata(&self.extract_path)?.permissions();
1078
1079                    // create a backup of our current app image
1080                    std::fs::rename(&self.extract_path, tmp_app_image)?;
1081
1082                    #[cfg(feature = "zip")]
1083                    if infer::archive::is_gz(bytes) {
1084                        log::debug!("extracting AppImage");
1085                        // extract the buffer to the tmp_dir
1086                        // we extract our signed archive into our final directory without any temp file
1087                        let archive = Cursor::new(bytes);
1088                        let decoder = flate2::read::GzDecoder::new(archive);
1089                        let mut archive = tar::Archive::new(decoder);
1090                        for mut entry in archive.entries()?.flatten() {
1091                            if let Ok(path) = entry.path() {
1092                                if path.extension() == Some(OsStr::new("AppImage")) {
1093                                    // if something went wrong during the extraction, we should restore previous app
1094                                    if let Err(err) = entry.unpack(&self.extract_path) {
1095                                        std::fs::rename(tmp_app_image, &self.extract_path)?;
1096                                        return Err(err.into());
1097                                    }
1098                                    // early finish we have everything we need here
1099                                    return Ok(());
1100                                }
1101                            }
1102                        }
1103                        // if we have not returned early we should restore the backup
1104                        std::fs::rename(tmp_app_image, &self.extract_path)?;
1105                        return Err(Error::BinaryNotFoundInArchive);
1106                    }
1107
1108                    log::debug!("rewriting AppImage");
1109                    return match std::fs::write(&self.extract_path, bytes)
1110                        .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
1111                    {
1112                        Err(err) => {
1113                            // if something went wrong during the extraction, we should restore previous app
1114                            std::fs::rename(tmp_app_image, &self.extract_path)?;
1115                            Err(err.into())
1116                        }
1117                        Ok(_) => Ok(()),
1118                    };
1119                }
1120            }
1121        }
1122
1123        Err(Error::TempDirNotOnSameMountPoint)
1124    }
1125
1126    fn install_deb(&self, bytes: &[u8]) -> Result<()> {
1127        // First verify the bytes are actually a .deb package
1128        if !infer::archive::is_deb(bytes) {
1129            log::warn!("update is not a valid deb package");
1130            return Err(Error::InvalidUpdaterFormat);
1131        }
1132
1133        self.try_tmp_locations(bytes, "dpkg", "-i", "deb")
1134    }
1135
1136    fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
1137        // First verify the bytes are actually a .rpm package
1138        if !infer::archive::is_rpm(bytes) {
1139            return Err(Error::InvalidUpdaterFormat);
1140        }
1141        self.try_tmp_locations(bytes, "rpm", "-U", "rpm")
1142    }
1143
1144    fn try_tmp_locations(
1145        &self,
1146        bytes: &[u8],
1147        install_cmd: &str,
1148        install_arg: &str,
1149        package_extension: &str,
1150    ) -> Result<()> {
1151        // Try different temp directories
1152        let tmp_dir_locations = vec![
1153            Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
1154            Box::new(dirs::cache_dir),
1155            Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
1156        ];
1157
1158        // Try writing to multiple temp locations until one succeeds
1159        for tmp_dir_location in tmp_dir_locations {
1160            if let Some(path) = tmp_dir_location() {
1161                let prefix = format!("tauri_{package_extension}_update");
1162                if let Ok(tmp_dir) = tempfile::Builder::new().prefix(&prefix).tempdir_in(path) {
1163                    let pkg_path = tmp_dir.path().join(format!("package.{package_extension}"));
1164
1165                    // Try writing the .deb / .rpm file
1166                    if std::fs::write(&pkg_path, bytes).is_ok() {
1167                        // If write succeeds, proceed with installation
1168                        return self.try_install_with_privileges(
1169                            &pkg_path,
1170                            install_cmd,
1171                            install_arg,
1172                        );
1173                    }
1174                    // If write fails, continue to next temp location
1175                }
1176            }
1177        }
1178
1179        // If we get here, all temp locations failed
1180        Err(Error::TempDirNotFound)
1181    }
1182
1183    fn try_install_with_privileges(
1184        &self,
1185        pkg_path: &Path,
1186        install_cmd: &str,
1187        install_arg: &str,
1188    ) -> Result<()> {
1189        // 1. First try using pkexec (graphical sudo prompt)
1190        if let Ok(status) = std::process::Command::new("pkexec")
1191            .arg(install_cmd)
1192            .arg(install_arg)
1193            .arg(pkg_path)
1194            .status()
1195        {
1196            if status.success() {
1197                log::debug!("installed {pkg_path:?} with pkexec");
1198                return Ok(());
1199            }
1200        }
1201
1202        // 2. Try zenity or kdialog for a graphical sudo experience
1203        if let Ok(password) = self.get_password_graphically() {
1204            if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
1205                log::debug!("installed {pkg_path:?} with GUI sudo");
1206                return Ok(());
1207            }
1208        }
1209
1210        // 3. Final fallback: terminal sudo
1211        let status = std::process::Command::new("sudo")
1212            .arg(install_cmd)
1213            .arg(install_arg)
1214            .arg(pkg_path)
1215            .status()?;
1216
1217        if status.success() {
1218            log::debug!("installed {pkg_path:?} with sudo");
1219            Ok(())
1220        } else {
1221            Err(Error::PackageInstallFailed)
1222        }
1223    }
1224
1225    fn get_password_graphically(&self) -> Result<String> {
1226        // Try zenity first
1227        let zenity_result = std::process::Command::new("zenity")
1228            .args([
1229                "--password",
1230                "--title=Authentication Required",
1231                "--text=Enter your password to install the update:",
1232            ])
1233            .output();
1234
1235        if let Ok(output) = zenity_result {
1236            if output.status.success() {
1237                return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1238            }
1239        }
1240
1241        // Fall back to kdialog if zenity fails or isn't available
1242        let kdialog_result = std::process::Command::new("kdialog")
1243            .args(["--password", "Enter your password to install the update:"])
1244            .output();
1245
1246        if let Ok(output) = kdialog_result {
1247            if output.status.success() {
1248                return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
1249            }
1250        }
1251
1252        Err(Error::AuthenticationFailed)
1253    }
1254
1255    fn install_with_sudo(
1256        &self,
1257        pkg_path: &Path,
1258        password: &str,
1259        install_cmd: &str,
1260        install_arg: &str,
1261    ) -> Result<bool> {
1262        use std::io::Write;
1263        use std::process::{Command, Stdio};
1264
1265        let mut child = Command::new("sudo")
1266            .arg("-S") // read password from stdin
1267            .arg(install_cmd)
1268            .arg(install_arg)
1269            .arg(pkg_path)
1270            .stdin(Stdio::piped())
1271            .stdout(Stdio::piped())
1272            .stderr(Stdio::piped())
1273            .spawn()?;
1274
1275        if let Some(mut stdin) = child.stdin.take() {
1276            // Write password to stdin
1277            writeln!(stdin, "{password}")?;
1278        }
1279
1280        let status = child.wait()?;
1281        Ok(status.success())
1282    }
1283}
1284
1285/// MacOS
1286#[cfg(target_os = "macos")]
1287impl Update {
1288    /// ### Expected structure:
1289    /// ├── [AppName]_[version]_x64.app.tar.gz       # GZ generated by tauri-bundler
1290    /// │   └──[AppName].app                         # Main application
1291    /// │      └── Contents                          # Application contents...
1292    /// │          └── ...
1293    /// └── ...
1294    fn install_inner(&self, bytes: &[u8]) -> Result<()> {
1295        use flate2::read::GzDecoder;
1296
1297        let cursor = Cursor::new(bytes);
1298        let mut extracted_files: Vec<PathBuf> = Vec::new();
1299
1300        // Create temp directories for backup and extraction
1301        let tmp_backup_dir = tempfile::Builder::new()
1302            .prefix("tauri_current_app")
1303            .tempdir()?;
1304
1305        let tmp_extract_dir = tempfile::Builder::new()
1306            .prefix("tauri_updated_app")
1307            .tempdir()?;
1308
1309        let decoder = GzDecoder::new(cursor);
1310        let mut archive = tar::Archive::new(decoder);
1311
1312        // Extract files to temporary directory
1313        for entry in archive.entries()? {
1314            let mut entry = entry?;
1315            let collected_path: PathBuf = entry.path()?.iter().skip(1).collect();
1316            let extraction_path = tmp_extract_dir.path().join(&collected_path);
1317
1318            // Ensure parent directories exist
1319            if let Some(parent) = extraction_path.parent() {
1320                std::fs::create_dir_all(parent)?;
1321            }
1322
1323            if let Err(err) = entry.unpack(&extraction_path) {
1324                // Cleanup on error
1325                std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1326                return Err(err.into());
1327            }
1328            extracted_files.push(extraction_path);
1329        }
1330
1331        // Try to move the current app to backup
1332        let move_result = std::fs::rename(
1333            &self.extract_path,
1334            tmp_backup_dir.path().join("current_app"),
1335        );
1336        let need_authorization = if let Err(err) = move_result {
1337            if err.kind() == std::io::ErrorKind::PermissionDenied {
1338                true
1339            } else {
1340                std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1341                return Err(err.into());
1342            }
1343        } else {
1344            false
1345        };
1346
1347        if need_authorization {
1348            log::debug!("app installation needs admin privileges");
1349            // Use AppleScript to perform moves with admin privileges
1350            let apple_script = format!(
1351                "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges",
1352                src = self.extract_path.display(),
1353                new = tmp_extract_dir.path().display()
1354            );
1355
1356            let (tx, rx) = std::sync::mpsc::channel();
1357            let res = (self.context.run_on_main_thread)(Box::new(move || {
1358                let mut script =
1359                    osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script);
1360                script.compile().expect("invalid AppleScript");
1361                let r = script.execute();
1362                tx.send(r).unwrap();
1363            }));
1364            let result = rx.recv().unwrap();
1365
1366            if res.is_err() || result.is_err() {
1367                std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
1368                return Err(Error::Io(std::io::Error::new(
1369                    std::io::ErrorKind::PermissionDenied,
1370                    "Failed to move the new app into place",
1371                )));
1372            }
1373        } else {
1374            // Remove existing directory if it exists
1375            if self.extract_path.exists() {
1376                std::fs::remove_dir_all(&self.extract_path)?;
1377            }
1378            // Move the new app to the target path
1379            std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?;
1380        }
1381
1382        let _ = std::process::Command::new("touch")
1383            .arg(&self.extract_path)
1384            .status();
1385
1386        Ok(())
1387    }
1388}
1389
1390/// Gets the base target string used by the updater. If bundle type is available it
1391/// will be added to this string when selecting the download URL and signature.
1392/// `tauri::utils::platform::bundle_type` method is used to obtain current bundle type.
1393pub fn target() -> Option<String> {
1394    if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
1395        Some(format!("{target}-{arch}"))
1396    } else {
1397        None
1398    }
1399}
1400
1401fn updater_os() -> Option<&'static str> {
1402    if cfg!(target_os = "linux") {
1403        Some("linux")
1404    } else if cfg!(target_os = "macos") {
1405        // TODO shouldn't this be macos instead?
1406        Some("darwin")
1407    } else if cfg!(target_os = "windows") {
1408        Some("windows")
1409    } else {
1410        None
1411    }
1412}
1413
1414fn updater_arch() -> Option<&'static str> {
1415    if cfg!(target_arch = "x86") {
1416        Some("i686")
1417    } else if cfg!(target_arch = "x86_64") {
1418        Some("x86_64")
1419    } else if cfg!(target_arch = "arm") {
1420        Some("armv7")
1421    } else if cfg!(target_arch = "aarch64") {
1422        Some("aarch64")
1423    } else if cfg!(target_arch = "riscv64") {
1424        Some("riscv64")
1425    } else {
1426        None
1427    }
1428}
1429
1430pub fn extract_path_from_executable(executable_path: &Path) -> Result<PathBuf> {
1431    // Return the path of the current executable by default
1432    // Example C:\Program Files\My App\
1433    let extract_path = executable_path
1434        .parent()
1435        .map(PathBuf::from)
1436        .ok_or(Error::FailedToDetermineExtractPath)?;
1437
1438    // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
1439    // We need to get /Applications/<app>.app
1440    // TODO(lemarier): Need a better way here
1441    // Maybe we could search for <*.app> to get the right path
1442    #[cfg(target_os = "macos")]
1443    if extract_path
1444        .display()
1445        .to_string()
1446        .contains("Contents/MacOS")
1447    {
1448        return extract_path
1449            .parent()
1450            .map(PathBuf::from)
1451            .ok_or(Error::FailedToDetermineExtractPath)?
1452            .parent()
1453            .map(PathBuf::from)
1454            .ok_or(Error::FailedToDetermineExtractPath);
1455    }
1456
1457    Ok(extract_path)
1458}
1459
1460impl<'de> Deserialize<'de> for RemoteRelease {
1461    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1462    where
1463        D: Deserializer<'de>,
1464    {
1465        #[derive(Deserialize)]
1466        struct InnerRemoteRelease {
1467            #[serde(alias = "name", deserialize_with = "parse_version")]
1468            version: Version,
1469            notes: Option<String>,
1470            pub_date: Option<String>,
1471            platforms: Option<HashMap<String, ReleaseManifestPlatform>>,
1472            // dynamic platform response
1473            url: Option<Url>,
1474            signature: Option<String>,
1475        }
1476
1477        let release = InnerRemoteRelease::deserialize(deserializer)?;
1478
1479        let pub_date = if let Some(date) = release.pub_date {
1480            Some(
1481                OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
1482                    .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
1483            )
1484        } else {
1485            None
1486        };
1487
1488        Ok(RemoteRelease {
1489            version: release.version,
1490            notes: release.notes,
1491            pub_date,
1492            data: if let Some(platforms) = release.platforms {
1493                RemoteReleaseInner::Static { platforms }
1494            } else {
1495                RemoteReleaseInner::Dynamic(ReleaseManifestPlatform {
1496                    url: release.url.ok_or_else(|| {
1497                        DeError::custom("the `url` field was not set on the updater response")
1498                    })?,
1499                    signature: release.signature.ok_or_else(|| {
1500                        DeError::custom("the `signature` field was not set on the updater response")
1501                    })?,
1502                })
1503            },
1504        })
1505    }
1506}
1507
1508fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
1509    match bundle? {
1510        BundleType::Deb => Some(Installer::Deb),
1511        BundleType::Rpm => Some(Installer::Rpm),
1512        BundleType::AppImage => Some(Installer::AppImage),
1513        BundleType::Msi => Some(Installer::Msi),
1514        BundleType::Nsis => Some(Installer::Nsis),
1515        BundleType::App => Some(Installer::App), // App is also returned for Dmg type
1516        _ => None,
1517    }
1518}
1519
1520fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
1521where
1522    D: serde::Deserializer<'de>,
1523{
1524    let str = String::deserialize(deserializer)?;
1525
1526    Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom)
1527}
1528
1529// Validate signature
1530fn verify_signature(
1531    data: &[u8],
1532    release_signature: &str,
1533    pub_key: &str,
1534    announced_version: &str,
1535    require_signed_version: bool,
1536) -> Result<()> {
1537    // we need to convert the pub key
1538    let pub_key_decoded = base64_to_string(pub_key)?;
1539    let public_key = PublicKey::decode(&pub_key_decoded)?;
1540    let signature_base64_decoded = base64_to_string(release_signature)?;
1541    let signature = Signature::decode(&signature_base64_decoded)?;
1542
1543    // Validate signature or bail out
1544    public_key.verify(data, &signature, true)?;
1545
1546    // Only now is the trusted comment usable: minisign's global signature covers it, and
1547    // `verify` above is what checks that global signature. Reading it before this point would
1548    // be trusting attacker controlled data.
1549    verify_signed_version(
1550        signature.trusted_comment(),
1551        announced_version,
1552        require_signed_version,
1553    )
1554}
1555
1556/// Checks the version the artifact was signed for against the version the update endpoint
1557/// announced.
1558///
1559/// The endpoint response is not signed, so its `version` field on its own does not prove which
1560/// release the `url` and `signature` actually point at. Comparing it against the signed version
1561/// is what stops a tampered response from pairing a new version number with an older release.
1562fn verify_signed_version(
1563    trusted_comment: &str,
1564    announced_version: &str,
1565    require_signed_version: bool,
1566) -> Result<()> {
1567    let Some(signed_version) = signed_version(trusted_comment) else {
1568        // Signatures produced before the Tauri CLI started recording the version carry none, so
1569        // this can only be enforced when the app opts in. Note that leaving it off means an
1570        // attacker can bypass the check outright by serving one of those older signatures.
1571        return if require_signed_version {
1572            Err(Error::MissingSignedVersion)
1573        } else {
1574            Ok(())
1575        };
1576    };
1577
1578    // compare as semver so that equivalent spellings like `1.2.3` and `v1.2.3` match, falling
1579    // back to a literal comparison for versions that are not valid semver
1580    let matches = match (
1581        Version::from_str(signed_version.trim_start_matches('v')),
1582        Version::from_str(announced_version.trim_start_matches('v')),
1583    ) {
1584        (Ok(signed), Ok(announced)) => signed == announced,
1585        _ => signed_version == announced_version,
1586    };
1587
1588    if matches {
1589        Ok(())
1590    } else {
1591        Err(Error::SignedVersionMismatch {
1592            signed: signed_version.to_string(),
1593            announced: announced_version.to_string(),
1594        })
1595    }
1596}
1597
1598/// Reads the `version` field out of a signature's trusted comment, which the Tauri CLI writes as
1599/// tab separated `key:value` pairs, e.g. `timestamp:1700000000\tfile:app.tar.gz\tversion:1.2.3`.
1600fn signed_version(trusted_comment: &str) -> Option<&str> {
1601    trusted_comment
1602        .split('\t')
1603        .find_map(|field| field.strip_prefix("version:"))
1604}
1605
1606fn base64_to_string(base64_string: &str) -> Result<String> {
1607    let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
1608    let result = std::str::from_utf8(decoded_string)
1609        .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
1610        .to_string();
1611    Ok(result)
1612}
1613
1614#[cfg(windows)]
1615fn encode_wide(string: impl AsRef<OsStr>) -> Vec<u16> {
1616    use std::os::windows::ffi::OsStrExt;
1617
1618    string
1619        .as_ref()
1620        .encode_wide()
1621        .chain(std::iter::once(0))
1622        .collect()
1623}
1624
1625#[cfg(windows)]
1626trait PathExt {
1627    fn wrap_in_quotes(&self) -> Self;
1628}
1629
1630#[cfg(windows)]
1631impl PathExt for PathBuf {
1632    fn wrap_in_quotes(&self) -> Self {
1633        let mut msi_path = OsString::from("\"");
1634        msi_path.push(self.as_os_str());
1635        msi_path.push("\"");
1636        PathBuf::from(msi_path)
1637    }
1638}
1639
1640// adapted from https://github.com/rust-lang/rust/blob/1c047506f94cd2d05228eb992b0a6bbed1942349/library/std/src/sys/args/windows.rs#L174
1641#[cfg(windows)]
1642fn escape_nsis_current_exe_arg(arg: impl AsRef<OsStr>) -> OsString {
1643    use std::os::windows::ffi::{OsStrExt, OsStringExt};
1644
1645    let arg = arg.as_ref();
1646    let mut cmd: Vec<u16> = Vec::new();
1647
1648    // compared to std we additionally escape `/` so that nsis won't interpret them as a beginning of an nsis argument.
1649    let quote = arg
1650        .as_encoded_bytes()
1651        .iter()
1652        .any(|c| *c == b' ' || *c == b'\t' || *c == b'/')
1653        || arg.is_empty();
1654    let escape = true;
1655    if quote {
1656        cmd.push('"' as u16);
1657    }
1658    let mut backslashes: usize = 0;
1659    for x in arg.encode_wide() {
1660        if escape {
1661            if x == '\\' as u16 {
1662                backslashes += 1;
1663            } else {
1664                if x == '"' as u16 {
1665                    // Add n+1 backslashes to total 2n+1 before internal '"'.
1666                    cmd.extend((0..=backslashes).map(|_| '\\' as u16));
1667                }
1668                backslashes = 0;
1669            }
1670        }
1671        cmd.push(x);
1672    }
1673    if quote {
1674        // Add n backslashes to total 2n before ending '"'.
1675        cmd.extend((0..backslashes).map(|_| '\\' as u16));
1676        cmd.push('"' as u16);
1677    }
1678    OsString::from_wide(&cmd)
1679}
1680
1681#[cfg(windows)]
1682fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
1683    let mut arg = arg.as_ref().to_string_lossy().to_string();
1684
1685    // Otherwise this argument will get lost in ShellExecute
1686    if arg.is_empty() {
1687        return "\"\"\"\"".to_string();
1688    } else if !arg.contains(' ') && !arg.contains('"') {
1689        return arg;
1690    }
1691
1692    if arg.contains('"') {
1693        arg = arg.replace('"', r#""""""#);
1694    }
1695
1696    if arg.starts_with('-') {
1697        if let Some((a1, a2)) = arg.split_once('=') {
1698            format!("{a1}=\"\"{a2}\"\"")
1699        } else {
1700            format!("\"\"{arg}\"\"")
1701        }
1702    } else {
1703        format!("\"\"{arg}\"\"")
1704    }
1705}
1706
1707#[cfg(test)]
1708mod tests {
1709    use super::{signed_version, verify_signed_version};
1710    use crate::error::Error;
1711
1712    const CURRENT: &str = "timestamp:1700000000\tfile:app_1.2.3_x64.msi.zip\tversion:1.2.3";
1713    // signatures produced before the CLI started embedding the version
1714    const LEGACY: &str = "timestamp:1600000000\tfile:app_1.0.0_x64.msi.zip";
1715
1716    #[test]
1717    fn reads_the_signed_version() {
1718        assert_eq!(signed_version(CURRENT), Some("1.2.3"));
1719        assert_eq!(signed_version(LEGACY), None);
1720        // must not match on a field that merely ends in `version:`
1721        assert_eq!(signed_version("timestamp:1\tfile:app-version:2.zip"), None);
1722    }
1723
1724    #[test]
1725    fn accepts_a_matching_version() {
1726        assert!(verify_signed_version(CURRENT, "1.2.3", true).is_ok());
1727        assert!(verify_signed_version(CURRENT, "1.2.3", false).is_ok());
1728        // the endpoint and the CLI may spell the same version differently
1729        assert!(verify_signed_version(CURRENT, "v1.2.3", true).is_ok());
1730    }
1731
1732    #[test]
1733    fn rejects_a_version_the_artifact_was_not_signed_for() {
1734        // the rollback the flag exists to stop: an old artifact announced as a new version
1735        let err = verify_signed_version(CURRENT, "9.9.9", false).unwrap_err();
1736        assert!(
1737            matches!(err, Error::SignedVersionMismatch { ref signed, ref announced }
1738                if signed == "1.2.3" && announced == "9.9.9"),
1739            "unexpected error: {err}"
1740        );
1741        // rejected regardless of whether the app opted in, since the signature does say
1742        // which version it covers
1743        assert!(verify_signed_version(CURRENT, "9.9.9", true).is_err());
1744        assert!(verify_signed_version(CURRENT, "1.2.4", true).is_err());
1745    }
1746
1747    #[test]
1748    fn only_requires_a_signed_version_when_configured() {
1749        assert!(verify_signed_version(LEGACY, "9.9.9", false).is_ok());
1750        assert!(matches!(
1751            verify_signed_version(LEGACY, "9.9.9", true).unwrap_err(),
1752            Error::MissingSignedVersion
1753        ));
1754    }
1755
1756    #[test]
1757    fn compares_non_semver_versions_literally() {
1758        let comment = "timestamp:1700000000\tfile:app.zip\tversion:2024-01-01";
1759        assert!(verify_signed_version(comment, "2024-01-01", true).is_ok());
1760        assert!(verify_signed_version(comment, "2024-01-02", true).is_err());
1761    }
1762
1763    #[test]
1764    #[cfg(windows)]
1765    fn it_wraps_correctly() {
1766        use super::PathExt;
1767        use std::path::PathBuf;
1768
1769        assert_eq!(
1770            PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(),
1771            PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"")
1772        )
1773    }
1774
1775    #[test]
1776    #[cfg(windows)]
1777    fn it_escapes_correctly_for_msi() {
1778        use crate::updater::escape_msi_property_arg;
1779
1780        // Explanation for quotes:
1781        // The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level.
1782        // To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later.
1783        // This means that the escaped strings can't ever have a single quotation mark!
1784        // Now there are 3 major things to look out for to not break the msiexec call:
1785        //   1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument.
1786        //   2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored.
1787        //   3) Escape emtpy args in quotation marks, otherwise the argument will get lost.
1788        let cases = [
1789            "something",
1790            "--flag",
1791            "--empty=",
1792            "--arg=value",
1793            "some space",                     // This simulates `./my-app "some string"`.
1794            "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
1795            "--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
1796            "--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
1797            "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
1798            "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
1799            "",            // `./my-app '""'`
1800        ];
1801        let cases_escaped = [
1802            "something",
1803            "--flag",
1804            "--empty=",
1805            "--arg=value",
1806            "\"\"some space\"\"",
1807            "\"\"--arg value\"\"",
1808            "--arg=\"\"unwrapped space\"\"",
1809            r#"--arg=""""""wrapped"""""""#,
1810            r#"--arg=""""""wrapped space"""""""#,
1811            r#"--arg=""midword""""wrapped space"""""""#,
1812            "\"\"\"\"",
1813        ];
1814
1815        // Just to be sure we didn't mess that up
1816        assert_eq!(cases.len(), cases_escaped.len());
1817
1818        for (orig, escaped) in cases.iter().zip(cases_escaped) {
1819            assert_eq!(escape_msi_property_arg(orig), escaped);
1820        }
1821    }
1822
1823    #[test]
1824    #[cfg(windows)]
1825    fn it_escapes_correctly_for_nsis() {
1826        use crate::updater::escape_nsis_current_exe_arg;
1827        use std::ffi::OsStr;
1828
1829        let cases = [
1830            "something",
1831            "--flag",
1832            "--empty=",
1833            "--arg=value",
1834            "some space",                     // This simulates `./my-app "some string"`.
1835            "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic.
1836            "--arg=unwrapped space", // `./my-app --arg="unwrapped space"`
1837            "--arg=\"wrapped\"", // `./my-app --args=""wrapped""`
1838            "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""`
1839            "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""`
1840            "",            // `./my-app '""'`
1841        ];
1842        // Note: These may not be the results we actually want (monitor this!).
1843        // We only make sure the implementation doesn't unintentionally change.
1844        let cases_escaped = [
1845            "something",
1846            "--flag",
1847            "--empty=",
1848            "--arg=value",
1849            "\"some space\"",
1850            "\"--arg value\"",
1851            "\"--arg=unwrapped space\"",
1852            "--arg=\\\"wrapped\\\"",
1853            "\"--arg=\\\"wrapped space\\\"\"",
1854            "\"--arg=midword\\\"wrapped space\\\"\"",
1855            "\"\"",
1856        ];
1857
1858        // Just to be sure we didn't mess that up
1859        assert_eq!(cases.len(), cases_escaped.len());
1860
1861        for (orig, escaped) in cases.iter().zip(cases_escaped) {
1862            assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped);
1863        }
1864    }
1865}