1use indexmap::IndexMap;
7use std::fmt;
8use std::str::FromStr;
9
10use super::originate::OriginateError;
11use crate::version::FreeswitchVersion;
12
13#[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 Enterprise,
25 Default,
27 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 fn leg_split_passes(self) -> u32 {
43 match self {
44 Self::Enterprise | Self::Default => 0,
45 Self::Channel => 2,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Variables {
70 vars_type: VariablesType,
71 inner: IndexMap<String, String>,
72 separator: Option<char>,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[non_exhaustive]
88pub enum DialStringCarrier {
89 EslApi,
92 Dialplan,
95}
96
97impl DialStringCarrier {
98 fn deletes_escaped_quote(self) -> bool {
101 matches!(self, Self::Dialplan)
102 }
103}
104
105#[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 #[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum UnvouchedVersion {
169 Dev {
171 version: FreeswitchVersion,
173 },
174 NewerThanVouched {
176 version: FreeswitchVersion,
178 },
179 OlderThanVouched {
181 version: FreeswitchVersion,
183 },
184}
185
186impl UnvouchedVersion {
187 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub struct DialStringTarget {
257 carrier: DialStringCarrier,
258 block_parse: BlockParse,
259}
260
261impl DialStringTarget {
262 pub fn new(carrier: DialStringCarrier) -> Self {
264 Self {
265 carrier,
266 block_parse: BlockParse::default(),
267 }
268 }
269
270 pub fn with_block_parse(mut self, block_parse: BlockParse) -> Self {
272 self.block_parse = block_parse;
273 self
274 }
275
276 pub fn carrier(&self) -> DialStringCarrier {
278 self.carrier
279 }
280
281 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 fn backslash_escape(self, scope: VariablesType) -> String {
295 "\\".repeat(1 << self.passes(scope))
296 }
297
298 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
320fn 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
373fn 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
393fn 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 let escaped = value
407 .replace('\\', &target.backslash_escape(vars_type))
408 .replace('\'', &target.quote_escape(vars_type));
409 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
428fn 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 pub fn new(vars_type: VariablesType) -> Self {
459 Self {
460 vars_type,
461 inner: IndexMap::new(),
462 separator: None,
463 }
464 }
465
466 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 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 pub fn separator(&self) -> Option<char> {
511 self.separator
512 }
513
514 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 pub fn remove(&mut self, key: &str) -> Option<String> {
522 self.inner
523 .shift_remove(key)
524 }
525
526 pub fn get(&self, key: &str) -> Option<&str> {
528 self.inner
529 .get(key)
530 .map(|s| s.as_str())
531 }
532
533 pub fn is_empty(&self) -> bool {
535 self.inner
536 .is_empty()
537 }
538
539 pub fn len(&self) -> usize {
541 self.inner
542 .len()
543 }
544
545 pub fn scope(&self) -> VariablesType {
547 self.vars_type
548 }
549
550 pub fn set_scope(&mut self, scope: VariablesType) {
552 self.vars_type = scope;
553 }
554
555 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 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 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 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#[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 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 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 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
785fn 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 #[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 assert!(
834 err.contains("presence"),
835 "error prescribes a fix without allowing that presence meant something: {err}"
836 );
837 }
838
839 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 assert_eq!(split_unescaped(r"a\\,b", ','), vec![r"a\\", "b"]);
1203 }
1204
1205 #[test]
1206 fn split_unescaped_triple_backslash() {
1207 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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}