///|
fn malformed(detail : String) -> ParseError {
  ParseError(kind=Malformed, detail~)
}

///|
fn too_large(detail : String) -> ParseError {
  ParseError(kind=HeadTooLarge, detail~)
}

///|
/// A `tchar` of RFC 9110, section 5.6.2.
fn is_tchar(c : Char) -> Bool {
  match c {
    'a'..<='z' | 'A'..<='Z' | '0'..<='9' => true
    _ => "!#$%&'*+-.^_`|~".contains_char(c)
  }
}

///|
/// A nonempty `token`.
fn token(text : StringView) -> Bool {
  !text.is_empty() && text.iter().all(is_tchar)
}

///|
/// Whether `c` may appear in a field value, a reason phrase or a quoted
/// string: anything but controls other than HTAB, and DEL.
fn is_field_char(c : Char) -> Bool {
  !((c < ' ' && c != '\t') || c == '\u{7f}')
}

///|
/// Each octet as the code point of the same value (ISO-8859-1), so obs-text
/// survives unchanged and the writers can emit it back byte for byte.
fn latin1(bytes : BytesView) -> String {
  let out = StringBuilder()
  for byte in bytes {
    out.write_char(byte.to_int().unsafe_to_char())
  }
  out.to_string()
}

///|
/// Strips HTTP optional whitespace (SP and HTAB) only, never Unicode
/// whitespace that obs-text may decode to.
fn ows(text : StringView) -> StringView {
  let mut start = 0
  let mut end = text.length()
  while start < end && (text[start] == ' ' || text[start] == '\t') {
    start += 1
  }
  while end > start && (text[end - 1] == ' ' || text[end - 1] == '\t') {
    end -= 1
  }
  text[start:end]
}

///|
/// Parses one field line of a head or trailer section. The name must be a
/// token directly followed by the colon, which refuses whitespace before the
/// colon and, because a folded continuation starts with SP or HTAB, obs-fold.
fn field(line : StringView) -> (String, String) raise ParseError {
  if line is [' ' | '\t', ..] {
    raise malformed("obsolete line folding")
  }
  guard line.find(":") is Some(colon) else {
    raise malformed("field line lacks colon")
  }
  let name = line[:colon]
  if !token(name) {
    raise malformed("invalid field name")
  }
  let value = line[colon + 1:]
  if !value.iter().all(is_field_char) {
    raise malformed("invalid field value")
  }
  (name.to_lower().to_owned(), ows(value).to_owned())
}