use std::fmt;
use crate::switch_passes::execute_on::{expansion_rewrites, opens_scope_variables, render, split};
use super::originate::OriginateError;
const APPLICATION: &str = "application name";
const ARGUMENT: &str = "argument";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExecuteOnFault {
Empty,
Cut,
LineBreak,
Expanded,
OpensScopeVariables,
}
impl fmt::Display for ExecuteOnFault {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("is empty, which names no application"),
Self::Cut => f.write_str(
"carries a space, a lone colon or a NUL, where the switch cuts the value",
),
Self::LineBreak => f.write_str("carries a line break, which no command line holds"),
Self::Expanded => f.write_str(
"carries an escape or a reference the expansion reads before the \
application does",
),
Self::OpensScopeVariables => f.write_str(
"opens a block of scope variables, which the application never receives",
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecuteOn {
app: String,
arg: Option<String>,
}
impl ExecuteOn {
pub fn new(
app: impl Into<String>,
arg: Option<impl Into<String>>,
) -> Result<Self, OriginateError> {
let app = app.into();
let arg = arg.map(Into::into);
if app.is_empty() {
return Err(undeliverable(APPLICATION, ExecuteOnFault::Empty));
}
if app.contains(['\n', '\r']) {
return Err(undeliverable(APPLICATION, ExecuteOnFault::LineBreak));
}
if let Some(arg) = &arg {
if arg.contains(['\n', '\r']) {
return Err(undeliverable(ARGUMENT, ExecuteOnFault::LineBreak));
}
if opens_scope_variables(arg) {
return Err(undeliverable(ARGUMENT, ExecuteOnFault::OpensScopeVariables));
}
if expansion_rewrites(arg) {
return Err(undeliverable(ARGUMENT, ExecuteOnFault::Expanded));
}
}
let value = render(&app, arg.as_deref());
let hook = split(&value);
if hook.app != app {
return Err(undeliverable(APPLICATION, ExecuteOnFault::Cut));
}
if hook.arg != arg.as_deref() {
return Err(undeliverable(ARGUMENT, ExecuteOnFault::Cut));
}
Ok(Self { app, arg })
}
pub fn lua(
script: impl AsRef<str>,
args: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<Self, OriginateError> {
let mut words = vec![script
.as_ref()
.to_string()];
words.extend(
args.into_iter()
.map(|a| {
a.as_ref()
.to_string()
}),
);
if words
.iter()
.any(|word| word.is_empty() || word.contains(' '))
{
return Err(OriginateError::ParseError(
"a lua argument is empty or carries a space, on which mod_lua splits its \
argument list"
.to_owned(),
));
}
Self::new("lua", Some(words.join(" ")))
}
pub fn app(&self) -> &str {
&self.app
}
pub fn arg(&self) -> Option<&str> {
self.arg
.as_deref()
}
}
fn undeliverable(part: &'static str, fault: ExecuteOnFault) -> OriginateError {
OriginateError::UndeliverableExecuteOn { part, fault }
}
impl fmt::Display for ExecuteOn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&render(
&self.app,
self.arg
.as_deref(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_app_and_argument() {
let hook = ExecuteOn::new("set", Some("probe=hooked")).unwrap();
assert_eq!(hook.to_string(), "set probe=hooked");
assert_eq!(hook.app(), "set");
assert_eq!(hook.arg(), Some("probe=hooked"));
let bare = ExecuteOn::new("answer", None::<String>).unwrap();
assert_eq!(bare.to_string(), "answer");
assert_eq!(bare.arg(), None);
}
#[test]
fn lua_joins_script_and_arguments() {
let hook = ExecuteOn::lua("/run/app/load.lua", ["/run/app/doc.xml", "pidf"]).unwrap();
assert_eq!(
hook.to_string(),
"lua /run/app/load.lua /run/app/doc.xml pidf"
);
}
#[test]
fn refuses_a_name_the_switch_would_split() {
assert!(ExecuteOn::new("lua:x", None::<String>).is_err());
assert!(ExecuteOn::new("my app", None::<String>).is_err());
assert!(ExecuteOn::new("", None::<String>).is_err());
}
#[test]
fn refuses_an_argument_the_switch_would_expand_or_the_wire_cannot_carry() {
assert!(ExecuteOn::new("set", Some("k=${v}")).is_err());
assert!(ExecuteOn::new("set", Some("k=a\nb")).is_err());
}
#[test]
fn refuses_an_argument_the_expansion_unescapes() {
assert!(ExecuteOn::new("set", Some(r"k=a\\b")).is_err());
assert!(ExecuteOn::new("set", Some(r"k=it\'s")).is_err());
assert!(ExecuteOn::new("set", Some(r"k=\$10\\")).is_err());
assert!(ExecuteOn::new("set", Some(r"k=\$10")).is_ok());
}
#[test]
fn refuses_an_argument_that_opens_scope_variables() {
assert!(ExecuteOn::new("set", Some("%[k=v]rest")).is_err());
assert!(ExecuteOn::new("set", Some("%|[k=v]rest")).is_err());
assert!(ExecuteOn::new("set", Some("%")).is_err());
}
#[test]
fn refuses_a_name_or_argument_the_wire_cuts() {
assert!(ExecuteOn::new("se\nt", None::<String>).is_err());
assert!(ExecuteOn::new("se\0t", None::<String>).is_err());
assert!(ExecuteOn::new("set", Some("k=a\0b")).is_err());
}
#[test]
fn lua_refuses_a_path_with_a_space() {
assert!(ExecuteOn::lua("/run/my app/load.lua", ["/run/doc.xml"]).is_err());
assert!(ExecuteOn::lua("/run/load.lua", ["/run/my doc.xml"]).is_err());
assert!(ExecuteOn::lua("/run/load.lua", [""]).is_err());
}
#[test]
fn a_perl_name_is_representable() {
let hook = ExecuteOn::new("perl", Some("/run/app/load.pl")).unwrap();
assert_eq!(hook.to_string(), "perl /run/app/load.pl");
}
}