use rsip::{Header, Uri};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DialogTriplet {
pub call_id: String,
pub local_tag: String,
pub remote_tag: String,
}
pub fn refer_to_header(target: &Uri) -> Header {
Header::Other("Refer-To".into(), format!("<{target}>"))
}
pub fn refer_to_with_replaces(target: &Uri, replaces: &DialogTriplet) -> Header {
let raw = format!(
"{};to-tag={};from-tag={}",
replaces.call_id, replaces.remote_tag, replaces.local_tag
);
let escaped = escape_uri_header_value(&raw);
Header::Other("Refer-To".into(), format!("<{target}?Replaces={escaped}>"))
}
fn escape_uri_header_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
pub fn parse_sipfrag_status(body: &[u8]) -> Option<u16> {
let text = std::str::from_utf8(body).ok()?;
let first_line = text.lines().next()?.trim();
let mut parts = first_line.split_whitespace();
let version = parts.next()?;
if !version.starts_with("SIP/") {
return None;
}
parts.next()?.parse::<u16>().ok()
}
pub fn is_final_sipfrag(code: u16) -> bool {
code >= 200
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refer_to_wraps_the_target_uri() {
let uri = Uri::try_from("sip:carol@example.com").unwrap();
let Header::Other(name, value) = refer_to_header(&uri) else {
panic!("expected Header::Other");
};
assert_eq!(name, "Refer-To");
assert_eq!(value, "<sip:carol@example.com>");
}
#[test]
fn refer_to_with_replaces_embeds_escaped_dialog() {
let uri = Uri::try_from("sip:bob@biloxi.example.com").unwrap();
let replaces = DialogTriplet {
call_id: "call-2@host".into(),
local_tag: "ustag".into(),
remote_tag: "bobtag".into(),
};
let Header::Other(name, value) = refer_to_with_replaces(&uri, &replaces) else {
panic!("expected Header::Other");
};
assert_eq!(name, "Refer-To");
assert_eq!(
value,
"<sip:bob@biloxi.example.com?Replaces=call-2%40host%3Bto-tag%3Dbobtag%3Bfrom-tag%3Dustag>"
);
}
#[test]
fn escape_leaves_unreserved_untouched() {
assert_eq!(escape_uri_header_value("aZ09-_.~"), "aZ09-_.~");
assert_eq!(escape_uri_header_value("a;b=c@d"), "a%3Bb%3Dc%40d");
}
#[test]
fn parses_final_and_provisional_sipfrag() {
assert_eq!(parse_sipfrag_status(b"SIP/2.0 100 Trying"), Some(100));
assert_eq!(parse_sipfrag_status(b"SIP/2.0 200 OK"), Some(200));
assert_eq!(
parse_sipfrag_status(b"SIP/2.0 486 Busy Here\r\n"),
Some(486)
);
assert_eq!(
parse_sipfrag_status(b" SIP/2.0 180 Ringing \r\n"),
Some(180)
);
}
#[test]
fn rejects_non_status_bodies() {
assert_eq!(parse_sipfrag_status(b""), None);
assert_eq!(parse_sipfrag_status(b"INVITE sip:x@y SIP/2.0"), None);
assert_eq!(parse_sipfrag_status(b"garbage"), None);
}
#[test]
fn finality_threshold_is_200() {
assert!(!is_final_sipfrag(100));
assert!(!is_final_sipfrag(180));
assert!(is_final_sipfrag(200));
assert!(is_final_sipfrag(486));
}
}