Skip to main content

freeswitch_types/commands/variables/
mod.rs

1//! Channel variable scope and ordered key-value storage for originate commands.
2
3use indexmap::IndexMap;
4use std::fmt;
5use std::str::FromStr;
6
7use super::originate::OriginateError;
8use crate::switch_passes::brackets::{check_separator, same_header, unbalanced, Block, PairEffect};
9use crate::switch_passes::escape::{escape_text, escape_value, EscapedField};
10use crate::switch_passes::originate_legs::{splits_into_threads, ENTERPRISE_DELIM};
11use crate::switch_passes::{pipeline, PipelineError};
12use crate::variables::names_outgoing_header;
13use crate::wire_safety::contains_header_control;
14
15mod target;
16#[cfg(test)]
17mod tests;
18
19pub use target::{
20    BlockParse, DialStringCarrier, DialStringTarget, InvalidArgvSeparator, ParseBlockParseError,
21    UnvouchedVersion,
22};
23
24/// Scope for channel variables in an originate command.
25///
26/// - `Enterprise` (`<>`) -- applies across all threads (`:_:` separated)
27/// - `Default` (`{}`) -- applies to all channels in this originate
28/// - `Channel` (`[]`) -- applies only to one specific channel
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
32#[non_exhaustive]
33pub enum VariablesType {
34    /// `<>` scope -- applies across all `:_:` separated threads.
35    Enterprise,
36    /// `{}` scope -- applies to all channels in this originate.
37    Default,
38    /// `[]` scope -- applies to one specific channel.
39    Channel,
40}
41
42impl VariablesType {
43    pub(crate) fn delimiters(self) -> (char, char) {
44        match self {
45            Self::Enterprise => ('<', '>'),
46            Self::Default => ('{', '}'),
47            Self::Channel => ('[', ']'),
48        }
49    }
50
51    /// The scope of the block the switch parsed.
52    pub(crate) fn of_block(block: &Block) -> Self {
53        match block.open {
54            '<' => Self::Enterprise,
55            '{' => Self::Default,
56            _ => Self::Channel,
57        }
58    }
59}
60
61/// Ordered set of channel variables with FreeSWITCH escaping.
62///
63/// A comma is escaped with `\,`, a backslash and a single quote with as many
64/// backslashes as the [`DialStringTarget`]'s passes consume, a space at either
65/// edge of a value as a `\s` escaped for the same passes, and a value with other
66/// spaces is wrapped in single quotes. This form round-trips through [`FromStr`];
67/// what the switch itself decodes depends on which command carries the block
68/// and which parser revision reads it, documented in `docs/dial-string-format.md`.
69///
70/// A key meets the same passes and is escaped the same way, with `\=` for the `=` split and an
71/// empty `''` ahead of one opening `^^`.
72///
73/// A value naming a variable (`${…}`) is left to the switch, which expands it or
74/// drops it at install unless `origination_nested_vars` is true.
75///
76/// # Serde format
77///
78/// [`Default`](VariablesType::Default) scope with the comma separator
79/// serializes as a flat JSON map: `{"key": "value", ...}`. Anything else
80/// serializes as `{"scope": "enterprise", "vars": {"key": "value"}}`, carrying
81/// a `"separator"` field only when [`with_separator`](Variables::with_separator)
82/// chose one. Deserialization accepts both formats; a flat map implies
83/// `Default` scope and the comma. A `separator` that cannot delimit the block,
84/// or that a key or value already contains, is refused at load.
85// qual:allow(srp, god_struct) reason: "public builder; accessors read disjoint fields"
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Variables {
88    vars_type: VariablesType,
89    inner: IndexMap<String, String>,
90    /// Set by [`with_separator`](Variables::with_separator). Parsing a `^^`
91    /// block does not populate it: reading one back and writing it out
92    /// canonicalises to a comma, and the other form is asked for explicitly.
93    separator: Option<char>,
94}
95
96/// Reject a key no escaping carries: empty, `:_:`, `[`, a quote in channel scope or an unbalanced
97/// bracket. The key is the offending text, so no error quotes it.
98fn check_key(key: &str, vars_type: VariablesType) -> Result<(), OriginateError> {
99    let fault = if key.is_empty() {
100        Some("is empty, which the switch installs under no name".to_owned())
101    } else if key.contains('[') {
102        Some(
103            "carries '[', which the switch reads as an array index, installing the value \
104             under the text before it"
105                .to_owned(),
106        )
107    } else if splits_into_threads(key) {
108        Some(format!(
109            "carries the enterprise separator {ENTERPRISE_DELIM}, on which the switch splits \
110             the dial string into threads whatever quoting or escaping surrounds it"
111        ))
112    } else if vars_type == VariablesType::Channel && key.contains('\'') {
113        Some(
114            "carries a single quote in channel scope, which the switch pairs with the next \
115             quote in the dial string before it parses the block"
116                .to_owned(),
117        )
118    } else {
119        unbalanced(key, vars_type.delimiters())
120    };
121    fault.map_or(Ok(()), |fault| {
122        Err(OriginateError::ParseError(format!(
123            "a variable name {fault}"
124        )))
125    })
126}
127
128/// Reject a pair no escaping carries: a key [`check_key`] refuses, or a value carrying `:_:`, a
129/// quote in channel scope, nothing or an unbalanced bracket. Each error names why the switch loses it.
130fn check_representable(
131    key: &str,
132    value: &str,
133    vars_type: VariablesType,
134) -> Result<(), OriginateError> {
135    check_key(key, vars_type)?;
136    if splits_into_threads(value) {
137        return Err(OriginateError::ParseError(format!(
138            "variable {key} carries the enterprise separator {ENTERPRISE_DELIM}, on which \
139             the switch splits the dial string into threads whatever quoting or escaping \
140             surrounds it"
141        )));
142    }
143    if vars_type == VariablesType::Channel && value.contains('\'') {
144        return Err(OriginateError::ParseError(format!(
145            "variable {key} carries a single quote in channel scope: the switch \
146             pairs it with the next quote in the dial string before it parses \
147             the block, whatever escaping precedes either. Use default scope, \
148             or keep the quote out of the value"
149        )));
150    }
151    if names_outgoing_header(key) && contains_header_control(value) {
152        return Err(OriginateError::ParseError(format!(
153            "variable {key} carries a control character: the switch copies it verbatim \
154             into an outgoing SIP header, whose field value cannot hold one"
155        )));
156    }
157    if value.is_empty() {
158        return Err(OriginateError::ParseError(format!(
159            "variable {key} has an empty value: the switch discards such a pair \
160             without logging it, so it cannot be told from an absent variable on \
161             the wire. Give it a value or remove it -- and if its presence was \
162             itself the signal, that signal needs a home outside the dial string"
163        )));
164    }
165
166    unbalanced(value, vars_type.delimiters()).map_or(Ok(()), |fault| {
167        Err(OriginateError::ParseError(format!(
168            "variable {key} {fault}"
169        )))
170    })
171}
172
173/// Reject a key naming, but for ASCII case, one of `earlier`: the block's event replaces a header
174/// by `strcasecmp`, so the two install as one variable. Position `at` names it, never its text.
175fn check_case_collision<'a>(
176    earlier: impl IntoIterator<Item = &'a String>,
177    key: &str,
178    at: usize,
179) -> Result<(), OriginateError> {
180    let collides = earlier
181        .into_iter()
182        .any(|name| name != key && same_header(name, key));
183    match collides {
184        true => Err(OriginateError::ParseError(format!(
185            "variable {at} differs only in case from an earlier name, which the switch \
186             installs as the same variable"
187        ))),
188        false => Ok(()),
189    }
190}
191
192impl Variables {
193    /// Create an empty variable set with the given scope.
194    pub fn new(vars_type: VariablesType) -> Self {
195        Self {
196            vars_type,
197            inner: IndexMap::new(),
198            separator: None,
199        }
200    }
201
202    /// Create from an existing set of key-value pairs.
203    pub fn with_vars(
204        vars_type: VariablesType,
205        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
206    ) -> Self {
207        Self {
208            vars_type,
209            inner: vars
210                .into_iter()
211                .map(|(k, v)| (k.into(), v.into()))
212                .collect(),
213            separator: None,
214        }
215    }
216
217    /// Separate the pairs with `sep` instead of a comma, emitting the block in
218    /// FreeSWITCH's `^^<sep>` form.
219    ///
220    /// A value expanded from `${...}` in a dialplan is substituted before the
221    /// block is parsed, so no escaping can be inserted into it; choosing a
222    /// separator none of the values contain is the only way such a value can
223    /// carry a comma. The separator is given rather than derived, so a block
224    /// renders the same way whatever its values happen to be that call.
225    ///
226    /// Fails if `sep` cannot delimit this block, or if a key or value already
227    /// present contains it. A pair inserted afterwards is not checked: one carrying
228    /// `sep` splits into a pair nobody wrote, silently, until
229    /// [`insert`](Self::insert) becomes fallible (`docs/next-major.md`).
230    ///
231    /// Refused: space, controls, non-ASCII, `\`, `'` and lowercase `n r t s`, which break the
232    /// switch's split or its escapes; either of the block's brackets, `=` and `^`; `$` and `{`,
233    /// which dialplan expansion reads as a reference across a pair boundary; `|` in a `[]` block.
234    pub fn with_separator(mut self, sep: char) -> Result<Self, OriginateError> {
235        check_separator(sep, self.vars_type)?;
236        if self
237            .inner
238            .keys()
239            .any(|k| k.contains(sep))
240        {
241            return Err(OriginateError::ParseError(format!(
242                "a variable name contains the chosen '{sep}' separator"
243            )));
244        }
245        if let Some((key, _)) = self
246            .inner
247            .iter()
248            .find(|(_, v)| v.contains(sep))
249        {
250            return Err(OriginateError::ParseError(format!(
251                "variable {key} contains the chosen '{sep}' separator"
252            )));
253        }
254        self.separator = Some(sep);
255        Ok(self)
256    }
257
258    /// The `^^` separator this block renders with, if one was chosen.
259    pub fn separator(&self) -> Option<char> {
260        self.separator
261    }
262
263    /// Insert or overwrite a variable.
264    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
265        self.inner
266            .insert(key.into(), value.into());
267    }
268
269    /// Remove a variable by name, returning its value if it existed.
270    pub fn remove(&mut self, key: &str) -> Option<String> {
271        self.inner
272            .shift_remove(key)
273    }
274
275    /// Look up a variable by name.
276    pub fn get(&self, key: &str) -> Option<&str> {
277        self.inner
278            .get(key)
279            .map(|s| s.as_str())
280    }
281
282    /// Whether the set contains no variables.
283    pub fn is_empty(&self) -> bool {
284        self.inner
285            .is_empty()
286    }
287
288    /// Number of variables.
289    pub fn len(&self) -> usize {
290        self.inner
291            .len()
292    }
293
294    /// Variable scope (Enterprise, Default, or Channel).
295    pub fn scope(&self) -> VariablesType {
296        self.vars_type
297    }
298
299    /// Change the variable scope.
300    pub fn set_scope(&mut self, scope: VariablesType) {
301        self.vars_type = scope;
302    }
303
304    /// Iterate over key-value pairs in insertion order.
305    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
306        self.inner
307            .iter()
308            .map(|(k, v)| (k.as_str(), v.as_str()))
309    }
310
311    /// Mutable iterator over key-value pairs in insertion order.
312    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut String)> {
313        self.inner
314            .iter_mut()
315            .map(|(k, v)| (k.as_str(), v))
316    }
317
318    /// Mutable iterator over values in insertion order.
319    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
320        self.inner
321            .values_mut()
322    }
323}
324
325#[cfg(feature = "serde")]
326impl serde::Serialize for Variables {
327    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
328        if self.vars_type == VariablesType::Default
329            && self
330                .separator
331                .is_none()
332        {
333            self.inner
334                .serialize(serializer)
335        } else {
336            use serde::ser::SerializeStruct;
337            let fields = 2 + usize::from(
338                self.separator
339                    .is_some(),
340            );
341            let mut s = serializer.serialize_struct("Variables", fields)?;
342            s.serialize_field("scope", &self.vars_type)?;
343            s.serialize_field("vars", &self.inner)?;
344            if let Some(sep) = self.separator {
345                s.serialize_field("separator", &sep)?;
346            }
347            s.end()
348        }
349    }
350}
351
352#[cfg(feature = "serde")]
353impl<'de> serde::Deserialize<'de> for Variables {
354    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
355        #[derive(serde::Deserialize)]
356        #[serde(untagged)]
357        enum VariablesRepr {
358            Scoped {
359                scope: VariablesType,
360                vars: IndexMap<String, String>,
361                #[serde(default)]
362                separator: Option<char>,
363            },
364            Flat(IndexMap<String, String>),
365        }
366
367        let (vars_type, inner, separator) = match VariablesRepr::deserialize(deserializer)? {
368            VariablesRepr::Scoped {
369                scope,
370                vars,
371                separator,
372            } => (scope, vars, separator),
373            VariablesRepr::Flat(map) => (VariablesType::Default, map, None),
374        };
375        // A config naming a value the wire cannot carry fails at load rather
376        // than on the call it was loaded for.
377        for (at, (key, value)) in inner
378            .iter()
379            .enumerate()
380        {
381            check_representable(key, value, vars_type)
382                .and_then(|()| {
383                    check_case_collision(
384                        inner
385                            .keys()
386                            .take(at),
387                        key,
388                        at,
389                    )
390                })
391                .map_err(serde::de::Error::custom)?;
392        }
393        let vars = Self {
394            vars_type,
395            inner,
396            separator: None,
397        };
398        match separator {
399            Some(sep) => vars
400                .with_separator(sep)
401                .map_err(serde::de::Error::custom),
402            None => Ok(vars),
403        }
404    }
405}
406
407/// Renders a [`Variables`] for one target. Returned by
408/// [`Variables::display_for`].
409#[derive(Debug, Clone, Copy)]
410pub struct VariablesDisplay<'a> {
411    vars: &'a Variables,
412    target: DialStringTarget,
413}
414
415impl fmt::Display for VariablesDisplay<'_> {
416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        self.target
418            .write_argument(f, |f, target| {
419                self.vars
420                    .write_for(f, target)
421            })
422    }
423}
424
425impl Variables {
426    /// Render for a named carrier or [`DialStringTarget`], rather than the
427    /// [`DialStringCarrier::EslApi`] default that [`Display`](fmt::Display) uses.
428    pub fn display_for(&self, target: impl Into<DialStringTarget>) -> VariablesDisplay<'_> {
429        VariablesDisplay {
430            vars: self,
431            target: target.into(),
432        }
433    }
434
435    pub(super) fn write_for(
436        &self,
437        f: &mut fmt::Formatter<'_>,
438        target: DialStringTarget,
439    ) -> fmt::Result {
440        let (open, close) = self
441            .vars_type
442            .delimiters();
443        f.write_fmt(format_args!("{}", open))?;
444        if let Some(sep) = self.separator {
445            write!(f, "^^{sep}")?;
446        }
447        // A chosen separator carries the values that a comma would have needed
448        // escaping for, so only a block splitting on commas escapes them.
449        let commas_separate = self
450            .separator
451            .is_none_or(|sep| sep == ',');
452        let sep = self
453            .separator
454            .unwrap_or(',');
455        for (i, (key, value)) in self
456            .inner
457            .iter()
458            .enumerate()
459        {
460            if i > 0 {
461                write!(f, "{sep}")?;
462            }
463            let key = escape_text(
464                key,
465                target,
466                EscapedField::Key {
467                    scope: self.vars_type,
468                    commas_separate,
469                },
470            );
471            let value = escape_value(value, target, commas_separate, self.vars_type);
472            write!(f, "{}={}", key, value)?;
473        }
474        f.write_fmt(format_args!("{}", close))
475    }
476}
477
478impl fmt::Display for Variables {
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        self.write_for(f, DialStringCarrier::EslApi.into())
481    }
482}
483
484impl FromStr for Variables {
485    type Err = OriginateError;
486
487    fn from_str(s: &str) -> Result<Self, Self::Err> {
488        Self::parse_for(s, DialStringCarrier::EslApi)
489    }
490}
491
492/// The endpoint text a lone block is read ahead of, so the switch's passes see a whole leg.
493const LONE_BLOCK_ENDPOINT: &str = "null/x";
494
495impl Variables {
496    /// Parse a block written for a named carrier or [`DialStringTarget`], as the switch installs
497    /// it there: the pairs are read through a port of every pass that target applies, so a block
498    /// [`display_for`](Self::display_for) wrote at the same target reads back as built.
499    /// [`FromStr`] uses the [`DialStringCarrier::EslApi`] default of [`Display`](fmt::Display).
500    pub fn parse_for(s: &str, target: impl Into<DialStringTarget>) -> Result<Self, OriginateError> {
501        let (argument, target) = target
502            .into()
503            .read_argument(s)?;
504        let s = argument.trim_matches(' ');
505        if s.len() < 2 {
506            return Err(OriginateError::ParseError(
507                "variable block too short".into(),
508            ));
509        }
510        match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
511            (b'{', b'}') | (b'<', b'>') | (b'[', b']') => {}
512            (open, close) => {
513                return Err(OriginateError::ParseError(format!(
514                    "unknown variable delimiters: {:?}..{:?}",
515                    open as char, close as char
516                )));
517            }
518        }
519        match read_leg(&format!("{s}{LONE_BLOCK_ENDPOINT}"), target)? {
520            (Some(block), endpoint) if endpoint == LONE_BLOCK_ENDPOINT => Self::from_block(&block),
521            _ => Err(OriginateError::ParseError(
522                "the switch ends the block before its last bracket".into(),
523            )),
524        }
525    }
526
527    /// The variables `block` installs, refusing what no render of this crate delivers.
528    pub(crate) fn from_block(block: &Block) -> Result<Self, OriginateError> {
529        let vars_type = VariablesType::of_block(block);
530        if block.separator != ',' {
531            check_separator(block.separator, vars_type)?;
532        }
533        if block.rewrites_following_text {
534            return Err(OriginateError::ParseError(
535                "the block's parse rewrites the text after it".into(),
536            ));
537        }
538        let mut inner = IndexMap::new();
539        for (i, pair) in block
540            .pairs
541            .iter()
542            .enumerate()
543        {
544            let value = match &pair.effect {
545                PairEffect::Set(value) => value.as_str(),
546                PairEffect::Cleared | PairEffect::Valueless => "",
547                PairEffect::Ignored => {
548                    return Err(OriginateError::ParseError(format!(
549                        "missing = in variable {i}"
550                    )))
551                }
552                PairEffect::Unreadable => {
553                    return Err(OriginateError::ParseError(format!(
554                        "variable {i} opens a non-ASCII ^^ separator the switch splits by byte"
555                    )))
556                }
557            };
558            let key = pair
559                .key
560                .as_str();
561            check_representable(key, value, vars_type)?;
562            check_case_collision(inner.keys(), key, i)?;
563            if block.separator != ','
564                && (key.contains(block.separator) || value.contains(block.separator))
565            {
566                return Err(OriginateError::ParseError(format!(
567                    "variable {i} contains the block's ^^ separator"
568                )));
569            }
570            inner.insert(key.to_owned(), value.to_owned());
571        }
572        Ok(Self {
573            vars_type,
574            inner,
575            separator: None,
576        })
577    }
578}
579
580/// The one leg the switch reads of `text` at `target`: its one block, if any, and the endpoint
581/// text after it.
582pub(crate) fn read_leg(
583    text: &str,
584    target: DialStringTarget,
585) -> Result<(Option<Block>, String), OriginateError> {
586    let list = pipeline::read(text, target).map_err(read_error)?;
587    let one_leg = || OriginateError::ParseError("the switch reads more than one leg".into());
588    let [thread] = &list.threads[..] else {
589        return Err(one_leg());
590    };
591    let [group] = &thread.groups[..] else {
592        return Err(one_leg());
593    };
594    let [leg] = &group[..] else {
595        return Err(one_leg());
596    };
597    let mut blocks = list
598        .blocks
599        .iter()
600        .chain(&thread.blocks)
601        .chain(&leg.blocks);
602    match (blocks.next(), blocks.next()) {
603        (Some(_), Some(_)) => Err(OriginateError::ParseError(
604            "an endpoint carries one variable block".into(),
605        )),
606        (block, _) => Ok((
607            block.cloned(),
608            leg.endpoint
609                .clone(),
610        )),
611    }
612}
613
614/// The variables `block` installs, or `None` for no block or one installing nothing.
615pub(crate) fn installed_variables(
616    block: Option<&Block>,
617) -> Result<Option<Variables>, OriginateError> {
618    Ok(block
619        .map(Variables::from_block)
620        .transpose()?
621        .filter(|vars| !vars.is_empty()))
622}
623
624/// Why the switch reads no dial string from the text.
625pub(crate) fn read_error(error: PipelineError) -> OriginateError {
626    OriginateError::ParseError(
627        match error {
628            PipelineError::Empty => "no endpoint to dial",
629            PipelineError::ArgvSplit => "originate's argument split cuts the dial string",
630            PipelineError::UnclosedBlock { .. } => "a variable block never closes",
631            PipelineError::SplitSeparatorUnreadable => {
632                "a split on a non-ASCII ^^ separator's first byte reaches past its text"
633            }
634        }
635        .into(),
636    )
637}