src/http.bend
emerging-ezhttp@0.21.0.0 · 1,837 lines · 233 definitions
1
2
3
4import Base
5import emerging-ezhttp@0.21.0.0/src/body.bend as Body
6
7
8type Cut is Data:
9 NoCut{}
10 Cut{before: String, after: String}
11
12
13type Header is Data:
14 H{name: String, value: String}
15
16
17type Reply is Data:
18 Torn{why: String}
19 Reply{status: U32, headers: List<&2, Header>, body: String}
20
21
22def first(ps: List<&2, String>) -> String:
23 match ps:
24 case []:
25 ""
26 case h <> t:
27 h
28
29
30def second(ps: List<&2, String>) -> String:
31 match ps:
32 case []:
33 ""
34 case h <> t:
35 first(t)
36
37
38def divide.push(h: Char, r: Cut) -> Cut:
39 match r:
40 case NoCut{}:
41 NoCut{}
42 case Cut{before, after}:
43 Cut{SCon{h, before}, after}
44
45
46def divide.step(here: Bool, h: Char, t: String, n: Nat,
47 rest: Unit -> Cut) -> Cut:
48 match here:
49 case True{}:
50 Cut{"", String.drop(SCon{h, t}, n)}
51 case False{}:
52 divide.push(h, rest(Unit{}))
53
54
55def divide.go(s: String, +pat: String, +n: Nat) -> Cut:
56 match s:
57 case SNil{}:
58 NoCut{}
59 case SCon{+h, +t}:
60 divide.step(String.starts_with(SCon{h, t}, pat), h, t, n,
61 _u => divide.go(t, pat, n))
62
63
64def divide(s: String, +pat: String) -> Cut:
65 divide.go(s, pat, String.length(pat))
66
67
68def hex.of(+u: U32) -> U32:
69 Bool.pick(U32, Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)), U32.sub(u, 48),
70 Bool.pick(U32, Bool.and(U32.is_ge(u, 97), U32.is_le(u, 102)), U32.sub(u, 87),
71 Bool.pick(U32, Bool.and(U32.is_ge(u, 65), U32.is_le(u, 70)), U32.sub(u, 55),
72 16)))
73
74
75def hex.val(c: Char) -> U32:
76 hex.of(Char.to_u32(c))
77
78
79def hex.step(done: Bool, acc: Nat, rest: Unit -> Nat) -> Nat:
80 match done:
81 case True{}:
82 acc
83 case False{}:
84 rest(Unit{})
85
86
87def hex.go(s: String, +acc: Nat) -> Nat:
88 match s:
89 case SNil{}:
90 acc
91 case SCon{+h, t}:
92 +v = hex.val(h)
93 hex.step(U32.is_ge(v, 16), acc,
94 _u => hex.go(t, Nat.add(Nat.mul(acc, 16n), U32.to_nat(v))))
95
96
97def hexlen(s: String) -> Nat:
98 hex.go(String.trim(first(String.split(s, ';'))), 0n)
99
100
101def hex.digit(c: Char) -> Bool:
102 U32.is_lt(hex.val(c), 16)
103
104
105def hex.rest(token: String, good: Bool) -> Bool:
106 match token good:
107 case _ False{}:
108 False{}
109 case SNil{} True{}:
110 True{}
111 case SCon{h, t} True{}:
112 hex.rest(t, hex.digit(h))
113
114
115def hex.well(token: String) -> Bool:
116 match token:
117 case SNil{}:
118 False{}
119 case SCon{h, t}:
120 hex.rest(t, hex.digit(h))
121
122
123def chunks.token(before: String) -> String:
124 String.trim(first(String.split(before, ';')))
125
126
127
128
129def utf8.width(+u: U32) -> Nat:
130 Bool.pick(Nat, U32.is_lt(u, 128), 1n,
131 Bool.pick(Nat, U32.is_lt(u, 2048), 2n,
132 Bool.pick(Nat, U32.is_lt(u, 65536), 3n, 4n)))
133
134
135def utf8.len(s: String) -> Nat:
136 match s:
137 case SNil{}:
138 0n
139 case SCon{h, t}:
140 Nat.add(utf8.width(Char.to_u32(h)), utf8.len(t))
141
142
143
144
145
146def octs.width(+_u: U32) -> Nat:
147 1n
148
149
150def octs.len(s: String) -> Nat:
151 match s:
152 case SNil{}:
153 0n
154 case SCon{h, t}:
155 Nat.add(octs.width(Char.to_u32(h)), octs.len(t))
156
157
158def octs.take.step(short: Bool, h: Char, rest: Unit -> String) -> String:
159 match short:
160 case True{}:
161 SNil{}
162 case False{}:
163 SCon{h, rest(Unit{})}
164
165
166def octs.take(s: String, +n: Nat) -> String:
167 match s:
168 case SNil{}:
169 SNil{}
170 case SCon{+h, t}:
171 +w = octs.width(Char.to_u32(h))
172 octs.take.step(Nat.is_lt(n, w), h, _u => octs.take(t, Nat.sub(n, w)))
173
174
175def octs.drop.step(short: Bool, h: Char, t: String,
176 rest: Unit -> String) -> String:
177 match short:
178 case True{}:
179 SCon{h, t}
180 case False{}:
181 rest(Unit{})
182
183
184def octs.drop(s: String, +n: Nat) -> String:
185 match s:
186 case SNil{}:
187 SNil{}
188 case SCon{+h, +t}:
189 +w = octs.width(Char.to_u32(h))
190 octs.drop.step(Nat.is_lt(n, w), h, t, _u => octs.drop(t, Nat.sub(n, w)))
191
192
193def digits.val.of(+u: U32) -> U32:
194 Bool.pick(U32, Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)), U32.sub(u, 48), 10)
195
196
197def digits.val(c: Char) -> U32:
198 digits.val.of(Char.to_u32(c))
199
200
201def digits.step(ok: Bool, next: Unit -> Maybe<&2, Nat>) -> Maybe<&2, Nat>:
202 match ok:
203 case False{}:
204 None{}
205 case True{}:
206 next(Unit{})
207
208
209def digits.go(s: String, +acc: Nat, left: Nat) -> Maybe<&2, Nat>:
210 match s left:
211 case _ 0n:
212 None{}
213 case SNil{} 1n+k:
214 Some{acc}
215 case SCon{+h, t} 1n+k:
216 +d = digits.val(h)
217 digits.step(U32.is_lt(d, 10),
218 _u => digits.go(t, Nat.add(Nat.mul(acc, 10n), U32.to_nat(d)), k))
219
220
221def digits.read(s: String) -> Maybe<&2, Nat>:
222 match s:
223 case SNil{}:
224 None{}
225 case SCon{h, t}:
226 digits.go(SCon{h, t}, 0n, 9n)
227
228
229
230def octs.has.step(short: Bool, rest: Unit -> Bool) -> Bool:
231 match short:
232 case True{}:
233 True{}
234 case False{}:
235 rest(Unit{})
236
237
238
239def octs.has(s: String, +n: Nat) -> Bool:
240 match s:
241 case SNil{}:
242 Bool.not(Nat.is_lt(0n, n))
243 case SCon{+h, t}:
244 +w = octs.width(Char.to_u32(h))
245 octs.has.step(Nat.is_lt(n, w), _u => octs.has(t, Nat.sub(n, w)))
246
247
248def chunks.ready(+after: String, +n: Nat) -> Bool:
249 Bool.and(octs.has(after, Nat.add(n, 2n)),
250 String.starts_with(octs.drop(after, n), "\r\n"))
251
252
253type Next is Data:
254 Stop{}
255 Go{cut: Cut}
256 Reject{why: String}
257
258
259def chunks.next.of(zero: Bool, ready: Bool, +after: String, +n: Nat) -> Next:
260 match zero ready:
261 case True{} _:
262 Stop{}
263 case False{} False{}:
264 Reject{"the chunked body stopped short"}
265 case False{} True{}:
266 Go{divide(octs.drop(after, Nat.add(n, 2n)), "\r\n")}
267
268
269def chunks.next.hex(+before: String, +after: String, ok: Bool) -> Next:
270 match ok:
271 case False{}:
272 Reject{"chunk size is not hexadecimal"}
273 case True{}:
274 +n = hexlen(before)
275 chunks.next.of(Nat.is_eq(n, 0n), chunks.ready(after, n), after, n)
276
277
278def chunks.next(+before: String, +after: String) -> Next:
279 chunks.next.hex(before, after, hex.well(chunks.token(before)))
280
281
282
283type Size is Data:
284 SizeBad{why: String}
285 SizeLast{}
286 SizeData{n: Nat}
287
288
289def chunk.size.n(+n: Nat, zero: Bool) -> Size:
290 match zero:
291 case True{}:
292 SizeLast{}
293 case False{}:
294 SizeData{n}
295
296
297def chunk.size.of(+before: String, ok: Bool) -> Size:
298 match ok:
299 case False{}:
300 SizeBad{"chunk size is not hexadecimal"}
301 case True{}:
302 +n = hexlen(before)
303 chunk.size.n(n, Nat.is_eq(n, 0n))
304
305
306def chunk.size(+before: String) -> Size:
307 chunk.size.of(before, hex.well(chunks.token(before)))
308
309
310def chunk.short() -> String:
311 "the chunked body stopped short"
312
313
314
315def chunk.next.data(+after: String, +n: Nat, ready: Bool) -> Next:
316 match ready:
317 case False{}:
318 Reject{chunk.short()}
319 case True{}:
320 Go{divide(octs.drop(after, Nat.add(n, 2n)), "\r\n")}
321
322
323def chunk.next(z: Size, +after: String) -> Next:
324 match z:
325 case SizeBad{why}:
326 Reject{why}
327 case SizeLast{}:
328 Stop{}
329 case SizeData{+n}:
330 chunk.next.data(after, n, chunks.ready(after, n))
331
332
333def chunks.scan.step(step: Next, rest: Cut -> Maybe<&2, String>)
334 -> Maybe<&2, String>:
335 match step:
336 case Stop{}:
337 None{}
338 case Reject{why}:
339 Some{why}
340 case Go{cut}:
341 rest(cut)
342
343
344def chunks.scan(fuel: Nat, c: Cut) -> Maybe<&2, String>:
345 match fuel c:
346 case 0n _:
347 Some{"the chunked body is truncated"}
348 case 1n+f NoCut{}:
349 None{}
350 case 1n+f Cut{before, +after}:
351 chunks.scan.step(chunks.next(before, after), cut => chunks.scan(f, cut))
352
353
354def chunks.more(+n: Nat, +after: String, rest: Unit -> String, zero: Bool)
355 -> String:
356 match zero:
357 case True{}:
358 ""
359 case False{}:
360 octs.take(after, n)
361 ++ rest(Unit{})
362
363
364def chunks(fuel: Nat, c: Cut) -> String:
365 match fuel c:
366 case 0n _:
367 ""
368 case 1n+f NoCut{}:
369 ""
370 case 1n+f Cut{before, +after}:
371 +n = hexlen(before)
372 chunks.more(n, after,
373 _u => chunks(f, divide(octs.drop(after, Nat.add(n, 2n)), "\r\n")),
374 Nat.is_eq(n, 0n))
375
376
377def dechunk(+s: String) -> String:
378 chunks(String.length(s), divide(s, "\r\n"))
379
380
381
382def head.hit(h: String, +name: String) -> Bool:
383 String.starts_with(String.to_lower(h), name ++ ":")
384
385
386def head.pick(hit: Bool, h: String, name: String, rest: String) -> String:
387 match hit:
388 case True{}:
389 String.trim(String.drop(h, Nat.add(String.length(name), 1n)))
390 case False{}:
391 rest
392
393
394def head.go(ls: List<&2, String>, +name: String) -> String:
395 match ls:
396 case []:
397 ""
398 case +h <> t:
399 head.pick(head.hit(h, name), h, name, head.go(t, name))
400
401
402def head.value(h: String, name: String) -> String:
403 String.trim(String.drop(h, Nat.add(String.length(name), 1n)))
404
405
406def head.values.cons(hit: Bool, value: String, rest: Unit -> List<&2, String>)
407 -> List<&2, String>:
408 match hit:
409 case False{}:
410 rest(Unit{})
411 case True{}:
412 value <> rest(Unit{})
413
414
415def head.values(ls: List<&2, String>, +name: String) -> List<&2, String>:
416 match ls:
417 case []:
418 []
419 case +h <> t:
420 head.values.cons(head.hit(h, name), head.value(h, name),
421 _u => head.values(t, name))
422
423
424def field.folded(+value: String) -> Bool:
425 Bool.or(String.contains(value, "\n"), String.contains(value, "\r"))
426
427
428def field.keep(name: String, +value: String, folded: Bool) -> Maybe<&2, Header>:
429 match folded:
430 case True{}:
431 None{}
432 case False{}:
433 Some{H{name, value}}
434
435
436def head.from_kept(before: String, +after: String) -> Maybe<&2, Header>:
437 +value = String.trim(after)
438 field.keep(before, value, field.folded(value))
439
440
441def head.from_cut(c: Cut) -> Maybe<&2, Header>:
442 match c:
443 case NoCut{}:
444 None{}
445 case Cut{before, after}:
446 head.from_kept(before, after)
447
448
449def head.line(h: String) -> Maybe<&2, Header>:
450 head.from_cut(divide(h, ":"))
451
452
453def head.cons(m: Maybe<&2, Header>, rest: List<&2, Header>) -> List<&2, Header>:
454 match m:
455 case None{}:
456 rest
457 case Some{hdr}:
458 hdr <> rest
459
460
461def head.list(ls: List<&2, String>) -> List<&2, Header>:
462 match ls:
463 case []:
464 []
465 case h <> t:
466 head.cons(head.line(h), head.list(t))
467
468
469def header.line(h: Header) -> String:
470 match h:
471 case H{name, value}:
472 name ++ ": " ++ value ++ "\r\n"
473
474
475def headers.find.pick(value: String, hit: Bool, rest: Unit -> String) -> String:
476 match hit:
477 case True{}:
478 value
479 case False{}:
480 rest(Unit{})
481
482
483def headers.find.go(hs: List<&2, Header>, +name: String) -> String:
484 match hs:
485 case []:
486 ""
487 case H{n, v} <> t:
488 headers.find.pick(v, String.eq(String.to_lower(n), name),
489 _u => headers.find.go(t, name))
490
491
492def headers.find(hs: List<&2, Header>, +name: String) -> String:
493 headers.find.go(hs, String.to_lower(name))
494
495
496def headers.lines(hs: List<&2, Header>) -> String:
497 match hs:
498 case []:
499 ""
500 case h <> t:
501 header.line(h) ++ headers.lines(t)
502
503
504def clen.field(+n: Nat, empty: Bool) -> List<&2, Header>:
505 match empty:
506 case True{}:
507 []
508 case False{}:
509 [H{"Content-Length", Nat.show(n)}]
510
511
512def clen.line(+n: Nat, empty: Bool) -> String:
513 headers.lines(clen.field(n, empty))
514
515
516
517type Field is Data:
518 FHost{}
519 FAgent{}
520 FAccept{}
521 FConn{}
522 FClen{}
523 FOther{}
524
525
526def field.rank(f: Field) -> U32:
527 match f:
528 case FHost{}:
529 0
530 case FAgent{}:
531 1
532 case FAccept{}:
533 2
534 case FConn{}:
535 3
536 case FClen{}:
537 4
538 case FOther{}:
539 5
540
541
542def field.same(a: Field, b: Field) -> Bool:
543 U32.is_eq(field.rank(a), field.rank(b))
544
545
546def field.of.clen(hit: Bool) -> Field:
547 match hit:
548 case True{}:
549 FClen{}
550 case False{}:
551 FOther{}
552
553
554def field.of.conn(hit: Bool, +l: String) -> Field:
555 match hit:
556 case True{}:
557 FConn{}
558 case False{}:
559 field.of.clen(String.eq(l, "content-length"))
560
561
562def field.of.accept(hit: Bool, +l: String) -> Field:
563 match hit:
564 case True{}:
565 FAccept{}
566 case False{}:
567 field.of.conn(String.eq(l, "connection"), l)
568
569
570def field.of.agent(hit: Bool, +l: String) -> Field:
571 match hit:
572 case True{}:
573 FAgent{}
574 case False{}:
575 field.of.accept(String.eq(l, "accept"), l)
576
577
578def field.of.host(hit: Bool, +l: String) -> Field:
579 match hit:
580 case True{}:
581 FHost{}
582 case False{}:
583 field.of.agent(String.eq(l, "user-agent"), l)
584
585
586def field.of(+l: String) -> Field:
587 field.of.host(String.eq(l, "host"), l)
588
589
590def field.name(name: String) -> Field:
591 field.of(String.to_lower(name))
592
593
594def header.field(h: Header) -> Field:
595 match h:
596 case H{name, value}:
597 field.name(name)
598
599
600def fields.put(keep: Bool, h: Header, rest: List<&2, Header>)
601 -> List<&2, Header>:
602 match keep:
603 case True{}:
604 h <> rest
605 case False{}:
606 rest
607
608
609def fields.of(+f: Field, hs: List<&2, Header>) -> List<&2, Header>:
610 match hs:
611 case []:
612 []
613 case +h <> t:
614 fields.put(field.same(f, header.field(h)), h, fields.of(f, t))
615
616
617def fields.or(given: List<&2, Header>, dflt: List<&2, Header>)
618 -> List<&2, Header>:
619 match given:
620 case []:
621 dflt
622 case h <> t:
623 h <> t
624
625
626
627
628
629
630
631def request.fields.n(host: String, +hs: List<&2, Header>, +n: Nat)
632 -> List<&2, Header>:
633 List.append(&2, Header, fields.or(fields.of(FHost{}, hs), [H{"Host", host}]),
634 List.append(&2, Header,
635 fields.or(fields.of(FAgent{}, hs), [H{"User-Agent", "ezhttp"}]),
636 List.append(&2, Header,
637 fields.or(fields.of(FAccept{}, hs), [H{"Accept", "*/*"}]),
638 H{"Connection", "close"}
639 <> List.append(&2, Header, fields.of(FOther{}, hs),
640 clen.field(n, Nat.is_eq(n, 0n))))))
641
642
643def request.fields(host: String, +hs: List<&2, Header>, +body: String)
644 -> List<&2, Header>:
645 request.fields.n(host, hs, utf8.len(body))
646
647
648
649def request(method: String, host: String, path: String,
650 headers: List<&2, Header>, +body: String) -> String:
651 method ++ " " ++ path ++ " HTTP/1.1\r\n"
652 ++ headers.lines(request.fields(host, headers, body))
653 ++ "\r\n"
654 ++ body
655
656
657
658def port.dflt(secure: Bool) -> U32:
659 match secure:
660 case True{}:
661 443
662 case False{}:
663 80
664
665
666def authority.of(host: String, +port: U32, dflt: Bool) -> String:
667 match dflt:
668 case True{}:
669 host
670 case False{}:
671 host ++ ":" ++ U32.show(port)
672
673
674
675def authority(secure: Bool, host: String, +port: U32) -> String:
676 authority.of(host, port, U32.is_eq(port, port.dflt(secure)))
677
678
679def request.at(method: String, secure: Bool, host: String, port: U32,
680 path: String, headers: List<&2, Header>, body: String) -> String:
681 request(method, authority(secure, host, port), path, headers, body)
682
683
684
685def request.head(method: String, host: String, path: String,
686 headers: List<&2, Header>, +n: Nat) -> String:
687 method ++ " " ++ path ++ " HTTP/1.1\r\n"
688 ++ headers.lines(request.fields.n(host, headers, n))
689 ++ "\r\n"
690
691
692def request.head.at(method: String, secure: Bool, host: String, port: U32,
693 path: String, headers: List<&2, Header>, n: Nat) -> String:
694 request.head(method, authority(secure, host, port), path, headers, n)
695
696
697def get_req(host: String, path: String, headers: List<&2, Header>) -> String:
698 request("GET", host, path, headers, "")
699
700
701def head_req(host: String, path: String, headers: List<&2, Header>) -> String:
702 request("HEAD", host, path, headers, "")
703
704
705def post_req(host: String, path: String, headers: List<&2, Header>,
706 body: String) -> String:
707 request("POST", host, path, headers, body)
708
709
710def put_req(host: String, path: String, headers: List<&2, Header>,
711 body: String) -> String:
712 request("PUT", host, path, headers, body)
713
714
715def delete_req(host: String, path: String, headers: List<&2, Header>,
716 body: String) -> String:
717 request("DELETE", host, path, headers, body)
718
719
720def options_req(host: String, path: String, headers: List<&2, Header>) -> String:
721 request("OPTIONS", host, path, headers, "")
722
723
724def method.safe(+method: String) -> Bool:
725 Bool.or(String.eq(method, "GET"),
726 Bool.or(String.eq(method, "HEAD"), String.eq(method, "OPTIONS")))
727
728
729def method.idempotent(+method: String) -> Bool:
730 Bool.or(method.safe(method),
731 Bool.or(String.eq(method, "PUT"), String.eq(method, "DELETE")))
732
733
734def token.ok.step(space: Bool, rest: Unit -> Bool) -> Bool:
735 match space:
736 case True{}:
737 False{}
738 case False{}:
739 rest(Unit{})
740
741
742def token.ok(s: String) -> Bool:
743 match s:
744 case SNil{}:
745 True{}
746 case SCon{h, t}:
747 token.ok.step(Char.is_space(h), _u => token.ok(t))
748
749
750def target.origin(path: String) -> Bool:
751 String.starts_with(path, "/")
752
753
754def target.asterisk(method: String, star: Bool) -> Bool:
755 match star:
756 case False{}:
757 False{}
758 case True{}:
759 String.eq(method, "OPTIONS")
760
761
762def target.form(method: String, path: String, absolute: Bool) -> Bool:
763 match absolute:
764 case True{}:
765 True{}
766 case False{}:
767 target.asterisk(method, String.eq(path, "*"))
768
769
770type Frame is Data:
771 Bare{}
772 Sized{n: Nat}
773 Chunked{}
774 Closed{}
775
776
777def parse.short(status: U32, headers: List<&2, Header>, n: Nat, body: String,
778 short: Bool) -> Reply:
779 match short:
780 case True{}:
781 Torn{"the response body stopped before its Content-Length"}
782 case False{}:
783 Reply{status, headers, octs.take(body, n)}
784
785
786def parse.clen(status: U32, headers: List<&2, Header>, +n: Nat, +body: String)
787 -> Reply:
788 parse.short(status, headers, n, body, Nat.is_lt(octs.len(body), n))
789
790
791def parse.len(status: U32, headers: List<&2, Header>, m: Maybe<&2, Nat>,
792 body: String) -> Reply:
793 match m:
794 case None{}:
795 Reply{status, headers, body}
796 case Some{n}:
797 parse.clen(status, headers, n, body)
798
799
800def clen.agree.step(same: Bool, rest: Unit -> Bool) -> Bool:
801 match same:
802 case False{}:
803 False{}
804 case True{}:
805 rest(Unit{})
806
807
808def clen.agree.go(rest: List<&2, String>, +h: String) -> Bool:
809 match rest:
810 case []:
811 True{}
812 case x <> r:
813 clen.agree.step(String.eq(x, h), _u => clen.agree.go(r, h))
814
815
816def clen.agree(vs: List<&2, String>) -> Bool:
817 match vs:
818 case []:
819 True{}
820 case h <> t:
821 clen.agree.go(t, h)
822
823
824def clen.any(vs: List<&2, String>) -> Bool:
825 match vs:
826 case []:
827 False{}
828 case h <> t:
829 True{}
830
831
832def parse.chunked(status: U32, headers: List<&2, Header>, body: String,
833 bad: Maybe<&2, String>) -> Reply:
834 match bad:
835 case None{}:
836 Reply{status, headers, dechunk(body)}
837 case Some{why}:
838 Torn{why}
839
840
841def parse.chunks(status: U32, headers: List<&2, Header>, +body: String) -> Reply:
842 parse.chunked(status, headers, body,
843 chunks.scan(String.length(body), divide(body, "\r\n")))
844
845
846
847
848
849type Plan is Data:
850 Unplanned{why: String}
851 Planned{status: U32, headers: List<&2, Header>, frame: Frame}
852
853
854def plan.clash(status: U32, headers: List<&2, Header>, present: Bool) -> Plan:
855 match present:
856 case True{}:
857 Unplanned{"Content-Length and Transfer-Encoding conflict"}
858 case False{}:
859 Planned{status, headers, Chunked{}}
860
861
862def plan.len(status: U32, headers: List<&2, Header>, m: Maybe<&2, Nat>) -> Plan:
863 match m:
864 case None{}:
865 Planned{status, headers, Closed{}}
866 case Some{n}:
867 Planned{status, headers, Sized{n}}
868
869
870def plan.te(status: U32, headers: List<&2, Header>, +lens: List<&2, String>,
871 chunked: Bool) -> Plan:
872 match chunked:
873 case False{}:
874 plan.len(status, headers, digits.read(first(lens)))
875 case True{}:
876 plan.clash(status, headers, clen.any(lens))
877
878
879def plan.agree(status: U32, headers: List<&2, Header>, te: String,
880 +lens: List<&2, String>, ok: Bool) -> Plan:
881 match ok:
882 case False{}:
883 Unplanned{"Content-Length values disagree"}
884 case True{}:
885 plan.te(status, headers, lens,
886 String.contains(String.to_lower(te), "chunked"))
887
888
889def head.plan(m: Maybe<&2, U32>, +ls: List<&2, String>) -> Plan:
890 match m:
891 case None{}:
892 Unplanned{"the response has no status line"}
893 case Some{n}:
894 +lens = head.values(ls, "content-length")
895 plan.agree(n, head.list(ls), head.go(ls, "transfer-encoding"), lens,
896 clen.agree(lens))
897
898
899def plan.body(status: U32, headers: List<&2, Header>, f: Frame, +body: String)
900 -> Reply:
901 match f:
902 case Bare{}:
903 Reply{status, headers, ""}
904 case Sized{n}:
905 parse.clen(status, headers, n, body)
906 case Chunked{}:
907 parse.chunks(status, headers, body)
908 case Closed{}:
909 Reply{status, headers, body}
910
911
912def plan.text(p: Plan, +body: String) -> Reply:
913 match p:
914 case Unplanned{why}:
915 Torn{why}
916 case Planned{status, headers, f}:
917 plan.body(status, headers, f, body)
918
919
920def parse.status(m: Maybe<&2, U32>, +ls: List<&2, String>, body: String)
921 -> Reply:
922 plan.text(head.plan(m, ls), body)
923
924
925def status.of(ls: List<&2, String>) -> Maybe<&2, U32>:
926 U32.read(second(String.split(first(ls), ' ')))
927
928
929def parse.head(+ls: List<&2, String>, body: String) -> Reply:
930 parse.status(status.of(ls), ls, body)
931
932
933
934def parse.cut(c: Cut) -> Reply:
935 match c:
936 case NoCut{}:
937 Torn{"the response head has no blank line after it"}
938 case Cut{before, after}:
939 parse.head(String.lines(before), after)
940
941
942def parse(raw: String) -> Reply:
943 parse.cut(divide(raw, "\r\n\r\n"))
944
945
946def show(r: Reply) -> String:
947 match r:
948 case Torn{why}:
949 "torn " ++ why
950 case Reply{status, headers, body}:
951 U32.show(status) ++ " " ++ body
952
953
954
955
956type Request is Data:
957 Bad{why: String}
958 Request{method: String, target: String, headers: List<&2, Header>,
959 body: String}
960
961
962type Phrase is Data:
963 P{code: U32, text: String}
964
965
966def reasons() -> List<&2, Phrase>:
967 [P{200, "OK"}, P{201, "Created"}, P{202, "Accepted"}, P{204, "No Content"},
968 P{301, "Moved Permanently"}, P{302, "Found"}, P{303, "See Other"},
969 P{304, "Not Modified"}, P{307, "Temporary Redirect"},
970 P{308, "Permanent Redirect"}, P{400, "Bad Request"},
971 P{401, "Unauthorized"}, P{403, "Forbidden"}, P{404, "Not Found"},
972 P{405, "Method Not Allowed"}, P{409, "Conflict"}, P{410, "Gone"},
973 P{413, "Content Too Large"}, P{415, "Unsupported Media Type"},
974 P{422, "Unprocessable Content"}, P{429, "Too Many Requests"},
975 P{500, "Internal Server Error"}, P{501, "Not Implemented"},
976 P{502, "Bad Gateway"}, P{503, "Service Unavailable"},
977 P{504, "Gateway Timeout"}]
978
979
980def reason.pick(text: String, hit: Bool, rest: Unit -> String) -> String:
981 match hit:
982 case True{}:
983 text
984 case False{}:
985 rest(Unit{})
986
987
988def reason.in(ps: List<&2, Phrase>, +code: U32) -> String:
989 match ps:
990 case []:
991 ""
992 case P{c, text} <> t:
993 reason.pick(text, U32.is_eq(code, c), _u => reason.in(t, code))
994
995
996def reason(+code: U32) -> String:
997 reason.in(reasons(), code)
998
999
1000
1001def respond.head.onto(+status: U32, headers: List<&2, Header>, +n: Nat,
1002 rest: String) -> String:
1003 "HTTP/1.1 " ++ U32.show(status) ++ " " ++ reason(status) ++ "\r\n"
1004 ++ "Connection: close\r\n"
1005 ++ headers.lines(headers)
1006 ++ clen.line(n, Nat.is_eq(n, 0n))
1007 ++ "\r\n"
1008 ++ rest
1009
1010
1011def respond.head(+status: U32, headers: List<&2, Header>, +n: Nat) -> String:
1012 respond.head.onto(status, headers, n, "")
1013
1014
1015
1016def respond(+status: U32, headers: List<&2, Header>, +body: String) -> String:
1017 respond.head.onto(status, headers, utf8.len(body), body)
1018
1019
1020def status.nocontent(+code: U32) -> Bool:
1021 Bool.or(U32.is_eq(code, 204), U32.is_eq(code, 304))
1022
1023
1024def te.line(te: Bool) -> String:
1025 match te:
1026 case True{}:
1027 "Transfer-Encoding: chunked\r\n"
1028 case False{}:
1029 ""
1030
1031
1032
1033def respond.stream(+status: U32, headers: List<&2, Header>) -> String:
1034 "HTTP/1.1 " ++ U32.show(status) ++ " " ++ reason(status) ++ "\r\n"
1035 ++ "Connection: close\r\n"
1036 ++ headers.lines(headers)
1037 ++ te.line(Bool.not(status.nocontent(status)))
1038 ++ "\r\n"
1039
1040
1041def hex.nib(+v: U32) -> Char:
1042 Char.from_u32(Bool.pick(U32, U32.is_lt(v, 10), U32.add(v, 48),
1043 U32.add(v, 87)))
1044
1045
1046def hex.at(+n: U32, k: Nat) -> Char:
1047 hex.nib(U32.and(U32.shrn(n, k), 15))
1048
1049
1050def chunk.line(+n: U32) -> String:
1051 SCon{hex.at(n, 28n), SCon{hex.at(n, 24n), SCon{hex.at(n, 20n),
1052 SCon{hex.at(n, 16n), SCon{hex.at(n, 12n), SCon{hex.at(n, 8n),
1053 SCon{hex.at(n, 4n), SCon{hex.at(n, 0n), "\r\n"}}}}}}}}
1054
1055
1056
1057type Chunk is Data:
1058 NoChunk{}
1059 Chunk{line: String}
1060
1061
1062def chunk.frame.of(+n: U32, empty: Bool) -> Chunk:
1063 match empty:
1064 case True{}:
1065 NoChunk{}
1066 case False{}:
1067 Chunk{chunk.line(n)}
1068
1069
1070def chunk.frame(+n: U32) -> Chunk:
1071 chunk.frame.of(n, U32.is_eq(n, 0))
1072
1073
1074def chunk.end() -> String:
1075 "0\r\n\r\n"
1076
1077
1078def respond.of.body(+status: U32, headers: List<&2, Header>, body: String,
1079 drop: Bool) -> String:
1080 match drop:
1081 case True{}:
1082 respond(status, headers, "")
1083 case False{}:
1084 respond(status, headers, body)
1085
1086
1087def respond.of(r: Reply) -> String:
1088 match r:
1089 case Torn{why}:
1090 respond(500, [], why)
1091 case Reply{+status, headers, body}:
1092 respond.of.body(status, headers, body, status.nocontent(status))
1093
1094
1095def reply.for.pick(reply: Reply, head: Bool) -> Reply:
1096 match reply head:
1097 case Torn{why} _:
1098 Torn{why}
1099 case Reply{status, headers, body} False{}:
1100 Reply{status, headers, body}
1101 case Reply{status, headers, body} True{}:
1102 Reply{status, headers, ""}
1103
1104
1105def reply.for(method: String, reply: Reply) -> Reply:
1106 reply.for.pick(reply, String.eq(method, "HEAD"))
1107
1108
1109def method.of(req: Request) -> String:
1110 match req:
1111 case Bad{why}:
1112 ""
1113 case Request{method, target, headers, body}:
1114 method
1115
1116
1117def ask.short(method: String, target: String, headers: List<&2, Header>,
1118 n: Nat, body: String, short: Bool) -> Request:
1119 match short:
1120 case True{}:
1121 Bad{"the request body stopped before its Content-Length"}
1122 case False{}:
1123 Request{method, target, headers, octs.take(body, n)}
1124
1125
1126def ask.clen(method: String, target: String, headers: List<&2, Header>,
1127 +n: Nat, +body: String) -> Request:
1128 ask.short(method, target, headers, n, body, Nat.is_lt(octs.len(body), n))
1129
1130
1131def ask.len(method: String, target: String, headers: List<&2, Header>,
1132 m: Maybe<&2, Nat>, body: String) -> Request:
1133 match m:
1134 case None{}:
1135 Request{method, target, headers, body}
1136 case Some{n}:
1137 ask.clen(method, target, headers, n, body)
1138
1139
1140def ask.chunked(method: String, target: String, headers: List<&2, Header>,
1141 body: String, bad: Maybe<&2, String>) -> Request:
1142 match bad:
1143 case None{}:
1144 Request{method, target, headers, dechunk(body)}
1145 case Some{why}:
1146 Bad{why}
1147
1148
1149def ask.chunks(method: String, target: String, headers: List<&2, Header>,
1150 +body: String) -> Request:
1151 ask.chunked(method, target, headers, body,
1152 chunks.scan(String.length(body), divide(body, "\r\n")))
1153
1154
1155def ask.te.clash(method: String, target: String, headers: List<&2, Header>,
1156 body: String, present: Bool) -> Request:
1157 match present:
1158 case True{}:
1159 Bad{"Content-Length and Transfer-Encoding conflict"}
1160 case False{}:
1161 ask.chunks(method, target, headers, body)
1162
1163
1164def ask.te(method: String, target: String, headers: List<&2, Header>,
1165 +lens: List<&2, String>, +body: String, chunked: Bool) -> Request:
1166 match chunked:
1167 case False{}:
1168 ask.len(method, target, headers, digits.read(first(lens)), body)
1169 case True{}:
1170 ask.te.clash(method, target, headers, body, clen.any(lens))
1171
1172
1173def ask.agree(method: String, target: String, headers: List<&2, Header>,
1174 te: String, +lens: List<&2, String>, body: String, ok: Bool) -> Request:
1175 match ok:
1176 case False{}:
1177 Bad{"Content-Length values disagree"}
1178 case True{}:
1179 ask.te(method, target, headers, lens, body,
1180 String.contains(String.to_lower(te), "chunked"))
1181
1182
1183def ask.frame(method: String, target: String, headers: List<&2, Header>,
1184 te: String, +lens: List<&2, String>, body: String) -> Request:
1185 ask.agree(method, target, headers, te, lens, body, clen.agree(lens))
1186
1187
1188def ask.origin(method: String, target: String, +ls: List<&2, String>,
1189 body: String, ok: Bool) -> Request:
1190 match ok:
1191 case False{}:
1192 Bad{"the request-target is not in origin-form"}
1193 case True{}:
1194 +hs = head.list(ls)
1195 ask.frame(method, target, hs, head.go(ls, "transfer-encoding"),
1196 head.values(ls, "content-length"), body)
1197
1198
1199def ask.line(+method: String, +target: String, +ls: List<&2, String>,
1200 body: String) -> Request:
1201 ask.origin(method, target, ls, body,
1202 target.form(method, target, target.origin(target)))
1203
1204
1205def ask.parts(ps: List<&2, String>, +ls: List<&2, String>, body: String)
1206 -> Request:
1207 match ps:
1208 case []:
1209 Bad{"the request has no request line"}
1210 case m <> rest:
1211 match rest:
1212 case []:
1213 Bad{"the request line has no target"}
1214 case t <> _ver:
1215 ask.line(m, t, ls, body)
1216
1217
1218def ask.head(+ls: List<&2, String>, body: String) -> Request:
1219 match ls:
1220 case []:
1221 Bad{"the request has no request line"}
1222 case _:
1223 ask.parts(String.split(first(ls), ' '), ls, body)
1224
1225
1226def ask.cut(c: Cut) -> Request:
1227 match c:
1228 case NoCut{}:
1229 Bad{"the request head has no blank line after it"}
1230 case Cut{before, after}:
1231 ask.head(String.lines(before), after)
1232
1233
1234def ask(raw: String) -> Request:
1235 ask.cut(divide(raw, "\r\n\r\n"))
1236
1237
1238def ask.show(r: Request) -> String:
1239 match r:
1240 case Bad{why}:
1241 "bad " ++ why
1242 case Request{method, target, headers, body}:
1243 method ++ " " ++ target ++ " " ++ body
1244
1245
1246
1247
1248
1249def status.bare(m: Maybe<&2, U32>) -> Bool:
1250 match m:
1251 case None{}:
1252 False{}
1253 case Some{n}:
1254 status.nocontent(n)
1255
1256
1257
1258def status.interim(m: Maybe<&2, U32>) -> Bool:
1259 match m:
1260 case None{}:
1261 False{}
1262 case Some{+n}:
1263 Bool.and(U32.is_le(100, n), U32.is_lt(n, 200))
1264
1265
1266def frame.sized(m: Maybe<&2, Nat>) -> Frame:
1267 match m:
1268 case None{}:
1269 Closed{}
1270 case Some{n}:
1271 Sized{n}
1272
1273
1274def frame.agree(+lens: List<&2, String>, ok: Bool) -> Frame:
1275 match ok:
1276 case False{}:
1277 Closed{}
1278 case True{}:
1279 frame.sized(digits.read(first(lens)))
1280
1281
1282def frame.te(+lens: List<&2, String>, chunked: Bool) -> Frame:
1283 match chunked:
1284 case True{}:
1285 Chunked{}
1286 case False{}:
1287 frame.agree(lens, clen.agree(lens))
1288
1289
1290
1291def frame(bare: Bool, te: String, +lens: List<&2, String>) -> Frame:
1292 match bare:
1293 case True{}:
1294 Bare{}
1295 case False{}:
1296 frame.te(lens, String.contains(String.to_lower(te), "chunked"))
1297
1298
1299def chunks.trailer(+after: String) -> Bool:
1300 Bool.or(String.starts_with(after, "\r\n"), String.contains(after, "\r\n\r\n"))
1301
1302
1303def chunks.ended.data(+after: String, +n: Nat, ready: Bool,
1304 rest: Cut -> Bool) -> Bool:
1305 match ready:
1306 case False{}:
1307 False{}
1308 case True{}:
1309 rest(divide(octs.drop(after, Nat.add(n, 2n)), "\r\n"))
1310
1311
1312def chunks.ended.size(+after: String, +n: Nat, zero: Bool,
1313 rest: Cut -> Bool) -> Bool:
1314 match zero:
1315 case True{}:
1316 chunks.trailer(after)
1317 case False{}:
1318 chunks.ended.data(after, n, chunks.ready(after, n), rest)
1319
1320
1321def chunks.ended.hex(+before: String, +after: String, ok: Bool,
1322 rest: Cut -> Bool) -> Bool:
1323 match ok:
1324 case False{}:
1325 True{}
1326 case True{}:
1327 +n = hexlen(before)
1328 chunks.ended.size(after, n, Nat.is_eq(n, 0n), rest)
1329
1330
1331def chunks.ended.at(c: Cut, rest: Cut -> Bool) -> Bool:
1332 match c:
1333 case NoCut{}:
1334 False{}
1335 case Cut{+before, +after}:
1336 chunks.ended.hex(before, after, hex.well(chunks.token(before)), rest)
1337
1338
1339def chunks.ended(fuel: Nat, c: Cut) -> Bool:
1340 match fuel:
1341 case 0n:
1342 False{}
1343 case 1n+f:
1344 chunks.ended.at(c, cut => chunks.ended(f, cut))
1345
1346
1347def whole.of(f: Frame, +body: String) -> Bool:
1348 match f:
1349 case Bare{}:
1350 True{}
1351 case Sized{n}:
1352 Bool.not(Nat.is_lt(octs.len(body), n))
1353 case Chunked{}:
1354 chunks.ended(String.length(body), divide(body, "\r\n"))
1355 case Closed{}:
1356 False{}
1357
1358
1359
1360def whole.step(asked: Bool, +ls: List<&2, String>, +after: String,
1361 interim: Bool, rest: Cut -> Bool) -> Bool:
1362 match interim:
1363 case True{}:
1364 rest(divide(after, "\r\n\r\n"))
1365 case False{}:
1366 whole.of(frame(Bool.or(asked, status.bare(status.of(ls))),
1367 head.go(ls, "transfer-encoding"), head.values(ls, "content-length")),
1368 after)
1369
1370
1371def whole.at(asked: Bool, c: Cut, rest: Cut -> Bool) -> Bool:
1372 match c:
1373 case NoCut{}:
1374 False{}
1375 case Cut{before, after}:
1376 +ls = String.lines(before)
1377 whole.step(asked, ls, after, status.interim(status.of(ls)), rest)
1378
1379
1380def whole.go(fuel: Nat, +asked: Bool, c: Cut) -> Bool:
1381 match fuel:
1382 case 0n:
1383 False{}
1384 case 1n+f:
1385 whole.at(asked, c, cut => whole.go(f, asked, cut))
1386
1387
1388
1389
1390def whole(+method: String, +raw: String) -> Bool:
1391 whole.go(String.length(raw), String.eq(method, "HEAD"),
1392 divide(raw, "\r\n\r\n"))
1393
1394
1395
1396
1397def status.empty(+m: Maybe<&2, U32>) -> Bool:
1398 Bool.or(status.interim(m), status.bare(m))
1399
1400
1401def head.bare(m: Maybe<&2, U32>, ls: List<&2, String>) -> Plan:
1402 match m:
1403 case None{}:
1404 Unplanned{"the response has no status line"}
1405 case Some{n}:
1406 Planned{n, head.list(ls), Bare{}}
1407
1408
1409def head.final.of(bare: Bool, +m: Maybe<&2, U32>, +ls: List<&2, String>)
1410 -> Plan:
1411 match bare:
1412 case True{}:
1413 head.bare(m, ls)
1414 case False{}:
1415 head.plan(m, ls)
1416
1417
1418
1419def head.final(asked: Bool, +ls: List<&2, String>) -> Plan:
1420 +m = status.of(ls)
1421 head.final.of(Bool.or(asked, status.empty(m)), m, ls)
1422
1423
1424def interim.skip.of(m: Maybe<&2, U32>, more: Bool) -> Bool:
1425 Bool.and(status.interim(m), more)
1426
1427
1428def interim.skip(m: Maybe<&2, U32>, after: String) -> Bool:
1429 interim.skip.of(m, Bool.not(String.is_empty(after)))
1430
1431
1432def parse.to.bare(m: Maybe<&2, U32>, ls: List<&2, String>) -> Reply:
1433 match m:
1434 case None{}:
1435 Torn{"the response has no status line"}
1436 case Some{n}:
1437 Reply{n, head.list(ls), ""}
1438
1439
1440def parse.to.final(m: Maybe<&2, U32>, +ls: List<&2, String>, after: String,
1441 bare: Bool) -> Reply:
1442 match bare:
1443 case True{}:
1444 parse.to.bare(m, ls)
1445 case False{}:
1446 parse.status(m, ls, after)
1447
1448
1449def parse.to.step(head: Bool, +ls: List<&2, String>, +m: Maybe<&2, U32>,
1450 +after: String, skip: Bool, rest: Cut -> Reply) -> Reply:
1451 match skip:
1452 case True{}:
1453 rest(divide(after, "\r\n\r\n"))
1454 case False{}:
1455 parse.to.final(m, ls, after, Bool.or(head, status.empty(m)))
1456
1457
1458def parse.to.at(head: Bool, c: Cut, rest: Cut -> Reply) -> Reply:
1459 match c:
1460 case NoCut{}:
1461 Torn{"the response head has no blank line after it"}
1462 case Cut{before, +after}:
1463 +ls = String.lines(before)
1464 +m = status.of(ls)
1465 parse.to.step(head, ls, m, after, interim.skip(m, after), rest)
1466
1467
1468def parse.to.go(fuel: Nat, +head: Bool, c: Cut) -> Reply:
1469 match fuel:
1470 case 0n:
1471 Torn{"the response head has no blank line after it"}
1472 case 1n+f:
1473 parse.to.at(head, c, cut => parse.to.go(f, head, cut))
1474
1475
1476
1477def parse.to(+method: String, +raw: String) -> Reply:
1478 parse.to.go(String.length(raw), String.eq(method, "HEAD"),
1479 divide(raw, "\r\n\r\n"))
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493type Skip is Data:
1494 Short{owed: Nat}
1495 Over{left: Nat, rest: String}
1496
1497
1498def octs.skip.step(short: Bool, +n: Nat, h: Char, t: String,
1499 rest: Unit -> Skip) -> Skip:
1500 match short:
1501 case True{}:
1502 Over{n, SCon{h, t}}
1503 case False{}:
1504 rest(Unit{})
1505
1506
1507def octs.skip(s: String, +n: Nat) -> Skip:
1508 match s:
1509 case SNil{}:
1510 Short{n}
1511 case SCon{+h, +t}:
1512 +w = octs.width(Char.to_u32(h))
1513 octs.skip.step(Nat.is_lt(n, w), n, h, t, _u => octs.skip(t, Nat.sub(n, w)))
1514
1515
1516
1517
1518
1519type Walk is Data:
1520 Ended{}
1521 Hold{fuel: Nat, rest: String}
1522 Owed{fuel: Nat, owed: Nat}
1523 Tail{fuel: Nat, left: Nat, rest: String}
1524
1525
1526
1527def walk.crlf(g: Nat, +left: Nat, +rest: String, ready: Bool,
1528 go: String -> Walk) -> Walk:
1529 match ready:
1530 case True{}:
1531 go(octs.drop(rest, Nat.add(left, 2n)))
1532 case False{}:
1533 Tail{g, left, rest}
1534
1535
1536def walk.data(+g: Nat, s: Skip, go: String -> Walk) -> Walk:
1537 match s:
1538 case Short{owed}:
1539 Owed{g, owed}
1540 case Over{+left, +rest}:
1541 walk.crlf(g, left, rest, Bool.and(octs.has(rest, Nat.add(left, 2n)),
1542 String.starts_with(rest, "\r\n")), go)
1543
1544
1545def walk.trail(g: Nat, rest: String, ended: Bool) -> Walk:
1546 match ended:
1547 case True{}:
1548 Ended{}
1549 case False{}:
1550 Hold{1n+g, rest}
1551
1552
1553def walk.size(+g: Nat, rest: String, +after: String, +n: Nat, zero: Bool,
1554 go: String -> Walk) -> Walk:
1555 match zero:
1556 case True{}:
1557 walk.trail(g, rest, chunks.trailer(after))
1558 case False{}:
1559 walk.data(g, octs.skip(after, n), go)
1560
1561
1562def walk.hex(+g: Nat, rest: String, +before: String, +after: String, ok: Bool,
1563 go: String -> Walk) -> Walk:
1564 match ok:
1565 case False{}:
1566 Ended{}
1567 case True{}:
1568 +n = hexlen(before)
1569 walk.size(g, rest, after, n, Nat.is_eq(n, 0n), go)
1570
1571
1572def walk.at(+g: Nat, rest: String, c: Cut, go: String -> Walk) -> Walk:
1573 match c:
1574 case NoCut{}:
1575 Hold{1n+g, rest}
1576 case Cut{+before, +after}:
1577 walk.hex(g, rest, before, after, hex.well(chunks.token(before)), go)
1578
1579
1580def walk(fuel: Nat, +rest: String) -> Walk:
1581 match fuel:
1582 case 0n:
1583 Hold{0n, rest}
1584 case 1n+f:
1585 +g = f
1586 walk.at(g, rest, divide(rest, "\r\n"), s => walk(g, s))
1587
1588
1589def walk.done(w: Walk) -> Bool:
1590 match w:
1591 case Ended{}:
1592 True{}
1593 case Hold{fuel, rest}:
1594 False{}
1595 case Owed{fuel, owed}:
1596 False{}
1597 case Tail{fuel, left, rest}:
1598 False{}
1599
1600
1601def walk.more(w: Walk, +m: Nat, b: String) -> Walk:
1602 match w:
1603 case Ended{}:
1604 Ended{}
1605 case Hold{fuel, rest}:
1606 walk(Nat.add(fuel, m), rest ++ b)
1607 case Owed{g, owed}:
1608 +f = Nat.add(g, m)
1609 walk.data(f, octs.skip(b, owed), s => walk(f, s))
1610 case Tail{g, left, rest}:
1611 +f = Nat.add(g, m)
1612 walk.data(f, Over{left, rest ++ b}, s => walk(f, s))
1613
1614
1615
1616
1617
1618type Hear is Data:
1619 Heads{fuel: Nat, asked: Bool, raw: String}
1620 Whole{}
1621 Never{}
1622 Count{n: Nat, have: Nat}
1623 Walking{w: Walk}
1624
1625
1626def hear.body(f: Frame, +after: String) -> Hear:
1627 match f:
1628 case Bare{}:
1629 Whole{}
1630 case Sized{n}:
1631 Count{n, octs.len(after)}
1632 case Chunked{}:
1633 Walking{walk(String.length(after), after)}
1634 case Closed{}:
1635 Never{}
1636
1637
1638def hear.step(asked: Bool, +ls: List<&2, String>, +after: String,
1639 interim: Bool, go: String -> Hear) -> Hear:
1640 match interim:
1641 case True{}:
1642 go(after)
1643 case False{}:
1644 hear.body(frame(Bool.or(asked, status.bare(status.of(ls))),
1645 head.go(ls, "transfer-encoding"), head.values(ls, "content-length")),
1646 after)
1647
1648
1649def hear.at(+g: Nat, +asked: Bool, raw: String, c: Cut, go: String -> Hear)
1650 -> Hear:
1651 match c:
1652 case NoCut{}:
1653 Heads{1n+g, asked, raw}
1654 case Cut{before, after}:
1655 +ls = String.lines(before)
1656 hear.step(asked, ls, after, status.interim(status.of(ls)), go)
1657
1658
1659def hear.go(fuel: Nat, +asked: Bool, +raw: String) -> Hear:
1660 match fuel:
1661 case 0n:
1662 Heads{0n, asked, raw}
1663 case 1n+f:
1664 +g = f
1665 hear.at(g, asked, raw, divide(raw, "\r\n\r\n"), s => hear.go(g, asked, s))
1666
1667
1668
1669def hear.of(+method: String, +raw: String) -> Hear:
1670 hear.go(String.length(raw), String.eq(method, "HEAD"), raw)
1671
1672
1673def hear.done(h: Hear) -> Bool:
1674 match h:
1675 case Heads{fuel, asked, raw}:
1676 False{}
1677 case Whole{}:
1678 True{}
1679 case Never{}:
1680 False{}
1681 case Count{n, have}:
1682 Bool.not(Nat.is_lt(have, n))
1683 case Walking{w}:
1684 walk.done(w)
1685
1686
1687def hear.more(h: Hear, +b: String) -> Hear:
1688 match h:
1689 case Heads{fuel, asked, raw}:
1690 hear.go(Nat.add(fuel, String.length(b)), asked, raw ++ b)
1691 case Whole{}:
1692 Whole{}
1693 case Never{}:
1694 Never{}
1695 case Count{n, have}:
1696 Count{n, Nat.add(have, octs.len(b))}
1697 case Walking{w}:
1698 Walking{walk.more(w, String.length(b), b)}
1699
1700
1701
1702def walk.counts(w: Walk, +m: Nat) -> Bool:
1703 match w:
1704 case Ended{}:
1705 False{}
1706 case Hold{fuel, rest}:
1707 False{}
1708 case Owed{fuel, owed}:
1709 Bool.not(Nat.is_lt(owed, m))
1710 case Tail{fuel, left, rest}:
1711 False{}
1712
1713
1714
1715def walk.more.n(w: Walk, +m: Nat) -> Walk:
1716 match w:
1717 case Ended{}:
1718 Ended{}
1719 case Hold{fuel, rest}:
1720 Hold{fuel, rest}
1721 case Owed{g, owed}:
1722 Owed{Nat.add(g, m), Nat.sub(owed, m)}
1723 case Tail{fuel, left, rest}:
1724 Tail{fuel, left, rest}
1725
1726
1727
1728
1729def hear.counts(h: Hear, +m: Nat) -> Bool:
1730 match h:
1731 case Heads{fuel, asked, raw}:
1732 False{}
1733 case Whole{}:
1734 True{}
1735 case Never{}:
1736 True{}
1737 case Count{n, have}:
1738 True{}
1739 case Walking{w}:
1740 walk.counts(w, m)
1741
1742
1743
1744def hear.more.n(h: Hear, +m: Nat) -> Hear:
1745 match h:
1746 case Heads{fuel, asked, raw}:
1747 Heads{fuel, asked, raw}
1748 case Whole{}:
1749 Whole{}
1750 case Never{}:
1751 Never{}
1752 case Count{n, have}:
1753 Count{n, Nat.add(have, m)}
1754 case Walking{w}:
1755 Walking{walk.more.n(w, m)}
1756
1757
1758
1759
1760
1761def phrase.chop.at(
1762 cr: Bool,
1763 ch: Char,
1764 tl: String,
1765 rest: Unit -> String
1766) -> String:
1767 match cr tl:
1768 case True{} SNil{}:
1769 SNil{}
1770 case True{} SCon{x, xt}:
1771 SCon{ch, rest(Unit{})}
1772 case False{} _:
1773 SCon{ch, rest(Unit{})}
1774
1775
1776def phrase.chop(text: String) -> String:
1777 match text:
1778 case SNil{}:
1779 SNil{}
1780 case SCon{+ch, +tl}:
1781 phrase.chop.at(Char.is_eq(ch, '\r'), ch, tl, _u => phrase.chop(tl))
1782
1783
1784def phrase.code(cut: Cut) -> String:
1785 match cut:
1786 case NoCut{}:
1787 ""
1788 case Cut{before, after}:
1789 phrase.chop(after)
1790
1791
1792def phrase.after(cut: Cut) -> String:
1793 match cut:
1794 case NoCut{}:
1795 ""
1796 case Cut{before, after}:
1797 phrase.code(divide(after, " "))
1798
1799
1800
1801def phrase.line(line: String) -> String:
1802 phrase.after(divide(line, " "))
1803
1804
1805def phrase.to.step(
1806 +ls: List<&2, String>,
1807 +after: String,
1808 skip: Bool,
1809 rest: Cut -> String
1810) -> String:
1811 match skip:
1812 case True{}:
1813 rest(divide(after, "\r\n\r\n"))
1814 case False{}:
1815 phrase.line(first(ls))
1816
1817
1818def phrase.to.at(cut: Cut, rest: Cut -> String) -> String:
1819 match cut:
1820 case NoCut{}:
1821 ""
1822 case Cut{before, +after}:
1823 +ls = String.lines(before)
1824 phrase.to.step(ls, after, interim.skip(status.of(ls), after), rest)
1825
1826
1827def phrase.to.go(fuel: Nat, cut: Cut) -> String:
1828 match fuel:
1829 case 0n:
1830 ""
1831 case 1n+f:
1832 phrase.to.at(cut, next => phrase.to.go(f, next))
1833
1834
1835
1836def phrase.to(+raw: String) -> String:
1837 phrase.to.go(String.length(raw), divide(raw, "\r\n\r\n"))