freeswitch_types/commands/
execute_on.rs1use std::fmt;
5
6use crate::switch_passes::execute_on::{expansion_rewrites, opens_scope_variables, render, split};
7
8use super::originate::OriginateError;
9
10const APPLICATION: &str = "application name";
12const ARGUMENT: &str = "argument";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ExecuteOnFault {
19 Empty,
21 Cut,
23 LineBreak,
25 Expanded,
27 OpensScopeVariables,
29}
30
31impl fmt::Display for ExecuteOnFault {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::Empty => f.write_str("is empty, which names no application"),
35 Self::Cut => f.write_str(
36 "carries a space, a lone colon or a NUL, where the switch cuts the value",
37 ),
38 Self::LineBreak => f.write_str("carries a line break, which no command line holds"),
39 Self::Expanded => f.write_str(
40 "carries an escape or a reference the expansion reads before the \
41 application does",
42 ),
43 Self::OpensScopeVariables => f.write_str(
44 "opens a block of scope variables, which the application never receives",
45 ),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ExecuteOn {
64 app: String,
65 arg: Option<String>,
66}
67
68impl ExecuteOn {
69 pub fn new(
72 app: impl Into<String>,
73 arg: Option<impl Into<String>>,
74 ) -> Result<Self, OriginateError> {
75 let app = app.into();
76 let arg = arg.map(Into::into);
77 if app.is_empty() {
78 return Err(undeliverable(APPLICATION, ExecuteOnFault::Empty));
79 }
80 if app.contains(['\n', '\r']) {
81 return Err(undeliverable(APPLICATION, ExecuteOnFault::LineBreak));
82 }
83 if let Some(arg) = &arg {
84 if arg.contains(['\n', '\r']) {
85 return Err(undeliverable(ARGUMENT, ExecuteOnFault::LineBreak));
86 }
87 if opens_scope_variables(arg) {
88 return Err(undeliverable(ARGUMENT, ExecuteOnFault::OpensScopeVariables));
89 }
90 if expansion_rewrites(arg) {
91 return Err(undeliverable(ARGUMENT, ExecuteOnFault::Expanded));
92 }
93 }
94 let value = render(&app, arg.as_deref());
95 let hook = split(&value);
96 if hook.app != app {
97 return Err(undeliverable(APPLICATION, ExecuteOnFault::Cut));
98 }
99 if hook.arg != arg.as_deref() {
100 return Err(undeliverable(ARGUMENT, ExecuteOnFault::Cut));
101 }
102 Ok(Self { app, arg })
103 }
104
105 pub fn lua(
108 script: impl AsRef<str>,
109 args: impl IntoIterator<Item = impl AsRef<str>>,
110 ) -> Result<Self, OriginateError> {
111 let mut words = vec![script
112 .as_ref()
113 .to_string()];
114 words.extend(
115 args.into_iter()
116 .map(|a| {
117 a.as_ref()
118 .to_string()
119 }),
120 );
121 if words
122 .iter()
123 .any(|word| word.is_empty() || word.contains(' '))
124 {
125 return Err(OriginateError::ParseError(
126 "a lua argument is empty or carries a space, on which mod_lua splits its \
127 argument list"
128 .to_owned(),
129 ));
130 }
131 Self::new("lua", Some(words.join(" ")))
132 }
133
134 pub fn app(&self) -> &str {
136 &self.app
137 }
138
139 pub fn arg(&self) -> Option<&str> {
141 self.arg
142 .as_deref()
143 }
144}
145
146fn undeliverable(part: &'static str, fault: ExecuteOnFault) -> OriginateError {
147 OriginateError::UndeliverableExecuteOn { part, fault }
148}
149
150impl fmt::Display for ExecuteOn {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.write_str(&render(
153 &self.app,
154 self.arg
155 .as_deref(),
156 ))
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn renders_app_and_argument() {
166 let hook = ExecuteOn::new("set", Some("probe=hooked")).unwrap();
167 assert_eq!(hook.to_string(), "set probe=hooked");
168 assert_eq!(hook.app(), "set");
169 assert_eq!(hook.arg(), Some("probe=hooked"));
170
171 let bare = ExecuteOn::new("answer", None::<String>).unwrap();
172 assert_eq!(bare.to_string(), "answer");
173 assert_eq!(bare.arg(), None);
174 }
175
176 #[test]
177 fn lua_joins_script_and_arguments() {
178 let hook = ExecuteOn::lua("/run/app/load.lua", ["/run/app/doc.xml", "pidf"]).unwrap();
179 assert_eq!(
180 hook.to_string(),
181 "lua /run/app/load.lua /run/app/doc.xml pidf"
182 );
183 }
184
185 #[test]
188 fn refuses_a_name_the_switch_would_split() {
189 assert!(ExecuteOn::new("lua:x", None::<String>).is_err());
190 assert!(ExecuteOn::new("my app", None::<String>).is_err());
191 assert!(ExecuteOn::new("", None::<String>).is_err());
192 }
193
194 #[test]
195 fn refuses_an_argument_the_switch_would_expand_or_the_wire_cannot_carry() {
196 assert!(ExecuteOn::new("set", Some("k=${v}")).is_err());
197 assert!(ExecuteOn::new("set", Some("k=a\nb")).is_err());
198 }
199
200 #[test]
204 fn refuses_an_argument_the_expansion_unescapes() {
205 assert!(ExecuteOn::new("set", Some(r"k=a\\b")).is_err());
206 assert!(ExecuteOn::new("set", Some(r"k=it\'s")).is_err());
207 assert!(ExecuteOn::new("set", Some(r"k=\$10\\")).is_err());
208 assert!(ExecuteOn::new("set", Some(r"k=\$10")).is_ok());
209 }
210
211 #[test]
214 fn refuses_an_argument_that_opens_scope_variables() {
215 assert!(ExecuteOn::new("set", Some("%[k=v]rest")).is_err());
216 assert!(ExecuteOn::new("set", Some("%|[k=v]rest")).is_err());
217 assert!(ExecuteOn::new("set", Some("%")).is_err());
218 }
219
220 #[test]
223 fn refuses_a_name_or_argument_the_wire_cuts() {
224 assert!(ExecuteOn::new("se\nt", None::<String>).is_err());
225 assert!(ExecuteOn::new("se\0t", None::<String>).is_err());
226 assert!(ExecuteOn::new("set", Some("k=a\0b")).is_err());
227 }
228
229 #[test]
230 fn lua_refuses_a_path_with_a_space() {
231 assert!(ExecuteOn::lua("/run/my app/load.lua", ["/run/doc.xml"]).is_err());
232 assert!(ExecuteOn::lua("/run/load.lua", ["/run/my doc.xml"]).is_err());
233 assert!(ExecuteOn::lua("/run/load.lua", [""]).is_err());
234 }
235
236 #[test]
238 fn a_perl_name_is_representable() {
239 let hook = ExecuteOn::new("perl", Some("/run/app/load.pl")).unwrap();
240 assert_eq!(hook.to_string(), "perl /run/app/load.pl");
241 }
242}