
Chunked.pudu
Pudu119 lines4.6 KB
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) | Exceeded141516const LINE_LIMIT: Int = 8192171819const CRLF: Str = "\r\n"202122export fn decoder() -> Decoder { Decoder{phase: Size, pending: ByteSeq.empty(), decoded: 0, trailers: []} }232425export fn isDone(state: &Decoder) -> Bool { state.phase == Done }262728export fn leftover(state: &Decoder) -> Bytes { if state.phase == Done { state.pending } else { ByteSeq.empty() } }29303132export 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(¤t.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(¤t.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}858687fn 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}100101102fn 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}113114115fn 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