Skip to main content

freeswitch_types/sofia/
channel_name.rs

1//! Parser for sofia channel names (`sofia/<profile>/<destination>`).
2//!
3//! mod_sofia builds every channel name as `sofia/<profile>/<channame>` and
4//! truncates it at the first `;` (`sofia_glue_set_name()` in `sofia_glue.c`),
5//! so names never carry URI parameters. For inbound calls the destination is
6//! `user@host[:port]` from the parsed From URL (or bare `host[:port]` when the
7//! user part is empty); for outbound calls it is the freeform dial-string tail,
8//! which may contain a `sip:` scheme, further `/`, or no `@` at all.
9
10/// Borrowed view of a parsed sofia channel name. No allocation.
11///
12/// Only `sofia/...` names parse; other drivers (`loopback/...`, `error/...`)
13/// return `None`. All accessors return verbatim slices of the input: no
14/// URL-decoding, no `sip:` stripping, no port splitting — callers decide.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct SofiaChannelName<'a> {
17    profile: &'a str,
18    destination: &'a str,
19}
20
21impl<'a> SofiaChannelName<'a> {
22    /// Parses a channel name; `Some` only for well-formed
23    /// `sofia/<profile>/<destination>` names (non-empty profile).
24    pub fn parse(name: &'a str) -> Option<Self> {
25        let rest = name.strip_prefix("sofia/")?;
26        let (profile, destination) = rest.split_once('/')?;
27        if profile.is_empty() {
28            return None;
29        }
30        Some(Self {
31            profile,
32            destination,
33        })
34    }
35
36    /// Sofia profile name (second segment).
37    pub fn profile(&self) -> &'a str {
38        self.profile
39    }
40
41    /// Raw third segment, verbatim. May contain further `/` (dial-string
42    /// tails), a `sip:` scheme, or a `:port` suffix.
43    pub fn destination(&self) -> &'a str {
44        self.destination
45    }
46
47    /// Part of the destination before the last `@`, or `None` when there is
48    /// no `@`. The split is on the *last* `@` because mod_sofia URL-decodes
49    /// the user part, which may therefore contain `@`; a host never can.
50    pub fn user(&self) -> Option<&'a str> {
51        self.destination
52            .rsplit_once('@')
53            .map(|(user, _)| user)
54    }
55
56    /// Part of the destination after the last `@` (may include `:port`), or
57    /// `None` when there is no `@`. A destination without `@` is ambiguous
58    /// (bare host on inbound, user/number on gateway dials) — use
59    /// [`destination`](Self::destination) and decide from context.
60    pub fn host(&self) -> Option<&'a str> {
61        self.destination
62            .rsplit_once('@')
63            .map(|(_, host)| host)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn inbound_full() {
73        let n = SofiaChannelName::parse("sofia/esinet1-v4-tcp/+15550001@bcf.example.test")
74            .expect("sofia name parses");
75        assert_eq!(n.profile(), "esinet1-v4-tcp");
76        assert_eq!(n.destination(), "+15550001@bcf.example.test");
77        assert_eq!(n.user(), Some("+15550001"));
78        assert_eq!(n.host(), Some("bcf.example.test"));
79    }
80
81    #[test]
82    fn simple_user_host() {
83        let n = SofiaChannelName::parse("sofia/internal-v6/x@host").expect("sofia name parses");
84        assert_eq!(n.profile(), "internal-v6");
85        assert_eq!(n.user(), Some("x"));
86        assert_eq!(n.host(), Some("host"));
87    }
88
89    #[test]
90    fn host_with_port() {
91        let n = SofiaChannelName::parse("sofia/external/1001@10.0.0.5:5080")
92            .expect("sofia name parses");
93        assert_eq!(n.user(), Some("1001"));
94        assert_eq!(n.host(), Some("10.0.0.5:5080"));
95    }
96
97    #[test]
98    fn bare_host_inbound_no_at() {
99        let n = SofiaChannelName::parse("sofia/internal/10.0.0.5").expect("sofia name parses");
100        assert_eq!(n.profile(), "internal");
101        assert_eq!(n.destination(), "10.0.0.5");
102        assert_eq!(n.user(), None);
103        assert_eq!(n.host(), None);
104    }
105
106    #[test]
107    fn gateway_dial_no_at() {
108        let n = SofiaChannelName::parse("sofia/gw1/1002").expect("sofia name parses");
109        assert_eq!(n.profile(), "gw1");
110        assert_eq!(n.destination(), "1002");
111        assert_eq!(n.user(), None);
112        assert_eq!(n.host(), None);
113    }
114
115    #[test]
116    fn dial_tail_keeps_slashes() {
117        let n = SofiaChannelName::parse("sofia/internal/sip:a@b/extra").expect("sofia name parses");
118        assert_eq!(n.destination(), "sip:a@b/extra");
119        assert_eq!(n.user(), Some("sip:a"));
120        assert_eq!(n.host(), Some("b/extra"));
121    }
122
123    #[test]
124    fn multi_at_splits_on_last() {
125        let n = SofiaChannelName::parse("sofia/internal/a@b@c.example.test")
126            .expect("sofia name parses");
127        assert_eq!(n.user(), Some("a@b"));
128        assert_eq!(n.host(), Some("c.example.test"));
129    }
130
131    #[test]
132    fn empty_destination() {
133        let n = SofiaChannelName::parse("sofia/internal/").expect("sofia name parses");
134        assert_eq!(n.profile(), "internal");
135        assert_eq!(n.destination(), "");
136        assert_eq!(n.user(), None);
137        assert_eq!(n.host(), None);
138    }
139
140    #[test]
141    fn non_sofia_and_malformed_are_none() {
142        for name in [
143            "loopback/x",
144            "error/USER_BUSY",
145            "sofia/internal",
146            "sofia//x",
147            "sofia/",
148            "sofia",
149            "",
150            "Sofia/internal/x@h",
151        ] {
152            assert_eq!(SofiaChannelName::parse(name), None, "{name:?}");
153        }
154    }
155}