b64.bend

cswap-bend-cli@0.1.0.0 · 223 lines · 33 definitions

1import 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
8def 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
19def 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.
23def 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
30def sel(c: Bool, x: U32, y: U32) -> U32:
31  match c:
32    case True{}:
33      x
34    case False{}:
35      y
36
37def 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.
45def 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
48def 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.
54def utf8.need(+b: U32, +need: U32) -> U32:
55  sel(U32.is_eq(need, 0), utf8.len(b), (need - 1 : U32))
56
57def 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
60def 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
63def 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).
71def 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
79def utf8.decode(bs: List<&2, U32>) -> String:
80  utf8.decode.go(bs, 0, 0)
81
82# ---- base64 ---------------------------------------------------------------
83
84def 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
90def 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.
105def encode_bytes(bs: List<&2, U32>) -> String:
106  b64.enc.go(bs)
107
108# Base64 of the UTF-8 form of the text.
109def encode(s: String) -> String:
110  encode_bytes(utf8.encode(s))
111
112# 0..63 the sextet, 64 for `=`, 255 for anything else.
113def 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
121def 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
130def 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
141def 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.
146type Quad is Data:
147  QEnd{}
148  QBad{}
149  Q{w: U32, x: U32, y: U32, z: U32, rest: String}
150
151def 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
158def 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
165def 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
172def 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.
182def 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
193def b64.dec.start(+s: String) -> Maybe<&2, List<&2, U32>>:
194  b64.dec.go(String.length(s), b64.quad.of(s))
195
196def 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
203def 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.
211def decode_bytes(s: String) -> Maybe<&2, List<&2, U32>>:
212  b64.dec.start(b64.strip(s))
213
214def 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.
222def decode(s: String) -> Maybe<&2, String>:
223  b64.text(decode_bytes(s))