~/bend-docscommunity

multipart.bend source

multipart.bend on the hub · documented module

# multipart/form-data (RFC 7578): an encoder with a secure random boundary and a streaming decoder over Bytes. Source: https://github.com/paymog/bend-kit/tree/main/multipartimport Baseimport bend-kit-bytes@0.3.1.0/bytes.bend as Bytesimport bend-kit-crypto@0.2.2.1/crypto.bend as Crypto# Names, filenames, and header values are byte strings, one Char per octet, as in Http.# Bodies are Bytes and pass through untouched, so binary payloads keep every octet.# Names and filenames are escaped as WHATWG does: '"' is %22, CR is %0D, LF is %0A.#   import bend-kit-multipart@0.1.1.0/multipart.bend as Mp# One form field. filename marks a file; ctype is its Content-Type (RFC 7578 §4.4 defaults to text/plain).type Part is Type:  Part{name: String, filename: Maybe<&2, String>, ctype: Maybe<&2, String>, body: Bytes.Bytes}# What the streaming decoder reports, in order: Head, then zero or more Body pieces, then Tail, per part.type Item is Type:  Head{name: String, filename: Maybe<&2, String>, ctype: Maybe<&2, String>}  Body{chunk: Bytes.Bytes}  Tail{}# ---- shared text helpers# The text before the first c, and the text after it. No c gives (s, "").def split1.cons(+h: U32, r: String & String) -> String & String:  (a, b) = r  (SCon{Chr{h}, a}, b)def split1(s: String, +c: U32) -> String & String:  match s:    case SNil{}:      (SNil{}, SNil{})    case SCon{Chr{+h}, +t}:      Bool.pick(String & String, U32.is_eq(h, c), (SNil{}, t), split1.cons(h, split1(t, c)))# Header parameters (RFC 2045 §5.1, RFC 7578 §4.2): key=token or key="quoted", split on ';'.# Keys are lowercased; a backslash escapes the next char inside quotes. An unterminated quote drops its parameter.type Cls is Data:  CEq{}  CSemi{}  CQuote{}  CSlash{}  CSp{}  COther{}type Pm is Data:  PKey{}  PStart{}  PQuo{}  PEsc{}  PTok{}  PSkip{}type Param is Data:  Param{key: String, val: String}# mode, the key and value so far (reversed), and the parameters found (latest first).type PS is Data:  PS{mode: Pm, key: String, val: String, acc: List<&2, Param>}def cls(+c: U32) -> Cls:  Bool.pick(Cls, U32.is_eq(c, 61), CEq{}, Bool.pick(Cls, U32.is_eq(c, 59), CSemi{}, Bool.pick(Cls, U32.is_eq(c, 34), CQuote{}, Bool.pick(Cls, U32.is_eq(c, 92), CSlash{}, Bool.pick(Cls, Bool.or(U32.is_eq(c, 32), U32.is_eq(c, 9)), CSp{}, COther{})))))def params.emit(key: String, val: String, acc: List<&2, Param>) -> List<&2, Param>:  Con{Param{String.reverse(key), String.reverse(val)}, acc}def params.step(st: PS, k: Cls, +c: U32) -> PS:  PS{mode, key, val, acc} = st  match mode:    case PKey{}:      match k:        case CEq{}:          PS{PStart{}, key, SNil{}, acc}        case CSemi{}:          PS{PKey{}, SNil{}, SNil{}, acc}        case CSp{}:          PS{PKey{}, key, val, acc}        case _:          PS{PKey{}, SCon{Char.to_lower(Chr{c}), key}, val, acc}    case PStart{}:      match k:        case CQuote{}:          PS{PQuo{}, key, SNil{}, acc}        case CSp{}:          PS{PStart{}, key, val, acc}        case CSemi{}:          PS{PKey{}, SNil{}, SNil{}, params.emit(key, SNil{}, acc)}        case _:          PS{PTok{}, key, SCon{Chr{c}, SNil{}}, acc}    case PQuo{}:      match k:        case CSlash{}:          PS{PEsc{}, key, val, acc}        case CQuote{}:          PS{PSkip{}, SNil{}, SNil{}, params.emit(key, val, acc)}        case _:          PS{PQuo{}, key, SCon{Chr{c}, val}, acc}    case PEsc{}:      PS{PQuo{}, key, SCon{Chr{c}, val}, acc}    case PTok{}:      match k:        case CSemi{}:          PS{PKey{}, SNil{}, SNil{}, params.emit(key, String.trim_start(val), acc)}        case _:          PS{PTok{}, key, SCon{Chr{c}, val}, acc}    case PSkip{}:      match k:        case CSemi{}:          PS{PKey{}, SNil{}, SNil{}, acc}        case _:          PS{PSkip{}, key, val, acc}def params.end(st: PS) -> List<&2, Param>:  PS{mode, key, val, acc} = st  match mode:    case PStart{}:      params.emit(key, SNil{}, acc)    case PTok{}:      params.emit(key, String.trim_start(val), acc)    case _:      accdef params.go(s: String, st: PS) -> PS:  match s:    case SNil{}:      st    case SCon{Chr{+c}, t}:      params.go(t, params.step(st, cls(c), c))# The parameters of a header value's text after its first ';', in order.def params(s: String) -> List<&2, Param>:  List.reverse(&2, Param, params.end(params.go(s, PS{PKey{}, SNil{}, SNil{}, Nil{}})))# The first parameter named k (lowercase).def lookup(xs: List<&2, Param>, +k: String) -> Maybe<&2, String>:  match xs:    case Nil{}:      None{}    case Con{Param{+k2, +v}, t}:      Bool.pick(Maybe<&2, String>, String.eq(k2, k), Some{v}, lookup(t, k))# WHATWG form-data escapes for names and filenames.def esc.one(+c: U32) -> String:  Bool.pick(String, U32.is_eq(c, 34), "%22", Bool.pick(String, U32.is_eq(c, 13), "%0D", Bool.pick(String, U32.is_eq(c, 10), "%0A", SCon{Chr{c}, SNil{}})))def esc(s: String) -> String:  match s:    case SNil{}:      SNil{}    case SCon{Chr{+c}, t}:      esc.one(c) ++ esc(t)# acc is reversed, so "%22" shows as "22%".def unesc.fix(+a: String) -> String:  Bool.pick(String, String.starts_with(a, "22%"), SCon{Chr{34}, String.drop(a, 3n)}, Bool.pick(String, String.starts_with(a, "D0%"), SCon{Chr{13}, String.drop(a, 3n)}, Bool.pick(String, String.starts_with(a, "A0%"), SCon{Chr{10}, String.drop(a, 3n)}, a)))def unesc.go(s: String, acc: String) -> String:  match s:    case SNil{}:      acc    case SCon{c, t}:      unesc.go(t, unesc.fix(SCon{c, acc}))# Undoes esc, as the Fetch spec's form-data parser does. A literal "%22" in a name reads back as '"'.def unesc(s: String) -> String:  String.reverse(unesc.go(s, SNil{}))def unesc.m(m: Maybe<&2, String>) -> Maybe<&2, String>:  match m:    case None{}:      None{}    case Some{x}:      Some{unesc(x)}# ---- boundary# RFC 2046 §5.1.1 bchars: DIGIT ALPHA ' ( ) + _ , - . / : = ? and space.def bchar(+c: U32) -> Bool:  Bool.or(Bool.or(Bool.or(Bytes.in(39, c, 41), Bytes.in(43, c, 58)), Bool.or(Bytes.in(65, c, 90), Bytes.in(97, c, 122))), Bool.or(Bool.or(U32.is_eq(c, 61), U32.is_eq(c, 63)), Bool.or(U32.is_eq(c, 95), U32.is_eq(c, 32))))def bchars(s: String) -> Bool:  match s:    case SNil{}:      True{}    case SCon{Chr{+c}, t}:      Bool.and(bchar(c), bchars(t))# 1 to 70 bchars, not ending in a space.def boundary.ok(+b: String) -> Bool:  +n = Bytes.count(b, 0)  Bool.and(Bool.and(U32.is_le(1, n), U32.is_le(n, 70)), Bool.and(bchars(b), Bool.not(String.ends_with(b, " "))))def boundary.words(r: Result<&1, &1, U32 & String, U32 & Array<U32>>) -> Result<&1, &1, U32 & String, String>:  match r:    case Fail{e}:      Fail{e}    case Done{(+n, w)}:      Done{"bend-kit-" ++ Bytes.to_hex(Bytes.Bytes{n, w})}# "bend-kit-" and 128 bits from the OS secure random source, as hex: 41 bchars.def boundary.new() -> IO(Result<&1, &1, U32 & String, String>):  do IO<Result<&1, &1, U32 & String, String>>:    r : Result<&1, &1, U32 & String, U32 & Array<U32>> <- Crypto.random.words(16)    return boundary.words(r)# The Content-Type header value for a body encoded with boundary b.def content_type(b: String) -> String:  "multipart/form-data; boundary=" ++ bdef boundary.get.of(r: String & String) -> Maybe<&2, String>:  (ty, rest) = r  lookup(params(rest), "boundary")# The boundary parameter of a Content-Type value, such as a request's, or None.def boundary.get(ct: String) -> Maybe<&2, String>:  boundary.get.of(split1(ct, 59))# ---- encoderdef enc.fname(m: Maybe<&2, String>) -> String:  match m:    case None{}:      SNil{}    case Some{f}:      "; filename=\"" ++ esc(f) ++ "\""def enc.ctype(m: Maybe<&2, String>) -> String:  match m:    case None{}:      SNil{}    case Some{c}:      "Content-Type: " ++ c ++ "\r\n"def enc.head(+b: String, name: String, fname: Maybe<&2, String>, ct: Maybe<&2, String>) -> String:  "--" ++ b ++ "\r\nContent-Disposition: form-data; name=\"" ++ esc(name) ++ "\"" ++ enc.fname(fname) ++ "\r\n" ++ enc.ctype(ct) ++ "\r\n"# A Content-Type with CR or LF would inject header lines.def enc.ctype.ok(m: Maybe<&2, String>) -> Bool:  match m:    case None{}:      True{}    case Some{+c}:      Bool.not(Bool.or(String.contains(c, "\r"), String.contains(c, "\n")))def enc.found(r: Bytes.Bytes & Maybe<&2, U32>, +head: String, xs: List<&1, Bytes.Bytes>) -> Result<&1, &1, String, List<&1, Bytes.Bytes>>:  (body, hit) = r  match hit:    case Some{i}:      Fail{"multipart: boundary occurs in a part body"}    case None{}:      Done{Con{Bytes.from_string("\r\n"), Con{body, Con{Bytes.from_string(head), xs}}}}def enc.body(ok: Bool, r: Bytes.Bytes & Maybe<&2, U32>, +head: String, xs: List<&1, Bytes.Bytes>) -> Result<&1, &1, String, List<&1, Bytes.Bytes>>:  match ok:    case False{}:      Fail{"multipart: Content-Type has CR or LF"}    case True{}:      enc.found(r, head, xs)# The pieces so far, latest first. A body cannot hold CRLF "--" boundary; bchars hold no CR,# so the delimiter cannot start inside the body and end in the one after it.def enc.part(acc: Result<&1, &1, String, List<&1, Bytes.Bytes>>, p: Part, +b: String, +delim: String) -> Result<&1, &1, String, List<&1, Bytes.Bytes>>:  match acc:    case Fail{e}:      Fail{e}    case Done{xs}:      Part{name, fname, +ct, body} = p      enc.body(enc.ctype.ok(ct), Bytes.find(body, delim), enc.head(b, name, fname, ct), xs)def enc.fin(+b: String, acc: Result<&1, &1, String, List<&1, Bytes.Bytes>>) -> Result<&1, &1, String, Bytes.Bytes>:  match acc:    case Fail{e}:      Fail{e}    case Done{xs}:      Done{Bytes.concat(List.reverse(&1, Bytes.Bytes, Con{Bytes.from_string("--" ++ b ++ "--\r\n"), xs}))}def enc.go(parts: List<&1, Part>, +b: String, +delim: String, acc: Result<&1, &1, String, List<&1, Bytes.Bytes>>) -> Result<&1, &1, String, Bytes.Bytes>:  match parts:    case Nil{}:      enc.fin(b, acc)    case Con{p, t}:      enc.go(t, b, delim, enc.part(acc, p, b, delim))def encode.if(ok: Bool, +b: String, parts: List<&1, Part>) -> Result<&1, &1, String, Bytes.Bytes>:  match ok:    case False{}:      Fail{"multipart: boundary must be 1 to 70 RFC 2046 bchars, not ending in a space"}    case True{}:      enc.go(parts, b, "\r\n--" ++ b, Done{Nil{}})# The body for parts under boundary b (RFC 7578 §4.1). Fails when b is not a valid boundary,# b's delimiter occurs in a body, or a Content-Type holds CR or LF.def encode(+b: String, parts: List<&1, Part>) -> Result<&1, &1, String, Bytes.Bytes>:  encode.if(boundary.ok(b), b, parts)def form.enc(+b: String, r: Result<&1, &1, String, Bytes.Bytes>) -> Result<&1, &1, U32 & String, String & Bytes.Bytes>:  match r:    case Fail{m}:      Fail{(22, m)}    case Done{body}:      Done{(content_type(b), body)}def form.of(r: Result<&1, &1, U32 & String, String>, parts: List<&1, Part>) -> Result<&1, &1, U32 & String, String & Bytes.Bytes>:  match r:    case Fail{e}:      Fail{e}    case Done{+b}:      form.enc(b, encode(b, parts))# The Content-Type value and body for parts, under a fresh random boundary.# Fails with the random source's error, or EINVAL (22) when encode fails.def form(parts: List<&1, Part>) -> IO(Result<&1, &1, U32 & String, String & Bytes.Bytes>):  do IO<Result<&1, &1, U32 & String, String & Bytes.Bytes>>:    r : Result<&1, &1, U32 & String, String> <- boundary.new()    return form.of(r, parts)# ---- streaming decoder# Pre: before the first delimiter. After: past a delimiter, before its line ends.# Heads: in a header block. Text: in a body. End: past the close delimiter.type Stage is Data:  Pre{}  After{}  Heads{}  Text{}  End{}type Step is Type:  More{stage: Stage, buf: Bytes.Bytes, out: List<&1, Item>}  Pause{stage: Stage, buf: Bytes.Bytes, out: List<&1, Item>}  Bad{error: String}# delim is CRLF "--" boundary and dlen its length; buf holds the bytes not yet decided.type Dec is Type:  Dec{delim: String, dlen: U32, stage: Stage, buf: Bytes.Bytes}# ponytail: a fixed cap on a header block or boundary line; make it a decoder field if a caller needs another.def MAX_HEAD() -> U32:  16384def drop(b: Bytes.Bytes, +n: U32) -> Bytes.Bytes:  Bytes.snd(Bytes.slice(b, n, 4294967295))def keep.two(+cut: U32, +k: U32, r: Bytes.Bytes & Bytes.Bytes) -> Bytes.Bytes & Bytes.Bytes:  (b, h) = r  (h, Bytes.snd(Bytes.slice(b, cut, k)))def keep.of(+k: U32, r: Bytes.Bytes & U32) -> Bytes.Bytes & Bytes.Bytes:  (b, +n) = r  +cut = (n - U32.min(k, n) : U32)  keep.two(cut, k, Bytes.slice(b, 0, cut))# b as (all but its last k bytes, its last k bytes).def keep(b: Bytes.Bytes, +k: U32) -> Bytes.Bytes & Bytes.Bytes:  keep.of(k, Bytes.length(b))def emit(empty: Bool, b: Bytes.Bytes, out: List<&1, Item>) -> List<&1, Item>:  match empty:    case True{}:      out    case False{}:      Con{Body{b}, out}def wait.if(big: Bool, st: Stage, buf: Bytes.Bytes, out: List<&1, Item>) -> Step:  match big:    case True{}:      Bad{"multipart: header block or boundary line too long"}    case False{}:      Pause{st, buf, out}# Wait for more input, unless the undecided bytes already pass MAX_HEAD.def wait.big(st: Stage, r: Bytes.Bytes & U32, out: List<&1, Item>) -> Step:  (buf, +n) = r  wait.if(U32.is_lt(MAX_HEAD(), n), st, buf, out)# What follows a delimiter match (RFC 2046 §5.1.1): "--", or transport padding then CRLF, makes it a# delimiter (0). Anything else makes it data, as "--Bextra" is for boundary B (1). Too few bytes: 2.def suffix.dash(s: String) -> U32:  match s:    case SNil{}:      2    case SCon{Chr{+c}, t}:      Bool.pick(U32, U32.is_eq(c, 45), 0, 1)def suffix.lf(s: String) -> U32:  match s:    case SNil{}:      2    case SCon{Chr{+c}, t}:      Bool.pick(U32, U32.is_eq(c, 10), 0, 1)def suffix(s: String, +first: Bool) -> U32:  match s:    case SNil{}:      2    case SCon{Chr{+c}, +t}:      Bool.pick(U32, Bool.and(first, U32.is_eq(c, 45)), suffix.dash(t), Bool.pick(U32, Bool.or(U32.is_eq(c, 32), U32.is_eq(c, 9)), suffix(t, False{}), Bool.pick(U32, U32.is_eq(c, 13), suffix.lf(t), 1)))# ponytail: looks at 64 bytes past the match; padding longer than that counts as data.def suffix.win(+s: String) -> U32:  +k = suffix(s, True{})  Bool.pick(U32, Bool.and(U32.is_eq(k, 2), U32.is_eq(Bytes.count(s, 0), 64)), 1, k)def suffix.of(r: Bytes.Bytes & Bytes.Bytes) -> Bytes.Bytes & U32:  (buf, w) = r  (buf, suffix.win(Bytes.to_string(w)))# The buffer and the kind of the match at i.def suffix.at(buf: Bytes.Bytes, +i: U32, +dlen: U32) -> Bytes.Bytes & U32:  suffix.of(Bytes.slice(buf, (i + dlen : U32), 64))# Pre: the preamble is dropped. Keeping dlen - 1 bytes means a delimiter split across chunks is still found.# A 0/1/2 suffix kind as two flags, since only a parameter can be matched.def pre.if(delim: Bool, data: Bool, buf: Bytes.Bytes, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step:  match delim:    case True{}:      More{After{}, drop(buf, (i + dlen : U32)), out}    case False{}:      match data:        case True{}:          More{Pre{}, drop(buf, (i + 1 : U32)), out}        case False{}:          Pause{Pre{}, drop(buf, i), out}def pre.kind(r: Bytes.Bytes & U32, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step:  (buf, +k) = r  pre.if(U32.is_eq(k, 0), U32.is_eq(k, 1), buf, i, dlen, out)def pre.hit(+dlen: U32, r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step:  (buf, hit) = r  match hit:    case Some{+i}:      pre.kind(suffix.at(buf, i, dlen), i, dlen, out)    case None{}:      Pause{Pre{}, Bytes.snd(keep(buf, (dlen - 1 : U32))), out}# After: suffix already saw "--" or padding then CRLF, so both are in buf. "--" closes the body.# The CRLF stays in buf, so the header block is found as CRLF ... CRLF CRLF, empty or not.def after.line(r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step:  (buf, hit) = r  match hit:    case Some{+i}:      More{Heads{}, drop(buf, i), out}    case None{}:      wait.big(After{}, Bytes.length(buf), out)def after.dash(r: Bytes.Bytes & Bool, out: List<&1, Item>) -> Step:  (buf, dash) = r  match dash:    case True{}:      More{End{}, Bytes.new(0), out}    case False{}:      after.line(Bytes.find(buf, "\r\n"), out)# Header block to a Head (RFC 7578 §4.2, §4.4). Other headers are ignored (§4.8).def meta.line(r: String & String, st: Maybe<&2, String> & Maybe<&2, String>) -> Maybe<&2, String> & Maybe<&2, String>:  (n, v) = r  (cd, ct) = st  +key = String.to_lower(String.trim(n))  +val = String.trim(v)  (Bool.pick(Maybe<&2, String>, String.eq(key, "content-disposition"), Some{val}, cd), Bool.pick(Maybe<&2, String>, String.eq(key, "content-type"), Some{val}, ct))def meta.go(lines: List<&2, String>, st: Maybe<&2, String> & Maybe<&2, String>) -> Maybe<&2, String> & Maybe<&2, String>:  match lines:    case Nil{}:      st    case Con{h, t}:      meta.go(t, meta.line(split1(h, 58), st))def head.named(n: Maybe<&2, String>, f: Maybe<&2, String>, ct: Maybe<&2, String>) -> Result<&1, &1, String, Item>:  match n:    case None{}:      Fail{"multipart: part has no name"}    case Some{x}:      Done{Head{unesc(x), unesc.m(f), ct}}def head.form(ok: Bool, +ps: List<&2, Param>, ct: Maybe<&2, String>) -> Result<&1, &1, String, Item>:  match ok:    case False{}:      Fail{"multipart: Content-Disposition is not form-data"}    case True{}:      head.named(lookup(ps, "name"), lookup(ps, "filename"), ct)def head.disp(r: String & String, ct: Maybe<&2, String>) -> Result<&1, &1, String, Item>:  (ty, rest) = r  head.form(String.eq(String.to_lower(String.trim(ty)), "form-data"), params(rest), ct)def head.meta(r: Maybe<&2, String> & Maybe<&2, String>) -> Result<&1, &1, String, Item>:  (cd, ct) = r  match cd:    case None{}:      Fail{"multipart: part has no Content-Disposition"}    case Some{v}:      head.disp(split1(v, 59), ct)# Lines split on LF; trimming drops each CR.def head.parse(block: String) -> Result<&1, &1, String, Item>:  head.meta(meta.go(String.split(block, Chr{10}), (None{}, None{})))def heads.item(h: Result<&1, &1, String, Item>, rest: Bytes.Bytes, out: List<&1, Item>) -> Step:  match h:    case Fail{e}:      Bad{e}    case Done{it}:      More{Text{}, rest, Con{it, out}}def heads.cut(+j: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step:  (buf, block) = r  heads.item(head.parse(Bytes.to_string(block)), drop(buf, (j + 4 : U32)), out)# buf starts with the CRLF that ended the boundary line, so a match at 0 is an empty block.def heads.found(r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step:  (buf, hit) = r  match hit:    case Some{+j}:      heads.cut(j, Bytes.slice(buf, 2, (U32.max(j, 2) - 2 : U32)), out)    case None{}:      wait.big(Heads{}, Bytes.length(buf), out)# Text: bytes that cannot begin a delimiter go out as Body; the last dlen - 1 wait for the next chunk.def text.flush.len(r: Bytes.Bytes & U32, tail: Bytes.Bytes, out: List<&1, Item>) -> Step:  (head, +n) = r  Pause{Text{}, tail, emit(U32.is_eq(n, 0), head, out)}def text.flush(r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step:  (head, tail) = r  text.flush.len(Bytes.length(head), tail, out)def text.cut(+i: U32, +dlen: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step:  (buf, piece) = r  More{After{}, drop(buf, (i + dlen : U32)), Con{Tail{}, emit(U32.is_eq(i, 0), piece, out)}}# A match that is data goes out through its first byte, and the search goes on after it.def text.data(+n: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step:  (buf, piece) = r  More{Text{}, drop(buf, n), emit(False{}, piece, out)}# A match whose suffix has not arrived: the bytes before it go out, and it waits.def text.hold(+i: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step:  (buf, piece) = r  Pause{Text{}, drop(buf, i), emit(U32.is_eq(i, 0), piece, out)}def text.if(delim: Bool, data: Bool, buf: Bytes.Bytes, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step:  match delim:    case True{}:      text.cut(i, dlen, Bytes.slice(buf, 0, i), out)    case False{}:      match data:        case True{}:          text.data((i + 1 : U32), Bytes.slice(buf, 0, (i + 1 : U32)), out)        case False{}:          text.hold(i, Bytes.slice(buf, 0, i), out)def text.kind(r: Bytes.Bytes & U32, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step:  (buf, +k) = r  text.if(U32.is_eq(k, 0), U32.is_eq(k, 1), buf, i, dlen, out)def text.hit(+dlen: U32, r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step:  (buf, hit) = r  match hit:    case Some{+i}:      text.kind(suffix.at(buf, i, dlen), i, dlen, out)    case None{}:      text.flush(keep(buf, (dlen - 1 : U32)), out)def step(st: Stage, +delim: String, +dlen: U32, buf: Bytes.Bytes, out: List<&1, Item>) -> Step:  match st:    case Pre{}:      pre.hit(dlen, Bytes.find(buf, delim), out)    case After{}:      after.dash(Bytes.starts_with(buf, "--"), out)    case Heads{}:      heads.found(Bytes.find(buf, "\r\n\r\n"), out)    case Text{}:      text.hit(dlen, Bytes.find(buf, delim), out)    case End{}:      Pause{End{}, Bytes.new(0), out}def go.stop(+delim: String, +dlen: U32, s: Step) -> Result<&1, &1, String, Dec & List<&1, Item>>:  match s:    case More{st, buf, out}:      Done{(Dec{delim, dlen, st, buf}, List.reverse(&1, Item, out))}    case Pause{st, buf, out}:      Done{(Dec{delim, dlen, st, buf}, List.reverse(&1, Item, out))}    case Bad{e}:      Fail{e}# Each More step consumes a byte, except the two after a delimiter, which itself consumes dlen >= 5,# so len + 8 steps suffice. Running out still returns a sound state: the next feed goes on from it.def go(fuel: Nat, +delim: String, +dlen: U32, s: Step) -> Result<&1, &1, String, Dec & List<&1, Item>>:  match fuel:    case 0n:      go.stop(delim, dlen, s)    case 1n+p:      match s:        case More{st, buf, out}:          go(p, delim, dlen, step(st, delim, dlen, buf, out))        case Pause{st, buf, out}:          go.stop(delim, dlen, Pause{st, buf, out})        case Bad{e}:          Fail{e}# A decoder for boundary b. It starts with a CRLF, so a delimiter at the very start is found like any other.def decoder(b: String) -> Dec:  +delim = "\r\n--" ++ b  Dec{delim, Bytes.count(delim, 0), Pre{}, Bytes.from_string("\r\n")}def feed.go(+delim: String, +dlen: U32, st: Stage, r: Bytes.Bytes & U32) -> Result<&1, &1, String, Dec & List<&1, Item>>:  (buf, +n) = r  go(U32.to_nat((n + 8 : U32)), delim, dlen, More{st, buf, Nil{}})# The next chunk of a body, in any split. Answers the decoder and the items the chunk completes,# or the first error. A body piece is held back only while it could start a delimiter.def feed(d: Dec, chunk: Bytes.Bytes) -> Result<&1, &1, String, Dec & List<&1, Item>>:  Dec{+delim, +dlen, st, buf} = d  feed.go(delim, dlen, st, Bytes.length(Bytes.append(buf, chunk)))# Done once the close delimiter has been read; the epilogue is ignored.def finish(d: Dec) -> Result<&1, &1, String, Unit>:  Dec{delim, dlen, st, buf} = d  match st:    case End{}:      Done{Unit{}}    case _:      Fail{"multipart: body ends before the close delimiter"}def collect.add(ps: List<&1, Part>, b: Bytes.Bytes) -> List<&1, Part>:  match ps:    case Nil{}:      Nil{}    case Con{Part{n, f, c, body}, t}:      Con{Part{n, f, c, Bytes.append(body, b)}, t}# Items folded into parts, latest first.def collect(items: List<&1, Item>, ps: List<&1, Part>) -> List<&1, Part>:  match items:    case Nil{}:      ps    case Con{Head{n, f, c}, t}:      collect(t, Con{Part{n, f, c, Bytes.new(0)}, ps})    case Con{Body{b}, t}:      collect(t, collect.add(ps, b))    case Con{Tail{}, t}:      collect(t, ps)def chunks.fed(r: Result<&1, &1, String, Dec & List<&1, Item>>, ps: List<&1, Part>) -> Result<&1, &1, String, Dec & List<&1, Part>>:  match r:    case Fail{e}:      Fail{e}    case Done{(d, items)}:      Done{(d, collect(items, ps))}def chunks.step(st: Result<&1, &1, String, Dec & List<&1, Part>>, c: Bytes.Bytes) -> Result<&1, &1, String, Dec & List<&1, Part>>:  match st:    case Fail{e}:      Fail{e}    case Done{(d, ps)}:      chunks.fed(feed(d, c), ps)def chunks.fin(r: Result<&1, &1, String, Unit>, ps: List<&1, Part>) -> Result<&1, &1, String, List<&1, Part>>:  match r:    case Fail{e}:      Fail{e}    case Done{u}:      Done{List.reverse(&1, Part, ps)}def chunks.end(st: Result<&1, &1, String, Dec & List<&1, Part>>) -> Result<&1, &1, String, List<&1, Part>>:  match st:    case Fail{e}:      Fail{e}    case Done{(d, ps)}:      chunks.fin(finish(d), ps)def chunks.go(cs: List<&1, Bytes.Bytes>, st: Result<&1, &1, String, Dec & List<&1, Part>>) -> Result<&1, &1, String, List<&1, Part>>:  match cs:    case Nil{}:      chunks.end(st)    case Con{c, t}:      chunks.go(t, chunks.step(st, c))# The parts of a body that arrives as chunks, split anywhere.def decode.chunks(b: String, cs: List<&1, Bytes.Bytes>) -> Result<&1, &1, String, List<&1, Part>>:  chunks.go(cs, Done{(decoder(b), Nil{})})# The parts of a whole body.def decode(b: String, body: Bytes.Bytes) -> Result<&1, &1, String, List<&1, Part>>:  decode.chunks(b, [body])