Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#![warn(clippy::all)]
#![forbid(unsafe_code)]
#[macro_use]
extern crate log;
#[cfg(feature = "serialize")]
#[macro_use]
extern crate serde_derive;
#[cfg(feature = "serialize")]
extern crate serde;
use std::convert::TryFrom;
use std::fmt;
#[macro_use]
pub mod attribute_type;
pub mod address;
pub mod anonymizer;
pub mod error;
pub mod media_type;
pub mod network;
use address::{AddressTyped, ExplicitlyTypedAddress};
use anonymizer::{AnonymizingClone, StatefulSdpAnonymizer};
use attribute_type::{
parse_attribute, SdpAttribute, SdpAttributeRid, SdpAttributeSimulcastVersion, SdpAttributeType,
SdpSingleDirection,
};
use error::{SdpParserError, SdpParserInternalError};
use media_type::{
parse_media, parse_media_vector, SdpFormatList, SdpMedia, SdpMediaLine, SdpMediaValue,
SdpProtocolValue,
};
use network::{parse_address_type, parse_network_type};
/*
* RFC4566
* bandwidth-fields = *(%x62 "=" bwtype ":" bandwidth CRLF)
*/
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub enum SdpBandwidth {
As(u32),
Ct(u32),
Tias(u32),
Unknown(String, u32),
}
impl fmt::Display for SdpBandwidth {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (tp_string, value) = match *self {
SdpBandwidth::As(ref x) => ("AS", x),
SdpBandwidth::Ct(ref x) => ("CT", x),
SdpBandwidth::Tias(ref x) => ("TIAS", x),
SdpBandwidth::Unknown(ref tp, ref x) => (&tp[..], x),
};
write!(f, "{tp_string}:{value}")
}
}
/*
* RFC4566
* connection-field = [%x63 "=" nettype SP addrtype SP
* connection-address CRLF]
*/
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub struct SdpConnection {
pub address: ExplicitlyTypedAddress,
pub ttl: Option<u8>,
pub amount: Option<u32>,
}
impl fmt::Display for SdpConnection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.address.fmt(f)?;
write_option_string!(f, "/{}", self.ttl)?;
write_option_string!(f, "/{}", self.amount)
}
}
impl AnonymizingClone for SdpConnection {
fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
let mut masked = self.clone();
masked.address = anon.mask_typed_address(&self.address);
masked
}
}
/*
* RFC4566
* origin-field = %x6f "=" username SP sess-id SP sess-version SP
* nettype SP addrtype SP unicast-address CRLF
*/
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub struct SdpOrigin {
pub username: String,
pub session_id: u64,
pub session_version: u64,
pub unicast_addr: ExplicitlyTypedAddress,
}
impl fmt::Display for SdpOrigin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{username} {sess_id} {sess_vers} {unicast_addr}",
username = self.username,
sess_id = self.session_id,
sess_vers = self.session_version,
unicast_addr = self.unicast_addr
)
}
}
impl AnonymizingClone for SdpOrigin {
fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
let mut masked = self.clone();
masked.username = anon.mask_origin_user(&self.username);
masked.unicast_addr = anon.mask_typed_address(&masked.unicast_addr);
masked
}
}
/*
* RFC4566
* time-fields = 1*( %x74 "=" start-time SP stop-time
* *(CRLF repeat-fields) CRLF)
* [zone-adjustments CRLF]
*/
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub struct SdpTiming {
pub start: u64,
pub stop: u64,
}
impl fmt::Display for SdpTiming {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{start} {stop}", start = self.start, stop = self.stop)
}
}
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub enum SdpType {
// Note: Email, Information, Key, Phone, Repeat, Uri and Zone are left out
// on purposes as we don't want to support them.
Attribute(SdpAttribute),
Bandwidth(SdpBandwidth),
Connection(SdpConnection),
Media(SdpMediaLine),
Origin(SdpOrigin),
Session(String),
Timing(SdpTiming),
Version(u64),
}
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub struct SdpLine {
pub line_number: usize,
pub sdp_type: SdpType,
pub text: String,
}
/*
* RFC4566
* ; SDP Syntax
* session-description = proto-version
* origin-field
* session-name-field
* information-field
* uri-field
* email-fields
* phone-fields
* connection-field
* bandwidth-fields
* time-fields
* key-field
* attribute-fields
* media-descriptions
*/
#[derive(Clone)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
pub struct SdpSession {
pub version: u64,
pub origin: SdpOrigin,
pub session: Option<String>,
pub connection: Option<SdpConnection>,
pub bandwidth: Vec<SdpBandwidth>,
pub timing: Option<SdpTiming>,
pub attribute: Vec<SdpAttribute>,
pub media: Vec<SdpMedia>,
pub warnings: Vec<SdpParserError>, // unsupported values:
// information: Option<String>,
// uri: Option<String>,
// email: Option<String>,
// phone: Option<String>,
// repeat: Option<String>,
// zone: Option<String>,
// key: Option<String>
}
impl fmt::Display for SdpSession {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"v={version}\r\n\
o={origin}\r\n\
s={session}\r\n\
{timing}\
{bandwidth}\
{connection}\
{session_attributes}\
{media_sections}",
version = self.version,
origin = self.origin,
session = self.get_session_text(),
timing = option_to_string!("t={}\r\n", self.timing),
bandwidth = maybe_vector_to_string!("b={}\r\n", self.bandwidth, "\r\nb="),
connection = option_to_string!("c={}\r\n", self.connection),
session_attributes = maybe_vector_to_string!("a={}\r\n", self.attribute, "\r\na="),
media_sections = self.media.iter().map(|s| s.to_string()).collect::<String>(),
)
}
}
impl SdpSession {
pub fn new(version: u64, origin: SdpOrigin, session: String) -> SdpSession {
let session = match session.trim() {
s if !s.is_empty() => Some(s.to_owned()),
_ => None,
};
SdpSession {
version,
origin,
session,
connection: None,
bandwidth: Vec::new(),
timing: None,
attribute: Vec::new(),
media: Vec::new(),
warnings: Vec::new(),
}
}
pub fn get_version(&self) -> u64 {
self.version
}
pub fn get_origin(&self) -> &SdpOrigin {
&self.origin
}
pub fn get_session(&self) -> &Option<String> {
&self.session
}
pub fn get_session_text(&self) -> &str {
if let Some(text) = &self.session {
text.as_str()
} else {
" "
}
}
pub fn get_connection(&self) -> &Option<SdpConnection> {
&self.connection
}
pub fn set_connection(&mut self, c: SdpConnection) {
self.connection = Some(c)
}
pub fn add_bandwidth(&mut self, b: SdpBandwidth) {
self.bandwidth.push(b)
}
pub fn set_timing(&mut self, t: SdpTiming) {
self.timing = Some(t)
}
pub fn add_attribute(&mut self, a: SdpAttribute) -> Result<(), SdpParserInternalError> {
if !a.allowed_at_session_level() {
return Err(SdpParserInternalError::Generic(format!(
"{a} not allowed at session level"
)));
};
self.attribute.push(a);
Ok(())
}
pub fn extend_media(&mut self, v: Vec<SdpMedia>) {
self.media.extend(v)
}
pub fn parse_session_vector(&mut self, lines: &mut Vec<SdpLine>) -> Result<(), SdpParserError> {
while !lines.is_empty() {
let line = lines.remove(0);
match line.sdp_type {
SdpType::Attribute(a) => {
let _line_number = line.line_number;
self.add_attribute(a).map_err(|e: SdpParserInternalError| {
SdpParserError::Sequence {
message: format!("{e}"),
line_number: _line_number,
}
})?
}
SdpType::Bandwidth(b) => self.add_bandwidth(b),
SdpType::Timing(t) => self.set_timing(t),
SdpType::Connection(c) => self.set_connection(c),
SdpType::Origin(_) | SdpType::Session(_) | SdpType::Version(_) => {
return Err(SdpParserError::Sequence {
message: "version, origin or session at wrong level".to_string(),
line_number: line.line_number,
});
}
SdpType::Media(_) => {
return Err(SdpParserError::Sequence {
message: "media line not allowed in session parser".to_string(),
line_number: line.line_number,
});
}
}
}
Ok(())
}
pub fn get_attribute(&self, t: SdpAttributeType) -> Option<&SdpAttribute> {
self.attribute
.iter()
.find(|a| SdpAttributeType::from(*a) == t)
}
pub fn add_media(
&mut self,
media_type: SdpMediaValue,
direction: SdpAttribute,
port: u32,
protocol: SdpProtocolValue,
addr: ExplicitlyTypedAddress,
) -> Result<(), SdpParserInternalError> {
let mut media = SdpMedia::new(SdpMediaLine {
media: media_type,
port,
port_count: 1,
proto: protocol,
formats: SdpFormatList::Integers(Vec::new()),
});
media.add_attribute(direction)?;
media.set_connection(SdpConnection {
address: addr,
ttl: None,
amount: None,
});
self.media.push(media);
Ok(())
}
}
impl AnonymizingClone for SdpSession {
fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
let mut masked: SdpSession = SdpSession {
version: self.version,
session: self.session.clone(),
origin: self.origin.masked_clone(anon),
connection: self.connection.clone(),
timing: self.timing.clone(),
bandwidth: self.bandwidth.clone(),
attribute: Vec::new(),
media: Vec::new(),
warnings: Vec::new(),
};
masked.origin = self.origin.masked_clone(anon);
masked.connection = masked.connection.map(|con| con.masked_clone(anon));
for i in &self.attribute {
masked.attribute.push(i.masked_clone(anon));
}
masked
}
}
/* removing this wrap would not allow us to call this from the match statement inside
* parse_sdp_line() */
#[allow(clippy::unnecessary_wraps)]
fn parse_session(value: &str) -> Result<SdpType, SdpParserInternalError> {
trace!("session: {}", value);
Ok(SdpType::Session(String::from(value)))
}
fn parse_version(value: &str) -> Result<SdpType, SdpParserInternalError> {
let ver = value.parse::<u64>()?;
if ver != 0 {
return Err(SdpParserInternalError::Generic(format!(
"version type contains unsupported value {ver}"
)));
};
trace!("version: {}", ver);
Ok(SdpType::Version(ver))
}
fn parse_origin(value: &str) -> Result<SdpType, SdpParserInternalError> {
let mut tokens = value.split_whitespace();
let username = match tokens.next() {
None => {
return Err(SdpParserInternalError::Generic(
"Origin type is missing username token".to_string(),
));
}
Some(x) => x,
};
let session_id = match tokens.next() {
None => {
return Err(SdpParserInternalError::Generic(
"Origin type is missing session ID token".to_string(),
));
}
Some(x) => x.parse::<u64>()?,
};
let session_version = match tokens.next() {
None => {
return Err(SdpParserInternalError::Generic(
"Origin type is missing session version token".to_string(),
));
}
Some(x) => x.parse::<u64>()?,
};
match tokens.next() {
None => {
return Err(SdpParserInternalError::Generic(
"Origin type is missing network type token".to_string(),
));
}
Some(x) => parse_network_type(x)?,
};
let addrtype = match tokens.next() {
None => {
return Err(SdpParserInternalError::Generic(
"Origin type is missing address type token".to_string(),
));
}
Some(x) => parse_address_type(x)?,
};
let unicast_addr = match tokens.next() {
None => {
return Err(SdpParserInternalError::Generic(
"Origin type is missing IP address token".to_string(),
));
}
Some(x) => ExplicitlyTypedAddress::try_from((addrtype, x))?,
};
if addrtype != unicast_addr.address_type() {
return Err(SdpParserInternalError::Generic(
"Origin addrtype does not match address.".to_string(),
));
}
let o = SdpOrigin {
username: String::from(username),
session_id,
session_version,
unicast_addr,
};
trace!("origin: {}", o);
Ok(SdpType::Origin(o))
}
fn parse_connection(value: &str) -> Result<SdpType, SdpParserInternalError> {
let cv: Vec<&str> = value.split_whitespace().collect();
if cv.len() != 3 {
return Err(SdpParserInternalError::Generic(
"connection attribute must have three tokens".to_string(),
));
}
parse_network_type(cv[0])?;
let addrtype = parse_address_type(cv[1])?;
let mut ttl = None;
let mut amount = None;
let mut addr_token = cv[2];
if addr_token.find('/').is_some() {
let addr_tokens: Vec<&str> = addr_token.split('/').collect();
if addr_tokens.len() >= 3 {
amount = Some(addr_tokens[2].parse::<u32>()?);
}
ttl = Some(addr_tokens[1].parse::<u8>()?);
addr_token = addr_tokens[0];
}
let address = ExplicitlyTypedAddress::try_from((addrtype, addr_token))?;
let c = SdpConnection {
address,
ttl,
amount,
};
trace!("connection: {}", c);
Ok(SdpType::Connection(c))
}
fn parse_bandwidth(value: &str) -> Result<SdpType, SdpParserInternalError> {
let bv: Vec<&str> = value.split(':').collect();
if bv.len() != 2 {
return Err(SdpParserInternalError::Generic(
"bandwidth attribute must have two tokens".to_string(),
));
}
let bandwidth = bv[1].parse::<u32>()?;
let bw = match bv[0].to_uppercase().as_ref() {
"AS" => SdpBandwidth::As(bandwidth),
"CT" => SdpBandwidth::Ct(bandwidth),
"TIAS" => SdpBandwidth::Tias(bandwidth),
_ => SdpBandwidth::Unknown(String::from(bv[0]), bandwidth),
};
trace!("bandwidth: {}", bw);
Ok(SdpType::Bandwidth(bw))
}
fn parse_timing(value: &str) -> Result<SdpType, SdpParserInternalError> {
let tv: Vec<&str> = value.split_whitespace().collect();
if tv.len() != 2 {
return Err(SdpParserInternalError::Generic(
"timing attribute must have two tokens".to_string(),
));
}
let start = tv[0].parse::<u64>()?;
let stop = tv[1].parse::<u64>()?;
let t = SdpTiming { start, stop };
trace!("timing: {}", t);
Ok(SdpType::Timing(t))
}
pub fn parse_sdp_line(line: &str, line_number: usize) -> Result<SdpLine, SdpParserError> {
if line.find('=').is_none() {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("missing = character in line".to_string()),
line: line.to_string(),
line_number,
});
}
let mut splitted_line = line.splitn(2, '=');
let line_type = match splitted_line.next() {
None => {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("missing type".to_string()),
line: line.to_string(),
line_number,
});
}
Some(t) => {
let trimmed = t.trim();
if trimmed.len() > 1 {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("type too long".to_string()),
line: line.to_string(),
line_number,
});
}
if trimmed.is_empty() {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("type is empty".to_string()),
line: line.to_string(),
line_number,
});
}
trimmed.to_lowercase()
}
};
let (line_value, untrimmed_line_value) = match splitted_line.next() {
None => {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("missing value".to_string()),
line: line.to_string(),
line_number,
});
}
Some(v) => {
let trimmed = v.trim();
// For compatibility with sites that don't adhere to "s=-" for no session ID
if trimmed.is_empty() && line_type.as_str() != "s" {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("value is empty".to_string()),
line: line.to_string(),
line_number,
});
}
(trimmed, v)
}
};
match line_type.as_ref() {
"a" => parse_attribute(line_value),
"b" => parse_bandwidth(line_value),
"c" => parse_connection(line_value),
"e" => Err(SdpParserInternalError::Generic(format!(
"unsupported type email: {line_value}"
))),
"i" => Err(SdpParserInternalError::Generic(format!(
"unsupported type information: {line_value}"
))),
"k" => Err(SdpParserInternalError::Generic(format!(
"unsupported insecure key exchange: {line_value}"
))),
"m" => parse_media(line_value),
"o" => parse_origin(line_value),
"p" => Err(SdpParserInternalError::Generic(format!(
"unsupported type phone: {line_value}"
))),
"r" => Err(SdpParserInternalError::Generic(format!(
"unsupported type repeat: {line_value}"
))),
"s" => parse_session(untrimmed_line_value),
"t" => parse_timing(line_value),
"u" => Err(SdpParserInternalError::Generic(format!(
"unsupported type uri: {line_value}"
))),
"v" => parse_version(line_value),
"z" => Err(SdpParserInternalError::Generic(format!(
"unsupported type zone: {line_value}"
))),
_ => Err(SdpParserInternalError::Generic(
"unknown sdp type".to_string(),
)),
}
.map(|sdp_type| SdpLine {
line_number,
sdp_type,
text: line.to_owned(),
})
.map_err(|e| match e {
SdpParserInternalError::UnknownAddressType(..)
| SdpParserInternalError::AddressTypeMismatch { .. }
| SdpParserInternalError::Generic(..)
| SdpParserInternalError::Integer(..)
| SdpParserInternalError::Float(..)
| SdpParserInternalError::Domain(..)
| SdpParserInternalError::IpAddress(..) => SdpParserError::Line {
error: e,
line: line.to_string(),
line_number,
},
SdpParserInternalError::Unsupported(..) => SdpParserError::Unsupported {
error: e,
line: line.to_string(),
line_number,
},
})
}
fn sanity_check_sdp_session(session: &SdpSession) -> Result<(), SdpParserError> {
let make_seq_error = |x: &str| SdpParserError::Sequence {
message: x.to_string(),
line_number: 0,
};
if session.timing.is_none() {
return Err(make_seq_error("Missing timing type at session level"));
}
// Checks that all media have connections if there is no top level
// This explicitly allows for zero connection lines if there are no media
// sections for interoperability reasons.
let media_cons = &session.media.iter().all(|m| m.get_connection().is_some());
if !media_cons && session.get_connection().is_none() {
return Err(make_seq_error(
"Without connection type at session level all media sections must have connection types",
));
}
// Check that extmaps are not defined on session and media level
if session.get_attribute(SdpAttributeType::Extmap).is_some() {
for msection in &session.media {
if msection.get_attribute(SdpAttributeType::Extmap).is_some() {
return Err(make_seq_error(
"Extmap can't be define at session and media level",
));
}
}
}
for msection in &session.media {
if msection
.get_attribute(SdpAttributeType::RtcpMuxOnly)
.is_some()
&& msection.get_attribute(SdpAttributeType::RtcpMux).is_none()
{
return Err(make_seq_error(
"rtcp-mux-only media sections must also contain the rtcp-mux attribute",
));
}
let rids: Vec<&SdpAttributeRid> = msection
.get_attributes()
.iter()
.filter_map(|attr| match *attr {
SdpAttribute::Rid(ref rid) => Some(rid),
_ => None,
})
.collect();
let recv_rids: Vec<&str> = rids
.iter()
.filter_map(|rid| match rid.direction {
SdpSingleDirection::Recv => Some(rid.id.as_str()),
_ => None,
})
.collect();
let send_rids: Vec<&str> = rids
.iter()
.filter_map(|rid| match rid.direction {
SdpSingleDirection::Send => Some(rid.id.as_str()),
_ => None,
})
.collect();
for rid_format in rids.iter().flat_map(|rid| &rid.formats) {
match *msection.get_formats() {
SdpFormatList::Integers(ref int_fmt) => {
if !int_fmt.contains(&(u32::from(*rid_format))) {
return Err(make_seq_error(
"Rid pts must be declared in the media section",
));
}
}
SdpFormatList::Strings(ref str_fmt) => {
if !str_fmt.contains(&rid_format.to_string()) {
return Err(make_seq_error(
"Rid pts must be declared in the media section",
));
}
}
}
}
if let Some(SdpAttribute::Simulcast(simulcast)) =
msection.get_attribute(SdpAttributeType::Simulcast)
{
let check_defined_rids =
|simulcast_version_list: &Vec<SdpAttributeSimulcastVersion>,
rid_ids: &[&str]|
-> Result<(), SdpParserError> {
for simulcast_rid in simulcast_version_list.iter().flat_map(|x| &x.ids) {
if !rid_ids.contains(&simulcast_rid.id.as_str()) {
return Err(make_seq_error(
"Simulcast RIDs must be defined in any rid attribute",
));
}
}
Ok(())
};
check_defined_rids(&simulcast.receive, &recv_rids)?;
check_defined_rids(&simulcast.send, &send_rids)?;
}
}
Ok(())
}
fn parse_sdp_vector(lines: &mut Vec<SdpLine>) -> Result<SdpSession, SdpParserError> {
if lines.len() < 4 {
return Err(SdpParserError::Sequence {
message: "SDP neeeds at least 4 lines".to_string(),
line_number: 0,
});
}
let version = match lines.remove(0).sdp_type {
SdpType::Version(v) => v,
_ => {
return Err(SdpParserError::Sequence {
message: "first line needs to be version number".to_string(),
line_number: 0,
});
}
};
let origin = match lines.remove(0).sdp_type {
SdpType::Origin(v) => v,
_ => {
return Err(SdpParserError::Sequence {
message: "second line needs to be origin".to_string(),
line_number: 1,
});
}
};
let session = match lines.remove(0).sdp_type {
SdpType::Session(v) => v,
_ => {
return Err(SdpParserError::Sequence {
message: "third line needs to be session".to_string(),
line_number: 2,
});
}
};
let mut sdp_session = SdpSession::new(version, origin, session);
let _media_pos = lines
.iter()
.position(|l| matches!(l.sdp_type, SdpType::Media(_)));
match _media_pos {
Some(p) => {
let mut media: Vec<_> = lines.drain(p..).collect();
sdp_session.parse_session_vector(lines)?;
sdp_session.extend_media(parse_media_vector(&mut media)?);
}
None => sdp_session.parse_session_vector(lines)?,
};
sanity_check_sdp_session(&sdp_session)?;
Ok(sdp_session)
}
pub fn parse_sdp(sdp: &str, fail_on_warning: bool) -> Result<SdpSession, SdpParserError> {
if sdp.is_empty() {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("empty SDP".to_string()),
line: sdp.to_string(),
line_number: 0,
});
}
// see test_parse_sdp_minimal_sdp_successfully
if sdp.len() < 51 {
return Err(SdpParserError::Line {
error: SdpParserInternalError::Generic("string too short to be valid SDP".to_string()),
line: sdp.to_string(),
line_number: 0,
});
}
let lines = sdp.lines();
let mut errors: Vec<SdpParserError> = Vec::new();
let mut warnings: Vec<SdpParserError> = Vec::new();
let mut sdp_lines: Vec<SdpLine> = Vec::new();
for (line_number, line) in lines.enumerate() {
let stripped_line = line.trim();
if stripped_line.is_empty() {
continue;
}
match parse_sdp_line(line, line_number) {
Ok(n) => {
sdp_lines.push(n);
}
Err(e) => {
match e {
// TODO is this really a good way to accomplish this?
SdpParserError::Line {
error,
line,
line_number,
} => errors.push(SdpParserError::Line {
error,
line,
line_number,
}),
SdpParserError::Unsupported {
error,
line,
line_number,
} => {
warnings.push(SdpParserError::Unsupported {
error,
line,
line_number,
});
}
SdpParserError::Sequence {
message,
line_number,
} => errors.push(SdpParserError::Sequence {
message,
line_number,
}),
}
}
};
}
if fail_on_warning && (!warnings.is_empty()) {
return Err(warnings.remove(0));
}
// We just return the last of the errors here
if let Some(e) = errors.pop() {
return Err(e);
};
let mut session = parse_sdp_vector(&mut sdp_lines)?;
session.warnings = warnings;
for warning in &session.warnings {
warn!("Warning: {}", &warning);
}
Ok(session)
}
#[cfg(test)]
#[path = "./lib_tests.rs"]
mod tests;