#[cfg(any(shell_execute, shell_sidecar))]
use crate::api::process::Command;
#[cfg(feature = "shell-open-api")]
use crate::api::shell::Program;
use regex::Regex;
use tauri_utils::{config::Config, Env, PackageInfo};
use std::collections::HashMap;
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[non_exhaustive]
pub enum ExecuteArgs {
None,
Single(String),
List(Vec<String>),
}
impl ExecuteArgs {
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)
}
}
#[derive(Debug, Clone)]
pub struct ScopeConfig {
pub open: Option<Regex>,
pub scopes: HashMap<String, ScopeAllowedCommand>,
}
#[derive(Debug, Clone)]
pub struct ScopeAllowedCommand {
pub command: std::path::PathBuf,
pub args: Option<Vec<ScopeAllowedArg>>,
pub sidecar: bool,
}
#[derive(Debug, Clone)]
pub enum ScopeAllowedArg {
Fixed(String),
Var {
validator: Regex,
},
}
impl ScopeAllowedArg {
pub fn is_fixed(&self) -> bool {
matches!(self, Self::Fixed(_))
}
pub fn is_var(&self) -> bool {
matches!(self, Self::Var { .. })
}
}
#[derive(Clone)]
pub struct Scope(ScopeConfig);
#[derive(Debug, thiserror::Error)]
pub enum ScopeError {
#[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,
#[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),
#[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),
#[error(
"Scoped command argument at position {0} must match regex validation {1} but it was not found"
)]
#[cfg(any(shell_execute, shell_sidecar))]
#[cfg_attr(
doc_cfg,
doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar")))
)]
MissingVar(usize, String),
#[cfg(shell_scope)]
#[cfg_attr(
doc_cfg,
doc(cfg(any(feature = "shell-execute", feature = "shell-open")))
)]
#[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")]
Validation {
index: usize,
validation: String,
},
#[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),
#[cfg(shell_scope)]
#[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 {
pub(crate) fn new(
config: &Config,
package_info: &PackageInfo,
env: &Env,
mut scope: ScopeConfig,
) -> Self {
for cmd in scope.scopes.values_mut() {
if let Ok(path) = crate::api::path::parse(config, package_info, env, &cmd.command) {
cmd.command = path;
}
}
Self(scope)
}
#[cfg(shell_sidecar)]
pub fn prepare_sidecar(
&self,
command_name: &str,
command_script: &str,
args: ExecuteArgs,
) -> Result<Command, ScopeError> {
self._prepare(command_name, args, Some(command_script))
}
#[cfg(shell_execute)]
pub fn prepare(&self, command_name: &str, args: ExecuteArgs) -> Result<Command, ScopeError> {
self._prepare(command_name, args, None)
}
#[cfg(any(shell_execute, shell_sidecar))]
pub fn _prepare(
&self,
command_name: &str,
args: ExecuteArgs,
sidecar: Option<&str>,
) -> 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.is_some() {
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]),
(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(|| ScopeError::MissingVar(i, validator.to_string()))?
.to_string();
if validator.is_match(&value) {
Ok(value)
} else {
Err(ScopeError::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(ScopeError::InvalidInput(command_name.into())),
(Some(_), _) => Err(ScopeError::InvalidInput(command_name.into())),
}?;
let command_s = sidecar
.map(|s| {
std::path::PathBuf::from(s)
.components()
.last()
.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(ScopeError::Sidecar)?
} else {
Command::new(command_s)
};
Ok(command.args(args))
}
#[cfg(feature = "shell-open-api")]
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), ScopeError> {
if let Some(regex) = &self.0.open {
if !regex.is_match(path) {
return Err(ScopeError::Validation {
index: 0,
validation: regex.as_str().into(),
});
}
}
match with.map(Program::name) {
Some(program) => ::open::with(path, program),
None => ::open::that(path),
}
.map_err(Into::into)
}
}