use keyhog_core::decode_standard_base64;
fn encode_standard_base64_reference(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char);
out.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char);
match chunk.len() {
1 => {
out.push('=');
out.push('=');
}
2 => {
out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char);
out.push('=');
}
_ => {
out.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char);
out.push(ALPHABET[(n & 0x3F) as usize] as char);
}
}
}
out
}
struct Xorshift64(u64);
impl Xorshift64 {
fn next_byte(&mut self) -> u8 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
(x.wrapping_mul(0x2545F4914F6CDD1D) >> 56) as u8
}
fn bytes(&mut self, n: usize) -> Vec<u8> {
(0..n).map(|_| self.next_byte()).collect()
}
}
#[test]
fn reference_encoder_matches_rfc4648_test_vectors() {
let vectors: &[(&[u8], &str)] = &[
(b"", ""),
(b"f", "Zg=="),
(b"fo", "Zm8="),
(b"foo", "Zm9v"),
(b"foob", "Zm9vYg=="),
(b"fooba", "Zm9vYmE="),
(b"foobar", "Zm9vYmFy"),
];
for (raw, expected) in vectors {
assert_eq!(
encode_standard_base64_reference(raw),
*expected,
"reference encoder disagrees with RFC-4648 §10 on {raw:?}, the \
differential oracle is broken, fix the test encoder first"
);
}
}
#[test]
fn every_payload_length_round_trips_to_exact_bytes() {
let mut rng = Xorshift64(0x9E3779B97F4A7C15);
let mut cases = 0usize;
for len in 0..=768usize {
let payload = rng.bytes(len);
let encoded = encode_standard_base64_reference(&payload);
let decoded = decode_standard_base64(&encoded).unwrap_or_else(|e| {
panic!("len {len}: well-formed base64 {encoded:?} must decode, got error {e:?}")
});
assert_eq!(
decoded.len(),
len,
"len {len}: decoded {} bytes, expected {len}. TAIL TRUNCATION \
regression (the wave-1 base64 fix). encoded={encoded:?}",
decoded.len()
);
assert_eq!(
decoded, payload,
"len {len}: round-trip corrupted the payload. encoded={encoded:?}"
);
cases += 1;
}
assert_eq!(cases, 769, "expected 769 length cases, ran {cases}");
}
#[test]
fn unpadded_form_round_trips_to_exact_bytes() {
let mut rng = Xorshift64(0xD1B54A32D192ED03);
let mut cases = 0usize;
for len in 0..=768usize {
let payload = rng.bytes(len);
let encoded = encode_standard_base64_reference(&payload);
let unpadded = encoded.trim_end_matches('=');
let decoded = decode_standard_base64(unpadded).unwrap_or_else(|e| {
panic!("len {len}: unpadded {unpadded:?} must decode, got error {e:?}")
});
assert_eq!(
decoded, payload,
"len {len}: unpadded round-trip corrupted the payload. unpadded={unpadded:?}"
);
cases += 1;
}
assert_eq!(cases, 769, "expected 769 unpadded cases, ran {cases}");
}
#[test]
fn interior_padding_followed_by_data_is_always_rejected() {
let mut rng = Xorshift64(0x2545F4914F6CDD1D);
const MSG: &str = "invalid base64: data after padding '=' (padding may only appear at the end)";
let mut cases = 0usize;
for len in 3..=120usize {
let payload = rng.bytes(len);
let encoded = encode_standard_base64_reference(&payload);
let base = encoded.trim_end_matches('=').to_string();
let chars: Vec<char> = base.chars().collect();
for cut in 1..chars.len() {
let mut mutated: String = chars[..cut].iter().collect();
mutated.push('=');
mutated.extend(&chars[cut..]); let res = decode_standard_base64(&mutated);
assert!(
res.is_err(),
"len {len} cut {cut}: interior '=' then data {mutated:?} must be \
REJECTED (silent-truncation regression), but it decoded to {:?}",
res.clone().unwrap_or_default()
);
assert_eq!(
res.unwrap_err(),
MSG,
"len {len} cut {cut}: interior-padding rejection message regressed for {mutated:?}"
);
cases += 1;
}
}
assert!(
cases >= 2000,
"interior-padding matrix should generate >=2000 mutation cases, ran {cases}"
);
}
#[test]
fn out_of_alphabet_bytes_are_rejected_not_skipped() {
let in_alphabet = |c: u8| {
c.is_ascii_uppercase()
|| c.is_ascii_lowercase()
|| c.is_ascii_digit()
|| c == b'+'
|| c == b'/'
|| c == b'='
};
let mut cases = 0usize;
for bad in 0u8..=127 {
if in_alphabet(bad) {
continue;
}
let probe = format!("AB{}D", bad as char);
let res = decode_standard_base64(&probe);
assert!(
res.is_err(),
"byte {bad:#x} is outside the base64 alphabet; {probe:?} must be \
rejected, not silently skipped (it decoded to {:?})",
res.clone().unwrap_or_default()
);
let msg = res.unwrap_err();
assert!(
msg.contains("invalid base64 char") && msg.contains(&format!("{bad:#x}")),
"byte {bad:#x}: rejection must name the offending char, got {msg:?}"
);
cases += 1;
}
assert_eq!(
cases, 63,
"expected 63 invalid-ASCII-byte cases, ran {cases}"
);
}
#[test]
fn lone_trailing_char_is_rejected() {
let mut rng = Xorshift64(0x106689D45497FDB5);
let mut cases = 0usize;
for len in 1..=200usize {
let payload = rng.bytes(len);
let base = encode_standard_base64_reference(&payload)
.trim_end_matches('=')
.to_string();
let mut l = base.len();
while l > 0 && l % 4 != 1 {
l -= 1;
}
if l == 0 || l == base.len() {
continue; }
let truncated = &base[..l];
let res = decode_standard_base64(truncated);
assert!(
res.is_err(),
"len {len}: a final quad with one leftover char {truncated:?} \
(len%4==1) must be rejected, but decoded to {:?}",
res.unwrap_or_default()
);
cases += 1;
}
assert!(
cases >= 50,
"lone-trailing-char matrix should generate >=50 cases, ran {cases}"
);
}
#[test]
fn oversize_input_is_rejected_at_the_exact_boundary() {
let max_standard_base64_input_bytes =
keyhog_core::testing::CoreTestApi::max_standard_base64_input_bytes(
&keyhog_core::testing::TestApi,
);
let over = "A".repeat(max_standard_base64_input_bytes + 1);
let err = decode_standard_base64(&over).expect_err("input over the cap must be rejected");
assert_eq!(
err,
format!("base64 input exceeds {max_standard_base64_input_bytes} bytes"),
"oversize rejection message regressed"
);
assert_eq!(
decode_standard_base64("QUJD").expect("under-cap input decodes"),
b"ABC",
"the size guard must not reject inputs under the cap"
);
assert_eq!(
max_standard_base64_input_bytes,
16 * 1024 * 1024,
"the standard-base64 input cap changed, update the DoS-bound contract \
and the scanner's matching limit together"
);
}