1use crate::sdp::codec::SdpMediaType;
7use std::fmt;
8
9#[derive(Debug)]
11#[non_exhaustive]
12pub enum SdpCodecError {
13 ParseFailure {
18 message: String,
20 source: Box<dyn std::error::Error + Send + Sync>,
22 },
23 MalformedRtpmap(String),
26 NonNumericPayloadType(String),
28 MalformedFmtp(String),
33}
34
35impl SdpCodecError {
36 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#[derive(Debug, Clone, PartialEq, Eq)]
83#[non_exhaustive]
84pub enum CodecStringError {
85 AtInFmtp(String),
91 DotInFmtpWithoutModule(String),
97 InvalidCodecName(String),
99 WireInjection {
104 field: String,
106 value: String,
108 },
109 QualifierParseError {
117 raw: String,
119 reason: String,
121 },
122 InvalidCharInName {
130 field: String,
132 ch: char,
134 value: String,
136 },
137 MultipleEntries(String),
143 TooManyEntries {
150 entries: usize,
152 limit: usize,
154 },
155}
156
157impl CodecStringError {
158 pub(crate) fn at_in_fmtp(value: impl Into<String>) -> Self {
160 Self::AtInFmtp(value.into())
161 }
162
163 pub(crate) fn dot_in_fmtp_without_module(value: impl Into<String>) -> Self {
165 Self::DotInFmtpWithoutModule(value.into())
166 }
167
168 pub(crate) fn invalid_codec_name(value: impl Into<String>) -> Self {
170 Self::InvalidCodecName(value.into())
171 }
172
173 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 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 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 pub(crate) fn multiple_entries(value: impl Into<String>) -> Self {
204 Self::MultipleEntries(value.into())
205 }
206
207 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#[derive(Debug, Clone, PartialEq, Eq)]
265#[non_exhaustive]
266pub struct UnmappedPayload {
267 pub payload_type: u8,
269 pub media_type: SdpMediaType,
271}
272
273impl UnmappedPayload {
274 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#[derive(Debug, Clone, PartialEq, Eq)]
295#[non_exhaustive]
296pub enum SdpWarning {
297 UnparseableNumericAttribute {
303 attribute: String,
305 raw_value: String,
307 },
308 CodecStringQualifier {
314 raw: String,
316 reason: String,
318 },
319 FmtpUnrepresentable {
326 codec_name: String,
328 fmtp: String,
330 reason: String,
332 },
333 CodecNameUnrepresentable {
343 codec_name: String,
345 reason: String,
347 },
348 MalformedMediaSection {
355 media_type: String,
357 reason: String,
359 },
360 CodecStringTruncated {
366 entries: usize,
368 limit: usize,
370 },
371 NonCanonicalDirectionAttribute {
376 attribute: String,
378 },
379}
380
381impl SdpWarning {
382 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 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 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 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 pub(crate) fn non_canonical_direction_attribute(attribute: impl Into<String>) -> Self {
430 Self::NonCanonicalDirectionAttribute {
431 attribute: attribute.into(),
432 }
433 }
434
435 pub(crate) fn codec_string_truncated(entries: usize, limit: usize) -> Self {
437 Self::CodecStringTruncated { entries, limit }
438 }
439
440 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 #[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 #[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 #[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 #[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}