mod common;
use base64::Engine as _;
use common::{Rng, CODECS};
use tape_base64::{Codec, DecodeError};
const SOUP: [u8; 32] = [
b'A', b'B', b'M', b'Z', b'a', b'b', b'm', b'z', b'0', b'5', b'9', b'+', b'/', b'-', b'_', b'=',
b'@', b'[', b'`', b'{', b':', b';', b'<', b'>', b'^', b'.', b',', 0x00, 0x0A, 0x7F, 0x80, 0xFF,
];
fn streamed_encode(codec: Codec, pieces: &[&[u8]]) -> Vec<u8> {
let mut encoder = codec.encoder();
let mut out = Vec::new();
for piece in pieces {
let mut spot = vec![0u8; piece.len() * 2 + 8];
let written = encoder.push(piece, &mut spot).expect("a roomy buffer");
out.extend_from_slice(&spot[..written]);
}
let mut spot = [0u8; 4];
let written = encoder.finish(&mut spot).expect("a roomy buffer");
out.extend_from_slice(&spot[..written]);
out
}
fn streamed_decode(codec: Codec, pieces: &[&[u8]]) -> Result<Vec<u8>, DecodeError> {
let mut decoder = codec.decoder();
let mut out = Vec::new();
let mut refused = false;
for piece in pieces {
let mut spot = vec![0u8; piece.len() * 2 + 8];
match decoder.push(piece, &mut spot) {
Ok(written) if !refused => out.extend_from_slice(&spot[..written]),
Ok(_) => {}
Err(_) => refused = true,
}
}
let mut spot = [0u8; 4];
let written = decoder.finish(&mut spot)?;
assert!(!refused, "a refusal is reported again at the end");
out.extend_from_slice(&spot[..written]);
Ok(out)
}
fn singles(input: &[u8]) -> Vec<&[u8]> {
input.iter().map(core::slice::from_ref).collect()
}
fn chunked<'a>(input: &'a [u8], rng: &mut Rng, most: usize) -> Vec<&'a [u8]> {
let mut pieces = Vec::new();
let mut rest = input;
while !rest.is_empty() {
let take = (rng.below(most) + 1).min(rest.len());
pieces.push(&rest[..take]);
rest = &rest[take..];
}
pieces
}
fn cuts<'a>(input: &'a [u8], rng: &mut Rng, shapes: &mut Vec<Vec<&'a [u8]>>) {
for at in 0..=input.len() {
shapes.push(vec![&input[..at], &input[at..]]);
}
if input.len() > 6 {
shapes.push(chunked(input, rng, 7));
}
}
#[test]
fn encode_pieces_match() {
for entry in &CODECS {
for len in 0..70usize {
let input: Vec<u8> = (0..len).map(|at| (at * 37 + 11) as u8).collect();
let whole = common::encode_to_vec(entry.codec, &input);
let mut shapes = Vec::new();
cuts(&input, &mut Rng::new(len as u64 + 1), &mut shapes);
shapes.push(singles(&input));
for pieces in &shapes {
assert_eq!(
streamed_encode(entry.codec, pieces),
whole,
"{} at {} pieces",
entry.name,
pieces.len()
);
}
}
}
}
#[test]
fn decode_pieces_match() {
for entry in &CODECS {
for len in 0..70usize {
let input: Vec<u8> = (0..len).map(|at| (at * 53 + 7) as u8).collect();
let encoded = common::encode_to_vec(entry.codec, &input);
let joined = encoded.as_slice();
let mut shapes = Vec::new();
cuts(joined, &mut Rng::new(len as u64 + 2), &mut shapes);
shapes.push(singles(joined));
for pieces in &shapes {
let mine = streamed_decode(entry.codec, pieces).expect("a valid encoding");
assert_eq!(mine, input, "{} in {} pieces", entry.name, pieces.len());
assert!(entry.reference.decode(joined).is_ok());
}
}
}
}
#[test]
fn refusals_match_the_batch() {
let mut rng = Rng::new(0x243F_6A88);
for _ in 0..4000 {
let entry = &CODECS[rng.below(CODECS.len())];
let mut source = [0u8; 60];
rng.fill(&mut source);
let taken = rng.below(61);
let mut encoded = common::encode_to_vec(entry.codec, &source[..taken]);
for _ in 0..=rng.below(3) {
let at = rng.below(encoded.len().max(1));
if encoded.is_empty() {
break;
}
encoded[at] = if rng.below(3) == 0 {
rng.next_u64() as u8
} else {
rng.pick(&SOUP)
};
}
let left = common::encode_to_vec(entry.codec, &source[..rng.below(taken / 2 + 1)]);
let right = common::encode_to_vec(
entry.codec,
&source[taken / 2..taken.max(taken / 2 + rng.below(taken - taken / 2 + 1))],
);
let candidates: [&[u8]; 3] = [&encoded, &left, &right];
for joined in candidates {
let batch = common::decode_to_vec(entry.codec, joined);
let mut shapes = Vec::new();
cuts(joined, &mut rng, &mut shapes);
shapes.push(singles(joined));
for pieces in shapes.iter().take(5) {
let streamed = streamed_decode(entry.codec, pieces);
match (&batch, &streamed) {
(Ok(theirs), Ok(mine)) => {
assert_eq!(mine, theirs, "{} reading {}", entry.name, label(joined))
}
(Err(theirs), Err(mine)) => {
assert_eq!(mine, theirs, "{} refusing {}", entry.name, label(joined))
}
_ => panic!(
"{} disagreed with itself about {}: {batch:?} against {streamed:?}",
entry.name,
label(joined)
),
}
assert_eq!(
streamed.is_ok(),
entry.reference.decode(joined).is_ok(),
"{} disagrees with the reference about {}",
entry.name,
label(joined)
);
}
}
}
}
#[test]
fn a_refusal_partway_matches_the_batch() {
let mut rng = Rng::new(0x0BAD_5EED);
let mut source = vec![0u8; 512];
for _ in 0..200 {
let entry = &CODECS[rng.below(CODECS.len())];
rng.fill(&mut source);
let taken = 64 + rng.below(449);
let mut encoded = common::encode_to_vec(entry.codec, &source[..taken]);
let at = rng.below(encoded.len() / 4);
encoded[at] = rng.pick(&SOUP);
encoded.resize(encoded.len() + rng.below(4), b'=');
encoded.truncate(encoded.len() - rng.below(3));
let batch = common::decode_to_vec(entry.codec, &encoded);
let mut shapes = vec![vec![encoded.as_slice()]];
for most in [1, 5, 64, 512] {
shapes.push(chunked(&encoded, &mut rng, most));
}
for pieces in &shapes {
assert_eq!(
streamed_decode(entry.codec, pieces),
batch,
"{} in {} pieces reading {}",
entry.name,
pieces.len(),
label(&encoded)
);
}
}
}
#[test]
fn short_encode_buffer() {
let mut encoder = tape_base64::STANDARD.encoder();
let mut spot = [0u8; 3];
assert!(encoder.push(b"abcd", &mut spot).is_err());
assert!(encoder.push(b"abc", &mut spot).is_err());
let mut spot = [0u8; 4];
assert_eq!(encoder.push(b"abc", &mut spot).unwrap(), 4);
assert_eq!(&spot, b"YWJj");
}
#[test]
fn short_decode_buffer() {
let mut decoder = tape_base64::STANDARD.decoder();
let mut spot = [0u8; 5];
assert_eq!(
decoder.push(b"QUJDREVG", &mut spot),
Err(DecodeError::BufferTooSmall { needed: 6 })
);
let mut room = [0u8; 6];
assert_eq!(decoder.push(b"QUJDREVG", &mut room).unwrap(), 6);
assert_eq!(&room, b"ABCDEF");
assert_eq!(decoder.finish(&mut []).unwrap(), 0);
}
#[test]
fn helper_sized_buffers_are_accepted() {
const INPUTS: [&[u8]; 6] = [
b"QQ==",
b"aGVsbG8=",
b"QQQQ",
b"aGVsbG8h",
b"Zm8=",
b"Zm9vYmFy",
];
for entry in &CODECS {
for input in INPUTS {
let Ok(exact) = entry.codec.decoded_len(input) else {
continue;
};
let mut whole = vec![0u8; exact];
let Ok(expected) = entry.codec.decode(input, &mut whole) else {
continue;
};
for room in [exact, entry.codec.decoded_len_max(input.len())] {
let mut out = vec![0u8; room];
let mut decoder = entry.codec.decoder();
let written = decoder
.push(input, &mut out)
.unwrap_or_else(|_| panic!("{} refused {room} bytes", entry.name));
let rest = decoder.finish(&mut out[written..]).expect("the same room");
assert_eq!(written + rest, expected, "{}", entry.name);
assert_eq!(out[..expected], whole[..expected], "{}", entry.name);
}
}
}
}
#[test]
fn short_finish_buffer() {
let mut decoder = tape_base64::STANDARD.decoder();
assert_eq!(decoder.push(b"QQ==", &mut [0u8; 8]).unwrap(), 0);
assert_eq!(
decoder.finish(&mut []),
Err(DecodeError::BufferTooSmall { needed: 1 })
);
assert_eq!(
tape_base64::STANDARD.decode(b"QQ==", &mut []),
Err(DecodeError::BufferTooSmall { needed: 1 })
);
}
#[test]
fn finish_checks_the_last_symbol() {
let codec = tape_base64::STANDARD_NO_PAD;
let refusal = DecodeError::InvalidLastSymbol {
position: 2,
byte: b'B',
};
let mut decoder = codec.decoder();
assert_eq!(decoder.push(b"AAB", &mut [0u8; 8]).unwrap(), 0);
assert_eq!(
decoder.finish(&mut [0u8; 1]),
Err(DecodeError::BufferTooSmall { needed: 2 })
);
assert_eq!(
codec.decode(b"AAB", &mut [0u8; 1]),
Err(DecodeError::BufferTooSmall { needed: 2 })
);
let mut decoder = codec.decoder();
assert_eq!(decoder.push(b"AAB", &mut [0u8; 8]).unwrap(), 0);
assert_eq!(decoder.finish(&mut [0u8; 2]), Err(refusal));
assert_eq!(codec.decode(b"AAB", &mut [0u8; 2]), Err(refusal));
}
#[test]
fn ending_then_more() {
let pieces: [&[u8]; 3] = [b"AB", b"==", b"CD"];
let mine = streamed_decode(tape_base64::STANDARD, &pieces);
let batch = common::decode_to_vec(tape_base64::STANDARD, b"AB==CD");
assert_eq!(mine, batch);
}
fn label(input: &[u8]) -> String {
String::from_utf8_lossy(input).into_owned()
}