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::{HistoryInfo, HistoryInfoError, SipHeaderLookup, UriInfo, UriInfoError};
21
22use crate::headers::{case_alias_key, normalize_header_key};
23use crate::lookup::{variable_key, HeaderLookup};
24use crate::variables::{EslArray, EslArrayError, MAX_ARRAY_ITEMS};
25
26/// A flat header store that decodes FreeSWITCH ARRAY and bracket encoding
27/// when answering typed SIP header queries.
28///
29/// Construct with [`EslHeaders::new`] or [`EslHeaders::from_map`]. Use it
30/// anywhere a [`HeaderLookup`] or [`SipHeaderLookup`] implementor is
31/// expected:
32///
33/// ```
34/// use freeswitch_types::{EslHeaders, HeaderLookup};
35/// use freeswitch_types::sip_header::SipHeaderLookup;
36///
37/// let mut h = EslHeaders::new();
38/// h.insert("Unique-ID", "abc-123");
39/// h.insert("Call-Info", "ARRAY::<sip:a@example.com>;purpose=icon|:<sip:b@example.com>");
40///
41/// assert_eq!(h.header_str("Unique-ID"), Some("abc-123"));
42/// let ci = h.call_info().unwrap().unwrap();
43/// assert_eq!(ci.entries().len(), 2);
44/// ```
45///
46/// `HeaderLookup` delegates straight to the map; `SipHeaderLookup` methods
47/// that parse RFC-structured values (`call_info`, `history_info`, and any
48/// future multi-value parsers) first peel the FreeSWITCH encoding and then
49/// hand pre-split entries to `sip-header`. Non-parsing lookups
50/// (`sip_header_str`, `sip_header`) return the raw stored value untouched —
51/// the caller sees exactly what FreeSWITCH put on the wire.
52///
53/// Every write normalizes its key with
54/// [`normalize_header_key`](crate::normalize_header_key), so a payload
55/// spelling one header two ways collapses to one entry, and every read falls
56/// back to a lowercase alias, so a query in any casing resolves. Keys
57/// containing `_` are exempt from both: their suffix carries SIP wire casing.
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
60pub struct EslHeaders {
61    map: IndexMap<String, String>,
62    /// Lowercase alias to canonical key, for the case-insensitive read path.
63    /// Derived from `map`, so it is rebuilt rather than serialized.
64    #[cfg_attr(feature = "serde", serde(skip))]
65    aliases: IndexMap<String, String>,
66}
67
68impl EslHeaders {
69    /// Create an empty store.
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Wrap an existing map, normalizing every key.
75    pub fn from_map(map: IndexMap<String, String>) -> Self {
76        map.into_iter()
77            .collect()
78    }
79
80    /// Access the underlying map, keyed canonically.
81    pub fn as_map(&self) -> &IndexMap<String, String> {
82        &self.map
83    }
84
85    /// Consume and return the underlying map, keyed canonically.
86    pub fn into_map(self) -> IndexMap<String, String> {
87        self.map
88    }
89
90    /// Insert a header under its canonical key, replacing any entry there.
91    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
92        let key = normalize_header_key(&key.into());
93        if let Some(alias) = case_alias_key(&key) {
94            self.aliases
95                .insert(alias, key.clone());
96        }
97        self.map
98            .insert(key, value.into());
99    }
100
101    /// Remove a header, by canonical key or by any other casing of it.
102    pub fn remove(&mut self, key: &str) -> Option<String> {
103        let canonical = if self
104            .map
105            .contains_key(key)
106        {
107            key.to_string()
108        } else {
109            let alias = case_alias_key(key)?;
110            self.aliases
111                .get(&alias)?
112                .clone()
113        };
114        if let Some(alias) = case_alias_key(&canonical) {
115            self.aliases
116                .shift_remove(&alias);
117        }
118        self.map
119            .shift_remove(&canonical)
120    }
121
122    /// Number of entries.
123    pub fn len(&self) -> usize {
124        self.map
125            .len()
126    }
127
128    /// `true` if there are no entries.
129    pub fn is_empty(&self) -> bool {
130        self.map
131            .is_empty()
132    }
133
134    fn get(&self, name: &str) -> Option<&str> {
135        if let Some(value) = self
136            .map
137            .get(name)
138        {
139            return Some(value.as_str());
140        }
141        self.aliases
142            .get(&case_alias_key(name)?)
143            .and_then(|canonical| {
144                self.map
145                    .get(canonical)
146            })
147            .map(|s| s.as_str())
148    }
149}
150
151impl<K: Into<String>, V: Into<String>> FromIterator<(K, V)> for EslHeaders {
152    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
153        let mut headers = Self::new();
154        headers.extend(iter);
155        headers
156    }
157}
158
159impl<K: Into<String>, V: Into<String>> Extend<(K, V)> for EslHeaders {
160    fn extend<I: IntoIterator<Item = (K, V)>>(&mut self, iter: I) {
161        for (key, value) in iter {
162            self.insert(key, value);
163        }
164    }
165}
166
167impl From<IndexMap<String, String>> for EslHeaders {
168    fn from(map: IndexMap<String, String>) -> Self {
169        Self::from_map(map)
170    }
171}
172
173#[cfg(feature = "serde")]
174impl<'de> serde::Deserialize<'de> for EslHeaders {
175    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
176    where
177        D: serde::Deserializer<'de>,
178    {
179        Ok(Self::from_map(IndexMap::deserialize(deserializer)?))
180    }
181}
182
183/// Strip a single pair of outer `[...]` brackets from FreeSWITCH log-derived
184/// header values. If the value is not bracket-wrapped, returns it unchanged.
185fn strip_brackets(s: &str) -> &str {
186    if let Some(inner) = s.strip_prefix('[') {
187        if let Some(inner) = inner.strip_suffix(']') {
188            return inner;
189        }
190    }
191    s
192}
193
194/// One row per header occurrence: bracket unwrapping, then `ARRAY::` splitting.
195/// A value without the prefix is one occurrence and stays whole.
196fn split_rows(value: &str) -> Result<Vec<&str>, EslArrayError> {
197    let value = strip_brackets(value);
198    let Some(body) = value.strip_prefix(EslArray::PREFIX) else {
199        return Ok(vec![value]);
200    };
201    let items: Vec<&str> = body
202        .split(EslArray::SEPARATOR)
203        .take(MAX_ARRAY_ITEMS + 1)
204        .collect();
205    if items.len() > MAX_ARRAY_ITEMS {
206        return Err(EslArrayError::TooManyItems {
207            count: body
208                .split(EslArray::SEPARATOR)
209                .count(),
210            max: MAX_ARRAY_ITEMS,
211        });
212    }
213    Ok(items)
214}
215
216/// List entries of a raw value: every row comma-split, since `from_entries`
217/// takes entries a transport already delimited.
218fn split_entries_ref(value: &str) -> Result<Vec<&str>, EslArrayError> {
219    Ok(split_rows(value)?
220        .into_iter()
221        .filter(|row| {
222            !row.trim()
223                .is_empty()
224        })
225        .flat_map(sip_header::split_comma_entries)
226        .collect())
227}
228
229/// A structurally invalid ESL encoding hands the value back whole, so the RFC
230/// parser reports it rather than this layer dropping entries.
231fn rows_or_whole(value: &str) -> Vec<&str> {
232    split_rows(value).unwrap_or_else(|_| vec![value])
233}
234
235impl EslHeaders {
236    /// Parse a FreeSWITCH-transported SIP URI-list value into typed [`UriInfo`]
237    /// entries, handling both `ARRAY::` encoding and bracket wrapping.
238    ///
239    /// Use this when you hold a *raw* value — e.g. the `sip_call_info` /
240    /// `sip_alert_info` channel variable fetched over ESL — rather than a
241    /// populated [`EslHeaders`]. It accepts any of the forms FreeSWITCH emits:
242    ///
243    /// - **Single RFC entry**: `<sip:a@example.test>;purpose=emergency-CallId`
244    /// - **ARRAY encoding**: `ARRAY::<sip:a@example.test>;purpose=icon|:<sip:b@example.test>`
245    /// - **Bracket-wrapped**: `[<sip:a@example.test>;purpose=icon]`
246    ///
247    /// This is the same decoding the [`call_info`](SipHeaderLookup::call_info)
248    /// and [`alert_info`](SipHeaderLookup::alert_info) methods apply; iterate
249    /// the result via `.entries()`.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`UriInfoError`] if the value is malformed or if the `ARRAY::`
254    /// structure is invalid. Structural `EslArrayError` cases (e.g.
255    /// `TooManyItems`) are surfaced as [`UriInfoError::Malformed`] carrying
256    /// the cause.
257    ///
258    /// # Example
259    ///
260    /// ```
261    /// use freeswitch_types::EslHeaders;
262    ///
263    /// let value = "ARRAY::<urn:emergency:uid:callid:bcf.test>;purpose=emergency-CallId\
264    ///              |:<urn:emergency:uid:incidentid:bcf.test>;purpose=emergency-IncidentId";
265    /// let info = EslHeaders::parse_uri_info(value).unwrap();
266    /// assert_eq!(info.entries().len(), 2);
267    /// ```
268    pub fn parse_uri_info(value: &str) -> Result<UriInfo, UriInfoError> {
269        let entries =
270            Self::split_entries(value).map_err(|e| UriInfoError::Malformed(e.to_string()))?;
271        UriInfo::from_entries(
272            entries
273                .iter()
274                .map(String::as_str),
275        )
276    }
277
278    /// Split one comma-list SIP header value held as an ESL variable into its
279    /// entries, applying the same decoding as [`parse_uri_info`](Self::parse_uri_info)
280    /// and [`parse_history_info`](Self::parse_history_info): bracket unwrapping,
281    /// `ARRAY::` splitting, then the RFC comma split of each item.
282    ///
283    /// Only for headers that are lists (`Call-Info`, `Alert-Info`,
284    /// `History-Info`, `Diversion`, …). A header whose value legitimately
285    /// contains commas without being a list — `Date`, `Subject`, `User-Agent` —
286    /// must not go through it.
287    ///
288    /// # Errors
289    ///
290    /// A missing `ARRAY::` prefix is the RFC form, not an error. Every other
291    /// [`EslArrayError`] is a structural fault in the ESL encoding and is
292    /// returned as-is. An empty or whitespace-only value yields no entries,
293    /// which the typed parsers turn into their own `Empty` error.
294    ///
295    /// ```
296    /// use freeswitch_types::EslHeaders;
297    ///
298    /// let entries =
299    ///     EslHeaders::split_entries("ARRAY::<sip:a@example.test>|:<sip:b@example.test>").unwrap();
300    /// assert_eq!(entries, ["<sip:a@example.test>", "<sip:b@example.test>"]);
301    /// ```
302    pub fn split_entries(value: &str) -> Result<Vec<String>, EslArrayError> {
303        Ok(split_entries_ref(value)?
304            .into_iter()
305            .map(str::to_string)
306            .collect())
307    }
308
309    /// Rows of a header `sip-header` marks multi-valued, one per occurrence, for
310    /// [`sip_header_all_str`](SipHeaderLookup::sip_header_all_str). Commas are
311    /// left to the accessor, which knows whether the header is a list.
312    #[doc(hidden)]
313    pub fn split_multi_value<'a>(value: Option<&'a str>, name: &str) -> Vec<&'a str> {
314        let Some(value) = value else {
315            return Vec::new();
316        };
317        let multi = name
318            .parse::<sip_header::SipHeader>()
319            .is_ok_and(|h| h.is_multi_valued());
320        if multi {
321            rows_or_whole(value)
322        } else {
323            vec![value]
324        }
325    }
326
327    /// Parse a FreeSWITCH-transported `History-Info` value into a typed
328    /// [`HistoryInfo`], handling both `ARRAY::` encoding and bracket wrapping.
329    ///
330    /// The raw-value counterpart to [`history_info`](SipHeaderLookup::history_info).
331    ///
332    /// # Errors
333    ///
334    /// Structural `EslArrayError` cases (e.g. `TooManyItems`) are surfaced as
335    /// [`HistoryInfoError::Malformed`] carrying the cause.
336    pub fn parse_history_info(value: &str) -> Result<HistoryInfo, HistoryInfoError> {
337        let entries =
338            Self::split_entries(value).map_err(|e| HistoryInfoError::Malformed(e.to_string()))?;
339        HistoryInfo::from_entries(
340            entries
341                .iter()
342                .map(String::as_str),
343        )
344    }
345}
346
347/// Internal: emits the one [`SipHeaderLookup`] override that peels
348/// FreeSWITCH's ARRAY and bracket encoding, `sip_header_all_str`, which every
349/// list-valued default routes through. Invoke inside an
350/// `impl SipHeaderLookup for T` block. Not part of the stable API.
351#[doc(hidden)]
352#[macro_export]
353macro_rules! esl_sip_header_overrides {
354    () => {
355        fn sip_header_all_str<'a>(&'a self, name: &str) -> Vec<&'a str> {
356            $crate::variables::EslHeaders::split_multi_value(self.sip_header_str(name), name)
357        }
358    };
359}
360
361impl SipHeaderLookup for EslHeaders {
362    fn sip_header_str(&self, name: &str) -> Option<&str> {
363        self.get(name)
364    }
365
366    crate::esl_sip_header_overrides!();
367}
368
369impl HeaderLookup for EslHeaders {
370    fn header_str(&self, name: &str) -> Option<&str> {
371        self.get(name)
372    }
373
374    fn variable_str(&self, name: &str) -> Option<&str> {
375        self.get(&variable_key(name))
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use crate::headers::EventHeader;
383
384    #[test]
385    fn every_write_path_normalizes_the_key() {
386        let mut inserted = EslHeaders::new();
387        inserted.insert("unique-id", "abc-123");
388
389        let mut extended = EslHeaders::new();
390        extended.extend([("unique-id", "abc-123")]);
391
392        let map: IndexMap<String, String> = [("unique-id".to_string(), "abc-123".to_string())]
393            .into_iter()
394            .collect();
395
396        for h in [
397            inserted,
398            extended,
399            [("unique-id", "abc-123")]
400                .into_iter()
401                .collect(),
402            EslHeaders::from_map(map.clone()),
403            EslHeaders::from(map),
404        ] {
405            assert_eq!(
406                h.as_map()
407                    .keys()
408                    .collect::<Vec<_>>(),
409                vec!["Unique-ID"]
410            );
411            assert_eq!(h.unique_id(), Some("abc-123"));
412        }
413    }
414
415    #[test]
416    fn lookup_resolves_a_non_canonical_spelling() {
417        let mut h = EslHeaders::new();
418        h.insert("Unique-ID", "abc-123");
419        h.insert("Call-Info", "<sip:a@example.test>;purpose=icon");
420        assert_eq!(h.header_str("unique-id"), Some("abc-123"));
421        assert_eq!(h.header_str("UNIQUE-ID"), Some("abc-123"));
422        assert_eq!(
423            h.sip_header_str("call-info"),
424            Some("<sip:a@example.test>;purpose=icon")
425        );
426    }
427
428    #[test]
429    fn underscore_keys_keep_their_wire_casing() {
430        let mut h = EslHeaders::new();
431        h.insert("variable_sip_h_X-My-CUSTOM-Header", "val");
432        assert_eq!(
433            h.header_str("variable_sip_h_X-My-CUSTOM-Header"),
434            Some("val")
435        );
436        assert_eq!(h.header_str("variable_sip_h_x-my-custom-header"), None);
437    }
438
439    #[test]
440    fn two_spellings_of_one_header_are_one_entry() {
441        let mut h = EslHeaders::new();
442        h.insert("Channel-Read-Codec-Bit-Rate", "first");
443        h.insert("channel-read-codec-bit-rate", "second");
444        assert_eq!(h.len(), 1);
445        assert_eq!(h.header_str("Channel-Read-Codec-Bit-Rate"), Some("second"));
446    }
447
448    #[test]
449    fn remove_accepts_a_non_canonical_spelling() {
450        let mut h = EslHeaders::new();
451        h.insert("Unique-ID", "abc-123");
452        assert_eq!(h.remove("unique-id"), Some("abc-123".to_string()));
453        assert!(h.is_empty());
454        assert_eq!(h.header_str("Unique-ID"), None);
455        assert_eq!(h.remove("unique-id"), None);
456    }
457
458    #[test]
459    fn remove_by_canonical_key_drops_the_alias_too() {
460        let mut h = EslHeaders::new();
461        h.insert("unique-id", "abc-123");
462        assert_eq!(h.remove("Unique-ID"), Some("abc-123".to_string()));
463        assert_eq!(h.header_str("unique-id"), None);
464    }
465
466    #[cfg(feature = "serde")]
467    #[test]
468    fn deserialization_normalizes_like_the_wire_does() {
469        let json = r#"{"unique-id":"abc-123","variable_sip_call_id":"call-1"}"#;
470        let h: EslHeaders = serde_json::from_str(json).expect("map payload");
471        assert_eq!(h.unique_id(), Some("abc-123"));
472        assert_eq!(h.variable_str("sip_call_id"), Some("call-1"));
473        assert_eq!(
474            h.as_map()
475                .keys()
476                .collect::<Vec<_>>(),
477            vec!["Unique-ID", "variable_sip_call_id"]
478        );
479
480        let round_tripped: EslHeaders =
481            serde_json::from_str(&serde_json::to_string(&h).expect("serialize"))
482                .expect("round trip");
483        assert_eq!(round_tripped, h);
484    }
485
486    #[test]
487    fn array_encoding_decodes_for_every_multi_value_header() {
488        let mut h = EslHeaders::new();
489        h.insert(
490            "P-Asserted-Identity",
491            "ARRAY::<sip:alice@example.test>|:<tel:+15551234567>",
492        );
493        h.insert(
494            "Error-Info",
495            "ARRAY::<sip:busy@example.test>|:<http://example.test/why.html>",
496        );
497        h.insert(
498            "Contact",
499            "ARRAY::<sip:a@192.0.2.1>;expires=60|:<sip:b@192.0.2.2>",
500        );
501        h.insert(
502            "Via",
503            "ARRAY::SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bK1|:SIP/2.0/TCP 192.0.2.2;branch=z9hG4bK2",
504        );
505        h.insert(
506            "Warning",
507            "ARRAY::370 192.0.2.1 \"Insufficient bandwidth\"|:399 192.0.2.2 \"Misc, warning\"",
508        );
509        h.insert("Accept", "ARRAY::application/sdp|:text/plain;q=0.5");
510
511        assert_eq!(
512            h.via()
513                .expect("ARRAY value must decode")
514                .expect("header is present")
515                .len(),
516            2
517        );
518        assert_eq!(
519            h.warning()
520                .expect("ARRAY value must decode")
521                .expect("header is present")
522                .len(),
523            2
524        );
525        assert_eq!(
526            h.accept()
527                .expect("ARRAY value must decode")
528                .expect("header is present")
529                .len(),
530            2
531        );
532        assert_eq!(
533            h.p_asserted_identity()
534                .expect("ARRAY value must decode")
535                .len(),
536            2
537        );
538        assert_eq!(
539            h.error_info()
540                .expect("ARRAY value must decode")
541                .expect("header is present")
542                .entries()
543                .len(),
544            2
545        );
546        assert_eq!(
547            h.contact()
548                .expect("ARRAY value must decode")
549                .len(),
550            2
551        );
552    }
553
554    #[test]
555    fn a_single_valued_header_keeps_its_commas() {
556        let mut h = EslHeaders::new();
557        h.insert("Subject", "one, two");
558        assert_eq!(h.sip_header_all_str("Subject"), vec!["one, two"]);
559    }
560
561    /// RFC 3261 section 7.3.1 forbids comma-joining the auth headers, so the
562    /// commas between their parameters never separate rows.
563    #[test]
564    fn an_auth_header_keeps_its_parameter_commas() {
565        let digest = "Digest username=\"alice\", realm=\"example.com\", nonce=\"abc\"";
566        let mut h = EslHeaders::new();
567        h.insert("Authorization", digest);
568        h.insert("Proxy-Authorization", digest);
569        h.insert("WWW-Authenticate", digest);
570        h.insert("Proxy-Authenticate", digest);
571
572        for values in [
573            h.authorization(),
574            h.proxy_authorization(),
575            h.www_authenticate(),
576            h.proxy_authenticate(),
577        ] {
578            let values = values.expect("one Digest value parses");
579            assert_eq!(values.len(), 1);
580            assert_eq!(values[0].username(), Some("alice"));
581            assert_eq!(values[0].realm(), Some("example.com"));
582        }
583    }
584
585    #[test]
586    fn an_array_of_auth_headers_is_one_row_per_occurrence() {
587        let mut h = EslHeaders::new();
588        h.insert(
589            "Authorization",
590            "ARRAY::Digest username=\"a\", realm=\"r1\"|:Digest username=\"b\", realm=\"r2\"",
591        );
592        let values = h
593            .authorization()
594            .expect("both Digest values parse");
595        assert_eq!(values.len(), 2);
596        assert_eq!(values[1].realm(), Some("r2"));
597    }
598
599    #[test]
600    fn a_list_header_still_splits_on_commas() {
601        let mut h = EslHeaders::new();
602        h.insert("Call-Info", "<sip:a@example.com>, <sip:b@example.com>");
603        assert_eq!(
604            h.call_info()
605                .expect("both entries parse")
606                .expect("header is present")
607                .len(),
608            2
609        );
610    }
611
612    #[test]
613    fn header_str_passthrough() {
614        let mut h = EslHeaders::new();
615        h.insert("Unique-ID", "abc-123");
616        assert_eq!(h.header_str("Unique-ID"), Some("abc-123"));
617    }
618
619    #[test]
620    fn variable_str_prepends_variable_prefix() {
621        let mut h = EslHeaders::new();
622        h.insert("variable_sip_call_id", "call-1");
623        assert_eq!(h.variable_str("sip_call_id"), Some("call-1"));
624        assert_eq!(h.variable_str("missing"), None);
625    }
626
627    /// The stored-header entry point and the raw-value one must agree, so
628    /// every form runs through both.
629    #[test]
630    fn both_entry_points_decode_every_transport_form() {
631        let forms = [
632            (
633                "rfc",
634                "<sip:alice@example.test>;purpose=icon",
635                vec![Some("icon")],
636            ),
637            (
638                "array",
639                "ARRAY::<sip:a@example.test>;purpose=icon|:<sip:b@example.test>;purpose=info",
640                vec![Some("icon"), Some("info")],
641            ),
642            (
643                "bracketed",
644                "[<sip:alice@example.test>;purpose=icon]",
645                vec![Some("icon")],
646            ),
647            (
648                "bracketed array",
649                "[ARRAY::<sip:a@example.test>;purpose=icon|:<sip:b@example.test>]",
650                vec![Some("icon"), None],
651            ),
652        ];
653
654        for (label, value, purposes) in forms {
655            let mut h = EslHeaders::new();
656            h.insert("Call-Info", value);
657            let from_header = h
658                .call_info()
659                .unwrap_or_else(|e| panic!("{label}: {e}"))
660                .unwrap_or_else(|| panic!("{label}: header is present"));
661            let from_value = EslHeaders::parse_uri_info(value)
662                .unwrap_or_else(|e| panic!("{label} as a raw value: {e}"));
663            assert_eq!(from_header, from_value, "{label}");
664            assert_eq!(
665                from_header
666                    .entries()
667                    .iter()
668                    .map(|e| e.purpose())
669                    .collect::<Vec<_>>(),
670                purposes,
671                "{label}"
672            );
673        }
674    }
675
676    #[test]
677    fn call_info_absent_is_ok_none() {
678        let h = EslHeaders::new();
679        assert!(h
680            .call_info()
681            .unwrap()
682            .is_none());
683    }
684
685    #[test]
686    fn history_info_array_encoding() {
687        let mut h = EslHeaders::new();
688        h.insert(
689            "History-Info",
690            "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1",
691        );
692        let hi = h
693            .history_info()
694            .unwrap()
695            .expect("present");
696        assert_eq!(
697            hi.entries()
698                .len(),
699            2
700        );
701    }
702
703    #[test]
704    fn header_lookup_typed_accessors() {
705        let mut h = EslHeaders::new();
706        h.insert(EventHeader::UniqueId.as_str(), "uuid-1");
707        h.insert(EventHeader::ChannelName.as_str(), "sofia/a/b");
708        assert_eq!(h.unique_id(), Some("uuid-1"));
709        assert_eq!(h.channel_name(), Some("sofia/a/b"));
710    }
711
712    #[test]
713    fn parse_uri_info_empty_value() {
714        assert!(EslHeaders::parse_uri_info("").is_err());
715    }
716
717    #[test]
718    fn parse_uri_info_malformed_no_panic() {
719        // sip-header's UriInfo is lenient: an addr-spec needs no angle brackets.
720        let info = EslHeaders::parse_uri_info("sip:bare@example.test").expect("lenient parse");
721        assert_eq!(
722            info.entries()
723                .len(),
724            1
725        );
726    }
727
728    #[test]
729    fn parse_uri_info_structural_failure_is_malformed() {
730        let value = format!(
731            "ARRAY::{}",
732            vec!["<sip:a@example.com>"; crate::variables::MAX_ARRAY_ITEMS + 1].join("|:")
733        );
734        let err = EslHeaders::parse_uri_info(&value).expect_err("over-limit array must fail");
735        assert!(matches!(err, UriInfoError::Malformed(_)), "got {err:?}");
736    }
737
738    #[test]
739    fn parse_history_info_structural_failure_is_malformed() {
740        let value = format!(
741            "ARRAY::{}",
742            vec!["<sip:a@example.com>;index=1"; crate::variables::MAX_ARRAY_ITEMS + 1].join("|:")
743        );
744        let err = EslHeaders::parse_history_info(&value).expect_err("over-limit array must fail");
745        assert!(matches!(err, HistoryInfoError::Malformed(_)), "got {err:?}");
746    }
747
748    #[test]
749    fn collects_from_key_value_pairs() {
750        let h: EslHeaders = [
751            ("Unique-ID", "uuid-1"),
752            ("Caller-Destination-Number", "911"),
753        ]
754        .into_iter()
755        .collect();
756        assert_eq!(h.header_str("Unique-ID"), Some("uuid-1"));
757    }
758
759    #[test]
760    fn extend_adds_pairs() {
761        let mut h = EslHeaders::new();
762        h.insert("Unique-ID", "uuid-1");
763        h.extend([("Channel-Name", "sofia/a/b")]);
764        assert_eq!(h.len(), 2);
765    }
766
767    #[test]
768    fn split_entries_array_form() {
769        let entries =
770            EslHeaders::split_entries("ARRAY::<sip:a@example.test>|:<sip:b@example.test>")
771                .expect("ARRAY form");
772        assert_eq!(entries, ["<sip:a@example.test>", "<sip:b@example.test>"]);
773    }
774
775    #[test]
776    fn split_entries_rfc_comma_form() {
777        let entries = EslHeaders::split_entries(
778            "<sip:a@example.test>;text=\"one, two\",<sip:b@example.test>",
779        )
780        .expect("RFC form");
781        assert_eq!(entries.len(), 2);
782        assert_eq!(entries[0], "<sip:a@example.test>;text=\"one, two\"");
783    }
784
785    #[test]
786    fn split_entries_bracket_wrapped() {
787        let entries =
788            EslHeaders::split_entries("[<sip:a@example.test>;purpose=icon]").expect("bracket form");
789        assert_eq!(entries, ["<sip:a@example.test>;purpose=icon"]);
790    }
791
792    #[test]
793    fn split_entries_blank_value_yields_no_entries() {
794        let entries = EslHeaders::split_entries("  ").expect("blank value");
795        assert!(entries.is_empty());
796    }
797
798    #[test]
799    fn parse_history_info_blank_value_is_empty_error() {
800        let err = EslHeaders::parse_history_info("  ").expect_err("blank value must fail");
801        assert!(matches!(err, HistoryInfoError::Empty), "got {err:?}");
802    }
803
804    #[test]
805    fn split_entries_too_many_items_is_err() {
806        let value = format!(
807            "ARRAY::{}",
808            vec!["<sip:a@example.test>"; crate::variables::MAX_ARRAY_ITEMS + 1].join("|:")
809        );
810        let err = EslHeaders::split_entries(&value).expect_err("over-limit array must fail");
811        assert!(
812            matches!(err, EslArrayError::TooManyItems { .. }),
813            "got {err:?}"
814        );
815    }
816
817    #[test]
818    fn parse_history_info_array_form() {
819        let value = "ARRAY::<sip:a@example.com>;index=1|:<sip:b@example.com>;index=1.1";
820        let info = EslHeaders::parse_history_info(value).expect("parse ARRAY form");
821        assert_eq!(
822            info.entries()
823                .len(),
824            2
825        );
826    }
827}