Skip to main content

freeswitch_types/commands/
execute_on.rs

1//! The value of an `execute_on_*` channel variable: an application and its
2//! argument.
3
4use std::fmt;
5
6use super::originate::OriginateError;
7
8/// An application for an `execute_on_*` hook such as
9/// [`ChannelVariable::ExecuteOnOriginate`](crate::ChannelVariable::ExecuteOnOriginate).
10///
11/// The switch splits the value at the first space or single colon, expands
12/// variables in what follows, and runs the application on the channel. Set as
13/// `execute_on_originate` in an originate's block, it runs on the new channel
14/// before that channel's session thread starts, so a variable it sets is on the
15/// channel before a SIP leg builds its INVITE. That is how a large or free-text
16/// value reaches a leg without crossing the dial string's tokenizer: the block
17/// carries paths, and the application reads the value from where they point.
18/// Not on a loopback leg, which the hook wedges in `CS_INIT`.
19/// `docs/dial-string-format.md` has the measurements and the traps.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ExecuteOn {
22    app: String,
23    arg: Option<String>,
24}
25
26impl ExecuteOn {
27    /// Refuses an application name the switch would split, an argument it would
28    /// expand, and a line break the wire cannot carry.
29    pub fn new(
30        app: impl Into<String>,
31        arg: Option<impl Into<String>>,
32    ) -> Result<Self, OriginateError> {
33        let app = app.into();
34        let arg = arg.map(Into::into);
35        if app.is_empty() {
36            return Err(OriginateError::ParseError(
37                "execute_on application name is empty".into(),
38            ));
39        }
40        if app.contains([' ', ':']) {
41            return Err(OriginateError::ParseError(format!(
42                "execute_on application name {app:?} carries a space or colon, where the \
43                 switch splits the name from its argument"
44            )));
45        }
46        if let Some(arg) = &arg {
47            if arg.contains("${") {
48                return Err(OriginateError::ParseError(format!(
49                    "execute_on argument for {app} carries `${{`, which the switch expands \
50                     before the application sees it; pass a path, not content"
51                )));
52            }
53            if arg.contains(['\n', '\r']) {
54                return Err(OriginateError::ParseError(format!(
55                    "execute_on argument for {app} carries a line break"
56                )));
57            }
58        }
59        Ok(Self { app, arg })
60    }
61
62    /// `lua <script> <args…>`. mod_lua splits its argument on spaces, so a
63    /// script path or argument carrying one is refused.
64    pub fn lua(
65        script: impl AsRef<str>,
66        args: impl IntoIterator<Item = impl AsRef<str>>,
67    ) -> Result<Self, OriginateError> {
68        let mut words = vec![script
69            .as_ref()
70            .to_string()];
71        words.extend(
72            args.into_iter()
73                .map(|a| {
74                    a.as_ref()
75                        .to_string()
76                }),
77        );
78        if let Some(word) = words
79            .iter()
80            .find(|w| w.is_empty() || w.contains(' '))
81        {
82            return Err(OriginateError::ParseError(format!(
83                "lua argument {word:?} is empty or carries a space, on which mod_lua splits \
84                 its argument list"
85            )));
86        }
87        Self::new("lua", Some(words.join(" ")))
88    }
89
90    /// The application name.
91    pub fn app(&self) -> &str {
92        &self.app
93    }
94
95    /// The argument, as the application receives it after expansion.
96    pub fn arg(&self) -> Option<&str> {
97        self.arg
98            .as_deref()
99    }
100}
101
102impl fmt::Display for ExecuteOn {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match &self.arg {
105            Some(arg) => write!(f, "{} {}", self.app, arg),
106            None => f.write_str(&self.app),
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn renders_app_and_argument() {
117        let hook = ExecuteOn::new("set", Some("probe=hooked")).unwrap();
118        assert_eq!(hook.to_string(), "set probe=hooked");
119        assert_eq!(hook.app(), "set");
120        assert_eq!(hook.arg(), Some("probe=hooked"));
121
122        let bare = ExecuteOn::new("answer", None::<String>).unwrap();
123        assert_eq!(bare.to_string(), "answer");
124        assert_eq!(bare.arg(), None);
125    }
126
127    #[test]
128    fn lua_joins_script_and_arguments() {
129        let hook = ExecuteOn::lua("/run/app/load.lua", ["/run/app/doc.xml", "pidf"]).unwrap();
130        assert_eq!(
131            hook.to_string(),
132            "lua /run/app/load.lua /run/app/doc.xml pidf"
133        );
134    }
135
136    /// The switch splits the name from the argument at the first space or
137    /// single colon, so a name carrying either names a different application.
138    #[test]
139    fn refuses_a_name_the_switch_would_split() {
140        assert!(ExecuteOn::new("lua:x", None::<String>).is_err());
141        assert!(ExecuteOn::new("my app", None::<String>).is_err());
142        assert!(ExecuteOn::new("", None::<String>).is_err());
143    }
144
145    #[test]
146    fn refuses_an_argument_the_switch_would_expand_or_the_wire_cannot_carry() {
147        assert!(ExecuteOn::new("set", Some("k=${v}")).is_err());
148        assert!(ExecuteOn::new("set", Some("k=a\nb")).is_err());
149    }
150
151    #[test]
152    fn lua_refuses_a_path_with_a_space() {
153        assert!(ExecuteOn::lua("/run/my app/load.lua", ["/run/doc.xml"]).is_err());
154        assert!(ExecuteOn::lua("/run/load.lua", ["/run/my doc.xml"]).is_err());
155        assert!(ExecuteOn::lua("/run/load.lua", [""]).is_err());
156    }
157}