// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The bit packer behind base16, base32 and base64.
///
/// The three differ in one number — how many bits a character carries — and in
/// their alphabets. Writing the packing once means a fix to the padding rules
/// or the error positions lands in all three at once, and it is why each of
/// those packages is a table and five one-line functions.
pub fn pack(
input : BytesView,
bits : Int,
digits : Array[Char],
pad : Bool,
) -> String {
let out = StringBuilder()
let mut buffer = 0
let mut held = 0
let mask = (1 << bits) - 1
let mut count = 0
for i in 0..= bits {
held = held - bits
out.write_char(digits[(buffer >> held) & mask])
count = count + 1
}
buffer = buffer & ((1 << held) - 1)
}
if held > 0 {
out.write_char(digits[(buffer << (bits - held)) & mask])
count = count + 1
}
if pad {
let group = group_of(bits)
while count % group != 0 {
out.write_char('=')
count = count + 1
}
}
out.to_string()
}
///|
/// The same packing, as the ASCII bytes it would be written with.
///
/// Wire formats — an HTTP header, a gRPC metadata value — hold bytes, and
/// going through a `String` only to turn it back into bytes costs a pass over
/// the data for nothing.
pub fn pack_bytes(
input : BytesView,
bits : Int,
digits : Array[Char],
pad : Bool,
) -> Bytes {
let text = pack(input, bits, digits, pad)
let out = Array::make(text.length(), b'\x00')
for i in 0.. Bytes raise Malformed {
let out : Array[Byte] = []
let mut buffer = 0
let mut held = 0
let mut taken = 0
let mut padding = false
for i in 0..= 0 && code < 128 { lookup[code] } else { -1 }
if value < 0 {
if lossy {
continue
}
raise Bad(at=i, char=code.unsafe_to_char())
}
buffer = (buffer << bits) | value
held = held + bits
taken = taken + 1
while held >= 8 {
held = held - 8
out.push(((buffer >> held) & 0xff).to_byte())
}
buffer = buffer & ((1 << held) - 1)
}
if taken * bits % 8 >= bits {
// A whole character is left over that cannot become a byte.
raise Truncated(at=input.length())
}
Bytes::from_array(out)
}
///|
/// Unpack from ASCII bytes, for callers who never had a string.
pub fn unpack_bytes(
input : BytesView,
bits : Int,
lookup : Array[Int],
lossy : Bool,
) -> Bytes raise Malformed {
let out = StringBuilder()
for i in 0.. Array[Int] {
let table = Array::make(128, -1)
let digits = chars.to_array()
for i in 0..= 0 && code < 128 {
table[code] = i
}
}
table
}
///|
/// The same table, accepting either case, for the encodings whose decoders do.
pub fn lookup_of_both(lower : String, upper : String) -> Array[Int] {
let table = lookup_of(lower)
let digits = upper.to_array()
for i in 0..= 0 && code < 128 {
table[code] = i
}
}
table
}
///|
/// How many characters make a whole number of bytes.
fn group_of(bits : Int) -> Int {
let mut chars = 1
while chars * bits % 8 != 0 {
chars = chars + 1
}
chars
}