src/pct.bend
emerging-ezhttp@0.20.0.0 · 89 lines · 0 definitions
1# ezhttp/src/pct: percent-encoding (RFC 3986 §2.1), the way Python's
2# urllib.parse.quote does it. Unreserved characters (§2.3) are written as
3# themselves; the path form also keeps "/"; every other character is written
4# as the bytes of its UTF-8 encoding (RFC 3629 §3), each as "%" and two
5# upper-case hex digits. SPEC.md HTTP-URL lists what is guaranteed.
6import Base
7
8# RFC 3986 §2.3 unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~"
9def unreserved(+cp: U32) -> Bool:
10 Bool.or(Bool.and(U32.is_le(65, cp), U32.is_le(cp, 90)),
11 Bool.or(Bool.and(U32.is_le(97, cp), U32.is_le(cp, 122)),
12 Bool.or(Bool.and(U32.is_le(48, cp), U32.is_le(cp, 57)),
13 Bool.or(U32.is_eq(cp, 45),
14 Bool.or(U32.is_eq(cp, 46),
15 Bool.or(U32.is_eq(cp, 95), U32.is_eq(cp, 126)))))))
16
17# a character a form writes as itself: unreserved, or "/" when `slash` (the
18# path form) keeps it
19def safe(slash: Bool, +cp: U32) -> Bool:
20 Bool.or(unreserved(cp), Bool.and(U32.is_eq(cp, 47), slash))
21
22# one hex digit, upper case: 0-9 then A-F ('A' is 55 + 10)
23def hex(+nib: U32) -> Char:
24 Chr{Bool.pick(U32, U32.is_lt(nib, 10), U32.add(48, nib), U32.add(55, nib))}
25
26# bytes as %XX triples, high nibble first, followed by `rest`
27def pct(bytes: List<&2, U32>, rest: String) -> String:
28 match bytes:
29 case []:
30 rest
31 case +byte <> tail:
32 SCon{'%', SCon{hex(U32.shrn(byte, 4n)), SCon{hex(U32.and(byte, 15)),
33 pct(tail, rest)}}}
34
35# a continuation byte: 10xxxxxx from the six bits at `shift`
36def cont(+cp: U32, shift: Nat) -> U32:
37 U32.or(128, U32.and(U32.shrn(cp, shift), 63))
38
39# four bytes: U+10000 to U+10FFFF
40def utf8.four(+cp: U32) -> List<&2, U32>:
41 [U32.or(240, U32.shrn(cp, 18n)), cont(cp, 12n), cont(cp, 6n), cont(cp, 0n)]
42
43# three bytes below U+10000, else four
44def utf8.three(+cp: U32, fits: Bool) -> List<&2, U32>:
45 match fits:
46 case True{}:
47 [U32.or(224, U32.shrn(cp, 12n)), cont(cp, 6n), cont(cp, 0n)]
48 case False{}:
49 utf8.four(cp)
50
51# two bytes below U+0800, else more
52def utf8.two(+cp: U32, fits: Bool) -> List<&2, U32>:
53 match fits:
54 case True{}:
55 [U32.or(192, U32.shrn(cp, 6n)), cont(cp, 0n)]
56 case False{}:
57 utf8.three(cp, U32.is_lt(cp, 65536))
58
59# one byte below U+0080, else more
60def utf8.one(+cp: U32, fits: Bool) -> List<&2, U32>:
61 match fits:
62 case True{}:
63 [cp]
64 case False{}:
65 utf8.two(cp, U32.is_lt(cp, 2048))
66
67# the UTF-8 bytes of one code point, in order (RFC 3629 §3)
68def utf8(+cp: U32) -> List<&2, U32>:
69 utf8.one(cp, U32.is_lt(cp, 128))
70
71# one character, once it is known whether the form keeps it
72def one.of(+cp: U32, rest: String, kept: Bool) -> String:
73 match kept:
74 case True{}:
75 SCon{Chr{cp}, rest}
76 case False{}:
77 pct(utf8(cp), rest)
78
79# one character as the form writes it, followed by `rest`
80def one(slash: Bool, +cp: U32, rest: String) -> String:
81 one.of(cp, rest, safe(slash, cp))
82
83# a string as the form writes it: each character in turn
84def quote(+slash: Bool, shift: String) -> String:
85 match shift:
86 case SNil{}:
87 SNil{}
88 case SCon{Chr{+cp}, tail}:
89 one(slash, cp, quote(slash, tail))