Skip to main content

freeswitch_types/variables/
sip_passthrough.rs

1//! FreeSWITCH SIP header passthrough variables (`sip_h_*`, `sip_i_*`, etc.).
2//!
3//! FreeSWITCH exposes SIP headers as channel variables through six prefixes,
4//! each controlling a different direction or SIP method:
5//!
6//! | Prefix | Wire example | Purpose |
7//! |---|---|---|
8//! | `sip_i_` | `sip_i_call_info` | Read incoming INVITE headers |
9//! | `sip_h_` | `sip_h_Call-Info` | Inject header on outgoing request |
10//! | `sip_rh_` | `sip_rh_Call-Info` | Inject header on outgoing response |
11//! | `sip_ph_` | `sip_ph_Call-Info` | Inject header on provisional response |
12//! | `sip_bye_h_` | `sip_bye_h_Call-Info` | Inject header on BYE |
13//! | `sip_nobye_h_` | `sip_nobye_h_Call-Info` | Suppress header on BYE |
14//!
15//! The `Invite` prefix uses a different wire format from the others:
16//! lowercase with hyphens replaced by underscores (`sip_i_call_info`),
17//! while all other prefixes preserve the canonical SIP header casing
18//! (`sip_h_Call-Info`).
19
20use sip_header::SipHeader;
21
22/// Error returned when a raw header name contains invalid characters.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct InvalidHeaderName(String);
25
26impl std::fmt::Display for InvalidHeaderName {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            f,
30            "invalid SIP header name {:?}: contains \\n or \\r",
31            self.0
32        )
33    }
34}
35
36impl std::error::Error for InvalidHeaderName {}
37
38/// FreeSWITCH SIP header passthrough variable prefix.
39///
40/// Each prefix controls which SIP message direction the header variable
41/// applies to.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum SipHeaderPrefix {
45    /// `sip_i_` — raw INVITE header (read-only, set by `parse-all-invite-headers`).
46    Invite,
47    /// `sip_h_` — inject header on outgoing request (INVITE, REFER, etc.).
48    Request,
49    /// `sip_rh_` — inject header on outgoing response (200 OK, etc.).
50    Response,
51    /// `sip_ph_` — inject header on provisional response (180, 183).
52    Provisional,
53    /// `sip_bye_h_` — inject header on outgoing BYE.
54    Bye,
55    /// `sip_nobye_h_` — suppress a specific header on outgoing BYE.
56    NoBye,
57}
58
59impl SipHeaderPrefix {
60    /// Wire prefix string including trailing separator.
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            Self::Invite => "sip_i_",
64            Self::Request => "sip_h_",
65            Self::Response => "sip_rh_",
66            Self::Provisional => "sip_ph_",
67            Self::Bye => "sip_bye_h_",
68            Self::NoBye => "sip_nobye_h_",
69        }
70    }
71}
72
73impl std::fmt::Display for SipHeaderPrefix {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.write_str(self.as_str())
76    }
77}
78
79/// Error returned when parsing an unrecognized passthrough header variable name.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ParseSipPassthroughError(pub String);
82
83impl std::fmt::Display for ParseSipPassthroughError {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "not a SIP passthrough variable: {}", self.0)
86    }
87}
88
89impl std::error::Error for ParseSipPassthroughError {}
90
91/// A FreeSWITCH SIP passthrough header variable name.
92///
93/// Combines a [`SipHeaderPrefix`] (direction/method) with a SIP header name
94/// to produce the channel variable name used on the wire. Use with
95/// [`HeaderLookup::variable()`](crate::HeaderLookup::variable) for lookups,
96/// or insert into [`Variables`](crate::Variables) for originate commands.
97///
98/// # Typed constructors (known SIP headers)
99///
100/// ```
101/// use freeswitch_types::variables::{SipPassthroughHeader, SipHeaderPrefix};
102/// use sip_header::SipHeader;
103///
104/// let h = SipPassthroughHeader::request(SipHeader::CallInfo);
105/// assert_eq!(h.as_str(), "sip_h_Call-Info");
106///
107/// let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
108/// assert_eq!(h.as_str(), "sip_i_call_info");
109/// ```
110///
111/// # Raw constructors (custom headers)
112///
113/// ```
114/// use freeswitch_types::variables::SipPassthroughHeader;
115///
116/// let h = SipPassthroughHeader::request_raw("X-Tenant").unwrap();
117/// assert_eq!(h.as_str(), "sip_h_X-Tenant");
118/// ```
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub struct SipPassthroughHeader {
121    prefix: SipHeaderPrefix,
122    canonical_name: String,
123    wire: String,
124}
125
126/// Prefix patterns ordered longest-first for unambiguous `FromStr` matching.
127///
128/// Ordered so that longer prefixes (e.g. `sip_nobye_h_`) are tried before
129/// shorter ones that share a common start (e.g. `sip_bye_h_`). The order
130/// here is the single source of truth — `as_str()` provides the actual strings.
131const PREFIX_PATTERNS: &[SipHeaderPrefix] = &[
132    SipHeaderPrefix::NoBye,
133    SipHeaderPrefix::Bye,
134    SipHeaderPrefix::Provisional,
135    SipHeaderPrefix::Response,
136    SipHeaderPrefix::Invite,
137    SipHeaderPrefix::Request,
138];
139
140/// Derive the canonical SIP header name from a wire suffix under a given prefix.
141///
142/// For `Invite`, reverses the lowercase+underscore transformation:
143/// `"call_info"` → `"call-info"` → tried against `SipHeader::from_str`.
144/// For all other prefixes, the suffix is used verbatim or matched against
145/// known `SipHeader` variants for canonicalization.
146fn canonical_from_suffix(prefix: SipHeaderPrefix, suffix: &str) -> String {
147    match prefix {
148        SipHeaderPrefix::Invite => {
149            // Reverse the lowercase+underscore transformation:
150            // "call_info" → "call-info" → try SipHeader::from_str
151            let with_hyphens = suffix.replace('_', "-");
152            match with_hyphens.parse::<SipHeader>() {
153                Ok(h) => h
154                    .as_str()
155                    .to_string(),
156                Err(_) => with_hyphens,
157            }
158        }
159        _ => match suffix.parse::<SipHeader>() {
160            Ok(h) => h
161                .as_str()
162                .to_string(),
163            Err(_) => suffix.to_string(),
164        },
165    }
166}
167
168fn validate_header_name(name: &str) -> Result<(), InvalidHeaderName> {
169    if name.is_empty() || crate::wire_safety::contains_wire_terminator(name) {
170        return Err(InvalidHeaderName(name.to_string()));
171    }
172    Ok(())
173}
174
175fn build_wire(prefix: SipHeaderPrefix, canonical: &str) -> String {
176    match prefix {
177        SipHeaderPrefix::Invite => {
178            let mut wire = String::with_capacity(6 + canonical.len());
179            wire.push_str("sip_i_");
180            for ch in canonical.chars() {
181                if ch == '-' {
182                    wire.push('_');
183                } else {
184                    wire.push(ch.to_ascii_lowercase());
185                }
186            }
187            wire
188        }
189        _ => {
190            let pfx = prefix.as_str();
191            let mut wire = String::with_capacity(pfx.len() + canonical.len());
192            wire.push_str(pfx);
193            wire.push_str(canonical);
194            wire
195        }
196    }
197}
198
199impl SipPassthroughHeader {
200    /// Create from a prefix and a known [`SipHeader`].
201    pub fn new(prefix: SipHeaderPrefix, header: SipHeader) -> Self {
202        let canonical = header
203            .as_str()
204            .to_string();
205        let wire = build_wire(prefix, &canonical);
206        Self {
207            prefix,
208            canonical_name: canonical,
209            wire,
210        }
211    }
212
213    /// Create from a prefix and an arbitrary header name.
214    ///
215    /// Returns `Err` if the name contains `\n` or `\r` (wire injection risk).
216    pub fn new_raw(
217        prefix: SipHeaderPrefix,
218        name: impl Into<String>,
219    ) -> Result<Self, InvalidHeaderName> {
220        let canonical = name.into();
221        validate_header_name(&canonical)?;
222        let wire = build_wire(prefix, &canonical);
223        Ok(Self {
224            prefix,
225            canonical_name: canonical,
226            wire,
227        })
228    }
229
230    /// Incoming INVITE header (`sip_i_*`).
231    pub fn invite(header: SipHeader) -> Self {
232        Self::new(SipHeaderPrefix::Invite, header)
233    }
234
235    /// Incoming INVITE header from raw name.
236    pub fn invite_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
237        Self::new_raw(SipHeaderPrefix::Invite, name)
238    }
239
240    /// Outgoing request header (`sip_h_*`).
241    pub fn request(header: SipHeader) -> Self {
242        Self::new(SipHeaderPrefix::Request, header)
243    }
244
245    /// Outgoing request header from raw name.
246    pub fn request_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
247        Self::new_raw(SipHeaderPrefix::Request, name)
248    }
249
250    /// Outgoing response header (`sip_rh_*`).
251    pub fn response(header: SipHeader) -> Self {
252        Self::new(SipHeaderPrefix::Response, header)
253    }
254
255    /// Outgoing response header from raw name.
256    pub fn response_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
257        Self::new_raw(SipHeaderPrefix::Response, name)
258    }
259
260    /// Provisional response header (`sip_ph_*`).
261    pub fn provisional(header: SipHeader) -> Self {
262        Self::new(SipHeaderPrefix::Provisional, header)
263    }
264
265    /// Provisional response header from raw name.
266    pub fn provisional_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
267        Self::new_raw(SipHeaderPrefix::Provisional, name)
268    }
269
270    /// BYE request header (`sip_bye_h_*`).
271    pub fn bye(header: SipHeader) -> Self {
272        Self::new(SipHeaderPrefix::Bye, header)
273    }
274
275    /// BYE request header from raw name.
276    pub fn bye_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
277        Self::new_raw(SipHeaderPrefix::Bye, name)
278    }
279
280    /// Suppress header on BYE (`sip_nobye_h_*`).
281    pub fn no_bye(header: SipHeader) -> Self {
282        Self::new(SipHeaderPrefix::NoBye, header)
283    }
284
285    /// Suppress header on BYE from raw name.
286    pub fn no_bye_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
287        Self::new_raw(SipHeaderPrefix::NoBye, name)
288    }
289
290    /// The prefix (direction/method) of this variable.
291    pub fn prefix(&self) -> SipHeaderPrefix {
292        self.prefix
293    }
294
295    /// The canonical SIP header name (e.g. `"Call-Info"`, `"X-Tenant"`).
296    pub fn canonical_name(&self) -> &str {
297        &self.canonical_name
298    }
299
300    /// The pre-computed wire variable name (e.g. `"sip_h_Call-Info"`).
301    pub fn as_str(&self) -> &str {
302        &self.wire
303    }
304
305    /// Extract this header's values from a raw SIP message.
306    ///
307    /// Returns each occurrence as a separate entry (RFC 3261 §7.3.1).
308    /// Delegates to [`sip_header::extract_header()`] using the canonical name.
309    pub fn extract_from(&self, message: &str) -> Vec<String> {
310        sip_header::extract_header(message, &self.canonical_name)
311    }
312
313    /// Whether this header may contain ARRAY-encoded values when read from FreeSWITCH.
314    ///
315    /// Only meaningful for the `Invite` prefix — FreeSWITCH stores multi-valued
316    /// incoming SIP headers in `ARRAY::val1|:val2` format. For other prefixes
317    /// (which are write-only), this always returns `false`.
318    pub fn is_array_header(&self) -> bool {
319        if self.prefix != SipHeaderPrefix::Invite {
320            return false;
321        }
322        self.canonical_name
323            .parse::<SipHeader>()
324            .map(|h| h.is_multi_valued())
325            .unwrap_or(false)
326    }
327}
328
329impl super::VariableName for SipPassthroughHeader {
330    fn as_str(&self) -> &str {
331        &self.wire
332    }
333}
334
335impl std::fmt::Display for SipPassthroughHeader {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        f.write_str(&self.wire)
338    }
339}
340
341impl AsRef<str> for SipPassthroughHeader {
342    fn as_ref(&self) -> &str {
343        &self.wire
344    }
345}
346
347impl From<SipPassthroughHeader> for String {
348    fn from(h: SipPassthroughHeader) -> Self {
349        h.wire
350    }
351}
352
353impl std::str::FromStr for SipPassthroughHeader {
354    type Err = ParseSipPassthroughError;
355
356    fn from_str(s: &str) -> Result<Self, Self::Err> {
357        for &prefix in PREFIX_PATTERNS {
358            if let Some(suffix) = s.strip_prefix(prefix.as_str()) {
359                if suffix.is_empty() {
360                    return Err(ParseSipPassthroughError(s.to_string()));
361                }
362                let canonical = canonical_from_suffix(prefix, suffix);
363                return Ok(Self {
364                    prefix,
365                    canonical_name: canonical,
366                    wire: s.to_string(),
367                });
368            }
369        }
370
371        // Case-insensitive prefix matching for sip_i_ (FreeSWITCH may
372        // uppercase in some contexts)
373        let lower = s.to_ascii_lowercase();
374        if lower != s {
375            for &prefix in PREFIX_PATTERNS {
376                if let Some(suffix) = lower.strip_prefix(prefix.as_str()) {
377                    if suffix.is_empty() {
378                        return Err(ParseSipPassthroughError(s.to_string()));
379                    }
380                    let canonical = canonical_from_suffix(prefix, suffix);
381                    let wire = build_wire(prefix, &canonical);
382                    return Ok(Self {
383                        prefix,
384                        canonical_name: canonical,
385                        wire,
386                    });
387                }
388            }
389        }
390
391        Err(ParseSipPassthroughError(s.to_string()))
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    // --- Display / wire format ---
400
401    #[test]
402    fn invite_typed_wire_format() {
403        assert_eq!(
404            SipPassthroughHeader::invite(SipHeader::CallInfo).as_str(),
405            "sip_i_call_info"
406        );
407        assert_eq!(
408            SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).as_str(),
409            "sip_i_p_asserted_identity"
410        );
411        assert_eq!(
412            SipPassthroughHeader::invite(SipHeader::Via).as_str(),
413            "sip_i_via"
414        );
415        assert_eq!(
416            SipPassthroughHeader::invite(SipHeader::CallId).as_str(),
417            "sip_i_call_id"
418        );
419    }
420
421    #[test]
422    fn request_typed_wire_format() {
423        assert_eq!(
424            SipPassthroughHeader::request(SipHeader::CallInfo).as_str(),
425            "sip_h_Call-Info"
426        );
427        assert_eq!(
428            SipPassthroughHeader::request(SipHeader::PAssertedIdentity).as_str(),
429            "sip_h_P-Asserted-Identity"
430        );
431    }
432
433    #[test]
434    fn response_typed_wire_format() {
435        assert_eq!(
436            SipPassthroughHeader::response(SipHeader::CallInfo).as_str(),
437            "sip_rh_Call-Info"
438        );
439    }
440
441    #[test]
442    fn provisional_typed_wire_format() {
443        assert_eq!(
444            SipPassthroughHeader::provisional(SipHeader::AlertInfo).as_str(),
445            "sip_ph_Alert-Info"
446        );
447    }
448
449    #[test]
450    fn bye_typed_wire_format() {
451        assert_eq!(
452            SipPassthroughHeader::bye(SipHeader::Reason).as_str(),
453            "sip_bye_h_Reason"
454        );
455    }
456
457    #[test]
458    fn no_bye_typed_wire_format() {
459        assert_eq!(
460            SipPassthroughHeader::no_bye(SipHeader::Reason).as_str(),
461            "sip_nobye_h_Reason"
462        );
463    }
464
465    #[test]
466    fn raw_custom_header() {
467        assert_eq!(
468            SipPassthroughHeader::request_raw("X-Tenant")
469                .unwrap()
470                .as_str(),
471            "sip_h_X-Tenant"
472        );
473        assert_eq!(
474            SipPassthroughHeader::invite_raw("X-Custom")
475                .unwrap()
476                .as_str(),
477            "sip_i_x_custom"
478        );
479    }
480
481    #[test]
482    fn raw_rejects_newlines() {
483        assert!(SipPassthroughHeader::request_raw("X-Bad\nHeader").is_err());
484        assert!(SipPassthroughHeader::request_raw("X-Bad\rHeader").is_err());
485        assert!(SipPassthroughHeader::request_raw("").is_err());
486    }
487
488    // --- Display trait ---
489
490    #[test]
491    fn display_matches_as_str() {
492        let h = SipPassthroughHeader::request(SipHeader::CallInfo);
493        assert_eq!(h.to_string(), h.as_str());
494    }
495
496    // --- FromStr round-trips ---
497
498    #[test]
499    fn from_str_request() {
500        let parsed: SipPassthroughHeader = "sip_h_Call-Info"
501            .parse()
502            .unwrap();
503        assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
504        assert_eq!(parsed.canonical_name(), "Call-Info");
505        assert_eq!(parsed.as_str(), "sip_h_Call-Info");
506    }
507
508    #[test]
509    fn from_str_invite() {
510        let parsed: SipPassthroughHeader = "sip_i_call_info"
511            .parse()
512            .unwrap();
513        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
514        assert_eq!(parsed.canonical_name(), "Call-Info");
515        assert_eq!(parsed.as_str(), "sip_i_call_info");
516    }
517
518    #[test]
519    fn from_str_invite_p_asserted_identity() {
520        let parsed: SipPassthroughHeader = "sip_i_p_asserted_identity"
521            .parse()
522            .unwrap();
523        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
524        assert_eq!(parsed.canonical_name(), "P-Asserted-Identity");
525    }
526
527    #[test]
528    fn from_str_response() {
529        let parsed: SipPassthroughHeader = "sip_rh_Call-Info"
530            .parse()
531            .unwrap();
532        assert_eq!(parsed.prefix(), SipHeaderPrefix::Response);
533        assert_eq!(parsed.canonical_name(), "Call-Info");
534    }
535
536    #[test]
537    fn from_str_bye() {
538        let parsed: SipPassthroughHeader = "sip_bye_h_Reason"
539            .parse()
540            .unwrap();
541        assert_eq!(parsed.prefix(), SipHeaderPrefix::Bye);
542        assert_eq!(parsed.canonical_name(), "Reason");
543    }
544
545    #[test]
546    fn from_str_no_bye() {
547        let parsed: SipPassthroughHeader = "sip_nobye_h_Reason"
548            .parse()
549            .unwrap();
550        assert_eq!(parsed.prefix(), SipHeaderPrefix::NoBye);
551        assert_eq!(parsed.canonical_name(), "Reason");
552    }
553
554    #[test]
555    fn from_str_unknown_custom_header() {
556        let parsed: SipPassthroughHeader = "sip_h_X-Tenant"
557            .parse()
558            .unwrap();
559        assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
560        assert_eq!(parsed.canonical_name(), "X-Tenant");
561    }
562
563    #[test]
564    fn from_str_invite_unknown_custom() {
565        let parsed: SipPassthroughHeader = "sip_i_x_custom"
566            .parse()
567            .unwrap();
568        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
569        // Unknown header: canonical is the hyphenated form
570        assert_eq!(parsed.canonical_name(), "x-custom");
571    }
572
573    #[test]
574    fn from_str_case_insensitive_invite() {
575        let parsed: SipPassthroughHeader = "SIP_I_CALL_INFO"
576            .parse()
577            .unwrap();
578        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
579        assert_eq!(parsed.canonical_name(), "Call-Info");
580    }
581
582    #[test]
583    fn from_str_rejects_no_prefix() {
584        assert!("call_info"
585            .parse::<SipPassthroughHeader>()
586            .is_err());
587        assert!("sip_call_info"
588            .parse::<SipPassthroughHeader>()
589            .is_err());
590    }
591
592    #[test]
593    fn from_str_rejects_empty_suffix() {
594        assert!("sip_h_"
595            .parse::<SipPassthroughHeader>()
596            .is_err());
597        assert!("sip_i_"
598            .parse::<SipPassthroughHeader>()
599            .is_err());
600    }
601
602    #[test]
603    fn from_str_round_trip_all_prefixes() {
604        let headers = [
605            SipPassthroughHeader::invite(SipHeader::CallInfo),
606            SipPassthroughHeader::request(SipHeader::CallInfo),
607            SipPassthroughHeader::response(SipHeader::CallInfo),
608            SipPassthroughHeader::provisional(SipHeader::AlertInfo),
609            SipPassthroughHeader::bye(SipHeader::Reason),
610            SipPassthroughHeader::no_bye(SipHeader::Reason),
611        ];
612        for h in &headers {
613            let parsed: SipPassthroughHeader = h
614                .as_str()
615                .parse()
616                .unwrap();
617            assert_eq!(&parsed, h, "round-trip failed for {}", h.as_str());
618        }
619    }
620
621    // --- Accessors ---
622
623    #[test]
624    fn prefix_accessor() {
625        assert_eq!(
626            SipPassthroughHeader::invite(SipHeader::Via).prefix(),
627            SipHeaderPrefix::Invite
628        );
629        assert_eq!(
630            SipPassthroughHeader::request(SipHeader::Via).prefix(),
631            SipHeaderPrefix::Request
632        );
633    }
634
635    #[test]
636    fn canonical_name_accessor() {
637        assert_eq!(
638            SipPassthroughHeader::invite(SipHeader::CallInfo).canonical_name(),
639            "Call-Info"
640        );
641        assert_eq!(
642            SipPassthroughHeader::request_raw("X-Tenant")
643                .unwrap()
644                .canonical_name(),
645            "X-Tenant"
646        );
647    }
648
649    // --- is_array_header ---
650
651    #[test]
652    fn is_array_header_invite_multi_valued() {
653        assert!(SipPassthroughHeader::invite(SipHeader::Via).is_array_header());
654        assert!(SipPassthroughHeader::invite(SipHeader::CallInfo).is_array_header());
655        assert!(SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).is_array_header());
656        assert!(SipPassthroughHeader::invite(SipHeader::RecordRoute).is_array_header());
657    }
658
659    #[test]
660    fn is_array_header_invite_single_valued() {
661        assert!(!SipPassthroughHeader::invite(SipHeader::From).is_array_header());
662        assert!(!SipPassthroughHeader::invite(SipHeader::CallId).is_array_header());
663        assert!(!SipPassthroughHeader::invite(SipHeader::ContentType).is_array_header());
664    }
665
666    #[test]
667    fn is_array_header_non_invite_always_false() {
668        assert!(!SipPassthroughHeader::request(SipHeader::Via).is_array_header());
669        assert!(!SipPassthroughHeader::response(SipHeader::CallInfo).is_array_header());
670    }
671
672    #[test]
673    fn is_array_header_raw_unknown() {
674        assert!(!SipPassthroughHeader::invite_raw("X-Custom")
675            .unwrap()
676            .is_array_header());
677    }
678
679    // --- extract_from ---
680
681    #[test]
682    fn extract_from_sip_message() {
683        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
684                   Call-Info: <sip:example.com>;answer-after=0\r\n\
685                   \r\n";
686        let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
687        assert_eq!(
688            h.extract_from(msg),
689            vec!["<sip:example.com>;answer-after=0"]
690        );
691    }
692
693    #[test]
694    fn extract_from_missing() {
695        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
696                   From: Alice <sip:alice@example.com>\r\n\
697                   \r\n";
698        let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
699        assert!(h
700            .extract_from(msg)
701            .is_empty());
702    }
703
704    // --- PREFIX_PATTERNS completeness ---
705
706    #[test]
707    fn prefix_patterns_covers_all_variants_exactly_once() {
708        let all_variants = [
709            SipHeaderPrefix::Invite,
710            SipHeaderPrefix::Request,
711            SipHeaderPrefix::Response,
712            SipHeaderPrefix::Provisional,
713            SipHeaderPrefix::Bye,
714            SipHeaderPrefix::NoBye,
715        ];
716        for &variant in &all_variants {
717            let count = PREFIX_PATTERNS
718                .iter()
719                .filter(|&&p| p == variant)
720                .count();
721            assert_eq!(
722                count, 1,
723                "{:?} appears {count} times in PREFIX_PATTERNS (expected exactly 1)",
724                variant
725            );
726        }
727        assert_eq!(
728            PREFIX_PATTERNS.len(),
729            all_variants.len(),
730            "PREFIX_PATTERNS length differs from all-variants count"
731        );
732    }
733
734    // --- VariableName trait ---
735
736    #[test]
737    fn variable_name_trait() {
738        use crate::variables::VariableName;
739        let h = SipPassthroughHeader::request(SipHeader::CallInfo);
740        let name: &str = VariableName::as_str(&h);
741        assert_eq!(name, "sip_h_Call-Info");
742    }
743}