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}