// Hand-written matchers for the RFC 7230 grammar fragments that h11 uses.
// Each function corresponds to a `regex.fullmatch` in the Python version
// (see `h11/_abnf.py`).
///|
/// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
/// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
fn is_tchar(b : Byte) -> Bool {
is_digit(b) ||
is_ascii_lower(b) ||
is_ascii_upper(b) ||
b
is (b'!'
| b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~')
}
///|
/// VCHAR = %x21-7E
fn is_vchar(b : Byte) -> Bool {
b >= b'\x21' && b <= b'\x7e'
}
///|
/// Our relaxed field-vchar: anything except NUL and whitespace (this
/// includes obs-text and most control characters, see h11 issue #57).
fn is_field_vchar(b : Byte) -> Bool {
b != b'\x00' && !is_ascii_whitespace(b)
}
///|
fn is_ows(b : Byte) -> Bool {
b is (b' ' | b'\t')
}
///|
/// token = 1*tchar
fn match_token(s : BytesView) -> Bool {
!s.is_empty() && s.iter().all(is_tchar)
}
///|
/// field-value = [ field-vchar *( *( SP / HTAB ) field-vchar ) ]
fn match_field_value(s : BytesView) -> Bool {
if s.is_empty() {
return true
}
is_field_vchar(s[0]) &&
is_field_vchar(s[s.length() - 1]) &&
s.iter().all(b => is_field_vchar(b) || is_ows(b))
}
///|
/// header-field = field-name ":" OWS field-value OWS
///
/// Returns the `(field_name, field_value)` groups on success.
fn match_header_field(line : BytesView) -> (BytesView, BytesView)? {
let mut i = 0
while i < line.length() && is_tchar(line[i]) {
i += 1
}
if i == 0 || i >= line.length() || line[i] != b':' {
return None
}
let name = line[:i]
let mut start = i + 1
let mut end = line.length()
while start < end && is_ows(line[start]) {
start += 1
}
while end > start && is_ows(line[end - 1]) {
end -= 1
}
let value = line[start:end]
if match_field_value(value) {
Some((name, value))
} else {
None
}
}
///|
/// HTTP-version = "HTTP/" DIGIT "." DIGIT, returning the "d.d" part.
fn match_http_version(s : BytesView) -> BytesView? {
if s is [b'H', b'T', b'T', b'P', b'/', d1, b'.', d2] &&
is_digit(d1) &&
is_digit(d2) {
Some(s[5:])
} else {
None
}
}
///|
/// request-line = method SP request-target SP HTTP-version
///
/// Returns `(method, target, http_version)`.
fn match_request_line(line : BytesView) -> (BytesView, BytesView, BytesView)? {
let n = line.length()
let mut i = 0
while i < n && is_tchar(line[i]) {
i += 1
}
if i == 0 || i >= n || line[i] != b' ' {
return None
}
let meth = line[:i]
let target_start = i + 1
let mut j = target_start
while j < n && is_vchar(line[j]) {
j += 1
}
if j == target_start || j >= n || line[j] != b' ' {
return None
}
let target = line[target_start:j]
guard match_http_version(line[j + 1:]) is Some(version) else { return None }
Some((meth, target, version))
}
///|
/// reason-phrase = *( HTAB / SP / VCHAR / obs-text ), relaxed like
/// field-vchar.
fn is_reason_byte(b : Byte) -> Bool {
is_ows(b) || is_field_vchar(b)
}
///|
/// status-line = HTTP-version SP status-code [ SP reason-phrase ]
///
/// (The reason phrase is optional because many servers omit it.)
/// Returns `(http_version, status_code, reason)`.
fn match_status_line(line : BytesView) -> (BytesView, Int, BytesView?)? {
if line.length() < 12 {
return None
}
guard match_http_version(line[:8]) is Some(version) else { return None }
guard line[8] == b' ' else { return None }
let code = line[9:12]
guard code.iter().all(is_digit) else { return None }
let status_code = (code[0] - b'0').to_int() * 100 +
(code[1] - b'0').to_int() * 10 +
(code[2] - b'0').to_int()
if line.length() == 12 {
return Some((version, status_code, None))
}
guard line[12] == b' ' else { return None }
let reason = line[13:]
guard reason.iter().all(is_reason_byte) else { return None }
Some((version, status_code, Some(reason)))
}
///|
/// chunk-header = 1*20HEXDIG [ ";" chunk-ext ] OWS CRLF
///
/// Chunk extensions are accepted and discarded. Returns the chunk size
/// (saturated at `Int64` max, see `parse_digits`).
fn match_chunk_header(line : BytesView) -> Int64? {
guard line.length() >= 2 &&
line[line.length() - 2] == b'\r' &&
line[line.length() - 1] == b'\n' else {
return None
}
let body = line[:line.length() - 2]
let mut i = 0
while i < body.length() && hex_value(body[i]) is Some(_) {
i += 1
}
if i == 0 || i > 20 {
return None
}
let rest = body[i:]
let ok = if rest is [b';', .. ext] {
// `;.*` followed by OWS: `.` matches anything except \n
ext.iter().all(b => b != b'\n')
} else {
rest.iter().all(is_ows)
}
if ok {
parse_hex(body[:i])
} else {
None
}
}