mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 13:02:25 +00:00
235 lines
8.5 KiB
Rust
235 lines
8.5 KiB
Rust
//! Standard base64 (RFC 4648 §4, with `=` padding), as VectorCraft writes members in the JSON.
|
|
//! The decoder skips ASCII whitespace.
|
|
|
|
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
/// Sextet value of each byte; 64 for bytes outside the alphabet.
|
|
const VALUE: [u8; 256] = {
|
|
let mut t = [64u8; 256];
|
|
let mut i = 0;
|
|
while i < 64 {
|
|
t[ALPHABET[i] as usize] = i as u8;
|
|
i += 1;
|
|
}
|
|
t
|
|
};
|
|
|
|
fn symbol(sextet: u32) -> char {
|
|
ALPHABET.get((sextet & 63) as usize).map_or('A', |&b| char::from(b))
|
|
}
|
|
|
|
/// `data` as base64, padded with `=`.
|
|
pub fn encode(data: &[u8]) -> String {
|
|
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
|
|
for c in data.chunks(3) {
|
|
let byte = |i: usize| u32::from(c.get(i).copied().unwrap_or(0));
|
|
let n = byte(0) << 16 | byte(1) << 8 | byte(2);
|
|
out.push(symbol(n >> 18));
|
|
out.push(symbol(n >> 12));
|
|
out.push(if c.len() > 1 { symbol(n >> 6) } else { '=' });
|
|
out.push(if c.len() > 2 { symbol(n) } else { '=' });
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The number of bytes `s` decodes to when it is valid: 3 for every 4 characters other than
|
|
/// whitespace, less one for each trailing `=` (at most 2).
|
|
pub fn decoded_len(s: &str) -> u64 {
|
|
let clean = s.bytes().filter(|b| !b.is_ascii_whitespace());
|
|
let n = clean.clone().count() as u64;
|
|
let pad = clean.rev().take(2).take_while(|&b| b == b'=').count() as u64;
|
|
(n / 4 * 3).saturating_sub(pad)
|
|
}
|
|
|
|
pub(crate) const CHECK_CHUNK: usize = 64 * 1024;
|
|
|
|
/// [`decoded_len`] with a fallible checkpoint before each bounded portion of input.
|
|
pub fn decoded_len_with_check<E>(s: &str, mut check: impl FnMut() -> Result<(), E>) -> Result<u64, E> {
|
|
let (n, pad) = measured_with_check(s, &mut check)?;
|
|
Ok((n / 4 * 3).saturating_sub(pad))
|
|
}
|
|
|
|
fn measured_with_check<E>(s: &str, mut check: impl FnMut() -> Result<(), E>) -> Result<(u64, u64), E> {
|
|
check()?;
|
|
let (mut n, mut pad) = (0u64, 0u64);
|
|
for chunk in s.as_bytes().chunks(CHECK_CHUNK) {
|
|
check()?;
|
|
for &b in chunk {
|
|
if !b.is_ascii_whitespace() {
|
|
n += 1;
|
|
pad = if b == b'=' { (pad + 1).min(2) } else { 0 };
|
|
}
|
|
}
|
|
}
|
|
Ok((n, pad))
|
|
}
|
|
|
|
/// [`decode`] with fallible checkpoints during length calculation and decoding. Whitespace,
|
|
/// padding and non-canonical groups follow [`decode`]; `Ok(None)` means invalid base64.
|
|
pub fn decode_with_check<E>(s: &str, mut check: impl FnMut() -> Result<(), E>) -> Result<Option<Vec<u8>>, E> {
|
|
let (n, pad) = measured_with_check(s, &mut check)?;
|
|
if !n.is_multiple_of(4) {
|
|
return Ok(None);
|
|
}
|
|
let Ok(len) = usize::try_from((n / 4 * 3).saturating_sub(pad)) else { return Ok(None) };
|
|
decode_chunks(s, len, &mut check)
|
|
}
|
|
|
|
fn decode_chunks<E>(s: &str, capacity: usize, check: &mut impl FnMut() -> Result<(), E>) -> Result<Option<Vec<u8>>, E> {
|
|
let mut out = Vec::with_capacity(capacity);
|
|
let mut clean = Vec::with_capacity(CHECK_CHUNK);
|
|
let mut ended = false;
|
|
for chunk in s.as_bytes().chunks(CHECK_CHUNK) {
|
|
check()?;
|
|
for &b in chunk {
|
|
if b.is_ascii_whitespace() {
|
|
continue;
|
|
}
|
|
if ended {
|
|
return Ok(None);
|
|
}
|
|
clean.push(b);
|
|
if clean.len() == CHECK_CHUNK {
|
|
let Ok(text) = std::str::from_utf8(&clean) else { return Ok(None) };
|
|
let Some(decoded) = decode(text) else { return Ok(None) };
|
|
out.extend_from_slice(&decoded);
|
|
ended = clean.contains(&b'=');
|
|
clean.clear();
|
|
}
|
|
}
|
|
}
|
|
let Ok(text) = std::str::from_utf8(&clean) else { return Ok(None) };
|
|
let Some(decoded) = decode(text) else { return Ok(None) };
|
|
out.extend_from_slice(&decoded);
|
|
Ok(Some(out))
|
|
}
|
|
|
|
/// The bytes `s` encodes; `None` when it is not valid padded base64. Padding is allowed only at
|
|
/// the end. The unused bits before the padding are not checked, so a non-canonical group decodes
|
|
/// like its canonical form (`Zh==` gives the bytes of `Zg==`, and `Zm9=` those of `Zm8=`), as
|
|
/// VectorCraft's own decoder reads them.
|
|
pub fn decode(s: &str) -> Option<Vec<u8>> {
|
|
let clean = s.bytes().filter(|b| !b.is_ascii_whitespace());
|
|
let n = clean.clone().count();
|
|
if !n.is_multiple_of(4) {
|
|
return None;
|
|
}
|
|
let mut out = Vec::with_capacity(n / 4 * 3);
|
|
let (mut group, mut len, mut pad, mut ended) = (0u32, 0usize, 0usize, false);
|
|
for b in clean {
|
|
if ended {
|
|
return None;
|
|
}
|
|
group <<= 6;
|
|
if b == b'=' {
|
|
pad += 1;
|
|
} else {
|
|
let v = *VALUE.get(usize::from(b))?;
|
|
if v == 64 || pad > 0 {
|
|
return None;
|
|
}
|
|
group |= u32::from(v);
|
|
}
|
|
len += 1;
|
|
if len == 4 {
|
|
if pad > 2 {
|
|
return None;
|
|
}
|
|
let [_, a, b, c] = group.to_be_bytes();
|
|
out.push(a);
|
|
if pad < 2 {
|
|
out.push(b);
|
|
}
|
|
if pad < 1 {
|
|
out.push(c);
|
|
}
|
|
ended = pad > 0;
|
|
(group, len) = (0, 0);
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_rfc_vectors_encode_and_decode() {
|
|
for (dec, enc) in
|
|
[("", ""), ("f", "Zg=="), ("fo", "Zm8="), ("foo", "Zm9v"), ("foob", "Zm9vYg=="), ("fooba", "Zm9vYmE="), ("foobar", "Zm9vYmFy")]
|
|
{
|
|
assert_eq!(encode(dec.as_bytes()), enc);
|
|
assert_eq!(decode(enc).as_deref(), Some(dec.as_bytes()), "{enc}");
|
|
assert_eq!(decoded_len(enc), dec.len() as u64, "{enc}");
|
|
assert_eq!(decode_with_check(enc, || Ok::<_, ()>(())).unwrap(), decode(enc));
|
|
assert_eq!(decoded_len_with_check(enc, || Ok::<_, ()>(())).unwrap(), decoded_len(enc));
|
|
}
|
|
assert_eq!(decode("Zm9v\nYmFy").as_deref(), Some(&b"foobar"[..]));
|
|
}
|
|
|
|
#[test]
|
|
fn every_byte_value_round_trips() {
|
|
let all: Vec<u8> = (0..=255).collect();
|
|
for n in 0..all.len() {
|
|
assert_eq!(decode(&encode(&all[..n])).as_deref(), Some(&all[..n]));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nonzero_bits_before_the_padding_are_ignored() {
|
|
for (loose, canonical) in [("Zh==", "Zg=="), ("Zm9=", "Zm8=")] {
|
|
assert_eq!(decode(loose), decode(canonical), "{loose}");
|
|
assert_eq!(decoded_len(loose), decoded_len(canonical), "{loose}");
|
|
}
|
|
assert_eq!(decode("Zh==").as_deref(), Some(&b"f"[..]));
|
|
assert_eq!(decode("Zm9=").as_deref(), Some(&b"fo"[..]));
|
|
}
|
|
|
|
#[test]
|
|
fn misplaced_padding_and_foreign_characters_are_refused() {
|
|
for s in ["====", "A===", "AA=A", "=AAA", "QQ==QUJD", "QUI=QUJD", "QUJ", "QU*D", "QUJDé"] {
|
|
assert_eq!(decode(s), None, "{s}");
|
|
assert_eq!(decode_with_check(s, || Ok::<_, ()>(())).unwrap(), None, "{s}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn checked_blocks_preserve_padding_and_whitespace() {
|
|
for pad in ["QUJD", "Zh==", "Zm9=", "A===", "QUJ"] {
|
|
let data = format!("{}\n{pad}\t", "QUJD".repeat(CHECK_CHUNK / 4 - 1));
|
|
for data in [data.clone(), format!("{data}QUJD")] {
|
|
assert_eq!(decode_with_check(&data, || Ok::<_, ()>(())).unwrap(), decode(&data));
|
|
assert_eq!(decoded_len_with_check(&data, || Ok::<_, ()>(())).unwrap(), decoded_len(&data));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decoding_can_stop_after_its_first_block() {
|
|
let data = "QUJD".repeat(CHECK_CHUNK);
|
|
let mut polls = 0;
|
|
let mut check = || {
|
|
polls += 1;
|
|
if polls == 2 { Err("stopped") } else { Ok(()) }
|
|
};
|
|
assert_eq!(decode_chunks(&data, 0, &mut check), Err("stopped"));
|
|
assert_eq!(polls, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn checked_image_reads_keep_bounds_and_blob_ranges() {
|
|
let mut image = crate::Image::inline("application/pdf", b"abcdef");
|
|
for max in [0, 5, 6, u64::MAX] {
|
|
assert_eq!(image.bytes_at_most_with_check(b"", max, || Ok::<_, ()>(())).unwrap(), image.bytes_at_most(b"", max));
|
|
}
|
|
image.blob = Some([2, 3]);
|
|
for tail in [&b"123456"[..], b"12"] {
|
|
for max in [0, 2, 3, u64::MAX] {
|
|
assert_eq!(image.bytes_at_most_with_check(tail, max, || Ok::<_, ()>(())).unwrap(), image.bytes_at_most(tail, max));
|
|
}
|
|
}
|
|
assert_eq!(image.bytes_at_most_with_check(b"123456", 3, || Err(17)), Err(17));
|
|
}
|
|
}
|