ソースを参照

feat(core): add argument parsing on shell scope (#44)

* feat: initial implementation

* feat: arg parser

* refactor: move codegen to context struct

* feat: regex validation

* fix tests

* fix: support magic argument value `-`

* feat: support value regex starting with `-`

* refactor: shell_scope mod, add happy path tests

* wip: scope command arguments

* wip: commands

* add better scoped errors and pattern matching

* add documentation to scoped command items

* support using the new shell scope for open

* use the proper items in tauri::scope during codegen

* shell-open uses ScopeError::Validation also

* use shell scoping for sidecar commands

* fix: cli.rs build

* fix: validation when arg list is empty

* require args in a non-fixed, non-empty config list

Co-authored-by: Chip Reed <chip@chip.sh>
chip 3 年 前
コミット
10314cd5cf

+ 1 - 0
core/tauri-codegen/Cargo.toml

@@ -31,3 +31,4 @@ uuid = { version = "0.8", features = [ "v4" ] }
 default = [ "compression" ]
 compression = [ "zstd", "tauri-utils/compression" ]
 isolation = ["tauri-utils/isolation"]
+shell-scope = []

+ 118 - 6
core/tauri-codegen/src/context.rs

@@ -7,11 +7,15 @@ use std::path::{Path, PathBuf};
 
 use proc_macro2::TokenStream;
 use quote::quote;
+use regex::Regex;
 use sha2::{Digest, Sha256};
 
 use tauri_utils::assets::AssetKey;
-use tauri_utils::config::{AppUrl, Config, PatternKind, WindowUrl};
-use tauri_utils::html::{inject_nonce_token, parse as parse_html, NodeRef, PatternObject};
+use tauri_utils::config::{AppUrl, Config, PatternKind, ShellAllowlistOpen, WindowUrl};
+use tauri_utils::html::{inject_nonce_token, parse as parse_html, NodeRef};
+
+#[cfg(feature = "shell-scope")]
+use tauri_utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistScope};
 
 use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError};
 
@@ -47,8 +51,8 @@ fn load_csp(document: &mut NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes)
 fn map_core_assets(
   options: &AssetOptions,
 ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> Result<(), EmbeddedAssetsError> {
-  #[allow(unused_variables)]
-  let pattern = PatternObject::from(&options.pattern);
+  #[cfg(feature = "isolation")]
+  let pattern = tauri_utils::html::PatternObject::from(&options.pattern);
   let csp = options.csp;
   move |key, path, input, csp_hashes| {
     if path.extension() == Some(OsStr::new("html")) {
@@ -58,7 +62,7 @@ fn map_core_assets(
         load_csp(&mut document, key, csp_hashes);
 
         #[cfg(feature = "isolation")]
-        if let PatternObject::Isolation { .. } = &pattern {
+        if let tauri_utils::html::PatternObject::Isolation { .. } = &pattern {
           // create the csp for the isolation iframe styling now, to make the runtime less complex
           let mut hasher = Sha256::new();
           hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
@@ -288,6 +292,35 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     }
   };
 
+  #[cfg(feature = "shell-scope")]
+  let shell_scopes = get_allowed_clis(&root, &config.tauri.allowlist.shell.scope);
+
+  #[cfg(not(feature = "shell-scope"))]
+  let shell_scopes = quote!(::std::collections::HashMap::new());
+
+  let shell_scope_open = match &config.tauri.allowlist.shell.open {
+    ShellAllowlistOpen::Flag(false) => quote!(::std::option::Option::None),
+    ShellAllowlistOpen::Flag(true) => {
+      quote!(::std::option::Option::Some(#root::regex::Regex::new("^https?://").unwrap()))
+    }
+    ShellAllowlistOpen::Validate(regex) => match Regex::new(regex) {
+      Ok(_) => quote!(::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())),
+      Err(error) => {
+        let error = error.to_string();
+        quote!({
+          compile_error!(#error);
+          ::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())
+        })
+      }
+    },
+    _ => panic!("unknown shell open format, unable to prepare"),
+  };
+
+  let shell_scope_config = quote!(#root::ShellScopeConfig {
+    open: #shell_scope_open,
+    scopes: #shell_scopes
+  });
+
   Ok(quote!(#root::Context::new(
     #config,
     ::std::sync::Arc::new(#assets),
@@ -295,7 +328,8 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     #system_tray_icon,
     #package_info,
     #info_plist,
-    #pattern
+    #pattern,
+    #shell_scope_config
   )))
 }
 
@@ -315,3 +349,81 @@ fn find_icon<F: Fn(&&String) -> bool>(
     .unwrap_or_else(|| default.to_string());
   config_parent.join(icon_path).display().to_string()
 }
+
+#[cfg(feature = "shell-scope")]
+fn get_allowed_clis(root: &TokenStream, scope: &ShellAllowlistScope) -> TokenStream {
+  let commands = scope
+    .0
+    .iter()
+    .map(|scope| {
+      let sidecar = &scope.sidecar;
+
+      let name = &scope.name;
+      let name = quote!(#name.into());
+
+      let command = scope.command.to_string_lossy();
+      let command = quote!(::std::path::PathBuf::from(#command));
+
+      let args = match &scope.args {
+        ShellAllowedArgs::Flag(true) => quote!(::std::option::Option::None),
+        ShellAllowedArgs::Flag(false) => quote!(::std::option::Option::Some(::std::vec![])),
+        ShellAllowedArgs::List(list) => {
+          let list = list.iter().map(|arg| match arg {
+            ShellAllowedArg::Fixed(fixed) => {
+              quote!(#root::scope::ShellScopeAllowedArg::Fixed(#fixed.into()))
+            }
+            ShellAllowedArg::Var { name, validate } => {
+              let validate = match validate {
+                None => quote!(::std::option::Option::None),
+                Some(regex) => match regex::Regex::new(regex) {
+                  Ok(regex) => {
+                    let regex = regex.as_str();
+                    quote!(::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap()))
+                  }
+                  Err(error) => {
+                    let error = error.to_string();
+                    quote!({
+                      compile_error!(#error);
+                      ::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())
+                    })
+                  }
+                },
+              };
+
+              quote!(#root::scope::ShellScopeAllowedArg::Var { name: #name.into(), validate: #validate })
+            }
+            _ => panic!("unknown shell scope arg, unable to prepare"),
+          });
+
+          quote!(::std::option::Option::Some(::std::vec![#(#list),*]))
+        }
+        _ => panic!("unknown shell scope command, unable to prepare"),
+      };
+
+      (
+        quote!(#name),
+        quote!(
+          #root::scope::ShellScopeAllowedCommand {
+            command: #command,
+            args: #args,
+            sidecar: #sidecar,
+          }
+        ),
+      )
+    })
+    .collect::<Vec<_>>();
+
+  if commands.is_empty() {
+    quote!(::std::collections::HashMap::new())
+  } else {
+    let insertions = commands
+      .iter()
+      .map(|(name, value)| quote!(hashmap.insert(#name, #value);));
+
+    quote!({
+      let mut hashmap = ::std::collections::HashMap::new();
+      #(#insertions)*
+      hashmap
+    })
+  }
+}

+ 1 - 0
core/tauri-macros/Cargo.toml

@@ -26,3 +26,4 @@ tauri-codegen = { version = "1.0.0-beta.4", default-features = false, path = "..
 custom-protocol = [ ]
 compression = [ "tauri-codegen/compression" ]
 isolation = ["tauri-codegen/isolation"]
+shell-scope = ["tauri-codegen/shell-scope"]

+ 1 - 1
core/tauri-runtime-wry/Cargo.toml

@@ -14,7 +14,7 @@ readme = "README.md"
 
 [dependencies]
 #wry = { version = "0.12", default-features = false, features = [ "file-drop", "protocol" ] }
-wry = { git = "https://github.com/tauri-sec/wry", branch = "next", default-features = false, features = [ "file-drop", "protocol" ] }
+wry = { git = "ssh://git@github.com/tauri-sec/wry", branch = "next", default-features = false, features = [ "file-drop", "protocol" ] }
 tauri-runtime = { version = "0.2.1", path = "../tauri-runtime" }
 tauri-utils = { version = "1.0.0-beta.3", path = "../tauri-utils" }
 uuid = { version = "0.8.2", features = [ "v4" ] }

+ 0 - 1
core/tauri-utils/Cargo.toml

@@ -31,7 +31,6 @@ ring = { version = "0.16", optional = true, features = ["std"] }
 once_cell = { version = "1.8", optional = true }
 serialize-to-javascript = { git = "https://github.com/chippers/serialize-to-javascript" }
 
-
 [target."cfg(target_os = \"linux\")".dependencies]
 heck = "0.4"
 

+ 181 - 7
core/tauri-utils/src/config.rs

@@ -594,6 +594,7 @@ macro_rules! check_feature {
 
 /// Filesystem scope definition.
 /// It is a list of glob patterns that restrict the API access from the webview.
+///
 /// Each pattern can start with a variable that resolves to a system base directory.
 /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
 /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
@@ -842,6 +843,114 @@ impl Allowlist for WindowAllowlistConfig {
   }
 }
 
+/// A command allowed to be executed by the webview API.
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub struct ShellAllowedCommand {
+  /// The name for this allowed shell command configuration.
+  ///
+  /// This name will be used inside of the webview API to call this command along with
+  /// any specified arguments.
+  pub name: String,
+
+  /// The command name.
+  /// It can start with a variable that resolves to a system base directory.
+  /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
+  /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
+  /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$CWD`.
+  #[serde(rename = "cmd")]
+  pub command: PathBuf,
+
+  /// The allowed arguments for the command execution.
+  #[serde(default)]
+  pub args: ShellAllowedArgs,
+
+  /// If this command is a sidecar command.
+  #[serde(default)]
+  pub sidecar: bool,
+}
+
+/// A set of command arguments allowed to be executed by the webview API.
+///
+/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all
+/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to
+/// be passed to the attached command configuration.
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(untagged, deny_unknown_fields)]
+#[non_exhaustive]
+pub enum ShellAllowedArgs {
+  /// Use a simple boolean to allow all or disable all arguments to this command configuration.
+  Flag(bool),
+
+  /// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.
+  List(Vec<ShellAllowedArg>),
+}
+
+impl Default for ShellAllowedArgs {
+  fn default() -> Self {
+    Self::Flag(false)
+  }
+}
+
+/// A command argument allowed to be executed by the webview API.
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(untagged, deny_unknown_fields)]
+#[non_exhaustive]
+pub enum ShellAllowedArg {
+  /// A non-configurable argument that is passed to the command in the order it was specified.
+  Fixed(String),
+
+  /// A variable that is set while calling the command from the webview API.
+  ///
+  Var {
+    /// The name of the variable to be passed in.
+    ///
+    /// This will try to match the key of the passed arguments object from the webview API.
+    name: String,
+
+    /// Optional [regex] validator to require passed values to conform to an expected input.
+    ///
+    /// This will require the argument value passed to this variable to match the `validate` regex
+    /// before it will be executed.
+    ///
+    /// [regex]: https://docs.rs/regex/latest/regex/#syntax
+    #[serde(default)]
+    validate: Option<String>,
+  },
+}
+
+/// Shell scope definition.
+/// It is a list of command names and associated CLI arguments that restrict the API access from the webview.
+#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
+
+/// Defines the `shell > open` api scope.
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(untagged, deny_unknown_fields)]
+#[non_exhaustive]
+pub enum ShellAllowlistOpen {
+  /// If the shell open API should be enabled.
+  ///
+  /// If enabled, the default validation regex (`^https?://`) is used.
+  Flag(bool),
+
+  /// Enable the shell open API, with a custom regex that the opened path must match against.
+  ///
+  /// If using a custom regex to support a non-http(s) schema, care should be used to prevent values
+  /// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.
+  Validate(String),
+}
+
+impl Default for ShellAllowlistOpen {
+  fn default() -> Self {
+    Self::Flag(false)
+  }
+}
+
 /// Allowlist for the shell APIs.
 #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
 #[cfg_attr(feature = "schema", derive(JsonSchema))]
@@ -850,21 +959,21 @@ pub struct ShellAllowlistConfig {
   /// Access scope for the binary execution APIs.
   /// Sidecars are automatically enabled.
   #[serde(default)]
-  pub scope: FsAllowlistScope,
+  pub scope: ShellAllowlistScope,
   /// Use this flag to enable all shell API features.
   #[serde(default)]
   pub all: bool,
   /// Enable binary execution.
   #[serde(default)]
   pub execute: bool,
-  /// Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar program,
+  /// Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar command,
   /// an executable that is shipped with the application.
   /// For more information see <https://tauri.studio/en/docs/usage/guides/bundler/sidecar>.
   #[serde(default)]
   pub sidecar: bool,
   /// Open URL with the user's default application.
   #[serde(default)]
-  pub open: bool,
+  pub open: ShellAllowlistOpen,
 }
 
 impl Allowlist for ShellAllowlistConfig {
@@ -874,7 +983,7 @@ impl Allowlist for ShellAllowlistConfig {
       all: false,
       execute: true,
       sidecar: true,
-      open: true,
+      open: ShellAllowlistOpen::Flag(true),
     };
     let mut features = allowlist.to_features();
     features.push("shell-all");
@@ -888,7 +997,11 @@ impl Allowlist for ShellAllowlistConfig {
       let mut features = Vec::new();
       check_feature!(self, features, execute, "shell-execute");
       check_feature!(self, features, sidecar, "shell-sidecar");
-      check_feature!(self, features, open, "shell-open");
+
+      if !matches!(self.open, ShellAllowlistOpen::Flag(false)) {
+        features.push("shell-open")
+      }
+
       features
     }
   }
@@ -1467,6 +1580,7 @@ pub struct UpdaterConfig {
   /// The updater endpoints. TLS is enforced on production.
   pub endpoints: Option<Vec<UpdaterEndpoint>>,
   /// Signature public key.
+  #[serde(default)] // use default just so the schema doesn't flag it as required
   pub pubkey: String,
 }
 
@@ -1497,7 +1611,7 @@ impl<'de> Deserialize<'de> for UpdaterConfig {
       active: config.active,
       dialog: config.dialog,
       endpoints: config.endpoints,
-      pubkey: config.pubkey.unwrap_or_else(String::new),
+      pubkey: config.pubkey.unwrap_or_default(),
     })
   }
 }
@@ -1782,7 +1896,6 @@ mod build {
   /// Create a `PathBuf` constructor `TokenStream`.
   ///
   /// e.g. `"Hello World" -> String::from("Hello World").
-  /// This takes a `&String` to reduce casting all the `&String` -> `&str` manually.
   fn path_buf_lit(s: impl AsRef<Path>) -> TokenStream {
     let s = s.as_ref().to_string_lossy().into_owned();
     quote! { ::std::path::PathBuf::from(#s) }
@@ -2239,6 +2352,67 @@ mod build {
     }
   }
 
+  impl ToTokens for ShellAllowedCommand {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let name = str_lit(&self.name);
+      let command = path_buf_lit(&self.command);
+      let args = &self.args;
+      let sidecar = &self.sidecar;
+
+      literal_struct!(tokens, ShellAllowedCommand, name, command, args, sidecar);
+    }
+  }
+
+  impl ToTokens for ShellAllowedArgs {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let prefix = quote! { ::tauri::utils::config::ShellAllowedArgs };
+
+      tokens.append_all(match self {
+        Self::Flag(flag) => quote!(#prefix::Flag(#flag)),
+        Self::List(list) => {
+          let list = vec_lit(list, identity);
+          quote!(#prefix::List(#list))
+        }
+      })
+    }
+  }
+
+  impl ToTokens for ShellAllowedArg {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let prefix = quote! { ::tauri::utils::config::ShellAllowedArg };
+
+      tokens.append_all(match self {
+        Self::Fixed(fixed) => {
+          let fixed = str_lit(fixed);
+          quote!(#prefix::Fixed(#fixed))
+        }
+        Self::Var { name, validate } => {
+          let name = str_lit(name);
+          let validate = opt_str_lit(validate.as_ref());
+          quote!(#prefix::Var { name: #name, validate: #validate })
+        }
+      })
+    }
+  }
+
+  impl ToTokens for ShellAllowlistOpen {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let prefix = quote! { ::tauri::utils::config::ShellAllowlistOpen };
+
+      tokens.append_all(match self {
+        Self::Flag(flag) => quote!(#prefix::Flag(#flag)),
+        Self::Validate(regex) => quote!(#prefix::Validate(#regex)),
+      })
+    }
+  }
+
+  impl ToTokens for ShellAllowlistScope {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let allowed_commands = vec_lit(&self.0, identity);
+      tokens.append_all(quote! { ::tauri::utils::config::ShellAllowlistScope(#allowed_commands) })
+    }
+  }
+
   impl ToTokens for ShellAllowlistConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let scope = &self.scope;

+ 1 - 1
core/tauri/Cargo.toml

@@ -175,7 +175,7 @@ process-relaunch = []
 protocol-all = ["protocol-asset"]
 protocol-asset = []
 shell-all = ["shell-execute", "shell-sidecar", "shell-open"]
-shell-execute = ["command"]
+shell-execute = ["command", "clap", "tauri-macros/shell-scope"]
 shell-sidecar = ["command"]
 shell-open = ["open"]
 window-all = [

ファイルの差分が大きいため隠しています
+ 0 - 0
core/tauri/scripts/bundle.js


+ 2 - 10
core/tauri/src/api/process/command.rs

@@ -150,18 +150,10 @@ pub struct Output {
   pub stderr: String,
 }
 
-#[cfg(not(windows))]
 fn relative_command_path(command: String) -> crate::Result<String> {
+  let ext = if cfg!(windows) { ".exe" } else { "" };
   match platform::current_exe()?.parent() {
-    Some(exe_dir) => Ok(format!("{}/{}", exe_dir.to_string_lossy(), command)),
-    None => Err(crate::api::Error::Command("Could not evaluate executable dir".to_string()).into()),
-  }
-}
-
-#[cfg(windows)]
-fn relative_command_path(command: String) -> crate::Result<String> {
-  match platform::current_exe()?.parent() {
-    Some(exe_dir) => Ok(format!("{}/{}.exe", exe_dir.to_string_lossy(), command)),
+    Some(exe_dir) => Ok(format!("{}/{}{}", exe_dir.display(), command, ext)),
     None => Err(crate::api::Error::Command("Could not evaluate executable dir".to_string()).into()),
   }
 }

+ 9 - 10
core/tauri/src/api/shell.rs

@@ -4,6 +4,7 @@
 
 //! Types and functions related to shell.
 
+use crate::ShellScope;
 use std::str::FromStr;
 
 /// Program to use on the [`open()`] call.
@@ -55,7 +56,7 @@ impl FromStr for Program {
 }
 
 impl Program {
-  fn name(self) -> &'static str {
+  pub(crate) fn name(self) -> &'static str {
     match self {
       Self::Open => "open",
       Self::Start => "start",
@@ -89,13 +90,11 @@ impl Program {
 }
 
 /// Opens path or URL with program specified in `with`, or system default if `None`.
-pub fn open(path: String, with: Option<Program>) -> crate::api::Result<()> {
-  {
-    let exit_status = if let Some(with) = with {
-      open::with(&path, with.name())
-    } else {
-      open::that(&path)
-    };
-    exit_status.map_err(|err| crate::api::Error::Shell(format!("failed to open: {}", err)))
-  }
+///
+/// The path will be matched against the shell open validation regex, defaulting to `^https?://`.
+/// A custom validation regex may be supplied in the config in `tauri > allowlist > scope > open`.
+pub fn open(scope: &ShellScope, path: String, with: Option<Program>) -> crate::api::Result<()> {
+  scope
+    .open(&path, with)
+    .map_err(|err| crate::api::Error::Shell(format!("failed to open: {}", err)))
 }

+ 7 - 12
core/tauri/src/app.rs

@@ -26,6 +26,8 @@ use crate::{
   Context, Invoke, InvokeError, InvokeResponse, Manager, Scopes, StateManager, Window,
 };
 
+use crate::scope::ShellScope;
+
 use tauri_macros::default_runtime;
 use tauri_utils::PackageInfo;
 
@@ -460,8 +462,7 @@ macro_rules! shared_app_impl {
       }
 
       /// Gets the scope for the shell execute APIs.
-      #[cfg(shell_execute)]
-      pub fn shell_scope(&self) -> FsScope {
+      pub fn shell_scope(&self) -> ShellScope {
         self.state::<Scopes>().inner().shell.clone()
       }
     }
@@ -708,6 +709,7 @@ impl<R: Runtime> Builder<R> {
   ///
   /// The `initialization_script` is a script that initializes `window.__TAURI_POST_MESSAGE__`.
   /// That function must take the `message: object` argument and send it to the backend.
+  #[must_use]
   pub fn invoke_system<F>(mut self, initialization_script: String, responder: F) -> Self
   where
     F: Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
@@ -976,6 +978,8 @@ impl<R: Runtime> Builder<R> {
       .map(|t| t.icon_as_template)
       .unwrap_or_default();
 
+    let shell_scope = context.shell_scope.clone();
+
     let manager = WindowManager::with_handlers(
       context,
       self.plugins,
@@ -1035,7 +1039,6 @@ impl<R: Runtime> Builder<R> {
         app.package_info(),
         &env,
         &app.config().tauri.allowlist.fs.scope,
-        true,
       ),
       #[cfg(protocol_asset)]
       asset_protocol: FsScope::for_fs_api(
@@ -1043,18 +1046,10 @@ impl<R: Runtime> Builder<R> {
         app.package_info(),
         &env,
         &app.config().tauri.allowlist.protocol.asset_scope,
-        true,
       ),
       #[cfg(http_request)]
       http: crate::scope::HttpScope::for_http_api(&app.config().tauri.allowlist.http.scope),
-      #[cfg(shell_execute)]
-      shell: FsScope::for_fs_api(
-        &app.manager.config(),
-        app.package_info(),
-        &env,
-        &app.config().tauri.allowlist.shell.scope,
-        false,
-      ),
+      shell: ShellScope::new(shell_scope),
     });
     app.manage(env);
 

+ 46 - 29
core/tauri/src/endpoints/shell.rs

@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: MIT
 
 use super::InvokeContext;
-use crate::{api::ipc::CallbackFn, Runtime};
+use crate::{api::ipc::CallbackFn, ExecuteArgs, Runtime};
 #[cfg(shell_execute)]
 use crate::{Manager, Scopes};
 use serde::Deserialize;
@@ -56,8 +56,8 @@ pub enum Cmd {
   /// The execute script API.
   #[serde(rename_all = "camelCase")]
   Execute {
-    program: PathBuf,
-    args: Vec<String>,
+    program: String,
+    args: ExecuteArgs,
     on_event_fn: CallbackFn,
     #[serde(default)]
     options: CommandOptions,
@@ -79,8 +79,8 @@ impl Cmd {
   #[allow(unused_variables)]
   fn execute<R: Runtime>(
     context: InvokeContext<R>,
-    program: PathBuf,
-    args: Vec<String>,
+    program: String,
+    args: ExecuteArgs,
     on_event_fn: CallbackFn,
     options: CommandOptions,
   ) -> crate::Result<ChildId> {
@@ -91,6 +91,7 @@ impl Cmd {
       ));
       #[cfg(shell_sidecar)]
       {
+        let program = PathBuf::from(program);
         let program_as_string = program.display().to_string();
         let program_no_ext_as_string = program.with_extension("").display().to_string();
         let is_configured = context
@@ -106,7 +107,12 @@ impl Cmd {
           })
           .unwrap_or_default();
         if is_configured {
-          crate::api::process::Command::new_sidecar(program_as_string)?
+          context
+            .window
+            .state::<Scopes>()
+            .shell
+            .prepare(&program.to_string_lossy(), args, true)
+            .map_err(Box::new)?
         } else {
           return Err(crate::Error::SidecarNotAllowed(program));
         }
@@ -117,15 +123,18 @@ impl Cmd {
         "shell > execute".to_string(),
       ));
       #[cfg(shell_execute)]
-      if context.window.state::<Scopes>().shell.is_allowed(&program) {
-        crate::api::process::Command::new(program.display().to_string())
-      } else {
-        return Err(crate::Error::ProgramNotAllowed(program));
+      match context
+        .window
+        .state::<Scopes>()
+        .shell
+        .prepare(&program, args, false)
+      {
+        Ok(cmd) => cmd,
+        Err(_) => return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program))),
       }
     };
     #[cfg(any(shell_execute, shell_sidecar))]
     {
-      command = command.args(args);
       if let Some(cwd) = options.cwd {
         command = command.current_dir(cwd);
       }
@@ -196,33 +205,35 @@ impl Cmd {
     ))
   }
 
+  /// Open a (url) path with a default or specific browser opening program.
+  ///
+  /// See [`crate::api::shell::open`] for how it handles security-related measures.
   #[module_command_handler(shell_open, "shell > open")]
   fn open<R: Runtime>(
-    _context: InvokeContext<R>,
+    context: InvokeContext<R>,
     path: String,
     with: Option<String>,
   ) -> crate::Result<()> {
-    match crate::api::shell::open(
-      path,
-      if let Some(w) = with {
-        use std::str::FromStr;
-        Some(crate::api::shell::Program::from_str(&w)?)
-      } else {
-        None
-      },
-    ) {
-      Ok(_) => Ok(()),
-      Err(err) => Err(crate::Error::FailedToExecuteApi(err)),
-    }
+    use std::str::FromStr;
+
+    with
+      .as_deref()
+      // only allow pre-determined programs to be specified
+      .map(crate::api::shell::Program::from_str)
+      .transpose()
+      .map_err(Into::into)
+      // validate and open path
+      .and_then(|with| {
+        crate::api::shell::open(&context.window.state::<Scopes>().shell, path, with)
+          .map_err(Into::into)
+      })
   }
 }
 
 #[cfg(test)]
 mod tests {
   use super::{Buffer, ChildId, CommandOptions};
-  use crate::api::ipc::CallbackFn;
-  use std::path::PathBuf;
-
+  use crate::{api::ipc::CallbackFn, ExecuteArgs};
   use quickcheck::{Arbitrary, Gen};
 
   impl Arbitrary for CommandOptions {
@@ -241,11 +252,17 @@ mod tests {
     }
   }
 
+  impl Arbitrary for ExecuteArgs {
+    fn arbitrary(_: &mut Gen) -> Self {
+      ExecuteArgs::None
+    }
+  }
+
   #[tauri_macros::module_command_test(shell_execute, "shell > execute")]
   #[quickcheck_macros::quickcheck]
   fn execute(
-    _program: PathBuf,
-    _args: Vec<String>,
+    _program: String,
+    _args: ExecuteArgs,
     _on_event_fn: CallbackFn,
     _options: CommandOptions,
   ) {

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

@@ -93,6 +93,9 @@ pub enum Error {
   /// Sidecar not allowed by the configuration.
   #[error("sidecar not configured under `tauri.conf.json > tauri > bundle > externalBin`: {0}")]
   SidecarNotAllowed(PathBuf),
+  /// Sidecar was not found by the configuration.
+  #[error("sidecar configuration found, but unable to create a path to it: {0}")]
+  SidecarNotFound(#[from] Box<crate::ShellScopeError>),
   /// Program not allowed by the scope.
   #[error("program not allowed on the configured shell scope: {0}")]
   ProgramNotAllowed(PathBuf),

+ 19 - 1
core/tauri/src/lib.rs

@@ -128,11 +128,17 @@
 #![warn(missing_docs, rust_2018_idioms)]
 #![cfg_attr(doc_cfg, feature(doc_cfg))]
 
+#[cfg(feature = "shell-execute")]
+#[doc(hidden)]
+pub use clap;
 #[cfg(target_os = "macos")]
 #[doc(hidden)]
 pub use embed_plist;
 /// The Tauri error enum.
 pub use error::Error;
+#[cfg(feature = "shell-execute")]
+#[doc(hidden)]
+pub use regex;
 pub use tauri_macros::{command, generate_handler};
 
 pub mod api;
@@ -149,7 +155,8 @@ mod pattern;
 pub mod plugin;
 pub mod window;
 pub use tauri_runtime as runtime;
-mod scope;
+/// The allowlist scopes.
+pub mod scope;
 pub mod settings;
 mod state;
 #[cfg(feature = "updater")]
@@ -272,6 +279,7 @@ pub struct Context<A: Assets> {
   pub(crate) package_info: PackageInfo,
   pub(crate) _info_plist: (),
   pub(crate) pattern: Pattern,
+  pub(crate) shell_scope: ShellScopeConfig,
 }
 
 impl<A: Assets> fmt::Debug for Context<A> {
@@ -282,6 +290,7 @@ impl<A: Assets> fmt::Debug for Context<A> {
       .field("system_tray_icon", &self.system_tray_icon)
       .field("package_info", &self.package_info)
       .field("pattern", &self.pattern)
+      .field("shell_scope", &self.shell_scope)
       .finish()
   }
 }
@@ -353,8 +362,15 @@ impl<A: Assets> Context<A> {
     &self.pattern
   }
 
+  /// The scoped shell commands, where the `HashMap` key is the name each configuration.
+  #[inline(always)]
+  pub fn allowed_commands(&self) -> &ShellScopeConfig {
+    &self.shell_scope
+  }
+
   /// Create a new [`Context`] from the minimal required items.
   #[inline(always)]
+  #[allow(clippy::too_many_arguments)]
   pub fn new(
     config: Config,
     assets: Arc<A>,
@@ -363,6 +379,7 @@ impl<A: Assets> Context<A> {
     package_info: PackageInfo,
     info_plist: (),
     pattern: Pattern,
+    shell_scope: ShellScopeConfig,
   ) -> Self {
     Self {
       config,
@@ -372,6 +389,7 @@ impl<A: Assets> Context<A> {
       package_info,
       _info_plist: info_plist,
       pattern,
+      shell_scope,
     }
   }
 }

+ 1 - 1
core/tauri/src/manager.rs

@@ -719,7 +719,7 @@ impl<R: Runtime> WindowManager<R> {
               &mut asset,
               self.inner.assets.clone(),
               &asset_path,
-              &self,
+              self,
               csp,
             ));
           }

+ 2 - 8
core/tauri/src/scope/fs.rs

@@ -16,7 +16,6 @@ use crate::api::path::parse as parse_path;
 #[derive(Clone)]
 pub struct Scope {
   allow_patterns: Vec<Pattern>,
-  is_fs_path: bool,
 }
 
 impl fmt::Debug for Scope {
@@ -30,7 +29,6 @@ impl fmt::Debug for Scope {
           .map(|p| p.as_str())
           .collect::<Vec<&str>>(),
       )
-      .field("is_fs_path", &self.is_fs_path)
       .finish()
   }
 }
@@ -42,7 +40,6 @@ impl Scope {
     package_info: &PackageInfo,
     env: &Env,
     scope: &FsAllowlistScope,
-    is_fs_path: bool,
   ) -> Self {
     let mut allow_patterns = Vec::new();
     for path in &scope.0 {
@@ -56,16 +53,13 @@ impl Scope {
         }
       }
     }
-    Self {
-      allow_patterns,
-      is_fs_path,
-    }
+    Self { allow_patterns }
   }
 
   /// Determines if the given path is allowed on this scope.
   pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
     let path = path.as_ref();
-    let path = if !path.exists() || !self.is_fs_path {
+    let path = if !path.exists() {
       crate::Result::Ok(path.to_path_buf())
     } else {
       std::fs::canonicalize(path).map_err(Into::into)

+ 112 - 2
core/tauri/src/scope/mod.rs

@@ -4,9 +4,120 @@
 
 mod fs;
 mod http;
+mod shell;
 
 pub use self::http::Scope as HttpScope;
 pub use fs::Scope as FsScope;
+use regex::Regex;
+pub use shell::{Scope as ShellScope, ScopeError as ShellScopeError};
+
+use std::collections::HashMap;
+
+/// Allowed representation of `Execute` command arguments.
+#[derive(Debug, Clone, serde::Deserialize)]
+#[serde(untagged, deny_unknown_fields)]
+#[non_exhaustive]
+pub enum ExecuteArgs {
+  /// No arguments
+  None,
+
+  /// A single string argument
+  Single(String),
+
+  /// Multiple string arguments
+  List(Vec<String>),
+
+  /// Multiple string arguments in a key-value fashion
+  Map(HashMap<String, String>),
+}
+
+impl ExecuteArgs {
+  /// Whether the argument list is empty or not.
+  pub fn is_empty(&self) -> bool {
+    match self {
+      Self::None => true,
+      Self::Single(s) if s == "" => true,
+      Self::List(l) => l.is_empty(),
+      Self::Map(m) => m.is_empty(),
+      _ => false,
+    }
+  }
+}
+
+impl From<()> for ExecuteArgs {
+  fn from(_: ()) -> Self {
+    Self::None
+  }
+}
+
+impl From<String> for ExecuteArgs {
+  fn from(string: String) -> Self {
+    Self::Single(string)
+  }
+}
+
+impl From<Vec<String>> for ExecuteArgs {
+  fn from(vec: Vec<String>) -> Self {
+    Self::List(vec)
+  }
+}
+
+impl From<HashMap<String, String>> for ExecuteArgs {
+  fn from(map: HashMap<String, String>) -> Self {
+    Self::Map(map)
+  }
+}
+
+/// Shell scope configuration.
+#[derive(Debug, Clone)]
+pub struct ShellScopeConfig {
+  /// The validation regex that `shell > open` paths must match against.
+  pub open: Option<Regex>,
+
+  /// All allowed commands, using their unique command name as the keys.
+  pub scopes: HashMap<String, ShellScopeAllowedCommand>,
+}
+
+/// A configured scoped shell command.
+#[derive(Debug, Clone)]
+pub struct ShellScopeAllowedCommand {
+  /// The shell command to be called.
+  pub command: std::path::PathBuf,
+
+  /// The arguments the command is allowed to be called with.
+  pub args: Option<Vec<ShellScopeAllowedArg>>,
+
+  /// If this command is a sidecar command.
+  pub sidecar: bool,
+}
+
+/// A configured argument to a scoped shell command.
+#[derive(Debug, Clone)]
+pub enum ShellScopeAllowedArg {
+  /// A non-configurable argument.
+  Fixed(String),
+
+  /// An argument with a value to be evaluated at runtime, optionally must pass a regex validation.
+  Var {
+    /// The key name of the argument variable
+    name: String,
+
+    /// The validation, if set, that the variable value must pass in order to be called.
+    validate: Option<regex::Regex>,
+  },
+}
+
+impl ShellScopeAllowedArg {
+  /// If the argument is fixed.
+  pub fn is_fixed(&self) -> bool {
+    matches!(self, Self::Fixed(_))
+  }
+
+  /// If the argument is a variable value.
+  pub fn is_var(&self) -> bool {
+    matches!(self, Self::Var { .. })
+  }
+}
 
 pub(crate) struct Scopes {
   pub fs: FsScope,
@@ -14,6 +125,5 @@ pub(crate) struct Scopes {
   pub asset_protocol: FsScope,
   #[cfg(http_request)]
   pub http: HttpScope,
-  #[cfg(shell_execute)]
-  pub shell: FsScope,
+  pub shell: ShellScope,
 }

+ 194 - 0
core/tauri/src/scope/shell.rs

@@ -0,0 +1,194 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#[cfg(shell_open)]
+use crate::api::shell::Program;
+use crate::ShellScopeConfig;
+#[cfg(any(shell_execute, shell_sidecar))]
+use crate::{api::process::Command, ExecuteArgs, ShellScopeAllowedArg};
+
+/// Scope for filesystem access.
+#[derive(Clone)]
+pub struct Scope(ShellScopeConfig);
+
+/// All errors that can happen while validating a scoped command.
+#[derive(Debug, thiserror::Error)]
+pub enum ScopeError {
+  /// At least one argument did not pass input validation.
+  #[cfg(any(shell_execute, shell_sidecar))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
+  )]
+  #[error("The scoped command was called with the improper sidecar flag set")]
+  BadSidecarFlag,
+
+  /// The sidecar program validated but failed to find the sidecar path.
+  ///
+  /// Note: This can be called on `shell-execute` feature too due to [`Scope::prepare`] checking if
+  /// it's a sidecar from the config.
+  #[cfg(any(shell_execute, shell_sidecar))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
+  )]
+  #[error(
+    "The scoped sidecar command was validated, but failed to create the path to the command: {0}"
+  )]
+  Sidecar(crate::Error),
+
+  /// The named command was not found in the scoped config.
+  #[error("Scoped command {0} not found")]
+  #[cfg(any(shell_execute, shell_sidecar))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
+  )]
+  NotFound(String),
+
+  /// A command variable has no value set in the arguments.
+  #[error("Scoped command argument {0} was not found")]
+  #[cfg(any(shell_execute, shell_sidecar))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
+  )]
+  MissingVar(String),
+
+  /// At least one argument did not pass input validation.
+  #[cfg(any(shell_execute, shell_open))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-open")))
+  )]
+  #[error("Scoped command argument {var} was found, but failed regex validation {validation}")]
+  Validation {
+    /// Name of the variable.
+    var: String,
+
+    /// Regex that the variable value failed to match.
+    validation: String,
+  },
+
+  /// The format of the passed input does not match the expected shape.
+  ///
+  /// This can happen from passing a string or array of strings to a command that is expecting
+  /// named variables, and vice-versa.
+  #[cfg(any(shell_execute, shell_sidecar))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
+  )]
+  #[error("Scoped command {0} received arguments in an unexpected format")]
+  InvalidInput(String),
+
+  /// A generic IO error that occurs while executing specified shell commands.
+  #[cfg(any(shell_execute, shell_sidecar))]
+  #[cfg_attr(
+    doc_cfg,
+    doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
+  )]
+  #[error("Scoped shell IO error: {0}")]
+  Io(#[from] std::io::Error),
+}
+
+impl Scope {
+  /// Creates a new shell scope.
+  pub fn new(scope: ShellScopeConfig) -> Self {
+    Self(scope)
+  }
+
+  /// Validates argument inputs and creates a Tauri [`Command`].
+  #[cfg(any(shell_execute, shell_sidecar))]
+  pub fn prepare(
+    &self,
+    command_name: &str,
+    args: ExecuteArgs,
+    sidecar: bool,
+  ) -> Result<Command, ScopeError> {
+    let command = match self.0.scopes.get(command_name) {
+      Some(command) => command,
+      None => return Err(ScopeError::NotFound(command_name.into())),
+    };
+
+    if command.sidecar != sidecar {
+      return Err(ScopeError::BadSidecarFlag);
+    }
+
+    let args = match (&command.args, args) {
+      (None, ExecuteArgs::None) => Ok(vec![]),
+      (None, ExecuteArgs::List(list)) => Ok(list),
+      (None, ExecuteArgs::Single(string)) => Ok(vec![string]),
+      (None, _) => Err(ScopeError::InvalidInput(command_name.into())),
+      (Some(list), ExecuteArgs::Map(args)) => list
+        .iter()
+        .map(|arg| match arg {
+          ShellScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
+          ShellScopeAllowedArg::Var { name, validate } => {
+            let value = args
+              .get(name)
+              .ok_or_else(|| ScopeError::MissingVar(name.into()))?
+              .to_string();
+            match validate {
+              None => Ok(value),
+              Some(regex) => {
+                if regex.is_match(&value) {
+                  Ok(value)
+                } else {
+                  Err(ScopeError::Validation {
+                    var: name.into(),
+                    validation: regex.as_str().into(),
+                  })
+                }
+              }
+            }
+          }
+        })
+        .collect(),
+      (Some(list), arg) if arg.is_empty() && list.iter().all(ShellScopeAllowedArg::is_fixed) => list
+        .iter()
+        .map(|arg| match arg {
+          ShellScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
+          _ => unreachable!(),
+        })
+        .collect(),
+      (Some(list), _) if list.is_empty() => Err(ScopeError::InvalidInput(command_name.into())),
+      (Some(_), _) => Err(ScopeError::InvalidInput(command_name.into())),
+    }?;
+
+    let command_s = command.command.to_string_lossy();
+    let command = if command.sidecar {
+      Command::new_sidecar(command_s).map_err(ScopeError::Sidecar)?
+    } else {
+      Command::new(command_s)
+    };
+
+    Ok(command.args(args))
+  }
+
+  /// Open a path in the default (or specified) browser.
+  ///
+  /// The path is validated against the `tauri > allowlist > shell > open` validation regex, which
+  /// defaults to `^https?://`.
+  #[cfg(shell_open)]
+  pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), ScopeError> {
+    // ensure we pass validation if the configuration has one
+    if let Some(regex) = &self.0.open {
+      if !regex.is_match(path) {
+        return Err(ScopeError::Validation {
+          var: "open".into(),
+          validation: regex.as_str().into(),
+        });
+      }
+    }
+
+    // The prevention of argument escaping is handled by the usage of std::process::Command::arg by
+    // the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.
+    match with.map(Program::name) {
+      Some(program) => ::open::with(&path, program),
+      None => ::open::that(&path),
+    }
+    .map_err(Into::into)
+  }
+}

+ 8 - 3
core/tauri/src/test/mod.rs

@@ -7,9 +7,10 @@
 mod mock_runtime;
 pub use mock_runtime::*;
 
+use std::collections::HashMap;
 use std::{borrow::Cow, sync::Arc};
 
-use crate::{Manager, Pattern};
+use crate::{Manager, Pattern, ShellScopeConfig};
 use tauri_utils::{
   assets::{AssetKey, Assets, CspHash},
   config::{CliConfig, Config, PatternKind, TauriConfig},
@@ -66,11 +67,15 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
     package_info: crate::PackageInfo {
       name: "test".into(),
       version: "0.1.0".into(),
-      authors: "Tauri".into(),
-      description: "Tauri test".into(),
+      authors: "Tauri",
+      description: "Tauri test",
     },
     _info_plist: (),
     pattern: Pattern::Brownfield(std::marker::PhantomData),
+    shell_scope: ShellScopeConfig {
+      open: None,
+      scopes: HashMap::new(),
+    },
   }
 }
 

+ 164 - 33
core/tauri/tests/restart/Cargo.lock

@@ -1027,7 +1027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518"
 dependencies = [
  "anyhow",
- "heck",
+ "heck 0.3.3",
  "proc-macro-crate 1.1.0",
  "proc-macro-error",
  "proc-macro2",
@@ -1144,7 +1144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79"
 dependencies = [
  "anyhow",
- "heck",
+ "heck 0.3.3",
  "proc-macro-crate 1.1.0",
  "proc-macro-error",
  "proc-macro2",
@@ -1161,6 +1161,12 @@ dependencies = [
  "unicode-segmentation",
 ]
 
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.19"
@@ -2429,7 +2435,7 @@ version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
 dependencies = [
- "heck",
+ "heck 0.3.3",
  "proc-macro2",
  "quote",
  "syn",
@@ -2441,7 +2447,7 @@ version = "0.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
 dependencies = [
- "heck",
+ "heck 0.3.3",
  "proc-macro2",
  "quote",
  "syn",
@@ -2464,7 +2470,7 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b"
 dependencies = [
- "heck",
+ "heck 0.3.3",
  "pkg-config",
  "strum 0.18.0",
  "strum_macros 0.18.0",
@@ -2481,7 +2487,7 @@ checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6"
 dependencies = [
  "anyhow",
  "cfg-expr 0.8.1",
- "heck",
+ "heck 0.3.3",
  "itertools",
  "pkg-config",
  "strum 0.21.0",
@@ -2498,7 +2504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e"
 dependencies = [
  "cfg-expr 0.9.0",
- "heck",
+ "heck 0.3.3",
  "pkg-config",
  "toml",
  "version-compare 0.0.11",
@@ -2538,7 +2544,7 @@ dependencies = [
  "scopeguard",
  "serde",
  "unicode-segmentation",
- "windows",
+ "windows 0.25.0",
  "x11-dl",
 ]
 
@@ -2574,7 +2580,7 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "rand 0.8.4",
- "raw-window-handle 0.3.4",
+ "raw-window-handle 0.4.2",
  "regex",
  "semver 1.0.4",
  "serde",
@@ -2618,7 +2624,7 @@ dependencies = [
 name = "tauri-macros"
 version = "1.0.0-beta.5"
 dependencies = [
- "heck",
+ "heck 0.3.3",
  "proc-macro2",
  "quote",
  "syn",
@@ -2638,7 +2644,8 @@ dependencies = [
  "tauri-utils",
  "thiserror",
  "uuid",
- "webview2-com",
+ "webview2-com 0.9.0",
+ "windows 0.29.0",
 ]
 
 [[package]]
@@ -2652,8 +2659,8 @@ dependencies = [
  "tauri-runtime",
  "tauri-utils",
  "uuid",
- "webview2-com",
- "windows",
+ "webview2-com 0.9.0",
+ "windows 0.29.0",
  "wry",
 ]
 
@@ -2662,7 +2669,7 @@ name = "tauri-utils"
 version = "1.0.0-beta.3"
 dependencies = [
  "base64",
- "heck",
+ "heck 0.4.0",
  "html5ever",
  "kuchiki",
  "phf 0.10.1",
@@ -3011,9 +3018,21 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "abdc9ca7cebd96a1005d5ba1e9d70c61c0f6c276a41cddaeecb7842d436ab3bc"
 dependencies = [
- "webview2-com-macros",
- "webview2-com-sys",
- "windows",
+ "webview2-com-macros 0.4.0",
+ "webview2-com-sys 0.7.0",
+ "windows 0.25.0",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b0f21eed16a0078ef52de94d15d6e3a22f9998cf45bdabaf9ef4a235ae235ac"
+dependencies = [
+ "webview2-com-macros 0.5.0",
+ "webview2-com-sys 0.9.0",
+ "windows 0.29.0",
+ "windows_macros 0.29.0",
 ]
 
 [[package]]
@@ -3027,6 +3046,17 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "webview2-com-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515c6c82fcee93f6edaacc72c8e233dbe4ff3ca569dce1901dfc36c404a3e99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "webview2-com-sys"
 version = "0.7.0"
@@ -3037,7 +3067,21 @@ dependencies = [
  "serde",
  "serde_json",
  "thiserror",
- "windows",
+ "windows 0.25.0",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56fe9356e3729233bed63e7c002c0f5064f5d2148f169ce77eec8932a98c4c0"
+dependencies = [
+ "regex",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "windows 0.29.0",
+ "windows-bindgen",
 ]
 
 [[package]]
@@ -3077,14 +3121,37 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e46c474738425c090573ecf5472d54ee5f78132e6195d0bbfcc2aabc0ed29f37"
 dependencies = [
- "windows_aarch64_msvc",
- "windows_gen",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_macros",
- "windows_reader",
- "windows_x86_64_gnu",
- "windows_x86_64_msvc",
+ "windows_aarch64_msvc 0.25.0",
+ "windows_gen 0.25.0",
+ "windows_i686_gnu 0.25.0",
+ "windows_i686_msvc 0.25.0",
+ "windows_macros 0.25.0",
+ "windows_reader 0.25.0",
+ "windows_x86_64_gnu 0.25.0",
+ "windows_x86_64_msvc 0.25.0",
+]
+
+[[package]]
+name = "windows"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac7fef12f4b59cd0a29339406cc9203ab44e440ddff6b3f5a41455349fa9cf3"
+dependencies = [
+ "windows_aarch64_msvc 0.29.0",
+ "windows_i686_gnu 0.29.0",
+ "windows_i686_msvc 0.29.0",
+ "windows_x86_64_gnu 0.29.0",
+ "windows_x86_64_msvc 0.29.0",
+]
+
+[[package]]
+name = "windows-bindgen"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b01138bf46333583966ea4b86fd4f61a9b524c0f5f88bc3c18768d6b66cb6c4e"
+dependencies = [
+ "windows_quote 0.29.0",
+ "windows_reader 0.29.0",
 ]
 
 [[package]]
@@ -3093,14 +3160,30 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3022d174000fcaeb6f95933fb04171ea0e21b9289ac57fe4400bfa148e41df79"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d027175d00b01e0cbeb97d6ab6ebe03b12330a35786cbaca5252b1c4bf5d9b"
+
 [[package]]
 name = "windows_gen"
 version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "54e0f0e40e950724f92de0f714817c7030a88161738b9b1c58d62c817246fe1c"
 dependencies = [
- "windows_quote",
- "windows_reader",
+ "windows_quote 0.25.0",
+ "windows_reader 0.25.0",
+]
+
+[[package]]
+name = "windows_gen"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e59eb69ef41a029911bb604a850f70ec1f58c8587511bc10ed84a3465931df0b"
+dependencies = [
+ "windows_quote 0.29.0",
+ "windows_reader 0.29.0",
 ]
 
 [[package]]
@@ -3109,12 +3192,24 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03b1584eebf06654708eab4301152032c13c1e47f4a754ffc93c733f10993e85"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8793f59f7b8e8b01eda1a652b2697d87b93097198ae85f823b969ca5b89bba58"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f49df16591e9ad429997ec57d462b0cc45168f639d03489e8c2e933ea9c389d7"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8602f6c418b67024be2996c512f5f995de3ba417f4c75af68401ab8756796ae4"
+
 [[package]]
 name = "windows_macros"
 version = "0.25.0"
@@ -3122,9 +3217,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6103bcf1a7396d66f6f08a2d67d8a2ab34efaf4b1d7567301af2c002507c8c3b"
 dependencies = [
  "syn",
- "windows_gen",
- "windows_quote",
- "windows_reader",
+ "windows_gen 0.25.0",
+ "windows_quote 0.25.0",
+ "windows_reader 0.25.0",
+]
+
+[[package]]
+name = "windows_macros"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f6443f71f760ce91f4cc7fc81ee78f680dccb8ec110c52a92ec857a7def39a3"
+dependencies = [
+ "syn",
+ "windows_gen 0.29.0",
+ "windows_quote 0.29.0",
+ "windows_reader 0.29.0",
 ]
 
 [[package]]
@@ -3133,24 +3240,48 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e414df8d5dd2013f2317fdc414d3ad035effcb7aef1f16bf508ac5743154835a"
 
+[[package]]
+name = "windows_quote"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd83f20d7c391dc3b115a7e8a0851b71d0a9c356be2791571e858063b5f823c"
+
 [[package]]
 name = "windows_reader"
 version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8132c9fb77903d852ea20053af816bd15c088a6e8d283b8283e80353347bb6b9"
 
+[[package]]
+name = "windows_reader"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87b34c04457bad3c5436ffe1ed262c908228bb634e3bda34f4ce2c252495787"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2cb06177184100374f97d5e7261ee0b6adefa8ee32e38f87518ca22b519bb80e"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d615f419543e0bd7d2b3323af0d86ff19cbc4f816e6453f36a2c2ce889c354"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3c27bcbb33ddbed3569e36c14775c99f72b97c72ce49f81d128637fb48a061f"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d95421d9ed3672c280884da53201a5c46b7b2765ca6faf34b0d71cf34a3561"
+
 [[package]]
 name = "wry"
 version = "0.12.2"
@@ -3175,8 +3306,8 @@ dependencies = [
  "url",
  "webkit2gtk",
  "webkit2gtk-sys",
- "webview2-com",
- "windows",
+ "webview2-com 0.7.0",
+ "windows 0.25.0",
 ]
 
 [[package]]

+ 18 - 1
examples/api/src-tauri/tauri.conf.json

@@ -78,7 +78,24 @@
         "scope": ["$APP/db", "$DOWNLOAD/**", "$RESOURCE/**"]
       },
       "shell": {
-        "scope": ["sh", "cmd"]
+        "scope": [
+          {
+            "cmd": "__test",
+            "args": [
+              "-d --date <DATE> ^\\d{4}-\\d{2}-\\d{2}$",
+              "-i --input [INPUT]",
+              "-f --flag",
+              "-v --verbose ...",
+              "<MAGIC> -"
+            ]
+          },
+          {
+            "cmd": "sh"
+          },
+          {
+            "cmd": "cmd"
+          }
+        ]
       },
       "protocol": {
         "asset": true,

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

@@ -6,7 +6,7 @@ edition = "2021"
 rust-version = "1.57"
 
 [build-dependencies]
-tauri-build = { path = "../../../core/tauri-build", features = [ "codegen" ] }
+tauri-build = { path = "../../../core/tauri-build", features = ["codegen"] }
 
 [dependencies]
 serde_json = "1.0"

+ 3 - 4
tooling/api/src/shell.ts

@@ -56,8 +56,7 @@ interface ChildProcess {
  * Spawns a process.
  *
  * @ignore
- * @param program The name of the program to execute e.g. 'mkdir' or 'node'.
- * @param sidecar Whether the program is a sidecar or a system program.
+ * @param program The name of the scoped command.
  * @param onEvent Event handler.
  * @param args Program arguments.
  * @param options Configuration for the process spawn.
@@ -66,7 +65,7 @@ interface ChildProcess {
 async function execute(
   onEvent: (event: CommandEvent) => void,
   program: string,
-  args?: string | string[],
+  args?: string | string[] | { [key: string]: string },
   options?: InternalSpawnOptions
 ): Promise<number> {
   if (typeof args === 'object') {
@@ -78,7 +77,7 @@ async function execute(
     message: {
       cmd: 'execute',
       program,
-      args: typeof args === 'string' ? [args] : args,
+      args,
       options,
       onEventFn: transformCallback(onEvent)
     }

+ 27 - 20
tooling/cli.rs/Cargo.lock

@@ -20,7 +20,7 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
 dependencies = [
- "generic-array 0.14.4",
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -140,9 +140,9 @@ dependencies = [
 
 [[package]]
 name = "block-buffer"
-version = "0.10.0"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
 dependencies = [
  "generic-array 0.14.5",
 ]
@@ -153,7 +153,7 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
 dependencies = [
- "generic-array 0.14.4",
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -544,14 +544,11 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.10.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
 dependencies = [
- "block-buffer 0.10.0",
- "crypto-common",
  "generic-array 0.14.5",
- "subtle",
 ]
 
 [[package]]
@@ -562,7 +559,7 @@ checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b"
 dependencies = [
  "block-buffer 0.10.0",
  "crypto-common",
- "generic-array 0.14.4",
+ "generic-array 0.14.5",
  "subtle",
 ]
 
@@ -1409,6 +1406,12 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
 
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
 [[package]]
 name = "openssl"
 version = "0.10.38"
@@ -2048,7 +2051,7 @@ dependencies = [
  "hmac",
  "pbkdf2",
  "salsa20",
- "sha2 0.10.0",
+ "sha2 0.10.1",
 ]
 
 [[package]]
@@ -2214,7 +2217,7 @@ dependencies = [
  "block-buffer 0.7.3",
  "digest 0.8.1",
  "fake-simd",
- "opaque-debug",
+ "opaque-debug 0.2.3",
 ]
 
 [[package]]
@@ -2225,20 +2228,22 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
 
 [[package]]
 name = "sha2"
-version = "0.10.1"
+version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
 dependencies = [
+ "block-buffer 0.9.0",
  "cfg-if 1.0.0",
  "cpufeatures",
- "digest 0.10.1",
+ "digest 0.9.0",
+ "opaque-debug 0.3.0",
 ]
 
 [[package]]
 name = "sha2"
-version = "0.10.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "900d964dd36bb15bcf2f2b35694c072feab74969a54f2bbeec7a2d725d2bdcb6"
+checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
 dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
@@ -2379,7 +2384,7 @@ dependencies = [
  "regex",
  "serde",
  "serde_json",
- "sha2 0.9.8",
+ "sha2 0.10.1",
  "strsim",
  "tar",
  "tempfile",
@@ -2443,13 +2448,15 @@ dependencies = [
  "heck",
  "html5ever",
  "kuchiki",
+ "once_cell",
  "phf 0.10.1",
+ "ring",
  "schemars",
  "serde",
  "serde_json",
  "serde_with",
  "serialize-to-javascript",
- "sha2 0.9.8",
+ "sha2 0.9.9",
  "thiserror",
  "url",
 ]
@@ -2650,7 +2657,7 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
 dependencies = [
- "generic-array 0.14.4",
+ "generic-array 0.14.5",
  "subtle",
 ]
 

+ 112 - 19
tooling/cli.rs/schema.json

@@ -103,9 +103,7 @@
             "all": false,
             "execute": false,
             "open": false,
-            "scope": [
-              "$APP/**"
-            ],
+            "scope": [],
             "sidecar": false
           },
           "window": {
@@ -348,9 +346,7 @@
             "all": false,
             "execute": false,
             "open": false,
-            "scope": [
-              "$APP/**"
-            ],
+            "scope": [],
             "sidecar": false
           },
           "allOf": [
@@ -1025,7 +1021,7 @@
       "additionalProperties": false
     },
     "FsAllowlistScope": {
-      "description": "Filesystem scope definition. It is a list of glob patterns that restrict the API access from the webview. Each pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$CWD`.",
+      "description": "Filesystem scope definition. It is a list of glob patterns that restrict the API access from the webview.\n\nEach pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$CWD`.",
       "type": "array",
       "items": {
         "type": "string"
@@ -1321,6 +1317,85 @@
       },
       "additionalProperties": false
     },
+    "ShellAllowedArg": {
+      "description": "A command argument allowed to be executed by the webview API.",
+      "anyOf": [
+        {
+          "description": "A non-configurable argument that is passed to the command in the order it was specified.",
+          "type": "string"
+        },
+        {
+          "description": "A variable that is set while calling the command from the webview API.",
+          "type": "object",
+          "required": [
+            "name"
+          ],
+          "properties": {
+            "name": {
+              "description": "The name of the variable to be passed in.\n\nThis will try to match the key of the passed arguments object from the webview API.",
+              "type": "string"
+            },
+            "validate": {
+              "description": "Optional [regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validate` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax",
+              "default": null,
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "ShellAllowedArgs": {
+      "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
+      "anyOf": [
+        {
+          "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
+          "type": "boolean"
+        },
+        {
+          "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ShellAllowedArg"
+          }
+        }
+      ]
+    },
+    "ShellAllowedCommand": {
+      "description": "A command allowed to be executed by the webview API.",
+      "type": "object",
+      "required": [
+        "cmd",
+        "name"
+      ],
+      "properties": {
+        "args": {
+          "description": "The allowed arguments for the command execution.",
+          "default": false,
+          "allOf": [
+            {
+              "$ref": "#/definitions/ShellAllowedArgs"
+            }
+          ]
+        },
+        "cmd": {
+          "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$CWD`.",
+          "type": "string"
+        },
+        "name": {
+          "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
+          "type": "string"
+        },
+        "sidecar": {
+          "description": "If this command is a sidecar command.",
+          "default": false,
+          "type": "boolean"
+        }
+      }
+    },
     "ShellAllowlistConfig": {
       "description": "Allowlist for the shell APIs.",
       "type": "object",
@@ -1338,27 +1413,49 @@
         "open": {
           "description": "Open URL with the user's default application.",
           "default": false,
-          "type": "boolean"
+          "allOf": [
+            {
+              "$ref": "#/definitions/ShellAllowlistOpen"
+            }
+          ]
         },
         "scope": {
           "description": "Access scope for the binary execution APIs. Sidecars are automatically enabled.",
-          "default": [
-            "$APP/**"
-          ],
+          "default": [],
           "allOf": [
             {
-              "$ref": "#/definitions/FsAllowlistScope"
+              "$ref": "#/definitions/ShellAllowlistScope"
             }
           ]
         },
         "sidecar": {
-          "description": "Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar program, an executable that is shipped with the application. For more information see <https://tauri.studio/en/docs/usage/guides/bundler/sidecar>.",
+          "description": "Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar command, an executable that is shipped with the application. For more information see <https://tauri.studio/en/docs/usage/guides/bundler/sidecar>.",
           "default": false,
           "type": "boolean"
         }
       },
       "additionalProperties": false
     },
+    "ShellAllowlistOpen": {
+      "description": "Defines the `shell > open` api scope.",
+      "anyOf": [
+        {
+          "description": "If the shell open API should be enabled.\n\nIf enabled, the default validation regex (`^https?://`) is used.",
+          "type": "boolean"
+        },
+        {
+          "description": "Enable the shell open API, with a custom regex that the opened path must match against.\n\nIf using a custom regex to support a non-http(s) schema, care should be used to prevent values that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.",
+          "type": "string"
+        }
+      ]
+    },
+    "ShellAllowlistScope": {
+      "description": "Shell scope definition. It is a list of command names and associated CLI arguments that restrict the API access from the webview.",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/ShellAllowedCommand"
+      }
+    },
     "SystemTrayConfig": {
       "description": "Configuration for application system tray icon.",
       "type": "object",
@@ -1446,9 +1543,7 @@
               "all": false,
               "execute": false,
               "open": false,
-              "scope": [
-                "$APP/**"
-              ],
+              "scope": [],
               "sidecar": false
             },
             "window": {
@@ -1607,9 +1702,6 @@
     "UpdaterConfig": {
       "description": "The Updater configuration object.",
       "type": "object",
-      "required": [
-        "pubkey"
-      ],
       "properties": {
         "active": {
           "description": "Whether the updater is active or not.",
@@ -1633,6 +1725,7 @@
         },
         "pubkey": {
           "description": "Signature public key.",
+          "default": "",
           "type": "string"
         }
       },

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません