Skip to main content

freeswitch_types/commands/flattened/
mod.rs

1//! Dial strings the switch produced, read the way the switch reads them.
2//!
3//! A directory group's expansion or any other list the switch wrote is read by
4//! the switch's own passes for a named [`DialStringTarget`], so each leg's
5//! variables are the ones its channel receives. Every leg keeps its input text,
6//! and [`FlattenedDialString::display_raw`] forwards the kept legs unchanged.
7
8use std::fmt;
9use std::ops::Range;
10use std::sync::Arc;
11
12use crate::channel::HangupCause;
13use crate::commands::endpoint::Endpoint;
14use crate::commands::originate::OriginateError;
15use crate::commands::variables::{DialStringTarget, Variables, VariablesType};
16use crate::switch_passes::brackets::{install, same_header, Block, Pair, PairEffect};
17use crate::switch_passes::escape::{escape_text, EscapedField};
18use crate::switch_passes::expansion::names_a_variable;
19use crate::switch_passes::originate_legs::{
20    channel_variables, DialList, Leg, PastLimit, Thread, ENTERPRISE_DELIM, MAX_PEERS,
21};
22use crate::switch_passes::{pipeline, PipelineError};
23use crate::variables::VariableName;
24
25#[cfg(test)]
26mod tests;
27
28#[cfg(all(test, feature = "serde"))]
29mod c_oracle;
30
31/// A dial string read by the passes the switch applies for one target.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct FlattenedDialString {
34    head: String,
35    blocks: Vec<Block>,
36    threads: Vec<FlattenedThread>,
37    tail: String,
38    warnings: Vec<ListWarning>,
39}
40
41/// One `:_:` thread, or the whole list when there is none.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct FlattenedThread {
44    separator: String,
45    head: String,
46    blocks: Vec<Block>,
47    groups: Vec<FlattenedGroup>,
48    trailer: String,
49}
50
51/// Legs rung together; groups of a thread are tried in turn.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct FlattenedGroup {
54    separator: String,
55    legs: Vec<FlattenedLeg>,
56}
57
58/// One leg as its channel receives it.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct FlattenedLeg {
61    separator: String,
62    raw: String,
63    leg: Leg,
64    inherited: Arc<[Block]>,
65    nested_vars: bool,
66    target: LegTarget,
67    warnings: Vec<LegWarning>,
68}
69
70/// What a leg dials.
71#[derive(Debug, Clone, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum LegTarget {
74    /// `error/`, which ends the leg with a cause and places no call.
75    Error(ErrorLeg),
76    /// An endpoint this crate models; it carries no variable block.
77    Endpoint(Endpoint),
78    /// An endpoint this crate does not model, or an empty leg.
79    Unparsed(UnparsedLeg),
80}
81
82/// An `error/` leg.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ErrorLeg {
85    as_written: String,
86    reading: CauseReading,
87}
88
89/// Endpoint text no typed endpoint accepts.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct UnparsedLeg {
92    endpoint: String,
93    error: OriginateError,
94}
95
96/// How `switch_channel_str2cause` reads the text after `error/`.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum CauseReading {
100    /// A cause name, in any case.
101    Name(HangupCause),
102    /// A leading digit run, read with `atoi`.
103    Number(u32),
104    /// Neither; the switch ends the leg with its default cause.
105    Unrecognized,
106}
107
108/// A pair or block of the leg's own that does not reach the channel as written.
109#[derive(Debug, Clone, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum LegWarning {
112    /// The pair has no `=`, so nothing is installed.
113    PairIgnored {
114        /// Index among the leg's blocks.
115        block: usize,
116        /// Variable name.
117        key: String,
118    },
119    /// The value is empty, which deletes every earlier value of the key.
120    PairCleared {
121        /// Index among the leg's blocks.
122        block: usize,
123        /// Variable name.
124        key: String,
125    },
126    /// The value holds `${`, which the switch refuses unless `origination_nested_vars=true`
127    /// appears in the leg's thread or a `<>` block ahead of an enterprise split sets it true.
128    NestedVarsRefused {
129        /// Index among the leg's blocks.
130        block: usize,
131        /// Variable name.
132        key: String,
133    },
134    /// The block's `^^` separator is not ASCII. The switch splits on its first
135    /// byte, which no typed name can carry, so the block contributes no pairs.
136    BlockSeparatorUnreadable {
137        /// Index among the leg's blocks.
138        block: usize,
139    },
140    /// Parsing the block writes into the leg's text after it, as a block of `^^` alone does. The
141    /// endpoint is read from the rewritten text, so a render of the leg reads differently.
142    BlockRewritesFollowingText {
143        /// Index among the leg's blocks.
144        block: usize,
145    },
146    /// A pair opens a `^^` head naming a non-ASCII separator, which the switch splits on its
147    /// first byte; what it installs, if anything, no string carries.
148    PairUnreadable {
149        /// Index among the leg's blocks.
150        block: usize,
151    },
152}
153
154/// Something about the whole list the typed view cannot show.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156#[non_exhaustive]
157pub enum ListWarning {
158    /// A quote kept a leg or group separator from splitting.
159    QuoteSpansLegs,
160    /// The dialplan carrier substitutes a `${}` reference with a value only the
161    /// switch knows; the reference is kept as written.
162    CarrierExpands,
163    /// A list or thread block's `^^` separator is not ASCII. The switch splits on
164    /// its first byte, which no typed name can carry, so the block contributes no pairs.
165    BlockSeparatorUnreadable {
166        /// Index among the list's blocks and then each thread's, in reading order.
167        block: usize,
168    },
169    /// Parsing a list or thread block writes into the text after it, as a block of `^^` alone
170    /// does. What follows is read from the rewritten text, so a render of the list reads differently.
171    BlockRewritesFollowingText {
172        /// Index among the list's blocks and then each thread's, in reading order.
173        block: usize,
174    },
175    /// A split stopped at the switch's `MAX_PEERS` with text left: its last thread, group or leg
176    /// keeps the rest, separators included, and nothing past it is dialled.
177    LegsPastLimit {
178        /// Index among [`FlattenedDialString::threads`] of the thread whose groups or legs were
179        /// split, `None` for the `:_:` split.
180        thread: Option<usize>,
181        /// Index among [`FlattenedThread::groups`] of the group whose legs were split, `None`
182        /// for a `:_:` or `|` split.
183        group: Option<usize>,
184        /// Threads, groups or legs past the limit the switch never splits off. Empty tokens do
185        /// not count, so this is zero when only empty ones or a trailing separator follow.
186        excess: usize,
187    },
188}
189
190/// What stops the switch from reading the dial string at all.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192#[non_exhaustive]
193pub enum FlattenedDialStringError {
194    /// Nothing to dial.
195    Empty,
196    /// The API carrier's argument split cuts the dial string into more than one
197    /// argument, leaves a quote open, or finds a quote holding a
198    /// [`DialStringTarget::argv_separator`].
199    ArgvSplit,
200    /// A block never closes, which aborts the whole originate.
201    UnclosedBlock {
202        /// Index of the leg in reading order.
203        leg: usize,
204    },
205    /// The text ends in `\n`, as a reply body does before its suffix is stripped.
206    TrailingNewline,
207    /// A split the switch runs on a non-ASCII `^^` separator's first byte reaches text no string
208    /// carries: a group or leg opening such a head, or a block with one ending in a backslash.
209    SplitSeparatorUnreadable,
210}
211
212/// Renders a [`FlattenedDialString`]. Returned by
213/// [`FlattenedDialString::display_raw`] and [`FlattenedDialString::display_for`].
214#[derive(Debug, Clone, Copy)]
215pub struct FlattenedDialStringDisplay<'a> {
216    list: &'a FlattenedDialString,
217    render: Render,
218}
219
220#[derive(Debug, Clone, Copy)]
221enum Render {
222    Raw,
223    For(DialStringTarget),
224}
225
226impl FlattenedDialString {
227    /// Read `input` through every pass `target` applies.
228    pub fn parse_for(
229        input: &str,
230        target: impl Into<DialStringTarget>,
231    ) -> Result<Self, FlattenedDialStringError> {
232        if input.ends_with('\n') {
233            return Err(FlattenedDialStringError::TrailingNewline);
234        }
235        let list = pipeline::read(input, target.into())
236            .map_err(FlattenedDialStringError::from_pipeline)?;
237        let DialList {
238            blocks,
239            threads,
240            quote_spans_legs,
241            carrier_expands,
242            past_limit,
243        } = list;
244        let past_limit: Vec<ListWarning> = past_limit
245            .into_iter()
246            .map(|cut| ListWarning::past_limit(&threads, cut))
247            .collect();
248        let unreadable = blocks
249            .iter()
250            .chain(
251                threads
252                    .iter()
253                    .flat_map(|thread| &thread.blocks),
254            )
255            .enumerate()
256            .filter_map(|(block, parsed)| {
257                if parsed.separator_unreadable() {
258                    Some(ListWarning::BlockSeparatorUnreadable { block })
259                } else {
260                    parsed
261                        .rewrites_following_text
262                        .then_some(ListWarning::BlockRewritesFollowingText { block })
263                }
264            });
265        let warnings = [
266            (quote_spans_legs, ListWarning::QuoteSpansLegs),
267            (carrier_expands, ListWarning::CarrierExpands),
268        ]
269        .into_iter()
270        .filter_map(|(raised, warning)| raised.then_some(warning))
271        .chain(unreadable)
272        .chain(past_limit)
273        .collect();
274        let mut kept: Vec<(Range<usize>, FlattenedThread)> = Vec::new();
275        for thread in threads {
276            let raw = thread
277                .raw
278                .clone();
279            let separator_start = kept
280                .last()
281                .map_or(raw.start, |(before, _)| before.end);
282            if let Some(read) = FlattenedThread::read(input, thread, &blocks, separator_start) {
283                kept.push((raw, read));
284            }
285        }
286        let (Some((first, _)), Some((last, _))) = (kept.first(), kept.last()) else {
287            return Err(FlattenedDialStringError::Empty);
288        };
289        Ok(Self {
290            head: slice(input, 0..first.start),
291            tail: slice(input, last.end..input.len()),
292            blocks,
293            threads: kept
294                .into_iter()
295                .map(|(_, thread)| thread)
296                .collect(),
297            warnings,
298        })
299    }
300
301    /// Threads in reading order.
302    pub fn threads(&self) -> impl Iterator<Item = &FlattenedThread> {
303        self.threads
304            .iter()
305    }
306
307    /// Every leg of every thread, in reading order.
308    pub fn legs(&self) -> impl Iterator<Item = &FlattenedLeg> {
309        self.threads
310            .iter()
311            .flat_map(FlattenedThread::legs)
312    }
313
314    /// The pairs of the `<>` blocks ahead of a `:_:` split, which every thread inherits, in the
315    /// order their event holds them: a name set again moves to the end, an empty value deletes it.
316    /// Empty without a split, where [`FlattenedThread::variables`] holds every list block.
317    pub fn variables(&self) -> impl Iterator<Item = (&str, &str)> {
318        install(&self.blocks).into_iter()
319    }
320
321    /// Warnings about the whole list.
322    pub fn warnings(&self) -> &[ListWarning] {
323        &self.warnings
324    }
325
326    /// Keep the legs `keep` accepts. A group or thread left with no leg goes too.
327    pub fn retain(&mut self, mut keep: impl FnMut(&FlattenedLeg) -> bool) {
328        for thread in &mut self.threads {
329            for group in &mut thread.groups {
330                group
331                    .legs
332                    .retain(&mut keep);
333            }
334            thread
335                .groups
336                .retain(|group| {
337                    !group
338                        .legs
339                        .is_empty()
340                });
341        }
342        self.threads
343            .retain(|thread| {
344                !thread
345                    .groups
346                    .is_empty()
347            });
348    }
349
350    /// Whether no leg is left.
351    pub fn is_empty(&self) -> bool {
352        self.threads
353            .is_empty()
354    }
355
356    fn write_raw(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        if self.is_empty() {
358            return Ok(());
359        }
360        f.write_str(&self.head)?;
361        for (t, thread) in self
362            .threads
363            .iter()
364            .enumerate()
365        {
366            if t > 0 {
367                f.write_str(&thread.separator)?;
368            }
369            f.write_str(&thread.head)?;
370            for (g, group) in thread
371                .groups
372                .iter()
373                .enumerate()
374            {
375                if g > 0 {
376                    f.write_str(&group.separator)?;
377                }
378                for (l, leg) in group
379                    .legs
380                    .iter()
381                    .enumerate()
382                {
383                    if l > 0 {
384                        f.write_str(&leg.separator)?;
385                    }
386                    f.write_str(&leg.raw)?;
387                }
388            }
389            f.write_str(&thread.trailer)?;
390        }
391        f.write_str(&self.tail)
392    }
393
394    fn write_for(&self, f: &mut fmt::Formatter<'_>, target: DialStringTarget) -> fmt::Result {
395        if self.is_empty() {
396            return Ok(());
397        }
398        write_blocks(f, &self.blocks, target)?;
399        for (t, thread) in self
400            .threads
401            .iter()
402            .enumerate()
403        {
404            if t > 0 {
405                f.write_str(ENTERPRISE_DELIM)?;
406            }
407            write_blocks(f, &thread.blocks, target)?;
408            for (g, group) in thread
409                .groups
410                .iter()
411                .enumerate()
412            {
413                if g > 0 {
414                    f.write_str("|")?;
415                }
416                for (l, leg) in group
417                    .legs
418                    .iter()
419                    .enumerate()
420                {
421                    if l > 0 {
422                        f.write_str(",")?;
423                    }
424                    write_blocks(
425                        f,
426                        &leg.leg
427                            .blocks,
428                        target,
429                    )?;
430                    f.write_str(&escape_text(
431                        &leg.leg
432                            .endpoint,
433                        target,
434                        EscapedField::Endpoint,
435                    ))?;
436                }
437            }
438        }
439        Ok(())
440    }
441
442    /// The input text of the kept legs, joined by the separators that stood
443    /// before each of them in the input; byte-identical when nothing was removed.
444    pub fn display_raw(&self) -> FlattenedDialStringDisplay<'_> {
445        FlattenedDialStringDisplay {
446            list: self,
447            render: Render::Raw,
448        }
449    }
450
451    /// The list rendered by this crate for `target`, carrying only the pairs
452    /// that set a value.
453    pub fn display_for(
454        &self,
455        target: impl Into<DialStringTarget>,
456    ) -> FlattenedDialStringDisplay<'_> {
457        FlattenedDialStringDisplay {
458            list: self,
459            render: Render::For(target.into()),
460        }
461    }
462}
463
464impl fmt::Display for FlattenedDialStringDisplay<'_> {
465    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466        match self.render {
467            Render::Raw => self
468                .list
469                .write_raw(f),
470            Render::For(target) => target.write_argument(f, |f, target| {
471                self.list
472                    .write_for(f, target)
473            }),
474        }
475    }
476}
477
478/// Every block through [`Variables`], with only the pairs that set a value.
479fn write_blocks(
480    f: &mut fmt::Formatter<'_>,
481    blocks: &[Block],
482    target: DialStringTarget,
483) -> fmt::Result {
484    for block in blocks {
485        let mut vars = Variables::new(VariablesType::of_block(block));
486        for pair in &block.pairs {
487            if let PairEffect::Set(value) = &pair.effect {
488                vars.insert(
489                    pair.key
490                        .as_str(),
491                    value.as_str(),
492                );
493            }
494        }
495        if vars.is_empty() {
496            continue;
497        }
498        // A `^^` separator the builder refuses renders comma-separated, which
499        // reads back to the same values.
500        let vars = match block.separator {
501            ',' => vars,
502            sep => vars
503                .clone()
504                .with_separator(sep)
505                .unwrap_or(vars),
506        };
507        write!(f, "{}", vars.display_for(target))?;
508    }
509    Ok(())
510}
511
512/// `range` is a pipeline trace: char-boundary offsets into `input`, in reading order.
513fn slice(input: &str, range: Range<usize>) -> String {
514    debug_assert!(input
515        .get(range.clone())
516        .is_some());
517    input
518        .get(range)
519        .unwrap_or_default()
520        .to_owned()
521}
522
523impl FlattenedThread {
524    fn read(
525        input: &str,
526        thread: Thread,
527        list_blocks: &[Block],
528        separator_start: usize,
529    ) -> Option<Self> {
530        let Thread {
531            raw,
532            blocks,
533            groups: pipeline_groups,
534            nested_vars,
535        } = thread;
536        let inherited: Arc<[Block]> = list_blocks
537            .iter()
538            .chain(&blocks)
539            .cloned()
540            .collect();
541        let mut first_start = None;
542        let mut last_end = None;
543        let mut groups = Vec::new();
544        for group in pipeline_groups {
545            let mut separator = None;
546            let mut legs = Vec::with_capacity(group.len());
547            let mut leg_end = None;
548            for leg in group {
549                let start = leg
550                    .raw
551                    .start;
552                separator.get_or_insert_with(|| slice(input, last_end.unwrap_or(start)..start));
553                first_start.get_or_insert(start);
554                let leg_separator = slice(input, leg_end.unwrap_or(start)..start);
555                leg_end = Some(
556                    leg.raw
557                        .end,
558                );
559                legs.push(FlattenedLeg::read(
560                    input,
561                    leg,
562                    leg_separator,
563                    &inherited,
564                    nested_vars,
565                ));
566            }
567            let Some(separator) = separator else {
568                continue;
569            };
570            last_end = leg_end;
571            groups.push(FlattenedGroup { separator, legs });
572        }
573        let (start, end) = first_start.zip(last_end)?;
574        Some(Self {
575            separator: slice(input, separator_start..raw.start),
576            head: slice(input, raw.start..start),
577            blocks,
578            groups,
579            trailer: slice(input, end..raw.end),
580        })
581    }
582
583    /// The pairs of the thread's own `<>` and `{}` blocks, which every leg of the thread inherits,
584    /// in the order their event holds them: a name set again moves to the end, an empty value
585    /// deletes it. Without a `:_:` split the list's blocks are the one thread's.
586    pub fn variables(&self) -> impl Iterator<Item = (&str, &str)> {
587        install(&self.blocks).into_iter()
588    }
589
590    /// Groups in the order they are tried.
591    pub fn groups(&self) -> impl Iterator<Item = &FlattenedGroup> {
592        self.groups
593            .iter()
594    }
595
596    /// Every leg of every group.
597    pub fn legs(&self) -> impl Iterator<Item = &FlattenedLeg> {
598        self.groups
599            .iter()
600            .flat_map(FlattenedGroup::legs)
601    }
602}
603
604impl FlattenedGroup {
605    /// Legs rung together.
606    pub fn legs(&self) -> impl Iterator<Item = &FlattenedLeg> {
607        self.legs
608            .iter()
609    }
610}
611
612impl FlattenedLeg {
613    fn read(
614        input: &str,
615        leg: Leg,
616        separator: String,
617        inherited: &Arc<[Block]>,
618        nested_vars: bool,
619    ) -> Self {
620        let warnings = leg
621            .blocks
622            .iter()
623            .enumerate()
624            .flat_map(|(block, parsed)| {
625                parsed
626                    .separator_unreadable()
627                    .then_some(LegWarning::BlockSeparatorUnreadable { block })
628                    .into_iter()
629                    .chain(
630                        parsed
631                            .rewrites_following_text
632                            .then_some(LegWarning::BlockRewritesFollowingText { block }),
633                    )
634                    .chain(
635                        parsed
636                            .pairs
637                            .iter()
638                            .filter_map(move |pair| LegWarning::of(block, pair, nested_vars)),
639                    )
640            })
641            .collect();
642        Self {
643            separator,
644            raw: slice(
645                input,
646                leg.raw
647                    .clone(),
648            ),
649            target: LegTarget::read(&leg.endpoint),
650            leg,
651            inherited: Arc::clone(inherited),
652            nested_vars,
653            warnings,
654        }
655    }
656
657    /// The leg's own input text.
658    pub fn raw(&self) -> &str {
659        &self.raw
660    }
661
662    /// The value the leg's channel receives, after the list and thread blocks
663    /// are installed in the order `local_var_clobber` decides.
664    ///
665    /// A value naming a variable is refused at install, as the switch refuses it, unless
666    /// `origination_nested_vars=true` appears in the leg's thread or a `<>` block ahead of an
667    /// enterprise split sets it true. On a channel with `CF_NO_PRESENCE`, originate deletes
668    /// `presence_id`.
669    pub fn variable(&self, name: impl VariableName) -> Option<&str> {
670        self.variable_str(name.as_str())
671    }
672
673    /// Look up a leg variable by its bare wire name, for a key with no
674    /// [`VariableName`] enum of its own (e.g. a `sip_h_` passthrough).
675    ///
676    /// ```
677    /// use freeswitch_types::commands::{DialStringCarrier, FlattenedDialString};
678    ///
679    /// let list = FlattenedDialString::parse_for(
680    ///     "[sip_h_X-Seat=42]loopback/9199/test",
681    ///     DialStringCarrier::EslApi,
682    /// )
683    /// .unwrap();
684    /// let leg = list.legs().next().unwrap();
685    /// assert_eq!(leg.variable_str("sip_h_X-Seat"), Some("42"));
686    /// ```
687    pub fn variable_str(&self, name: &str) -> Option<&str> {
688        self.variables()
689            .find(|(held, _)| same_header(held, name))
690            .map(|(_, value)| value)
691    }
692
693    /// Every variable the leg's channel receives, in the order it holds them once the list, thread
694    /// and leg blocks are installed as `local_var_clobber` decides, each name once with the value
695    /// [`variable_str`](Self::variable_str) returns.
696    ///
697    /// A name installed again moves to the end, spelled as that install spells it. A value refused
698    /// at install, as [`variable`](Self::variable) describes, is absent.
699    pub fn variables(&self) -> impl Iterator<Item = (&str, &str)> {
700        channel_variables(
701            self.inherited
702                .iter(),
703            &self.leg,
704            self.nested_vars,
705        )
706        .into_iter()
707    }
708
709    /// What the leg dials.
710    pub fn target(&self) -> &LegTarget {
711        &self.target
712    }
713
714    /// Pairs and blocks of the leg's own that do not reach the channel as written.
715    pub fn warnings(&self) -> &[LegWarning] {
716        &self.warnings
717    }
718}
719
720impl ErrorLeg {
721    /// The text after `error/`.
722    pub fn as_written(&self) -> &str {
723        &self.as_written
724    }
725
726    /// How the switch reads that text.
727    pub fn reading(&self) -> CauseReading {
728        self.reading
729    }
730
731    /// The cause named, or the numbered cause when the number fits `u16` and
732    /// names one.
733    ///
734    /// The switch ends a zero with `DESTINATION_OUT_OF_ORDER` and an
735    /// unrecognized cause with `NORMAL_CLEARING`; this crate fabricates neither.
736    pub fn cause(&self) -> Option<HangupCause> {
737        match self.reading {
738            CauseReading::Name(cause) => Some(cause),
739            CauseReading::Number(number) => u16::try_from(number)
740                .ok()
741                .and_then(HangupCause::from_number),
742            CauseReading::Unrecognized => None,
743        }
744    }
745}
746
747impl LegTarget {
748    fn read(endpoint: &str) -> Self {
749        if let Some(cause) = endpoint.strip_prefix("error/") {
750            return Self::Error(ErrorLeg {
751                as_written: cause.to_owned(),
752                reading: str2cause(cause),
753            });
754        }
755        match Endpoint::parse_bare(endpoint) {
756            Ok(parsed) => Self::Endpoint(parsed),
757            Err(error) => Self::Unparsed(UnparsedLeg {
758                endpoint: endpoint.to_owned(),
759                error,
760            }),
761        }
762    }
763}
764
765/// `switch_channel_str2cause` on the text after `error/`.
766fn str2cause(text: &str) -> CauseReading {
767    if text.starts_with(|c: char| c.is_ascii_digit()) {
768        // glibc's atoi: strtol clamps to a 64-bit long, which the switch truncates to its cause.
769        let long = text
770            .bytes()
771            .take_while(u8::is_ascii_digit)
772            .fold(0i128, |n, digit| {
773                (n * 10 + i128::from(digit - b'0')).min(i128::from(i64::MAX))
774            });
775        return CauseReading::Number(long as i64 as u32);
776    }
777    text.to_ascii_uppercase()
778        .parse::<HangupCause>()
779        .map_or(CauseReading::Unrecognized, CauseReading::Name)
780}
781
782impl LegWarning {
783    fn of(block: usize, pair: &Pair, nested_vars: bool) -> Option<Self> {
784        let key = || {
785            pair.key
786                .clone()
787        };
788        match &pair.effect {
789            PairEffect::Ignored | PairEffect::Valueless => {
790                Some(Self::PairIgnored { block, key: key() })
791            }
792            PairEffect::Unreadable => Some(Self::PairUnreadable { block }),
793            PairEffect::Cleared => Some(Self::PairCleared { block, key: key() }),
794            PairEffect::Set(value) if !nested_vars && names_a_variable(value) => {
795                Some(Self::NestedVarsRefused { block, key: key() })
796            }
797            PairEffect::Set(_) => None,
798        }
799    }
800}
801
802impl ListWarning {
803    /// `cut`, its indices counting only the threads and groups [`FlattenedThread::read`] keeps.
804    fn past_limit(threads: &[Thread], cut: PastLimit) -> Self {
805        let kept_groups = |thread: &Thread, before: usize| {
806            thread
807                .groups
808                .iter()
809                .take(before)
810                .filter(|group| !group.is_empty())
811                .count()
812        };
813        Self::LegsPastLimit {
814            thread: cut
815                .thread
816                .map(|t| {
817                    threads
818                        .iter()
819                        .take(t)
820                        .filter(|thread| kept_groups(thread, usize::MAX) > 0)
821                        .count()
822                }),
823            group: cut
824                .thread
825                .and_then(|t| threads.get(t))
826                .zip(cut.group)
827                .map(|(thread, g)| kept_groups(thread, g)),
828            excess: cut.excess,
829        }
830    }
831}
832
833impl FlattenedDialStringError {
834    fn from_pipeline(error: PipelineError) -> Self {
835        match error {
836            PipelineError::Empty => Self::Empty,
837            PipelineError::ArgvSplit => Self::ArgvSplit,
838            PipelineError::UnclosedBlock { leg } => Self::UnclosedBlock { leg },
839            PipelineError::SplitSeparatorUnreadable => Self::SplitSeparatorUnreadable,
840        }
841    }
842}
843
844impl UnparsedLeg {
845    /// The endpoint text after the leg's blocks.
846    pub fn endpoint(&self) -> &str {
847        &self.endpoint
848    }
849
850    /// Why no typed endpoint accepts that text.
851    pub fn error(&self) -> &OriginateError {
852        &self.error
853    }
854}
855
856impl fmt::Display for UnparsedLeg {
857    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
858        let kind = if self
859            .endpoint
860            .is_empty()
861        {
862            "empty leg"
863        } else {
864            "unrecognized endpoint"
865        };
866        write!(
867            f,
868            "{kind} ({} bytes)",
869            self.endpoint
870                .len()
871        )
872    }
873}
874
875impl fmt::Display for LegWarning {
876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877        match self {
878            Self::PairIgnored { block, key } => {
879                write!(
880                    f,
881                    "pair {key} in block {block} has no value and is not installed"
882                )
883            }
884            Self::PairCleared { block, key } => {
885                write!(
886                    f,
887                    "pair {key} in block {block} is empty and deletes earlier values"
888                )
889            }
890            Self::NestedVarsRefused { block, key } => write!(
891                f,
892                "pair {key} in block {block} names a variable while origination_nested_vars is off"
893            ),
894            Self::BlockSeparatorUnreadable { block } => write!(
895                f,
896                "block {block} has a non-ASCII separator and contributes no pairs"
897            ),
898            Self::BlockRewritesFollowingText { block } => {
899                write!(f, "parsing block {block} rewrites the leg's text after it")
900            }
901            Self::PairUnreadable { block } => write!(
902                f,
903                "a pair in block {block} opens a non-ASCII ^^ separator the switch splits by byte"
904            ),
905        }
906    }
907}
908
909impl fmt::Display for ListWarning {
910    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911        match self {
912            Self::QuoteSpansLegs => f.write_str("a quote holds a leg or group separator"),
913            Self::CarrierExpands => {
914                f.write_str("the dialplan carrier expands a variable reference kept as written")
915            }
916            Self::BlockSeparatorUnreadable { block } => write!(
917                f,
918                "list block {block} has a non-ASCII separator and contributes no pairs"
919            ),
920            Self::BlockRewritesFollowingText { block } => {
921                write!(f, "parsing list block {block} rewrites the text after it")
922            }
923            Self::LegsPastLimit {
924                thread,
925                group,
926                excess,
927            } => {
928                match (thread, group) {
929                    (Some(thread), Some(group)) => {
930                        write!(f, "the leg split of group {group} in thread {thread}")?
931                    }
932                    (Some(thread), None) => write!(f, "the group split of thread {thread}")?,
933                    (None, _) => f.write_str("the thread split")?,
934                }
935                write!(
936                    f,
937                    " stops at the switch's limit of {MAX_PEERS}; \
938                     {excess} more are never split off"
939                )
940            }
941        }
942    }
943}
944
945impl fmt::Display for FlattenedDialStringError {
946    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
947        match self {
948            Self::Empty => f.write_str("dial string has no leg to dial"),
949            Self::ArgvSplit => f.write_str("originate's argument split cuts the dial string"),
950            Self::UnclosedBlock { leg } => write!(f, "a block on leg {leg} never closes"),
951            Self::TrailingNewline => f.write_str("dial string ends in a newline"),
952            Self::SplitSeparatorUnreadable => f.write_str(
953                "a split on a non-ASCII ^^ separator's first byte reaches past its text",
954            ),
955        }
956    }
957}
958
959impl std::error::Error for FlattenedDialStringError {}