Skip to main content

sipx_sdp/
rtpmap.rs

1//! `a=rtpmap` values: the format one names, and whether two of them name the same format.
2//!
3//! **This module is the single authority for RFC 8866 §6.6 format identity.** The question it
4//! answers used to be answered twice. [`mod@crate::answer`] asked it to decide which offered
5//! formats go into the answer, `sipx-call` asked it to decide which codec to build the media
6//! session with, and the two disagreed: one compared the clock rate as text where the other
7//! parsed it to a number. `08000` and `8000` are numerically equal and textually different, so an
8//! offer spelling the rate that way settled on µ-law while the answer named only A-law. sipx then
9//! sent on a payload type the answer never offered, and decoded the peer's A-law through a µ-law
10//! session — audible garbage rather than silence, with nothing in the stack reporting an error
11//! (`M-31`).
12//!
13//! The rule lives *here*, in the lower crate, because the dependency only runs one way:
14//! `sipx-call` can call down, and [`mod@crate::answer`] cannot call up. Nothing that belongs to the
15//! layer above comes with it — this module knows the grammar and what makes two values equal, and
16//! has no concept of a codec set or of which format to prefer. Choosing among the values that
17//! match stays where it belongs, above.
18//!
19//! `docs/specs/sdp-format-identity.md` is normative.
20
21/// Why an `a=rtpmap` value names no format.
22///
23/// A value that names nothing is not an error the stack reports to anyone: both callers turn it
24/// into a non-match, which is the conservative reading of hostile input — a format sipx cannot
25/// identify is a format sipx does not agree to. The variants exist so the reason is available to
26/// a caller that wants to log it, and so a malformed clock rate is a typed outcome rather than a
27/// panic ([AGENTS.md](../../../AGENTS.md) non-negotiable 3).
28#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29#[non_exhaustive]
30pub enum RtpmapError {
31    /// Nothing before the first `/`.
32    #[error("no encoding name")]
33    MissingEncoding,
34    /// No `/` at all. RFC 8866 §6.6 makes the clock rate part of the format's identity, so a
35    /// value without one identifies nothing.
36    #[error("no clock rate")]
37    MissingClockRate,
38    /// The clock rate is not a decimal number that fits in 32 bits.
39    #[error("clock rate is not a decimal number: {0}")]
40    ClockRate(String),
41    /// The encoding parameter is not a decimal number that fits in 32 bits. A value carrying more
42    /// fields than the grammar has arrives here, because the extra `/` is not a digit.
43    #[error("encoding parameter is not a decimal number: {0}")]
44    EncodingParameter(String),
45}
46
47/// The format an `a=rtpmap` value names.
48///
49/// `<encoding name>/<clock rate>[/<encoding parameters>]` (RFC 8866 §6.6). For audio the encoding
50/// parameter is the channel count, and an omitted one means one channel.
51///
52/// **Deliberately not `PartialEq`.** Derived equality would compare the encoding name as bytes,
53/// and the name compares case-insensitively — so `==` would be a second, wrong answer to the
54/// question this module exists to answer once. [`Rtpmap::same_format_as`] is the only comparison.
55#[derive(Debug, Clone, Copy)]
56pub struct Rtpmap<'a> {
57    encoding: &'a str,
58    clock_rate: u32,
59    channels: u32,
60}
61
62impl<'a> Rtpmap<'a> {
63    /// Read the format a value names.
64    ///
65    /// The value is the part of the attribute after the payload type — `PCMU/8000`, not
66    /// `0 PCMU/8000` — which is what [`crate::MediaDescription::rtpmap`] returns.
67    ///
68    /// # Errors
69    ///
70    /// [`RtpmapError`] when the value is not RFC 8866 §6.6's grammar. This is a parser for data
71    /// that arrived from the network, so every rejection is a returned error: an empty rate, a
72    /// rate that is not digits, a rate too large for a `u32`, and a value carrying a field the
73    /// grammar does not have.
74    pub fn parse(value: &'a str) -> Result<Self, RtpmapError> {
75        let (encoding, rest) = value.split_once('/').ok_or(RtpmapError::MissingClockRate)?;
76        if encoding.is_empty() {
77            return Err(RtpmapError::MissingEncoding);
78        }
79
80        // Everything after the second `/` stays with the encoding parameter on purpose. A value
81        // with a fourth field is outside the grammar, and letting it through as "the parameter,
82        // plus some text nobody read" is how a stack agrees to a format it did not understand.
83        let (clock_rate, parameter) = match rest.split_once('/') {
84            Some((clock_rate, parameter)) => (clock_rate, Some(parameter)),
85            None => (rest, None),
86        };
87
88        Ok(Self {
89            encoding,
90            clock_rate: decimal(clock_rate)
91                .map_err(|field| RtpmapError::ClockRate(field.to_owned()))?,
92            channels: match parameter {
93                // RFC 8866 §6.6: an omitted encoding parameter means one channel.
94                None => 1,
95                Some(parameter) => decimal(parameter)
96                    .map_err(|field| RtpmapError::EncodingParameter(field.to_owned()))?,
97            },
98        })
99    }
100
101    /// The encoding name, as the description spelled it.
102    #[must_use]
103    pub fn encoding(&self) -> &'a str {
104        self.encoding
105    }
106
107    /// The RTP clock rate, which for several codecs is not the sample rate.
108    #[must_use]
109    pub fn clock_rate(&self) -> u32 {
110        self.clock_rate
111    }
112
113    /// The channel count, with RFC 8866 §6.6's default of one already applied.
114    #[must_use]
115    pub fn channels(&self) -> u32 {
116        self.channels
117    }
118
119    /// Whether two values name the same format.
120    ///
121    /// RFC 8866 §6.6: the encoding name compares case-insensitively, and the clock rate and
122    /// channel count are part of the format's identity — the same codec at two rates is two
123    /// formats.
124    ///
125    /// The rate and the count compare **by value**, because they are numbers and the identity of a
126    /// number is numeric. Comparing them as text answers a different question — whether they are
127    /// *spelled* the same — and answering that one by accident is exactly the defect `M-31` fixed.
128    #[must_use]
129    pub fn same_format_as(&self, other: &Rtpmap<'_>) -> bool {
130        self.encoding.eq_ignore_ascii_case(other.encoding)
131            && self.clock_rate == other.clock_rate
132            && self.channels == other.channels
133    }
134}
135
136/// Whether two `a=rtpmap` values name the same format, reading both.
137///
138/// The predicate both callers use, so neither has a rule of its own to drift from the other's.
139///
140/// `false` when either value names no format at all. A value that identifies nothing matches
141/// nothing — including another value that identifies nothing, since `PCMU` and `G729` are not the
142/// same format merely because neither carries a clock rate.
143#[must_use]
144pub fn same_format(offered: &str, local: &str) -> bool {
145    match (Rtpmap::parse(offered), Rtpmap::parse(local)) {
146        (Ok(offered), Ok(local)) => offered.same_format_as(&local),
147        _ => false,
148    }
149}
150
151/// A decimal digit string, by value.
152///
153/// Strict about the spelling in every way but one. `u32::from_str` on its own would accept `+8000`
154/// while rejecting ` 8000` and `8_000`, which is a different rule from any reader that looks at the
155/// characters, so the digits are checked here rather than left to it: an empty field, a sign,
156/// surrounding whitespace and a digit separator all name no rate.
157///
158/// **Leading zeros are tolerated on purpose.** RFC 8866 §9's `integer` rule starts at a non-zero
159/// digit, so `08000` is strictly ungrammatical — but it is unambiguously eight thousand, it is
160/// what a zero-padded field in somebody's config generator produces, and refusing it would decline
161/// a format the peer plainly named. Tolerating it costs nothing precisely because there is now one
162/// reader: the two rules cannot tolerate it differently.
163///
164/// Returns the offending field so the caller can name it in a typed error. A value too large for a
165/// `u32` fails here rather than wrapping or panicking.
166fn decimal(field: &str) -> Result<u32, &str> {
167    if field.is_empty() || !field.bytes().all(|byte| byte.is_ascii_digit()) {
168        return Err(field);
169    }
170    field.parse::<u32>().map_err(|_| field)
171}
172
173#[cfg(test)]
174#[allow(
175    clippy::unwrap_used,
176    clippy::expect_used,
177    clippy::panic,
178    clippy::indexing_slicing
179)]
180mod tests {
181    use super::*;
182
183    /// §4.1's vectors: what the grammar admits, and what each field reads as.
184    #[test]
185    fn a_value_reads_as_its_three_fields() {
186        let pcmu = Rtpmap::parse("PCMU/8000").expect("the grammar");
187        assert_eq!(pcmu.encoding(), "PCMU");
188        assert_eq!(pcmu.clock_rate(), 8_000);
189        assert_eq!(pcmu.channels(), 1, "an omitted parameter is one channel");
190
191        let opus = Rtpmap::parse("opus/48000/2").expect("the grammar");
192        assert_eq!(opus.encoding(), "opus");
193        assert_eq!(opus.clock_rate(), 48_000);
194        assert_eq!(opus.channels(), 2);
195    }
196
197    /// The identity rule, field by field. A rate or a channel count that differs is a *different
198    /// format*, not the same one spelled loosely — the same codec at two rates is two formats.
199    #[test]
200    fn identity_is_the_name_case_insensitively_and_the_numbers_by_value() {
201        assert!(
202            same_format("PCMU/8000", "pcmu/8000"),
203            "the name is case-blind"
204        );
205        assert!(
206            same_format("PCMU/8000", "PCMU/8000/1"),
207            "one channel is the default"
208        );
209        assert!(!same_format("PCMU/8000", "PCMA/8000"), "a different name");
210        assert!(!same_format("PCMU/16000", "PCMU/8000"), "a different rate");
211        assert!(
212            !same_format("PCMU/8000/2", "PCMU/8000"),
213            "a different channel count"
214        );
215    }
216
217    /// **The `M-31` class.** A spelling that is numerically equal and textually different is the
218    /// same format, in either numeric field. These are the rows a text comparison got wrong.
219    #[test]
220    fn a_number_spelled_differently_is_the_same_number() {
221        assert!(
222            same_format("PCMU/08000", "PCMU/8000"),
223            "a leading zero in the rate"
224        );
225        assert!(
226            same_format("PCMU/8000/01", "PCMU/8000"),
227            "a leading zero in the count"
228        );
229        assert!(
230            same_format("PCMU/0008000/0001", "PCMU/8000/1"),
231            "both, padded"
232        );
233        assert!(
234            same_format("opus/048000/2", "opus/48000/2"),
235            "the gated codec too"
236        );
237    }
238
239    /// Hostile input from a peer is a typed error and a non-match, never a panic. §4.2's vectors.
240    #[test]
241    fn a_value_that_identifies_nothing_is_a_typed_error() {
242        for (value, expected) in [
243            ("PCMU", RtpmapError::MissingClockRate),
244            ("", RtpmapError::MissingClockRate),
245            ("/8000", RtpmapError::MissingEncoding),
246            ("PCMU/", RtpmapError::ClockRate(String::new())),
247            ("PCMU/+8000", RtpmapError::ClockRate("+8000".to_owned())),
248            ("PCMU/-8000", RtpmapError::ClockRate("-8000".to_owned())),
249            ("PCMU/ 8000", RtpmapError::ClockRate(" 8000".to_owned())),
250            ("PCMU/8_000", RtpmapError::ClockRate("8_000".to_owned())),
251            ("PCMU/eight", RtpmapError::ClockRate("eight".to_owned())),
252            // Larger than u32::MAX. Parsed, not wrapped and not panicked on.
253            (
254                "PCMU/99999999999999",
255                RtpmapError::ClockRate("99999999999999".to_owned()),
256            ),
257            ("PCMU/8000/", RtpmapError::EncodingParameter(String::new())),
258            (
259                "PCMU/8000/two",
260                RtpmapError::EncodingParameter("two".to_owned()),
261            ),
262            // A fourth field is outside the grammar and stays with the parameter, so it is
263            // rejected rather than silently ignored.
264            (
265                "PCMU/8000/1/9",
266                RtpmapError::EncodingParameter("1/9".to_owned()),
267            ),
268        ] {
269            assert_eq!(
270                Rtpmap::parse(value).err(),
271                Some(expected),
272                "reading {value:?}"
273            );
274            assert!(
275                !same_format(value, "PCMU/8000"),
276                "{value:?} names no format, so it matches none"
277            );
278            assert!(
279                !same_format("PCMU/8000", value),
280                "{value:?} matches none from the other side either"
281            );
282        }
283    }
284
285    /// Two values that each name nothing are not thereby equal. Without this, an offer of `PCMU`
286    /// with no rate would match a local `G729` with no rate.
287    #[test]
288    fn nothing_does_not_match_nothing() {
289        assert!(!same_format("PCMU", "G729"));
290        assert!(!same_format("PCMU", "PCMU"), "not even the identical value");
291    }
292
293    /// The largest rate the type holds is read, so the boundary is a value and not a panic.
294    #[test]
295    fn the_largest_representable_rate_reads() {
296        let max = format!("X/{}", u32::MAX);
297        assert_eq!(
298            Rtpmap::parse(&max).expect("u32::MAX fits").clock_rate(),
299            u32::MAX
300        );
301
302        let over = format!("X/{}", u64::from(u32::MAX) + 1);
303        assert!(Rtpmap::parse(&over).is_err(), "one past the top is refused");
304    }
305}