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
120impl EslHeaders {
121    /// Parse a FreeSWITCH-transported SIP URI-list value into typed [`UriInfo`]
122    /// entries, handling both `ARRAY::` encoding and bracket wrapping.
123    ///
124    /// Use this when you hold a *raw* value — e.g. the `sip_call_info` /
125    /// `sip_alert_info` channel variable fetched over ESL — rather than a
126    /// populated [`EslHeaders`]. It accepts any of the forms FreeSWITCH emits:
127    ///
128    /// - **Single RFC entry**: `<sip:a@example.test>;purpose=emergency-CallId`
129    /// - **ARRAY encoding**: `ARRAY::<sip:a@example.test>;purpose=icon|:<sip:b@example.test>`
130    /// - **Bracket-wrapped**: `[<sip:a@example.test>;purpose=icon]`
131    ///
132    /// This is the same decoding the [`call_info`](SipHeaderLookup::call_info)
133    /// and [`alert_info`](SipHeaderLookup::alert_info) methods apply; iterate
134    /// the result via `.entries()`.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`UriInfoError`] if the value is malformed or if the `ARRAY::`
139    /// structure is invalid. Structural `EslArrayError` cases (e.g.
140    /// `TooManyItems`) are surfaced via [`UriInfoError::MissingAngleBrackets`]
141    /// carrying the cause so operators see the actual reason in logs.
142    ///
143    /// # Example
144    ///
145    /// ```
146    /// use freeswitch_types::EslHeaders;
147    ///
148    /// let value = "ARRAY::<urn:emergency:uid:callid:bcf.test>;purpose=emergency-CallId\
149    ///              |:<urn:emergency:uid:incidentid:bcf.test>;purpose=emergency-IncidentId";
150    /// let info = EslHeaders::parse_uri_info(value).unwrap();
151    /// assert_eq!(info.entries().len(), 2);
152    /// ```
153    pub fn parse_uri_info(value: &str) -> Result<UriInfo, UriInfoError> {
154        let value = strip_brackets(value);
155        match EslArray::parse(value) {
156            Ok(array) => UriInfo::from_entries(
157                array
158                    .items()
159                    .iter()
160                    .map(String::as_str),
161            ),
162            Err(EslArrayError::MissingPrefix) => UriInfo::parse(value),
163            // Upstream UriInfoError lacks a generic "structural array
164            // failure" variant; carry the cause in MissingAngleBrackets so
165            // operators see the actual reason in logs.
166            Err(other) => Err(UriInfoError::MissingAngleBrackets(format!(
167                "ARRAY:: parse failed: {other}"
168            ))),
169        }
170    }
171
172    /// Parse a FreeSWITCH-transported `History-Info` value into a typed
173    /// [`HistoryInfo`], handling both `ARRAY::` encoding and bracket wrapping.
174    ///
175    /// The raw-value counterpart to [`history_info`](SipHeaderLookup::history_info).
176    ///
177    /// # Errors
178    ///
179    /// Structural `EslArrayError` cases (e.g. `TooManyItems`) are surfaced as
180    /// [`HistoryInfoError::Empty`] rather than silently falling back — upstream
181    /// lacks a richer variant for non-entry array failures.
182    pub fn parse_history_info(value: &str) -> Result<HistoryInfo, HistoryInfoError> {
183        let value = strip_brackets(value);
184        match EslArray::parse(value) {
185            Ok(array) => HistoryInfo::from_entries(
186                array
187                    .items()
188                    .iter()
189                    .map(String::as_str),
190            ),
191            Err(EslArrayError::MissingPrefix) => HistoryInfo::parse(value),
192            Err(_) => Err(HistoryInfoError::Empty),
193        }
194    }
195}
196
197impl SipHeaderLookup for EslHeaders {
198    fn sip_header_str(&self, name: &str) -> Option<&str> {
199        self.0
200            .get(name)
201            .map(|s| s.as_str())
202    }
203
204    fn call_info(&self) -> Result<Option<UriInfo>, UriInfoError> {
205        match self.sip_header(SipHeader::CallInfo) {
206            Some(s) => Self::parse_uri_info(s).map(Some),
207            None => Ok(None),
208        }
209    }
210
211    fn history_info(&self) -> Result<Option<HistoryInfo>, HistoryInfoError> {
212        match self.sip_header(SipHeader::HistoryInfo) {
213            Some(s) => Self::parse_history_info(s).map(Some),
214            None => Ok(None),
215        }
216    }
217
218    fn alert_info(&self) -> Result<Option<UriInfo>, UriInfoError> {
219        match self.sip_header(SipHeader::AlertInfo) {
220            Some(s) => Self::parse_uri_info(s).map(Some),
221            None => Ok(None),
222        }
223    }
224}
225
226impl HeaderLookup for EslHeaders {
227    fn header_str(&self, name: &str) -> Option<&str> {
228        self.0
229            .get(name)
230            .map(|s| s.as_str())
231    }
232
233    fn variable_str(&self, name: &str) -> Option<&str> {
234        self.0
235            .get(&format!("variable_{name}"))
236            .map(|s| s.as_str())
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::headers::EventHeader;
244
245    #[test]
246    fn header_str_passthrough() {
247        let mut h = EslHeaders::new();
248        h.insert("Unique-ID", "abc-123");
249        assert_eq!(h.header_str("Unique-ID"), Some("abc-123"));
250    }
251
252    #[test]
253    fn variable_str_prepends_variable_prefix() {
254        let mut h = EslHeaders::new();
255        h.insert("variable_sip_call_id", "call-1");
256        assert_eq!(h.variable_str("sip_call_id"), Some("call-1"));
257        assert_eq!(h.variable_str("missing"), None);
258    }
259
260    #[test]
261    fn call_info_single_value_rfc() {
262        let mut h = EslHeaders::new();
263        h.insert(
264            "Call-Info",
265            "<sip:alice@example.com>;purpose=emergency-CallId",
266        );
267        let ci = h
268            .call_info()
269            .unwrap()
270            .expect("present");
271        assert_eq!(
272            ci.entries()
273                .len(),
274            1
275        );
276        assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
277    }
278
279    #[test]
280    fn call_info_array_encoding() {
281        let mut h = EslHeaders::new();
282        h.insert(
283            "Call-Info",
284            "ARRAY::<sip:a@example.com>;purpose=icon|:<sip:b@example.com>;purpose=info",
285        );
286        let ci = h
287            .call_info()
288            .unwrap()
289            .expect("present");
290        assert_eq!(
291            ci.entries()
292                .len(),
293            2
294        );
295        assert_eq!(ci.entries()[0].purpose(), Some("icon"));
296        assert_eq!(ci.entries()[1].purpose(), Some("info"));
297    }
298
299    #[test]
300    fn call_info_bracket_wrapped() {
301        let mut h = EslHeaders::new();
302        h.insert(
303            "Call-Info",
304            "[<sip:alice@example.com>;purpose=emergency-CallId]",
305        );
306        let ci = h
307            .call_info()
308            .unwrap()
309            .expect("present");
310        assert_eq!(
311            ci.entries()
312                .len(),
313            1
314        );
315    }
316
317    #[test]
318    fn call_info_absent_is_ok_none() {
319        let h = EslHeaders::new();
320        assert!(h
321            .call_info()
322            .unwrap()
323            .is_none());
324    }
325
326    #[test]
327    fn history_info_array_encoding() {
328        let mut h = EslHeaders::new();
329        h.insert(
330            "History-Info",
331            "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1",
332        );
333        let hi = h
334            .history_info()
335            .unwrap()
336            .expect("present");
337        assert_eq!(
338            hi.entries()
339                .len(),
340            2
341        );
342    }
343
344    #[test]
345    fn header_lookup_typed_accessors() {
346        let mut h = EslHeaders::new();
347        h.insert(EventHeader::UniqueId.as_str(), "uuid-1");
348        h.insert(EventHeader::ChannelName.as_str(), "sofia/a/b");
349        assert_eq!(h.unique_id(), Some("uuid-1"));
350        assert_eq!(h.channel_name(), Some("sofia/a/b"));
351    }
352
353    #[test]
354    fn parse_uri_info_array_form() {
355        let value = "ARRAY::<urn:emergency:uid:callid:bcf.example.test>;purpose=emergency-CallId\
356                     |:<urn:emergency:uid:incidentid:bcf.example.test>;purpose=emergency-IncidentId\
357                     |:<https://eido.example.test/v1/bcf.example.test/abc?test-call=true>;purpose=emergency-eido";
358        let info = EslHeaders::parse_uri_info(value).expect("parse ARRAY form");
359        let entries = info.entries();
360        assert_eq!(entries.len(), 3);
361        assert_eq!(entries[0].purpose(), Some("emergency-CallId"));
362        assert_eq!(entries[1].purpose(), Some("emergency-IncidentId"));
363        assert_eq!(entries[2].purpose(), Some("emergency-eido"));
364    }
365
366    #[test]
367    fn parse_uri_info_single_entry() {
368        let value = "<urn:emergency:uid:callid:test>;purpose=emergency-CallId";
369        let info = EslHeaders::parse_uri_info(value).expect("parse single entry");
370        assert_eq!(
371            info.entries()
372                .len(),
373            1
374        );
375        assert_eq!(info.entries()[0].purpose(), Some("emergency-CallId"));
376    }
377
378    #[test]
379    fn parse_uri_info_empty_value() {
380        // Empty string is an error (no angle brackets)
381        let result = EslHeaders::parse_uri_info("");
382        assert!(result.is_err());
383    }
384
385    #[test]
386    fn parse_uri_info_malformed_no_panic() {
387        // sip-header UriInfo is lenient - this parses without angle brackets
388        let info = EslHeaders::parse_uri_info("sip:bare@example.test").expect("lenient parse");
389        assert_eq!(
390            info.entries()
391                .len(),
392            1
393        );
394    }
395
396    #[test]
397    fn parse_uri_info_bracket_wrapped() {
398        let value = "[<urn:emergency:uid:callid:test>;purpose=emergency-CallId]";
399        let info = EslHeaders::parse_uri_info(value).expect("parse bracket-wrapped");
400        assert_eq!(
401            info.entries()
402                .len(),
403            1
404        );
405        assert_eq!(info.entries()[0].purpose(), Some("emergency-CallId"));
406    }
407
408    #[test]
409    fn parse_history_info_array_form() {
410        let value = "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1";
411        let info = EslHeaders::parse_history_info(value).expect("parse ARRAY form");
412        assert_eq!(
413            info.entries()
414                .len(),
415            2
416        );
417    }
418}