base64-ng 2.0.0

no_std-first Base64 encoding and decoding with strict APIs and a security-heavy release process
Documentation
use super::{
    contracts::InputError,
    ordinary::{self, OneShotError},
    rfc4648_oracle::{self as oracle, DecodeFailure, ErrorClass, Profile},
};
use crate::{DecodeError, STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD};

const RFC_VECTORS: &[(&[u8], &[u8])] = &[
    (b"", b""),
    (b"f", b"Zg=="),
    (b"fo", b"Zm8="),
    (b"foo", b"Zm9v"),
    (b"foob", b"Zm9vYg=="),
    (b"fooba", b"Zm9vYmE="),
    (b"foobar", b"Zm9vYmFy"),
];

#[test]
fn oracle_self_tests_against_rfc4648_vectors() {
    for &(plain, encoded) in RFC_VECTORS {
        assert_eq!(oracle::encode(Profile::StandardPadded, plain), encoded);
        assert_eq!(
            oracle::decode(Profile::StandardPadded, encoded),
            Ok(plain.to_vec())
        );
    }
}

#[test]
fn oracle_is_strict_about_alphabet_padding_and_canonical_bits() {
    assert_eq!(
        oracle::decode(Profile::StandardPadded, b"Zm9v\n"),
        Err(DecodeFailure {
            class: ErrorClass::Length,
            offset: None,
        })
    );
    assert_eq!(
        oracle::decode(Profile::StandardPadded, b"Zh=="),
        Err(DecodeFailure {
            class: ErrorClass::Padding,
            offset: Some(1),
        })
    );
    assert_eq!(
        oracle::decode(Profile::UrlSafeUnpadded, b"AA/A"),
        Err(DecodeFailure {
            class: ErrorClass::Byte,
            offset: Some(2),
        })
    );
}

#[test]
fn legacy_and_v2_compatibility_boundary_match_independent_oracle() {
    let profiles = [
        Profile::StandardPadded,
        Profile::StandardUnpadded,
        Profile::UrlSafePadded,
        Profile::UrlSafeUnpadded,
    ];
    let mut input = [0u8; 97];

    for len in 0..=input.len() {
        for (index, byte) in input[..len].iter_mut().enumerate() {
            *byte = u8::try_from((index * 53 + len * 29) % 256).unwrap();
        }

        for profile in profiles {
            let expected = oracle::encode(profile, &input[..len]);
            let mut legacy = [0u8; 132];
            let legacy_len = legacy_encode(profile, &input[..len], &mut legacy);
            let mut emerging = [0u8; 132];
            let emerging_len = ordinary::encode(profile, &input[..len], &mut emerging).unwrap();

            assert_eq!(&legacy[..legacy_len], expected);
            assert_eq!(&emerging[..emerging_len], expected);

            let decoded = oracle::decode(profile, &expected).unwrap();
            let mut legacy_plain = [0u8; 97];
            let legacy_plain_len = legacy_decode(profile, &expected, &mut legacy_plain).unwrap();
            let mut emerging_plain = [0u8; 97];
            let emerging_plain_len =
                ordinary::decode(profile, &expected, &mut emerging_plain).unwrap();

            assert_eq!(&legacy_plain[..legacy_plain_len], decoded);
            assert_eq!(&emerging_plain[..emerging_plain_len], decoded);
        }
    }
}

#[test]
fn malformed_diagnostics_match_where_compatibility_is_intentional() {
    for (profile, input) in [
        (Profile::StandardPadded, &b"Zm!v"[..]),
        (Profile::StandardPadded, b"Zh=="),
        (Profile::StandardPadded, b"Zg="),
        (Profile::StandardUnpadded, b"Z"),
        (Profile::StandardUnpadded, b"Zh"),
        (Profile::UrlSafePadded, b"AA/A"),
        (Profile::UrlSafeUnpadded, b"AA+A"),
    ] {
        let expected = oracle::decode(profile, input).unwrap_err();
        let mut legacy = [0x55; 16];
        let legacy_error = legacy_decode(profile, input, &mut legacy).unwrap_err();
        let mut emerging = [0xaa; 16];
        let emerging_error = ordinary::decode(profile, input, &mut emerging).unwrap_err();

        assert_eq!(normalized(legacy_error), expected);
        assert_eq!(normalized_v2(emerging_error), expected);
    }
}

fn legacy_encode(profile: Profile, input: &[u8], output: &mut [u8]) -> usize {
    match profile {
        Profile::StandardPadded => STANDARD.encode_slice(input, output),
        Profile::StandardUnpadded => STANDARD_NO_PAD.encode_slice(input, output),
        Profile::UrlSafePadded => URL_SAFE.encode_slice(input, output),
        Profile::UrlSafeUnpadded => URL_SAFE_NO_PAD.encode_slice(input, output),
    }
    .unwrap()
}

fn legacy_decode(profile: Profile, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
    match profile {
        Profile::StandardPadded => STANDARD.decode_slice(input, output),
        Profile::StandardUnpadded => STANDARD_NO_PAD.decode_slice(input, output),
        Profile::UrlSafePadded => URL_SAFE.decode_slice(input, output),
        Profile::UrlSafeUnpadded => URL_SAFE_NO_PAD.decode_slice(input, output),
    }
}

fn normalized(error: DecodeError) -> DecodeFailure {
    match error {
        DecodeError::InvalidLength => DecodeFailure {
            class: ErrorClass::Length,
            offset: None,
        },
        DecodeError::InvalidByte { index, .. } => DecodeFailure {
            class: ErrorClass::Byte,
            offset: Some(index),
        },
        DecodeError::InvalidPadding { index } => DecodeFailure {
            class: ErrorClass::Padding,
            offset: Some(index),
        },
        other => panic!("unexpected compatibility error: {other:?}"),
    }
}

fn normalized_v2(error: OneShotError) -> DecodeFailure {
    match error {
        OneShotError::Input(InputError::InvalidLength | InputError::TruncatedInput { .. }) => {
            DecodeFailure {
                class: ErrorClass::Length,
                offset: None,
            }
        }
        OneShotError::Input(InputError::InvalidByte { index, .. }) => DecodeFailure {
            class: ErrorClass::Byte,
            offset: Some(index),
        },
        OneShotError::Input(
            InputError::InvalidPadding { index }
            | InputError::NonCanonicalTrailingBits { index }
            | InputError::TrailingData { index },
        ) => DecodeFailure {
            class: ErrorClass::Padding,
            offset: Some(index),
        },
        other => panic!("unexpected 2.0 compatibility error: {other:?}"),
    }
}