1 # ezhttp/src/b64: Base64 encoding for HTTP credentials (RFC 4648 §4). The 2 # alphabet is the standard table, with `=` padding. Decoding is not required 3 # for the Authorization helpers. 4 import Base 5 6 # how many octets of the current quantum are still missing 7 type Phase is Data: 8 Need3{} 9 Need2{first: U32} 10 Need1{first: U32, second: U32} 11 12 # RFC 4648 §4 alphabet, index 0 is `A` 13 def b64.table() -> String: 14 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 15 16 # the character at a 0..63 index, `A` when the index is out of the table 17 def b64.char(m: Maybe<&2, Char>) -> Char: 18 match m: 19 case None{}: 20 'A' 21 case Some{c}: 22 c 23 24 # one 6-bit value as an alphabet character 25 def b64.at(+n: U32) -> Char: 26 b64.char(String.get(b64.table(), U32.to_nat(U32.and(n, 63)))) 27 28 # 24 bits from three octets 29 def b64.pack(+a: U32, +b: U32, +c: U32) -> U32: 30 U32.or(U32.or(U32.shln(a, 16n), U32.shln(b, 8n)), c) 31 32 # the last two characters when the quantum was short 33 def b64.tail.more(+n: U32, left: Nat) -> String: 34 match left: 35 case 0n: 36 SCon{b64.at(U32.shrn(n, 6n)), SCon{'=', SNil{}}} 37 case 1n+k: 38 SCon{'=', SCon{'=', SNil{}}} 39 40 # four characters from a packed quantum; pad is how many `=` to write 41 def b64.tail(+n: U32, pad: Nat) -> String: 42 match pad: 43 case 0n: 44 SCon{b64.at(U32.shrn(n, 6n)), SCon{b64.at(n), SNil{}}} 45 case 1n+p: 46 b64.tail.more(n, p) 47 48 # one quantum as four characters 49 def b64.quad(+a: U32, +b: U32, +c: U32, pad: Nat) -> String: 50 +n = b64.pack(a, b, c) 51 SCon{b64.at(U32.shrn(n, 18n)), SCon{b64.at(U32.shrn(n, 12n)), b64.tail(n, pad)}} 52 53 # padding for a quantum that ended early 54 def b64.end(phase: Phase) -> String: 55 match phase: 56 case Need3{}: 57 "" 58 case Need2{first}: 59 b64.quad(first, 0, 0, 2n) 60 case Need1{first, second}: 61 b64.quad(first, second, 0, 1n) 62 63 # octets to Base64, three at a time (RFC 4648 §4) 64 def b64.go(bs: List<&2, U32>, phase: Phase) -> String: 65 match bs: 66 case []: 67 b64.end(phase) 68 case h <> t: 69 match phase: 70 case Need3{}: 71 b64.go(t, Need2{h}) 72 case Need2{first}: 73 b64.go(t, Need1{first, h}) 74 case Need1{first, second}: 75 b64.quad(first, second, h, 0n) ++ b64.go(t, Need3{}) 76 77 # code points of a string, one octet each (credentials are ASCII) 78 def b64.bytes(s: String) -> List<&2, U32>: 79 match s: 80 case SNil{}: 81 [] 82 case SCon{h, t}: 83 Char.to_u32(h) <> b64.bytes(t) 84 85 # Base64 of a string (RFC 4648 §4) 86 def b64.encode(s: String) -> String: 87 b64.go(b64.bytes(s), Need3{})