Skip to main content

freeswitch_types/
headers.rs

1//! Typed event header names for FreeSWITCH ESL events.
2
3sip_header::define_header_enum! {
4    tests_mod: event_header_generated_tests,
5    error_type: ParseEventHeaderError => "unknown event header",
6    /// Top-level header names that appear in FreeSWITCH ESL events.
7    ///
8    /// These are the headers on the parsed event itself (not protocol framing
9    /// headers like `Content-Type`). Use with [`EslEvent::header()`](crate::EslEvent::header) for
10    /// type-safe lookups.
11    pub enum EventHeader {
12        EventName => "Event-Name",
13        EventSubclass => "Event-Subclass",
14        UniqueId => "Unique-ID",
15        CallerUniqueId => "Caller-Unique-ID",
16        OtherLegUniqueId => "Other-Leg-Unique-ID",
17        ChannelCallUuid => "Channel-Call-UUID",
18        JobUuid => "Job-UUID",
19        ChannelName => "Channel-Name",
20        ChannelState => "Channel-State",
21        ChannelStateNumber => "Channel-State-Number",
22        ChannelCallState => "Channel-Call-State",
23        AnswerState => "Answer-State",
24        CallDirection => "Call-Direction",
25        HangupCause => "Hangup-Cause",
26        CallerCallerIdName => "Caller-Caller-ID-Name",
27        CallerCallerIdNumber => "Caller-Caller-ID-Number",
28        CallerOrigCallerIdName => "Caller-Orig-Caller-ID-Name",
29        CallerOrigCallerIdNumber => "Caller-Orig-Caller-ID-Number",
30        CallerCalleeIdName => "Caller-Callee-ID-Name",
31        CallerCalleeIdNumber => "Caller-Callee-ID-Number",
32        CallerDestinationNumber => "Caller-Destination-Number",
33        CallerContext => "Caller-Context",
34        CallerDirection => "Caller-Direction",
35        CallerNetworkAddr => "Caller-Network-Addr",
36        CoreUuid => "Core-UUID",
37        DtmfDigit => "DTMF-Digit",
38        Priority => "priority",
39        LogLevel => "Log-Level",
40        /// SIP NOTIFY body content (JSON payload from `NOTIFY_IN` events).
41        PlData => "pl_data",
42        /// SIP event package name from `NOTIFY_IN` events (e.g. `emergency-AbandonedCall`).
43        SipEvent => "event",
44        /// SIP content type from `NOTIFY_IN` events.
45        SipContentType => "sip_content_type",
46        /// Gateway that received the SIP NOTIFY.
47        GatewayName => "gateway_name",
48
49        // --- Codec (from switch_channel_event_set_data / switch_core_codec.c) ---
50        // Audio read
51        ChannelReadCodecName => "Channel-Read-Codec-Name",
52        ChannelReadCodecRate => "Channel-Read-Codec-Rate",
53        ChannelReadCodecBitRate => "Channel-Read-Codec-Bit-Rate",
54        /// Only present when actual_samples_per_second != samples_per_second.
55        ChannelReportedReadCodecRate => "Channel-Reported-Read-Codec-Rate",
56        // Audio write
57        ChannelWriteCodecName => "Channel-Write-Codec-Name",
58        ChannelWriteCodecRate => "Channel-Write-Codec-Rate",
59        ChannelWriteCodecBitRate => "Channel-Write-Codec-Bit-Rate",
60        /// Only present when actual_samples_per_second != samples_per_second.
61        ChannelReportedWriteCodecRate => "Channel-Reported-Write-Codec-Rate",
62        // Video read/write
63        ChannelVideoReadCodecName => "Channel-Video-Read-Codec-Name",
64        ChannelVideoReadCodecRate => "Channel-Video-Read-Codec-Rate",
65        ChannelVideoWriteCodecName => "Channel-Video-Write-Codec-Name",
66        ChannelVideoWriteCodecRate => "Channel-Video-Write-Codec-Rate",
67        /// Active session count from `HEARTBEAT` events.
68        SessionCount => "Session-Count",
69        FreeswitchHostname => "FreeSWITCH-Hostname",
70        FreeswitchSwitchname => "FreeSWITCH-Switchname",
71        FreeswitchIpv4 => "FreeSWITCH-IPv4",
72        FreeswitchIpv6 => "FreeSWITCH-IPv6",
73        FreeswitchVersion => "FreeSWITCH-Version",
74        FreeswitchDomain => "FreeSWITCH-Domain",
75        FreeswitchUser => "FreeSWITCH-User",
76
77        // --- Application (from switch_core_session.c) ---
78        Application => "Application",
79        ApplicationData => "Application-Data",
80        ApplicationResponse => "Application-Response",
81        ApplicationUuid => "Application-UUID",
82
83        // --- Event metadata (from switch_event_prep_for_delivery_detailed) ---
84        EventDateLocal => "Event-Date-Local",
85        EventDateGmt => "Event-Date-GMT",
86        EventDateTimestamp => "Event-Date-Timestamp",
87        EventCallingFile => "Event-Calling-File",
88        EventCallingFunction => "Event-Calling-Function",
89        EventCallingLineNumber => "Event-Calling-Line-Number",
90        EventSequence => "Event-Sequence",
91
92        // --- Channel basic data (from switch_channel_event_set_basic_data) ---
93        ChannelPresenceId => "Channel-Presence-ID",
94        ChannelPresenceData => "Channel-Presence-Data",
95        PresenceDataCols => "Presence-Data-Cols",
96        PresenceCallDirection => "Presence-Call-Direction",
97        ChannelHitDialplan => "Channel-HIT-Dialplan",
98        SessionExternalId => "Session-External-ID",
99        /// `originator` or `originatee` on bridged channel events.
100        OtherType => "Other-Type",
101
102        // --- Callstate change (from switch_channel_perform_set_callstate) ---
103        ChannelCallStateNumber => "Channel-Call-State-Number",
104        OriginalChannelCallState => "Original-Channel-Call-State",
105
106        // --- DTMF (from switch_channel_dequeue_dtmf) ---
107        DtmfDuration => "DTMF-Duration",
108        DtmfSource => "DTMF-Source",
109
110        // --- Caller profile (from switch_caller_profile_event_set_data, "Caller-" prefix) ---
111        CallerLogicalDirection => "Caller-Logical-Direction",
112        CallerUsername => "Caller-Username",
113        CallerDialplan => "Caller-Dialplan",
114        CallerAni => "Caller-ANI",
115        CallerAniii => "Caller-ANI-II",
116        CallerSource => "Caller-Source",
117        CallerTransferSource => "Caller-Transfer-Source",
118        CallerRdnis => "Caller-RDNIS",
119        CallerChannelName => "Caller-Channel-Name",
120        CallerProfileIndex => "Caller-Profile-Index",
121        CallerScreenBit => "Caller-Screen-Bit",
122        CallerPrivacyHideName => "Caller-Privacy-Hide-Name",
123        CallerPrivacyHideNumber => "Caller-Privacy-Hide-Number",
124
125        // --- Other-leg profile (from switch_caller_profile_event_set_data, "Other-Leg" prefix) ---
126        OtherLegDirection => "Other-Leg-Direction",
127        OtherLegLogicalDirection => "Other-Leg-Logical-Direction",
128        OtherLegUsername => "Other-Leg-Username",
129        OtherLegDialplan => "Other-Leg-Dialplan",
130        OtherLegCallerIdName => "Other-Leg-Caller-ID-Name",
131        OtherLegCallerIdNumber => "Other-Leg-Caller-ID-Number",
132        OtherLegOrigCallerIdName => "Other-Leg-Orig-Caller-ID-Name",
133        OtherLegOrigCallerIdNumber => "Other-Leg-Orig-Caller-ID-Number",
134        OtherLegCalleeIdName => "Other-Leg-Callee-ID-Name",
135        OtherLegCalleeIdNumber => "Other-Leg-Callee-ID-Number",
136        OtherLegNetworkAddr => "Other-Leg-Network-Addr",
137        OtherLegAni => "Other-Leg-ANI",
138        OtherLegAniii => "Other-Leg-ANI-II",
139        OtherLegDestinationNumber => "Other-Leg-Destination-Number",
140        OtherLegSource => "Other-Leg-Source",
141        OtherLegTransferSource => "Other-Leg-Transfer-Source",
142        OtherLegContext => "Other-Leg-Context",
143        OtherLegRdnis => "Other-Leg-RDNIS",
144        OtherLegChannelName => "Other-Leg-Channel-Name",
145        OtherLegProfileIndex => "Other-Leg-Profile-Index",
146        OtherLegScreenBit => "Other-Leg-Screen-Bit",
147        OtherLegPrivacyHideName => "Other-Leg-Privacy-Hide-Name",
148        OtherLegPrivacyHideNumber => "Other-Leg-Privacy-Hide-Number",
149
150        // --- Heartbeat (from send_heartbeat in switch_core.c) ---
151        /// Seconds since FreeSWITCH startup.
152        UpTime => "Up-Time",
153        /// Milliseconds since FreeSWITCH startup.
154        UptimeMsec => "Uptime-msec",
155        MaxSessions => "Max-Sessions",
156        SessionPeakMax => "Session-Peak-Max",
157        SessionPeakFiveMin => "Session-Peak-FiveMin",
158        SessionPerSec => "Session-Per-Sec",
159        SessionPerSecFiveMin => "Session-Per-Sec-FiveMin",
160        SessionPerSecMax => "Session-Per-Sec-Max",
161        SessionPerSecLast => "Session-Per-Sec-Last",
162        SessionSinceStartup => "Session-Since-Startup",
163        IdleCpu => "Idle-CPU",
164        HeartbeatInterval => "Heartbeat-Interval",
165        EventInfo => "Event-Info",
166
167        // --- Log (from switch_log_meta_vprintf in switch_log.c) ---
168        LogData => "Log-Data",
169        LogFile => "Log-File",
170        LogFunction => "Log-Function",
171        LogLine => "Log-Line",
172        UserData => "User-Data",
173
174        // --- Application (from switch_core_session_exec in switch_core_session.c) ---
175        ApplicationUuidName => "Application-UUID-Name",
176
177        // --- Sofia event headers (from mod_sofia CUSTOM events) ---
178        Gateway => "Gateway",
179        State => "State",
180        PingStatus => "Ping-Status",
181        Phrase => "Phrase",
182        /// Sofia profile name, hyphen spelling. Carried by the registration,
183        /// gateway and user-state `CUSTOM` subclasses
184        /// ([`SofiaEventSubclass::REGISTRATION_EVENTS`](crate::sofia::SofiaEventSubclass::REGISTRATION_EVENTS),
185        /// [`GATEWAY_EVENTS`](crate::sofia::SofiaEventSubclass::GATEWAY_EVENTS),
186        /// [`SipUserState`](crate::sofia::SofiaEventSubclass::SipUserState)).
187        /// The underscore spelling is [`EventHeader::ProfileNameSnake`].
188        ProfileName => "profile-name",
189        /// SIP response code (integer) from gateway_state and sip_user_state events.
190        Status => "Status",
191
192        // --- Sofia snake_case headers (from mod_sofia's SIP-method-mirror core events) ---
193        /// Emitting module, always the literal `mod_sofia`.
194        ModuleName => "module_name",
195        /// Sofia profile name, underscore spelling. Carried by mod_sofia's
196        /// SIP-method-mirror core events (`NOTIFY_IN`, `FAILURE`, `PUBLISH`,
197        /// `UNPUBLISH`, `PHONE_FEATURE_SUBSCRIBE`) and by
198        /// [`SofiaEventSubclass::ProfileStart`](crate::sofia::SofiaEventSubclass::ProfileStart).
199        /// The hyphen spelling is [`EventHeader::ProfileName`].
200        ProfileNameSnake => "profile_name",
201        /// Bind URL of the Sofia profile named by
202        /// [`ProfileNameSnake`](EventHeader::ProfileNameSnake).
203        /// Absent on `FAILURE` when no profile resolved.
204        ProfileUri => "profile_uri",
205    }
206}
207
208/// Return a lowercase case-alias for `key` when case-insensitive aliasing applies.
209///
210/// The aliasing rule: keys containing `_` (channel variables such as
211/// `variable_*`, and SIP passthrough headers such as `sip_h_*` / `sip_i_*`)
212/// are excluded from case-insensitive aliasing because their suffix encodes
213/// original SIP wire casing that must be preserved verbatim. All other keys
214/// (dash-separated event and framing headers) may be looked up case-insensitively
215/// via their lowercase form.
216///
217/// Returns `Some(key.to_ascii_lowercase())` when aliasing applies, `None` for
218/// underscore-containing keys.
219pub fn case_alias_key(key: &str) -> Option<String> {
220    if key.contains('_') {
221        None
222    } else {
223        Some(key.to_ascii_lowercase())
224    }
225}
226
227/// Normalize a header key to its canonical form for case-insensitive storage.
228///
229/// FreeSWITCH's C ESL uses case-insensitive header lookups (`strcasecmp`), but
230/// stores header names verbatim. Multiple C code paths emit the same logical
231/// header with different casing (e.g. `switch_channel.c` sends `Unique-ID`
232/// while `switch_event.c` sends `unique-id`). This function normalizes keys
233/// so that both resolve to the same `HashMap` entry.
234///
235/// **Strategy:**
236/// 1. Known [`EventHeader`] variants are matched first (case-insensitive) and
237///    returned in their canonical wire form (e.g. `unique-id` → `Unique-ID`).
238/// 2. Keys containing underscores pass through verbatim (see [`case_alias_key`]).
239/// 3. Unknown dash-separated keys are Title-Cased to match FreeSWITCH's
240///    dominant convention for event and framing headers.
241pub fn normalize_header_key(raw: &str) -> String {
242    if let Ok(eh) = raw.parse::<EventHeader>() {
243        return eh
244            .as_str()
245            .to_string();
246    }
247    if case_alias_key(raw).is_none() {
248        raw.to_string()
249    } else {
250        title_case_dashes(raw)
251    }
252}
253
254fn title_case_dashes(s: &str) -> String {
255    let mut result = String::with_capacity(s.len());
256    let mut capitalize_next = true;
257    for c in s.chars() {
258        if c == '-' {
259            result.push('-');
260            capitalize_next = true;
261        } else if capitalize_next {
262            result.push(c.to_ascii_uppercase());
263            capitalize_next = false;
264        } else {
265            result.push(c.to_ascii_lowercase());
266        }
267    }
268    result
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn display_round_trip() {
277        assert_eq!(EventHeader::UniqueId.to_string(), "Unique-ID");
278        assert_eq!(
279            EventHeader::ChannelCallState.to_string(),
280            "Channel-Call-State"
281        );
282        assert_eq!(
283            EventHeader::CallerCallerIdName.to_string(),
284            "Caller-Caller-ID-Name"
285        );
286        assert_eq!(EventHeader::Priority.to_string(), "priority");
287    }
288
289    #[test]
290    fn as_ref_str() {
291        let h: &str = EventHeader::UniqueId.as_ref();
292        assert_eq!(h, "Unique-ID");
293    }
294
295    #[test]
296    fn from_str_case_insensitive() {
297        assert_eq!(
298            "unique-id".parse::<EventHeader>(),
299            Ok(EventHeader::UniqueId)
300        );
301        assert_eq!(
302            "UNIQUE-ID".parse::<EventHeader>(),
303            Ok(EventHeader::UniqueId)
304        );
305        assert_eq!(
306            "Unique-ID".parse::<EventHeader>(),
307            Ok(EventHeader::UniqueId)
308        );
309        assert_eq!(
310            "channel-call-state".parse::<EventHeader>(),
311            Ok(EventHeader::ChannelCallState)
312        );
313    }
314
315    #[test]
316    fn from_str_unknown() {
317        let err = "X-Custom-Not-In-Enum".parse::<EventHeader>();
318        assert!(err.is_err());
319        assert_eq!(
320            err.unwrap_err()
321                .to_string(),
322            "unknown event header (20 bytes)"
323        );
324    }
325
326    // --- normalize_header_key tests ---
327    // FreeSWITCH C ESL uses strcasecmp for header lookups but stores names
328    // verbatim. Multiple C code paths emit the same logical header with
329    // different casing (switch_channel.c Title-Case vs switch_event.c lowercase
330    // vs switch_core_codec.c mixed). normalize_header_key canonicalizes keys
331    // so they collapse to a single HashMap entry.
332
333    #[test]
334    fn normalize_known_enum_variants_return_canonical_form() {
335        // EventHeader::from_str is case-insensitive; canonical as_str() is returned
336        assert_eq!(normalize_header_key("unique-id"), "Unique-ID");
337        assert_eq!(normalize_header_key("UNIQUE-ID"), "Unique-ID");
338        assert_eq!(normalize_header_key("Unique-ID"), "Unique-ID");
339        assert_eq!(normalize_header_key("dtmf-digit"), "DTMF-Digit");
340        assert_eq!(normalize_header_key("DTMF-DIGIT"), "DTMF-Digit");
341        assert_eq!(
342            normalize_header_key("channel-call-uuid"),
343            "Channel-Call-UUID"
344        );
345        assert_eq!(normalize_header_key("event-name"), "Event-Name");
346    }
347
348    #[test]
349    fn normalize_known_underscore_variants_return_canonical_form() {
350        // Headers whose canonical form contains underscores
351        assert_eq!(normalize_header_key("priority"), "priority");
352        assert_eq!(normalize_header_key("PRIORITY"), "priority");
353        assert_eq!(normalize_header_key("pl_data"), "pl_data");
354        assert_eq!(normalize_header_key("PL_DATA"), "pl_data");
355        assert_eq!(normalize_header_key("sip_content_type"), "sip_content_type");
356        assert_eq!(normalize_header_key("gateway_name"), "gateway_name");
357        assert_eq!(normalize_header_key("event"), "event");
358        assert_eq!(normalize_header_key("EVENT"), "event");
359    }
360
361    #[test]
362    fn normalize_codec_headers_from_switch_core_codec() {
363        // switch_core_codec.c sends lowercase, switch_channel_event_set_data sends Title-Case
364        // Both must normalize to the canonical EventHeader form
365        assert_eq!(
366            normalize_header_key("channel-read-codec-bit-rate"),
367            "Channel-Read-Codec-Bit-Rate"
368        );
369        assert_eq!(
370            normalize_header_key("Channel-Read-Codec-Bit-Rate"),
371            "Channel-Read-Codec-Bit-Rate"
372        );
373        // switch_core_codec.c mixed case for write: "Channel-Write-codec-bit-rate"
374        assert_eq!(
375            normalize_header_key("Channel-Write-codec-bit-rate"),
376            "Channel-Write-Codec-Bit-Rate"
377        );
378        assert_eq!(
379            normalize_header_key("channel-video-read-codec-name"),
380            "Channel-Video-Read-Codec-Name"
381        );
382    }
383
384    #[test]
385    fn normalize_unknown_underscore_keys_passthrough() {
386        // Channel variables and sip_h_* passthrough preserve original casing
387        assert_eq!(
388            normalize_header_key("variable_sip_call_id"),
389            "variable_sip_call_id"
390        );
391        assert_eq!(
392            normalize_header_key("variable_sip_h_X-My-CUSTOM-Header"),
393            "variable_sip_h_X-My-CUSTOM-Header"
394        );
395        assert_eq!(
396            normalize_header_key("variable_sip_h_Diversion"),
397            "variable_sip_h_Diversion"
398        );
399    }
400
401    #[test]
402    fn normalize_unknown_dash_keys_title_case() {
403        // Framing and unknown event headers get Title-Cased
404        assert_eq!(normalize_header_key("content-type"), "Content-Type");
405        assert_eq!(normalize_header_key("Content-Type"), "Content-Type");
406        assert_eq!(normalize_header_key("CONTENT-TYPE"), "Content-Type");
407        assert_eq!(normalize_header_key("x-custom-header"), "X-Custom-Header");
408        assert_eq!(
409            normalize_header_key("Content-Disposition"),
410            "Content-Disposition"
411        );
412        assert_eq!(normalize_header_key("reply-text"), "Reply-Text");
413    }
414
415    #[test]
416    fn normalize_idempotent_for_all_enum_variants() {
417        // Normalizing an already-canonical wire string must return it unchanged
418        let variants = [
419            EventHeader::EventName,
420            EventHeader::UniqueId,
421            EventHeader::ChannelCallUuid,
422            EventHeader::DtmfDigit,
423            EventHeader::Priority,
424            EventHeader::PlData,
425            EventHeader::SipEvent,
426            EventHeader::GatewayName,
427            EventHeader::SipContentType,
428            EventHeader::ChannelReadCodecBitRate,
429            EventHeader::ChannelVideoWriteCodecRate,
430            EventHeader::LogLevel,
431        ];
432        for v in variants {
433            let canonical = v.as_str();
434            assert_eq!(
435                normalize_header_key(canonical),
436                canonical,
437                "normalization not idempotent for {canonical}"
438            );
439        }
440    }
441
442    #[test]
443    fn parse_sofia_event_headers() {
444        assert_eq!("Gateway".parse::<EventHeader>(), Ok(EventHeader::Gateway));
445        assert_eq!("State".parse::<EventHeader>(), Ok(EventHeader::State));
446        assert_eq!(
447            "Ping-Status".parse::<EventHeader>(),
448            Ok(EventHeader::PingStatus)
449        );
450        assert_eq!("Phrase".parse::<EventHeader>(), Ok(EventHeader::Phrase));
451    }
452
453    #[test]
454    fn parse_sofia_profile_headers_are_spelling_exact() {
455        assert_eq!(
456            "profile-name".parse::<EventHeader>(),
457            Ok(EventHeader::ProfileName)
458        );
459        assert_eq!(
460            "profile_name".parse::<EventHeader>(),
461            Ok(EventHeader::ProfileNameSnake)
462        );
463        assert_eq!(
464            "module_name".parse::<EventHeader>(),
465            Ok(EventHeader::ModuleName)
466        );
467        assert_eq!(
468            "profile_uri".parse::<EventHeader>(),
469            Ok(EventHeader::ProfileUri)
470        );
471    }
472
473    #[test]
474    fn normalize_keeps_profile_name_spellings_apart() {
475        assert_eq!(normalize_header_key("profile-name"), "profile-name");
476        assert_eq!(normalize_header_key("PROFILE-NAME"), "profile-name");
477        assert_eq!(normalize_header_key("profile_name"), "profile_name");
478    }
479
480    #[test]
481    fn case_alias_key_underscore_excluded() {
482        // Underscore keys are excluded from case-insensitive aliasing.
483        assert_eq!(case_alias_key("variable_foo"), None);
484        assert_eq!(case_alias_key("sip_h_Call-Info"), None);
485        assert_eq!(case_alias_key("sip_i_contact"), None);
486    }
487
488    #[test]
489    fn case_alias_key_dash_keys_lowercased() {
490        // Dash-separated keys return their lowercase alias.
491        assert_eq!(
492            case_alias_key("Content-Type"),
493            Some("content-type".to_string())
494        );
495        assert_eq!(case_alias_key("UNIQUE-ID"), Some("unique-id".to_string()));
496        assert_eq!(case_alias_key("reply-text"), Some("reply-text".to_string()));
497    }
498}