use std::fmt;
use std::ops::Range;
use std::sync::Arc;
use crate::channel::HangupCause;
use crate::commands::endpoint::Endpoint;
use crate::commands::originate::OriginateError;
use crate::commands::variables::{DialStringTarget, Variables, VariablesType};
use crate::switch_passes::brackets::{Block, Pair, PairEffect};
use crate::switch_passes::escape::{escape_text, EscapedField};
use crate::switch_passes::expansion::names_a_variable;
use crate::switch_passes::originate_legs::{resolve, DialList, Leg, Thread, ENTERPRISE_DELIM};
use crate::switch_passes::{pipeline, PipelineError};
use crate::variables::VariableName;
#[cfg(test)]
mod tests;
#[cfg(all(test, feature = "serde"))]
mod c_oracle;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlattenedDialString {
head: String,
blocks: Vec<Block>,
threads: Vec<FlattenedThread>,
tail: String,
warnings: Vec<ListWarning>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlattenedThread {
separator: String,
head: String,
blocks: Vec<Block>,
groups: Vec<FlattenedGroup>,
trailer: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlattenedGroup {
separator: String,
legs: Vec<FlattenedLeg>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlattenedLeg {
separator: String,
raw: String,
leg: Leg,
inherited: Arc<[Block]>,
nested_vars: bool,
target: LegTarget,
warnings: Vec<LegWarning>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LegTarget {
Error(ErrorLeg),
Endpoint(Endpoint),
Unparsed(UnparsedLeg),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorLeg {
as_written: String,
reading: CauseReading,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnparsedLeg {
endpoint: String,
error: OriginateError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CauseReading {
Name(HangupCause),
Number(u32),
Unrecognized,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LegWarning {
PairIgnored {
block: usize,
key: String,
},
PairCleared {
block: usize,
key: String,
},
NestedVarsRefused {
block: usize,
key: String,
},
BlockSeparatorUnreadable {
block: usize,
},
BlockRewritesFollowingText {
block: usize,
},
PairUnreadable {
block: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ListWarning {
QuoteSpansLegs,
CarrierExpands,
BlockSeparatorUnreadable {
block: usize,
},
BlockRewritesFollowingText {
block: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlattenedDialStringError {
Empty,
ArgvSplit,
UnclosedBlock {
leg: usize,
},
TrailingNewline,
SplitSeparatorUnreadable,
}
#[derive(Debug, Clone, Copy)]
pub struct FlattenedDialStringDisplay<'a> {
list: &'a FlattenedDialString,
render: Render,
}
#[derive(Debug, Clone, Copy)]
enum Render {
Raw,
For(DialStringTarget),
}
impl FlattenedDialString {
pub fn parse_for(
input: &str,
target: impl Into<DialStringTarget>,
) -> Result<Self, FlattenedDialStringError> {
if input.ends_with('\n') {
return Err(FlattenedDialStringError::TrailingNewline);
}
let list = pipeline::read(input, target.into())
.map_err(FlattenedDialStringError::from_pipeline)?;
let DialList {
blocks,
threads,
quote_spans_legs,
carrier_expands,
} = list;
let unreadable = blocks
.iter()
.chain(
threads
.iter()
.flat_map(|thread| &thread.blocks),
)
.enumerate()
.filter_map(|(block, parsed)| {
if parsed.separator_unreadable() {
Some(ListWarning::BlockSeparatorUnreadable { block })
} else {
parsed
.rewrites_following_text
.then_some(ListWarning::BlockRewritesFollowingText { block })
}
});
let warnings = [
(quote_spans_legs, ListWarning::QuoteSpansLegs),
(carrier_expands, ListWarning::CarrierExpands),
]
.into_iter()
.filter_map(|(raised, warning)| raised.then_some(warning))
.chain(unreadable)
.collect();
let mut kept: Vec<(Range<usize>, FlattenedThread)> = Vec::new();
for thread in threads {
let raw = thread
.raw
.clone();
let separator_start = kept
.last()
.map_or(raw.start, |(before, _)| before.end);
if let Some(read) = FlattenedThread::read(input, thread, &blocks, separator_start) {
kept.push((raw, read));
}
}
let (Some((first, _)), Some((last, _))) = (kept.first(), kept.last()) else {
return Err(FlattenedDialStringError::Empty);
};
Ok(Self {
head: slice(input, 0..first.start),
tail: slice(input, last.end..input.len()),
blocks,
threads: kept
.into_iter()
.map(|(_, thread)| thread)
.collect(),
warnings,
})
}
pub fn threads(&self) -> impl Iterator<Item = &FlattenedThread> {
self.threads
.iter()
}
pub fn legs(&self) -> impl Iterator<Item = &FlattenedLeg> {
self.threads
.iter()
.flat_map(FlattenedThread::legs)
}
pub fn warnings(&self) -> &[ListWarning] {
&self.warnings
}
pub fn retain(&mut self, mut keep: impl FnMut(&FlattenedLeg) -> bool) {
for thread in &mut self.threads {
for group in &mut thread.groups {
group
.legs
.retain(&mut keep);
}
thread
.groups
.retain(|group| {
!group
.legs
.is_empty()
});
}
self.threads
.retain(|thread| {
!thread
.groups
.is_empty()
});
}
pub fn is_empty(&self) -> bool {
self.threads
.is_empty()
}
fn write_raw(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return Ok(());
}
f.write_str(&self.head)?;
for (t, thread) in self
.threads
.iter()
.enumerate()
{
if t > 0 {
f.write_str(&thread.separator)?;
}
f.write_str(&thread.head)?;
for (g, group) in thread
.groups
.iter()
.enumerate()
{
if g > 0 {
f.write_str(&group.separator)?;
}
for (l, leg) in group
.legs
.iter()
.enumerate()
{
if l > 0 {
f.write_str(&leg.separator)?;
}
f.write_str(&leg.raw)?;
}
}
f.write_str(&thread.trailer)?;
}
f.write_str(&self.tail)
}
fn write_for(&self, f: &mut fmt::Formatter<'_>, target: DialStringTarget) -> fmt::Result {
if self.is_empty() {
return Ok(());
}
write_blocks(f, &self.blocks, target)?;
for (t, thread) in self
.threads
.iter()
.enumerate()
{
if t > 0 {
f.write_str(ENTERPRISE_DELIM)?;
}
write_blocks(f, &thread.blocks, target)?;
for (g, group) in thread
.groups
.iter()
.enumerate()
{
if g > 0 {
f.write_str("|")?;
}
for (l, leg) in group
.legs
.iter()
.enumerate()
{
if l > 0 {
f.write_str(",")?;
}
write_blocks(
f,
&leg.leg
.blocks,
target,
)?;
f.write_str(&escape_text(
&leg.leg
.endpoint,
target,
EscapedField::Endpoint,
))?;
}
}
}
Ok(())
}
pub fn display_raw(&self) -> FlattenedDialStringDisplay<'_> {
FlattenedDialStringDisplay {
list: self,
render: Render::Raw,
}
}
pub fn display_for(
&self,
target: impl Into<DialStringTarget>,
) -> FlattenedDialStringDisplay<'_> {
FlattenedDialStringDisplay {
list: self,
render: Render::For(target.into()),
}
}
}
impl fmt::Display for FlattenedDialStringDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.render {
Render::Raw => self
.list
.write_raw(f),
Render::For(target) => target.write_argument(f, |f, target| {
self.list
.write_for(f, target)
}),
}
}
}
fn write_blocks(
f: &mut fmt::Formatter<'_>,
blocks: &[Block],
target: DialStringTarget,
) -> fmt::Result {
for block in blocks {
let mut vars = Variables::new(VariablesType::of_block(block));
for pair in &block.pairs {
if let PairEffect::Set(value) = &pair.effect {
vars.insert(
pair.key
.as_str(),
value.as_str(),
);
}
}
if vars.is_empty() {
continue;
}
let vars = match block.separator {
',' => vars,
sep => vars
.clone()
.with_separator(sep)
.unwrap_or(vars),
};
write!(f, "{}", vars.display_for(target))?;
}
Ok(())
}
fn slice(input: &str, range: Range<usize>) -> String {
debug_assert!(input
.get(range.clone())
.is_some());
input
.get(range)
.unwrap_or_default()
.to_owned()
}
impl FlattenedThread {
fn read(
input: &str,
thread: Thread,
list_blocks: &[Block],
separator_start: usize,
) -> Option<Self> {
let Thread {
raw,
blocks,
groups: pipeline_groups,
nested_vars,
} = thread;
let inherited: Arc<[Block]> = list_blocks
.iter()
.chain(&blocks)
.cloned()
.collect();
let mut first_start = None;
let mut last_end = None;
let mut groups = Vec::new();
for group in pipeline_groups {
let mut separator = None;
let mut legs = Vec::with_capacity(group.len());
let mut leg_end = None;
for leg in group {
let start = leg
.raw
.start;
separator.get_or_insert_with(|| slice(input, last_end.unwrap_or(start)..start));
first_start.get_or_insert(start);
let leg_separator = slice(input, leg_end.unwrap_or(start)..start);
leg_end = Some(
leg.raw
.end,
);
legs.push(FlattenedLeg::read(
input,
leg,
leg_separator,
&inherited,
nested_vars,
));
}
let Some(separator) = separator else {
continue;
};
last_end = leg_end;
groups.push(FlattenedGroup { separator, legs });
}
let (start, end) = first_start.zip(last_end)?;
Some(Self {
separator: slice(input, separator_start..raw.start),
head: slice(input, raw.start..start),
blocks,
groups,
trailer: slice(input, end..raw.end),
})
}
pub fn groups(&self) -> impl Iterator<Item = &FlattenedGroup> {
self.groups
.iter()
}
pub fn legs(&self) -> impl Iterator<Item = &FlattenedLeg> {
self.groups
.iter()
.flat_map(FlattenedGroup::legs)
}
}
impl FlattenedGroup {
pub fn legs(&self) -> impl Iterator<Item = &FlattenedLeg> {
self.legs
.iter()
}
}
impl FlattenedLeg {
fn read(
input: &str,
leg: Leg,
separator: String,
inherited: &Arc<[Block]>,
nested_vars: bool,
) -> Self {
let warnings = leg
.blocks
.iter()
.enumerate()
.flat_map(|(block, parsed)| {
parsed
.separator_unreadable()
.then_some(LegWarning::BlockSeparatorUnreadable { block })
.into_iter()
.chain(
parsed
.rewrites_following_text
.then_some(LegWarning::BlockRewritesFollowingText { block }),
)
.chain(
parsed
.pairs
.iter()
.filter_map(move |pair| LegWarning::of(block, pair, nested_vars)),
)
})
.collect();
Self {
separator,
raw: slice(
input,
leg.raw
.clone(),
),
target: LegTarget::read(&leg.endpoint),
leg,
inherited: Arc::clone(inherited),
nested_vars,
warnings,
}
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn variable(&self, name: impl VariableName) -> Option<&str> {
self.variable_str(name.as_str())
}
pub fn variable_str(&self, name: &str) -> Option<&str> {
resolve(
self.inherited
.iter(),
&self.leg,
name,
self.nested_vars,
)
}
pub fn target(&self) -> &LegTarget {
&self.target
}
pub fn warnings(&self) -> &[LegWarning] {
&self.warnings
}
}
impl ErrorLeg {
pub fn as_written(&self) -> &str {
&self.as_written
}
pub fn reading(&self) -> CauseReading {
self.reading
}
pub fn cause(&self) -> Option<HangupCause> {
match self.reading {
CauseReading::Name(cause) => Some(cause),
CauseReading::Number(number) => u16::try_from(number)
.ok()
.and_then(HangupCause::from_number),
CauseReading::Unrecognized => None,
}
}
}
impl LegTarget {
fn read(endpoint: &str) -> Self {
if let Some(cause) = endpoint.strip_prefix("error/") {
return Self::Error(ErrorLeg {
as_written: cause.to_owned(),
reading: str2cause(cause),
});
}
match Endpoint::parse_bare(endpoint) {
Ok(parsed) => Self::Endpoint(parsed),
Err(error) => Self::Unparsed(UnparsedLeg {
endpoint: endpoint.to_owned(),
error,
}),
}
}
}
fn str2cause(text: &str) -> CauseReading {
if text.starts_with(|c: char| c.is_ascii_digit()) {
let long = text
.bytes()
.take_while(u8::is_ascii_digit)
.fold(0i128, |n, digit| {
(n * 10 + i128::from(digit - b'0')).min(i128::from(i64::MAX))
});
return CauseReading::Number(long as i64 as u32);
}
text.to_ascii_uppercase()
.parse::<HangupCause>()
.map_or(CauseReading::Unrecognized, CauseReading::Name)
}
impl LegWarning {
fn of(block: usize, pair: &Pair, nested_vars: bool) -> Option<Self> {
let key = || {
pair.key
.clone()
};
match &pair.effect {
PairEffect::Ignored | PairEffect::Valueless => {
Some(Self::PairIgnored { block, key: key() })
}
PairEffect::Unreadable => Some(Self::PairUnreadable { block }),
PairEffect::Cleared => Some(Self::PairCleared { block, key: key() }),
PairEffect::Set(value) if !nested_vars && names_a_variable(value) => {
Some(Self::NestedVarsRefused { block, key: key() })
}
PairEffect::Set(_) => None,
}
}
}
impl FlattenedDialStringError {
fn from_pipeline(error: PipelineError) -> Self {
match error {
PipelineError::Empty => Self::Empty,
PipelineError::ArgvSplit => Self::ArgvSplit,
PipelineError::UnclosedBlock { leg } => Self::UnclosedBlock { leg },
PipelineError::SplitSeparatorUnreadable => Self::SplitSeparatorUnreadable,
}
}
}
impl UnparsedLeg {
pub fn endpoint(&self) -> &str {
&self.endpoint
}
pub fn error(&self) -> &OriginateError {
&self.error
}
}
impl fmt::Display for UnparsedLeg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = if self
.endpoint
.is_empty()
{
"empty leg"
} else {
"unrecognized endpoint"
};
write!(
f,
"{kind} ({} bytes)",
self.endpoint
.len()
)
}
}
impl fmt::Display for LegWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PairIgnored { block, key } => {
write!(
f,
"pair {key} in block {block} has no value and is not installed"
)
}
Self::PairCleared { block, key } => {
write!(
f,
"pair {key} in block {block} is empty and deletes earlier values"
)
}
Self::NestedVarsRefused { block, key } => write!(
f,
"pair {key} in block {block} names a variable while origination_nested_vars is off"
),
Self::BlockSeparatorUnreadable { block } => write!(
f,
"block {block} has a non-ASCII separator and contributes no pairs"
),
Self::BlockRewritesFollowingText { block } => {
write!(f, "parsing block {block} rewrites the leg's text after it")
}
Self::PairUnreadable { block } => write!(
f,
"a pair in block {block} opens a non-ASCII ^^ separator the switch splits by byte"
),
}
}
}
impl fmt::Display for ListWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::QuoteSpansLegs => f.write_str("a quote holds a leg or group separator"),
Self::CarrierExpands => {
f.write_str("the dialplan carrier expands a variable reference kept as written")
}
Self::BlockSeparatorUnreadable { block } => write!(
f,
"list block {block} has a non-ASCII separator and contributes no pairs"
),
Self::BlockRewritesFollowingText { block } => {
write!(f, "parsing list block {block} rewrites the text after it")
}
}
}
}
impl fmt::Display for FlattenedDialStringError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("dial string has no leg to dial"),
Self::ArgvSplit => f.write_str("originate's argument split cuts the dial string"),
Self::UnclosedBlock { leg } => write!(f, "a block on leg {leg} never closes"),
Self::TrailingNewline => f.write_str("dial string ends in a newline"),
Self::SplitSeparatorUnreadable => f.write_str(
"a split on a non-ASCII ^^ separator's first byte reaches past its text",
),
}
}
}
impl std::error::Error for FlattenedDialStringError {}