~/bend-docscommunity

multipart.bend relies on unsafe/foreign

raw source on the hub · import bend-kit-multipart@0.1.1.0/multipart.bend as Multipart

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/multipart

3 imports
import Base
import bend-kit-bytes@0.3.1.0/bytes.bend as Bytes
import bend-kit-crypto@0.1.0.0/crypto.bend as Crypto

Types

type Part source · line 12 · raw

Type

One form field. filename marks a file; ctype is its Content-Type (RFC 7578 §4.4 defaults to text/plain).

type Item source · line 16 · raw

Type

What the streaming decoder reports, in order: Head, then zero or more Body pieces, then Tail, per part.

type Cls source · line 37 · raw

Data

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 Pm source · line 45 · raw

Data

type Param source · line 53 · raw

Data

type PS source · line 57 · raw

Data

mode, the key and value so far (reversed), and the parameters found (latest first).

type Stage source · line 320 · raw

Data

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 Step source · line 327 · raw

Type

type Dec source · line 333 · raw

Type

delim is CRLF "--" boundary and dlen its length; buf holds the bytes not yet decided.

Definitions

def split1.cons source · line 24 · raw

@+h:U32 -> @r:Pair(String, String) -> Pair(String, String)

The text before the first c, and the text after it. No c gives (s, "").

def split1 source · line 28 · raw

@s:String -> @+c:U32 -> Pair(String, String)

def cls source · line 60 · raw

@+c:U32 -> Cls

def params.emit source · line 63 · raw

@key:String -> @val:String -> @acc:List<&2, Param> -> List<&2, Param>

def params.step source · line 66 · raw

@st:PS -> @k:Cls -> @+c:U32 -> PS

def params.end source · line 112 · raw

@st:PS -> List<&2, Param>

def params.go source · line 122 · raw

@s:String -> @st:PS -> PS

def params source · line 130 · raw

@s:String -> List<&2, Param>

The parameters of a header value's text after its first ';', in order.

def lookup source · line 134 · raw

@xs:List<&2, Param> -> @+k:String -> Maybe<&2, String>

The first parameter named k (lowercase).

def esc.one source · line 142 · raw

@+c:U32 -> String

WHATWG form-data escapes for names and filenames.

def esc source · line 145 · raw

@s:String -> String

def unesc.fix source · line 153 · raw

@+a:String -> String

acc is reversed, so "%22" shows as "22%".

def unesc.go source · line 156 · raw

@s:String -> @acc:String -> String

def unesc source · line 164 · raw

@s:String -> String

Undoes esc, as the Fetch spec's form-data parser does. A literal "%22" in a name reads back as '"'.

def unesc.m source · line 167 · raw

@m:Maybe<&2, String> -> Maybe<&2, String>

def bchar source · line 177 · raw

@+c:U32 -> Bool

RFC 2046 §5.1.1 bchars: DIGIT ALPHA ' ( ) + _ , - . / : = ? and space.

def bchars source · line 180 · raw

@s:String -> Bool

def boundary.ok source · line 188 · raw

@+b:String -> Bool

1 to 70 bchars, not ending in a space.

def boundary.words source · line 192 · raw

@r:Result<&1, &1, Pair(U32, String), Pair(U32, Array<U32>)> -> Result<&1, &1, Pair(U32, String), String>

def boundary.new source · line 200 · raw

IO(Result<&1, &1, Pair(U32, String), String>)

"bend-kit-" and 128 bits from the OS secure random source, as hex: 41 bchars.

def content_type source · line 206 · raw

@b:String -> String

The Content-Type header value for a body encoded with boundary b.

def boundary.get.of source · line 209 · raw

@r:Pair(String, String) -> Maybe<&2, String>

def boundary.get source · line 214 · raw

@ct:String -> Maybe<&2, String>

The boundary parameter of a Content-Type value, such as a request's, or None.

def enc.fname source · line 219 · raw

@m:Maybe<&2, String> -> String

def enc.ctype source · line 226 · raw

@m:Maybe<&2, String> -> String

def enc.head source · line 233 · raw

@+b:String -> @name:String -> @fname:Maybe<&2, String> -> @ct:Maybe<&2, String> -> String

def enc.ctype.ok source · line 237 · raw

@m:Maybe<&2, String> -> Bool

A Content-Type with CR or LF would inject header lines.

def enc.found source · line 244 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> @+head:String -> @xs:List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes> -> Result<&1, &1, String, List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>>

def enc.body source · line 252 · raw

@ok:Bool -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> @+head:String -> @xs:List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes> -> Result<&1, &1, String, List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>>

def enc.part source · line 261 · raw

@acc:Result<&1, &1, String, List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>> -> @p:Part -> @+b:String -> @+delim:String -> Result<&1, &1, String, List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>>

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.fin source · line 269 · raw

@+b:String -> @acc:Result<&1, &1, String, List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>> -> Result<&1, &1, String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>

def enc.go source · line 276 · raw

@parts:List<&1, Part> -> @+b:String -> @+delim:String -> @acc:Result<&1, &1, String, List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>> -> Result<&1, &1, String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>

def encode.if source · line 283 · raw

@ok:Bool -> @+b:String -> @parts:List<&1, Part> -> Result<&1, &1, String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>

def encode source · line 292 · raw

@+b:String -> @parts:List<&1, Part> -> Result<&1, &1, String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>

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 form.enc source · line 295 · raw

@+b:String -> @r:Result<&1, &1, String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes> -> Result<&1, &1, Pair(U32, String), Pair(String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes)>

def form.of source · line 302 · raw

@r:Result<&1, &1, Pair(U32, String), String> -> @parts:List<&1, Part> -> Result<&1, &1, Pair(U32, String), Pair(String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes)>

def form source · line 311 · raw

@parts:List<&1, Part> -> IO(Result<&1, &1, Pair(U32, String), Pair(String, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes)>)

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 MAX_HEAD source · line 337 · raw

U32

ponytail: a fixed cap on a header block or boundary line; make it a decoder field if a caller needs another.

def drop source · line 340 · raw

@b:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @+n:U32 -> 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes

def keep.two source · line 343 · raw

@+cut:U32 -> @+k:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes)

def keep.of source · line 347 · raw

@+k:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32) -> Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes)

def keep source · line 353 · raw

@b:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @+k:U32 -> Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes)

b as (all but its last k bytes, its last k bytes).

def emit source · line 356 · raw

@empty:Bool -> @b:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> List<&1, Item>

def wait.if source · line 363 · raw

@big:Bool -> @st:Stage -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> Step

def wait.big source · line 371 · raw

@st:Stage -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32) -> @out:List<&1, Item> -> Step

Wait for more input, unless the undecided bytes already pass MAX_HEAD.

def suffix.dash source · line 377 · raw

@s:String -> U32

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.lf source · line 384 · raw

@s:String -> U32

def suffix source · line 391 · raw

@s:String -> @+first:Bool -> U32

def suffix.win source · line 399 · raw

@+s:String -> U32

ponytail: looks at 64 bytes past the match; padding longer than that counts as data.

def suffix.of source · line 403 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32)

def suffix.at source · line 408 · raw

@buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @+i:U32 -> @+dlen:U32 -> Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32)

The buffer and the kind of the match at i.

def pre.if source · line 413 · raw

@delim:Bool -> @data:Bool -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @+i:U32 -> @+dlen:U32 -> @out:List<&1, Item> -> Step

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.kind source · line 424 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32) -> @+i:U32 -> @+dlen:U32 -> @out:List<&1, Item> -> Step

def pre.hit source · line 428 · raw

@+dlen:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> @out:List<&1, Item> -> Step

def after.line source · line 438 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> @out:List<&1, Item> -> Step

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.dash source · line 446 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Bool) -> @out:List<&1, Item> -> Step

def meta.line source · line 455 · raw

@r:Pair(String, String) -> @st:Pair(Maybe<&2, String>, Maybe<&2, String>) -> Pair(Maybe<&2, String>, Maybe<&2, String>)

Header block to a Head (RFC 7578 §4.2, §4.4). Other headers are ignored (§4.8).

def meta.go source · line 462 · raw

@lines:List<&2, String> -> @st:Pair(Maybe<&2, String>, Maybe<&2, String>) -> Pair(Maybe<&2, String>, Maybe<&2, String>)

def head.named source · line 469 · raw

@n:Maybe<&2, String> -> @f:Maybe<&2, String> -> @ct:Maybe<&2, String> -> Result<&1, &1, String, Item>

def head.form source · line 476 · raw

@ok:Bool -> @+ps:List<&2, Param> -> @ct:Maybe<&2, String> -> Result<&1, &1, String, Item>

def head.disp source · line 483 · raw

@r:Pair(String, String) -> @ct:Maybe<&2, String> -> Result<&1, &1, String, Item>

def head.meta source · line 487 · raw

@r:Pair(Maybe<&2, String>, Maybe<&2, String>) -> Result<&1, &1, String, Item>

def head.parse source · line 496 · raw

@block:String -> Result<&1, &1, String, Item>

Lines split on LF; trimming drops each CR.

def heads.item source · line 499 · raw

@h:Result<&1, &1, String, Item> -> @rest:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> Step

def heads.cut source · line 506 · raw

@+j:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> @out:List<&1, Item> -> Step

def heads.found source · line 511 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> @out:List<&1, Item> -> Step

buf starts with the CRLF that ended the boundary line, so a match at 0 is an empty block.

def text.flush.len source · line 520 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32) -> @tail:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> Step

Text: bytes that cannot begin a delimiter go out as Body; the last dlen - 1 wait for the next chunk.

def text.flush source · line 524 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> @out:List<&1, Item> -> Step

def text.cut source · line 528 · raw

@+i:U32 -> @+dlen:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> @out:List<&1, Item> -> Step

def text.data source · line 533 · raw

@+n:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> @out:List<&1, Item> -> Step

A match that is data goes out through its first byte, and the search goes on after it.

def text.hold source · line 538 · raw

@+i:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes) -> @out:List<&1, Item> -> Step

A match whose suffix has not arrived: the bytes before it go out, and it waits.

def text.if source · line 542 · raw

@delim:Bool -> @data:Bool -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @+i:U32 -> @+dlen:U32 -> @out:List<&1, Item> -> Step

def text.kind source · line 553 · raw

@r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32) -> @+i:U32 -> @+dlen:U32 -> @out:List<&1, Item> -> Step

def text.hit source · line 557 · raw

@+dlen:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> @out:List<&1, Item> -> Step

def step source · line 565 · raw

@st:Stage -> @+delim:String -> @+dlen:U32 -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> Step

def go.stop source · line 578 · raw

@+delim:String -> @+dlen:U32 -> @s:Step -> Result<&1, &1, String, Pair(Dec, List<&1, Item>)>

def go source · line 589 · raw

@fuel:Nat -> @+delim:String -> @+dlen:U32 -> @s:Step -> Result<&1, &1, String, Pair(Dec, List<&1, Item>)>

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 decoder source · line 603 · raw

@b:String -> Dec

A decoder for boundary b. It starts with a CRLF, so a delimiter at the very start is found like any other.

def feed.go source · line 607 · raw

@+delim:String -> @+dlen:U32 -> @st:Stage -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, U32) -> Result<&1, &1, String, Pair(Dec, List<&1, Item>)>

def feed source · line 613 · raw

@d:Dec -> @chunk:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Result<&1, &1, String, Pair(Dec, List<&1, Item>)>

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 finish source · line 618 · raw

@d:Dec -> Result<&1, &1, String, Unit>

Done once the close delimiter has been read; the epilogue is ignored.

def collect.add source · line 626 · raw

@ps:List<&1, Part> -> @b:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> List<&1, Part>

def collect source · line 634 · raw

@items:List<&1, Item> -> @ps:List<&1, Part> -> List<&1, Part>

Items folded into parts, latest first.

def chunks.fed source · line 645 · raw

@r:Result<&1, &1, String, Pair(Dec, List<&1, Item>)> -> @ps:List<&1, Part> -> Result<&1, &1, String, Pair(Dec, List<&1, Part>)>

def chunks.step source · line 652 · raw

@st:Result<&1, &1, String, Pair(Dec, List<&1, Part>)> -> @c:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Result<&1, &1, String, Pair(Dec, List<&1, Part>)>

def chunks.fin source · line 659 · raw

@r:Result<&1, &1, String, Unit> -> @ps:List<&1, Part> -> Result<&1, &1, String, List<&1, Part>>

def chunks.end source · line 666 · raw

@st:Result<&1, &1, String, Pair(Dec, List<&1, Part>)> -> Result<&1, &1, String, List<&1, Part>>

def chunks.go source · line 673 · raw

@cs:List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes> -> @st:Result<&1, &1, String, Pair(Dec, List<&1, Part>)> -> Result<&1, &1, String, List<&1, Part>>

def decode.chunks source · line 681 · raw

@b:String -> @cs:List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes> -> Result<&1, &1, String, List<&1, Part>>

The parts of a body that arrives as chunks, split anywhere.

def decode source · line 685 · raw

@b:String -> @body:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Result<&1, &1, String, List<&1, Part>>

The parts of a whole body.