1 # 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 2 import Base 3 import bend-kit-bytes@0.3.1.0/bytes.bend as Bytes 4 import bend-kit-crypto@0.1.0.0/crypto.bend as Crypto 5 6 # Names, filenames, and header values are byte strings, one Char per octet, as in Http. 7 # Bodies are Bytes and pass through untouched, so binary payloads keep every octet. 8 # Names and filenames are escaped as WHATWG does: '"' is %22, CR is %0D, LF is %0A. 9 # import bend-kit-multipart@0.1.0.0/multipart.bend as Mp 10 11 # One form field. filename marks a file; ctype is its Content-Type (RFC 7578 §4.4 defaults to text/plain). 12 type Part is Type: 13 Part{name: String, filename: Maybe<&2, String>, ctype: Maybe<&2, String>, body: Bytes.Bytes} 14 15 # What the streaming decoder reports, in order: Head, then zero or more Body pieces, then Tail, per part. 16 type Item is Type: 17 Head{name: String, filename: Maybe<&2, String>, ctype: Maybe<&2, String>} 18 Body{chunk: Bytes.Bytes} 19 Tail{} 20 21 # ---- shared text helpers 22 23 # The text before the first c, and the text after it. No c gives (s, ""). 24 def split1.cons(+h: U32, r: String & String) -> String & String: 25 (a, b) = r 26 (SCon{Chr{h}, a}, b) 27 28 def split1(s: String, +c: U32) -> String & String: 29 match s: 30 case SNil{}: 31 (SNil{}, SNil{}) 32 case SCon{Chr{+h}, +t}: 33 Bool.pick(String & String, U32.is_eq(h, c), (SNil{}, t), split1.cons(h, split1(t, c))) 34 35 # Header parameters (RFC 2045 §5.1, RFC 7578 §4.2): key=token or key="quoted", split on ';'. 36 # Keys are lowercased; a backslash escapes the next char inside quotes. An unterminated quote drops its parameter. 37 type Cls is Data: 38 CEq{} 39 CSemi{} 40 CQuote{} 41 CSlash{} 42 CSp{} 43 COther{} 44 45 type Pm is Data: 46 PKey{} 47 PStart{} 48 PQuo{} 49 PEsc{} 50 PTok{} 51 PSkip{} 52 53 type Param is Data: 54 Param{key: String, val: String} 55 56 # mode, the key and value so far (reversed), and the parameters found (latest first). 57 type PS is Data: 58 PS{mode: Pm, key: String, val: String, acc: List<&2, Param>} 59 60 def cls(+c: U32) -> Cls: 61 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{}))))) 62 63 def params.emit(key: String, val: String, acc: List<&2, Param>) -> List<&2, Param>: 64 Con{Param{String.reverse(key), String.reverse(val)}, acc} 65 66 def params.step(st: PS, k: Cls, +c: U32) -> PS: 67 PS{mode, key, val, acc} = st 68 match mode: 69 case PKey{}: 70 match k: 71 case CEq{}: 72 PS{PStart{}, key, SNil{}, acc} 73 case CSemi{}: 74 PS{PKey{}, SNil{}, SNil{}, acc} 75 case CSp{}: 76 PS{PKey{}, key, val, acc} 77 case _: 78 PS{PKey{}, SCon{Char.to_lower(Chr{c}), key}, val, acc} 79 case PStart{}: 80 match k: 81 case CQuote{}: 82 PS{PQuo{}, key, SNil{}, acc} 83 case CSp{}: 84 PS{PStart{}, key, val, acc} 85 case CSemi{}: 86 PS{PKey{}, SNil{}, SNil{}, params.emit(key, SNil{}, acc)} 87 case _: 88 PS{PTok{}, key, SCon{Chr{c}, SNil{}}, acc} 89 case PQuo{}: 90 match k: 91 case CSlash{}: 92 PS{PEsc{}, key, val, acc} 93 case CQuote{}: 94 PS{PSkip{}, SNil{}, SNil{}, params.emit(key, val, acc)} 95 case _: 96 PS{PQuo{}, key, SCon{Chr{c}, val}, acc} 97 case PEsc{}: 98 PS{PQuo{}, key, SCon{Chr{c}, val}, acc} 99 case PTok{}: 100 match k: 101 case CSemi{}: 102 PS{PKey{}, SNil{}, SNil{}, params.emit(key, String.trim_start(val), acc)} 103 case _: 104 PS{PTok{}, key, SCon{Chr{c}, val}, acc} 105 case PSkip{}: 106 match k: 107 case CSemi{}: 108 PS{PKey{}, SNil{}, SNil{}, acc} 109 case _: 110 PS{PSkip{}, key, val, acc} 111 112 def params.end(st: PS) -> List<&2, Param>: 113 PS{mode, key, val, acc} = st 114 match mode: 115 case PStart{}: 116 params.emit(key, SNil{}, acc) 117 case PTok{}: 118 params.emit(key, String.trim_start(val), acc) 119 case _: 120 acc 121 122 def params.go(s: String, st: PS) -> PS: 123 match s: 124 case SNil{}: 125 st 126 case SCon{Chr{+c}, t}: 127 params.go(t, params.step(st, cls(c), c)) 128 129 # The parameters of a header value's text after its first ';', in order. 130 def params(s: String) -> List<&2, Param>: 131 List.reverse(&2, Param, params.end(params.go(s, PS{PKey{}, SNil{}, SNil{}, Nil{}}))) 132 133 # The first parameter named k (lowercase). 134 def lookup(xs: List<&2, Param>, +k: String) -> Maybe<&2, String>: 135 match xs: 136 case Nil{}: 137 None{} 138 case Con{Param{+k2, +v}, t}: 139 Bool.pick(Maybe<&2, String>, String.eq(k2, k), Some{v}, lookup(t, k)) 140 141 # WHATWG form-data escapes for names and filenames. 142 def esc.one(+c: U32) -> String: 143 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{}}))) 144 145 def esc(s: String) -> String: 146 match s: 147 case SNil{}: 148 SNil{} 149 case SCon{Chr{+c}, t}: 150 esc.one(c) ++ esc(t) 151 152 # acc is reversed, so "%22" shows as "22%". 153 def unesc.fix(+a: String) -> String: 154 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))) 155 156 def unesc.go(s: String, acc: String) -> String: 157 match s: 158 case SNil{}: 159 acc 160 case SCon{c, t}: 161 unesc.go(t, unesc.fix(SCon{c, acc})) 162 163 # Undoes esc, as the Fetch spec's form-data parser does. A literal "%22" in a name reads back as '"'. 164 def unesc(s: String) -> String: 165 String.reverse(unesc.go(s, SNil{})) 166 167 def unesc.m(m: Maybe<&2, String>) -> Maybe<&2, String>: 168 match m: 169 case None{}: 170 None{} 171 case Some{x}: 172 Some{unesc(x)} 173 174 # ---- boundary 175 176 # RFC 2046 §5.1.1 bchars: DIGIT ALPHA ' ( ) + _ , - . / : = ? and space. 177 def bchar(+c: U32) -> Bool: 178 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)))) 179 180 def bchars(s: String) -> Bool: 181 match s: 182 case SNil{}: 183 True{} 184 case SCon{Chr{+c}, t}: 185 Bool.and(bchar(c), bchars(t)) 186 187 # 1 to 70 bchars, not ending in a space. 188 def boundary.ok(+b: String) -> Bool: 189 +n = Bytes.count(b, 0) 190 Bool.and(Bool.and(U32.is_le(1, n), U32.is_le(n, 70)), Bool.and(bchars(b), Bool.not(String.ends_with(b, " ")))) 191 192 def boundary.words(r: Result<&1, &1, U32 & String, U32 & Array>) -> Result<&1, &1, U32 & String, String>: 193 match r: 194 case Fail{e}: 195 Fail{e} 196 case Done{(+n, w)}: 197 Done{"bend-kit-" ++ Bytes.to_hex(Bytes.Bytes{n, w})} 198 199 # "bend-kit-" and 128 bits from the OS secure random source, as hex: 41 bchars. 200 def boundary.new() -> IO(Result<&1, &1, U32 & String, String>): 201 do IO>: 202 r : Result<&1, &1, U32 & String, U32 & Array> <- Crypto.random.words(16) 203 return boundary.words(r) 204 205 # The Content-Type header value for a body encoded with boundary b. 206 def content_type(b: String) -> String: 207 "multipart/form-data; boundary=" ++ b 208 209 def boundary.get.of(r: String & String) -> Maybe<&2, String>: 210 (ty, rest) = r 211 lookup(params(rest), "boundary") 212 213 # The boundary parameter of a Content-Type value, such as a request's, or None. 214 def boundary.get(ct: String) -> Maybe<&2, String>: 215 boundary.get.of(split1(ct, 59)) 216 217 # ---- encoder 218 219 def enc.fname(m: Maybe<&2, String>) -> String: 220 match m: 221 case None{}: 222 SNil{} 223 case Some{f}: 224 "; filename=\"" ++ esc(f) ++ "\"" 225 226 def enc.ctype(m: Maybe<&2, String>) -> String: 227 match m: 228 case None{}: 229 SNil{} 230 case Some{c}: 231 "Content-Type: " ++ c ++ "\r\n" 232 233 def enc.head(+b: String, name: String, fname: Maybe<&2, String>, ct: Maybe<&2, String>) -> String: 234 "--" ++ b ++ "\r\nContent-Disposition: form-data; name=\"" ++ esc(name) ++ "\"" ++ enc.fname(fname) ++ "\r\n" ++ enc.ctype(ct) ++ "\r\n" 235 236 # A Content-Type with CR or LF would inject header lines. 237 def enc.ctype.ok(m: Maybe<&2, String>) -> Bool: 238 match m: 239 case None{}: 240 True{} 241 case Some{+c}: 242 Bool.not(Bool.or(String.contains(c, "\r"), String.contains(c, "\n"))) 243 244 def enc.found(r: Bytes.Bytes & Maybe<&2, U32>, +head: String, xs: List<&1, Bytes.Bytes>) -> Result<&1, &1, String, List<&1, Bytes.Bytes>>: 245 (body, hit) = r 246 match hit: 247 case Some{i}: 248 Fail{"multipart: boundary occurs in a part body"} 249 case None{}: 250 Done{Con{Bytes.from_string("\r\n"), Con{body, Con{Bytes.from_string(head), xs}}}} 251 252 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>>: 253 match ok: 254 case False{}: 255 Fail{"multipart: Content-Type has CR or LF"} 256 case True{}: 257 enc.found(r, head, xs) 258 259 # The pieces so far, latest first. A body cannot hold CRLF "--" boundary; bchars hold no CR, 260 # so the delimiter cannot start inside the body and end in the one after it. 261 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>>: 262 match acc: 263 case Fail{e}: 264 Fail{e} 265 case Done{xs}: 266 Part{name, fname, +ct, body} = p 267 enc.body(enc.ctype.ok(ct), Bytes.find(body, delim), enc.head(b, name, fname, ct), xs) 268 269 def enc.fin(+b: String, acc: Result<&1, &1, String, List<&1, Bytes.Bytes>>) -> Result<&1, &1, String, Bytes.Bytes>: 270 match acc: 271 case Fail{e}: 272 Fail{e} 273 case Done{xs}: 274 Done{Bytes.concat(List.reverse(&1, Bytes.Bytes, Con{Bytes.from_string("--" ++ b ++ "--\r\n"), xs}))} 275 276 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>: 277 match parts: 278 case Nil{}: 279 enc.fin(b, acc) 280 case Con{p, t}: 281 enc.go(t, b, delim, enc.part(acc, p, b, delim)) 282 283 def encode.if(ok: Bool, +b: String, parts: List<&1, Part>) -> Result<&1, &1, String, Bytes.Bytes>: 284 match ok: 285 case False{}: 286 Fail{"multipart: boundary must be 1 to 70 RFC 2046 bchars, not ending in a space"} 287 case True{}: 288 enc.go(parts, b, "\r\n--" ++ b, Done{Nil{}}) 289 290 # The body for parts under boundary b (RFC 7578 §4.1). Fails when b is not a valid boundary, 291 # b's delimiter occurs in a body, or a Content-Type holds CR or LF. 292 def encode(+b: String, parts: List<&1, Part>) -> Result<&1, &1, String, Bytes.Bytes>: 293 encode.if(boundary.ok(b), b, parts) 294 295 def form.enc(+b: String, r: Result<&1, &1, String, Bytes.Bytes>) -> Result<&1, &1, U32 & String, String & Bytes.Bytes>: 296 match r: 297 case Fail{m}: 298 Fail{(22, m)} 299 case Done{body}: 300 Done{(content_type(b), body)} 301 302 def form.of(r: Result<&1, &1, U32 & String, String>, parts: List<&1, Part>) -> Result<&1, &1, U32 & String, String & Bytes.Bytes>: 303 match r: 304 case Fail{e}: 305 Fail{e} 306 case Done{+b}: 307 form.enc(b, encode(b, parts)) 308 309 # The Content-Type value and body for parts, under a fresh random boundary. 310 # Fails with the random source's error, or EINVAL (22) when encode fails. 311 def form(parts: List<&1, Part>) -> IO(Result<&1, &1, U32 & String, String & Bytes.Bytes>): 312 do IO>: 313 r : Result<&1, &1, U32 & String, String> <- boundary.new() 314 return form.of(r, parts) 315 316 # ---- streaming decoder 317 318 # Pre: before the first delimiter. After: past a delimiter, before its line ends. 319 # Heads: in a header block. Text: in a body. End: past the close delimiter. 320 type Stage is Data: 321 Pre{} 322 After{} 323 Heads{} 324 Text{} 325 End{} 326 327 type Step is Type: 328 More{stage: Stage, buf: Bytes.Bytes, out: List<&1, Item>} 329 Wait{stage: Stage, buf: Bytes.Bytes, out: List<&1, Item>} 330 Bad{error: String} 331 332 # delim is CRLF "--" boundary and dlen its length; buf holds the bytes not yet decided. 333 type Dec is Type: 334 Dec{delim: String, dlen: U32, stage: Stage, buf: Bytes.Bytes} 335 336 # ponytail: a fixed cap on a header block or boundary line; make it a decoder field if a caller needs another. 337 def MAX_HEAD() -> U32: 338 16384 339 340 def drop(b: Bytes.Bytes, +n: U32) -> Bytes.Bytes: 341 Bytes.snd(Bytes.slice(b, n, 4294967295)) 342 343 def keep.two(+cut: U32, +k: U32, r: Bytes.Bytes & Bytes.Bytes) -> Bytes.Bytes & Bytes.Bytes: 344 (b, h) = r 345 (h, Bytes.snd(Bytes.slice(b, cut, k))) 346 347 def keep.of(+k: U32, r: Bytes.Bytes & U32) -> Bytes.Bytes & Bytes.Bytes: 348 (b, +n) = r 349 +cut = (n - U32.min(k, n) : U32) 350 keep.two(cut, k, Bytes.slice(b, 0, cut)) 351 352 # b as (all but its last k bytes, its last k bytes). 353 def keep(b: Bytes.Bytes, +k: U32) -> Bytes.Bytes & Bytes.Bytes: 354 keep.of(k, Bytes.length(b)) 355 356 def emit(empty: Bool, b: Bytes.Bytes, out: List<&1, Item>) -> List<&1, Item>: 357 match empty: 358 case True{}: 359 out 360 case False{}: 361 Con{Body{b}, out} 362 363 def wait.if(big: Bool, st: Stage, buf: Bytes.Bytes, out: List<&1, Item>) -> Step: 364 match big: 365 case True{}: 366 Bad{"multipart: header block or boundary line too long"} 367 case False{}: 368 Wait{st, buf, out} 369 370 # Wait for more input, unless the undecided bytes already pass MAX_HEAD. 371 def wait.big(st: Stage, r: Bytes.Bytes & U32, out: List<&1, Item>) -> Step: 372 (buf, +n) = r 373 wait.if(U32.is_lt(MAX_HEAD(), n), st, buf, out) 374 375 # What follows a delimiter match (RFC 2046 §5.1.1): "--", or transport padding then CRLF, makes it a 376 # delimiter (0). Anything else makes it data, as "--Bextra" is for boundary B (1). Too few bytes: 2. 377 def suffix.dash(s: String) -> U32: 378 match s: 379 case SNil{}: 380 2 381 case SCon{Chr{+c}, t}: 382 Bool.pick(U32, U32.is_eq(c, 45), 0, 1) 383 384 def suffix.lf(s: String) -> U32: 385 match s: 386 case SNil{}: 387 2 388 case SCon{Chr{+c}, t}: 389 Bool.pick(U32, U32.is_eq(c, 10), 0, 1) 390 391 def suffix(s: String, +first: Bool) -> U32: 392 match s: 393 case SNil{}: 394 2 395 case SCon{Chr{+c}, +t}: 396 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))) 397 398 # ponytail: looks at 64 bytes past the match; padding longer than that counts as data. 399 def suffix.win(+s: String) -> U32: 400 +k = suffix(s, True{}) 401 Bool.pick(U32, Bool.and(U32.is_eq(k, 2), U32.is_eq(Bytes.count(s, 0), 64)), 1, k) 402 403 def suffix.of(r: Bytes.Bytes & Bytes.Bytes) -> Bytes.Bytes & U32: 404 (buf, w) = r 405 (buf, suffix.win(Bytes.to_string(w))) 406 407 # The buffer and the kind of the match at i. 408 def suffix.at(buf: Bytes.Bytes, +i: U32, +dlen: U32) -> Bytes.Bytes & U32: 409 suffix.of(Bytes.slice(buf, (i + dlen : U32), 64)) 410 411 # Pre: the preamble is dropped. Keeping dlen - 1 bytes means a delimiter split across chunks is still found. 412 # A 0/1/2 suffix kind as two flags, since only a parameter can be matched. 413 def pre.if(delim: Bool, data: Bool, buf: Bytes.Bytes, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step: 414 match delim: 415 case True{}: 416 More{After{}, drop(buf, (i + dlen : U32)), out} 417 case False{}: 418 match data: 419 case True{}: 420 More{Pre{}, drop(buf, (i + 1 : U32)), out} 421 case False{}: 422 Wait{Pre{}, drop(buf, i), out} 423 424 def pre.kind(r: Bytes.Bytes & U32, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step: 425 (buf, +k) = r 426 pre.if(U32.is_eq(k, 0), U32.is_eq(k, 1), buf, i, dlen, out) 427 428 def pre.hit(+dlen: U32, r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step: 429 (buf, hit) = r 430 match hit: 431 case Some{+i}: 432 pre.kind(suffix.at(buf, i, dlen), i, dlen, out) 433 case None{}: 434 Wait{Pre{}, Bytes.snd(keep(buf, (dlen - 1 : U32))), out} 435 436 # After: suffix already saw "--" or padding then CRLF, so both are in buf. "--" closes the body. 437 # The CRLF stays in buf, so the header block is found as CRLF ... CRLF CRLF, empty or not. 438 def after.line(r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step: 439 (buf, hit) = r 440 match hit: 441 case Some{+i}: 442 More{Heads{}, drop(buf, i), out} 443 case None{}: 444 wait.big(After{}, Bytes.length(buf), out) 445 446 def after.dash(r: Bytes.Bytes & Bool, out: List<&1, Item>) -> Step: 447 (buf, dash) = r 448 match dash: 449 case True{}: 450 More{End{}, Bytes.new(0), out} 451 case False{}: 452 after.line(Bytes.find(buf, "\r\n"), out) 453 454 # Header block to a Head (RFC 7578 §4.2, §4.4). Other headers are ignored (§4.8). 455 def meta.line(r: String & String, st: Maybe<&2, String> & Maybe<&2, String>) -> Maybe<&2, String> & Maybe<&2, String>: 456 (n, v) = r 457 (cd, ct) = st 458 +key = String.to_lower(String.trim(n)) 459 +val = String.trim(v) 460 (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)) 461 462 def meta.go(lines: List<&2, String>, st: Maybe<&2, String> & Maybe<&2, String>) -> Maybe<&2, String> & Maybe<&2, String>: 463 match lines: 464 case Nil{}: 465 st 466 case Con{h, t}: 467 meta.go(t, meta.line(split1(h, 58), st)) 468 469 def head.named(n: Maybe<&2, String>, f: Maybe<&2, String>, ct: Maybe<&2, String>) -> Result<&1, &1, String, Item>: 470 match n: 471 case None{}: 472 Fail{"multipart: part has no name"} 473 case Some{x}: 474 Done{Head{unesc(x), unesc.m(f), ct}} 475 476 def head.form(ok: Bool, +ps: List<&2, Param>, ct: Maybe<&2, String>) -> Result<&1, &1, String, Item>: 477 match ok: 478 case False{}: 479 Fail{"multipart: Content-Disposition is not form-data"} 480 case True{}: 481 head.named(lookup(ps, "name"), lookup(ps, "filename"), ct) 482 483 def head.disp(r: String & String, ct: Maybe<&2, String>) -> Result<&1, &1, String, Item>: 484 (ty, rest) = r 485 head.form(String.eq(String.to_lower(String.trim(ty)), "form-data"), params(rest), ct) 486 487 def head.meta(r: Maybe<&2, String> & Maybe<&2, String>) -> Result<&1, &1, String, Item>: 488 (cd, ct) = r 489 match cd: 490 case None{}: 491 Fail{"multipart: part has no Content-Disposition"} 492 case Some{v}: 493 head.disp(split1(v, 59), ct) 494 495 # Lines split on LF; trimming drops each CR. 496 def head.parse(block: String) -> Result<&1, &1, String, Item>: 497 head.meta(meta.go(String.split(block, Chr{10}), (None{}, None{}))) 498 499 def heads.item(h: Result<&1, &1, String, Item>, rest: Bytes.Bytes, out: List<&1, Item>) -> Step: 500 match h: 501 case Fail{e}: 502 Bad{e} 503 case Done{it}: 504 More{Text{}, rest, Con{it, out}} 505 506 def heads.cut(+j: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step: 507 (buf, block) = r 508 heads.item(head.parse(Bytes.to_string(block)), drop(buf, (j + 4 : U32)), out) 509 510 # buf starts with the CRLF that ended the boundary line, so a match at 0 is an empty block. 511 def heads.found(r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step: 512 (buf, hit) = r 513 match hit: 514 case Some{+j}: 515 heads.cut(j, Bytes.slice(buf, 2, (U32.max(j, 2) - 2 : U32)), out) 516 case None{}: 517 wait.big(Heads{}, Bytes.length(buf), out) 518 519 # Text: bytes that cannot begin a delimiter go out as Body; the last dlen - 1 wait for the next chunk. 520 def text.flush.len(r: Bytes.Bytes & U32, tail: Bytes.Bytes, out: List<&1, Item>) -> Step: 521 (head, +n) = r 522 Wait{Text{}, tail, emit(U32.is_eq(n, 0), head, out)} 523 524 def text.flush(r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step: 525 (head, tail) = r 526 text.flush.len(Bytes.length(head), tail, out) 527 528 def text.cut(+i: U32, +dlen: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step: 529 (buf, piece) = r 530 More{After{}, drop(buf, (i + dlen : U32)), Con{Tail{}, emit(U32.is_eq(i, 0), piece, out)}} 531 532 # A match that is data goes out through its first byte, and the search goes on after it. 533 def text.data(+n: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step: 534 (buf, piece) = r 535 More{Text{}, drop(buf, n), emit(False{}, piece, out)} 536 537 # A match whose suffix has not arrived: the bytes before it go out, and it waits. 538 def text.hold(+i: U32, r: Bytes.Bytes & Bytes.Bytes, out: List<&1, Item>) -> Step: 539 (buf, piece) = r 540 Wait{Text{}, drop(buf, i), emit(U32.is_eq(i, 0), piece, out)} 541 542 def text.if(delim: Bool, data: Bool, buf: Bytes.Bytes, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step: 543 match delim: 544 case True{}: 545 text.cut(i, dlen, Bytes.slice(buf, 0, i), out) 546 case False{}: 547 match data: 548 case True{}: 549 text.data((i + 1 : U32), Bytes.slice(buf, 0, (i + 1 : U32)), out) 550 case False{}: 551 text.hold(i, Bytes.slice(buf, 0, i), out) 552 553 def text.kind(r: Bytes.Bytes & U32, +i: U32, +dlen: U32, out: List<&1, Item>) -> Step: 554 (buf, +k) = r 555 text.if(U32.is_eq(k, 0), U32.is_eq(k, 1), buf, i, dlen, out) 556 557 def text.hit(+dlen: U32, r: Bytes.Bytes & Maybe<&2, U32>, out: List<&1, Item>) -> Step: 558 (buf, hit) = r 559 match hit: 560 case Some{+i}: 561 text.kind(suffix.at(buf, i, dlen), i, dlen, out) 562 case None{}: 563 text.flush(keep(buf, (dlen - 1 : U32)), out) 564 565 def step(st: Stage, +delim: String, +dlen: U32, buf: Bytes.Bytes, out: List<&1, Item>) -> Step: 566 match st: 567 case Pre{}: 568 pre.hit(dlen, Bytes.find(buf, delim), out) 569 case After{}: 570 after.dash(Bytes.starts_with(buf, "--"), out) 571 case Heads{}: 572 heads.found(Bytes.find(buf, "\r\n\r\n"), out) 573 case Text{}: 574 text.hit(dlen, Bytes.find(buf, delim), out) 575 case End{}: 576 Wait{End{}, Bytes.new(0), out} 577 578 def go.stop(+delim: String, +dlen: U32, s: Step) -> Result<&1, &1, String, Dec & List<&1, Item>>: 579 match s: 580 case More{st, buf, out}: 581 Done{(Dec{delim, dlen, st, buf}, List.reverse(&1, Item, out))} 582 case Wait{st, buf, out}: 583 Done{(Dec{delim, dlen, st, buf}, List.reverse(&1, Item, out))} 584 case Bad{e}: 585 Fail{e} 586 587 # Each More step consumes a byte, except the two after a delimiter, which itself consumes dlen >= 5, 588 # so len + 8 steps suffice. Running out still returns a sound state: the next feed goes on from it. 589 def go(fuel: Nat, +delim: String, +dlen: U32, s: Step) -> Result<&1, &1, String, Dec & List<&1, Item>>: 590 match fuel: 591 case 0n: 592 go.stop(delim, dlen, s) 593 case 1n+p: 594 match s: 595 case More{st, buf, out}: 596 go(p, delim, dlen, step(st, delim, dlen, buf, out)) 597 case Wait{st, buf, out}: 598 go.stop(delim, dlen, Wait{st, buf, out}) 599 case Bad{e}: 600 Fail{e} 601 602 # A decoder for boundary b. It starts with a CRLF, so a delimiter at the very start is found like any other. 603 def decoder(b: String) -> Dec: 604 +delim = "\r\n--" ++ b 605 Dec{delim, Bytes.count(delim, 0), Pre{}, Bytes.from_string("\r\n")} 606 607 def feed.go(+delim: String, +dlen: U32, st: Stage, r: Bytes.Bytes & U32) -> Result<&1, &1, String, Dec & List<&1, Item>>: 608 (buf, +n) = r 609 go(U32.to_nat((n + 8 : U32)), delim, dlen, More{st, buf, Nil{}}) 610 611 # The next chunk of a body, in any split. Answers the decoder and the items the chunk completes, 612 # or the first error. A body piece is held back only while it could start a delimiter. 613 def feed(d: Dec, chunk: Bytes.Bytes) -> Result<&1, &1, String, Dec & List<&1, Item>>: 614 Dec{+delim, +dlen, st, buf} = d 615 feed.go(delim, dlen, st, Bytes.length(Bytes.append(buf, chunk))) 616 617 # Done once the close delimiter has been read; the epilogue is ignored. 618 def finish(d: Dec) -> Result<&1, &1, String, Unit>: 619 Dec{delim, dlen, st, buf} = d 620 match st: 621 case End{}: 622 Done{Unit{}} 623 case _: 624 Fail{"multipart: body ends before the close delimiter"} 625 626 def collect.add(ps: List<&1, Part>, b: Bytes.Bytes) -> List<&1, Part>: 627 match ps: 628 case Nil{}: 629 Nil{} 630 case Con{Part{n, f, c, body}, t}: 631 Con{Part{n, f, c, Bytes.append(body, b)}, t} 632 633 # Items folded into parts, latest first. 634 def collect(items: List<&1, Item>, ps: List<&1, Part>) -> List<&1, Part>: 635 match items: 636 case Nil{}: 637 ps 638 case Con{Head{n, f, c}, t}: 639 collect(t, Con{Part{n, f, c, Bytes.new(0)}, ps}) 640 case Con{Body{b}, t}: 641 collect(t, collect.add(ps, b)) 642 case Con{Tail{}, t}: 643 collect(t, ps) 644 645 def chunks.fed(r: Result<&1, &1, String, Dec & List<&1, Item>>, ps: List<&1, Part>) -> Result<&1, &1, String, Dec & List<&1, Part>>: 646 match r: 647 case Fail{e}: 648 Fail{e} 649 case Done{(d, items)}: 650 Done{(d, collect(items, ps))} 651 652 def chunks.step(st: Result<&1, &1, String, Dec & List<&1, Part>>, c: Bytes.Bytes) -> Result<&1, &1, String, Dec & List<&1, Part>>: 653 match st: 654 case Fail{e}: 655 Fail{e} 656 case Done{(d, ps)}: 657 chunks.fed(feed(d, c), ps) 658 659 def chunks.fin(r: Result<&1, &1, String, Unit>, ps: List<&1, Part>) -> Result<&1, &1, String, List<&1, Part>>: 660 match r: 661 case Fail{e}: 662 Fail{e} 663 case Done{u}: 664 Done{List.reverse(&1, Part, ps)} 665 666 def chunks.end(st: Result<&1, &1, String, Dec & List<&1, Part>>) -> Result<&1, &1, String, List<&1, Part>>: 667 match st: 668 case Fail{e}: 669 Fail{e} 670 case Done{(d, ps)}: 671 chunks.fin(finish(d), ps) 672 673 def chunks.go(cs: List<&1, Bytes.Bytes>, st: Result<&1, &1, String, Dec & List<&1, Part>>) -> Result<&1, &1, String, List<&1, Part>>: 674 match cs: 675 case Nil{}: 676 chunks.end(st) 677 case Con{c, t}: 678 chunks.go(t, chunks.step(st, c)) 679 680 # The parts of a body that arrives as chunks, split anywhere. 681 def decode.chunks(b: String, cs: List<&1, Bytes.Bytes>) -> Result<&1, &1, String, List<&1, Part>>: 682 chunks.go(cs, Done{(decoder(b), Nil{})}) 683 684 # The parts of a whole body. 685 def decode(b: String, body: Bytes.Bytes) -> Result<&1, &1, String, List<&1, Part>>: 686 decode.chunks(b, [body])