// Base64, for the `$bytes` representation.
//
// Encoding uses the STANDARD alphabet with the padding stripped, which is what
// the reference implementation emits and therefore what a byte-for-byte
// round-trip requires. Decoding accepts both the standard and URL-safe
// alphabets, with or without padding, because being liberal about what arrives
// costs nothing here and a server that pads is not wrong.
///|
const B64_ALPHABET : String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
///|
let b64_digits : Array[Char] = B64_ALPHABET.iter().collect()
///|
/// The digit value of a base64 character, accepting both alphabets: `-` and `_`
/// are the URL-safe spellings of `+` and `/`.
fn b64_digit(unit : UInt16) -> Int? {
let i = unit.to_int()
if i >= 'A'.to_int() && i <= 'Z'.to_int() {
Some(i - 'A'.to_int())
} else if i >= 'a'.to_int() && i <= 'z'.to_int() {
Some(i - 'a'.to_int() + 26)
} else if i >= '0'.to_int() && i <= '9'.to_int() {
Some(i - '0'.to_int() + 52)
} else if i == '+'.to_int() || i == '-'.to_int() {
Some(62)
} else if i == '/'.to_int() || i == '_'.to_int() {
Some(63)
} else {
None
}
}
///|
pub fn base64_encode(bytes : Bytes) -> String {
let b = StringBuilder::new(size_hint=(bytes.length() + 2) / 3 * 4)
let mut i = 0
while i + 2 < bytes.length() {
let chunk = (bytes[i].to_int() << 16) |
(bytes[i + 1].to_int() << 8) |
bytes[i + 2].to_int()
b.write_char(b64_digits[(chunk >> 18) & 0x3F])
b.write_char(b64_digits[(chunk >> 12) & 0x3F])
b.write_char(b64_digits[(chunk >> 6) & 0x3F])
b.write_char(b64_digits[chunk & 0x3F])
i = i + 3
}
// The tail, unpadded: two leftover bytes become three characters and one
// becomes two, rather than four with `=` filling the gap.
let left = bytes.length() - i
if left == 1 {
let chunk = bytes[i].to_int() << 16
b.write_char(b64_digits[(chunk >> 18) & 0x3F])
b.write_char(b64_digits[(chunk >> 12) & 0x3F])
} else if left == 2 {
let chunk = (bytes[i].to_int() << 16) | (bytes[i + 1].to_int() << 8)
b.write_char(b64_digits[(chunk >> 18) & 0x3F])
b.write_char(b64_digits[(chunk >> 12) & 0x3F])
b.write_char(b64_digits[(chunk >> 6) & 0x3F])
}
b.to_string()
}
///|
/// `None` if the text is not base64. A lone trailing character is not: base64
/// has no encoding that produces one, so a 4n+1 length means truncation.
pub fn base64_decode(text : String) -> Bytes? {
let out = []
let mut buffer = 0
let mut bits = 0
let mut digits = 0
for i = 0; i < text.length(); i = i + 1 {
let unit = text[i]
// Padding is accepted and contributes nothing; anything after it would be
// caught by the length check below.
if unit.to_int() == '='.to_int() {
continue
}
guard b64_digit(unit) is Some(digit) else { return None }
digits = digits + 1
buffer = (buffer << 6) | digit
bits = bits + 6
if bits >= 8 {
bits = bits - 8
out.push(((buffer >> bits) & 0xFF).to_byte())
}
}
// No encoding produces a group of one character: three bytes become four
// characters, two become three and one becomes two, so a count of 4n+1 means
// the text was truncated.
guard digits % 4 != 1 else { return None }
// Whatever is left must be zero-valued padding bits, and there must be fewer
// than a whole byte of them.
guard bits < 8 && (buffer & ((1 << bits) - 1)) == 0 else { return None }
Some(Bytes::from_array(out))
}