Skip to main content

freeswitch_types/commands/
variables.rs

1//! Channel variable scope and ordered key-value storage for originate commands.
2//!
3//! Line numbers in this module index FreeSWITCH `v1.11.1`
4//! (`c2c59645f6911a76589e5008c4d73349ded44b65`).
5
6use indexmap::IndexMap;
7use std::fmt;
8use std::str::FromStr;
9
10use super::originate::OriginateError;
11use crate::version::FreeswitchVersion;
12
13/// Scope for channel variables in an originate command.
14///
15/// - `Enterprise` (`<>`) -- applies across all threads (`:_:` separated)
16/// - `Default` (`{}`) -- applies to all channels in this originate
17/// - `Channel` (`[]`) -- applies only to one specific channel
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
21#[non_exhaustive]
22pub enum VariablesType {
23    /// `<>` scope -- applies across all `:_:` separated threads.
24    Enterprise,
25    /// `{}` scope -- applies to all channels in this originate.
26    Default,
27    /// `[]` scope -- applies to one specific channel.
28    Channel,
29}
30
31impl VariablesType {
32    pub(super) fn delimiters(self) -> (char, char) {
33        match self {
34            Self::Enterprise => ('<', '>'),
35            Self::Default => ('{', '}'),
36            Self::Channel => ('[', ']'),
37        }
38    }
39
40    /// A `[]` block rides through the `|` and `,` leg splits before the block
41    /// parse, and both consume escapes.
42    fn leg_split_passes(self) -> u32 {
43        match self {
44            Self::Enterprise | Self::Default => 0,
45            Self::Channel => 2,
46        }
47    }
48}
49
50/// Ordered set of channel variables with FreeSWITCH escaping.
51///
52/// A comma is escaped with `\,`, a backslash and a single quote with as many
53/// backslashes as the [`DialStringTarget`]'s passes consume, and a value with
54/// spaces is wrapped in single quotes. This form round-trips through [`FromStr`];
55/// what the switch itself decodes depends on which command carries the block
56/// and which parser revision reads it, documented in `docs/dial-string-format.md`.
57///
58/// # Serde format
59///
60/// [`Default`](VariablesType::Default) scope with the comma separator
61/// serializes as a flat JSON map: `{"key": "value", ...}`. Anything else
62/// serializes as `{"scope": "enterprise", "vars": {"key": "value"}}`, carrying
63/// a `"separator"` field only when [`with_separator`](Variables::with_separator)
64/// chose one. Deserialization accepts both formats; a flat map implies
65/// `Default` scope and the comma. A `separator` that cannot delimit the block,
66/// or that a value already contains, is refused at load.
67// qual:allow(srp, god_struct) reason: "public builder; accessors read disjoint fields"
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Variables {
70    vars_type: VariablesType,
71    inner: IndexMap<String, String>,
72    /// Set by [`with_separator`](Variables::with_separator). Parsing a `^^`
73    /// block does not populate it: reading one back and writing it out
74    /// canonicalises to a comma, and the other form is asked for explicitly.
75    separator: Option<char>,
76}
77
78/// Which command carries a dial string, and therefore how deeply its variable
79/// values must be escaped.
80///
81/// FreeSWITCH escape-processes a bracket block a different number of times
82/// depending on the command it arrived on, and the correct escaping of a value
83/// containing a single quote differs between them with no form that satisfies
84/// both. Naming the carrier is the only way a renderer can be right.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[non_exhaustive]
88pub enum DialStringCarrier {
89    /// `api originate` / `bgapi originate`, whose argument list is split before
90    /// the block itself is parsed. The default for [`Display`](fmt::Display).
91    EslApi,
92    /// A dialplan application such as `bridge`, including via `sendmsg execute`,
93    /// which receives its argument whole.
94    Dialplan,
95}
96
97impl DialStringCarrier {
98    /// Dialplan variable expansion deletes an escaped quote outright, where every
99    /// other pass unescapes it.
100    fn deletes_escaped_quote(self) -> bool {
101        matches!(self, Self::Dialplan)
102    }
103}
104
105/// Which revision of the switch's bracket-block parser a dial string is rendered
106/// for.
107///
108/// How many escape-consuming passes a block meets is the switch's to change, and
109/// every escaped quote and backslash depends on it. It covers `{}`, `<>` and `[]`
110/// escaping only: the inline action list and the quote pre-scan ahead of a `[]`
111/// block are other parsers. Which revisions a FreeSWITCH version is known to run
112/// is documented in `docs/dial-string-format.md`.
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
116#[non_exhaustive]
117pub enum BlockParse {
118    /// Both of the block's own splits, on the separator and on `=`, run the
119    /// escape-consuming cleanup.
120    #[default]
121    PairSplitCleans,
122}
123
124impl BlockParse {
125    const ALL: &'static [Self] = &[Self::PairSplitCleans];
126
127    fn as_str(self) -> &'static str {
128        match self {
129            Self::PairSplitCleans => "pair_split_cleans",
130        }
131    }
132
133    fn cleanup_passes(self) -> u32 {
134        match self {
135            Self::PairSplitCleans => 2,
136        }
137    }
138
139    /// The revision a FreeSWITCH release is vouched to run, or why it cannot be.
140    ///
141    /// A release is vouched for only inside a range whose block parse
142    /// (`switch_event.c:1737`, `switch_utils.c:2804`) was checked at every tag and
143    /// whose line passed the live escaping suite. Anything else, a development
144    /// build included, is refused; name a revision explicitly for it instead.
145    pub fn for_version(version: &FreeswitchVersion) -> Result<Self, UnvouchedVersion> {
146        let version = *version;
147        let (first, last) = VOUCHED_PAIR_SPLIT_CLEANS;
148        if version.is_dev() {
149            Err(UnvouchedVersion::Dev { version })
150        } else if version < first {
151            Err(UnvouchedVersion::OlderThanVouched { version })
152        } else if version > last {
153            Err(UnvouchedVersion::NewerThanVouched { version })
154        } else {
155            Ok(Self::PairSplitCleans)
156        }
157    }
158}
159
160const VOUCHED_PAIR_SPLIT_CLEANS: (FreeswitchVersion, FreeswitchVersion) = (
161    FreeswitchVersion::new(1, 10, 0),
162    FreeswitchVersion::new(1, 10, 12),
163);
164
165/// A FreeSWITCH version [`BlockParse::for_version`] cannot vouch for.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum UnvouchedVersion {
169    /// A development build, whose version stays the same across commits.
170    Dev {
171        /// The version refused.
172        version: FreeswitchVersion,
173    },
174    /// A release newer than the vouched range.
175    NewerThanVouched {
176        /// The version refused.
177        version: FreeswitchVersion,
178    },
179    /// A release older than the vouched range.
180    OlderThanVouched {
181        /// The version refused.
182        version: FreeswitchVersion,
183    },
184}
185
186impl UnvouchedVersion {
187    /// The version refused.
188    pub fn version(&self) -> FreeswitchVersion {
189        match *self {
190            Self::Dev { version }
191            | Self::NewerThanVouched { version }
192            | Self::OlderThanVouched { version } => version,
193        }
194    }
195}
196
197impl fmt::Display for UnvouchedVersion {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        let (first, last) = VOUCHED_PAIR_SPLIT_CLEANS;
200        let why = match self {
201            Self::Dev { .. } => "is a development build",
202            Self::NewerThanVouched { .. } => "is newer than any vouched release",
203            Self::OlderThanVouched { .. } => "is older than any vouched release",
204        };
205        write!(
206            f,
207            "FreeSWITCH {} {why}; releases {first} to {last} are vouched for, name a BlockParse explicitly",
208            self.version()
209        )
210    }
211}
212
213impl std::error::Error for UnvouchedVersion {}
214
215impl fmt::Display for BlockParse {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        f.write_str(self.as_str())
218    }
219}
220
221impl FromStr for BlockParse {
222    type Err = ParseBlockParseError;
223
224    fn from_str(s: &str) -> Result<Self, Self::Err> {
225        Self::ALL
226            .iter()
227            .copied()
228            .find(|revision| {
229                revision
230                    .as_str()
231                    .eq_ignore_ascii_case(s)
232            })
233            .ok_or_else(|| ParseBlockParseError(s.to_string()))
234    }
235}
236
237/// A [`BlockParse`] name this crate does not know. The rejected name is the field.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct ParseBlockParseError(pub String);
240
241impl fmt::Display for ParseBlockParseError {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        f.write_str("unknown block-parse revision, expected one of:")?;
244        for revision in BlockParse::ALL {
245            write!(f, " {revision}")?;
246        }
247        Ok(())
248    }
249}
250
251impl std::error::Error for ParseBlockParseError {}
252
253/// Where a dial string is headed: the command carrying it and the block-parser
254/// revision of the switch reading it.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub struct DialStringTarget {
257    carrier: DialStringCarrier,
258    block_parse: BlockParse,
259}
260
261impl DialStringTarget {
262    /// Target `carrier` at the default [`BlockParse`].
263    pub fn new(carrier: DialStringCarrier) -> Self {
264        Self {
265            carrier,
266            block_parse: BlockParse::default(),
267        }
268    }
269
270    /// Target a switch running `block_parse`.
271    pub fn with_block_parse(mut self, block_parse: BlockParse) -> Self {
272        self.block_parse = block_parse;
273        self
274    }
275
276    /// The command carrying the dial string.
277    pub fn carrier(&self) -> DialStringCarrier {
278        self.carrier
279    }
280
281    /// The block-parser revision rendered for.
282    pub fn block_parse(&self) -> BlockParse {
283        self.block_parse
284    }
285
286    fn passes(self, scope: VariablesType) -> u32 {
287        1 + self
288            .block_parse
289            .cleanup_passes()
290            + scope.leg_split_passes()
291    }
292
293    /// Every pass halves a run of backslashes, so a literal one needs 2^passes.
294    fn backslash_escape(self, scope: VariablesType) -> String {
295        "\\".repeat(1 << self.passes(scope))
296    }
297
298    /// A quote must still read `\'` entering the last pass, or bare after a
299    /// carrier pass that deletes `\'`.
300    fn quote_escape(self, scope: VariablesType) -> String {
301        let consumed_by_carrier = if self
302            .carrier
303            .deletes_escaped_quote()
304        {
305            2
306        } else {
307            1
308        };
309        let run = (1usize << self.passes(scope)) - consumed_by_carrier;
310        format!("{}'", "\\".repeat(run))
311    }
312}
313
314impl From<DialStringCarrier> for DialStringTarget {
315    fn from(carrier: DialStringCarrier) -> Self {
316        Self::new(carrier)
317    }
318}
319
320/// Reject a value the wire cannot carry, whatever escaping is applied to it.
321///
322/// Three shapes qualify. An empty value is discarded by the switch under every
323/// encoding, and the block's own parse reports nothing when it happens. A value
324/// carrying an unbalanced bracket ends the block early, because the switch finds
325/// the block's end by counting depth and does not honour escapes while doing so;
326/// a balanced pair such as `${var}` is fine and common. A single quote in a
327/// channel-scope value pairs with the next quote anywhere before the peer split,
328/// escaped or not, and the pair between them is swallowed into the first value.
329fn check_representable(
330    key: &str,
331    value: &str,
332    vars_type: VariablesType,
333) -> Result<(), OriginateError> {
334    if vars_type == VariablesType::Channel && value.contains('\'') {
335        return Err(OriginateError::ParseError(format!(
336            "variable {key} carries a single quote in channel scope: the switch \
337             pairs it with the next quote in the dial string before it parses \
338             the block, whatever escaping precedes either. Use default scope, \
339             or keep the quote out of the value"
340        )));
341    }
342    if value.is_empty() {
343        return Err(OriginateError::ParseError(format!(
344            "variable {key} has an empty value: the switch discards such a pair \
345             without logging it, so it cannot be told from an absent variable on \
346             the wire. Give it a value or remove it -- and if its presence was \
347             itself the signal, that signal needs a home outside the dial string"
348        )));
349    }
350
351    let (open, close) = vars_type.delimiters();
352    let mut depth = 0i32;
353    for ch in value.chars() {
354        if ch == open {
355            depth += 1;
356        } else if ch == close {
357            depth -= 1;
358            if depth < 0 {
359                return Err(OriginateError::ParseError(format!(
360                    "variable {key} closes a '{open}' it never opened, ending the block early"
361                )));
362            }
363        }
364    }
365    if depth != 0 {
366        return Err(OriginateError::ParseError(format!(
367            "variable {key} opens a '{open}' it never closes, swallowing the block's end"
368        )));
369    }
370    Ok(())
371}
372
373/// Reject a separator that cannot delimit the block it was chosen for.
374///
375/// Either bracket moves the end the switch counts its way to, `=` splits the
376/// pair instead, `^` leaves the `^^` prefix reading as its own separator, and
377/// `|` in a `[]` block is read by the leg split before the block is parsed.
378fn check_separator(sep: char, vars_type: VariablesType) -> Result<(), OriginateError> {
379    let (open, close) = vars_type.delimiters();
380    if sep == open
381        || sep == close
382        || sep == '='
383        || sep == '^'
384        || (sep == '|' && vars_type == VariablesType::Channel)
385    {
386        return Err(OriginateError::ParseError(format!(
387            "invalid ^^ separator: '{sep}'"
388        )));
389    }
390    Ok(())
391}
392
393/// Escape a value for the wire, escaping the comma only when `commas_separate`
394/// says it is this block's separator; a `^^` block separates on something else
395/// and refuses a value carrying it, so a comma there is ordinary text. A `[]`
396/// block also escapes the pipe, which the leg split would otherwise read.
397fn escape_value(
398    value: &str,
399    target: impl Into<DialStringTarget>,
400    commas_separate: bool,
401    vars_type: VariablesType,
402) -> String {
403    let target = target.into();
404    // The backslash goes first, or the ones the other rules introduce get
405    // escaped in turn.
406    let escaped = value
407        .replace('\\', &target.backslash_escape(vars_type))
408        .replace('\'', &target.quote_escape(vars_type));
409    // `\,` and `\|` keep one backslash: a pass consumes `\x` only before a
410    // quote, a backslash, a named escape or that pass's own delimiter.
411    let escaped = if commas_separate {
412        escaped.replace(',', "\\,")
413    } else {
414        escaped
415    };
416    let escaped = if vars_type == VariablesType::Channel {
417        escaped.replace('|', "\\|")
418    } else {
419        escaped
420    };
421    if escaped.contains(' ') {
422        format!("'{}'", escaped)
423    } else {
424        escaped
425    }
426}
427
428/// Inverts [`escape_value`], undoing each substitution in the reverse order it
429/// was applied so an escape introduced by a later rule is not read as input to
430/// an earlier one.
431fn unescape_value(
432    value: &str,
433    target: DialStringTarget,
434    commas_separate: bool,
435    vars_type: VariablesType,
436) -> String {
437    let s = value
438        .strip_prefix('\'')
439        .and_then(|s| s.strip_suffix('\''))
440        .unwrap_or(value);
441
442    let s = if vars_type == VariablesType::Channel {
443        s.replace("\\|", "|")
444    } else {
445        s.to_string()
446    };
447    let s = if commas_separate {
448        s.replace("\\,", ",")
449    } else {
450        s
451    };
452    s.replace(&target.quote_escape(vars_type), "'")
453        .replace(&target.backslash_escape(vars_type), "\\")
454}
455
456impl Variables {
457    /// Create an empty variable set with the given scope.
458    pub fn new(vars_type: VariablesType) -> Self {
459        Self {
460            vars_type,
461            inner: IndexMap::new(),
462            separator: None,
463        }
464    }
465
466    /// Create from an existing set of key-value pairs.
467    pub fn with_vars(
468        vars_type: VariablesType,
469        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
470    ) -> Self {
471        Self {
472            vars_type,
473            inner: vars
474                .into_iter()
475                .map(|(k, v)| (k.into(), v.into()))
476                .collect(),
477            separator: None,
478        }
479    }
480
481    /// Separate the pairs with `sep` instead of a comma, emitting the block in
482    /// FreeSWITCH's `^^<sep>` form.
483    ///
484    /// A value expanded from `${...}` in a dialplan is substituted before the
485    /// block is parsed, so no escaping can be inserted into it; choosing a
486    /// separator none of the values contain is the only way such a value can
487    /// carry a comma. The separator is given rather than derived, so a block
488    /// renders the same way whatever its values happen to be that call.
489    ///
490    /// Fails if `sep` cannot delimit this block, or if a value already present
491    /// contains it. A value inserted afterwards is not checked: one carrying
492    /// `sep` splits into a pair nobody wrote, silently, until
493    /// [`insert`](Self::insert) becomes fallible (`docs/next-major.md`).
494    pub fn with_separator(mut self, sep: char) -> Result<Self, OriginateError> {
495        check_separator(sep, self.vars_type)?;
496        if let Some((key, _)) = self
497            .inner
498            .iter()
499            .find(|(_, v)| v.contains(sep))
500        {
501            return Err(OriginateError::ParseError(format!(
502                "variable {key} contains the chosen '{sep}' separator"
503            )));
504        }
505        self.separator = Some(sep);
506        Ok(self)
507    }
508
509    /// The `^^` separator this block renders with, if one was chosen.
510    pub fn separator(&self) -> Option<char> {
511        self.separator
512    }
513
514    /// Insert or overwrite a variable.
515    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
516        self.inner
517            .insert(key.into(), value.into());
518    }
519
520    /// Remove a variable by name, returning its value if it existed.
521    pub fn remove(&mut self, key: &str) -> Option<String> {
522        self.inner
523            .shift_remove(key)
524    }
525
526    /// Look up a variable by name.
527    pub fn get(&self, key: &str) -> Option<&str> {
528        self.inner
529            .get(key)
530            .map(|s| s.as_str())
531    }
532
533    /// Whether the set contains no variables.
534    pub fn is_empty(&self) -> bool {
535        self.inner
536            .is_empty()
537    }
538
539    /// Number of variables.
540    pub fn len(&self) -> usize {
541        self.inner
542            .len()
543    }
544
545    /// Variable scope (Enterprise, Default, or Channel).
546    pub fn scope(&self) -> VariablesType {
547        self.vars_type
548    }
549
550    /// Change the variable scope.
551    pub fn set_scope(&mut self, scope: VariablesType) {
552        self.vars_type = scope;
553    }
554
555    /// Iterate over key-value pairs in insertion order.
556    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
557        self.inner
558            .iter()
559            .map(|(k, v)| (k.as_str(), v.as_str()))
560    }
561
562    /// Mutable iterator over key-value pairs in insertion order.
563    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut String)> {
564        self.inner
565            .iter_mut()
566            .map(|(k, v)| (k.as_str(), v))
567    }
568
569    /// Mutable iterator over values in insertion order.
570    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
571        self.inner
572            .values_mut()
573    }
574}
575
576#[cfg(feature = "serde")]
577impl serde::Serialize for Variables {
578    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
579        if self.vars_type == VariablesType::Default
580            && self
581                .separator
582                .is_none()
583        {
584            self.inner
585                .serialize(serializer)
586        } else {
587            use serde::ser::SerializeStruct;
588            let fields = 2 + usize::from(
589                self.separator
590                    .is_some(),
591            );
592            let mut s = serializer.serialize_struct("Variables", fields)?;
593            s.serialize_field("scope", &self.vars_type)?;
594            s.serialize_field("vars", &self.inner)?;
595            if let Some(sep) = self.separator {
596                s.serialize_field("separator", &sep)?;
597            }
598            s.end()
599        }
600    }
601}
602
603#[cfg(feature = "serde")]
604impl<'de> serde::Deserialize<'de> for Variables {
605    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
606        #[derive(serde::Deserialize)]
607        #[serde(untagged)]
608        enum VariablesRepr {
609            Scoped {
610                scope: VariablesType,
611                vars: IndexMap<String, String>,
612                #[serde(default)]
613                separator: Option<char>,
614            },
615            Flat(IndexMap<String, String>),
616        }
617
618        let (vars_type, inner, separator) = match VariablesRepr::deserialize(deserializer)? {
619            VariablesRepr::Scoped {
620                scope,
621                vars,
622                separator,
623            } => (scope, vars, separator),
624            VariablesRepr::Flat(map) => (VariablesType::Default, map, None),
625        };
626        // A config naming a value the wire cannot carry fails at load rather
627        // than on the call it was loaded for.
628        for (key, value) in &inner {
629            check_representable(key, value, vars_type).map_err(serde::de::Error::custom)?;
630        }
631        let vars = Self {
632            vars_type,
633            inner,
634            separator: None,
635        };
636        match separator {
637            Some(sep) => vars
638                .with_separator(sep)
639                .map_err(serde::de::Error::custom),
640            None => Ok(vars),
641        }
642    }
643}
644
645/// Renders a [`Variables`] for one target. Returned by
646/// [`Variables::display_for`].
647#[derive(Debug, Clone, Copy)]
648pub struct VariablesDisplay<'a> {
649    vars: &'a Variables,
650    target: DialStringTarget,
651}
652
653impl fmt::Display for VariablesDisplay<'_> {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        self.vars
656            .write_for(f, self.target)
657    }
658}
659
660impl Variables {
661    /// Render for a named carrier or [`DialStringTarget`], rather than the
662    /// [`DialStringCarrier::EslApi`] default that [`Display`](fmt::Display) uses.
663    pub fn display_for(&self, target: impl Into<DialStringTarget>) -> VariablesDisplay<'_> {
664        VariablesDisplay {
665            vars: self,
666            target: target.into(),
667        }
668    }
669
670    pub(super) fn write_for(
671        &self,
672        f: &mut fmt::Formatter<'_>,
673        target: DialStringTarget,
674    ) -> fmt::Result {
675        let (open, close) = self
676            .vars_type
677            .delimiters();
678        f.write_fmt(format_args!("{}", open))?;
679        if let Some(sep) = self.separator {
680            write!(f, "^^{sep}")?;
681        }
682        // A chosen separator carries the values that a comma would have needed
683        // escaping for, so only the comma form escapes them.
684        let commas_separate = self
685            .separator
686            .is_none();
687        let sep = self
688            .separator
689            .unwrap_or(',');
690        for (i, (key, value)) in self
691            .inner
692            .iter()
693            .enumerate()
694        {
695            if i > 0 {
696                write!(f, "{sep}")?;
697            }
698            let value = escape_value(value, target, commas_separate, self.vars_type);
699            write!(f, "{}={}", key, value)?;
700        }
701        f.write_fmt(format_args!("{}", close))
702    }
703}
704
705impl fmt::Display for Variables {
706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707        self.write_for(f, DialStringCarrier::EslApi.into())
708    }
709}
710
711impl FromStr for Variables {
712    type Err = OriginateError;
713
714    fn from_str(s: &str) -> Result<Self, Self::Err> {
715        Self::parse_for(s, DialStringCarrier::EslApi)
716    }
717}
718
719impl Variables {
720    /// Parse a block written for a named carrier or [`DialStringTarget`],
721    /// mirroring [`display_for`](Self::display_for). [`FromStr`] uses the same
722    /// [`DialStringCarrier::EslApi`] default as [`Display`](fmt::Display), so
723    /// the two round-trip.
724    pub fn parse_for(s: &str, target: impl Into<DialStringTarget>) -> Result<Self, OriginateError> {
725        let target = target.into();
726        let s = s.trim();
727        if s.len() < 2 {
728            return Err(OriginateError::ParseError(
729                "variable block too short".into(),
730            ));
731        }
732
733        let (vars_type, inner_str) = match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
734            (b'{', b'}') => (VariablesType::Default, &s[1..s.len() - 1]),
735            (b'<', b'>') => (VariablesType::Enterprise, &s[1..s.len() - 1]),
736            (b'[', b']') => (VariablesType::Channel, &s[1..s.len() - 1]),
737            (open, close) => {
738                return Err(OriginateError::ParseError(format!(
739                    "unknown variable delimiters: {:?}..{:?}",
740                    open as char, close as char
741                )));
742            }
743        };
744
745        let (sep, var_str) = match inner_str.strip_prefix("^^") {
746            Some(rest) => {
747                let sep = rest
748                    .chars()
749                    .next()
750                    .ok_or_else(|| {
751                        OriginateError::ParseError("^^ without separator character".into())
752                    })?;
753                check_separator(sep, vars_type)?;
754                (sep, &rest[sep.len_utf8()..])
755            }
756            None => (',', inner_str),
757        };
758        let commas_separate = sep == ',';
759
760        let mut inner = IndexMap::new();
761        if !var_str.is_empty() {
762            for (i, part) in split_unescaped(var_str, sep)
763                .into_iter()
764                .enumerate()
765            {
766                let (key, value) = part
767                    .split_once('=')
768                    .ok_or_else(|| {
769                        OriginateError::ParseError(format!("missing = in variable {i}"))
770                    })?;
771                let value = unescape_value(value, target, commas_separate, vars_type);
772                check_representable(key, &value, vars_type)?;
773                inner.insert(key.to_string(), value);
774            }
775        }
776
777        Ok(Self {
778            vars_type,
779            inner,
780            separator: None,
781        })
782    }
783}
784
785/// Split on separators that are not escaped by a backslash.
786///
787/// A separator preceded by an odd number of backslashes is escaped (e.g. `\,`).
788/// One preceded by an even number is a real split point (e.g. `\\,` is an
789/// escaped backslash followed by the delimiter).
790fn split_unescaped(s: &str, sep: char) -> Vec<&str> {
791    let mut parts = Vec::new();
792    let mut start = 0;
793    let bytes = s.as_bytes();
794
795    for (i, ch) in s.char_indices() {
796        if ch == sep {
797            let mut backslashes = 0;
798            let mut j = i;
799            while j > 0 && bytes[j - 1] == b'\\' {
800                backslashes += 1;
801                j -= 1;
802            }
803            if backslashes % 2 == 0 {
804                parts.push(&s[start..i]);
805                start = i + ch.len_utf8();
806            }
807        }
808    }
809    parts.push(&s[start..]);
810    parts
811}
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816
817    /// Measured on a live switch: an empty value is discarded under every
818    /// encoding, silently, so nothing can be emitted for one and the refusal
819    /// has to name a remedy the caller could not guess.
820    #[test]
821    fn an_empty_value_is_refused_at_the_boundary() {
822        let err = "{k=,after=sentinel}"
823            .parse::<Variables>()
824            .unwrap_err()
825            .to_string();
826        assert!(err.contains('k'), "error does not name the variable: {err}");
827        assert!(
828            err.contains("remove it"),
829            "error does not name a remedy: {err}"
830        );
831        // Prescribing removal alone is wrong for a caller whose *presence* test
832        // is the signal: dropping the key silently reverses that decision.
833        assert!(
834            err.contains("presence"),
835            "error prescribes a fix without allowing that presence meant something: {err}"
836        );
837    }
838
839    /// The switch finds a block's end by counting bracket depth and honours no
840    /// escape while doing so, so a value closing a bracket it never opened ends
841    /// the block early and the rest becomes dial-string text.
842    #[test]
843    fn an_unbalanced_bracket_is_refused_while_a_balanced_one_is_not() {
844        assert!("{k=oops}extra}"
845            .parse::<Variables>()
846            .is_err());
847
848        let balanced: Variables = "{k=${some_var}}"
849            .parse()
850            .expect("a balanced ${...} is ordinary and must parse");
851        assert_eq!(balanced.get("k"), Some("${some_var}"));
852    }
853
854    /// A chosen separator is what carries a comma through a value that was
855    /// expanded before the block was parsed, so the comma must survive as
856    /// ordinary text rather than picking up an escape.
857    #[test]
858    fn chosen_separator_leaves_commas_alone() {
859        let mut vars = Variables::new(VariablesType::Default);
860        vars.insert("codecs", "PCMA,PCMU,G729");
861        let vars = vars
862            .with_separator(':')
863            .unwrap();
864
865        assert_eq!(vars.to_string(), "{^^:codecs=PCMA,PCMU,G729}");
866        assert_eq!(vars.separator(), Some(':'));
867    }
868
869    #[test]
870    fn separator_that_cannot_delimit_the_block_is_refused() {
871        let vars = Variables::new(VariablesType::Channel);
872        for sep in ['[', ']', '=', '^'] {
873            assert!(
874                vars.clone()
875                    .with_separator(sep)
876                    .is_err(),
877                "accepted {sep:?}"
878            );
879        }
880    }
881
882    /// The parser has to refuse what the builder refuses, `^` included:
883    /// accepting a block no render of this crate can reproduce hands the caller
884    /// a value that changes when it is written back out.
885    #[test]
886    fn the_parser_refuses_every_separator_the_builder_does() {
887        for sep in ['[', ']', '=', '^'] {
888            assert!(
889                format!("[^^{sep}a=1{sep}b=2]")
890                    .parse::<Variables>()
891                    .is_err(),
892                "parser accepted {sep:?}"
893            );
894        }
895    }
896
897    /// Refusing here is the whole point: a value carrying the separator would
898    /// split into a pair that was never written, and the switch reports nothing.
899    #[test]
900    fn separator_already_present_in_a_value_is_refused() {
901        let mut vars = Variables::new(VariablesType::Default);
902        vars.insert("uri", "sip:bob@example.com");
903        let err = vars
904            .with_separator(':')
905            .unwrap_err()
906            .to_string();
907        assert!(
908            err.contains("uri"),
909            "error does not name the variable: {err}"
910        );
911    }
912
913    /// Measured against a live switch, with two awkward values in one block —
914    /// a block carrying only one is more forgiving and hides the failure.
915    /// Sweeping the backslash count, each carrier succeeds at counts the other
916    /// fails, so these forms are the wire contract and not a preference.
917    #[test]
918    fn escaping_pins_the_measured_wire_forms() {
919        let cases = [
920            (DialStringCarrier::Dialplan, "it's", r"it\\\\\\'s"),
921            (DialStringCarrier::EslApi, "it's", r"it\\\\\\\'s"),
922            (DialStringCarrier::Dialplan, "l'a'b", r"l\\\\\\'a\\\\\\'b"),
923            (DialStringCarrier::EslApi, "l'a'b", r"l\\\\\\\'a\\\\\\\'b"),
924            (DialStringCarrier::Dialplan, r"a\nb", r"a\\\\\\\\nb"),
925            (DialStringCarrier::EslApi, r"a\nb", r"a\\\\\\\\nb"),
926            (DialStringCarrier::Dialplan, "a,b", r"a\,b"),
927            (DialStringCarrier::EslApi, "a,b", r"a\,b"),
928            (DialStringCarrier::Dialplan, "a|b", "a|b"),
929            (DialStringCarrier::EslApi, "a|b", "a|b"),
930        ];
931        for (carrier, value, want) in cases {
932            assert_eq!(
933                escape_value(value, carrier, true, VariablesType::Default),
934                want,
935                "{value:?} for {carrier:?}"
936            );
937        }
938    }
939
940    /// A `[]` block rides through the leg splits before it is parsed: two more
941    /// backslash-consuming passes, and a pipe the first of them would read.
942    #[test]
943    fn channel_scope_escapes_for_the_leg_splits() {
944        let cases = [
945            (
946                DialStringCarrier::Dialplan,
947                r"a\nb",
948                r"a\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nb",
949            ),
950            (
951                DialStringCarrier::EslApi,
952                r"a\nb",
953                r"a\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nb",
954            ),
955            (DialStringCarrier::Dialplan, "a|b", r"a\|b"),
956            (DialStringCarrier::EslApi, "a|b", r"a\|b"),
957            (DialStringCarrier::EslApi, "a,b", r"a\,b"),
958        ];
959        for (carrier, value, want) in cases {
960            assert_eq!(
961                escape_value(value, carrier, true, VariablesType::Channel),
962                want,
963                "{value:?} for {carrier:?}"
964            );
965        }
966    }
967
968    /// A `^^` block reaches the switch's tokenizer as often as the comma form,
969    /// so every rule above still holds there; only the comma stops being the
970    /// separator and so stops being escaped.
971    #[test]
972    fn a_chosen_separator_changes_only_the_comma() {
973        let cases = [
974            (DialStringCarrier::Dialplan, "it's", r"it\\\\\\'s"),
975            (DialStringCarrier::EslApi, "it's", r"it\\\\\\\'s"),
976            (DialStringCarrier::Dialplan, "l'a'b", r"l\\\\\\'a\\\\\\'b"),
977            (DialStringCarrier::EslApi, "l'a'b", r"l\\\\\\\'a\\\\\\\'b"),
978            (DialStringCarrier::Dialplan, r"a\nb", r"a\\\\\\\\nb"),
979            (DialStringCarrier::EslApi, r"a\nb", r"a\\\\\\\\nb"),
980            (DialStringCarrier::Dialplan, "a,b", "a,b"),
981            (DialStringCarrier::EslApi, "a,b", "a,b"),
982        ];
983        for (carrier, value, want) in cases {
984            assert_eq!(
985                escape_value(value, carrier, false, VariablesType::Default),
986                want,
987                "{value:?} for {carrier:?}"
988            );
989        }
990    }
991
992    /// The crate exists to drive `api originate`, so a block rendered with no
993    /// carrier named is rendered for that one.
994    #[test]
995    fn display_defaults_to_the_api_carrier() {
996        let mut vars = Variables::new(VariablesType::Default);
997        vars.insert("k", "it's");
998        assert_eq!(vars.to_string(), r"{k=it\\\\\\\'s}");
999        assert_eq!(
1000            vars.display_for(DialStringCarrier::Dialplan)
1001                .to_string(),
1002            r"{k=it\\\\\\'s}"
1003        );
1004    }
1005
1006    #[test]
1007    fn round_trips_at_either_carrier() {
1008        for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
1009            for value in ["it's", r"a\,b", r"C:\path", "a,b", "with space"] {
1010                let mut vars = Variables::new(VariablesType::Default);
1011                vars.insert("k", value);
1012                vars.insert("after", "sentinel");
1013                let rendered = vars
1014                    .display_for(carrier)
1015                    .to_string();
1016
1017                let back = Variables::parse_for(&rendered, carrier).unwrap_or_else(|e| {
1018                    panic!("{value:?} for {carrier:?} rendered {rendered}: {e}")
1019                });
1020                assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
1021                assert_eq!(back.get("after"), Some("sentinel"), "rendered {rendered}");
1022            }
1023        }
1024    }
1025
1026    /// A chosen separator changes which character needs escaping, not whether
1027    /// the block is escape-processed: the switch runs the same tokenizer over
1028    /// it, so a value still comes back through the same undoing.
1029    #[test]
1030    fn separated_block_round_trips_at_either_carrier() {
1031        for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
1032            for value in ["it's", "a,b", r"C:\path", r"a\nb", "with space"] {
1033                let mut vars = Variables::new(VariablesType::Default);
1034                vars.insert("k", value);
1035                vars.insert("after", "sentinel");
1036                let vars = vars
1037                    .with_separator('~')
1038                    .expect("'~' appears in none of these values");
1039                let rendered = vars
1040                    .display_for(carrier)
1041                    .to_string();
1042
1043                let back = Variables::parse_for(&rendered, carrier).unwrap_or_else(|e| {
1044                    panic!("{value:?} for {carrier:?} rendered {rendered}: {e}")
1045                });
1046                assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
1047                assert_eq!(back.get("after"), Some("sentinel"), "rendered {rendered}");
1048            }
1049        }
1050    }
1051
1052    /// `split_unescaped_commas` reads a comma behind an even number of
1053    /// backslashes as a real separator, so a value ending in a backslash has to
1054    /// be written with its own backslash escaped or the writer contradicts the
1055    /// reader and the block no longer parses.
1056    #[test]
1057    fn value_with_backslash_round_trips() {
1058        for value in [r"a\,b", r"C:\path", r"trailing\", r"\\", r"a\nb"] {
1059            let mut vars = Variables::new(VariablesType::Default);
1060            vars.insert("k", value);
1061            vars.insert("after", "sentinel");
1062            let rendered = vars.to_string();
1063
1064            let back: Variables = rendered
1065                .parse()
1066                .unwrap_or_else(|e| {
1067                    panic!("{value:?} rendered {rendered} and failed to parse: {e}")
1068                });
1069            assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
1070            assert_eq!(
1071                back.get("after"),
1072                Some("sentinel"),
1073                "value {value:?} ate the next variable: {rendered}"
1074            );
1075        }
1076    }
1077
1078    /// A variable block holds dialled numbers and passthrough header values, so a
1079    /// malformed part is reported by position.
1080    #[test]
1081    fn missing_equals_error_omits_the_fragment() {
1082        let msg = "{origination_caller_id_number=15551234567,15550009999}"
1083            .parse::<Variables>()
1084            .unwrap_err()
1085            .to_string();
1086        assert!(
1087            !msg.contains("15550009999"),
1088            "error quoted its input: {msg}"
1089        );
1090        assert!(
1091            msg.contains("variable 1"),
1092            "error does not name the part: {msg}"
1093        );
1094    }
1095
1096    #[test]
1097    fn unknown_delimiters_error_omits_the_block() {
1098        let msg = "(origination_caller_id_number=15551234567)"
1099            .parse::<Variables>()
1100            .unwrap_err()
1101            .to_string();
1102        assert!(
1103            !msg.contains("15551234567"),
1104            "error quoted its input: {msg}"
1105        );
1106    }
1107
1108    #[test]
1109    fn variables_standard_chars() {
1110        let mut vars = Variables::new(VariablesType::Default);
1111        vars.insert("test_key", "this_value");
1112        vars.insert("second", "2");
1113        assert_eq!(vars.to_string(), "{test_key=this_value,second=2}");
1114    }
1115
1116    #[test]
1117    fn variables_comma_escaped() {
1118        let mut vars = Variables::new(VariablesType::Default);
1119        vars.insert("test_key", "this,is,a,value");
1120        let result = vars.to_string();
1121        assert!(result.contains("\\,"));
1122    }
1123
1124    #[test]
1125    fn variables_spaces_quoted() {
1126        let mut vars = Variables::new(VariablesType::Default);
1127        vars.insert("test_key", "this is a value");
1128        let result = vars.to_string();
1129        assert_eq!(
1130            result
1131                .matches('\'')
1132                .count(),
1133            2
1134        );
1135    }
1136
1137    #[test]
1138    fn variables_single_quote_escaped() {
1139        let mut vars = Variables::new(VariablesType::Default);
1140        vars.insert("test_key", "let's_this_be_a_value");
1141        let result = vars.to_string();
1142        assert!(result.contains("\\'"));
1143    }
1144
1145    #[test]
1146    fn variables_enterprise_delimiters() {
1147        let mut vars = Variables::new(VariablesType::Enterprise);
1148        vars.insert("k", "v");
1149        let result = vars.to_string();
1150        assert!(result.starts_with('<'));
1151        assert!(result.ends_with('>'));
1152    }
1153
1154    #[test]
1155    fn variables_channel_delimiters() {
1156        let mut vars = Variables::new(VariablesType::Channel);
1157        vars.insert("k", "v");
1158        let result = vars.to_string();
1159        assert!(result.starts_with('['));
1160        assert!(result.ends_with(']'));
1161    }
1162
1163    #[test]
1164    fn variables_default_delimiters() {
1165        let mut vars = Variables::new(VariablesType::Default);
1166        vars.insert("k", "v");
1167        let result = vars.to_string();
1168        assert!(result.starts_with('{'));
1169        assert!(result.ends_with('}'));
1170    }
1171
1172    #[test]
1173    fn variables_parse_round_trip() {
1174        let mut vars = Variables::new(VariablesType::Default);
1175        vars.insert("origination_caller_id_number", "9005551212");
1176        vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
1177        let s = vars.to_string();
1178        let parsed: Variables = s
1179            .parse()
1180            .unwrap();
1181        assert_eq!(
1182            parsed.get("origination_caller_id_number"),
1183            Some("9005551212")
1184        );
1185        assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
1186    }
1187
1188    #[test]
1189    fn split_unescaped_basic() {
1190        assert_eq!(split_unescaped("a,b,c", ','), vec!["a", "b", "c"]);
1191        assert_eq!(split_unescaped("a~b~c", '~'), vec!["a", "b", "c"]);
1192    }
1193
1194    #[test]
1195    fn split_unescaped_escaped() {
1196        assert_eq!(split_unescaped(r"a\,b,c", ','), vec![r"a\,b", "c"]);
1197    }
1198
1199    #[test]
1200    fn split_unescaped_double_backslash() {
1201        // \\, = escaped backslash + comma delimiter
1202        assert_eq!(split_unescaped(r"a\\,b", ','), vec![r"a\\", "b"]);
1203    }
1204
1205    #[test]
1206    fn split_unescaped_triple_backslash() {
1207        // \\\, = escaped backslash + escaped comma (no split)
1208        assert_eq!(split_unescaped(r"a\\\,b", ','), vec![r"a\\\,b"]);
1209    }
1210
1211    #[test]
1212    fn variables_caret_caret_separator() {
1213        let vars: Variables =
1214            "[^^:sip_invite_domain=pbx.example.com:presence_id=1211@pbx.example.com]"
1215                .parse()
1216                .unwrap();
1217        assert_eq!(vars.scope(), VariablesType::Channel);
1218        assert_eq!(vars.get("sip_invite_domain"), Some("pbx.example.com"));
1219        assert_eq!(vars.get("presence_id"), Some("1211@pbx.example.com"));
1220    }
1221
1222    #[test]
1223    fn variables_caret_caret_display_uses_canonical_comma() {
1224        let vars: Variables = "[^^:a=1:b=2]"
1225            .parse()
1226            .unwrap();
1227        assert_eq!(vars.to_string(), "[a=1,b=2]");
1228    }
1229
1230    #[test]
1231    fn variables_caret_caret_default_scope() {
1232        let vars: Variables = "{^^|x=1|y=2}"
1233            .parse()
1234            .unwrap();
1235        assert_eq!(vars.scope(), VariablesType::Default);
1236        assert_eq!(vars.get("x"), Some("1"));
1237        assert_eq!(vars.get("y"), Some("2"));
1238    }
1239
1240    #[test]
1241    fn variables_caret_caret_enterprise_scope() {
1242        let vars: Variables = "<^^;a=1;b=2>"
1243            .parse()
1244            .unwrap();
1245        assert_eq!(vars.scope(), VariablesType::Enterprise);
1246        assert_eq!(vars.get("a"), Some("1"));
1247    }
1248
1249    /// The switch consumes a backslash only before a quote, another backslash,
1250    /// the separator in force, or a character it has an escape for -- so in a
1251    /// block separated on something else, `\,` is two literal characters.
1252    #[test]
1253    fn variables_caret_caret_keeps_an_escaped_comma_literal() {
1254        let vars: Variables = r"[^^:key=val\,ue:other=x]"
1255            .parse()
1256            .unwrap();
1257        assert_eq!(vars.get("key"), Some(r"val\,ue"));
1258    }
1259
1260    #[test]
1261    fn variables_caret_caret_values_with_commas() {
1262        let vars: Variables = "{^^|sip_h_X-Call-Info=<urn:foo>;purpose=bar,<urn:baz>|other=val}"
1263            .parse()
1264            .unwrap();
1265        assert_eq!(
1266            vars.get("sip_h_X-Call-Info"),
1267            Some("<urn:foo>;purpose=bar,<urn:baz>")
1268        );
1269        assert_eq!(vars.get("other"), Some("val"));
1270    }
1271
1272    #[test]
1273    fn variables_caret_caret_empty_vars() {
1274        let vars: Variables = "[^^:]"
1275            .parse()
1276            .unwrap();
1277        assert!(vars.is_empty());
1278        assert_eq!(vars.scope(), VariablesType::Channel);
1279    }
1280
1281    #[test]
1282    fn variables_caret_caret_missing_separator() {
1283        assert!("[^^]"
1284            .parse::<Variables>()
1285            .is_err());
1286    }
1287
1288    #[test]
1289    fn variables_caret_caret_closing_bracket_as_sep() {
1290        assert!("[^^]]"
1291            .parse::<Variables>()
1292            .is_err());
1293    }
1294
1295    #[test]
1296    fn variables_caret_caret_equals_as_sep() {
1297        assert!("[^^=a=1]"
1298            .parse::<Variables>()
1299            .is_err());
1300    }
1301
1302    #[test]
1303    fn variables_from_str_empty_block() {
1304        let result = "{}".parse::<Variables>();
1305        assert!(
1306            result.is_ok(),
1307            "empty variable block should parse successfully"
1308        );
1309        let vars = result.unwrap();
1310        assert!(
1311            vars.is_empty(),
1312            "parsed empty block should have no variables"
1313        );
1314    }
1315
1316    #[test]
1317    fn variables_from_str_empty_channel_block() {
1318        let result = "[]".parse::<Variables>();
1319        assert!(result.is_ok());
1320        let vars = result.unwrap();
1321        assert!(vars.is_empty());
1322        assert_eq!(vars.scope(), VariablesType::Channel);
1323    }
1324
1325    #[test]
1326    fn variables_from_str_empty_enterprise_block() {
1327        let result = "<>".parse::<Variables>();
1328        assert!(result.is_ok());
1329        let vars = result.unwrap();
1330        assert!(vars.is_empty());
1331        assert_eq!(vars.scope(), VariablesType::Enterprise);
1332    }
1333
1334    #[test]
1335    fn serde_variables_type() {
1336        let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1337        assert_eq!(json, "\"enterprise\"");
1338        let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1339        assert_eq!(parsed, VariablesType::Enterprise);
1340    }
1341
1342    #[test]
1343    fn serde_variables_flat_default() {
1344        let mut vars = Variables::new(VariablesType::Default);
1345        vars.insert("key1", "val1");
1346        vars.insert("key2", "val2");
1347        let json = serde_json::to_string(&vars).unwrap();
1348        let parsed: Variables = serde_json::from_str(&json).unwrap();
1349        assert_eq!(parsed.scope(), VariablesType::Default);
1350        assert_eq!(parsed.get("key1"), Some("val1"));
1351        assert_eq!(parsed.get("key2"), Some("val2"));
1352    }
1353
1354    #[test]
1355    fn serde_variables_scoped_enterprise() {
1356        let mut vars = Variables::new(VariablesType::Enterprise);
1357        vars.insert("key1", "val1");
1358        let json = serde_json::to_string(&vars).unwrap();
1359        assert!(json.contains("\"enterprise\""));
1360        let parsed: Variables = serde_json::from_str(&json).unwrap();
1361        assert_eq!(parsed.scope(), VariablesType::Enterprise);
1362        assert_eq!(parsed.get("key1"), Some("val1"));
1363    }
1364
1365    #[test]
1366    fn serde_variables_flat_map_deserializes_as_default() {
1367        let json = r#"{"key1":"val1","key2":"val2"}"#;
1368        let vars: Variables = serde_json::from_str(json).unwrap();
1369        assert_eq!(vars.scope(), VariablesType::Default);
1370        assert_eq!(vars.get("key1"), Some("val1"));
1371        assert_eq!(vars.get("key2"), Some("val2"));
1372    }
1373
1374    #[test]
1375    fn serde_variables_scoped_deserializes() {
1376        let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1377        let vars: Variables = serde_json::from_str(json).unwrap();
1378        assert_eq!(vars.scope(), VariablesType::Channel);
1379        assert_eq!(vars.get("k"), Some("v"));
1380    }
1381
1382    /// A block whose separator is dropped by a round trip renders as a comma
1383    /// block, which splits the very value the separator was chosen to carry.
1384    #[test]
1385    fn serde_variables_separator_round_trips() {
1386        let mut vars = Variables::new(VariablesType::Default);
1387        vars.insert("codecs", "PCMA,PCMU");
1388        let vars = vars
1389            .with_separator('|')
1390            .unwrap();
1391
1392        let json = serde_json::to_string(&vars).unwrap();
1393        let parsed: Variables = serde_json::from_str(&json).unwrap();
1394        assert_eq!(parsed.separator(), Some('|'));
1395        assert_eq!(parsed, vars);
1396        assert_eq!(parsed.to_string(), "{^^|codecs=PCMA,PCMU}");
1397    }
1398
1399    /// The default separator has no key, so a config written before the key
1400    /// existed serializes back byte-identical.
1401    #[test]
1402    fn serde_variables_default_separator_is_absent() {
1403        let mut flat = Variables::new(VariablesType::Default);
1404        flat.insert("k", "v");
1405        assert_eq!(serde_json::to_string(&flat).unwrap(), r#"{"k":"v"}"#);
1406
1407        let mut scoped = Variables::new(VariablesType::Channel);
1408        scoped.insert("k", "v");
1409        assert_eq!(
1410            serde_json::to_string(&scoped).unwrap(),
1411            r#"{"scope":"channel","vars":{"k":"v"}}"#
1412        );
1413    }
1414
1415    #[test]
1416    fn serde_variables_separator_deserializes_from_config() {
1417        let json = r#"{"scope":"channel","vars":{"codecs":"PCMA,PCMU"},"separator":":"}"#;
1418        let vars: Variables = serde_json::from_str(json).unwrap();
1419        assert_eq!(vars.scope(), VariablesType::Channel);
1420        assert_eq!(vars.separator(), Some(':'));
1421        assert_eq!(vars.to_string(), "[^^:codecs=PCMA,PCMU]");
1422    }
1423
1424    /// The leg split reads a `|` before a `[]` block is parsed, so it can
1425    /// separate nothing there; in `{}` it is ordinary.
1426    #[test]
1427    fn pipe_separator_is_refused_in_channel_scope_only() {
1428        let mut vars = Variables::new(VariablesType::Channel);
1429        vars.insert("k", "v");
1430        assert!(vars
1431            .clone()
1432            .with_separator('|')
1433            .is_err());
1434        let mut vars = Variables::new(VariablesType::Default);
1435        vars.insert("k", "v");
1436        assert!(vars
1437            .with_separator('|')
1438            .is_ok());
1439    }
1440
1441    /// The builder's two refusals have to hold at the config boundary too, or a
1442    /// YAML file produces a block the same crate would not build.
1443    #[test]
1444    fn serde_variables_unusable_separator_is_refused() {
1445        let undelimitable = r#"{"scope":"default","vars":{"k":"v"},"separator":"="}"#;
1446        assert!(serde_json::from_str::<Variables>(undelimitable).is_err());
1447
1448        let in_a_value =
1449            r#"{"scope":"default","vars":{"uri":"sip:bob@example.com"},"separator":":"}"#;
1450        let err = serde_json::from_str::<Variables>(in_a_value)
1451            .unwrap_err()
1452            .to_string();
1453        assert!(
1454            err.contains("uri"),
1455            "error does not name the variable: {err}"
1456        );
1457    }
1458
1459    /// Measured: `[p1=it's,p2=don't,p3=x]` reaches the channel as
1460    /// `p1=its,p2=dont` with no `p2`, at every escaping depth, because the scan
1461    /// ahead of the peer split pairs quotes across values. The same value in
1462    /// default scope is ordinary.
1463    #[test]
1464    fn a_quote_in_channel_scope_is_refused_at_every_boundary() {
1465        let err = r"[cid=it\\\\\\\'s]"
1466            .parse::<Variables>()
1467            .unwrap_err()
1468            .to_string();
1469        assert!(
1470            err.contains("cid") && err.contains("channel scope"),
1471            "error does not name the variable and scope: {err}"
1472        );
1473
1474        let err = serde_json::from_str::<Variables>(r#"{"scope":"channel","vars":{"cid":"it's"}}"#)
1475            .unwrap_err()
1476            .to_string();
1477        assert!(
1478            err.contains("cid") && err.contains("channel scope"),
1479            "error does not name the variable and scope: {err}"
1480        );
1481
1482        assert!(r"{cid=it\\\\\\\'s}"
1483            .parse::<Variables>()
1484            .is_ok());
1485        assert!(
1486            serde_json::from_str::<Variables>(r#"{"scope":"default","vars":{"cid":"it's"}}"#)
1487                .is_ok()
1488        );
1489    }
1490
1491    /// Every revision a block can be rendered for; a new variant joins this list.
1492    const REVISIONS: &[BlockParse] = &[BlockParse::PairSplitCleans];
1493
1494    #[test]
1495    fn a_bare_carrier_is_a_target_at_the_default_revision() {
1496        let target = DialStringTarget::from(DialStringCarrier::Dialplan);
1497        assert_eq!(target.carrier(), DialStringCarrier::Dialplan);
1498        assert_eq!(target.block_parse(), BlockParse::default());
1499        assert_eq!(BlockParse::default(), BlockParse::PairSplitCleans);
1500        assert_eq!(
1501            DialStringTarget::new(DialStringCarrier::EslApi)
1502                .with_block_parse(BlockParse::PairSplitCleans)
1503                .block_parse(),
1504            BlockParse::PairSplitCleans
1505        );
1506    }
1507
1508    /// An enterprise block is parsed at the same depth as a default one; nothing
1509    /// else pins its forms.
1510    #[test]
1511    fn enterprise_scope_escapes_like_default_scope() {
1512        let cases = [
1513            (DialStringCarrier::Dialplan, "it's", r"it\\\\\\'s"),
1514            (DialStringCarrier::EslApi, "it's", r"it\\\\\\\'s"),
1515            (DialStringCarrier::EslApi, r"a\nb", r"a\\\\\\\\nb"),
1516            (DialStringCarrier::EslApi, "a,b", r"a\,b"),
1517        ];
1518        for (carrier, value, want) in cases {
1519            let target =
1520                DialStringTarget::new(carrier).with_block_parse(BlockParse::PairSplitCleans);
1521            assert_eq!(
1522                escape_value(value, target, true, VariablesType::Enterprise),
1523                want,
1524                "{value:?} for {carrier:?}"
1525            );
1526        }
1527    }
1528
1529    #[test]
1530    fn round_trips_at_every_target() {
1531        let cases = [
1532            (
1533                VariablesType::Default,
1534                &["it's", r"C:\path", "a,b", r"a\nb", "with space"][..],
1535            ),
1536            (
1537                VariablesType::Enterprise,
1538                &["it's", r"C:\path", "a,b", "with space"][..],
1539            ),
1540            (
1541                VariablesType::Channel,
1542                &[r"C:\path", "a,b", "a|b", "with space"][..],
1543            ),
1544        ];
1545        for &block_parse in REVISIONS {
1546            for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
1547                let target = DialStringTarget::new(carrier).with_block_parse(block_parse);
1548                for (scope, values) in cases {
1549                    for &value in values {
1550                        let mut vars = Variables::new(scope);
1551                        vars.insert("k", value);
1552                        vars.insert("after", "sentinel");
1553                        let rendered = vars
1554                            .display_for(target)
1555                            .to_string();
1556
1557                        let back = Variables::parse_for(&rendered, target).unwrap_or_else(|e| {
1558                            panic!("{value:?} for {target:?} rendered {rendered}: {e}")
1559                        });
1560                        assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
1561                        assert_eq!(back.get("after"), Some("sentinel"), "rendered {rendered}");
1562                    }
1563                }
1564            }
1565        }
1566    }
1567
1568    /// What the switch cannot deliver is decided before any block parse runs, so
1569    /// no revision makes one of these representable.
1570    #[test]
1571    fn refusals_do_not_depend_on_the_revision() {
1572        for &block_parse in REVISIONS {
1573            for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
1574                let target = DialStringTarget::new(carrier).with_block_parse(block_parse);
1575                for block in [
1576                    r"[cid=it\\\\\\\'s]",
1577                    "{k=,after=sentinel}",
1578                    "{k=oops}extra}",
1579                    "{^^=a=1=b=2}",
1580                    "[^^|a=1|b=2]",
1581                ] {
1582                    assert!(
1583                        Variables::parse_for(block, target).is_err(),
1584                        "{block} accepted at {target:?}"
1585                    );
1586                }
1587            }
1588        }
1589    }
1590
1591    #[test]
1592    fn block_parse_reads_config_spellings_and_writes_the_canonical_one() {
1593        for spelling in [
1594            "pair_split_cleans",
1595            "PAIR_SPLIT_CLEANS",
1596            "Pair_Split_Cleans",
1597        ] {
1598            assert_eq!(
1599                spelling
1600                    .parse::<BlockParse>()
1601                    .ok(),
1602                Some(BlockParse::PairSplitCleans),
1603                "{spelling}"
1604            );
1605        }
1606        assert_eq!(BlockParse::PairSplitCleans.to_string(), "pair_split_cleans");
1607
1608        let err = "pair_split_whatever"
1609            .parse::<BlockParse>()
1610            .unwrap_err()
1611            .to_string();
1612        assert!(!err.contains("whatever"), "error quoted its input: {err}");
1613    }
1614
1615    #[test]
1616    fn serde_block_parse_uses_the_config_spelling() {
1617        assert_eq!(
1618            serde_json::to_string(&BlockParse::PairSplitCleans).unwrap(),
1619            r#""pair_split_cleans""#
1620        );
1621        let parsed: BlockParse = serde_json::from_str(r#""pair_split_cleans""#).unwrap();
1622        assert_eq!(parsed, BlockParse::PairSplitCleans);
1623    }
1624
1625    #[test]
1626    fn for_version_maps_a_vouched_release() {
1627        for version in [
1628            FreeswitchVersion::new(1, 10, 0),
1629            FreeswitchVersion::new(1, 10, 7),
1630            FreeswitchVersion::new(1, 10, 12),
1631        ] {
1632            assert_eq!(
1633                BlockParse::for_version(&version).ok(),
1634                Some(BlockParse::PairSplitCleans),
1635                "{version}"
1636            );
1637        }
1638    }
1639
1640    /// A dev build reports one version across every commit, so even one below
1641    /// the vouched range's last release is refused.
1642    #[test]
1643    fn for_version_refuses_what_it_cannot_vouch_for() {
1644        for version in [
1645            FreeswitchVersion::new(1, 10, 12).dev(),
1646            FreeswitchVersion::new(1, 10, 5).dev(),
1647        ] {
1648            let err = BlockParse::for_version(&version).expect_err(&version.to_string());
1649            assert!(
1650                matches!(err, UnvouchedVersion::Dev { .. }),
1651                "{version}: {err:?}"
1652            );
1653            assert_eq!(err.version(), version);
1654        }
1655        for version in [
1656            FreeswitchVersion::new(1, 10, 13),
1657            FreeswitchVersion::new(1, 11, 1),
1658        ] {
1659            let err = BlockParse::for_version(&version).expect_err(&version.to_string());
1660            assert!(
1661                matches!(err, UnvouchedVersion::NewerThanVouched { .. }),
1662                "{version}: {err:?}"
1663            );
1664        }
1665        let version = FreeswitchVersion::new(1, 8, 7);
1666        let err = BlockParse::for_version(&version).expect_err(&version.to_string());
1667        assert!(
1668            matches!(err, UnvouchedVersion::OlderThanVouched { .. }),
1669            "{version}: {err:?}"
1670        );
1671    }
1672
1673    /// The caller has to decide what to name instead, so the refusal says what
1674    /// would have been accepted.
1675    #[test]
1676    fn an_unvouched_version_names_the_vouched_range() {
1677        let msg = BlockParse::for_version(&FreeswitchVersion::new(1, 11, 1))
1678            .unwrap_err()
1679            .to_string();
1680        assert!(
1681            msg.contains("1.10.0") && msg.contains("1.10.12"),
1682            "does not name the vouched range: {msg}"
1683        );
1684    }
1685}