// header.mbt — message header field parsing, RFC 3261 §7.3 and §25.1.
//
// message-header = field-name ":" [ field-value ]
// field-name = token
// HCOLON = *( SP / HTAB ) ":" SWS
// field-value = *( field-content / LWS )
//
// Two properties matter more than speed here:
//
// * Folding. A CRLF followed by SP/HTAB continues the same field value and is
// semantically a single SP (RFC 3261 §7.3.1). A CRLF *not* followed by
// whitespace terminates the field. Getting this backwards is how header
// smuggling bugs start, so the distinction is explicit in `parse_field_value`.
//
// * Names are compared literally, case-insensitively, and are NOT unescaped.
// RFC 4475 §3.1.1.5 ("esc02") states that `C%6Fntact` is a *different* field
// from `Contact` — there are two Contact values in that message, not three.
///|
/// One message header field.
///
/// `value` is the semantic value: folds collapsed to a single SP, leading and
/// trailing whitespace trimmed. `raw` keeps the exact octets of the field line
/// (without its terminating CRLF) so a caller can round-trip byte-for-byte.
pub struct HeaderField {
name : Bytes
value : Bytes
raw : Bytes
offset : Int32
line : Int32
}
///|
/// The field name as written: case preserved, never percent-unescaped.
pub fn HeaderField::name(self : HeaderField) -> Bytes {
self.name
}
///|
/// The semantic value, with folds collapsed to one SP and outer whitespace trimmed.
pub fn HeaderField::value(self : HeaderField) -> Bytes {
self.value
}
///|
/// The field line as received, without its terminating CRLF.
pub fn HeaderField::raw(self : HeaderField) -> Bytes {
self.raw
}
///|
/// Byte offset of the field name in the input.
pub fn HeaderField::offset(self : HeaderField) -> Int32 {
self.offset
}
///|
/// 1-based line number, for reporting.
pub fn HeaderField::line(self : HeaderField) -> Int32 {
self.line
}
///|
/// RFC 3261 Table 2: the single-letter compact forms real networks still send.
/// Without this table an `I:` header is not recognised as `Call-ID`, which makes
/// otherwise valid messages look like they are missing a required field.
fn long_name_of(compact : Byte) -> String? {
match compact {
b'i' | b'I' => Some("Call-ID")
b'm' | b'M' => Some("Contact")
b'e' | b'E' => Some("Content-Encoding")
b'l' | b'L' => Some("Content-Length")
b'c' | b'C' => Some("Content-Type")
b'f' | b'F' => Some("From")
b's' | b'S' => Some("Subject")
b'k' | b'K' => Some("Supported")
b't' | b'T' => Some("To")
b'v' | b'V' => Some("Via")
_ => None
}
}
///|
/// Case-insensitive name test, honouring the compact forms in both directions.
pub fn HeaderField::name_is(self : HeaderField, name : String) -> Bool {
let want = ascii_bytes_of(name)
if bytes_eq_ignore_ascii_case(self.name.exact_view(), want.exact_view()) {
return true
}
if self.name.length() == 1 {
match long_name_of(self.name[0]) {
Some(long) =>
return bytes_eq_ignore_ascii_case(
want.exact_view(),
ascii_bytes_of(long).exact_view(),
)
None => ()
}
}
if want.length() == 1 {
match long_name_of(want[0]) {
Some(long) =>
return bytes_eq_ignore_ascii_case(
self.name.exact_view(),
ascii_bytes_of(long).exact_view(),
)
None => ()
}
}
false
}
///|
/// Parses a field value up to (but not including) the terminating CRLF.
///
/// A fold contributes exactly one SP and also records that folding happened, so
/// the caller can report suspicious folding without guessing.
fn parse_field_value(c : Cursor) -> Bytes {
let out : Array[Byte] = []
while c.pos < c.length() {
let b = c.src[c.pos]
if b == CR {
if c.at_fold() {
ignore(c.consume_fold())
out.push(SP)
continue
}
break
}
if b == LF {
// Bare LF: tolerated as a line terminator by every deployed stack, and
// reported by the diagnostics layer rather than rejected here.
break
}
out.push(b)
c.pos = c.pos + 1
}
let collected = Bytes::from_array(out)
bytes_of_view(trim_ows(collected.exact_view()))
}
///|
/// Trims leading and trailing SP/HTAB.
pub fn trim_ows(b : BytesView) -> BytesView {
let mut s = 0
let mut e = b.length()
while s < e && is_lws(b[s]) {
s = s + 1
}
while e > s && is_lws(b[e - 1]) {
e = e - 1
}
b.exact_view(start=s, end=e)
}
///|
/// Parses one header field. The cursor must be positioned at the first octet of
/// the field name.
pub fn parse_header_field(
c : Cursor,
line : Int32,
) -> HeaderField raise SipError {
let fstart = c.position()
let nstart = c.position()
while c.pos < c.length() {
let b = c.src[c.pos]
if is_token_char(b) {
c.pos = c.pos + 1
} else {
break
}
}
if c.pos == nstart {
raise Syntax(ExpectedToken, nstart)
}
let name = copy_range(c.src, nstart, c.position())
// HCOLON allows whitespace before the colon: `To :` is legal.
ignore(c.skip_lws())
if c.eof() || c.src[c.pos] != COLON {
raise Syntax(IllegalOctet, c.position())
}
c.pos = c.pos + 1
// SWS after the colon is part of the syntax, not the value.
ignore(c.skip_sws())
let value = parse_field_value(c)
{
name,
value,
raw: copy_range(c.src, fstart, c.position()),
offset: fstart,
line,
}
}