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 as [`UriInfoError::Malformed`] carrying
141    /// the cause.
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            Err(other) => Err(UriInfoError::Malformed(other.to_string())),
164        }
165    }
166
167    /// Parse a FreeSWITCH-transported `History-Info` value into a typed
168    /// [`HistoryInfo`], handling both `ARRAY::` encoding and bracket wrapping.
169    ///
170    /// The raw-value counterpart to [`history_info`](SipHeaderLookup::history_info).
171    ///
172    /// # Errors
173    ///
174    /// Structural `EslArrayError` cases (e.g. `TooManyItems`) are surfaced as
175    /// [`HistoryInfoError::Malformed`] carrying the cause.
176    pub fn parse_history_info(value: &str) -> Result<HistoryInfo, HistoryInfoError> {
177        let value = strip_brackets(value);
178        match EslArray::parse(value) {
179            Ok(array) => HistoryInfo::from_entries(
180                array
181                    .items()
182                    .iter()
183                    .map(String::as_str),
184            ),
185            Err(EslArrayError::MissingPrefix) => HistoryInfo::parse(value),
186            Err(other) => Err(HistoryInfoError::Malformed(other.to_string())),
187        }
188    }
189}
190
191impl SipHeaderLookup for EslHeaders {
192    fn sip_header_str(&self, name: &str) -> Option<&str> {
193        self.0
194            .get(name)
195            .map(|s| s.as_str())
196    }
197
198    fn call_info(&self) -> Result<Option<UriInfo>, UriInfoError> {
199        match self.sip_header(SipHeader::CallInfo) {
200            Some(s) => Self::parse_uri_info(s).map(Some),
201            None => Ok(None),
202        }
203    }
204
205    fn history_info(&self) -> Result<Option<HistoryInfo>, HistoryInfoError> {
206        match self.sip_header(SipHeader::HistoryInfo) {
207            Some(s) => Self::parse_history_info(s).map(Some),
208            None => Ok(None),
209        }
210    }
211
212    fn alert_info(&self) -> Result<Option<UriInfo>, UriInfoError> {
213        match self.sip_header(SipHeader::AlertInfo) {
214            Some(s) => Self::parse_uri_info(s).map(Some),
215            None => Ok(None),
216        }
217    }
218}
219
220impl HeaderLookup for EslHeaders {
221    fn header_str(&self, name: &str) -> Option<&str> {
222        self.0
223            .get(name)
224            .map(|s| s.as_str())
225    }
226
227    fn variable_str(&self, name: &str) -> Option<&str> {
228        self.0
229            .get(&format!("variable_{name}"))
230            .map(|s| s.as_str())
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::headers::EventHeader;
238
239    #[test]
240    fn header_str_passthrough() {
241        let mut h = EslHeaders::new();
242        h.insert("Unique-ID", "abc-123");
243        assert_eq!(h.header_str("Unique-ID"), Some("abc-123"));
244    }
245
246    #[test]
247    fn variable_str_prepends_variable_prefix() {
248        let mut h = EslHeaders::new();
249        h.insert("variable_sip_call_id", "call-1");
250        assert_eq!(h.variable_str("sip_call_id"), Some("call-1"));
251        assert_eq!(h.variable_str("missing"), None);
252    }
253
254    #[test]
255    fn call_info_single_value_rfc() {
256        let mut h = EslHeaders::new();
257        h.insert(
258            "Call-Info",
259            "<sip:alice@example.com>;purpose=emergency-CallId",
260        );
261        let ci = h
262            .call_info()
263            .unwrap()
264            .expect("present");
265        assert_eq!(
266            ci.entries()
267                .len(),
268            1
269        );
270        assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
271    }
272
273    #[test]
274    fn call_info_array_encoding() {
275        let mut h = EslHeaders::new();
276        h.insert(
277            "Call-Info",
278            "ARRAY::<sip:a@example.com>;purpose=icon|:<sip:b@example.com>;purpose=info",
279        );
280        let ci = h
281            .call_info()
282            .unwrap()
283            .expect("present");
284        assert_eq!(
285            ci.entries()
286                .len(),
287            2
288        );
289        assert_eq!(ci.entries()[0].purpose(), Some("icon"));
290        assert_eq!(ci.entries()[1].purpose(), Some("info"));
291    }
292
293    #[test]
294    fn call_info_bracket_wrapped() {
295        let mut h = EslHeaders::new();
296        h.insert(
297            "Call-Info",
298            "[<sip:alice@example.com>;purpose=emergency-CallId]",
299        );
300        let ci = h
301            .call_info()
302            .unwrap()
303            .expect("present");
304        assert_eq!(
305            ci.entries()
306                .len(),
307            1
308        );
309    }
310
311    #[test]
312    fn call_info_absent_is_ok_none() {
313        let h = EslHeaders::new();
314        assert!(h
315            .call_info()
316            .unwrap()
317            .is_none());
318    }
319
320    #[test]
321    fn history_info_array_encoding() {
322        let mut h = EslHeaders::new();
323        h.insert(
324            "History-Info",
325            "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1",
326        );
327        let hi = h
328            .history_info()
329            .unwrap()
330            .expect("present");
331        assert_eq!(
332            hi.entries()
333                .len(),
334            2
335        );
336    }
337
338    #[test]
339    fn header_lookup_typed_accessors() {
340        let mut h = EslHeaders::new();
341        h.insert(EventHeader::UniqueId.as_str(), "uuid-1");
342        h.insert(EventHeader::ChannelName.as_str(), "sofia/a/b");
343        assert_eq!(h.unique_id(), Some("uuid-1"));
344        assert_eq!(h.channel_name(), Some("sofia/a/b"));
345    }
346
347    #[test]
348    fn parse_uri_info_array_form() {
349        let value = "ARRAY::<urn:emergency:uid:callid:bcf.example.test>;purpose=emergency-CallId\
350                     |:<urn:emergency:uid:incidentid:bcf.example.test>;purpose=emergency-IncidentId\
351                     |:<https://eido.example.test/v1/bcf.example.test/abc?test-call=true>;purpose=emergency-eido";
352        let info = EslHeaders::parse_uri_info(value).expect("parse ARRAY form");
353        let entries = info.entries();
354        assert_eq!(entries.len(), 3);
355        assert_eq!(entries[0].purpose(), Some("emergency-CallId"));
356        assert_eq!(entries[1].purpose(), Some("emergency-IncidentId"));
357        assert_eq!(entries[2].purpose(), Some("emergency-eido"));
358    }
359
360    #[test]
361    fn parse_uri_info_single_entry() {
362        let value = "<urn:emergency:uid:callid:test>;purpose=emergency-CallId";
363        let info = EslHeaders::parse_uri_info(value).expect("parse single entry");
364        assert_eq!(
365            info.entries()
366                .len(),
367            1
368        );
369        assert_eq!(info.entries()[0].purpose(), Some("emergency-CallId"));
370    }
371
372    #[test]
373    fn parse_uri_info_empty_value() {
374        // Empty string is an error (no angle brackets)
375        let result = EslHeaders::parse_uri_info("");
376        assert!(result.is_err());
377    }
378
379    #[test]
380    fn parse_uri_info_malformed_no_panic() {
381        // sip-header UriInfo is lenient - this parses without angle brackets
382        let info = EslHeaders::parse_uri_info("sip:bare@example.test").expect("lenient parse");
383        assert_eq!(
384            info.entries()
385                .len(),
386            1
387        );
388    }
389
390    #[test]
391    fn parse_uri_info_bracket_wrapped() {
392        let value = "[<urn:emergency:uid:callid:test>;purpose=emergency-CallId]";
393        let info = EslHeaders::parse_uri_info(value).expect("parse bracket-wrapped");
394        assert_eq!(
395            info.entries()
396                .len(),
397            1
398        );
399        assert_eq!(info.entries()[0].purpose(), Some("emergency-CallId"));
400    }
401
402    #[test]
403    fn parse_uri_info_structural_failure_is_malformed() {
404        let value = format!(
405            "ARRAY::{}",
406            vec!["<sip:a@example.com>"; crate::variables::MAX_ARRAY_ITEMS + 1].join("|:")
407        );
408        let err = EslHeaders::parse_uri_info(&value).expect_err("over-limit array must fail");
409        assert!(matches!(err, UriInfoError::Malformed(_)), "got {err:?}");
410    }
411
412    #[test]
413    fn parse_history_info_structural_failure_is_malformed() {
414        let value = format!(
415            "ARRAY::{}",
416            vec!["<sip:a@example.com>;index=1"; crate::variables::MAX_ARRAY_ITEMS + 1].join("|:")
417        );
418        let err = EslHeaders::parse_history_info(&value).expect_err("over-limit array must fail");
419        assert!(matches!(err, HistoryInfoError::Malformed(_)), "got {err:?}");
420    }
421
422    #[test]
423    fn parse_history_info_array_form() {
424        let value = "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1";
425        let info = EslHeaders::parse_history_info(value).expect("parse ARRAY form");
426        assert_eq!(
427            info.entries()
428                .len(),
429            2
430        );
431    }
432}