Skip to main content

freeswitch_types/variables/
esl_headers.rs

1//! [`EslHeaders`] — a flat header store that understands FreeSWITCH's
2//! transport encodings.
3//!
4//! FreeSWITCH's ESL wire format carries headers and channel variables in the
5//! same flat key-value namespace, but with two transport quirks that plain
6//! RFC-SIP parsers don't account for:
7//!
8//! - **ARRAY encoding** — repeating SIP headers arrive as
9//!   `ARRAY::value1|:value2|:value3` (see [`EslArray`]).
10//! - **Bracket wrapping** — some log-sourced headers arrive as `[value]`.
11//!
12//! Routing those values through the default [`SipHeaderLookup`] methods
13//! produces parse errors because the string doesn't match RFC syntax.
14//! [`EslHeaders`] wraps an [`IndexMap<String, String>`] and overrides the
15//! relevant `SipHeaderLookup` methods to strip both quirks before parsing.
16//! The design-rationale doc §"EslHeaders: making the transport boundary
17//! visible" explains the layering.
18
19use indexmap::IndexMap;
20use sip_header::{
21    HistoryInfo, HistoryInfoError, SipHeader, SipHeaderLookup, UriInfo, UriInfoError,
22};
23
24use crate::lookup::HeaderLookup;
25use crate::variables::{EslArray, EslArrayError};
26
27/// A flat header store that decodes FreeSWITCH ARRAY and bracket encoding
28/// when answering typed SIP header queries.
29///
30/// Construct with [`EslHeaders::new`] or [`EslHeaders::from_map`]. Use it
31/// anywhere a [`HeaderLookup`] or [`SipHeaderLookup`] implementor is
32/// expected:
33///
34/// ```
35/// use freeswitch_types::{EslHeaders, HeaderLookup};
36/// use freeswitch_types::sip_header::SipHeaderLookup;
37///
38/// let mut h = EslHeaders::new();
39/// h.insert("Unique-ID", "abc-123");
40/// h.insert("Call-Info", "ARRAY::<sip:a@example.com>;purpose=icon|:<sip:b@example.com>");
41///
42/// assert_eq!(h.header_str("Unique-ID"), Some("abc-123"));
43/// let ci = h.call_info().unwrap().unwrap();
44/// assert_eq!(ci.entries().len(), 2);
45/// ```
46///
47/// `HeaderLookup` delegates straight to the map; `SipHeaderLookup` methods
48/// that parse RFC-structured values (`call_info`, `history_info`, and any
49/// future multi-value parsers) first peel the FreeSWITCH encoding and then
50/// hand pre-split entries to `sip-header`. Non-parsing lookups
51/// (`sip_header_str`, `sip_header`) return the raw stored value untouched —
52/// the caller sees exactly what FreeSWITCH put on the wire.
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct EslHeaders(IndexMap<String, String>);
56
57impl EslHeaders {
58    /// Create an empty store.
59    pub fn new() -> Self {
60        Self(IndexMap::new())
61    }
62
63    /// Wrap an existing map.
64    pub fn from_map(map: IndexMap<String, String>) -> Self {
65        Self(map)
66    }
67
68    /// Access the underlying map.
69    pub fn as_map(&self) -> &IndexMap<String, String> {
70        &self.0
71    }
72
73    /// Consume and return the underlying map.
74    pub fn into_map(self) -> IndexMap<String, String> {
75        self.0
76    }
77
78    /// Insert a header, replacing any existing entry at the same key.
79    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
80        self.0
81            .insert(key.into(), value.into());
82    }
83
84    /// Remove a header by key.
85    pub fn remove(&mut self, key: &str) -> Option<String> {
86        self.0
87            .shift_remove(key)
88    }
89
90    /// Number of entries.
91    pub fn len(&self) -> usize {
92        self.0
93            .len()
94    }
95
96    /// `true` if there are no entries.
97    pub fn is_empty(&self) -> bool {
98        self.0
99            .is_empty()
100    }
101}
102
103impl From<IndexMap<String, String>> for EslHeaders {
104    fn from(map: IndexMap<String, String>) -> Self {
105        Self(map)
106    }
107}
108
109/// Strip a single pair of outer `[...]` brackets from FreeSWITCH log-derived
110/// header values. If the value is not bracket-wrapped, returns it unchanged.
111fn strip_brackets(s: &str) -> &str {
112    if let Some(inner) = s.strip_prefix('[') {
113        if let Some(inner) = inner.strip_suffix(']') {
114            return inner;
115        }
116    }
117    s
118}
119
120/// Parse `value` as a `UriInfo`, handling both `ARRAY::` encoding and
121/// bracket wrapping. The plain `UriInfo::parse` path is only used when
122/// the value lacks the `ARRAY::` prefix; structural `EslArrayError`
123/// cases (e.g. `TooManyItems`) are surfaced via the closest-fit
124/// upstream variant rather than silently downgraded.
125fn parse_uri_info_value(value: &str) -> Result<UriInfo, UriInfoError> {
126    let value = strip_brackets(value);
127    match EslArray::parse(value) {
128        Ok(array) => UriInfo::from_entries(
129            array
130                .items()
131                .iter()
132                .map(String::as_str),
133        ),
134        Err(EslArrayError::MissingPrefix) => UriInfo::parse(value),
135        // Upstream UriInfoError lacks a generic "structural array
136        // failure" variant; carry the cause in MissingAngleBrackets so
137        // operators see the actual reason in logs.
138        Err(other) => Err(UriInfoError::MissingAngleBrackets(format!(
139            "ARRAY:: parse failed: {other}"
140        ))),
141    }
142}
143
144/// Parse `value` as a `HistoryInfo`, handling both `ARRAY::` encoding and
145/// bracket wrapping. Structural `EslArrayError` cases (e.g. `TooManyItems`)
146/// are surfaced as `HistoryInfoError::Empty` rather than silently falling
147/// back — upstream lacks a richer variant for non-entry array failures.
148fn parse_history_info_value(value: &str) -> Result<HistoryInfo, HistoryInfoError> {
149    let value = strip_brackets(value);
150    match EslArray::parse(value) {
151        Ok(array) => HistoryInfo::from_entries(
152            array
153                .items()
154                .iter()
155                .map(String::as_str),
156        ),
157        Err(EslArrayError::MissingPrefix) => HistoryInfo::parse(value),
158        Err(_) => Err(HistoryInfoError::Empty),
159    }
160}
161
162impl SipHeaderLookup for EslHeaders {
163    fn sip_header_str(&self, name: &str) -> Option<&str> {
164        self.0
165            .get(name)
166            .map(|s| s.as_str())
167    }
168
169    fn call_info(&self) -> Result<Option<UriInfo>, UriInfoError> {
170        match self.sip_header(SipHeader::CallInfo) {
171            Some(s) => parse_uri_info_value(s).map(Some),
172            None => Ok(None),
173        }
174    }
175
176    fn history_info(&self) -> Result<Option<HistoryInfo>, HistoryInfoError> {
177        match self.sip_header(SipHeader::HistoryInfo) {
178            Some(s) => parse_history_info_value(s).map(Some),
179            None => Ok(None),
180        }
181    }
182
183    fn alert_info(&self) -> Result<Option<UriInfo>, UriInfoError> {
184        match self.sip_header(SipHeader::AlertInfo) {
185            Some(s) => parse_uri_info_value(s).map(Some),
186            None => Ok(None),
187        }
188    }
189}
190
191impl HeaderLookup for EslHeaders {
192    fn header_str(&self, name: &str) -> Option<&str> {
193        self.0
194            .get(name)
195            .map(|s| s.as_str())
196    }
197
198    fn variable_str(&self, name: &str) -> Option<&str> {
199        self.0
200            .get(&format!("variable_{name}"))
201            .map(|s| s.as_str())
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::headers::EventHeader;
209
210    #[test]
211    fn header_str_passthrough() {
212        let mut h = EslHeaders::new();
213        h.insert("Unique-ID", "abc-123");
214        assert_eq!(h.header_str("Unique-ID"), Some("abc-123"));
215    }
216
217    #[test]
218    fn variable_str_prepends_variable_prefix() {
219        let mut h = EslHeaders::new();
220        h.insert("variable_sip_call_id", "call-1");
221        assert_eq!(h.variable_str("sip_call_id"), Some("call-1"));
222        assert_eq!(h.variable_str("missing"), None);
223    }
224
225    #[test]
226    fn call_info_single_value_rfc() {
227        let mut h = EslHeaders::new();
228        h.insert(
229            "Call-Info",
230            "<sip:alice@example.com>;purpose=emergency-CallId",
231        );
232        let ci = h
233            .call_info()
234            .unwrap()
235            .expect("present");
236        assert_eq!(
237            ci.entries()
238                .len(),
239            1
240        );
241        assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
242    }
243
244    #[test]
245    fn call_info_array_encoding() {
246        let mut h = EslHeaders::new();
247        h.insert(
248            "Call-Info",
249            "ARRAY::<sip:a@example.com>;purpose=icon|:<sip:b@example.com>;purpose=info",
250        );
251        let ci = h
252            .call_info()
253            .unwrap()
254            .expect("present");
255        assert_eq!(
256            ci.entries()
257                .len(),
258            2
259        );
260        assert_eq!(ci.entries()[0].purpose(), Some("icon"));
261        assert_eq!(ci.entries()[1].purpose(), Some("info"));
262    }
263
264    #[test]
265    fn call_info_bracket_wrapped() {
266        let mut h = EslHeaders::new();
267        h.insert(
268            "Call-Info",
269            "[<sip:alice@example.com>;purpose=emergency-CallId]",
270        );
271        let ci = h
272            .call_info()
273            .unwrap()
274            .expect("present");
275        assert_eq!(
276            ci.entries()
277                .len(),
278            1
279        );
280    }
281
282    #[test]
283    fn call_info_absent_is_ok_none() {
284        let h = EslHeaders::new();
285        assert!(h
286            .call_info()
287            .unwrap()
288            .is_none());
289    }
290
291    #[test]
292    fn history_info_array_encoding() {
293        let mut h = EslHeaders::new();
294        h.insert(
295            "History-Info",
296            "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1",
297        );
298        let hi = h
299            .history_info()
300            .unwrap()
301            .expect("present");
302        assert_eq!(
303            hi.entries()
304                .len(),
305            2
306        );
307    }
308
309    #[test]
310    fn header_lookup_typed_accessors() {
311        let mut h = EslHeaders::new();
312        h.insert(EventHeader::UniqueId.as_str(), "uuid-1");
313        h.insert(EventHeader::ChannelName.as_str(), "sofia/a/b");
314        assert_eq!(h.unique_id(), Some("uuid-1"));
315        assert_eq!(h.channel_name(), Some("sofia/a/b"));
316    }
317}