mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-29 12:06:01 +02:00
feat: update to tauri beta, add permissions (#862)
Co-authored-by: Lucas Nogueira <lucas@tauri.app> Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
This commit is contained in:
@@ -6,7 +6,10 @@ use std::{collections::HashMap, path::PathBuf, string::FromUtf8Error};
|
||||
|
||||
use encoding_rs::Encoding;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{ipc::Channel, Manager, Runtime, State, Window};
|
||||
use tauri::{
|
||||
ipc::{Channel, CommandScope, GlobalScope},
|
||||
Manager, Runtime, State, Window,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
open::Program,
|
||||
@@ -91,6 +94,7 @@ fn default_env() -> Option<HashMap<String, String>> {
|
||||
Some(HashMap::default())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub fn execute<R: Runtime>(
|
||||
window: Window<R>,
|
||||
@@ -99,14 +103,23 @@ pub fn execute<R: Runtime>(
|
||||
args: ExecuteArgs,
|
||||
on_event: Channel,
|
||||
options: CommandOptions,
|
||||
command_scope: CommandScope<'_, crate::scope::ScopeAllowedCommand>,
|
||||
global_scope: GlobalScope<'_, crate::scope::ScopeAllowedCommand>,
|
||||
) -> crate::Result<ChildId> {
|
||||
let scope = crate::scope::ShellScope {
|
||||
scopes: command_scope
|
||||
.allows()
|
||||
.iter()
|
||||
.chain(global_scope.allows())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let mut command = if options.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 configured_sidecar = window
|
||||
.config()
|
||||
.tauri
|
||||
.bundle
|
||||
.external_bin
|
||||
.as_ref()
|
||||
@@ -116,14 +129,12 @@ pub fn execute<R: Runtime>(
|
||||
})
|
||||
.cloned();
|
||||
if let Some(sidecar) = configured_sidecar {
|
||||
shell
|
||||
.scope
|
||||
.prepare_sidecar(&program.to_string_lossy(), &sidecar, args)?
|
||||
scope.prepare_sidecar(&program.to_string_lossy(), &sidecar, args)?
|
||||
} else {
|
||||
return Err(crate::Error::SidecarNotAllowed(program));
|
||||
}
|
||||
} else {
|
||||
match shell.scope.prepare(&program, args) {
|
||||
match scope.prepare(&program, args) {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
+1
-114
@@ -2,130 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Configuration for the shell plugin.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// Access scope for the binary execution APIs.
|
||||
/// Sidecars are automatically enabled.
|
||||
#[serde(default)]
|
||||
pub scope: ShellAllowlistScope,
|
||||
/// Open URL with the user's default application.
|
||||
#[serde(default)]
|
||||
pub open: ShellAllowlistOpen,
|
||||
}
|
||||
|
||||
/// A command allowed to be executed by the webview API.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||
// use default just so the schema doesn't flag it as required
|
||||
pub command: PathBuf,
|
||||
|
||||
/// The allowed arguments for the command execution.
|
||||
pub args: ShellAllowedArgs,
|
||||
|
||||
/// If this command is a sidecar command.
|
||||
pub sidecar: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ShellAllowedCommand {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct InnerShellAllowedCommand {
|
||||
name: String,
|
||||
#[serde(rename = "cmd")]
|
||||
command: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
args: ShellAllowedArgs,
|
||||
#[serde(default)]
|
||||
sidecar: bool,
|
||||
}
|
||||
|
||||
let config = InnerShellAllowedCommand::deserialize(deserializer)?;
|
||||
|
||||
if !config.sidecar && config.command.is_none() {
|
||||
return Err(DeError::custom(
|
||||
"The shell scope `command` value is required.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ShellAllowedCommand {
|
||||
name: config.name,
|
||||
command: config.command.unwrap_or_default(),
|
||||
args: config.args,
|
||||
sidecar: config.sidecar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, Eq, Clone, Deserialize)]
|
||||
#[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, Eq, Clone, Deserialize)]
|
||||
#[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 {
|
||||
/// [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 `validator` regex
|
||||
/// before it will be executed.
|
||||
///
|
||||
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
|
||||
validator: 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, Eq, Clone, Deserialize)]
|
||||
|
||||
pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
|
||||
|
||||
/// Defines the `shell > open` api scope.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
|
||||
@@ -24,6 +24,9 @@ pub enum Error {
|
||||
ProgramNotAllowed(PathBuf),
|
||||
#[error("unknown encoding {0}")]
|
||||
UnknownEncoding(String),
|
||||
/// JSON error.
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
|
||||
+12
-49
@@ -20,7 +20,6 @@ use std::{
|
||||
|
||||
use process::{Command, CommandChild};
|
||||
use regex::Regex;
|
||||
use scope::{Scope, ScopeAllowedCommand, ScopeConfig};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
AppHandle, Manager, RunEvent, Runtime,
|
||||
@@ -32,8 +31,8 @@ mod error;
|
||||
mod open;
|
||||
pub mod process;
|
||||
mod scope;
|
||||
mod scope_entry;
|
||||
|
||||
use config::{Config, ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope};
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
|
||||
@@ -41,7 +40,7 @@ type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
|
||||
pub struct Shell<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
app: AppHandle<R>,
|
||||
scope: Scope,
|
||||
open_scope: scope::OpenScope,
|
||||
children: ChildStore,
|
||||
}
|
||||
|
||||
@@ -63,7 +62,7 @@ impl<R: Runtime> Shell<R> {
|
||||
///
|
||||
/// See [`crate::api::shell::open`] for how it handles security-related measures.
|
||||
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
|
||||
open::open(&self.scope, path.into(), with).map_err(Into::into)
|
||||
open::open(&self.open_scope, path.into(), with).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +76,11 @@ impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
let mut init_script = include_str!("init-iife.js").to_string();
|
||||
init_script.push_str(include_str!("api-iife.js"));
|
||||
|
||||
Builder::<R, Option<Config>>::new("shell")
|
||||
Builder::<R, Option<config::Config>>::new("shell")
|
||||
.js_init_script(init_script)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::execute,
|
||||
@@ -90,12 +89,12 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
commands::open
|
||||
])
|
||||
.setup(|app, api| {
|
||||
let default_config = Config::default();
|
||||
let default_config = config::Config::default();
|
||||
let config = api.config().as_ref().unwrap_or(&default_config);
|
||||
app.manage(Shell {
|
||||
app: app.clone(),
|
||||
children: Default::default(),
|
||||
scope: Scope::new(app, shell_scope(config.scope.clone(), &config.open)),
|
||||
open_scope: open_scope(&config.open),
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
@@ -114,56 +113,20 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
.build()
|
||||
}
|
||||
|
||||
fn shell_scope(scope: ShellAllowlistScope, open: &ShellAllowlistOpen) -> ScopeConfig {
|
||||
let shell_scopes = get_allowed_clis(scope);
|
||||
|
||||
fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
|
||||
let shell_scope_open = match open {
|
||||
ShellAllowlistOpen::Flag(false) => None,
|
||||
ShellAllowlistOpen::Flag(true) => {
|
||||
config::ShellAllowlistOpen::Flag(false) => None,
|
||||
config::ShellAllowlistOpen::Flag(true) => {
|
||||
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
|
||||
}
|
||||
ShellAllowlistOpen::Validate(validator) => {
|
||||
config::ShellAllowlistOpen::Validate(validator) => {
|
||||
let validator =
|
||||
Regex::new(validator).unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
Some(validator)
|
||||
}
|
||||
};
|
||||
|
||||
ScopeConfig {
|
||||
scope::OpenScope {
|
||||
open: shell_scope_open,
|
||||
scopes: shell_scopes,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_allowed_clis(scope: ShellAllowlistScope) -> HashMap<String, ScopeAllowedCommand> {
|
||||
scope
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|scope| {
|
||||
let args = match scope.args {
|
||||
ShellAllowedArgs::Flag(true) => None,
|
||||
ShellAllowedArgs::Flag(false) => Some(Vec::new()),
|
||||
ShellAllowedArgs::List(list) => {
|
||||
let list = list.into_iter().map(|arg| match arg {
|
||||
ShellAllowedArg::Fixed(fixed) => scope::ScopeAllowedArg::Fixed(fixed),
|
||||
ShellAllowedArg::Var { validator } => {
|
||||
let validator = Regex::new(&validator)
|
||||
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
scope::ScopeAllowedArg::Var { validator }
|
||||
}
|
||||
});
|
||||
Some(list.collect())
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
scope.name,
|
||||
ScopeAllowedCommand {
|
||||
command: scope.command,
|
||||
args,
|
||||
sidecar: scope.sidecar,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use crate::scope::Scope;
|
||||
use crate::scope::OpenScope;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Program to use on the [`open()`] call.
|
||||
@@ -117,6 +117,6 @@ impl Program {
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn open<P: AsRef<str>>(scope: &Scope, path: P, with: Option<Program>) -> crate::Result<()> {
|
||||
pub fn open<P: AsRef<str>>(scope: &OpenScope, path: P, with: Option<Program>) -> crate::Result<()> {
|
||||
scope.open(path.as_ref(), with).map_err(Into::into)
|
||||
}
|
||||
|
||||
+82
-48
@@ -4,11 +4,10 @@
|
||||
|
||||
use crate::open::Program;
|
||||
use crate::process::Command;
|
||||
use crate::{Manager, Runtime};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use tauri::ipc::ScopeObject;
|
||||
use tauri::Manager;
|
||||
|
||||
/// Allowed representation of `Execute` command arguments.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
@@ -55,19 +54,12 @@ impl From<Vec<String>> for ExecuteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell scope configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScopeConfig {
|
||||
/// 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, ScopeAllowedCommand>,
|
||||
}
|
||||
|
||||
/// A configured scoped shell command.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScopeAllowedCommand {
|
||||
/// Name of the command (key).
|
||||
pub name: String,
|
||||
|
||||
/// The shell command to be called.
|
||||
pub command: std::path::PathBuf,
|
||||
|
||||
@@ -78,6 +70,47 @@ pub struct ScopeAllowedCommand {
|
||||
pub sidecar: bool,
|
||||
}
|
||||
|
||||
impl ScopeObject for ScopeAllowedCommand {
|
||||
type Error = crate::Error;
|
||||
fn deserialize<R: tauri::Runtime>(
|
||||
app: &tauri::AppHandle<R>,
|
||||
raw: tauri::utils::acl::Value,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let scope = serde_json::from_value::<crate::scope_entry::Entry>(raw.into())?;
|
||||
|
||||
let args = match scope.args.clone() {
|
||||
crate::scope_entry::ShellAllowedArgs::Flag(true) => None,
|
||||
crate::scope_entry::ShellAllowedArgs::Flag(false) => Some(Vec::new()),
|
||||
crate::scope_entry::ShellAllowedArgs::List(list) => {
|
||||
let list = list.into_iter().map(|arg| match arg {
|
||||
crate::scope_entry::ShellAllowedArg::Fixed(fixed) => {
|
||||
crate::scope::ScopeAllowedArg::Fixed(fixed)
|
||||
}
|
||||
crate::scope_entry::ShellAllowedArg::Var { validator } => {
|
||||
let validator = Regex::new(&validator)
|
||||
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
crate::scope::ScopeAllowedArg::Var { validator }
|
||||
}
|
||||
});
|
||||
Some(list.collect())
|
||||
}
|
||||
};
|
||||
|
||||
let command = if let Ok(path) = app.path().parse(&scope.command) {
|
||||
path
|
||||
} else {
|
||||
scope.command.clone()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
name: scope.name,
|
||||
command,
|
||||
args,
|
||||
sidecar: scope.sidecar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A configured argument to a scoped shell command.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ScopeAllowedArg {
|
||||
@@ -98,9 +131,18 @@ impl ScopeAllowedArg {
|
||||
}
|
||||
}
|
||||
|
||||
/// Scope for filesystem access.
|
||||
/// Scope for the open command
|
||||
pub struct OpenScope {
|
||||
/// The validation regex that `shell > open` paths must match against.
|
||||
pub open: Option<Regex>,
|
||||
}
|
||||
|
||||
/// Scope for shell process spawning.
|
||||
#[derive(Clone)]
|
||||
pub struct Scope(ScopeConfig);
|
||||
pub struct ShellScope<'a> {
|
||||
/// All allowed commands, using their unique command name as the keys.
|
||||
pub scopes: Vec<&'a ScopeAllowedCommand>,
|
||||
}
|
||||
|
||||
/// All errors that can happen while validating a scoped command.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -147,17 +189,33 @@ pub enum Error {
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Creates a new shell scope.
|
||||
pub(crate) fn new<R: Runtime, M: Manager<R>>(manager: &M, mut scope: ScopeConfig) -> Self {
|
||||
for cmd in scope.scopes.values_mut() {
|
||||
if let Ok(path) = manager.path().parse(&cmd.command) {
|
||||
cmd.command = path;
|
||||
impl OpenScope {
|
||||
/// Open a path in the default (or specified) browser.
|
||||
///
|
||||
/// The path is validated against the `plugins > shell > open` validation regex, which
|
||||
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
||||
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
|
||||
// ensure we pass validation if the configuration has one
|
||||
if let Some(regex) = &self.open {
|
||||
if !regex.is_match(path) {
|
||||
return Err(Error::Validation {
|
||||
index: 0,
|
||||
validation: regex.as_str().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Self(scope)
|
||||
}
|
||||
|
||||
// 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_detached(path, program),
|
||||
None => ::open::that_detached(path),
|
||||
}
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ShellScope<'a> {
|
||||
/// Validates argument inputs and creates a Tauri sidecar [`Command`].
|
||||
pub fn prepare_sidecar(
|
||||
&self,
|
||||
@@ -180,7 +238,7 @@ impl Scope {
|
||||
args: ExecuteArgs,
|
||||
sidecar: Option<&str>,
|
||||
) -> Result<Command, Error> {
|
||||
let command = match self.0.scopes.get(command_name) {
|
||||
let command = match self.scopes.iter().find(|s| s.name == command_name) {
|
||||
Some(command) => command,
|
||||
None => return Err(Error::NotFound(command_name.into())),
|
||||
};
|
||||
@@ -245,28 +303,4 @@ impl Scope {
|
||||
|
||||
Ok(command.args(args))
|
||||
}
|
||||
|
||||
/// Open a path in the default (or specified) browser.
|
||||
///
|
||||
/// The path is validated against the `plugins > shell > open` validation regex, which
|
||||
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
||||
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
|
||||
// ensure we pass validation if the configuration has one
|
||||
if let Some(regex) = &self.0.open {
|
||||
if !regex.is_match(path) {
|
||||
return Err(Error::Validation {
|
||||
index: 0,
|
||||
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_detached(path, program),
|
||||
None => ::open::that_detached(path),
|
||||
}
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A command allowed to be executed by the webview API.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, schemars::JsonSchema)]
|
||||
pub struct Entry {
|
||||
/// 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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||
// use default just so the schema doesn't flag it as required
|
||||
pub command: PathBuf,
|
||||
|
||||
/// The allowed arguments for the command execution.
|
||||
pub args: ShellAllowedArgs,
|
||||
|
||||
/// If this command is a sidecar command.
|
||||
pub sidecar: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Entry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct InnerEntry {
|
||||
name: String,
|
||||
#[serde(rename = "cmd")]
|
||||
command: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
args: ShellAllowedArgs,
|
||||
#[serde(default)]
|
||||
sidecar: bool,
|
||||
}
|
||||
|
||||
let config = InnerEntry::deserialize(deserializer)?;
|
||||
|
||||
if !config.sidecar && config.command.is_none() {
|
||||
return Err(DeError::custom(
|
||||
"The shell scope `command` value is required.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Entry {
|
||||
name: config.name,
|
||||
command: config.command.unwrap_or_default(),
|
||||
args: config.args,
|
||||
sidecar: config.sidecar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, Eq, Clone, Hash, Deserialize, schemars::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, Eq, Clone, Hash, Deserialize, schemars::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 {
|
||||
/// [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 `validator` regex
|
||||
/// before it will be executed.
|
||||
///
|
||||
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
|
||||
validator: String,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user