base64-ng 2.0.1

no_std-first Base64 encoding and decoding with strict RFC 4648 APIs and optional SIMD
Documentation
use super::{
    alphabet::ValidatedAlphabet,
    specifications::{
        Base64, Codec, CodecBuilder, CodecBuilderError, DecodePadding, EncodePadding, RuntimeSpec,
        STRICT_STANDARD_PADDED, STRICT_STANDARD_UNPADDED, STRICT_URL_SAFE_PADDED,
        STRICT_URL_SAFE_UNPADDED, TrailingBits,
    },
};
use crate::{Alphabet, Engine, STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD};

const STANDARD_TABLE: [u8; 64] =
    *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const CUSTOM_TABLE: [u8; 64] = *b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

#[test]
fn strict_presets_match_legacy_rfc4648_vectors() {
    let 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"),
    ];
    assert_strict_preset(STRICT_STANDARD_PADDED.settings(), STANDARD, vectors);
    assert_strict_preset(
        STRICT_STANDARD_UNPADDED.settings(),
        STANDARD_NO_PAD,
        vectors,
    );
    assert_strict_preset(STRICT_URL_SAFE_PADDED.settings(), URL_SAFE, vectors);
    assert_strict_preset(
        STRICT_URL_SAFE_UNPADDED.settings(),
        URL_SAFE_NO_PAD,
        vectors,
    );
}

fn assert_strict_preset<A: Alphabet, const PAD: bool>(
    settings: super::specifications::CodecSettings,
    legacy: Engine<A, PAD>,
    vectors: &[(&[u8], &[u8])],
) {
    assert_eq!(settings.alphabet().as_array(), &A::ENCODE);
    assert_eq!(
        settings.encode_padding(),
        if PAD {
            EncodePadding::Padded
        } else {
            EncodePadding::Unpadded
        }
    );
    assert_eq!(settings.trailing_bits(), TrailingBits::RequireCanonical);
    assert!(settings.permits_secret_processing());
    for &(plain, padded) in vectors {
        let expected_len = if PAD {
            padded.len()
        } else {
            padded
                .iter()
                .position(|byte| *byte == b'=')
                .unwrap_or(padded.len())
        };
        let mut encoded = [0u8; 16];
        let written = legacy.encode_slice(plain, &mut encoded).unwrap();
        assert_eq!(written, expected_len);
        let mut decoded = [0u8; 8];
        let decoded_len = legacy
            .decode_slice(&encoded[..written], &mut decoded)
            .unwrap();
        assert_eq!(&decoded[..decoded_len], plain);
    }
}

#[test]
fn builder_accepts_exactly_self_consistent_policy_combinations() {
    let alphabet = ValidatedAlphabet::new(CUSTOM_TABLE).unwrap();
    let mut accepted = 0;
    let mut rejected = 0;
    for encode in [EncodePadding::Padded, EncodePadding::Unpadded] {
        for decode in [
            DecodePadding::RequireCanonical,
            DecodePadding::Forbid,
            DecodePadding::Indifferent,
        ] {
            for trailing in [
                TrailingBits::RequireCanonical,
                TrailingBits::AllowNonCanonical,
            ] {
                let result = CodecBuilder::new(alphabet)
                    .encode_padding(encode)
                    .decode_padding(decode)
                    .trailing_bits(trailing)
                    .build();
                let expected = match (encode, decode) {
                    (EncodePadding::Padded, DecodePadding::Forbid) => {
                        Err(CodecBuilderError::EncodedPaddingRejected)
                    }
                    (EncodePadding::Unpadded, DecodePadding::RequireCanonical) => {
                        Err(CodecBuilderError::EncodedPaddingRequired)
                    }
                    _ => Ok(()),
                };
                assert_eq!(
                    result.as_ref().map(|_| ()).map_err(|error| *error),
                    expected
                );
                if result.is_ok() {
                    accepted += 1;
                } else {
                    rejected += 1;
                }
            }
        }
    }
    assert_eq!((accepted, rejected), (8, 4));
}

#[test]
fn runtime_codec_owns_alphabet_without_allocation() {
    let source = CUSTOM_TABLE;
    let codec = CodecBuilder::from_slice(&source)
        .unwrap()
        .decode_padding(DecodePadding::Indifferent)
        .build()
        .unwrap();
    let settings = codec.settings();

    assert_eq!(settings.alphabet().as_array(), &CUSTOM_TABLE);
    assert_eq!(settings.decode_padding(), DecodePadding::Indifferent);
    assert!(!settings.permits_secret_processing());
    assert_eq!(core::mem::size_of::<ValidatedAlphabet>(), 64);
    assert!(core::mem::size_of::<Base64<RuntimeSpec>>() <= 72);
}

#[test]
fn consumer_trait_is_object_safe_send_and_sync() {
    fn assert_send_sync<T: Send + Sync>() {}
    fn settings_through_object(codec: &dyn Codec) -> super::specifications::CodecSettings {
        codec.settings()
    }

    assert_send_sync::<Base64<RuntimeSpec>>();
    assert_eq!(
        core::mem::size_of_val(STRICT_STANDARD_PADDED.specification()),
        0
    );
    assert_eq!(
        settings_through_object(STRICT_STANDARD_PADDED.specification()),
        STRICT_STANDARD_PADDED.settings()
    );
}

#[test]
fn relaxed_modes_never_qualify_for_secret_processing() {
    let alphabet = ValidatedAlphabet::new(STANDARD_TABLE).unwrap();
    for decode in [
        DecodePadding::RequireCanonical,
        DecodePadding::Forbid,
        DecodePadding::Indifferent,
    ] {
        for trailing in [
            TrailingBits::RequireCanonical,
            TrailingBits::AllowNonCanonical,
        ] {
            let encode = if decode == DecodePadding::Forbid {
                EncodePadding::Unpadded
            } else {
                EncodePadding::Padded
            };
            let settings = CodecBuilder::new(alphabet)
                .encode_padding(encode)
                .decode_padding(decode)
                .trailing_bits(trailing)
                .build()
                .unwrap()
                .settings();
            assert_eq!(
                settings.permits_secret_processing(),
                decode != DecodePadding::Indifferent && trailing == TrailingBits::RequireCanonical
            );
        }
    }
}

#[test]
fn const_builder_produces_an_owned_runtime_codec() {
    const CODEC: Base64<RuntimeSpec> = match CodecBuilder::from_table(CUSTOM_TABLE) {
        Ok(builder) => match builder
            .encode_padding(EncodePadding::Unpadded)
            .decode_padding(DecodePadding::Forbid)
            .build()
        {
            Ok(codec) => codec,
            Err(_) => panic!("valid const policies rejected"),
        },
        Err(_) => panic!("valid const alphabet rejected"),
    };

    assert_eq!(CODEC.settings().alphabet().as_array(), &CUSTOM_TABLE);
}