use rsiprtp::sip::parser::Message as OurMessage;
use rsiprtp::sip::SipUri;
#[path = "parser_diff_oracle/mod.rs"]
mod oracle;
use oracle::{
assert_both_reject, assert_equivalent, normalize_value, ours_contact_diff, ours_cseq_diff,
ours_from_to_diff, ours_to_diff, ours_to_to_diff, ours_via_diff, rsip_contact_diff,
rsip_cseq_diff, rsip_from_to_diff, rsip_to_diff, rsip_to_to_diff, rsip_via_diff,
unquote_display_name, DiffContact,
};
#[test]
fn diff_mdsiprtp3_invite_with_via() {
let bytes = include_bytes!("fixtures/mdsiprtp3/invite_with_via.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_mdsiprtp3_response_200_ok() {
let bytes = include_bytes!("fixtures/mdsiprtp3/response_200_ok.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_mdsiprtp3_invite_with_body() {
let bytes = include_bytes!("fixtures/mdsiprtp3/invite_with_body.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_handcrafted_register_with_contact() {
let bytes = include_bytes!("fixtures/handcrafted/register_with_contact.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_handcrafted_invite_compact_via() {
let bytes = include_bytes!("fixtures/handcrafted/invite_compact_via.sip");
assert_equivalent(bytes);
}
#[test]
#[ignore = "rsip 0.4 rejects RFC 3261 §7.3.1 line folding; our parser correctly accepts it"]
fn diff_handcrafted_invite_folded_subject() {
let bytes = include_bytes!("fixtures/handcrafted/invite_folded_subject.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_handcrafted_response_407_with_proxy_authenticate() {
let bytes = include_bytes!("fixtures/handcrafted/response_407_with_proxy_authenticate.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_handcrafted_ack_for_2xx() {
let bytes = include_bytes!("fixtures/handcrafted/ack_for_2xx.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_handcrafted_cancel() {
let bytes = include_bytes!("fixtures/handcrafted/cancel.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_handcrafted_response_with_multi_via() {
let bytes = include_bytes!("fixtures/handcrafted/response_with_multi_via.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_wsinv_rsip_rejects() {
let bytes: &[u8] = include_bytes!("fixtures/rfc4475/wsinv.sip");
let rs = rsip::SipMessage::try_from(bytes);
assert!(
rs.is_err(),
"rsip 0.4 rejects RFC 4475 §3.1.1.1 wsinv torture test; \
got Ok({rs:?}) — update this test if rsip changed",
);
let ours = OurMessage::parse(bytes);
assert!(
ours.is_ok(),
"our parser must accept RFC 4475 §3.1.1.1 wsinv; got Err({ours:?})",
);
}
#[test]
fn diff_rfc4475_intmeth_both_reject() {
let bytes = include_bytes!("fixtures/rfc4475/intmeth.sip");
assert_both_reject("rfc4475_intmeth", bytes);
}
#[test]
fn diff_rfc4475_esc01_rsip_rejects_folding() {
let bytes: &[u8] = include_bytes!("fixtures/rfc4475/esc01.sip");
let rs = rsip::SipMessage::try_from(bytes);
assert!(
rs.is_err(),
"rsip 0.4 rejects RFC 4475 §3.1.1.3 esc01 (canonical bytes \
line-fold the Contact header); got Ok({rs:?}) — update this \
test if rsip changed",
);
let ours = OurMessage::parse(bytes);
assert!(
ours.is_ok(),
"our parser must accept RFC 4475 §3.1.1.3 esc01; got \
Err({ours:?})",
);
}
#[test]
fn diff_rfc4475_escnull() {
let bytes = include_bytes!("fixtures/rfc4475/escnull.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_esc02() {
let bytes = include_bytes!("fixtures/rfc4475/esc02.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_lwsdisp() {
let bytes = include_bytes!("fixtures/rfc4475/lwsdisp.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_longreq_rsip_rejects_hcolon_whitespace() {
let bytes: &[u8] = include_bytes!("fixtures/rfc4475/longreq.sip");
let rs = rsip::SipMessage::try_from(bytes);
assert!(
rs.is_err(),
"rsip 0.4 rejects RFC 4475 §3.1.1.7 longreq (canonical bytes \
carry HCOLON-with-interior-whitespace forms in the Via \
stack); got Ok({rs:?}) — update this test if rsip changed",
);
let ours = OurMessage::parse(bytes);
assert!(
ours.is_ok(),
"our parser must accept RFC 4475 §3.1.1.7 longreq; got \
Err({ours:?})",
);
}
#[test]
fn diff_rfc4475_dblreq_rsip_keeps_trailing() {
let bytes: &[u8] = include_bytes!("fixtures/rfc4475/dblreq.sip");
let ours = OurMessage::parse(bytes).expect("our parser accepts dblreq");
let our_body: &Vec<u8> = match &ours {
OurMessage::Request(r) => &r.body,
OurMessage::Response(r) => &r.body,
};
assert!(
our_body.is_empty(),
"ours: Content-Length: 0 → body must be empty; got {} bytes",
our_body.len(),
);
let rs = rsip::SipMessage::try_from(bytes).expect("rsip accepts dblreq framing");
let rs_body = match &rs {
rsip::SipMessage::Request(r) => &r.body,
rsip::SipMessage::Response(r) => &r.body,
};
assert!(
!rs_body.is_empty(),
"rsip 0.4 captures trailing octets as body; if it now \
truncates per Content-Length, this pin can be removed.",
);
}
#[test]
fn diff_rfc4475_semiuri_rsip_rejects() {
let bytes: &[u8] = include_bytes!("fixtures/rfc4475/semiuri.sip");
let ours = OurMessage::parse(bytes).expect("ours accepts semiuri");
if let OurMessage::Request(r) = &ours {
let uri = SipUri::parse(&r.uri).expect("our URI parses");
assert_eq!(uri.user(), Some("user;par=u%40example.net"));
assert_eq!(uri.host(), "example.com");
} else {
panic!("expected request");
}
let rs = rsip::SipMessage::try_from(bytes);
assert!(
rs.is_err(),
"rsip 0.4 rejects URI with `;` in user part; got Ok({rs:?}) \
— update this test if rsip changed",
);
}
#[test]
fn diff_rfc4475_transports_rsip_rejects_unknown_transport() {
let bytes: &[u8] = include_bytes!("fixtures/rfc4475/transports.sip");
let _ours_msg = OurMessage::parse(bytes).expect("ours frames transports");
let _rsip_msg = rsip::SipMessage::try_from(bytes).expect("rsip frames transports");
let unknown_via = "SIP/2.0/TUNA t6.example.com;branch=z9hG4bK6";
let r = rsip_via_diff(unknown_via);
assert!(
r.is_err(),
"rsip 0.4 rejects unknown transport in typed Via; got \
Ok({r:?}) — update this test if rsip changed",
);
let d = ours_via_diff(unknown_via).expect("ours typed-Via accepts unknown transport");
assert_eq!(d.transport, "TUNA");
}
#[test]
fn diff_rfc4475_mpart01() {
let bytes = include_bytes!("fixtures/rfc4475/mpart01.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_unreason() {
let bytes = include_bytes!("fixtures/rfc4475/unreason.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_noreason() {
let bytes = include_bytes!("fixtures/rfc4475/noreason.sip");
assert_equivalent(bytes);
}
#[test]
fn diff_rfc4475_invalid_no_version() {
let bytes = include_bytes!("fixtures/rfc4475_invalid/badaspec_no_version.sip");
assert_both_reject("badaspec_no_version", bytes);
}
#[test]
fn diff_rfc4475_invalid_garbage_start() {
let bytes = include_bytes!("fixtures/rfc4475_invalid/badaspec_garbage_start.sip");
assert_both_reject("badaspec_garbage_start", bytes);
}
#[test]
fn diff_request_line_with_non_sip_uri_rsip_accepts_we_reject() {
let bytes: &[u8] = b"INVITE http://x SIP/2.0\r\nCall-ID: x\r\nCSeq: 1 INVITE\r\n\
From: <sip:a>\r\nTo: <sip:b>\r\nVia: SIP/2.0/UDP h\r\n\
Max-Forwards: 70\r\nContent-Length: 0\r\n\r\n";
let rs = rsip::SipMessage::try_from(bytes);
assert!(
rs.is_ok(),
"rsip 0.4 accepts non-SIP Request-URIs as Scheme::Other; \
got Err({rs:?}) — update this test if rsip tightened",
);
let ours = OurMessage::parse(bytes);
assert!(
ours.is_err(),
"our framer must reject non-SIP Request-URI to keep \
SipRequest::uri() panic-free; got Ok({ours:?})",
);
}
#[test]
fn typed_status_line_sip1_x_version_rsip_accepts_we_reject() {
let bytes = b"SIP/1.0 200 OK\r\nCall-ID: x\r\nCSeq: 1 INVITE\r\n\
From: <sip:a>\r\nTo: <sip:b>\r\nVia: SIP/2.0/UDP h\r\n\
Content-Length: 0\r\n\r\n";
let rs = rsip::SipMessage::try_from(&bytes[..]);
assert!(
rs.is_ok(),
"rsip 0.4 accepts SIP/1.0 status lines; got Err({rs:?}) — \
update this test if rsip stops accepting non-2.0 versions",
);
let ours = OurMessage::parse(bytes);
assert!(
ours.is_err(),
"our parser must reject non-SIP/2.0 per RFC 3261 §7.1; \
got Ok({ours:?})",
);
}
#[test]
fn body_leading_crlf_rsip_strips_we_preserve() {
let bytes = b"SIP/2.0 200 OK\r\n\r\n\r\nHi!";
let rs = rsip_to_diff(bytes).expect("rsip should accept");
let ours = ours_to_diff(bytes).expect("ours should accept");
assert_eq!(
ours.body, b"\r\nHi!",
"ours preserves leading CRLF in body (RFC 3261 §7.5)",
);
assert_eq!(
rs.body, b"Hi!",
"rsip 0.4 strips the leading CRLF — update this test if rsip \
stops stripping leading body CRLFs",
);
assert!(
rs.body.len() < ours.body.len(),
"rsip body must be strictly shorter than ours (the divergence \
we're pinning); got rsip={:?} ours={:?}",
rs.body,
ours.body,
);
}
#[test]
fn header_missing_colon_rsip_accepts_we_reject() {
let bytes = b"SIP/2.0 202 \nNotAHeader\r\n\r\n\xac ";
let rs = oracle::rsip_to_diff(bytes);
let ours = oracle::ours_to_diff(bytes);
assert!(rs.is_ok(), "rsip should accept (its real behavior)");
let err = ours.expect_err("ours should reject");
assert!(
err.contains("missing ':'"),
"ours error should mention missing colon (proxy for the \
bare-LF-into-reason rsip bug); got: {err}",
);
}
#[test]
fn status_line_reason_ctl_byte_rsip_accepts_we_reject() {
let bytes: &[u8] = b"SIP/2.0 200 ab\rcd\r\n\r\n";
let rs = oracle::rsip_to_diff(bytes);
let ours = oracle::ours_to_diff(bytes);
assert!(
rs.is_ok(),
"rsip should accept (lenient §25.1): {:?}",
rs.err()
);
let err = ours.expect_err("ours should reject CTL byte in reason");
assert!(
err.contains("reason phrase contains forbidden control byte"),
"ours error should mention CTL byte in reason; got: {err}",
);
}
#[test]
fn header_section_contains_nul_rsip_rejects_we_accept() {
let mut bytes = b"SIP/2.0 200 OK\r\nFo".to_vec();
bytes.push(0); bytes.extend_from_slice(b"o: bar\r\nCall-ID: x\r\nCSeq: 1 INVITE\r\n");
bytes.extend_from_slice(b"From: <sip:a>\r\nTo: <sip:b>\r\nVia: SIP/2.0/UDP h\r\n");
bytes.extend_from_slice(b"Content-Length: 0\r\n\r\n");
let rs = oracle::rsip_to_diff(&bytes);
let ours = oracle::ours_to_diff(&bytes);
assert!(rs.is_err(), "rsip should reject NUL in header NAME");
let rs_err = rs.unwrap_err();
assert!(
rs_err.contains("Tokenizer error"),
"rsip rejection should be Tokenizer-class (the heuristic skip \
in parser_diff_oracle keys off this); got: {rs_err}",
);
assert!(
ours.is_ok(),
"we accept per OCTET grammar / M2-A permissive policy",
);
}
#[test]
fn status_line_missing_sp_after_code_both_reject() {
let bytes = b"SIP/2.0 202\r\nCall-ID: x\r\nCSeq: 1 INVITE\r\n\
From: <sip:a>\r\nTo: <sip:b>\r\nVia: SIP/2.0/UDP h\r\n\
Content-Length: 0\r\n\r\n";
assert_both_reject("status_line_missing_sp_after_code", bytes);
}
#[test]
fn body_starts_with_header_like_line_rsip_misinterprets() {
let bytes: &[u8] = b"SIP/2.0 202 \n\n6.55554\r\n4:::::::::::*\r\n\x00\x00\x00z*4(\r\n@\nTT";
let rs = oracle::rsip_to_diff(bytes).expect("rsip should accept");
let ours = oracle::ours_to_diff(bytes).expect("ours should accept");
assert_eq!(rs.kind, ours.kind, "both parsers see Response 202");
assert_eq!(
rs.headers.len(),
1,
"rsip absorbs bare-LF prefix into reason and parses one header",
);
assert_eq!(rs.headers[0].0, "4", "rsip's single header is name=\"4\"",);
assert_eq!(rs.body.len(), 13, "rsip's body is 13 bytes after CRLFCRLF");
assert!(
ours.headers.is_empty(),
"ours splits at the LFLF after the status line, no headers",
);
assert_eq!(
ours.body.len(),
37,
"ours's body is the prefix + the 13 trailing bytes (24 + 13)",
);
assert_eq!(
&ours.body[ours.body.len() - rs.body.len()..],
&rs.body[..],
"ours's body trails with the same 13 bytes rsip sees as body",
);
}
#[test]
fn diff_fuzz_corpus() {
let corpus_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("fuzz")
.join("sip_message_parse")
.join("corpus");
if !corpus_dir.exists() {
return;
}
let entries: Vec<_> = std::fs::read_dir(&corpus_dir)
.expect("corpus dir exists per check above")
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.collect();
for entry in entries {
let path = entry.path();
let bytes = std::fs::read(&path)
.unwrap_or_else(|e| panic!("read fuzz corpus file {:?}: {}", path, e));
let result = std::panic::catch_unwind(|| assert_equivalent(&bytes));
if let Err(payload) = result {
std::panic::resume_unwind(payload);
}
}
}
#[test]
fn header_value_with_bare_lf_rsip_accepts_we_reject() {
let bytes = b"SIP/2.0 200 OK\r\n6.~: 5\r-\n!S-*\x01\x0c~~~*\x0b* S54\r\n\r\n";
let rs = oracle::rsip_to_diff(bytes);
let ours = oracle::ours_to_diff(bytes);
assert!(rs.is_ok(), "rsip should accept (its real behavior)");
let err = ours.expect_err("ours should reject");
assert!(
err.contains("missing ':'"),
"ours error should mention missing colon (proxy for the \
bare-LF-into-value rsip bug); got: {err}",
);
}
#[test]
fn lflf_separator_only_rsip_rejects_we_accept() {
let bytes = b"SIP/2.0 200 OK\n\n\xb8\xb8\xb8";
let rs = oracle::rsip_to_diff(bytes);
let ours = oracle::ours_to_diff(bytes);
let rs_err = rs.expect_err("rsip should reject (no CRLFCRLF and high-bit body)");
assert!(
rs_err.contains("Tokenizer error"),
"rsip rejection should be Tokenizer-class (the new LFLF \
skip in parser_diff_oracle keys off this); got: {rs_err}",
);
let ours = ours.expect("ours should accept (LFLF fallback per M2-A)");
assert_eq!(
ours.body, b"\xb8\xb8\xb8",
"ours surfaces the post-LFLF bytes as body",
);
assert!(
!bytes.windows(4).any(|w| w == b"\r\n\r\n"),
"trigger requires no CRLFCRLF",
);
assert!(
bytes.windows(2).any(|w| w == b"\n\n"),
"trigger requires LFLF",
);
}
#[test]
fn normalize_collapses_runs_of_whitespace() {
assert_eq!(normalize_value("a b c"), "a b c");
assert_eq!(
normalize_value(" \tleading \t and trailing \t "),
"leading and trailing"
);
}
#[test]
fn normalize_strips_comments() {
assert_eq!(normalize_value("foo (this is a comment) bar"), "foo bar");
assert_eq!(
normalize_value("Acme/1.0 (server (nested) detail) baz"),
"Acme/1.0 baz",
);
}
#[test]
fn normalize_preserves_quoted_strings() {
assert_eq!(
normalize_value(r#""display (name)" <sip:a@b>"#),
r#""display (name)" <sip:a@b>"#,
);
}
#[test]
fn normalize_handles_quoted_pair_escapes() {
assert_eq!(normalize_value(r#""a\"b" trailing"#), r#""a\"b" trailing"#,);
}
#[test]
fn typed_from_param_order_is_normalized() {
let v1 = "Alice <sip:alice@example.com>;tag=x;foo=y";
let v2 = "Alice <sip:alice@example.com>;foo=y;tag=x";
let d1 = ours_from_to_diff(v1).unwrap();
let d2 = ours_from_to_diff(v2).unwrap();
assert_eq!(d1, d2, "param-order normalization failed: {d1:?} vs {d2:?}");
let r1 = rsip_from_to_diff(v1).unwrap();
let r2 = rsip_from_to_diff(v2).unwrap();
assert_eq!(r1, r2);
assert_eq!(d1, r1);
}
#[test]
fn typed_from_quoted_token_display_normalizes_to_unquoted() {
let v_quoted = r#""Alice" <sip:alice@example.com>;tag=t"#;
let v_token = "Alice <sip:alice@example.com>;tag=t";
let d_quoted = ours_from_to_diff(v_quoted).unwrap();
let d_token = ours_from_to_diff(v_token).unwrap();
assert_eq!(d_quoted, d_token);
let r_quoted = rsip_from_to_diff(v_quoted).unwrap();
let r_token = rsip_from_to_diff(v_token).unwrap();
assert_eq!(r_quoted, r_token);
assert_eq!(d_quoted, r_quoted);
}
#[test]
fn typed_from_quoted_with_space_preserves_inner() {
let v = r#""Alice Smith" <sip:a@b>;tag=t"#;
let d = ours_from_to_diff(v).unwrap();
let r = rsip_from_to_diff(v).unwrap();
assert_eq!(d.display_name.as_deref(), Some("Alice Smith"));
assert_eq!(d, r);
}
#[test]
fn typed_to_no_tag_is_normalized_consistently() {
let v = "Bob <sip:bob@example.com>";
let d = ours_to_to_diff(v).unwrap();
let r = rsip_to_to_diff(v).unwrap();
assert_eq!(d, r);
assert_eq!(d.display_name.as_deref(), Some("Bob"));
assert!(d.parameters.is_empty());
}
#[test]
fn typed_from_bare_addr_spec_normalizes() {
let v = "sip:bob@example.com;tag=xyz";
let d = ours_from_to_diff(v).unwrap();
let r = rsip_from_to_diff(v).unwrap();
assert_eq!(d, r);
assert_eq!(d.display_name, None);
assert_eq!(
d.parameters,
vec![("tag".to_string(), Some("xyz".to_string()))]
);
}
#[test]
fn unquote_display_name_handles_escapes() {
assert_eq!(unquote_display_name(r#""Alice""#), "Alice");
assert_eq!(unquote_display_name(r#""Alice Smith""#), "Alice Smith");
assert_eq!(
unquote_display_name(r#""He said \"hi\"""#),
r#"He said "hi""#
);
assert_eq!(unquote_display_name("Alice"), "Alice"); }
#[test]
fn typed_from_quoted_param_value_with_semicolon_rsip_rejects() {
let v = r#"<sip:a@b>;tag=t;name="x;y""#;
let d = ours_from_to_diff(v).unwrap();
assert_eq!(d.parameters.len(), 2, "ours: {:?}", d.parameters);
let name_value = d
.parameters
.iter()
.find(|(k, _)| k == "name")
.and_then(|(_, v)| v.as_deref());
assert_eq!(name_value, Some("\"x;y\""));
let r = rsip_from_to_diff(v);
assert!(
r.is_err(),
"rsip 0.4 rejects quoted-param-with-semicolon; \
got Ok({r:?}) — update this test if rsip changed",
);
}
#[test]
fn typed_from_quoted_param_value_rsip_rejects_broadly() {
let v = r#"<sip:a@b>;tag=t;name="hello""#;
let d = ours_from_to_diff(v).unwrap();
let name_value = d
.parameters
.iter()
.find(|(k, _)| k == "name")
.and_then(|(_, v)| v.as_deref());
assert_eq!(name_value, Some("\"hello\""));
let r = rsip_from_to_diff(v);
assert!(
r.is_err(),
"rsip 0.4 rejects all quoted-string param values; got \
Ok({r:?}) — update this test if rsip changed",
);
}
#[test]
fn typed_via_basic_normalizes() {
let v = "SIP/2.0/UDP host.example.com:5060;branch=z9hG4bK1";
let d = ours_via_diff(v).unwrap();
let r = rsip_via_diff(v).unwrap();
assert_eq!(d, r);
assert_eq!(d.protocol, "SIP/2.0");
assert_eq!(d.transport, "UDP");
assert_eq!(d.sent_by, "host.example.com:5060");
}
#[test]
fn typed_via_transport_case_normalized_to_upper() {
let v_upper = "SIP/2.0/UDP host:5060;branch=z";
let v_lower = "SIP/2.0/udp host:5060;branch=z";
let d_upper = ours_via_diff(v_upper).unwrap();
let d_lower = ours_via_diff(v_lower).unwrap();
assert_eq!(d_upper.transport, d_lower.transport);
}
#[test]
fn typed_via_host_case_normalized_to_lower() {
let v_lc = "SIP/2.0/UDP host.example.com:5060;branch=z";
let v_uc = "SIP/2.0/UDP HOST.EXAMPLE.COM:5060;branch=z";
let d_lc = ours_via_diff(v_lc).unwrap();
let d_uc = ours_via_diff(v_uc).unwrap();
assert_eq!(d_lc.sent_by, d_uc.sent_by);
}
#[test]
fn typed_via_ipv6_rsip_rejects() {
let v = "SIP/2.0/UDP [2001:db8::1]:5060;branch=z9hG4bKabc";
let d = ours_via_diff(v).unwrap();
assert_eq!(d.sent_by, "[2001:db8::1]:5060");
let r = rsip_via_diff(v);
assert!(
r.is_err(),
"rsip 0.4 rejects IPv6 sent-by; got Ok({r:?}) — \
update this test if rsip changed",
);
}
#[test]
fn typed_via_rport_flag_and_value_both_match_rsip() {
let v_flag = "SIP/2.0/UDP host:5060;branch=z;rport";
let d_flag = ours_via_diff(v_flag).unwrap();
let r_flag = rsip_via_diff(v_flag).unwrap();
assert_eq!(d_flag, r_flag);
let v_val = "SIP/2.0/UDP host:5060;branch=z;rport=12345";
let d_val = ours_via_diff(v_val).unwrap();
let r_val = rsip_via_diff(v_val).unwrap();
assert_eq!(d_val, r_val);
}
#[test]
fn typed_cseq_basic_normalizes() {
let v = "1 INVITE";
let d = ours_cseq_diff(v).unwrap();
let r = rsip_cseq_diff(v).unwrap();
assert_eq!(d, r);
assert_eq!(d.seq, 1);
assert_eq!(d.method, "INVITE");
}
#[test]
fn typed_cseq_method_case_normalized() {
let v = "42 invite";
let d = ours_cseq_diff(v).unwrap();
let r = rsip_cseq_diff(v).unwrap();
assert_eq!(d, r);
assert_eq!(d.method, "INVITE");
}
#[test]
fn typed_cseq_high_seq_numbers() {
let v = format!("{} BYE", u32::MAX);
let d = ours_cseq_diff(&v).unwrap();
let r = rsip_cseq_diff(&v).unwrap();
assert_eq!(d, r);
assert_eq!(d.seq, u32::MAX);
}
#[test]
fn typed_contact_simple_normalizes() {
let v = "<sip:alice@example.com>;expires=3600";
let d = ours_contact_diff(v).unwrap();
let r = rsip_contact_diff(v).unwrap();
assert_eq!(d, r);
if let DiffContact::Addr(a) = &d {
assert!(a.display_name.is_none());
} else {
panic!("expected Addr");
}
}
#[test]
fn typed_contact_wildcard_handled_on_both_sides() {
let v = "*";
let d = ours_contact_diff(v).unwrap();
let r = rsip_contact_diff(v).unwrap();
assert!(matches!(d, DiffContact::Wildcard));
assert!(matches!(r, DiffContact::Wildcard));
}
#[test]
fn typed_contact_wildcard_with_params_rsip_misclassifies() {
use rsip::headers::untyped::{ToTypedHeader, UntypedHeader};
let v = "*;expires=0";
let ours = ours_contact_diff(v).unwrap();
assert!(
matches!(ours, DiffContact::Wildcard),
"ours produced non-wildcard for `*;expires=0`: {ours:?}",
);
let untyped = rsip::headers::Contact::new(v);
let rsip_typed = untyped.typed().expect("rsip 0.4 accepts `*;expires=0`");
let rsip_host = rsip_typed.uri.host_with_port.host.to_string();
assert_eq!(
rsip_host, "*",
"rsip 0.4 unexpectedly stopped misclassifying `*` as a Domain host: \
host now = {rsip_host:?}; the wildcard-with-params divergence may be \
fixed and this pin can be retired",
);
}
#[test]
fn typed_contact_with_quoted_display_normalizes() {
let v = r#""Alice" <sip:alice@example.com>;expires=300;q=0.7"#;
let d = ours_contact_diff(v).unwrap();
let r = rsip_contact_diff(v).unwrap();
assert_eq!(d, r);
if let DiffContact::Addr(a) = &d {
assert_eq!(a.display_name.as_deref(), Some("Alice"));
} else {
panic!("expected Addr");
}
}
#[test]
fn typed_contact_bare_addr_spec_normalizes() {
let v = "sip:bob@example.com;expires=60";
let d = ours_contact_diff(v).unwrap();
let r = rsip_contact_diff(v).unwrap();
assert_eq!(d, r);
}
#[test]
fn normalize_does_not_apply_quoted_pair_outside_string_or_comment() {
let input = r"foo \(literal\) bar";
let out = normalize_value(input);
assert!(out.contains('('), "expected '(' preserved, got: {out:?}");
assert!(out.contains(')'), "expected ')' preserved, got: {out:?}");
}
#[test]
fn separator_lf_cr_lf_rsip_accepts_we_reject() {
let bytes: &[u8] = b"SIP/2.0 223 X\r\nH: V\n\r\nbody";
let rs = oracle::rsip_to_diff(bytes);
let ours = oracle::ours_to_diff(bytes);
assert!(
rs.is_ok(),
"rsip should accept the \\n\\r\\n separator: {:?}",
rs.err(),
);
let err = ours.expect_err("ours should reject the \\n\\r\\n separator");
assert!(
err.contains("no header/body separator"),
"ours error should mention missing separator; got: {err}",
);
}