// address.mbt — name-addr / addr-spec parsing, RFC 3261 §25.1.
//
// name-addr = [ display-name ] LAQUOT addr-spec RAQUOT
// display-name = *(token LWS) / quoted-string
//
// Two RFC 4475 verdicts shape this code:
//
// * §3.1.1.6 ("lwsdisp"): `From: caller;tag=323` must be
// ACCEPTED. The missing LWS before `<` is a known RFC 3261 grammar bug, so
// the token loop stops on `<` without requiring whitespace first.
//
// * §3.1.2.14 ("badaspec"): `To: "Watson, Thomas" < sip:t.watson@example.org >`
// must be REJECTED. Therefore no SWS is skipped between `<` and the
// addr-spec, nor between the addr-spec and `>`.
//
// One ambiguity is not resolvable and is documented instead of guessed at: when
// a field omits `<>`, header parameters and URI parameters live in the same
// octet run (`From: sip:a@b;tag=x`). `tag` is then looked up in both.
///|
/// One `;name[=value]` header parameter following a name-addr.
pub struct HeaderParam {
name : Bytes
value : Bytes?
raw : Bytes
}
///|
/// Returns the parameter name as written on the wire.
pub fn HeaderParam::name(self : HeaderParam) -> Bytes {
self.name
}
///|
/// Returns the parameter value, or `None` for a valueless flag such as `;lr`.
pub fn HeaderParam::value(self : HeaderParam) -> Bytes? {
self.value
}
///|
/// `addr-spec = SIP-URI / SIPS-URI / absoluteURI` (RFC 3261 §25.1). Non-SIP
/// absolute URIs are kept as octets: rejecting them is an application-layer
/// decision, which is how RFC 4475 §3.3.4 ("unksm2") expects `isbn:` and
/// `http:` values to survive parsing.
pub enum AddrSpec {
Sip(SipUri)
Other(Bytes)
}
///|
pub extend AddrSpec with Show::{to_string, output}
///|
pub impl Show for AddrSpec with fn to_string(self : AddrSpec) -> String {
match self {
Sip(u) => ascii_of(u.raw().exact_view())
Other(raw) => ascii_of(raw.exact_view())
}
}
///|
/// A parsed From/To/Contact/Route/Record-Route style address value.
///
/// `display` holds the semantic display name (quotes removed, quoted-pairs
/// resolved); `display_quoted` records whether the wire form used quotes, since
/// an unquoted display name that needed quotes is a reportable defect.
pub struct NameAddr {
raw : Bytes
display : Bytes?
display_quoted : Bool
uri : AddrSpec
params : Array[HeaderParam]
had_angle_brackets : Bool
}
///|
/// Returns the exact octets this address was parsed from.
pub fn NameAddr::raw(self : NameAddr) -> Bytes {
self.raw
}
///|
/// Returns the display name, or `None` when the value had none.
pub fn NameAddr::display(self : NameAddr) -> Bytes? {
self.display
}
///|
/// True when the wire form quoted the display name.
pub fn NameAddr::display_quoted(self : NameAddr) -> Bool {
self.display_quoted
}
///|
/// Returns the address spec: a parsed SIP URI, or verbatim octets for another scheme.
pub fn NameAddr::uri(self : NameAddr) -> AddrSpec {
self.uri
}
///|
/// The parsed SIP URI, when the address held one.
pub fn NameAddr::sip_uri(self : NameAddr) -> SipUri? {
match self.uri {
Sip(u) => Some(u)
Other(_) => None
}
}
///|
/// Returns the header parameters that followed the address.
pub fn NameAddr::params(self : NameAddr) -> Array[HeaderParam] {
self.params
}
///|
/// True when the value used `<>` around the addr-spec.
pub fn NameAddr::had_angle_brackets(self : NameAddr) -> Bool {
self.had_angle_brackets
}
///|
/// Looks up a header parameter, then falls back to a URI parameter of the same
/// name. The fallback exists only because fields without `<>` merge the two
/// namespaces; see the file header.
pub fn NameAddr::param(self : NameAddr, name : String) -> HeaderParam? {
let want = ascii_bytes_of(name).exact_view()
for p in self.params {
if bytes_eq_ignore_ascii_case(p.name.exact_view(), want) {
return Some(p)
}
}
match self.uri {
Sip(u) =>
match u.param(name) {
Some(up) => Some({ name: up.name, value: up.value, raw: up.raw, })
None => None
}
Other(_) => None
}
}
///|
/// The `tag` parameter, which every dialog-forming request must carry.
pub fn NameAddr::tag(self : NameAddr) -> Bytes? {
match self.param("tag") {
Some(p) => p.value
None => None
}
}
///|
/// Parses the parameters that follow a `>` (or follow an unbracketed URI).
fn parse_header_params(c : Cursor) -> Array[HeaderParam] raise SipError {
let params : Array[HeaderParam] = []
while true {
// Whitespace may separate `>` from the first `;` (§3.1.1.1 "wsinv").
ignore(c.skip_sws())
if c.pos >= c.length() || c.src[c.pos] != SEMI {
break
}
let pstart = c.position()
c.pos = c.pos + 1
ignore(c.skip_sws())
let nstart = c.position()
while c.pos < c.length() {
let x = c.src[c.pos]
if is_token_char(x) {
c.pos = c.pos + 1
} else {
break
}
}
let pname = copy_range(c.src, nstart, c.position())
let mut pval : Bytes? = None
// RFC 4475 §3.1.1.1 puts whitespace around `=` (`tag = 1918181833n`).
ignore(c.skip_sws())
if c.pos < c.length() && c.src[c.pos] == EQUALS {
c.pos = c.pos + 1
ignore(c.skip_sws())
let vstart = c.position()
if vstart < c.length() && c.src[vstart] == DQUOTE {
let qs = parse_quoted_string(c)
pval = Some(qs.value)
} else {
while c.pos < c.length() {
let x = c.src[c.pos]
if is_token_char(x) {
c.pos = c.pos + 1
} else {
break
}
}
pval = Some(copy_range(c.src, vstart, c.position()))
}
}
params.push({
name: pname,
value: pval,
raw: copy_range(c.src, pstart, c.position()),
})
}
params
}
///|
/// True when the cursor starts a syntactically plausible `scheme:` URI. Used to
/// recognise a bare `absoluteURI` where a display name would otherwise be read.
fn looks_like_absolute_uri(c : Cursor) -> Bool {
let mut i = c.pos
if i >= c.length() || !is_alpha(c.src[i]) {
return false
}
i = i + 1
while i < c.length() {
let b = c.src[i]
if is_alnum(b) || b == b'+' || b == b'-' || b == b'.' {
i = i + 1
} else {
break
}
}
i < c.length() && c.src[i] == COLON
}
///|
/// Parses a name-addr or a bare addr-spec.
///
/// The distinction is made by lookahead rather than by backtracking: if the value
/// starts with `sip:`/`sips:`, it is a bare addr-spec (RFC 3261 allows this for
/// From/To); otherwise the leading run is a display name and `<>` is mandatory.
pub fn parse_name_addr(c : Cursor) -> NameAddr raise SipError {
ignore(c.skip_sws())
let start = c.position()
let mut display : Bytes? = None
let mut display_quoted = false
let mut had_angle = false
if looks_like_sip_uri(c) {
// Bare addr-spec: `From: sip:a@b;tag=x`. Parameters are then ambiguous and
// are kept inside the URI; `NameAddr::param` searches both.
let uri = AddrSpec::Sip(parse_sip_uri(c))
// RFC 4475 §3.1.1.1 ("wsinv") puts whitespace before the `;`, so the URI
// parse stops early. Trailing parameters still belong to the field.
ignore(c.skip_sws())
let params = parse_header_params(c)
return {
raw: copy_range(c.src, start, c.position()),
display: None,
display_quoted: false,
uri,
params,
had_angle_brackets: false,
}
}
if c.pos < c.length() && c.src[c.pos] == DQUOTE {
let qs = parse_quoted_string(c)
display = Some(qs.value)
display_quoted = true
ignore(c.skip_sws())
if c.eof() || c.src[c.pos] != LT {
raise Syntax(IllegalOctet, c.position())
}
} else if c.pos < c.length() && c.src[c.pos] == LT {
// No display name at all: ``.
()
} else if looks_like_absolute_uri(c) {
// Bare absoluteURI such as `To: isbn:2983792873` (RFC 4475 §3.3.4).
// Octets are kept verbatim; scheme support is decided by the caller.
let ustart = c.position()
while c.pos < c.length() {
let b = c.src[c.pos]
if b == SEMI || b == COMMA || b == CR || b == LF || b == GT {
break
}
c.pos = c.pos + 1
}
let raw = copy_range(c.src, ustart, c.position())
ignore(c.skip_sws())
let params = parse_header_params(c)
return {
raw: copy_range(c.src, start, c.position()),
display: None,
display_quoted: false,
uri: AddrSpec::Other(raw),
params,
had_angle_brackets: false,
}
} else {
// Unquoted display name: one or more tokens separated by LWS. `dend` tracks
// the end of the last token so trailing whitespace is not part of the value.
let dstart = c.position()
let mut dend = dstart
while c.pos < c.length() {
let b = c.src[c.pos]
if is_token_char(b) {
c.pos = c.pos + 1
dend = c.position()
} else if is_lws(b) {
c.pos = c.pos + 1
} else {
break
}
}
if dend == dstart {
raise Syntax(ExpectedToken, dstart)
}
display = Some(copy_range(c.src, dstart, dend))
ignore(c.skip_sws())
if c.eof() || c.src[c.pos] != LT {
// A non-token, non-LWS octet before `<` means the display name needed
// quoting: RFC 4475 §3.1.2.15 ("baddn") calls this malformed.
raise Syntax(ExpectedQuotedString, c.position())
}
}
// LAQUOT
c.pos = c.pos + 1
had_angle = true
// No SWS here: `< sip:...` is malformed per RFC 4475 §3.1.2.14.
let uri = if looks_like_sip_uri(c) {
AddrSpec::Sip(parse_sip_uri(c))
} else {
// absoluteURI: scan to the closing bracket, keeping the octets verbatim.
let ustart = c.position()
let mut i = 0
while c.pos < c.length() {
let b = c.src[c.pos]
if b == GT {
break
}
// No LWS inside the brackets: RFC 4475 §3.1.2.14 ("badaspec") is
// malformed precisely because of `< sip:... >`.
if b == CR || b == LF || b == LT || b == SP || b == HTAB {
raise Syntax(IllegalOctet, c.pos)
}
c.pos = c.pos + 1
i = i + 1
}
if i == 0 {
raise Syntax(IllegalOctet, ustart)
}
AddrSpec::Other(copy_range(c.src, ustart, c.position()))
}
if c.eof() || c.src[c.pos] != GT {
raise Syntax(IllegalOctet, c.position())
}
c.pos = c.pos + 1
let params = parse_header_params(c)
{
raw: copy_range(c.src, start, c.position()),
display,
display_quoted,
uri,
params,
had_angle_brackets: had_angle,
}
}
/// A tiny mutable cell is not needed; the display-name end offset is tracked with
/// a plain `let mut` inside the parser.
///|
/// Parses a comma-separated list of addresses (Contact, Route, Record-Route).
///
/// Commas inside quoted strings do not split — that is the whole reason a
/// hand-rolled split on `,` is wrong for SIP.
pub fn parse_name_addr_list(c : Cursor) -> Array[NameAddr] raise SipError {
let out : Array[NameAddr] = []
ignore(c.skip_sws())
if c.eof() {
return out
}
while true {
let a = parse_name_addr(c)
out.push(a)
ignore(c.skip_sws())
if c.pos < c.length() && c.src[c.pos] == COMMA {
c.pos = c.pos + 1
ignore(c.skip_sws())
continue
}
break
}
out
}