multipart.bend relies on unsafe/foreign
raw source on the hub · import bend-kit-multipart@0.1.1.1/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.2.2.1/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).
Part@name:String -> @filename:Maybe<&2, String> -> @ctype:Maybe<&2, String> -> @body:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Part
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.
Head@name:String -> @filename:Maybe<&2, String> -> @ctype:Maybe<&2, String> -> Item
Body@chunk:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Item
TailItem
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.
CEqCls
CSemiCls
CQuoteCls
CSlashCls
CSpCls
COtherCls
type Pm source · line 45 · raw
Data
PKeyPm
PStartPm
PQuoPm
PEscPm
PTokPm
PSkipPm
type Param source · line 53 · raw
Data
Param@key:String -> @val:String -> Param
type PS source · line 57 · raw
Data
mode, the key and value so far (reversed), and the parameters found (latest first).
PS@mode:Pm -> @key:String -> @val:String -> @acc:List<&2, Param> -> PS
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.
PreStage
AfterStage
HeadsStage
TextStage
EndStage
type Step source · line 327 · raw
Type
More@stage:Stage -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> Step
Pause@stage:Stage -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @out:List<&1, Item> -> Step
Bad@error:String -> Step
type Dec source · line 333 · raw
Type
delim is CRLF "--" boundary and dlen its length; buf holds the bytes not yet decided.
Dec@delim:String -> @dlen:U32 -> @stage:Stage -> @buf:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Dec
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.