///|
/// Deterministic RFC-order writer and exact-source preserve writer.
fn write_line(sb : StringBuilder, kind : String, value : String) -> Unit {
sb.write_string(kind)
sb.write_char('=')
sb.write_string(value)
sb.write_string("\r\n")
}
///|
fn origin_value(item : Origin) -> String {
"\{item.username()} \{item.session_id()} \{item.session_version()} \{item.network_type()} \{item.address_type()} \{item.unicast_address()}"
}
///|
fn connection_value(item : Connection) -> String {
let sb = StringBuilder()
sb.write_string(item.network_type())
sb.write_char(' ')
sb.write_string(item.address_type())
sb.write_char(' ')
sb.write_string(item.address())
match item.ttl() {
Some(value) => {
sb.write_char('/')
sb.write_string(value.to_string())
}
None => ()
}
match item.address_count() {
Some(value) => {
sb.write_char('/')
sb.write_string(value.to_string())
}
None => ()
}
sb.to_string()
}
///|
fn write_bandwidth(sb : StringBuilder, item : Bandwidth) -> Unit {
write_line(sb, "b", "\{item.modifier()}:\{item.value()}")
}
///|
fn write_attribute(sb : StringBuilder, item : Attribute) -> Unit {
match item.value() {
None => write_line(sb, "a", ascii_lower(item.name()))
Some(value) => write_line(sb, "a", "\{ascii_lower(item.name())}:\{value}")
}
}
///|
fn write_media(sb : StringBuilder, media : MediaDescription) -> Unit {
let port = if media.port_count() == 1 {
media.port().to_string()
} else {
"\{media.port()}/\{media.port_count()}"
}
write_line(
sb,
"m",
"\{ascii_lower(media.media())} \{port} \{media.protocol()} \{join_with(media.formats(), " ")}",
)
match media.information() {
Some(value) => write_line(sb, "i", value)
None => ()
}
for item in media.connections() {
write_line(sb, "c", connection_value(item))
}
for item in media.bandwidths() {
write_bandwidth(sb, item)
}
for item in media.attributes() {
write_attribute(sb, item)
}
}
///|
/// Re-emits the exact source bytes supplied to the parser.
pub fn write_preserve(document : SessionDescription) -> String {
document.raw_source()
}
///|
/// Emits a deterministic CRLF document in RFC 8866 field order.
pub fn write_canonical(
document : SessionDescription,
) -> Result[String, SdpError] {
let origin_item = match document.origin() {
None =>
return Err(
sdp_error(
"writer", "missing-origin", 1, 1, "document", "cannot write a document without o=",
),
)
Some(value) => value
}
if document.session_name().length() == 0 {
return Err(
sdp_error(
"writer", "empty-session-name", 1, 1, "document", "cannot write an empty s=",
),
)
}
if document.timings().length() == 0 {
return Err(
sdp_error(
"writer", "missing-timing", 1, 1, "document", "cannot write a document without t=",
),
)
}
let sb = StringBuilder()
write_line(sb, "v", document.version().to_string())
write_line(sb, "o", origin_value(origin_item))
write_line(sb, "s", document.session_name())
match document.information() {
Some(value) => write_line(sb, "i", value)
None => ()
}
match document.uri() {
Some(value) => write_line(sb, "u", value)
None => ()
}
for value in document.emails() {
write_line(sb, "e", value)
}
for value in document.phones() {
write_line(sb, "p", value)
}
match document.connection() {
Some(value) => write_line(sb, "c", connection_value(value))
None => ()
}
for item in document.bandwidths() {
write_bandwidth(sb, item)
}
for item in document.timings() {
write_line(sb, "t", "\{item.start()} \{item.stop()}")
for repeat in item.repeats() {
write_line(sb, "r", repeat)
}
}
match document.timezone_adjustments() {
Some(value) => write_line(sb, "z", value)
None => ()
}
match document.encryption_key() {
Some(value) => write_line(sb, "k", value)
None => ()
}
for item in document.attributes() {
write_attribute(sb, item)
}
for media in document.media_descriptions() {
write_media(sb, media)
}
Ok(sb.to_string())
}
///|
/// Parses and canonicalizes in one operation.
pub fn canonicalize(input : String) -> Result[String, SdpError] {
match parse(input) {
Err(error) => Err(error)
Ok(document) => write_canonical(document)
}
}
///|
/// Checks canonical idempotence without relying on array equality.
pub fn canonical_is_idempotent(input : String) -> Bool {
match canonicalize(input) {
Err(_) => false
Ok(first) =>
match canonicalize(first) {
Err(_) => false
Ok(second) => first == second
}
}
}