///|
/// Checks header names and values before serialization. Empty names and
/// non-token name characters are refused; values cannot contain controls
/// other than horizontal tab, or characters outside the HTTP byte range.
pub fn Headers::validate(self : Headers) -> Unit raise TransportError {
  for (name, value) in self.iter() {
    if name.is_empty() || !name.iter().all(is_token) {
      raise TransportError(
        kind=Invalid,
        phase=Pool,
        detail="invalid header name",
      )
    }
    for c in value {
      if (c < ' ' && c != '\t') || c == '\u007f' || c.to_int() > 255 {
        raise TransportError(
          kind=Invalid,
          phase=Pool,
          detail="invalid header value",
        )
      }
    }
  }
}

///|
fn is_token(c : Char) -> Bool {
  c is ('a'..<='z' | 'A'..<='Z' | '0'..<='9') ||
  "!#$%&'*+-.^_`|~".contains_char(c)
}

///|
/// Validates the original URL before normalization can encode an invalid
/// request target. Unicode text is permitted and encoded by the URL parser.
pub fn validate_url(url : String) -> Unit raise TransportError {
  for c in url {
    if c <= ' ' || c == '\u007f' {
      raise TransportError(
        kind=Invalid,
        phase=Pool,
        detail="URL contains a space or control character",
      )
    }
  }
}

///|
/// Validates the method token, raw URL, headers and declared stream length
/// before a backend sends it. A stream body declaring a negative length is
/// refused here as `StreamBody::take` would refuse it, and its source is
/// closed, since no sender will take it.
pub fn Request::validate(self : Request) -> Unit raise TransportError {
  let meth = self.meth.to_string()
  if meth.is_empty() || !meth.iter().all(is_token) {
    raise TransportError(kind=Invalid, phase=Pool, detail="invalid HTTP method")
  }
  validate_url(self.url)
  self.headers.validate()
  if self.body is Stream(stream) && stream.length is Some(length) && length < 0L {
    ignore(stream.take())
  }
}

///|
/// Reads and closes a stream, refusing the first chunk that crosses `limit`.
/// Subtraction before comparison prevents accumulated-length overflow.
pub async fn read_bounded(
  stream : &BodyStream,
  limit : Int32,
) -> Bytes raise TransportError {
  defer stream.close()
  if limit < 0 {
    raise TransportError(kind=Invalid, phase=Body, detail="negative body limit")
  }
  let buffer = Buffer()
  while stream.read_some() is Some(chunk) {
    if chunk.is_empty() {
      raise TransportError(kind=Protocol, phase=Body, detail="empty body chunk")
    }
    if chunk.length() > limit - buffer.length() {
      raise TransportError(
        kind=TooLarge(limit~),
        phase=Body,
        detail="body exceeds limit",
      )
    }
    buffer.write_bytes(chunk)
  }
  buffer.to_bytes()
}