mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
9629c2f4f9
* [v2][log]: Add generic fern::Dispatch TargetKind to log Original: https://github.com/tauri-apps/plugins-workspace/pull/1130/commits/b691382d1ea54733ca837995cfac9f82ea05f168 * Add change file * clippy --------- Co-authored-by: Tony <legendmastertony@gmail.com> Co-authored-by: venkr <venkateshrameshkumar+1@gmail.com>
323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
use std::sync::Arc;
|
|
|
|
#[allow(deprecated)]
|
|
use crate::open::Program;
|
|
use crate::process::Command;
|
|
|
|
use regex::Regex;
|
|
use tauri::ipc::ScopeObject;
|
|
use tauri::Manager;
|
|
|
|
/// 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>),
|
|
}
|
|
|
|
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.is_empty() => true,
|
|
Self::List(l) => l.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)
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
|
|
/// The arguments the command is allowed to be called with.
|
|
pub args: Option<Vec<ScopeAllowedArg>>,
|
|
|
|
/// If this command is a sidecar command.
|
|
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, raw } => {
|
|
let regex = if raw {
|
|
validator
|
|
} else {
|
|
format!("^{validator}$")
|
|
};
|
|
let validator = Regex::new(®ex)
|
|
.unwrap_or_else(|e| panic!("invalid regex {regex}: {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 {
|
|
/// A non-configurable argument.
|
|
Fixed(String),
|
|
|
|
/// An argument with a value to be evaluated at runtime, must pass a regex validation.
|
|
Var {
|
|
/// The validation that the variable value must pass in order to be called.
|
|
validator: Regex,
|
|
},
|
|
}
|
|
|
|
impl ScopeAllowedArg {
|
|
/// If the argument is fixed.
|
|
pub fn is_fixed(&self) -> bool {
|
|
matches!(self, Self::Fixed(_))
|
|
}
|
|
}
|
|
|
|
/// Scope for the open command
|
|
pub struct OpenScope {
|
|
/// The validation regex that `shell > open` paths must match against.
|
|
/// When set to `None`, no values are accepted.
|
|
pub open: Option<Regex>,
|
|
}
|
|
|
|
/// Scope for shell process spawning.
|
|
#[derive(Clone)]
|
|
pub struct ShellScope<'a> {
|
|
/// All allowed commands, using their unique command name as the keys.
|
|
pub scopes: Vec<&'a Arc<ScopeAllowedCommand>>,
|
|
}
|
|
|
|
/// All errors that can happen while validating a scoped command.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum Error {
|
|
/// At least one argument did not pass input validation.
|
|
#[error("The scoped command was called with the improper sidecar flag set")]
|
|
BadSidecarFlag,
|
|
|
|
/// The sidecar program validated but failed to find the sidecar path.
|
|
#[error(
|
|
"The scoped sidecar command was validated, but failed to create the path to the command: {0}"
|
|
)]
|
|
Sidecar(String),
|
|
|
|
/// The named command was not found in the scoped config.
|
|
#[error("Scoped command {0} not found")]
|
|
NotFound(String),
|
|
|
|
/// A command variable has no value set in the arguments.
|
|
#[error(
|
|
"Scoped command argument at position {0} must match regex validation {1} but it was not found"
|
|
)]
|
|
MissingVar(usize, String),
|
|
|
|
/// At least one argument did not pass input validation.
|
|
#[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")]
|
|
Validation {
|
|
/// Index of the variable.
|
|
index: usize,
|
|
|
|
/// 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.
|
|
#[error("Scoped command {0} received arguments in an unexpected format")]
|
|
InvalidInput(String),
|
|
|
|
/// A generic IO error that occurs while executing specified shell commands.
|
|
#[error("Scoped shell IO error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
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+)).+`.
|
|
#[allow(deprecated)]
|
|
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(),
|
|
});
|
|
}
|
|
} else {
|
|
log::warn!("open() command called but the plugin configuration denies calls from JavaScript; set `tauri.conf.json > plugins > shell > open` to true or a validation regex string");
|
|
return Err(Error::Validation {
|
|
index: 0,
|
|
validation: "tauri^".to_string(), // purposefully impossible regex
|
|
});
|
|
}
|
|
|
|
// 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 ShellScope<'_> {
|
|
/// Validates argument inputs and creates a Tauri sidecar [`Command`].
|
|
pub fn prepare_sidecar(
|
|
&self,
|
|
command_name: &str,
|
|
command_script: &str,
|
|
args: ExecuteArgs,
|
|
) -> Result<Command, Error> {
|
|
self._prepare(command_name, args, Some(command_script))
|
|
}
|
|
|
|
/// Validates argument inputs and creates a Tauri [`Command`].
|
|
pub fn prepare(&self, command_name: &str, args: ExecuteArgs) -> Result<Command, Error> {
|
|
self._prepare(command_name, args, None)
|
|
}
|
|
|
|
/// Validates argument inputs and creates a Tauri [`Command`].
|
|
pub fn _prepare(
|
|
&self,
|
|
command_name: &str,
|
|
args: ExecuteArgs,
|
|
sidecar: Option<&str>,
|
|
) -> Result<Command, Error> {
|
|
let command = match self.scopes.iter().find(|s| s.name == command_name) {
|
|
Some(command) => command,
|
|
None => return Err(Error::NotFound(command_name.into())),
|
|
};
|
|
|
|
if command.sidecar != sidecar.is_some() {
|
|
return Err(Error::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]),
|
|
(Some(list), ExecuteArgs::List(args)) => list
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, arg)| match arg {
|
|
ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
|
|
ScopeAllowedArg::Var { validator } => {
|
|
let value = args
|
|
.get(i)
|
|
.ok_or_else(|| Error::MissingVar(i, validator.to_string()))?
|
|
.to_string();
|
|
if validator.is_match(&value) {
|
|
Ok(value)
|
|
} else {
|
|
Err(Error::Validation {
|
|
index: i,
|
|
validation: validator.to_string(),
|
|
})
|
|
}
|
|
}
|
|
})
|
|
.collect(),
|
|
(Some(list), arg) if arg.is_empty() && list.iter().all(ScopeAllowedArg::is_fixed) => {
|
|
list.iter()
|
|
.map(|arg| match arg {
|
|
ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
|
|
_ => unreachable!(),
|
|
})
|
|
.collect()
|
|
}
|
|
(Some(list), _) if list.is_empty() => Err(Error::InvalidInput(command_name.into())),
|
|
(Some(_), _) => Err(Error::InvalidInput(command_name.into())),
|
|
}?;
|
|
|
|
let command_s = sidecar
|
|
.map(|s| {
|
|
std::path::PathBuf::from(s)
|
|
.components()
|
|
.next_back()
|
|
.unwrap()
|
|
.as_os_str()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
})
|
|
.unwrap_or_else(|| command.command.to_string_lossy().into_owned());
|
|
let command = if command.sidecar {
|
|
Command::new_sidecar(command_s).map_err(|e| Error::Sidecar(e.to_string()))?
|
|
} else {
|
|
Command::new(command_s)
|
|
};
|
|
|
|
Ok(command.args(args))
|
|
}
|
|
}
|