Skip to main content

freeswitch_types/sdp/
error.rs

1//! Error and warning types for SDP codec parsing and codec-string construction.
2//!
3//! Line numbers in this module index FreeSWITCH `v1.11.1`
4//! (`c2c59645f6911a76589e5008c4d73349ded44b65`).
5
6use crate::sdp::codec::SdpMediaType;
7use std::fmt;
8
9/// Failures parsing an SDP payload or converting it to a codec string.
10#[derive(Debug)]
11#[non_exhaustive]
12pub enum SdpCodecError {
13    /// The `sdp-types` crate could not parse the SDP blob.
14    ///
15    /// The underlying parse error is accessible via [`std::error::Error::source`].
16    /// The `sdp-types` type is not exposed in any public signature.
17    ParseFailure {
18        /// Human-readable description of what failed.
19        message: String,
20        /// The underlying parse error, preserved for `Error::source()`.
21        source: Box<dyn std::error::Error + Send + Sync>,
22    },
23    /// An `a=rtpmap` line was malformed (wrong number of fields, non-numeric payload
24    /// type, non-numeric clock rate, etc.).
25    MalformedRtpmap(String),
26    /// A payload type in the `m=` format list could not be parsed as a number.
27    NonNumericPayloadType(String),
28    /// An `a=fmtp` line was malformed (non-numeric payload type).
29    ///
30    /// A non-numeric payload type in `a=fmtp` is the same structural breakage as
31    /// in `a=rtpmap` — the line is uninterpretable and cannot be recovered from.
32    MalformedFmtp(String),
33}
34
35impl SdpCodecError {
36    /// Construct a [`SdpCodecError::ParseFailure`] from a message and a source error.
37    ///
38    /// The source error is boxed so the `sdp-types` concrete type does not appear
39    /// in any public signature.
40    pub(crate) fn parse_failure(
41        message: impl Into<String>,
42        source: impl std::error::Error + Send + Sync + 'static,
43    ) -> Self {
44        Self::ParseFailure {
45            message: message.into(),
46            source: Box::new(source),
47        }
48    }
49}
50
51impl fmt::Display for SdpCodecError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::ParseFailure { message, .. } => write!(f, "SDP parse failure: {message}"),
55            Self::MalformedRtpmap(raw) => {
56                write!(f, "malformed a=rtpmap ({} bytes)", raw.len())
57            }
58            Self::NonNumericPayloadType(raw) => {
59                write!(
60                    f,
61                    "non-numeric payload type in m= format list ({} bytes)",
62                    raw.len()
63                )
64            }
65            Self::MalformedFmtp(raw) => {
66                write!(f, "malformed a=fmtp ({} bytes)", raw.len())
67            }
68        }
69    }
70}
71
72impl std::error::Error for SdpCodecError {
73    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
74        match self {
75            Self::ParseFailure { source, .. } => Some(source.as_ref()),
76            _ => None,
77        }
78    }
79}
80
81/// Failures building or parsing a FreeSWITCH codec-string entry.
82#[derive(Debug, Clone, PartialEq, Eq)]
83#[non_exhaustive]
84pub enum CodecStringError {
85    /// A format-parameter value contains `@`.
86    ///
87    /// The `@` split precedes the `~` split in FreeSWITCH's parser, so an `@`
88    /// inside fmtp is read as a qualifier rather than fmtp content — silently
89    /// producing a wrong codec entry.
90    AtInFmtp(String),
91    /// A format-parameter value contains `.` with no module prefix.
92    ///
93    /// The `.` split also precedes the `~` split, so the first dot becomes the
94    /// module-name boundary and the mangled name misses the hash — the codec is
95    /// dropped with no log.
96    DotInFmtpWithoutModule(String),
97    /// The codec name is empty or otherwise invalid.
98    InvalidCodecName(String),
99    /// A codec name or field contains `\n` or `\r`.
100    ///
101    /// ESL is a text protocol terminated by `\n\n`; any user-provided string
102    /// reaching the wire without validation can inject arbitrary ESL commands.
103    WireInjection {
104        /// Name of the field that contained the invalid character.
105        field: String,
106        /// The value that triggered the error.
107        value: String,
108    },
109    /// A codec-string qualifier value could not be parsed.
110    ///
111    /// In strict mode (the [`FromStr`](std::str::FromStr) impl), a qualifier
112    /// that overflows `u32` or carries no recognised letter (`h`/`k`/`i`/`b`/`c`)
113    /// is a hard error: the data would otherwise be silently lost. In lenient mode
114    /// ([`CodecString::parse_lenient`](super::codec_string::CodecString::parse_lenient)) the same condition records a
115    /// [`SdpWarning::CodecStringQualifier`] and continues.
116    QualifierParseError {
117        /// The raw qualifier part that could not be parsed (e.g. `"9999999999h"`).
118        raw: String,
119        /// Why parsing failed.
120        reason: String,
121    },
122    /// A codec name or module name contains a character that is a grammar delimiter.
123    ///
124    /// Names and module names are emitted unescaped in the codec-string `Display`
125    /// output. A delimiter character (`,` `@` `~` `.` `'` `\` or ASCII whitespace)
126    /// in a name would corrupt the grammar when the string is re-parsed, breaking
127    /// the round-trip invariant. The caller must not use such characters in codec
128    /// names — no real FreeSWITCH codec name contains any of them.
129    InvalidCharInName {
130        /// Name of the field (`"name"` or `"modname"`).
131        field: String,
132        /// The character that triggered the error.
133        ch: char,
134        /// The full value that contained the invalid character.
135        value: String,
136    },
137    /// The input contains multiple comma-separated entries where a single entry was expected.
138    ///
139    /// [`FromStr`](std::str::FromStr) for [`CodecStringEntry`](super::codec_string::CodecStringEntry)
140    /// requires exactly one entry. A top-level (unescaped, unquoted) comma signals
141    /// multiple entries — a silent take-first would hide the caller's mistake.
142    MultipleEntries(String),
143    /// More entries than FreeSWITCH parses from one codec string.
144    ///
145    /// `switch_separate_string` stops at `SWITCH_MAX_CODECS`
146    /// (`switch_types.h:595`) and the entries past it are lost with no log and no
147    /// return-value signal, so a string built past the cap negotiates something
148    /// other than what it says.
149    TooManyEntries {
150        /// How many entries the operation would have produced.
151        entries: usize,
152        /// The cap, [`CodecString::MAX_SWITCH_ENTRIES`](super::codec_string::CodecString::MAX_SWITCH_ENTRIES).
153        limit: usize,
154    },
155}
156
157impl CodecStringError {
158    /// Construct an [`AtInFmtp`](CodecStringError::AtInFmtp) error.
159    pub(crate) fn at_in_fmtp(value: impl Into<String>) -> Self {
160        Self::AtInFmtp(value.into())
161    }
162
163    /// Construct a [`DotInFmtpWithoutModule`](CodecStringError::DotInFmtpWithoutModule) error.
164    pub(crate) fn dot_in_fmtp_without_module(value: impl Into<String>) -> Self {
165        Self::DotInFmtpWithoutModule(value.into())
166    }
167
168    /// Construct an [`InvalidCodecName`](CodecStringError::InvalidCodecName) error.
169    pub(crate) fn invalid_codec_name(value: impl Into<String>) -> Self {
170        Self::InvalidCodecName(value.into())
171    }
172
173    /// Construct a [`WireInjection`](CodecStringError::WireInjection) error.
174    pub(crate) fn wire_injection(field: impl Into<String>, value: impl Into<String>) -> Self {
175        Self::WireInjection {
176            field: field.into(),
177            value: value.into(),
178        }
179    }
180
181    /// Construct a [`QualifierParseError`](CodecStringError::QualifierParseError) error.
182    pub(crate) fn qualifier_parse_error(raw: impl Into<String>, reason: impl Into<String>) -> Self {
183        Self::QualifierParseError {
184            raw: raw.into(),
185            reason: reason.into(),
186        }
187    }
188
189    /// Construct an [`InvalidCharInName`](CodecStringError::InvalidCharInName) error.
190    pub(crate) fn invalid_char_in_name(
191        field: impl Into<String>,
192        ch: char,
193        value: impl Into<String>,
194    ) -> Self {
195        Self::InvalidCharInName {
196            field: field.into(),
197            ch,
198            value: value.into(),
199        }
200    }
201
202    /// Construct a [`MultipleEntries`](CodecStringError::MultipleEntries) error.
203    pub(crate) fn multiple_entries(value: impl Into<String>) -> Self {
204        Self::MultipleEntries(value.into())
205    }
206
207    /// Construct a [`TooManyEntries`](CodecStringError::TooManyEntries) error.
208    pub(crate) fn too_many_entries(entries: usize, limit: usize) -> Self {
209        Self::TooManyEntries { entries, limit }
210    }
211}
212
213impl fmt::Display for CodecStringError {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            Self::AtInFmtp(v) => write!(
217                f,
218                "format-parameter value {v:?} contains '@': the '@' split precedes the '~' \
219                 split, so it would be read as a qualifier rather than fmtp content"
220            ),
221            Self::DotInFmtpWithoutModule(v) => write!(
222                f,
223                "format-parameter value {v:?} contains '.' with no module prefix: the first \
224                 dot becomes the module-name boundary and the codec would be silently dropped"
225            ),
226            Self::InvalidCodecName(v) => write!(f, "invalid or empty codec name: {v:?}"),
227            Self::WireInjection { field, value } => write!(
228                f,
229                "field {field:?} contains '\\n' or '\\r' in value {value:?}: \
230                 newline/CR in ESL wire data can inject arbitrary commands"
231            ),
232            Self::QualifierParseError { raw, reason } => write!(
233                f,
234                "codec-string qualifier {raw:?} could not be parsed: {reason}"
235            ),
236            Self::InvalidCharInName { field, ch, value } => write!(
237                f,
238                "field {field:?} value {value:?} contains {ch:?}: names and module names \
239                 are emitted unescaped, so grammar delimiters (`,`, `@`, `~`, `.`, `'`, \
240                 `\\`, whitespace) would corrupt the codec-string on re-parse"
241            ),
242            Self::MultipleEntries(v) => write!(
243                f,
244                "input {v:?} contains multiple comma-separated entries where a single \
245                 CodecStringEntry was expected"
246            ),
247            Self::TooManyEntries { entries, limit } => write!(
248                f,
249                "{entries} codec-string entries exceeds the {limit} FreeSWITCH parses; \
250                 the rest would be dropped with no log"
251            ),
252        }
253    }
254}
255
256impl std::error::Error for CodecStringError {}
257
258/// A payload type present in an `m=` format list that cannot be named.
259///
260/// This occurs when a payload type has no `a=rtpmap` and is not one of the
261/// RFC 3551 static types (PT 0–34). Dropping it silently would be
262/// indistinguishable from the peer never having offered it, so it is surfaced
263/// as data rather than an error.
264#[derive(Debug, Clone, PartialEq, Eq)]
265#[non_exhaustive]
266pub struct UnmappedPayload {
267    /// The payload type number from the `m=` format list.
268    pub payload_type: u8,
269    /// The media type of the section where this payload type appeared.
270    pub media_type: SdpMediaType,
271}
272
273impl UnmappedPayload {
274    /// Records a dynamic payload type that has no `a=rtpmap` and no static-table entry.
275    pub(crate) fn new(payload_type: u8, media_type: SdpMediaType) -> Self {
276        Self {
277            payload_type,
278            media_type,
279        }
280    }
281}
282
283impl fmt::Display for UnmappedPayload {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        write!(
286            f,
287            "payload type {} in {} section has no rtpmap and is not a static type",
288            self.payload_type, self.media_type
289        )
290    }
291}
292
293/// A recoverable oddity recorded during SDP parsing rather than failed on.
294#[derive(Debug, Clone, PartialEq, Eq)]
295#[non_exhaustive]
296pub enum SdpWarning {
297    /// A numeric SDP attribute could not be parsed as a positive integer.
298    ///
299    /// FreeSWITCH's `atoi` accepts `"abc"` as 0 and treats 0 as "not set".
300    /// A strict parser records this as a warning rather than hard-failing so
301    /// the session is not dropped over a cosmetic attribute.
302    UnparseableNumericAttribute {
303        /// The attribute name, e.g. `"ptime"` or `"maxptime"`.
304        attribute: String,
305        /// The raw on-wire value that could not be parsed.
306        raw_value: String,
307    },
308    /// A codec-string qualifier value was skipped during lenient parsing.
309    ///
310    /// Produced by [`CodecString::parse_lenient`](super::codec_string::CodecString::parse_lenient) when a qualifier overflows `u32`
311    /// or carries no recognised letter. The qualifier is omitted from the parsed entry.
312    /// In strict mode ([`FromStr`](std::str::FromStr)) the same condition is a hard error.
313    CodecStringQualifier {
314        /// The raw qualifier part that could not be parsed (e.g. `"9999999999h"`).
315        raw: String,
316        /// Why parsing failed.
317        reason: String,
318    },
319    /// A codec's fmtp could not be embedded in the codec-string entry and was cleared.
320    ///
321    /// Occurs when the offer's `a=fmtp` value contains characters that are unrepresentable
322    /// in the codec-string format (e.g. a dotted fmtp like `br=13.2-24.4` against a bare
323    /// policy entry that has no module prefix). The codec entry is still emitted; only the
324    /// fmtp qualifier is absent. The original fmtp is preserved here for inspection.
325    FmtpUnrepresentable {
326        /// The codec name for which fmtp could not be embedded.
327        codec_name: String,
328        /// The fmtp value that could not be represented.
329        fmtp: String,
330        /// Human-readable reason the fmtp could not be embedded.
331        reason: String,
332    },
333    /// An offered codec's `a=rtpmap` encoding name cannot be represented as a
334    /// codec-string entry at all, and the codec was skipped.
335    ///
336    /// Occurs when the name itself contains a codec-string grammar delimiter
337    /// (`,` `@` `~` `.` `'` `\` or ASCII whitespace). Unlike
338    /// [`FmtpUnrepresentable`](SdpWarning::FmtpUnrepresentable), there is no entry to fall
339    /// back to — the whole codec is dropped, narrowing the emitted list. Distinct from
340    /// [`UnmappedPayload`]: that payload type had no rtpmap name whatsoever, whereas this
341    /// one has a name that simply cannot survive the codec-string grammar.
342    CodecNameUnrepresentable {
343        /// The raw, unrepresentable encoding name from `a=rtpmap`.
344        codec_name: String,
345        /// Human-readable reason the name could not be embedded.
346        reason: String,
347    },
348    /// A single `m=` section was structurally broken and was skipped entirely.
349    ///
350    /// Occurs when an `a=rtpmap`, `a=fmtp`, or `m=` format-list entry in one media
351    /// section cannot be parsed at all (as opposed to [`CodecNameUnrepresentable`](SdpWarning::CodecNameUnrepresentable),
352    /// which drops a single codec). Every other section's codecs are still parsed
353    /// and returned.
354    MalformedMediaSection {
355        /// The media type of the skipped section (e.g. `"audio"`).
356        media_type: String,
357        /// Human-readable reason the section could not be parsed.
358        reason: String,
359    },
360    /// A lenient parse read more comma-separated slots than FreeSWITCH would.
361    ///
362    /// Every entry parsed is returned; the switch itself keeps only the first
363    /// `limit` slots (`switch_types.h:595`), empty ones included, and loses the
364    /// rest with no log.
365    CodecStringTruncated {
366        /// How many comma-separated slots the input carried.
367        entries: usize,
368        /// The cap, [`CodecString::MAX_SWITCH_ENTRIES`](super::codec_string::CodecString::MAX_SWITCH_ENTRIES).
369        limit: usize,
370    },
371    /// A value-less attribute names a direction in a case RFC 8866 does not accept.
372    ///
373    /// It is not read as one, so the section keeps the direction of the level above
374    /// it — which is what the peer wrote only by coincidence.
375    NonCanonicalDirectionAttribute {
376        /// The attribute name as the peer spelled it.
377        attribute: String,
378    },
379}
380
381impl SdpWarning {
382    /// Construct an [`UnparseableNumericAttribute`](SdpWarning::UnparseableNumericAttribute) warning.
383    pub(crate) fn unparseable_numeric_attribute(
384        attribute: impl Into<String>,
385        raw_value: impl Into<String>,
386    ) -> Self {
387        Self::UnparseableNumericAttribute {
388            attribute: attribute.into(),
389            raw_value: raw_value.into(),
390        }
391    }
392
393    /// Construct a [`CodecStringQualifier`](SdpWarning::CodecStringQualifier) warning.
394    pub(crate) fn codec_string_qualifier(
395        raw: impl Into<String>,
396        reason: impl Into<String>,
397    ) -> Self {
398        Self::CodecStringQualifier {
399            raw: raw.into(),
400            reason: reason.into(),
401        }
402    }
403
404    /// Construct an [`FmtpUnrepresentable`](SdpWarning::FmtpUnrepresentable) warning.
405    pub(crate) fn fmtp_unrepresentable(
406        codec_name: impl Into<String>,
407        fmtp: impl Into<String>,
408        reason: impl Into<String>,
409    ) -> Self {
410        Self::FmtpUnrepresentable {
411            codec_name: codec_name.into(),
412            fmtp: fmtp.into(),
413            reason: reason.into(),
414        }
415    }
416
417    /// Construct a [`CodecNameUnrepresentable`](SdpWarning::CodecNameUnrepresentable) warning.
418    pub(crate) fn codec_name_unrepresentable(
419        codec_name: impl Into<String>,
420        reason: impl Into<String>,
421    ) -> Self {
422        Self::CodecNameUnrepresentable {
423            codec_name: codec_name.into(),
424            reason: reason.into(),
425        }
426    }
427
428    /// Construct a [`NonCanonicalDirectionAttribute`](SdpWarning::NonCanonicalDirectionAttribute) warning.
429    pub(crate) fn non_canonical_direction_attribute(attribute: impl Into<String>) -> Self {
430        Self::NonCanonicalDirectionAttribute {
431            attribute: attribute.into(),
432        }
433    }
434
435    /// Construct a [`CodecStringTruncated`](SdpWarning::CodecStringTruncated) warning.
436    pub(crate) fn codec_string_truncated(entries: usize, limit: usize) -> Self {
437        Self::CodecStringTruncated { entries, limit }
438    }
439
440    /// Construct a [`MalformedMediaSection`](SdpWarning::MalformedMediaSection) warning.
441    pub(crate) fn malformed_media_section(
442        media_type: impl Into<String>,
443        reason: impl Into<String>,
444    ) -> Self {
445        Self::MalformedMediaSection {
446            media_type: media_type.into(),
447            reason: reason.into(),
448        }
449    }
450}
451
452impl fmt::Display for SdpWarning {
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        match self {
455            Self::UnparseableNumericAttribute {
456                attribute,
457                raw_value,
458            } => write!(
459                f,
460                "a={attribute} value ({} bytes) is not a valid positive integer; \
461                 treated as not set",
462                raw_value.len()
463            ),
464            Self::CodecStringQualifier { raw, reason } => write!(
465                f,
466                "codec-string qualifier {raw:?} skipped during lenient parse: {reason}"
467            ),
468            Self::FmtpUnrepresentable {
469                codec_name,
470                fmtp,
471                reason,
472            } => write!(
473                f,
474                "the a=fmtp ({} bytes) of an offered codec ({} bytes) could not be embedded \
475                 in the codec-string entry and was cleared: it {reason}",
476                fmtp.len(),
477                codec_name.len()
478            ),
479            Self::CodecNameUnrepresentable { codec_name, reason } => write!(
480                f,
481                "an offered codec name ({} bytes) cannot be a codec-string entry and was \
482                 skipped: it {reason}",
483                codec_name.len()
484            ),
485            Self::MalformedMediaSection { media_type, reason } => write!(
486                f,
487                "{media_type} section could not be parsed and was skipped entirely: {reason}"
488            ),
489            Self::NonCanonicalDirectionAttribute { attribute } => write!(
490                f,
491                "direction attribute ({} bytes) is not spelled as RFC 8866 defines it \
492                 and was not read as a direction",
493                attribute.len()
494            ),
495            Self::CodecStringTruncated { entries, limit } => write!(
496                f,
497                "codec string carries {entries} comma-separated slots; FreeSWITCH keeps \
498                 the first {limit} and drops the rest with no log"
499            ),
500        }
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use crate::sdp::codec::SdpMediaType;
508
509    // --- SdpCodecError ---
510
511    #[test]
512    fn sdp_codec_error_parse_failure_display() {
513        use std::fmt;
514
515        #[derive(Debug)]
516        struct DummyErr;
517        impl fmt::Display for DummyErr {
518            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519                f.write_str("no version line")
520            }
521        }
522        impl std::error::Error for DummyErr {}
523
524        let e = SdpCodecError::parse_failure("missing v= line", DummyErr);
525        let msg = e.to_string();
526        assert!(msg.contains("SDP parse failure"));
527        assert!(msg.contains("missing v= line"));
528    }
529
530    #[test]
531    fn sdp_codec_error_source_preserved() {
532        use std::fmt;
533
534        #[derive(Debug)]
535        struct Root;
536        impl fmt::Display for Root {
537            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538                f.write_str("root cause")
539            }
540        }
541        impl std::error::Error for Root {}
542
543        let e = SdpCodecError::parse_failure("wrapper", Root);
544        let src = std::error::Error::source(&e).expect("source must be present");
545        assert_eq!(src.to_string(), "root cause");
546    }
547
548    #[test]
549    fn sdp_codec_error_malformed_rtpmap_display() {
550        let e = SdpCodecError::MalformedRtpmap("opus/abc/2".into());
551        let msg = e.to_string();
552        assert!(msg.contains("malformed a=rtpmap"));
553        assert!(!msg.contains("opus/abc/2"), "error quoted its input: {msg}");
554        assert!(msg.contains("10 bytes"), "{msg}");
555        let SdpCodecError::MalformedRtpmap(raw) = &e else {
556            panic!("wrong variant");
557        };
558        assert_eq!(raw, "opus/abc/2");
559    }
560
561    #[test]
562    fn sdp_codec_error_non_numeric_pt_display() {
563        let e = SdpCodecError::NonNumericPayloadType("abc".into());
564        let msg = e.to_string();
565        assert!(msg.contains("non-numeric payload type"));
566        assert!(!msg.contains("abc"), "error quoted its input: {msg}");
567        assert!(msg.contains("3 bytes"), "{msg}");
568    }
569
570    #[test]
571    fn sdp_codec_error_other_variants_have_no_source() {
572        let e = SdpCodecError::MalformedRtpmap("x".into());
573        assert!(std::error::Error::source(&e).is_none());
574    }
575
576    #[test]
577    fn sdp_codec_error_malformed_fmtp_display() {
578        let e = SdpCodecError::MalformedFmtp("x minptime=10".into());
579        let msg = e.to_string();
580        assert!(msg.contains("malformed a=fmtp"));
581        assert!(!msg.contains("minptime"), "error quoted its input: {msg}");
582        assert!(msg.contains("13 bytes"), "{msg}");
583    }
584
585    // --- CodecStringError ---
586
587    #[test]
588    fn codec_string_error_at_in_fmtp() {
589        let e = CodecStringError::at_in_fmtp("br=13.2@24.4");
590        let msg = e.to_string();
591        assert!(msg.contains('@'));
592        assert!(msg.contains("qualifier"));
593    }
594
595    #[test]
596    fn codec_string_error_dot_in_fmtp_without_module() {
597        let e = CodecStringError::dot_in_fmtp_without_module("br=13.2-24.4");
598        let msg = e.to_string();
599        assert!(msg.contains('.'));
600        assert!(msg.contains("silently dropped"));
601    }
602
603    #[test]
604    fn codec_string_error_invalid_codec_name() {
605        let e = CodecStringError::invalid_codec_name("");
606        let msg = e.to_string();
607        assert!(msg.contains("invalid or empty codec name"));
608    }
609
610    #[test]
611    fn codec_string_error_wire_injection() {
612        let e = CodecStringError::wire_injection("name", "bad\ncodec");
613        let msg = e.to_string();
614        assert!(msg.contains("\\n") || msg.contains("newline"));
615        assert!(msg.contains("name"));
616    }
617
618    #[test]
619    fn codec_string_error_equality() {
620        assert_eq!(
621            CodecStringError::at_in_fmtp("x"),
622            CodecStringError::at_in_fmtp("x")
623        );
624        assert_ne!(
625            CodecStringError::at_in_fmtp("x"),
626            CodecStringError::at_in_fmtp("y")
627        );
628    }
629
630    // --- UnmappedPayload ---
631
632    #[test]
633    fn unmapped_payload_accessors() {
634        let u = UnmappedPayload::new(99, SdpMediaType::Audio);
635        assert_eq!(u.payload_type, 99);
636        assert_eq!(u.media_type, SdpMediaType::Audio);
637    }
638
639    #[test]
640    fn unmapped_payload_display() {
641        let u = UnmappedPayload::new(99, SdpMediaType::Video);
642        let msg = u.to_string();
643        assert!(msg.contains("99"));
644        assert!(msg.contains("video") || msg.contains("Video"));
645    }
646
647    // --- SdpWarning ---
648
649    #[test]
650    fn sdp_warning_unparseable_numeric_attribute() {
651        let w = SdpWarning::unparseable_numeric_attribute("ptime", "abc");
652        match &w {
653            SdpWarning::UnparseableNumericAttribute {
654                attribute,
655                raw_value,
656            } => {
657                assert_eq!(attribute, "ptime");
658                assert_eq!(raw_value, "abc");
659            }
660            #[allow(unreachable_patterns)]
661            _ => panic!("wrong variant"),
662        }
663    }
664
665    #[test]
666    fn sdp_warning_display_names_fields_and_lengths_not_peer_values() {
667        let cases: &[(SdpWarning, &str, &str)] = &[
668            (
669                SdpWarning::unparseable_numeric_attribute("maxptime", "abcdef"),
670                "maxptime",
671                "abcdef",
672            ),
673            (
674                SdpWarning::fmtp_unrepresentable("EVS", "br=13.2-24.4", "contains a dot"),
675                "12 bytes",
676                "br=13.2-24.4",
677            ),
678            (
679                SdpWarning::codec_name_unrepresentable("bad,name", "contains a delimiter"),
680                "8 bytes",
681                "bad,name",
682            ),
683            (
684                SdpWarning::non_canonical_direction_attribute("SENDONLY"),
685                "8 bytes",
686                "SENDONLY",
687            ),
688        ];
689        for (warning, expected, forbidden) in cases {
690            let msg = warning.to_string();
691            assert!(msg.contains(expected), "{msg}");
692            assert!(!msg.contains(forbidden), "warning quoted its input: {msg}");
693        }
694    }
695
696    #[test]
697    fn sdp_warning_equality() {
698        let a = SdpWarning::unparseable_numeric_attribute("ptime", "abc");
699        let b = SdpWarning::unparseable_numeric_attribute("ptime", "abc");
700        assert_eq!(a, b);
701    }
702}