///|
/// RFC 8866 line parser with bounded resource use and structured errors.
fn parse_origin_field(value : String, line : Int) -> Result[Origin, SdpError] {
let parts = value.split(" ").to_array()
if parts.length() != 6 {
return Err(
sdp_error(
"origin", "invalid-field", line, 3, "o-line", "origin requires six space-separated fields",
),
)
}
for part in parts {
if part.length() == 0 {
return Err(
sdp_error(
"origin", "invalid-spacing", line, 3, "o-line", "origin fields use one space separator",
),
)
}
}
if !valid_token(parts[0].to_owned()) ||
!valid_token(parts[1].to_owned()) ||
!valid_token(parts[2].to_owned()) ||
!valid_token(parts[3].to_owned()) ||
!valid_token(parts[4].to_owned()) ||
parts[5].length() == 0 {
return Err(
sdp_error(
"origin", "invalid-token", line, 3, "o-line", "origin contains an invalid token",
),
)
}
Ok(
origin(
parts[0].to_owned(),
parts[1].to_owned(),
parts[2].to_owned(),
parts[3].to_owned(),
parts[4].to_owned(),
parts[5].to_owned(),
),
)
}
///|
fn parse_connection_field(
value : String,
line : Int,
) -> Result[Connection, SdpError] {
let parts = value.split(" ").to_array()
if parts.length() != 3 {
return Err(
sdp_error(
"connection", "invalid-field", line, 3, "c-line", "connection requires network, address type and address",
),
)
}
let address_parts = parts[2].split("/").to_array()
if address_parts.length() == 0 || address_parts.length() > 3 {
return Err(
sdp_error(
"connection", "invalid-address", line, 3, "c-line", "invalid connection-address suffix",
),
)
}
let mut ttl : Int? = None
let mut count : Int? = None
if address_parts.length() >= 2 {
match parse_non_negative_int(address_parts[1].to_owned()) {
Some(value) => ttl = Some(value)
None =>
return Err(
sdp_error(
"connection", "invalid-ttl", line, 3, "c-line", "TTL must be a non-negative integer",
),
)
}
}
if address_parts.length() == 3 {
match parse_non_negative_int(address_parts[2].to_owned()) {
Some(value) => count = Some(value)
None =>
return Err(
sdp_error(
"connection", "invalid-count", line, 3, "c-line", "address count must be a non-negative integer",
),
)
}
}
Ok(
connection(
parts[0].to_owned(),
parts[1].to_owned(),
address_parts[0].to_owned(),
ttl,
count,
),
)
}
///|
fn parse_bandwidth_field(
value : String,
line : Int,
) -> Result[Bandwidth, SdpError] {
match value.split_once(":") {
None =>
Err(
sdp_error(
"bandwidth", "missing-colon", line, 3, "b-line", "bandwidth requires modifier:value",
),
)
Some(pair) => {
let modifier = pair.0.to_owned()
match parse_non_negative_int(pair.1.to_owned()) {
None =>
Err(
sdp_error(
"bandwidth", "invalid-value", line, 3, "b-line", "bandwidth value must be a non-negative integer",
),
)
Some(number) =>
if !valid_token(modifier) {
Err(
sdp_error(
"bandwidth", "invalid-modifier", line, 3, "b-line", "invalid bandwidth modifier",
),
)
} else {
Ok(bandwidth(modifier, number))
}
}
}
}
}
///|
fn parse_attribute_field(
value : String,
line : Int,
) -> Result[Attribute, SdpError] {
match value.split_once(":") {
None =>
if valid_attribute_name(value) {
Ok(attribute(value, None, line))
} else {
Err(
sdp_error(
"attribute", "invalid-name", line, 3, "a-line", "attribute name is invalid",
),
)
}
Some(pair) => {
let name = pair.0.to_owned()
if !valid_attribute_name(name) {
Err(
sdp_error(
"attribute", "invalid-name", line, 3, "a-line", "attribute name is invalid",
),
)
} else {
Ok(attribute(name, Some(pair.1.to_owned()), line))
}
}
}
}
///|
fn parse_timing_field(value : String, line : Int) -> Result[Timing, SdpError] {
let parts = value.split(" ").to_array()
if parts.length() != 2 || parts[0].length() == 0 || parts[1].length() == 0 {
Err(
sdp_error(
"timing", "invalid-field", line, 3, "t-line", "timing requires start and stop NTP values",
),
)
} else if parse_non_negative_int(parts[0].to_owned()) is None ||
parse_non_negative_int(parts[1].to_owned()) is None {
// RFC values can exceed this runtime's Int range. They remain strings,
// but the lexical digit check is performed independently below.
if all_digits(parts[0].to_owned()) && all_digits(parts[1].to_owned()) {
Ok(timing(parts[0].to_owned(), parts[1].to_owned()))
} else {
Err(
sdp_error(
"timing", "invalid-number", line, 3, "t-line", "timing values contain non-digits",
),
)
}
} else {
Ok(timing(parts[0].to_owned(), parts[1].to_owned()))
}
}
///|
fn all_digits(value : String) -> Bool {
if value.length() == 0 {
return false
}
for ch in value {
if !is_ascii_digit(ch) {
return false
}
}
true
}
///|
fn parse_media_field(
value : String,
line : Int,
limits : Limits,
) -> Result[MediaDescription, SdpError] {
let parts = value.split(" ").to_array()
if parts.length() < 4 {
return Err(
sdp_error(
"media", "invalid-field", line, 3, "m-line", "media requires media, port, protocol and at least one format",
),
)
}
for part in parts {
if part.length() == 0 {
return Err(
sdp_error(
"media", "invalid-spacing", line, 3, "m-line", "media fields use one space separator",
),
)
}
}
let port_parts = parts[1].split("/").to_array()
if port_parts.length() == 0 || port_parts.length() > 2 {
return Err(
sdp_error(
"media", "invalid-port", line, 3, "m-line", "media port syntax is invalid",
),
)
}
let port = match parse_non_negative_int(port_parts[0].to_owned()) {
None =>
return Err(
sdp_error(
"media", "invalid-port", line, 3, "m-line", "media port must be an integer",
),
)
Some(value) => value
}
let mut port_count = 1
if port_parts.length() == 2 {
match parse_non_negative_int(port_parts[1].to_owned()) {
None =>
return Err(
sdp_error(
"media", "invalid-port-count", line, 3, "m-line", "port count must be an integer",
),
)
Some(value) => port_count = value
}
}
let formats : Array[String] = Array::new()
for index = 3; index < parts.length(); index = index + 1 {
formats.push(parts[index].to_owned())
}
if formats.length() > limits.max_formats_per_media() {
return Err(
sdp_error(
"limit", "too-many-formats", line, 3, "m-line", "media format count exceeds configured limit",
),
)
}
Ok(
media_description(
parts[0].to_owned(),
port,
port_count,
parts[2].to_owned(),
formats,
),
)
}
///|
/// Parses SDP using compatibility defaults while preserving every source line.
pub fn parse(input : String) -> Result[SessionDescription, SdpError] {
match parse_with_options(input, compatible_options()) {
Ok(report) => Ok(report.document())
Err(error) => Err(error)
}
}
///|
/// Parses SDP using the strict RFC line-ending and obsolete-key policy.
pub fn parse_strict(input : String) -> Result[SessionDescription, SdpError] {
match parse_with_options(input, strict_options()) {
Ok(report) => Ok(report.document())
Err(error) => Err(error)
}
}
///|
/// Parses SDP with caller-selected limits and compatibility policy.
pub fn parse_with_options(
input : String,
options : ParseOptions,
) -> Result[ParseReport, SdpError] {
let active_limits = options.limits()
if byte_length(input) > active_limits.max_input_bytes() {
return Err(
sdp_error(
"limit", "input-too-large", 1, 1, "document", "input byte length exceeds configured limit",
),
)
}
if contains_illegal_control(input) {
return Err(
sdp_error(
"input", "control-character", 1, 1, "document", "input contains a forbidden control character",
),
)
}
if input.length() == 0 {
return Err(
sdp_error("input", "empty", 1, 1, "document", "SDP document is empty"),
)
}
let physical = input.split("\n").to_array()
let logical_count = if input.has_suffix("\n") {
physical.length() - 1
} else {
physical.length()
}
if logical_count > active_limits.max_lines() {
return Err(
sdp_error(
"limit",
"too-many-lines",
active_limits.max_lines() + 1,
1,
"document",
"line count exceeds configured limit",
),
)
}
if !options.allow_lf() && !input.has_suffix("\r\n") {
return Err(
sdp_error(
"input", "missing-crlf", logical_count, 1, "line-ending", "strict mode requires a final CRLF",
),
)
}
let document = empty_session(input)
let warnings : Array[SdpError] = Array::new()
let mut current_media : MediaDescription? = None
let mut saw_lf_only = false
let mut attribute_count = 0
for index = 0; index < logical_count; index = index + 1 {
let line_number = index + 1
let physical_line = physical[index].to_owned()
if !physical_line.has_suffix("\r") {
saw_lf_only = true
if !options.allow_lf() {
return Err(
sdp_error(
"input", "bare-lf", line_number, 1, "line-ending", "strict mode requires CRLF",
),
)
}
}
let raw = strip_one_cr(physical_line)
if byte_length(raw) > active_limits.max_line_bytes() {
return Err(
sdp_error(
"limit", "line-too-long", line_number, 1, "line", "line byte length exceeds configured limit",
),
)
}
let chars = raw.to_array()
if chars.length() < 2 || chars[1] != '=' {
return Err(
sdp_error(
"line", "invalid-shape", line_number, 1, "line", "line must have the form type=value",
),
)
}
if !is_ascii_alpha(chars[0]) || chars[0].to_int() > 127 {
return Err(
sdp_error(
"line", "invalid-type", line_number, 1, "line", "field type must be one ASCII letter",
),
)
}
let kind = String::from_array(chars[0:1])
let value = String::from_array(chars[2:])
let source = sdp_line(kind, value, line_number)
document.source_lines.push(source)
match chars[0] {
'v' =>
match parse_non_negative_int(value) {
Some(number) => document.set_version(number)
None =>
return Err(
sdp_error(
"version", "invalid-number", line_number, 3, "v-line", "version must contain digits",
),
)
}
'o' =>
match parse_origin_field(value, line_number) {
Ok(item) => document.set_origin(item)
Err(error) => return Err(error)
}
's' => document.set_session_name(value)
'i' =>
match current_media {
None => document.set_information(value)
Some(media) => media.set_information(value)
}
'u' => document.set_uri(value)
'e' => document.emails.push(value)
'p' => document.phones.push(value)
'c' =>
match parse_connection_field(value, line_number) {
Err(error) => return Err(error)
Ok(item) =>
match current_media {
None => document.set_connection(item)
Some(media) => media.connections.push(item)
}
}
'b' =>
match parse_bandwidth_field(value, line_number) {
Err(error) => return Err(error)
Ok(item) =>
match current_media {
None => document.bandwidths.push(item)
Some(media) => media.bandwidths.push(item)
}
}
't' =>
match current_media {
Some(_) =>
return Err(
sdp_error(
"order", "session-field-after-media", line_number, 1, "t-line", "timing is only valid at session level",
),
)
None =>
match parse_timing_field(value, line_number) {
Err(error) => return Err(error)
Ok(item) => {
if document.timings.length() >= active_limits.max_timings() {
return Err(
sdp_error(
"limit", "too-many-timings", line_number, 1, "t-line", "timing count exceeds configured limit",
),
)
}
document.timings.push(item)
}
}
}
'r' => {
if current_media is Some(_) || document.timings.length() == 0 {
return Err(
sdp_error(
"timing", "orphan-repeat", line_number, 1, "r-line", "repeat must follow a time field",
),
)
}
let timing_item = document.timings[document.timings.length() - 1]
if timing_item.repeats().length() >=
active_limits.max_repeats_per_timing() {
return Err(
sdp_error(
"limit", "too-many-repeats", line_number, 1, "r-line", "repeat count exceeds configured limit",
),
)
}
timing_item.repeats().push(value)
}
'z' => document.set_timezone_adjustments(value)
'k' => {
if !options.allow_obsolete_key() {
return Err(
sdp_error(
"key", "obsolete-field", line_number, 1, "k-line", "RFC 8866 marks k= obsolete",
),
)
}
match current_media {
None => document.set_encryption_key(value)
Some(_) => ()
}
warnings.push(
sdp_error(
"key", "obsolete-field", line_number, 1, "k-line", "obsolete k= field preserved",
),
)
}
'a' => {
attribute_count = attribute_count + 1
if attribute_count > active_limits.max_attributes() {
return Err(
sdp_error(
"limit", "too-many-attributes", line_number, 1, "a-line", "attribute count exceeds configured limit",
),
)
}
match parse_attribute_field(value, line_number) {
Err(error) => return Err(error)
Ok(item) =>
match current_media {
None => document.attributes.push(item)
Some(media) => media.attributes.push(item)
}
}
}
'm' => {
match current_media {
Some(media) => document.media_descriptions.push(media)
None => ()
}
if document.media_descriptions.length() >= active_limits.max_media() {
return Err(
sdp_error(
"limit", "too-many-media", line_number, 1, "m-line", "media count exceeds configured limit",
),
)
}
match parse_media_field(value, line_number, active_limits) {
Err(error) => return Err(error)
Ok(media) => {
media.source_lines.push(source)
current_media = Some(media)
}
}
}
_ =>
return Err(
sdp_error(
"line", "unknown-type", line_number, 1, "line", "unknown SDP field type",
),
)
}
match current_media {
Some(media) => if chars[0] != 'm' { media.source_lines.push(source) }
None => ()
}
}
match current_media {
Some(media) => document.media_descriptions.push(media)
None => ()
}
if saw_lf_only {
warnings.push(
sdp_error(
"input", "bare-lf", 1, 1, "line-ending", "LF-only input accepted in compatibility mode",
),
)
}
if document.origin() is None {
return Err(
sdp_error(
"required", "missing-origin", 1, 1, "document", "required o= field is missing",
),
)
}
if document.session_name().length() == 0 {
return Err(
sdp_error(
"required", "empty-session-name", 1, 1, "s-line", "session name must not be empty",
),
)
}
if document.timings().length() == 0 {
return Err(
sdp_error(
"required", "missing-timing", 1, 1, "document", "at least one t= field is required",
),
)
}
Ok(parse_report(document, warnings))
}