use indexmap::IndexMap;
use std::fmt;
use std::str::FromStr;
use super::originate::OriginateError;
use crate::version::FreeswitchVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
#[non_exhaustive]
pub enum VariablesType {
Enterprise,
Default,
Channel,
}
impl VariablesType {
pub(super) fn delimiters(self) -> (char, char) {
match self {
Self::Enterprise => ('<', '>'),
Self::Default => ('{', '}'),
Self::Channel => ('[', ']'),
}
}
fn leg_split_passes(self) -> u32 {
match self {
Self::Enterprise | Self::Default => 0,
Self::Channel => 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Variables {
vars_type: VariablesType,
inner: IndexMap<String, String>,
separator: Option<char>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum DialStringCarrier {
EslApi,
Dialplan,
}
impl DialStringCarrier {
fn deletes_escaped_quote(self) -> bool {
matches!(self, Self::Dialplan)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum BlockParse {
#[default]
PairSplitCleans,
}
impl BlockParse {
const ALL: &'static [Self] = &[Self::PairSplitCleans];
fn as_str(self) -> &'static str {
match self {
Self::PairSplitCleans => "pair_split_cleans",
}
}
fn cleanup_passes(self) -> u32 {
match self {
Self::PairSplitCleans => 2,
}
}
pub fn for_version(version: &FreeswitchVersion) -> Result<Self, UnvouchedVersion> {
let version = *version;
let (first, last) = VOUCHED_PAIR_SPLIT_CLEANS;
if version.is_dev() {
Err(UnvouchedVersion::Dev { version })
} else if version < first {
Err(UnvouchedVersion::OlderThanVouched { version })
} else if version > last {
Err(UnvouchedVersion::NewerThanVouched { version })
} else {
Ok(Self::PairSplitCleans)
}
}
}
const VOUCHED_PAIR_SPLIT_CLEANS: (FreeswitchVersion, FreeswitchVersion) = (
FreeswitchVersion::new(1, 10, 0),
FreeswitchVersion::new(1, 10, 12),
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnvouchedVersion {
Dev {
version: FreeswitchVersion,
},
NewerThanVouched {
version: FreeswitchVersion,
},
OlderThanVouched {
version: FreeswitchVersion,
},
}
impl UnvouchedVersion {
pub fn version(&self) -> FreeswitchVersion {
match *self {
Self::Dev { version }
| Self::NewerThanVouched { version }
| Self::OlderThanVouched { version } => version,
}
}
}
impl fmt::Display for UnvouchedVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (first, last) = VOUCHED_PAIR_SPLIT_CLEANS;
let why = match self {
Self::Dev { .. } => "is a development build",
Self::NewerThanVouched { .. } => "is newer than any vouched release",
Self::OlderThanVouched { .. } => "is older than any vouched release",
};
write!(
f,
"FreeSWITCH {} {why}; releases {first} to {last} are vouched for, name a BlockParse explicitly",
self.version()
)
}
}
impl std::error::Error for UnvouchedVersion {}
impl fmt::Display for BlockParse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for BlockParse {
type Err = ParseBlockParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL
.iter()
.copied()
.find(|revision| {
revision
.as_str()
.eq_ignore_ascii_case(s)
})
.ok_or_else(|| ParseBlockParseError(s.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseBlockParseError(pub String);
impl fmt::Display for ParseBlockParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unknown block-parse revision, expected one of:")?;
for revision in BlockParse::ALL {
write!(f, " {revision}")?;
}
Ok(())
}
}
impl std::error::Error for ParseBlockParseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DialStringTarget {
carrier: DialStringCarrier,
block_parse: BlockParse,
}
impl DialStringTarget {
pub fn new(carrier: DialStringCarrier) -> Self {
Self {
carrier,
block_parse: BlockParse::default(),
}
}
pub fn with_block_parse(mut self, block_parse: BlockParse) -> Self {
self.block_parse = block_parse;
self
}
pub fn carrier(&self) -> DialStringCarrier {
self.carrier
}
pub fn block_parse(&self) -> BlockParse {
self.block_parse
}
fn passes(self, scope: VariablesType) -> u32 {
1 + self
.block_parse
.cleanup_passes()
+ scope.leg_split_passes()
}
fn backslash_escape(self, scope: VariablesType) -> String {
"\\".repeat(1 << self.passes(scope))
}
fn quote_escape(self, scope: VariablesType) -> String {
let consumed_by_carrier = if self
.carrier
.deletes_escaped_quote()
{
2
} else {
1
};
let run = (1usize << self.passes(scope)) - consumed_by_carrier;
format!("{}'", "\\".repeat(run))
}
}
impl From<DialStringCarrier> for DialStringTarget {
fn from(carrier: DialStringCarrier) -> Self {
Self::new(carrier)
}
}
fn check_representable(
key: &str,
value: &str,
vars_type: VariablesType,
) -> Result<(), OriginateError> {
if vars_type == VariablesType::Channel && value.contains('\'') {
return Err(OriginateError::ParseError(format!(
"variable {key} carries a single quote in channel scope: the switch \
pairs it with the next quote in the dial string before it parses \
the block, whatever escaping precedes either. Use default scope, \
or keep the quote out of the value"
)));
}
if value.is_empty() {
return Err(OriginateError::ParseError(format!(
"variable {key} has an empty value: the switch discards such a pair \
without logging it, so it cannot be told from an absent variable on \
the wire. Give it a value or remove it -- and if its presence was \
itself the signal, that signal needs a home outside the dial string"
)));
}
let (open, close) = vars_type.delimiters();
let mut depth = 0i32;
for ch in value.chars() {
if ch == open {
depth += 1;
} else if ch == close {
depth -= 1;
if depth < 0 {
return Err(OriginateError::ParseError(format!(
"variable {key} closes a '{open}' it never opened, ending the block early"
)));
}
}
}
if depth != 0 {
return Err(OriginateError::ParseError(format!(
"variable {key} opens a '{open}' it never closes, swallowing the block's end"
)));
}
Ok(())
}
fn check_separator(sep: char, vars_type: VariablesType) -> Result<(), OriginateError> {
let (open, close) = vars_type.delimiters();
if sep == open
|| sep == close
|| sep == '='
|| sep == '^'
|| (sep == '|' && vars_type == VariablesType::Channel)
{
return Err(OriginateError::ParseError(format!(
"invalid ^^ separator: '{sep}'"
)));
}
Ok(())
}
fn escape_value(
value: &str,
target: impl Into<DialStringTarget>,
commas_separate: bool,
vars_type: VariablesType,
) -> String {
let target = target.into();
let escaped = value
.replace('\\', &target.backslash_escape(vars_type))
.replace('\'', &target.quote_escape(vars_type));
let escaped = if commas_separate {
escaped.replace(',', "\\,")
} else {
escaped
};
let escaped = if vars_type == VariablesType::Channel {
escaped.replace('|', "\\|")
} else {
escaped
};
if escaped.contains(' ') {
format!("'{}'", escaped)
} else {
escaped
}
}
fn unescape_value(
value: &str,
target: DialStringTarget,
commas_separate: bool,
vars_type: VariablesType,
) -> String {
let s = value
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or(value);
let s = if vars_type == VariablesType::Channel {
s.replace("\\|", "|")
} else {
s.to_string()
};
let s = if commas_separate {
s.replace("\\,", ",")
} else {
s
};
s.replace(&target.quote_escape(vars_type), "'")
.replace(&target.backslash_escape(vars_type), "\\")
}
impl Variables {
pub fn new(vars_type: VariablesType) -> Self {
Self {
vars_type,
inner: IndexMap::new(),
separator: None,
}
}
pub fn with_vars(
vars_type: VariablesType,
vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
Self {
vars_type,
inner: vars
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
separator: None,
}
}
pub fn with_separator(mut self, sep: char) -> Result<Self, OriginateError> {
check_separator(sep, self.vars_type)?;
if let Some((key, _)) = self
.inner
.iter()
.find(|(_, v)| v.contains(sep))
{
return Err(OriginateError::ParseError(format!(
"variable {key} contains the chosen '{sep}' separator"
)));
}
self.separator = Some(sep);
Ok(self)
}
pub fn separator(&self) -> Option<char> {
self.separator
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.inner
.insert(key.into(), value.into());
}
pub fn remove(&mut self, key: &str) -> Option<String> {
self.inner
.shift_remove(key)
}
pub fn get(&self, key: &str) -> Option<&str> {
self.inner
.get(key)
.map(|s| s.as_str())
}
pub fn is_empty(&self) -> bool {
self.inner
.is_empty()
}
pub fn len(&self) -> usize {
self.inner
.len()
}
pub fn scope(&self) -> VariablesType {
self.vars_type
}
pub fn set_scope(&mut self, scope: VariablesType) {
self.vars_type = scope;
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.inner
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut String)> {
self.inner
.iter_mut()
.map(|(k, v)| (k.as_str(), v))
}
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
self.inner
.values_mut()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Variables {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if self.vars_type == VariablesType::Default
&& self
.separator
.is_none()
{
self.inner
.serialize(serializer)
} else {
use serde::ser::SerializeStruct;
let fields = 2 + usize::from(
self.separator
.is_some(),
);
let mut s = serializer.serialize_struct("Variables", fields)?;
s.serialize_field("scope", &self.vars_type)?;
s.serialize_field("vars", &self.inner)?;
if let Some(sep) = self.separator {
s.serialize_field("separator", &sep)?;
}
s.end()
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Variables {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum VariablesRepr {
Scoped {
scope: VariablesType,
vars: IndexMap<String, String>,
#[serde(default)]
separator: Option<char>,
},
Flat(IndexMap<String, String>),
}
let (vars_type, inner, separator) = match VariablesRepr::deserialize(deserializer)? {
VariablesRepr::Scoped {
scope,
vars,
separator,
} => (scope, vars, separator),
VariablesRepr::Flat(map) => (VariablesType::Default, map, None),
};
for (key, value) in &inner {
check_representable(key, value, vars_type).map_err(serde::de::Error::custom)?;
}
let vars = Self {
vars_type,
inner,
separator: None,
};
match separator {
Some(sep) => vars
.with_separator(sep)
.map_err(serde::de::Error::custom),
None => Ok(vars),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct VariablesDisplay<'a> {
vars: &'a Variables,
target: DialStringTarget,
}
impl fmt::Display for VariablesDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.vars
.write_for(f, self.target)
}
}
impl Variables {
pub fn display_for(&self, target: impl Into<DialStringTarget>) -> VariablesDisplay<'_> {
VariablesDisplay {
vars: self,
target: target.into(),
}
}
pub(super) fn write_for(
&self,
f: &mut fmt::Formatter<'_>,
target: DialStringTarget,
) -> fmt::Result {
let (open, close) = self
.vars_type
.delimiters();
f.write_fmt(format_args!("{}", open))?;
if let Some(sep) = self.separator {
write!(f, "^^{sep}")?;
}
let commas_separate = self
.separator
.is_none();
let sep = self
.separator
.unwrap_or(',');
for (i, (key, value)) in self
.inner
.iter()
.enumerate()
{
if i > 0 {
write!(f, "{sep}")?;
}
let value = escape_value(value, target, commas_separate, self.vars_type);
write!(f, "{}={}", key, value)?;
}
f.write_fmt(format_args!("{}", close))
}
}
impl fmt::Display for Variables {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_for(f, DialStringCarrier::EslApi.into())
}
}
impl FromStr for Variables {
type Err = OriginateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_for(s, DialStringCarrier::EslApi)
}
}
impl Variables {
pub fn parse_for(s: &str, target: impl Into<DialStringTarget>) -> Result<Self, OriginateError> {
let target = target.into();
let s = s.trim();
if s.len() < 2 {
return Err(OriginateError::ParseError(
"variable block too short".into(),
));
}
let (vars_type, inner_str) = match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
(b'{', b'}') => (VariablesType::Default, &s[1..s.len() - 1]),
(b'<', b'>') => (VariablesType::Enterprise, &s[1..s.len() - 1]),
(b'[', b']') => (VariablesType::Channel, &s[1..s.len() - 1]),
(open, close) => {
return Err(OriginateError::ParseError(format!(
"unknown variable delimiters: {:?}..{:?}",
open as char, close as char
)));
}
};
let (sep, var_str) = match inner_str.strip_prefix("^^") {
Some(rest) => {
let sep = rest
.chars()
.next()
.ok_or_else(|| {
OriginateError::ParseError("^^ without separator character".into())
})?;
check_separator(sep, vars_type)?;
(sep, &rest[sep.len_utf8()..])
}
None => (',', inner_str),
};
let commas_separate = sep == ',';
let mut inner = IndexMap::new();
if !var_str.is_empty() {
for (i, part) in split_unescaped(var_str, sep)
.into_iter()
.enumerate()
{
let (key, value) = part
.split_once('=')
.ok_or_else(|| {
OriginateError::ParseError(format!("missing = in variable {i}"))
})?;
let value = unescape_value(value, target, commas_separate, vars_type);
check_representable(key, &value, vars_type)?;
inner.insert(key.to_string(), value);
}
}
Ok(Self {
vars_type,
inner,
separator: None,
})
}
}
fn split_unescaped(s: &str, sep: char) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let bytes = s.as_bytes();
for (i, ch) in s.char_indices() {
if ch == sep {
let mut backslashes = 0;
let mut j = i;
while j > 0 && bytes[j - 1] == b'\\' {
backslashes += 1;
j -= 1;
}
if backslashes % 2 == 0 {
parts.push(&s[start..i]);
start = i + ch.len_utf8();
}
}
}
parts.push(&s[start..]);
parts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_value_is_refused_at_the_boundary() {
let err = "{k=,after=sentinel}"
.parse::<Variables>()
.unwrap_err()
.to_string();
assert!(err.contains('k'), "error does not name the variable: {err}");
assert!(
err.contains("remove it"),
"error does not name a remedy: {err}"
);
assert!(
err.contains("presence"),
"error prescribes a fix without allowing that presence meant something: {err}"
);
}
#[test]
fn an_unbalanced_bracket_is_refused_while_a_balanced_one_is_not() {
assert!("{k=oops}extra}"
.parse::<Variables>()
.is_err());
let balanced: Variables = "{k=${some_var}}"
.parse()
.expect("a balanced ${...} is ordinary and must parse");
assert_eq!(balanced.get("k"), Some("${some_var}"));
}
#[test]
fn chosen_separator_leaves_commas_alone() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("codecs", "PCMA,PCMU,G729");
let vars = vars
.with_separator(':')
.unwrap();
assert_eq!(vars.to_string(), "{^^:codecs=PCMA,PCMU,G729}");
assert_eq!(vars.separator(), Some(':'));
}
#[test]
fn separator_that_cannot_delimit_the_block_is_refused() {
let vars = Variables::new(VariablesType::Channel);
for sep in ['[', ']', '=', '^'] {
assert!(
vars.clone()
.with_separator(sep)
.is_err(),
"accepted {sep:?}"
);
}
}
#[test]
fn the_parser_refuses_every_separator_the_builder_does() {
for sep in ['[', ']', '=', '^'] {
assert!(
format!("[^^{sep}a=1{sep}b=2]")
.parse::<Variables>()
.is_err(),
"parser accepted {sep:?}"
);
}
}
#[test]
fn separator_already_present_in_a_value_is_refused() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("uri", "sip:bob@example.com");
let err = vars
.with_separator(':')
.unwrap_err()
.to_string();
assert!(
err.contains("uri"),
"error does not name the variable: {err}"
);
}
#[test]
fn escaping_pins_the_measured_wire_forms() {
let cases = [
(DialStringCarrier::Dialplan, "it's", r"it\\\\\\'s"),
(DialStringCarrier::EslApi, "it's", r"it\\\\\\\'s"),
(DialStringCarrier::Dialplan, "l'a'b", r"l\\\\\\'a\\\\\\'b"),
(DialStringCarrier::EslApi, "l'a'b", r"l\\\\\\\'a\\\\\\\'b"),
(DialStringCarrier::Dialplan, r"a\nb", r"a\\\\\\\\nb"),
(DialStringCarrier::EslApi, r"a\nb", r"a\\\\\\\\nb"),
(DialStringCarrier::Dialplan, "a,b", r"a\,b"),
(DialStringCarrier::EslApi, "a,b", r"a\,b"),
(DialStringCarrier::Dialplan, "a|b", "a|b"),
(DialStringCarrier::EslApi, "a|b", "a|b"),
];
for (carrier, value, want) in cases {
assert_eq!(
escape_value(value, carrier, true, VariablesType::Default),
want,
"{value:?} for {carrier:?}"
);
}
}
#[test]
fn channel_scope_escapes_for_the_leg_splits() {
let cases = [
(
DialStringCarrier::Dialplan,
r"a\nb",
r"a\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nb",
),
(
DialStringCarrier::EslApi,
r"a\nb",
r"a\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\nb",
),
(DialStringCarrier::Dialplan, "a|b", r"a\|b"),
(DialStringCarrier::EslApi, "a|b", r"a\|b"),
(DialStringCarrier::EslApi, "a,b", r"a\,b"),
];
for (carrier, value, want) in cases {
assert_eq!(
escape_value(value, carrier, true, VariablesType::Channel),
want,
"{value:?} for {carrier:?}"
);
}
}
#[test]
fn a_chosen_separator_changes_only_the_comma() {
let cases = [
(DialStringCarrier::Dialplan, "it's", r"it\\\\\\'s"),
(DialStringCarrier::EslApi, "it's", r"it\\\\\\\'s"),
(DialStringCarrier::Dialplan, "l'a'b", r"l\\\\\\'a\\\\\\'b"),
(DialStringCarrier::EslApi, "l'a'b", r"l\\\\\\\'a\\\\\\\'b"),
(DialStringCarrier::Dialplan, r"a\nb", r"a\\\\\\\\nb"),
(DialStringCarrier::EslApi, r"a\nb", r"a\\\\\\\\nb"),
(DialStringCarrier::Dialplan, "a,b", "a,b"),
(DialStringCarrier::EslApi, "a,b", "a,b"),
];
for (carrier, value, want) in cases {
assert_eq!(
escape_value(value, carrier, false, VariablesType::Default),
want,
"{value:?} for {carrier:?}"
);
}
}
#[test]
fn display_defaults_to_the_api_carrier() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("k", "it's");
assert_eq!(vars.to_string(), r"{k=it\\\\\\\'s}");
assert_eq!(
vars.display_for(DialStringCarrier::Dialplan)
.to_string(),
r"{k=it\\\\\\'s}"
);
}
#[test]
fn round_trips_at_either_carrier() {
for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
for value in ["it's", r"a\,b", r"C:\path", "a,b", "with space"] {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("k", value);
vars.insert("after", "sentinel");
let rendered = vars
.display_for(carrier)
.to_string();
let back = Variables::parse_for(&rendered, carrier).unwrap_or_else(|e| {
panic!("{value:?} for {carrier:?} rendered {rendered}: {e}")
});
assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
assert_eq!(back.get("after"), Some("sentinel"), "rendered {rendered}");
}
}
}
#[test]
fn separated_block_round_trips_at_either_carrier() {
for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
for value in ["it's", "a,b", r"C:\path", r"a\nb", "with space"] {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("k", value);
vars.insert("after", "sentinel");
let vars = vars
.with_separator('~')
.expect("'~' appears in none of these values");
let rendered = vars
.display_for(carrier)
.to_string();
let back = Variables::parse_for(&rendered, carrier).unwrap_or_else(|e| {
panic!("{value:?} for {carrier:?} rendered {rendered}: {e}")
});
assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
assert_eq!(back.get("after"), Some("sentinel"), "rendered {rendered}");
}
}
}
#[test]
fn value_with_backslash_round_trips() {
for value in [r"a\,b", r"C:\path", r"trailing\", r"\\", r"a\nb"] {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("k", value);
vars.insert("after", "sentinel");
let rendered = vars.to_string();
let back: Variables = rendered
.parse()
.unwrap_or_else(|e| {
panic!("{value:?} rendered {rendered} and failed to parse: {e}")
});
assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
assert_eq!(
back.get("after"),
Some("sentinel"),
"value {value:?} ate the next variable: {rendered}"
);
}
}
#[test]
fn missing_equals_error_omits_the_fragment() {
let msg = "{origination_caller_id_number=15551234567,15550009999}"
.parse::<Variables>()
.unwrap_err()
.to_string();
assert!(
!msg.contains("15550009999"),
"error quoted its input: {msg}"
);
assert!(
msg.contains("variable 1"),
"error does not name the part: {msg}"
);
}
#[test]
fn unknown_delimiters_error_omits_the_block() {
let msg = "(origination_caller_id_number=15551234567)"
.parse::<Variables>()
.unwrap_err()
.to_string();
assert!(
!msg.contains("15551234567"),
"error quoted its input: {msg}"
);
}
#[test]
fn variables_standard_chars() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("test_key", "this_value");
vars.insert("second", "2");
assert_eq!(vars.to_string(), "{test_key=this_value,second=2}");
}
#[test]
fn variables_comma_escaped() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("test_key", "this,is,a,value");
let result = vars.to_string();
assert!(result.contains("\\,"));
}
#[test]
fn variables_spaces_quoted() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("test_key", "this is a value");
let result = vars.to_string();
assert_eq!(
result
.matches('\'')
.count(),
2
);
}
#[test]
fn variables_single_quote_escaped() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("test_key", "let's_this_be_a_value");
let result = vars.to_string();
assert!(result.contains("\\'"));
}
#[test]
fn variables_enterprise_delimiters() {
let mut vars = Variables::new(VariablesType::Enterprise);
vars.insert("k", "v");
let result = vars.to_string();
assert!(result.starts_with('<'));
assert!(result.ends_with('>'));
}
#[test]
fn variables_channel_delimiters() {
let mut vars = Variables::new(VariablesType::Channel);
vars.insert("k", "v");
let result = vars.to_string();
assert!(result.starts_with('['));
assert!(result.ends_with(']'));
}
#[test]
fn variables_default_delimiters() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("k", "v");
let result = vars.to_string();
assert!(result.starts_with('{'));
assert!(result.ends_with('}'));
}
#[test]
fn variables_parse_round_trip() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("origination_caller_id_number", "9005551212");
vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
let s = vars.to_string();
let parsed: Variables = s
.parse()
.unwrap();
assert_eq!(
parsed.get("origination_caller_id_number"),
Some("9005551212")
);
assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
}
#[test]
fn split_unescaped_basic() {
assert_eq!(split_unescaped("a,b,c", ','), vec!["a", "b", "c"]);
assert_eq!(split_unescaped("a~b~c", '~'), vec!["a", "b", "c"]);
}
#[test]
fn split_unescaped_escaped() {
assert_eq!(split_unescaped(r"a\,b,c", ','), vec![r"a\,b", "c"]);
}
#[test]
fn split_unescaped_double_backslash() {
assert_eq!(split_unescaped(r"a\\,b", ','), vec![r"a\\", "b"]);
}
#[test]
fn split_unescaped_triple_backslash() {
assert_eq!(split_unescaped(r"a\\\,b", ','), vec![r"a\\\,b"]);
}
#[test]
fn variables_caret_caret_separator() {
let vars: Variables =
"[^^:sip_invite_domain=pbx.example.com:presence_id=1211@pbx.example.com]"
.parse()
.unwrap();
assert_eq!(vars.scope(), VariablesType::Channel);
assert_eq!(vars.get("sip_invite_domain"), Some("pbx.example.com"));
assert_eq!(vars.get("presence_id"), Some("1211@pbx.example.com"));
}
#[test]
fn variables_caret_caret_display_uses_canonical_comma() {
let vars: Variables = "[^^:a=1:b=2]"
.parse()
.unwrap();
assert_eq!(vars.to_string(), "[a=1,b=2]");
}
#[test]
fn variables_caret_caret_default_scope() {
let vars: Variables = "{^^|x=1|y=2}"
.parse()
.unwrap();
assert_eq!(vars.scope(), VariablesType::Default);
assert_eq!(vars.get("x"), Some("1"));
assert_eq!(vars.get("y"), Some("2"));
}
#[test]
fn variables_caret_caret_enterprise_scope() {
let vars: Variables = "<^^;a=1;b=2>"
.parse()
.unwrap();
assert_eq!(vars.scope(), VariablesType::Enterprise);
assert_eq!(vars.get("a"), Some("1"));
}
#[test]
fn variables_caret_caret_keeps_an_escaped_comma_literal() {
let vars: Variables = r"[^^:key=val\,ue:other=x]"
.parse()
.unwrap();
assert_eq!(vars.get("key"), Some(r"val\,ue"));
}
#[test]
fn variables_caret_caret_values_with_commas() {
let vars: Variables = "{^^|sip_h_X-Call-Info=<urn:foo>;purpose=bar,<urn:baz>|other=val}"
.parse()
.unwrap();
assert_eq!(
vars.get("sip_h_X-Call-Info"),
Some("<urn:foo>;purpose=bar,<urn:baz>")
);
assert_eq!(vars.get("other"), Some("val"));
}
#[test]
fn variables_caret_caret_empty_vars() {
let vars: Variables = "[^^:]"
.parse()
.unwrap();
assert!(vars.is_empty());
assert_eq!(vars.scope(), VariablesType::Channel);
}
#[test]
fn variables_caret_caret_missing_separator() {
assert!("[^^]"
.parse::<Variables>()
.is_err());
}
#[test]
fn variables_caret_caret_closing_bracket_as_sep() {
assert!("[^^]]"
.parse::<Variables>()
.is_err());
}
#[test]
fn variables_caret_caret_equals_as_sep() {
assert!("[^^=a=1]"
.parse::<Variables>()
.is_err());
}
#[test]
fn variables_from_str_empty_block() {
let result = "{}".parse::<Variables>();
assert!(
result.is_ok(),
"empty variable block should parse successfully"
);
let vars = result.unwrap();
assert!(
vars.is_empty(),
"parsed empty block should have no variables"
);
}
#[test]
fn variables_from_str_empty_channel_block() {
let result = "[]".parse::<Variables>();
assert!(result.is_ok());
let vars = result.unwrap();
assert!(vars.is_empty());
assert_eq!(vars.scope(), VariablesType::Channel);
}
#[test]
fn variables_from_str_empty_enterprise_block() {
let result = "<>".parse::<Variables>();
assert!(result.is_ok());
let vars = result.unwrap();
assert!(vars.is_empty());
assert_eq!(vars.scope(), VariablesType::Enterprise);
}
#[test]
fn serde_variables_type() {
let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
assert_eq!(json, "\"enterprise\"");
let parsed: VariablesType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, VariablesType::Enterprise);
}
#[test]
fn serde_variables_flat_default() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("key1", "val1");
vars.insert("key2", "val2");
let json = serde_json::to_string(&vars).unwrap();
let parsed: Variables = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.scope(), VariablesType::Default);
assert_eq!(parsed.get("key1"), Some("val1"));
assert_eq!(parsed.get("key2"), Some("val2"));
}
#[test]
fn serde_variables_scoped_enterprise() {
let mut vars = Variables::new(VariablesType::Enterprise);
vars.insert("key1", "val1");
let json = serde_json::to_string(&vars).unwrap();
assert!(json.contains("\"enterprise\""));
let parsed: Variables = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.scope(), VariablesType::Enterprise);
assert_eq!(parsed.get("key1"), Some("val1"));
}
#[test]
fn serde_variables_flat_map_deserializes_as_default() {
let json = r#"{"key1":"val1","key2":"val2"}"#;
let vars: Variables = serde_json::from_str(json).unwrap();
assert_eq!(vars.scope(), VariablesType::Default);
assert_eq!(vars.get("key1"), Some("val1"));
assert_eq!(vars.get("key2"), Some("val2"));
}
#[test]
fn serde_variables_scoped_deserializes() {
let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
let vars: Variables = serde_json::from_str(json).unwrap();
assert_eq!(vars.scope(), VariablesType::Channel);
assert_eq!(vars.get("k"), Some("v"));
}
#[test]
fn serde_variables_separator_round_trips() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("codecs", "PCMA,PCMU");
let vars = vars
.with_separator('|')
.unwrap();
let json = serde_json::to_string(&vars).unwrap();
let parsed: Variables = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.separator(), Some('|'));
assert_eq!(parsed, vars);
assert_eq!(parsed.to_string(), "{^^|codecs=PCMA,PCMU}");
}
#[test]
fn serde_variables_default_separator_is_absent() {
let mut flat = Variables::new(VariablesType::Default);
flat.insert("k", "v");
assert_eq!(serde_json::to_string(&flat).unwrap(), r#"{"k":"v"}"#);
let mut scoped = Variables::new(VariablesType::Channel);
scoped.insert("k", "v");
assert_eq!(
serde_json::to_string(&scoped).unwrap(),
r#"{"scope":"channel","vars":{"k":"v"}}"#
);
}
#[test]
fn serde_variables_separator_deserializes_from_config() {
let json = r#"{"scope":"channel","vars":{"codecs":"PCMA,PCMU"},"separator":":"}"#;
let vars: Variables = serde_json::from_str(json).unwrap();
assert_eq!(vars.scope(), VariablesType::Channel);
assert_eq!(vars.separator(), Some(':'));
assert_eq!(vars.to_string(), "[^^:codecs=PCMA,PCMU]");
}
#[test]
fn pipe_separator_is_refused_in_channel_scope_only() {
let mut vars = Variables::new(VariablesType::Channel);
vars.insert("k", "v");
assert!(vars
.clone()
.with_separator('|')
.is_err());
let mut vars = Variables::new(VariablesType::Default);
vars.insert("k", "v");
assert!(vars
.with_separator('|')
.is_ok());
}
#[test]
fn serde_variables_unusable_separator_is_refused() {
let undelimitable = r#"{"scope":"default","vars":{"k":"v"},"separator":"="}"#;
assert!(serde_json::from_str::<Variables>(undelimitable).is_err());
let in_a_value =
r#"{"scope":"default","vars":{"uri":"sip:bob@example.com"},"separator":":"}"#;
let err = serde_json::from_str::<Variables>(in_a_value)
.unwrap_err()
.to_string();
assert!(
err.contains("uri"),
"error does not name the variable: {err}"
);
}
#[test]
fn a_quote_in_channel_scope_is_refused_at_every_boundary() {
let err = r"[cid=it\\\\\\\'s]"
.parse::<Variables>()
.unwrap_err()
.to_string();
assert!(
err.contains("cid") && err.contains("channel scope"),
"error does not name the variable and scope: {err}"
);
let err = serde_json::from_str::<Variables>(r#"{"scope":"channel","vars":{"cid":"it's"}}"#)
.unwrap_err()
.to_string();
assert!(
err.contains("cid") && err.contains("channel scope"),
"error does not name the variable and scope: {err}"
);
assert!(r"{cid=it\\\\\\\'s}"
.parse::<Variables>()
.is_ok());
assert!(
serde_json::from_str::<Variables>(r#"{"scope":"default","vars":{"cid":"it's"}}"#)
.is_ok()
);
}
const REVISIONS: &[BlockParse] = &[BlockParse::PairSplitCleans];
#[test]
fn a_bare_carrier_is_a_target_at_the_default_revision() {
let target = DialStringTarget::from(DialStringCarrier::Dialplan);
assert_eq!(target.carrier(), DialStringCarrier::Dialplan);
assert_eq!(target.block_parse(), BlockParse::default());
assert_eq!(BlockParse::default(), BlockParse::PairSplitCleans);
assert_eq!(
DialStringTarget::new(DialStringCarrier::EslApi)
.with_block_parse(BlockParse::PairSplitCleans)
.block_parse(),
BlockParse::PairSplitCleans
);
}
#[test]
fn enterprise_scope_escapes_like_default_scope() {
let cases = [
(DialStringCarrier::Dialplan, "it's", r"it\\\\\\'s"),
(DialStringCarrier::EslApi, "it's", r"it\\\\\\\'s"),
(DialStringCarrier::EslApi, r"a\nb", r"a\\\\\\\\nb"),
(DialStringCarrier::EslApi, "a,b", r"a\,b"),
];
for (carrier, value, want) in cases {
let target =
DialStringTarget::new(carrier).with_block_parse(BlockParse::PairSplitCleans);
assert_eq!(
escape_value(value, target, true, VariablesType::Enterprise),
want,
"{value:?} for {carrier:?}"
);
}
}
#[test]
fn round_trips_at_every_target() {
let cases = [
(
VariablesType::Default,
&["it's", r"C:\path", "a,b", r"a\nb", "with space"][..],
),
(
VariablesType::Enterprise,
&["it's", r"C:\path", "a,b", "with space"][..],
),
(
VariablesType::Channel,
&[r"C:\path", "a,b", "a|b", "with space"][..],
),
];
for &block_parse in REVISIONS {
for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
let target = DialStringTarget::new(carrier).with_block_parse(block_parse);
for (scope, values) in cases {
for &value in values {
let mut vars = Variables::new(scope);
vars.insert("k", value);
vars.insert("after", "sentinel");
let rendered = vars
.display_for(target)
.to_string();
let back = Variables::parse_for(&rendered, target).unwrap_or_else(|e| {
panic!("{value:?} for {target:?} rendered {rendered}: {e}")
});
assert_eq!(back.get("k"), Some(value), "rendered {rendered}");
assert_eq!(back.get("after"), Some("sentinel"), "rendered {rendered}");
}
}
}
}
}
#[test]
fn refusals_do_not_depend_on_the_revision() {
for &block_parse in REVISIONS {
for carrier in [DialStringCarrier::EslApi, DialStringCarrier::Dialplan] {
let target = DialStringTarget::new(carrier).with_block_parse(block_parse);
for block in [
r"[cid=it\\\\\\\'s]",
"{k=,after=sentinel}",
"{k=oops}extra}",
"{^^=a=1=b=2}",
"[^^|a=1|b=2]",
] {
assert!(
Variables::parse_for(block, target).is_err(),
"{block} accepted at {target:?}"
);
}
}
}
}
#[test]
fn block_parse_reads_config_spellings_and_writes_the_canonical_one() {
for spelling in [
"pair_split_cleans",
"PAIR_SPLIT_CLEANS",
"Pair_Split_Cleans",
] {
assert_eq!(
spelling
.parse::<BlockParse>()
.ok(),
Some(BlockParse::PairSplitCleans),
"{spelling}"
);
}
assert_eq!(BlockParse::PairSplitCleans.to_string(), "pair_split_cleans");
let err = "pair_split_whatever"
.parse::<BlockParse>()
.unwrap_err()
.to_string();
assert!(!err.contains("whatever"), "error quoted its input: {err}");
}
#[test]
fn serde_block_parse_uses_the_config_spelling() {
assert_eq!(
serde_json::to_string(&BlockParse::PairSplitCleans).unwrap(),
r#""pair_split_cleans""#
);
let parsed: BlockParse = serde_json::from_str(r#""pair_split_cleans""#).unwrap();
assert_eq!(parsed, BlockParse::PairSplitCleans);
}
#[test]
fn for_version_maps_a_vouched_release() {
for version in [
FreeswitchVersion::new(1, 10, 0),
FreeswitchVersion::new(1, 10, 7),
FreeswitchVersion::new(1, 10, 12),
] {
assert_eq!(
BlockParse::for_version(&version).ok(),
Some(BlockParse::PairSplitCleans),
"{version}"
);
}
}
#[test]
fn for_version_refuses_what_it_cannot_vouch_for() {
for version in [
FreeswitchVersion::new(1, 10, 12).dev(),
FreeswitchVersion::new(1, 10, 5).dev(),
] {
let err = BlockParse::for_version(&version).expect_err(&version.to_string());
assert!(
matches!(err, UnvouchedVersion::Dev { .. }),
"{version}: {err:?}"
);
assert_eq!(err.version(), version);
}
for version in [
FreeswitchVersion::new(1, 10, 13),
FreeswitchVersion::new(1, 11, 1),
] {
let err = BlockParse::for_version(&version).expect_err(&version.to_string());
assert!(
matches!(err, UnvouchedVersion::NewerThanVouched { .. }),
"{version}: {err:?}"
);
}
let version = FreeswitchVersion::new(1, 8, 7);
let err = BlockParse::for_version(&version).expect_err(&version.to_string());
assert!(
matches!(err, UnvouchedVersion::OlderThanVouched { .. }),
"{version}: {err:?}"
);
}
#[test]
fn an_unvouched_version_names_the_vouched_range() {
let msg = BlockParse::for_version(&FreeswitchVersion::new(1, 11, 1))
.unwrap_err()
.to_string();
assert!(
msg.contains("1.10.0") && msg.contains("1.10.12"),
"does not name the vouched range: {msg}"
);
}
}