#[cfg(feature = "alloc")]
#[allow(
unused_imports,
reason = "alloc prelude items; subset used per cfg/feature combination"
)]
use alloc::vec::Vec;
use super::alphabet::{B64_ALPHABET, b64_byte_to_index};
use super::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Qb64<'a>(pub &'a [u8]);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Qb2<'a>(pub &'a [u8]);
fn decode_block(chunk: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
let v0 = b64_byte_to_index(chunk[0])?;
let v1 = b64_byte_to_index(chunk[1])?;
let v2 = b64_byte_to_index(chunk[2])?;
let v3 = b64_byte_to_index(chunk[3])?;
let bits = (u32::from(v0) << 18) | (u32::from(v1) << 12) | (u32::from(v2) << 6) | u32::from(v3);
out.push(truncate_u32_to_u8(bits >> 16));
out.push(truncate_u32_to_u8(bits >> 8));
out.push(truncate_u32_to_u8(bits));
Ok(())
}
fn encode_block(chunk: &[u8], out: &mut Vec<u8>) {
let bits = (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]);
out.push(B64_ALPHABET[usize_from_u32((bits >> 18) & 0x3F)]);
out.push(B64_ALPHABET[usize_from_u32((bits >> 12) & 0x3F)]);
out.push(B64_ALPHABET[usize_from_u32((bits >> 6) & 0x3F)]);
out.push(B64_ALPHABET[usize_from_u32(bits & 0x3F)]);
}
impl Qb64<'_> {
pub fn decode(self) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
self.decode_into(&mut out)?;
Ok(out)
}
pub fn decode_into(self, out: &mut Vec<u8>) -> Result<(), Error> {
let qb64 = self.0;
if !qb64.len().is_multiple_of(4) {
return Err(Error::Misaligned {
len: qb64.len(),
unit: 4,
});
}
out.reserve(qb64.len() / 4 * 3);
for chunk in qb64.chunks_exact(4) {
decode_block(chunk, out)?;
}
Ok(())
}
}
impl Qb2<'_> {
pub fn encode(self) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
self.encode_into(&mut out)?;
Ok(out)
}
pub fn encode_into(self, out: &mut Vec<u8>) -> Result<(), Error> {
let qb2 = self.0;
if !qb2.len().is_multiple_of(3) {
return Err(Error::Misaligned {
len: qb2.len(),
unit: 3,
});
}
out.reserve(qb2.len() / 3 * 4);
for chunk in qb2.chunks_exact(3) {
encode_block(chunk, out);
}
Ok(())
}
}
#[allow(
clippy::as_conversions,
reason = "masked to u8 range; `as` is the only option for bit truncation"
)]
const fn truncate_u32_to_u8(v: u32) -> u8 {
(v & 0xFF) as u8
}
#[allow(
clippy::as_conversions,
reason = "value masked to 6 bits, always fits in usize"
)]
const fn usize_from_u32(v: u32) -> usize {
v as usize
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test code: panics acceptable"
)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn decode_counter() {
assert_eq!(Qb64(b"-AAB").decode().unwrap(), vec![0xF8, 0x00, 0x01]);
}
#[test]
fn roundtrip_multi_block() {
let original = b"-AAB-AAC";
let binary = Qb64(original).decode().unwrap();
assert_eq!(binary.len(), 6);
assert_eq!(&Qb2(&binary).encode().unwrap(), original);
}
#[test]
fn decode_rejects_misaligned() {
assert_eq!(
Qb64(b"ABC").decode().unwrap_err(),
Error::Misaligned { len: 3, unit: 4 }
);
}
#[test]
fn encode_rejects_misaligned() {
assert_eq!(
Qb2(&[0u8, 1]).encode().unwrap_err(),
Error::Misaligned { len: 2, unit: 3 }
);
}
#[test]
fn decode_into_leaves_buffer_untouched_on_misaligned() {
let mut buf = vec![0x01, 0x02];
assert!(Qb64(b"ABC").decode_into(&mut buf).is_err());
assert_eq!(buf, vec![0x01, 0x02]);
}
#[test]
fn decode_invalid_character() {
assert!(Qb64(b"-A!B").decode().is_err());
}
#[test]
fn empty_inputs() {
assert_eq!(Qb64(b"").decode().unwrap(), Vec::<u8>::new());
assert_eq!(Qb2(&[]).encode().unwrap(), Vec::<u8>::new());
}
}