///|
/// RFC 8866 validation and interoperability diagnostics.
pub enum IssueSeverity {
Error
Warning
Info
}
///|
pub struct ValidationIssue {
severity : IssueSeverity
code : String
line : Int
scope : String
message : String
reference : String
}
///|
pub struct ValidationReport {
issues : Array[ValidationIssue]
}
///|
fn validation_issue(
severity : IssueSeverity,
code : String,
line : Int,
scope : String,
message : String,
reference : String,
) -> ValidationIssue {
{ severity, code, line, scope, message, reference }
}
///|
pub fn ValidationIssue::severity(self : ValidationIssue) -> IssueSeverity {
self.severity
}
///|
pub fn ValidationIssue::code(self : ValidationIssue) -> String {
self.code
}
///|
pub fn ValidationIssue::line(self : ValidationIssue) -> Int {
self.line
}
///|
pub fn ValidationIssue::scope(self : ValidationIssue) -> String {
self.scope
}
///|
pub fn ValidationIssue::message(self : ValidationIssue) -> String {
self.message
}
///|
pub fn ValidationIssue::reference(self : ValidationIssue) -> String {
self.reference
}
///|
pub fn IssueSeverity::to_string(self : IssueSeverity) -> String {
match self {
Error => "error"
Warning => "warning"
Info => "info"
}
}
///|
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ValidationIssue] {
self.issues
}
///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
self.issues.length()
}
///|
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
let mut count = 0
for issue in self.issues {
if issue.severity() is Error {
count = count + 1
}
}
count
}
///|
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
let mut count = 0
for issue in self.issues {
if issue.severity() is Warning {
count = count + 1
}
}
count
}
///|
pub fn ValidationReport::info_count(self : ValidationReport) -> Int {
let mut count = 0
for issue in self.issues {
if issue.severity() is Info {
count = count + 1
}
}
count
}
///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
self.error_count() == 0
}
///|
pub fn ValidationReport::has_code(
self : ValidationReport,
code : String,
) -> Bool {
for issue in self.issues {
if issue.code() == code {
return true
}
}
false
}
///|
fn push_issue(
issues : Array[ValidationIssue],
severity : IssueSeverity,
code : String,
line : Int,
scope : String,
message : String,
reference : String,
) -> Unit {
issues.push(validation_issue(severity, code, line, scope, message, reference))
}
///|
fn count_lines(lines : Array[SdpLine], kind : String) -> Int {
let mut count = 0
for line in lines {
if line.kind() == kind {
count = count + 1
}
}
count
}
///|
fn first_line(lines : Array[SdpLine], kind : String) -> Int {
for line in lines {
if line.kind() == kind {
return line.line()
}
}
1
}
///|
fn validate_required(
document : SessionDescription,
issues : Array[ValidationIssue],
) -> Unit {
if document.version() != 0 {
push_issue(
issues,
Error,
"SDP_VERSION_NOT_ZERO",
first_line(document.source_lines(), "v"),
"session",
"RFC 8866 defines protocol version zero",
"RFC 8866 §5.1",
)
}
for kind in ["v", "o", "s"] {
let count = count_lines(document.source_lines(), kind)
if count != 1 {
push_issue(
issues,
Error,
"SDP_SINGLETON_\{kind.to_upper()}",
1,
"session",
"field \{kind}= must appear exactly once",
"RFC 8866 §5",
)
}
}
if document.timings().length() == 0 {
push_issue(
issues,
Error,
"SDP_MISSING_TIME",
1,
"session",
"at least one time description is required",
"RFC 8866 §5",
)
}
if document.session_name().length() == 0 {
push_issue(
issues,
Error,
"SDP_EMPTY_SESSION_NAME",
first_line(document.source_lines(), "s"),
"session",
"the session name must not be empty",
"RFC 8866 §5.3",
)
}
match document.origin() {
None => ()
Some(item) => {
if !all_digits(item.session_id()) || !all_digits(item.session_version()) {
push_issue(
issues,
Error,
"SDP_ORIGIN_NUMERIC",
first_line(document.source_lines(), "o"),
"session",
"origin session-id and session-version must be decimal integers",
"RFC 8866 §5.2",
)
}
if item.network_type() != "IN" {
push_issue(
issues,
Warning,
"SDP_UNKNOWN_NETTYPE",
first_line(document.source_lines(), "o"),
"session",
"origin network type is not registered as IN",
"IANA SDP Parameters",
)
}
if item.address_type() != "IP4" && item.address_type() != "IP6" {
push_issue(
issues,
Warning,
"SDP_UNKNOWN_ADDRTYPE",
first_line(document.source_lines(), "o"),
"session",
"origin address type is not a core registered value",
"IANA SDP Parameters",
)
}
}
}
}
///|
fn rank_session_field(kind : String) -> Int {
match kind {
"v" => 0
"o" => 1
"s" => 2
"i" => 3
"u" => 4
"e" => 5
"p" => 6
"c" => 7
"b" => 8
"t" => 9
"r" => 10
"z" => 11
"k" => 12
"a" => 13
"m" => 14
_ => 99
}
}
///|
fn rank_media_field(kind : String) -> Int {
match kind {
"m" => 0
"i" => 1
"c" => 2
"b" => 3
"k" => 4
"a" => 5
_ => 99
}
}
///|
fn validate_order(
document : SessionDescription,
issues : Array[ValidationIssue],
) -> Unit {
let mut in_media = false
let mut rank = -1
for line in document.source_lines() {
if line.kind() == "m" {
in_media = true
rank = 0
continue
}
let current = if in_media {
rank_media_field(line.kind())
} else {
rank_session_field(line.kind())
}
if current < rank && !(line.kind() == "r" && rank == 10) {
push_issue(
issues,
Error,
"SDP_FIELD_ORDER",
line.line(),
if in_media {
"media"
} else {
"session"
},
"field appears outside RFC 8866 ordering",
"RFC 8866 §5",
)
}
if current > rank {
rank = current
}
}
}
///|
fn validate_connection(
item : Connection,
line : Int,
scope : String,
issues : Array[ValidationIssue],
) -> Unit {
if item.network_type() != "IN" {
push_issue(
issues,
Warning,
"SDP_UNKNOWN_NETTYPE",
line,
scope,
"connection network type is not registered as IN",
"IANA SDP Parameters",
)
}
if item.address_type() != "IP4" && item.address_type() != "IP6" {
push_issue(
issues,
Warning,
"SDP_UNKNOWN_ADDRTYPE",
line,
scope,
"connection address type is not IP4 or IP6",
"IANA SDP Parameters",
)
}
match item.ttl() {
Some(value) =>
if value > 255 {
push_issue(
issues,
Error,
"SDP_TTL_RANGE",
line,
scope,
"IPv4 multicast TTL must be in the range 0..255",
"RFC 8866 §5.7",
)
}
None => ()
}
match item.address_count() {
Some(value) =>
if value < 1 {
push_issue(
issues,
Error,
"SDP_ADDRESS_COUNT",
line,
scope,
"address count must be at least one",
"RFC 8866 §5.7",
)
}
None => ()
}
}
///|
fn validate_bandwidths(
items : Array[Bandwidth],
line : Int,
scope : String,
issues : Array[ValidationIssue],
) -> Unit {
let seen : Array[String] = Array::new()
for item in items {
let key = item.modifier().to_upper()
if string_in(seen, key) {
push_issue(
issues,
Warning,
"SDP_DUPLICATE_BANDWIDTH",
line,
scope,
"bandwidth modifier appears more than once",
"RFC 8866 §5.8",
)
} else {
seen.push(key)
}
if !is_registered_bandwidth(item.modifier()) {
push_issue(
issues,
Info,
"SDP_EXTENSION_BANDWIDTH",
line,
scope,
"unregistered bandwidth modifier is treated as an extension",
"IANA SDP Parameters",
)
}
}
}
///|
fn validate_attributes(
items : Array[Attribute],
formats : Array[String],
scope : String,
issues : Array[ValidationIssue],
) -> Unit {
let directions = count_attribute_named(items, "sendrecv") +
count_attribute_named(items, "sendonly") +
count_attribute_named(items, "recvonly") +
count_attribute_named(items, "inactive")
if directions > 1 {
push_issue(
issues,
Error,
"SDP_MULTIPLE_DIRECTIONS",
1,
scope,
"only one direction attribute may apply at a level",
"RFC 3264 §5",
)
}
let rtp_payloads : Array[String] = Array::new()
let mids : Array[String] = Array::new()
for item in items {
if !is_registered_attribute(item.name()) {
push_issue(
issues,
Info,
"SDP_EXTENSION_ATTRIBUTE",
item.line(),
scope,
"unknown attribute is preserved as an extension",
"RFC 8866 §5.13",
)
}
if item.name().equal_ignore_ascii_case("rtcp-mux") &&
item.value() is Some(_) {
push_issue(
issues,
Error,
"SDP_RTCP_MUX_VALUE",
item.line(),
scope,
"rtcp-mux is a property attribute without a value",
"RFC 5761 §5.1",
)
}
if item.name().equal_ignore_ascii_case("rtpmap") {
match parse_rtpmap(item) {
Err(error) =>
push_issue(
issues,
Error,
"SDP_INVALID_RTPMAP",
item.line(),
scope,
error.message(),
"RFC 8866 §6.6",
)
Ok(mapping) => {
let key = mapping.payload_type().to_string()
if string_in(rtp_payloads, key) {
push_issue(
issues,
Error,
"SDP_DUPLICATE_RTPMAP",
item.line(),
scope,
"payload type has more than one rtpmap",
"RFC 8866 §6.6",
)
} else {
rtp_payloads.push(key)
}
if formats.length() > 0 && !string_in(formats, key) {
push_issue(
issues,
Error,
"SDP_RTPMAP_NOT_ADVERTISED",
item.line(),
scope,
"rtpmap payload is absent from the m= format list",
"RFC 8866 §6.6",
)
}
if mapping.payload_type() < 0 || mapping.payload_type() > 127 {
push_issue(
issues,
Error,
"SDP_RTP_PAYLOAD_RANGE",
item.line(),
scope,
"RTP payload type must be in the range 0..127",
"RFC 3551 §6",
)
}
if mapping.clock_rate() == 0 {
push_issue(
issues,
Error,
"SDP_RTPMAP_CLOCK_ZERO",
item.line(),
scope,
"rtpmap clock rate must be positive",
"RFC 8866 §6.6",
)
}
}
}
}
if item.name().equal_ignore_ascii_case("fmtp") {
match parse_fmtp(item) {
Err(error) =>
push_issue(
issues,
Error,
"SDP_INVALID_FMTP",
item.line(),
scope,
error.message(),
"RFC 8866 §6.6",
)
Ok(mapping) =>
if formats.length() > 0 && !string_in(formats, mapping.format()) {
push_issue(
issues,
Error,
"SDP_FMTP_NOT_ADVERTISED",
item.line(),
scope,
"fmtp format is absent from the m= format list",
"RFC 8866 §6.6",
)
}
}
}
if item.name().equal_ignore_ascii_case("mid") {
match item.value() {
None =>
push_issue(
issues,
Error,
"SDP_MID_EMPTY",
item.line(),
scope,
"mid requires a value",
"RFC 5888 §4",
)
Some(value) =>
if string_in(mids, value) {
push_issue(
issues,
Error,
"SDP_DUPLICATE_MID",
item.line(),
scope,
"mid value is duplicated within this level",
"RFC 5888 §4",
)
} else {
mids.push(value)
}
}
}
}
}
///|
fn validate_media(
document : SessionDescription,
media : MediaDescription,
index : Int,
issues : Array[ValidationIssue],
) -> Unit {
let scope = "media[\{index}]"
if media.port() < 0 || media.port() > 65535 {
push_issue(
issues,
Error,
"SDP_PORT_RANGE",
1,
scope,
"media port must be in the range 0..65535",
"RFC 8866 §5.14",
)
}
if media.port_count() < 1 {
push_issue(
issues,
Error,
"SDP_PORT_COUNT",
1,
scope,
"media port count must be positive",
"RFC 8866 §5.14",
)
}
if !is_registered_media(media.media()) {
push_issue(
issues,
Info,
"SDP_EXTENSION_MEDIA",
1,
scope,
"media token is not in the IANA core registry",
"IANA SDP Parameters",
)
}
if !is_registered_protocol(media.protocol()) {
push_issue(
issues,
Warning,
"SDP_UNKNOWN_PROTOCOL",
1,
scope,
"transport protocol is not in the bundled IANA snapshot",
"IANA SDP Parameters",
)
}
if document.connection() is None && media.connections().length() == 0 {
push_issue(
issues,
Error,
"SDP_MISSING_CONNECTION",
1,
scope,
"connection information is required at session or media level",
"RFC 8866 §5.7",
)
}
for item in media.connections() {
validate_connection(item, 1, scope, issues)
}
validate_bandwidths(media.bandwidths(), 1, scope, issues)
validate_attributes(media.attributes(), media.formats(), scope, issues)
if is_rtp_protocol(media.protocol()) {
for format in media.formats() {
match parse_non_negative_int(format) {
None =>
push_issue(
issues,
Error,
"SDP_NON_NUMERIC_RTP_FORMAT",
1,
scope,
"RTP media formats must be numeric payload types",
"RFC 8866 §5.14",
)
Some(payload) =>
if payload > 127 {
push_issue(
issues,
Error,
"SDP_RTP_PAYLOAD_RANGE",
1,
scope,
"RTP payload type must be in the range 0..127",
"RFC 3551 §6",
)
}
}
}
}
}
///|
/// Performs syntax-independent RFC and registry validation.
pub fn validate(document : SessionDescription) -> ValidationReport {
let issues : Array[ValidationIssue] = Array::new()
validate_required(document, issues)
validate_order(document, issues)
match document.connection() {
Some(item) => validate_connection(item, 1, "session", issues)
None => ()
}
validate_bandwidths(document.bandwidths(), 1, "session", issues)
validate_attributes(document.attributes(), Array::new(), "session", issues)
for index, media in document.media_descriptions() {
validate_media(document, media, index, issues)
}
{ issues, }
}
///|
/// Parses and validates, returning parse errors separately from diagnostics.
pub fn parse_and_validate(input : String) -> Result[ValidationReport, SdpError] {
match parse(input) {
Err(error) => Err(error)
Ok(document) => Ok(validate(document))
}
}
///|
/// A deterministic human-readable validation report.
pub fn ValidationReport::to_text(self : ValidationReport) -> String {
let sb = StringBuilder()
sb.write_string("validation: errors=")
sb.write_string(self.error_count().to_string())
sb.write_string(" warnings=")
sb.write_string(self.warning_count().to_string())
sb.write_string(" info=")
sb.write_string(self.info_count().to_string())
for issue in self.issues {
sb.write_string("\n[")
sb.write_string(issue.severity().to_string())
sb.write_string("] ")
sb.write_string(issue.code())
sb.write_string(" ")
sb.write_string(issue.scope())
sb.write_string(": ")
sb.write_string(issue.message())
}
sb.to_string()
}