Skip to main content

freeswitch_types/commands/variables/
target.rs

1//! Where a dial string is headed: the command carrying it, the block-parser revision reading it,
2//! and the split cutting that command's arguments.
3//!
4//! Line numbers in this module index FreeSWITCH `v1.11.1`
5//! (`c2c59645f6911a76589e5008c4d73349ded44b65`).
6
7use std::borrow::Cow;
8use std::fmt;
9use std::str::FromStr;
10
11use crate::commands::originate::OriginateError;
12use crate::switch_passes::api_argument::{
13    escape_argument, sole_argument, usable_argv_separator, write_escaped, ArgumentPass, ArgvCut,
14};
15use crate::switch_passes::separate::Token;
16use crate::switch_passes::{trace, untrace, Traced};
17use crate::version::FreeswitchVersion;
18
19/// Which command carries a dial string, and therefore how deeply its variable
20/// values must be escaped.
21///
22/// FreeSWITCH escape-processes a bracket block a different number of times
23/// depending on the command it arrived on, and the correct escaping of a value
24/// containing a single quote differs between them with no form that satisfies
25/// all. Naming the carrier is the only way a renderer can be right.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[non_exhaustive]
29pub enum DialStringCarrier {
30    /// `api originate` / `bgapi originate`, whose argument list is split before
31    /// the block itself is parsed. The default for [`Display`](fmt::Display).
32    EslApi,
33    /// A dialplan application such as `bridge`, including via `sendmsg execute`,
34    /// which receives its argument whole.
35    Dialplan,
36    /// Handed straight to `switch_ivr_originate`, as a script binding's originate does
37    /// (`freeswitch.Session(dest)` and `session:originate` through `CoreSession`): no argument
38    /// split, no expansion.
39    Direct,
40}
41
42impl DialStringCarrier {
43    /// Dialplan variable expansion deletes an escaped quote outright, where every
44    /// other pass unescapes it, and drops the first `$` of a `$$`.
45    pub(crate) fn expands(self) -> bool {
46        matches!(self, Self::Dialplan)
47    }
48}
49
50/// Which revision of the switch's bracket-block parser a dial string is rendered
51/// for.
52///
53/// How many escape-consuming passes a block meets is the switch's to change, and
54/// every escaped quote and backslash depends on it. It covers `{}`, `<>` and `[]`
55/// escaping only: the inline action list and the quote pre-scan ahead of a `[]`
56/// block are other parsers. Which revisions a FreeSWITCH version is known to run
57/// is documented in `docs/dial-string-format.md`.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
61#[non_exhaustive]
62pub enum BlockParse {
63    /// Both of the block's own splits, on the separator and on `=`, run the
64    /// escape-consuming cleanup.
65    #[default]
66    PairSplitCleans,
67}
68
69impl BlockParse {
70    const ALL: &'static [Self] = &[Self::PairSplitCleans];
71
72    fn as_str(self) -> &'static str {
73        match self {
74            Self::PairSplitCleans => "pair_split_cleans",
75        }
76    }
77
78    pub(crate) fn cleanup_passes(self) -> u32 {
79        match self {
80            Self::PairSplitCleans => 2,
81        }
82    }
83
84    /// The revision a FreeSWITCH release is vouched to run, or why it cannot be.
85    ///
86    /// A release is vouched for only inside a range whose block parse
87    /// (`switch_event.c:1737`, `switch_utils.c:2804`) was checked at every tag and
88    /// whose line passed the live escaping suite. Anything else, a development
89    /// build included, is refused; name a revision explicitly for it instead.
90    pub fn for_version(version: &FreeswitchVersion) -> Result<Self, UnvouchedVersion> {
91        let version = *version;
92        let (first, last) = VOUCHED_PAIR_SPLIT_CLEANS;
93        if version.is_dev() {
94            Err(UnvouchedVersion::Dev { version })
95        } else if version < first {
96            Err(UnvouchedVersion::OlderThanVouched { version })
97        } else if version > last {
98            Err(UnvouchedVersion::NewerThanVouched { version })
99        } else {
100            Ok(Self::PairSplitCleans)
101        }
102    }
103}
104
105const VOUCHED_PAIR_SPLIT_CLEANS: (FreeswitchVersion, FreeswitchVersion) = (
106    FreeswitchVersion::new(1, 10, 0),
107    FreeswitchVersion::new(1, 11, 3),
108);
109
110/// A FreeSWITCH version [`BlockParse::for_version`] cannot vouch for.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112#[non_exhaustive]
113pub enum UnvouchedVersion {
114    /// A development build, whose version stays the same across commits.
115    Dev {
116        /// The version refused.
117        version: FreeswitchVersion,
118    },
119    /// A release newer than the vouched range.
120    NewerThanVouched {
121        /// The version refused.
122        version: FreeswitchVersion,
123    },
124    /// A release older than the vouched range.
125    OlderThanVouched {
126        /// The version refused.
127        version: FreeswitchVersion,
128    },
129}
130
131impl UnvouchedVersion {
132    /// The version refused.
133    pub fn version(&self) -> FreeswitchVersion {
134        match *self {
135            Self::Dev { version }
136            | Self::NewerThanVouched { version }
137            | Self::OlderThanVouched { version } => version,
138        }
139    }
140}
141
142impl fmt::Display for UnvouchedVersion {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        let (first, last) = VOUCHED_PAIR_SPLIT_CLEANS;
145        let why = match self {
146            Self::Dev { .. } => "is a development build",
147            Self::NewerThanVouched { .. } => "is newer than any vouched release",
148            Self::OlderThanVouched { .. } => "is older than any vouched release",
149        };
150        write!(
151            f,
152            "FreeSWITCH {} {why}; releases {first} to {last} are vouched for, name a BlockParse explicitly",
153            self.version()
154        )
155    }
156}
157
158impl std::error::Error for UnvouchedVersion {}
159
160impl fmt::Display for BlockParse {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        f.write_str(self.as_str())
163    }
164}
165
166impl FromStr for BlockParse {
167    type Err = ParseBlockParseError;
168
169    fn from_str(s: &str) -> Result<Self, Self::Err> {
170        Self::ALL
171            .iter()
172            .copied()
173            .find(|revision| {
174                revision
175                    .as_str()
176                    .eq_ignore_ascii_case(s)
177            })
178            .ok_or_else(|| ParseBlockParseError(s.to_string()))
179    }
180}
181
182/// A [`BlockParse`] name this crate does not know. The rejected name is the field.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct ParseBlockParseError(pub String);
185
186impl fmt::Display for ParseBlockParseError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.write_str("unknown block-parse revision, expected one of:")?;
189        for revision in BlockParse::ALL {
190            write!(f, " {revision}")?;
191        }
192        Ok(())
193    }
194}
195
196impl std::error::Error for ParseBlockParseError {}
197
198/// Where a dial string is headed: the command carrying it, the block-parser
199/// revision of the switch reading it, and the split cutting that command's arguments.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub struct DialStringTarget {
202    carrier: DialStringCarrier,
203    block_parse: BlockParse,
204    argument: ArgumentPass,
205}
206
207/// Why a [`DialStringTarget`] takes no argument separator.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209#[non_exhaustive]
210pub enum InvalidArgvSeparator {
211    /// The carrier hands the dial string over whole, so no argument split reads it.
212    WrongCarrier(DialStringCarrier),
213    /// A separator [`DialStringTarget::with_argv_separator`] refuses.
214    Unusable(char),
215}
216
217impl fmt::Display for InvalidArgvSeparator {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        match self {
220            Self::WrongCarrier(carrier) => {
221                write!(f, "the {carrier:?} carrier takes no argument separator")
222            }
223            Self::Unusable(sep) => write!(f, "{sep:?} cannot separate originate's arguments"),
224        }
225    }
226}
227
228impl std::error::Error for InvalidArgvSeparator {}
229
230impl DialStringTarget {
231    /// Target `carrier` at the default [`BlockParse`].
232    pub fn new(carrier: DialStringCarrier) -> Self {
233        Self {
234            carrier,
235            block_parse: BlockParse::default(),
236            argument: ArgumentPass::Blank,
237        }
238    }
239
240    /// Split `originate`'s arguments on `sep`, as a line opening with `^^<sep>` asks the switch.
241    ///
242    /// The dial string is one argument of that split: rendered, it is escaped once for the
243    /// split's cleanup; parsed, that cleanup runs first and a second argument is refused.
244    ///
245    /// Refused: separators the switch splits or unescapes wrongly (space, `\`, `'`, lowercase
246    /// `n r t s`, controls, non-ASCII) and, as policy, `^ " , | [ ] { } < > = :`, which read as
247    /// dial-string grammar or quoting, and every other letter or digit, which reads as part of a word.
248    /// A `'` separator pairs with the next one as quotes.
249    pub fn with_argv_separator(mut self, sep: char) -> Result<Self, InvalidArgvSeparator> {
250        match self.carrier {
251            DialStringCarrier::EslApi => {}
252            DialStringCarrier::Dialplan | DialStringCarrier::Direct => {
253                return Err(InvalidArgvSeparator::WrongCarrier(self.carrier))
254            }
255        }
256        if !usable_argv_separator(sep) {
257            return Err(InvalidArgvSeparator::Unusable(sep));
258        }
259        self.argument = ArgumentPass::Char(sep);
260        Ok(self)
261    }
262
263    /// The separator cutting `originate`'s arguments in place of the blank split.
264    pub fn argv_separator(&self) -> Option<char> {
265        match self.argument {
266            ArgumentPass::Char(sep) => Some(sep),
267            ArgumentPass::Blank | ArgumentPass::Consumed => None,
268        }
269    }
270
271    /// `text` escaped as one argument of `originate`'s split, on blanks or on the
272    /// [`argv_separator`](Self::argv_separator), or `None` at a carrier that splits none.
273    ///
274    /// An empty text is written `''`, which the split keeps as an argument. On blanks a text
275    /// opening `^^` is written after `''`, since a line opening `^^` names its own separator.
276    pub fn escape_argument<'a>(&self, text: &'a str) -> Option<Cow<'a, str>> {
277        self.split_delimiter()
278            .and_then(|sep| escape_argument(text, sep))
279    }
280
281    /// `render` at this target, escaped once at its edge for an argv separator's split.
282    pub(crate) fn write_argument(
283        self,
284        f: &mut fmt::Formatter<'_>,
285        render: impl Fn(&mut fmt::Formatter<'_>, Self) -> fmt::Result,
286    ) -> fmt::Result {
287        match self.argv_separator() {
288            Some(sep) => write_escaped(
289                f,
290                sep,
291                RenderedAt {
292                    target: self.inner(),
293                    render: &render,
294                },
295            ),
296            None => render(f, self),
297        }
298    }
299
300    /// A separator [`with_argv_separator`](Self::with_argv_separator) refuses as policy, for
301    /// reading captures the switch accepts under it.
302    #[cfg(test)]
303    pub(crate) fn with_unchecked_argv_separator(mut self, sep: char) -> Self {
304        self.argument = ArgumentPass::Char(sep);
305        self
306    }
307
308    /// The delimiter of `originate`'s argument split still ahead of this target, if any.
309    fn split_delimiter(self) -> Option<char> {
310        match (self.carrier, self.argument) {
311            (DialStringCarrier::EslApi, ArgumentPass::Blank) => Some(' '),
312            (_, ArgumentPass::Char(sep)) => Some(sep),
313            (DialStringCarrier::Dialplan | DialStringCarrier::Direct, ArgumentPass::Blank)
314            | (_, ArgumentPass::Consumed) => None,
315        }
316    }
317
318    /// The one argument `originate`'s split leaves of `text`, or `None` where no split reads it.
319    pub(crate) fn split_argument(self, text: &[Traced]) -> Option<Result<Option<Token>, ArgvCut>> {
320        self.split_delimiter()
321            .map(|delim| sole_argument(text, delim))
322    }
323
324    /// This target inside an argument its split already read.
325    pub(crate) fn inner(mut self) -> Self {
326        if self
327            .split_delimiter()
328            .is_some()
329        {
330            self.argument = ArgumentPass::Consumed;
331        }
332        self
333    }
334
335    /// What this target's argument split leaves of `s`, and the target reading that.
336    pub(crate) fn read_argument(self, s: &str) -> Result<(Cow<'_, str>, Self), OriginateError> {
337        let Some(token) = self.split_argument(&trace(s)) else {
338            return Ok((Cow::Borrowed(s), self));
339        };
340        let token = token.map_err(|ArgvCut| {
341            OriginateError::ParseError("originate's argument split cuts the dial string".into())
342        })?;
343        let argument = token.map_or_else(String::new, |token| untrace(&token.text));
344        Ok((Cow::Owned(argument), self.inner()))
345    }
346
347    /// Target a switch running `block_parse`.
348    pub fn with_block_parse(mut self, block_parse: BlockParse) -> Self {
349        self.block_parse = block_parse;
350        self
351    }
352
353    /// The command carrying the dial string.
354    pub fn carrier(&self) -> DialStringCarrier {
355        self.carrier
356    }
357
358    /// The block-parser revision rendered for.
359    pub fn block_parse(&self) -> BlockParse {
360        self.block_parse
361    }
362
363    /// The carrier's own pass still ahead of this target, the argument split or the dialplan
364    /// expansion: one, or none once consumed and at the direct carrier.
365    pub(crate) fn argument_passes(self) -> u32 {
366        match (self.carrier, self.argument) {
367            (DialStringCarrier::Direct, _) | (_, ArgumentPass::Consumed) => 0,
368            (_, ArgumentPass::Blank | ArgumentPass::Char(_)) => 1,
369        }
370    }
371}
372
373struct RenderedAt<'r, R> {
374    target: DialStringTarget,
375    render: &'r R,
376}
377
378impl<R> fmt::Display for RenderedAt<'_, R>
379where
380    R: Fn(&mut fmt::Formatter<'_>, DialStringTarget) -> fmt::Result,
381{
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        (self.render)(f, self.target)
384    }
385}
386
387impl From<DialStringCarrier> for DialStringTarget {
388    fn from(carrier: DialStringCarrier) -> Self {
389        Self::new(carrier)
390    }
391}