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. 6 import Base 7 8 # RFC 3986 §2.3 unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~" 9 def 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 19 def 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) 23 def 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` 27 def 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` 36 def 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 40 def 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 44 def 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 52 def 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 60 def 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) 68 def 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 72 def 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` 80 def 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 84 def 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))