Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

Chunked.pudu

Pudu119 lines4.6 KB

GitHub ↗
1/** @Domain.Chunked.Module — a chunked body decoded as its bytes arrive */2module PuduLangHttpClient.Domain.Chunked34import Std.Bytes as ByteSeq56/** @Domain.Chunked.Phase — what the decoder expects next */7export type Phase = Size | Data(Int) | DataEnd | Trailer | Done89/** @Domain.Chunked.Decoder — the phase, the bytes not yet decoded, and what was decoded so far */10export type Decoder = { phase: Phase, pending: Bytes, decoded: Int, trailers: Array[(Str, Str)] }1112/** @Domain.Chunked.Problem — why the bytes are not a chunked body within its limit */13export type Problem = Malformed(Str) | Exceeded1415/// The longest chunk-size or trailer line accepted, in bytes.16const LINE_LIMIT: Int = 81921718/// The bytes that end every line of the framing.19const CRLF: Str = "\r\n"2021/// The decoder before any byte arrived.22export fn decoder() -> Decoder { Decoder{phase: Size, pending: ByteSeq.empty(), decoded: 0, trailers: []} }2324/// Whether the terminating chunk and its trailer section arrived.25export fn isDone(state: &Decoder) -> Bool { state.phase == Done }2627/// The bytes that arrived after the body ended.28export fn leftover(state: &Decoder) -> Bytes { if state.phase == Done { state.pending } else { ByteSeq.empty() } }2930/// The decoder after more bytes arrived, and the body bytes they completed; a body growing past31/// `limit` decoded bytes is `Exceeded`.32export fn feed(state: &Decoder, input: &Bytes, limit: Int) -> Result[(Decoder, Bytes), Problem] {33  var current = Decoder{..*state, pending: state.pending.concat(*input)}34  var produced: Array[Bytes] = []35  var going = true36  while going {37    match current.phase {38      case Size => {39        match line(&current.pending) ? {40          case None => { going = false }41          case Some((text, rest)) => {42            let size = sizeOf(text) ?43            current = Decoder{..current, pending: rest, phase: if size == 0 { Trailer } else { Data(size) }}44          }45        }46      }47      case Data(remaining) => {48        if current.pending.isEmpty() {49          going = false50        } else {51          let taken = current.pending.take(remaining)52          let decoded = current.decoded + taken.length()53          if decoded > limit { return Err(Exceeded) }54          produced = produced.push(taken)55          let left = remaining - taken.length()56          current = Decoder{..current, pending: current.pending.drop(taken.length()), decoded: decoded, phase: if left == 0 { DataEnd } else { Data(left) }}57        }58      }59      case DataEnd => {60        if current.pending.length() < 2 {61          going = false62        } else if current.pending.take(2) != CRLF.toBytes() {63          return Err(Malformed("a chunk is not followed by a line break"))64        } else {65          current = Decoder{..current, pending: current.pending.drop(2), phase: Size}66        }67      }68      case Trailer => {69        match line(&current.pending) ? {70          case None => { going = false }71          case Some((text, rest)) => {72            if text.isEmpty() {73              current = Decoder{..current, pending: rest, phase: Done}74            } else {75              current = Decoder{..current, pending: rest, trailers: current.trailers.push(fieldOf(text) ?)}76            }77          }78        }79      }80      case Done => { going = false }81    }82  }83  Ok((current, ByteSeq.join(&produced)))84}8586/// One framing line and the bytes after its line break, or `None` while the line is incomplete.87fn line(pending: &Bytes) -> Result[Option[(Str, Bytes)], Problem] {88  let end = pending.indexOf(CRLF.toBytes())89  if end < 0 {90    if pending.length() > LINE_LIMIT { Err(Malformed("a framing line is longer than 8192 bytes")) } else { Ok(None) }91  } else if end > LINE_LIMIT {92    Err(Malformed("a framing line is longer than 8192 bytes"))93  } else {94    match ByteSeq.toText(&pending.take(end)) {95      case Ok(text) => Ok(Some((text, pending.drop(end + 2))))96      case Err(_) => Err(Malformed("a framing line is not text"))97    }98  }99}100101/// The size a chunk-size line states, ignoring any extension after a semicolon.102fn sizeOf(text: Str) -> Result[Int, Problem] {103  let digits = text.split(";")[0].trim()104  if digits.isEmpty() || digits.length() > 15 { return Err(Malformed("a chunk size is missing or too long")) }105  var size = 0106  for character in digits.chars() {107    let digit = "0123456789abcdef".indexOf(character.toText().toLower())108    if digit < 0 { return Err(Malformed("a chunk size is not hexadecimal: " + digits)) }109    size = size * 16 + digit110  }111  Ok(size)112}113114/// A trailer field as a name and a value.115fn fieldOf(text: Str) -> Result[(Str, Str), Problem] {116  let colon = text.indexOf(":")117  if colon < 1 { Err(Malformed("a trailer field has no name")) } else { Ok((text.slice(0, colon).trim(), text.drop(colon + 1).trim())) }118}119