Browse Source

refactor(core): use `attohttpc` by default (#1861)

Lucas Fernandes Nogueira 4 years ago
parent
commit
17c7c4396f

+ 5 - 0
.changes/attohttpc-default-client.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Use `attohttpc` on the HTTP API by default for bundle size optimization. `reqwest` is implemented behind the `reqwest-client` feature flag.

+ 5 - 0
.changes/core-features.md

@@ -0,0 +1,5 @@
+---
+"cli.rs": patch
+---
+
+Properly keep all `tauri` features that are not managed by the CLI.

+ 10 - 3
core/tauri/Cargo.toml

@@ -27,6 +27,9 @@ targets = [
   "x86_64-apple-darwin"
 ]
 
+[package.metadata.cargo-udeps.ignore]
+normal = ["attohttpc"] # we ignore attohttpc because we can't remove it based on `not(feature = "reqwest-client")`
+
 [dependencies]
 serde_json = { version = "1.0", features = [ "raw_value" ] }
 serde = { version = "1.0", features = [ "derive" ] }
@@ -41,7 +44,6 @@ tauri-macros = { version = "1.0.0-beta.1", path = "../tauri-macros" }
 tauri-utils = { version = "1.0.0-beta.0", path = "../tauri-utils" }
 tauri-runtime-wry = { version = "0.1.1", path = "../tauri-runtime-wry", optional = true }
 rand = "0.8"
-reqwest = { version = "0.11", features = [ "json", "multipart" ] }
 tempfile = "3"
 semver = "0.11"
 serde_repr = "0.1"
@@ -53,7 +55,6 @@ tar = "0.4"
 flate2 = "1.0"
 rfd = "0.3.0"
 tinyfiledialogs = "3.3"
-bytes = { version = "1", features = [ "serde" ] }
 http = "0.2"
 clap = { version = "=3.0.0-beta.2", optional = true }
 notify-rust = { version = "4.5.0", optional = true }
@@ -65,6 +66,11 @@ minisign-verify = "0.1.8"
 state = "0.4"
 bincode = "1.3"
 
+# HTTP
+reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true }
+bytes = { version = "1", features = [ "serde" ], optional = true }
+attohttpc = { version = "0.17", features = [ "json", "form" ] }
+
 [build-dependencies]
 cfg_aliases = "0.1.1"
 
@@ -85,9 +91,10 @@ wry = [ "tauri-runtime-wry" ]
 cli = [ "clap" ]
 custom-protocol = [ "tauri-macros/custom-protocol" ]
 api-all = [ "notification-all", "global-shortcut-all", "updater" ]
-updater = [ "reqwest/default-tls" ]
+updater = [ ]
 menu = [ "tauri-runtime/menu", "tauri-runtime-wry/menu" ]
 system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ]
+reqwest-client = [ "reqwest", "bytes" ]
 fs-all = [ ]
 fs-read-text-file = [ ]
 fs-read-binary-file = [ ]

+ 7 - 2
core/tauri/src/api/error.rs

@@ -25,6 +25,11 @@ pub enum Error {
   #[error("user cancelled the dialog")]
   DialogCancelled,
   /// The network error.
+  #[cfg(not(feature = "reqwest-client"))]
+  #[error("Network Error: {0}")]
+  Network(#[from] attohttpc::Error),
+  /// The network error.
+  #[cfg(feature = "reqwest-client")]
   #[error("Network Error: {0}")]
   Network(#[from] reqwest::Error),
   /// HTTP method error.
@@ -32,10 +37,10 @@ pub enum Error {
   HttpMethod(#[from] http::method::InvalidMethod),
   /// Invalid HTTO header.
   #[error("{0}")]
-  HttpHeader(#[from] reqwest::header::InvalidHeaderName),
+  HttpHeader(#[from] http::header::InvalidHeaderName),
   /// Failed to serialize header value as string.
   #[error("failed to convert response header value to string")]
-  HttpHeaderToString(#[from] reqwest::header::ToStrError),
+  HttpHeaderToString(#[from] http::header::ToStrError),
   /// HTTP form to must be an object.
   #[error("http form must be an object")]
   InvalidHttpForm,

+ 132 - 12
core/tauri/src/api/http.rs

@@ -2,8 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use bytes::Bytes;
-use reqwest::{header::HeaderName, redirect::Policy, Method};
+use http::{header::HeaderName, Method};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -11,7 +10,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
 use std::{collections::HashMap, path::PathBuf, time::Duration};
 
 /// Client builder.
-#[derive(Default, Deserialize)]
+#[derive(Clone, Default, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct ClientBuilder {
   /// Max number of redirections to follow
@@ -38,12 +37,19 @@ impl ClientBuilder {
     self
   }
 
-  /// Builds the ClientOptions.
+  /// Builds the Client.
+  #[cfg(not(feature = "reqwest-client"))]
+  pub fn build(self) -> crate::api::Result<Client> {
+    Ok(Client(self))
+  }
+
+  /// Builds the Client.
+  #[cfg(feature = "reqwest-client")]
   pub fn build(self) -> crate::api::Result<Client> {
     let mut client_builder = reqwest::Client::builder();
 
     if let Some(max_redirections) = self.max_redirections {
-      client_builder = client_builder.redirect(Policy::limited(max_redirections))
+      client_builder = client_builder.redirect(reqwest::redirect::Policy::limited(max_redirections))
     }
 
     if let Some(connect_timeout) = self.connect_timeout {
@@ -56,16 +62,80 @@ impl ClientBuilder {
 }
 
 /// The HTTP client.
+#[cfg(feature = "reqwest-client")]
 #[derive(Clone)]
 pub struct Client(reqwest::Client);
 
+/// The HTTP client.
+#[cfg(not(feature = "reqwest-client"))]
+#[derive(Clone)]
+pub struct Client(ClientBuilder);
+
+#[cfg(not(feature = "reqwest-client"))]
+impl Client {
+  /// Executes an HTTP request
+  ///
+  /// The response will be transformed to String,
+  /// If reading the response as binary, the byte array will be serialized using serde_json.
+  pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
+    let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
+
+    let mut request_builder = attohttpc::RequestBuilder::try_new(method, &request.url)?;
+
+    if let Some(query) = request.query {
+      request_builder = request_builder.params(&query);
+    }
+
+    if let Some(headers) = request.headers {
+      for (header, header_value) in headers.iter() {
+        request_builder =
+          request_builder.header(HeaderName::from_bytes(header.as_bytes())?, header_value);
+      }
+    }
+
+    if let Some(timeout) = request.timeout {
+      request_builder = request_builder.timeout(Duration::from_secs(timeout));
+    }
+
+    let response = if let Some(body) = request.body {
+      match body {
+        Body::Bytes(data) => request_builder.body(attohttpc::body::Bytes(data)).send()?,
+        Body::Text(text) => request_builder.body(attohttpc::body::Bytes(text)).send()?,
+        Body::Json(json) => request_builder.json(&json)?.send()?,
+        Body::Form(form_body) => {
+          let mut form = Vec::new();
+          for (name, part) in form_body.0 {
+            match part {
+              FormPart::Bytes(bytes) => form.push((name, serde_json::to_string(&bytes)?)),
+              FormPart::File(file_path) => form.push((name, serde_json::to_string(&file_path)?)),
+              FormPart::Text(text) => form.push((name, text)),
+            }
+          }
+          request_builder.form(&form)?.send()?
+        }
+      }
+    } else {
+      request_builder.send()?
+    };
+
+    let response = response.error_for_status()?;
+    Ok(Response(
+      request.response_type.unwrap_or(ResponseType::Json),
+      response,
+      request.url,
+    ))
+  }
+}
+
+#[cfg(feature = "reqwest-client")]
 impl Client {
   /// Executes an HTTP request
   ///
   /// The response will be transformed to String,
-  /// If reading the response as binary, the byte array will be serialized using serde_json
+  /// If reading the response as binary, the byte array will be serialized using serde_json.
   pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
     let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
+
     let mut request_builder = self.0.request(method, &request.url);
 
     if let Some(query) = request.query {
@@ -85,8 +155,18 @@ impl Client {
 
     let response = if let Some(body) = request.body {
       match body {
-        Body::Bytes(data) => request_builder.body(Bytes::from(data)).send().await?,
-        Body::Text(text) => request_builder.body(Bytes::from(text)).send().await?,
+        Body::Bytes(data) => {
+          request_builder
+            .body(bytes::Bytes::from(data))
+            .send()
+            .await?
+        }
+        Body::Text(text) => {
+          request_builder
+            .body(bytes::Bytes::from(text))
+            .send()
+            .await?
+        }
         Body::Json(json) => request_builder.json(&json).send().await?,
         Body::Form(form_body) => {
           let mut form = Vec::new();
@@ -249,24 +329,50 @@ impl HttpRequestBuilder {
 }
 
 /// The HTTP response.
+#[cfg(feature = "reqwest-client")]
 pub struct Response(ResponseType, reqwest::Response);
+/// The HTTP response.
+#[cfg(not(feature = "reqwest-client"))]
+pub struct Response(ResponseType, attohttpc::Response, String);
 
 impl Response {
+  /// Reads the response as raw bytes.
+  pub async fn bytes(self) -> crate::api::Result<RawResponse> {
+    let status = self.1.status().as_u16();
+    #[cfg(feature = "reqwest-client")]
+    let data = self.1.bytes().await?.to_vec();
+    #[cfg(not(feature = "reqwest-client"))]
+    let data = self.1.bytes()?;
+    Ok(RawResponse { status, data })
+  }
+
   /// Reads the response and returns its info.
   pub async fn read(self) -> crate::api::Result<ResponseData> {
+    #[cfg(feature = "reqwest-client")]
     let url = self.1.url().to_string();
+    #[cfg(not(feature = "reqwest-client"))]
+    let url = self.2;
+
     let mut headers = HashMap::new();
     for (name, value) in self.1.headers() {
       headers.insert(name.as_str().to_string(), value.to_str()?.to_string());
     }
     let status = self.1.status().as_u16();
 
+    #[cfg(feature = "reqwest-client")]
     let data = match self.0 {
       ResponseType::Json => self.1.json().await?,
       ResponseType::Text => Value::String(self.1.text().await?),
       ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes().await?)?),
     };
 
+    #[cfg(not(feature = "reqwest-client"))]
+    let data = match self.0 {
+      ResponseType::Json => self.1.json()?,
+      ResponseType::Text => Value::String(self.1.text()?),
+      ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes()?)?),
+    };
+
     Ok(ResponseData {
       url,
       status,
@@ -276,12 +382,26 @@ impl Response {
   }
 }
 
+/// A response with raw bytes.
+#[non_exhaustive]
+pub struct RawResponse {
+  /// Response status code.
+  pub status: u16,
+  /// Response bytes.
+  pub data: Vec<u8>,
+}
+
 /// The response type.
 #[derive(Serialize)]
 #[serde(rename_all = "camelCase")]
+#[non_exhaustive]
 pub struct ResponseData {
-  url: String,
-  status: u16,
-  headers: HashMap<String, String>,
-  data: Value,
+  /// Response URL. Useful if it followed redirects.
+  pub url: String,
+  /// Response status code.
+  pub status: u16,
+  /// Response headers.
+  pub headers: HashMap<String, String>,
+  /// Response data.
+  pub data: Value,
 }

+ 37 - 35
core/tauri/src/updater/core.rs

@@ -5,16 +5,17 @@
 use super::error::{Error, Result};
 use crate::api::{file::Extract, version};
 use base64::decode;
+use http::StatusCode;
 use minisign_verify::{PublicKey, Signature};
-use reqwest::{self, header, StatusCode};
 use std::{
+  collections::HashMap,
   env,
   ffi::OsStr,
   fs::{read_dir, remove_file, File, OpenOptions},
   io::{prelude::*, BufReader, Read},
   path::{Path, PathBuf},
   str::from_utf8,
-  time::{Duration, SystemTime, UNIX_EPOCH},
+  time::{SystemTime, UNIX_EPOCH},
 };
 
 #[cfg(not(target_os = "macos"))]
@@ -23,6 +24,8 @@ use std::process::Command;
 #[cfg(target_os = "macos")]
 use crate::api::file::Move;
 
+use crate::api::http::{ClientBuilder, HttpRequestBuilder};
+
 #[cfg(target_os = "windows")]
 use std::process::exit;
 
@@ -271,31 +274,33 @@ impl<'a> UpdateBuilder<'a> {
       );
 
       // we want JSON only
-      let mut headers = header::HeaderMap::new();
-      headers.insert(header::ACCEPT, "application/json".parse().unwrap());
-
-      let resp = reqwest::Client::new()
-        .get(&fixed_link)
-        .headers(headers)
-        // wait 20sec for the firewall
-        .timeout(Duration::from_secs(20))
-        .send()
+      let mut headers = HashMap::new();
+      headers.insert("Accept".into(), "application/json".into());
+
+      let resp = ClientBuilder::new()
+        .build()?
+        .send(
+          HttpRequestBuilder::new("GET", &fixed_link)
+            .headers(headers)
+            // wait 20sec for the firewall
+            .timeout(20),
+        )
         .await;
 
       // If we got a success, we stop the loop
       // and we set our remote_release variable
-      if let Ok(ref res) = resp {
+      if let Ok(res) = resp {
+        let res = res.read().await?;
         // got status code 2XX
-        if res.status().is_success() {
+        if StatusCode::from_u16(res.status).unwrap().is_success() {
           // if we got 204
-          if StatusCode::NO_CONTENT == res.status() {
+          if StatusCode::NO_CONTENT.as_u16() == res.status {
             // return with `UpToDate` error
             // we should catch on the client
             return Err(Error::UpToDate);
           };
-          let json = resp?.json::<serde_json::Value>().await?;
           // Convert the remote result to our local struct
-          let built_release = RemoteRelease::from_release(&json, &target);
+          let built_release = RemoteRelease::from_release(&res.data, &target);
           // make sure all went well and the remote data is compatible
           // with what we need locally
           match built_release {
@@ -411,35 +416,32 @@ impl Update {
     let mut tmp_archive = File::create(&tmp_archive_path)?;
 
     // set our headers
-    let mut headers = header::HeaderMap::new();
-    headers.insert(header::ACCEPT, "application/octet-stream".parse().unwrap());
-
-    // make sure we have a valid agent
-    if !headers.contains_key(header::USER_AGENT) {
-      headers.insert(
-        header::USER_AGENT,
-        "tauri/updater".parse().expect("invalid user-agent"),
-      );
-    }
+    let mut headers = HashMap::new();
+    headers.insert("Accept".into(), "application/octet-stream".into());
+    headers.insert("User-Agent".into(), "tauri/updater".into());
 
     // Create our request
-    let resp = reqwest::Client::new()
-      .get(&url)
-      // wait 20sec for the firewall
-      .timeout(Duration::from_secs(20))
-      .headers(headers)
-      .send()
+    let resp = ClientBuilder::new()
+      .build()?
+      .send(
+        HttpRequestBuilder::new("GET", &url)
+          .headers(headers)
+          // wait 20sec for the firewall
+          .timeout(20),
+      )
+      .await?
+      .bytes()
       .await?;
 
     // make sure it's success
-    if !resp.status().is_success() {
+    if !StatusCode::from_u16(resp.status).unwrap().is_success() {
       return Err(Error::Network(format!(
         "Download request failed with status: {}",
-        resp.status()
+        resp.status
       )));
     }
 
-    tmp_archive.write_all(&resp.bytes().await?)?;
+    tmp_archive.write_all(&resp.data)?;
 
     // Validate signature ONLY if pubkey is available in tauri.conf.json
     if let Some(pub_key) = pub_key {

+ 0 - 3
core/tauri/src/updater/error.rs

@@ -11,9 +11,6 @@ pub enum Error {
   /// IO Errors.
   #[error("`{0}`")]
   Io(#[from] std::io::Error),
-  /// Reqwest Errors.
-  #[error("Request error: {0}")]
-  Reqwest(#[from] reqwest::Error),
   /// Semver Errors.
   #[error("Unable to compare version: {0}")]
   Semver(#[from] semver::SemVerError),

+ 1 - 1
examples/api/src-tauri/Cargo.toml

@@ -11,7 +11,7 @@ tauri-build = { path = "../../../core/tauri-build" }
 [dependencies]
 serde_json = "1.0"
 serde = { version = "1.0", features = [ "derive" ] }
-tauri = { path = "../../../core/tauri", features = ["api-all", "cli", "updater", "system-tray", "menu"] }
+tauri = { path = "../../../core/tauri", features = ["api-all", "cli", "menu", "system-tray", "updater"] }
 
 [features]
 default = [ "custom-protocol" ]

+ 41 - 41
tooling/cli.rs/config_definition.rs

@@ -300,7 +300,7 @@ pub struct SecurityConfig {
   pub csp: Option<String>,
 }
 
-trait Allowlist {
+pub trait Allowlist {
   fn to_features(&self) -> Vec<&str>;
 }
 
@@ -314,31 +314,31 @@ macro_rules! check_feature {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct FsAllowlistConfig {
+pub struct FsAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
   #[serde(default)]
-  read_text_file: bool,
+  pub read_text_file: bool,
   #[serde(default)]
-  read_binary_file: bool,
+  pub read_binary_file: bool,
   #[serde(default)]
-  write_file: bool,
+  pub write_file: bool,
   #[serde(default)]
-  write_binary_file: bool,
+  pub write_binary_file: bool,
   #[serde(default)]
-  read_dir: bool,
+  pub read_dir: bool,
   #[serde(default)]
-  copy_file: bool,
+  pub copy_file: bool,
   #[serde(default)]
-  create_dir: bool,
+  pub create_dir: bool,
   #[serde(default)]
-  remove_dir: bool,
+  pub remove_dir: bool,
   #[serde(default)]
-  remove_file: bool,
+  pub remove_file: bool,
   #[serde(default)]
-  rename_file: bool,
+  pub rename_file: bool,
   #[serde(default)]
-  path: bool,
+  pub path: bool,
 }
 
 impl Allowlist for FsAllowlistConfig {
@@ -365,11 +365,11 @@ impl Allowlist for FsAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct WindowAllowlistConfig {
+pub struct WindowAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
   #[serde(default)]
-  create: bool,
+  pub create: bool,
 }
 
 impl Allowlist for WindowAllowlistConfig {
@@ -386,13 +386,13 @@ impl Allowlist for WindowAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct ShellAllowlistConfig {
+pub struct ShellAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
   #[serde(default)]
-  execute: bool,
+  pub execute: bool,
   #[serde(default)]
-  open: bool,
+  pub open: bool,
 }
 
 impl Allowlist for ShellAllowlistConfig {
@@ -410,13 +410,13 @@ impl Allowlist for ShellAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct DialogAllowlistConfig {
+pub struct DialogAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
   #[serde(default)]
-  open: bool,
+  pub open: bool,
   #[serde(default)]
-  save: bool,
+  pub save: bool,
 }
 
 impl Allowlist for DialogAllowlistConfig {
@@ -434,11 +434,11 @@ impl Allowlist for DialogAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct HttpAllowlistConfig {
+pub struct HttpAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
   #[serde(default)]
-  request: bool,
+  pub request: bool,
 }
 
 impl Allowlist for HttpAllowlistConfig {
@@ -455,9 +455,9 @@ impl Allowlist for HttpAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct NotificationAllowlistConfig {
+pub struct NotificationAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
 }
 
 impl Allowlist for NotificationAllowlistConfig {
@@ -472,9 +472,9 @@ impl Allowlist for NotificationAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct GlobalShortcutAllowlistConfig {
+pub struct GlobalShortcutAllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
 }
 
 impl Allowlist for GlobalShortcutAllowlistConfig {
@@ -489,23 +489,23 @@ impl Allowlist for GlobalShortcutAllowlistConfig {
 
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-struct AllowlistConfig {
+pub struct AllowlistConfig {
   #[serde(default)]
-  all: bool,
+  pub all: bool,
   #[serde(default)]
-  fs: FsAllowlistConfig,
+  pub fs: FsAllowlistConfig,
   #[serde(default)]
-  window: WindowAllowlistConfig,
+  pub window: WindowAllowlistConfig,
   #[serde(default)]
-  shell: ShellAllowlistConfig,
+  pub shell: ShellAllowlistConfig,
   #[serde(default)]
-  dialog: DialogAllowlistConfig,
+  pub dialog: DialogAllowlistConfig,
   #[serde(default)]
-  http: HttpAllowlistConfig,
+  pub http: HttpAllowlistConfig,
   #[serde(default)]
-  notification: NotificationAllowlistConfig,
+  pub notification: NotificationAllowlistConfig,
   #[serde(default)]
-  global_shortcut: GlobalShortcutAllowlistConfig,
+  pub global_shortcut: GlobalShortcutAllowlistConfig,
 }
 
 impl Allowlist for AllowlistConfig {

+ 1 - 1
tooling/cli.rs/src/build/rust.rs

@@ -383,7 +383,7 @@ fn tauri_config_to_bundle_settings(
     // provides `libwebkit2gtk-4.0.so.37` and all `4.0` versions have the -37 package name
     depends.push("libwebkit2gtk-4.0-37".to_string());
     depends.push("libgtk-3-0".to_string());
-    if manifest.features.contains(&"menu".into()) || system_tray_config.is_some() {
+    if manifest.features.contains("menu") || system_tray_config.is_some() {
       depends.push("libgtksourceview-3.0-1".to_string());
     }
   }

+ 41 - 0
tooling/cli.rs/src/helpers/config.rs

@@ -101,3 +101,44 @@ pub fn reload(merge_config: Option<&str>) -> crate::Result<()> {
   get_internal(merge_config, true)?;
   Ok(())
 }
+
+pub fn all_allowlist_features() -> Vec<&'static str> {
+  AllowlistConfig {
+    all: true,
+    fs: FsAllowlistConfig {
+      all: true,
+      read_text_file: true,
+      read_binary_file: true,
+      write_file: true,
+      write_binary_file: true,
+      read_dir: true,
+      copy_file: true,
+      create_dir: true,
+      remove_dir: true,
+      remove_file: true,
+      rename_file: true,
+      path: true,
+    },
+    window: WindowAllowlistConfig {
+      all: true,
+      create: true,
+    },
+    shell: ShellAllowlistConfig {
+      all: true,
+      execute: true,
+      open: true,
+    },
+    dialog: DialogAllowlistConfig {
+      all: true,
+      open: true,
+      save: true,
+    },
+    http: HttpAllowlistConfig {
+      all: true,
+      request: true,
+    },
+    notification: NotificationAllowlistConfig { all: true },
+    global_shortcut: GlobalShortcutAllowlistConfig { all: true },
+  }
+  .to_features()
+}

+ 30 - 27
tooling/cli.rs/src/helpers/manifest.rs

@@ -2,19 +2,23 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{app_paths::tauri_dir, config::ConfigHandle};
+use super::{
+  app_paths::tauri_dir,
+  config::{all_allowlist_features, ConfigHandle},
+};
 
 use anyhow::Context;
 use toml_edit::{Array, Document, InlineTable, Item, Value};
 
 use std::{
+  collections::HashSet,
   fs::File,
   io::{Read, Write},
   path::Path,
 };
 
 pub struct Manifest {
-  pub features: Vec<String>,
+  pub features: HashSet<String>,
 }
 
 fn read_manifest(manifest_path: &Path) -> crate::Result<Document> {
@@ -31,14 +35,14 @@ fn read_manifest(manifest_path: &Path) -> crate::Result<Document> {
   Ok(manifest)
 }
 
-fn features_to_vec(features: &Array) -> Vec<String> {
-  let mut string_features = Vec::new();
-  for feat in features.iter() {
-    if let Value::String(feature) = feat {
-      string_features.push(feature.value().to_string());
-    }
+fn toml_array(features: &HashSet<String>) -> Array {
+  let mut f = Array::default();
+  let mut features: Vec<String> = features.iter().map(|f| f.to_string()).collect();
+  features.sort();
+  for feature in features {
+    f.push(feature.as_str()).unwrap();
   }
-  string_features
+  f
 }
 
 pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result<Manifest> {
@@ -56,32 +60,35 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result<Manifest> {
   let config = config_guard.as_ref().unwrap();
 
   let allowlist_features = config.tauri.features();
-  let mut features = Array::default();
+  let mut features = HashSet::new();
   for feature in allowlist_features {
-    features.push(feature).unwrap();
+    features.insert(feature.to_string());
   }
   if config.tauri.cli.is_some() {
-    features.push("cli").unwrap();
+    features.insert("cli".to_string());
   }
   if config.tauri.updater.active {
-    features.push("updater").unwrap();
+    features.insert("updater".to_string());
   }
   if config.tauri.system_tray.is_some() {
-    features.push("system-tray").unwrap();
+    features.insert("system-tray".to_string());
   }
 
+  let mut cli_managed_features = all_allowlist_features();
+  cli_managed_features.extend(vec!["cli", "updater", "system-tray"]);
+
   if let Some(tauri) = tauri_entry.as_table_mut() {
     let manifest_features = tauri.entry("features");
     if let Item::Value(Value::Array(f)) = &manifest_features {
       for feat in f.iter() {
         if let Value::String(feature) = feat {
-          if feature.value() == "menu" {
-            features.push("menu").unwrap();
+          if !cli_managed_features.contains(&feature.value().as_str()) {
+            features.insert(feature.value().to_string());
           }
         }
       }
     }
-    *manifest_features = Item::Value(Value::Array(features.clone()));
+    *manifest_features = Item::Value(Value::Array(toml_array(&features)));
   } else if let Some(tauri) = tauri_entry.as_value_mut() {
     match tauri {
       Value::InlineTable(table) => {
@@ -89,13 +96,13 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result<Manifest> {
         if let Value::Array(f) = &manifest_features {
           for feat in f.iter() {
             if let Value::String(feature) = feat {
-              if feature.value() == "menu" {
-                features.push("menu").unwrap();
+              if !cli_managed_features.contains(&feature.value().as_str()) {
+                features.insert(feature.value().to_string());
               }
             }
           }
         }
-        *manifest_features = Value::Array(features.clone());
+        *manifest_features = Value::Array(toml_array(&features));
       }
       Value::String(version) => {
         let mut def = InlineTable::default();
@@ -103,7 +110,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result<Manifest> {
           "version",
           version.to_string().replace("\"", "").replace(" ", ""),
         );
-        def.get_or_insert("features", Value::Array(features.clone()));
+        def.get_or_insert("features", Value::Array(toml_array(&features)));
         *tauri = Value::InlineTable(def);
       }
       _ => {
@@ -113,9 +120,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result<Manifest> {
       }
     }
   } else {
-    return Ok(Manifest {
-      features: features_to_vec(&features),
-    });
+    return Ok(Manifest { features });
   }
 
   let mut manifest_file =
@@ -132,9 +137,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result<Manifest> {
   )?;
   manifest_file.flush()?;
 
-  Ok(Manifest {
-    features: features_to_vec(&features),
-  })
+  Ok(Manifest { features })
 }
 
 pub fn get_workspace_members() -> crate::Result<Vec<String>> {