src/b64.bend

bendonnect-rpc@0.1.0.0 · 61 lines · 10 definitions

1# Base64 (RFC 4648 sections 4 and 5) over Bytes. Pure.
2# Encoders return String (ASCII); decode takes a String and accepts either
3# alphabet, padded or not. Built on Bytes.to_base64 / Bytes.from_base64.
4import Base
5import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
6
7# Drops the leading '=' run of a reversed string (i.e. trailing pad of the original).
8def strip_pad.pick(hit: Bool, h: Char, t: String, rest: Unit -> String) -> String:
9  match hit:
10    case True{}:
11      rest(Unit{})
12    case False{}:
13      SCon{h, t}
14
15def strip_pad.rev(s: String) -> String:
16  match s:
17    case SNil{}:
18      SNil{}
19    case SCon{Chr{+c}, +t}:
20      strip_pad.pick(U32.is_eq(c, 61), Chr{c}, t, u => strip_pad.rev(t))
21
22# Maps char a1 to b1 and a2 to b2, leaving other chars alone.
23def swap2(s: String, +a1: U32, +b1: U32, +a2: U32, +b2: U32) -> String:
24  match s:
25    case SNil{}:
26      SNil{}
27    case SCon{Chr{+c}, t}:
28      SCon{Chr{Bool.pick(U32, U32.is_eq(c, a1), b1, Bool.pick(U32, U32.is_eq(c, a2), b2, c))}, swap2(t, a1, b1, a2, b2)}
29
30# Standard alphabet ('+', '/') with '=' padding.
31def encode_std(b: Bytes.Bytes) -> String:
32  Bytes.to_base64(b)
33
34# Standard alphabet, no padding.
35def encode_std_raw(b: Bytes.Bytes) -> String:
36  String.reverse(strip_pad.rev(String.reverse(Bytes.to_base64(b))))
37
38# URL-safe alphabet ('-', '_'), no padding.
39def encode_url_raw(b: Bytes.Bytes) -> String:
40  swap2(encode_std_raw(b), 43, 45, 47, 95)
41
42def has_pad(s: String) -> Bool:
43  match s:
44    case SNil{}:
45      False{}
46    case SCon{Chr{+c}, t}:
47      Bool.or(U32.is_eq(c, 61), has_pad(t))
48
49def decode.padded(+s: String, +m: U32) -> Maybe<&1, Bytes.Bytes>:
50  Bool.pick(Maybe<&1, Bytes.Bytes>, U32.is_eq(m, 0), Bytes.from_base64(s),
51  Bool.pick(Maybe<&1, Bytes.Bytes>, U32.is_eq(m, 2), Bytes.from_base64(String.append(s, "==")),
52  Bool.pick(Maybe<&1, Bytes.Bytes>, U32.is_eq(m, 3), Bytes.from_base64(String.append(s, "=")),
53  None{})))
54
55def decode.norm(+s: String, +n: U32) -> Maybe<&1, Bytes.Bytes>:
56  Bool.pick(Maybe<&1, Bytes.Bytes>, has_pad(s), Bytes.from_base64(s), decode.padded(s, (n .&. 3 : U32)))
57
58# Either alphabet, padded or unpadded. None on any invalid input (bad char,
59# bad padding, impossible length).
60def decode(+s: String) -> Maybe<&1, Bytes.Bytes>:
61  decode.norm(swap2(s, 45, 43, 95, 47), Bytes.count(s, 0))