1 import Base 2 3 # Standard base64 (RFC 4648, padded). `.creds-*.enc` files are the base64 of 4 # the credential JSON, so text goes through UTF-8 first. 5 6 # ---- UTF-8 ---------------------------------------------------------------- 7 8 def utf8.cat.pick(a: Bool, b: Bool, d: Bool, +c: U32, rest: List<&2, U32>) -> List<&2, U32>: 9 match a b d: 10 case True{} _ _: 11 c <> rest 12 case False{} True{} _: 13 (192 .|. (c >> 6n) : U32) <> (128 .|. (c .&. 63) : U32) <> rest 14 case False{} False{} True{}: 15 (224 .|. (c >> 12n) : U32) <> (128 .|. ((c >> 6n) .&. 63) : U32) <> (128 .|. (c .&. 63) : U32) <> rest 16 case False{} False{} False{}: 17 (240 .|. (c >> 18n) : U32) <> (128 .|. ((c >> 12n) .&. 63) : U32) <> (128 .|. ((c >> 6n) .&. 63) : U32) <> (128 .|. (c .&. 63) : U32) <> rest 18 19 def utf8.cat(+c: U32, rest: List<&2, U32>) -> List<&2, U32>: 20 utf8.cat.pick(U32.is_lt(c, 128), U32.is_lt(c, 2048), U32.is_lt(c, 65536), c, rest) 21 22 # The UTF-8 bytes of a string. 23 def utf8.encode(s: String) -> List<&2, U32>: 24 match s: 25 case SNil{}: 26 [] 27 case SCon{Chr{c}, t}: 28 utf8.cat(c, utf8.encode(t)) 29 30 def sel(c: Bool, x: U32, y: U32) -> U32: 31 match c: 32 case True{}: 33 x 34 case False{}: 35 y 36 37 def sel.b(c: Bool, x: Bool, y: Bool) -> Bool: 38 match c: 39 case True{}: 40 x 41 case False{}: 42 y 43 44 # Continuation bytes a lead byte announces. 45 def utf8.len(+b: U32) -> U32: 46 sel(U32.is_lt(b, 128), 0, sel(U32.is_lt(b, 224), 1, sel(U32.is_lt(b, 240), 2, 3))) 47 48 def utf8.mask(+n: U32) -> U32: 49 sel(U32.is_eq(n, 0), 255, sel(U32.is_eq(n, 1), 31, sel(U32.is_eq(n, 2), 15, 7))) 50 51 # The decoder state is (need, cp): continuation bytes still due and the code 52 # point so far. These three give the next need, the next cp, and whether cp 53 # is complete. 54 def utf8.need(+b: U32, +need: U32) -> U32: 55 sel(U32.is_eq(need, 0), utf8.len(b), (need - 1 : U32)) 56 57 def utf8.cp(+b: U32, +need: U32, +cp: U32) -> U32: 58 sel(U32.is_eq(need, 0), (b .&. utf8.mask(utf8.len(b)) : U32), ((cp << 6n) .|. (b .&. 63) : U32)) 59 60 def utf8.emit(+b: U32, +need: U32) -> Bool: 61 sel.b(U32.is_eq(need, 0), U32.is_eq(utf8.len(b), 0), U32.is_eq(need, 1)) 62 63 def utf8.put(emit: Bool, c: U32, rest: String) -> String: 64 match emit: 65 case True{}: 66 SCon{Chr{c}, rest} 67 case False{}: 68 rest 69 70 # Decode UTF-8 bytes (lenient on malformed input). 71 def utf8.decode.go(bs: List<&2, U32>, +need: U32, +cp: U32) -> String: 72 match bs: 73 case Nil{}: 74 SNil{} 75 case Con{+b, t}: 76 +cp2 = utf8.cp(b, need, cp) 77 utf8.put(utf8.emit(b, need), cp2, utf8.decode.go(t, utf8.need(b, need), cp2)) 78 79 def utf8.decode(bs: List<&2, U32>) -> String: 80 utf8.decode.go(bs, 0, 0) 81 82 # ---- base64 --------------------------------------------------------------- 83 84 def b64.char(+n: U32) -> Char: 85 Chr{sel(U32.is_lt(n, 26), (n + 65 : U32), 86 sel(U32.is_lt(n, 52), (n + 71 : U32), 87 sel(U32.is_lt(n, 62), (n - 4 : U32), 88 sel(U32.is_eq(n, 62), 43, 47))))} 89 90 def b64.enc.go(bs: List<&2, U32>) -> String: 91 match bs: 92 case Nil{}: 93 SNil{} 94 case Con{+a, Nil{}}: 95 SCon{b64.char((a >> 2n : U32)), SCon{b64.char(((a .&. 3) << 4n : U32)), "=="}} 96 case Con{+a, Con{+b, Nil{}}}: 97 SCon{b64.char((a >> 2n : U32)), SCon{b64.char((((a .&. 3) << 4n) .|. (b >> 4n) : U32)), 98 SCon{b64.char(((b .&. 15) << 2n : U32)), "="}}} 99 case Con{+a, Con{+b, Con{+c, t}}}: 100 SCon{b64.char((a >> 2n : U32)), SCon{b64.char((((a .&. 3) << 4n) .|. (b >> 4n) : U32)), 101 SCon{b64.char((((b .&. 15) << 2n) .|. (c >> 6n) : U32)), SCon{b64.char((c .&. 63 : U32)), 102 b64.enc.go(t)}}}} 103 104 # Base64 of the bytes. 105 def encode_bytes(bs: List<&2, U32>) -> String: 106 b64.enc.go(bs) 107 108 # Base64 of the UTF-8 form of the text. 109 def encode(s: String) -> String: 110 encode_bytes(utf8.encode(s)) 111 112 # 0..63 the sextet, 64 for `=`, 255 for anything else. 113 def b64.val(+c: U32) -> U32: 114 sel(U32.is_ge(c, 65) && U32.is_le(c, 90), (c - 65 : U32), 115 sel(U32.is_ge(c, 97) && U32.is_le(c, 122), (c - 71 : U32), 116 sel(U32.is_ge(c, 48) && U32.is_le(c, 57), (c + 4 : U32), 117 sel(U32.is_eq(c, 43), 62, 118 sel(U32.is_eq(c, 47), 63, 119 sel(U32.is_eq(c, 61), 64, 255)))))) 120 121 def b64.quad.emit(p3: Bool, p4: Bool, a: U32, b: U32, c: U32, r: List<&2, U32>) -> Maybe<&2, List<&2, U32>>: 122 match p3 p4: 123 case True{} _: 124 Some{a <> r} 125 case False{} True{}: 126 Some{a <> b <> r} 127 case False{} False{}: 128 Some{a <> b <> c <> r} 129 130 def b64.quad.pick(ok: Bool, p3: Bool, p4: Bool, +w: U32, +x: U32, +y: U32, +z: U32, 131 rest: Maybe<&2, List<&2, U32>>) -> Maybe<&2, List<&2, U32>>: 132 match ok rest: 133 case False{} _: 134 None{} 135 case True{} None{}: 136 None{} 137 case True{} Some{r}: 138 b64.quad.emit(p3, p4, ((w << 2n) .|. (x >> 4n) : U32), (((x .&. 15) << 4n) .|. (y >> 2n) : U32), 139 (((y .&. 3) << 6n) .|. z : U32), r) 140 141 def b64.dec.quad(+w: U32, +x: U32, +y: U32, +z: U32, rest: Maybe<&2, List<&2, U32>>) -> Maybe<&2, List<&2, U32>>: 142 b64.quad.pick(U32.is_lt(w, 64) && U32.is_lt(x, 64) && (U32.is_lt(y, 64) || U32.is_eq(y, 64)) && (U32.is_lt(z, 64) || U32.is_eq(z, 64)), 143 U32.is_eq(y, 64), U32.is_eq(z, 64), w, x, y, z, rest) 144 145 # The next four characters of the input as sextets, or the end, or a bad tail. 146 type Quad is Data: 147 QEnd{} 148 QBad{} 149 Q{w: U32, x: U32, y: U32, z: U32, rest: String} 150 151 def b64.quad.of4(c1: U32, c2: U32, c3: U32, s: String) -> Quad: 152 match s: 153 case SCon{Chr{c4}, t}: 154 Q{b64.val(c1), b64.val(c2), b64.val(c3), b64.val(c4), t} 155 case _: 156 QBad{} 157 158 def b64.quad.of3(c1: U32, c2: U32, s: String) -> Quad: 159 match s: 160 case SCon{Chr{c3}, t}: 161 b64.quad.of4(c1, c2, c3, t) 162 case _: 163 QBad{} 164 165 def b64.quad.of2(c1: U32, s: String) -> Quad: 166 match s: 167 case SCon{Chr{c2}, t}: 168 b64.quad.of3(c1, c2, t) 169 case _: 170 QBad{} 171 172 def b64.quad.of(s: String) -> Quad: 173 match s: 174 case SNil{}: 175 QEnd{} 176 case SCon{Chr{c1}, t}: 177 b64.quad.of2(c1, t) 178 case _: 179 QBad{} 180 181 # `fuel` is the input length, at least the number of quads. 182 def b64.dec.go(fuel: Nat, q: Quad) -> Maybe<&2, List<&2, U32>>: 183 match fuel q: 184 case _ QEnd{}: 185 Some{[]} 186 case _ QBad{}: 187 None{} 188 case 0n _: 189 None{} 190 case 1n+p Q{w, x, y, z, t}: 191 b64.dec.quad(w, x, y, z, b64.dec.go(p, b64.quad.of(t))) 192 193 def b64.dec.start(+s: String) -> Maybe<&2, List<&2, U32>>: 194 b64.dec.go(String.length(s), b64.quad.of(s)) 195 196 def b64.strip.put(sp: Bool, c: Char, rest: String) -> String: 197 match sp: 198 case True{}: 199 rest 200 case False{}: 201 SCon{c, rest} 202 203 def b64.strip(s: String) -> String: 204 match s: 205 case SNil{}: 206 SNil{} 207 case SCon{+c, t}: 208 b64.strip.put(Char.is_space(c), c, b64.strip(t)) 209 210 # Decode to bytes; None on a bad character, bad padding or a short tail. 211 def decode_bytes(s: String) -> Maybe<&2, List<&2, U32>>: 212 b64.dec.start(b64.strip(s)) 213 214 def b64.text(m: Maybe<&2, List<&2, U32>>) -> Maybe<&2, String>: 215 match m: 216 case None{}: 217 None{} 218 case Some{bs}: 219 Some{utf8.decode(bs)} 220 221 # Decode to UTF-8 text. 222 def decode(s: String) -> Maybe<&2, String>: 223 b64.text(decode_bytes(s))