base64-ng 2.0.1

no_std-first Base64 encoding and decoding with strict RFC 4648 APIs and optional SIMD
Documentation
use super::{
    Failure, InputError, OneShotError, OperationError, STRICT_STANDARD_PADDED, Status,
    legacy::ASCII_WHITESPACE,
};

const IGNORED: [u8; 4] = *b" \t\r\n";

#[test]
fn exactly_four_ascii_bytes_are_ignored_at_every_position() {
    for byte in 0u8..=127 {
        for position in 0..=4 {
            let mut input = [0u8; 5];
            input[..position].copy_from_slice(&b"Zg=="[..position]);
            input[position] = byte;
            input[position + 1..].copy_from_slice(&b"Zg=="[position..]);

            let mut actual = [0xa5; 4];
            let actual_result =
                ASCII_WHITESPACE.decode_into(&STRICT_STANDARD_PADDED, &input, &mut actual);
            if IGNORED.contains(&byte) {
                assert_eq!(actual_result, Ok(1), "byte={byte:#04x} position={position}");
                assert_eq!(actual[0], b'f');
            } else {
                let mut expected = [0xa5; 4];
                let expected_result = STRICT_STANDARD_PADDED.decode_into(&input, &mut expected);
                assert_eq!(
                    actual_result, expected_result,
                    "byte={byte:#04x} position={position}"
                );
                assert_eq!(actual, expected, "byte={byte:#04x} position={position}");
            }
        }
    }
}

#[test]
fn whitespace_only_chunks_advance_original_positions() {
    let mut decoder = ASCII_WHITESPACE.decoder(&STRICT_STANDARD_PADDED);
    let first = decoder.update(b" \t", &mut []).unwrap();
    assert_eq!(first.progress().input_consumed(), 2);
    assert_eq!(first.status(), Status::NeedInput);

    let second = decoder.update(b"\r\nZg", &mut []).unwrap();
    assert_eq!(second.progress().input_consumed(), 4);
    let error = decoder.update(b"!", &mut []).unwrap_err();
    assert_eq!(
        error,
        OperationError::Failed(Failure::Input(InputError::InvalidByte {
            index: 6,
            byte: b'!'
        }))
    );
    assert_eq!(decoder.update(b"==", &mut []).unwrap_err(), error);
}

#[test]
fn incremental_chunks_preserve_output_and_terminal_whitespace() {
    let mut decoder = ASCII_WHITESPACE.decoder(&STRICT_STANDARD_PADDED);
    let mut output = [0u8; 3];
    assert_eq!(
        decoder.update(b"Z \n", &mut output[..0]).unwrap().status(),
        Status::NeedInput
    );
    let second = decoder.update(b"g=\t=\r\n", &mut output[..1]).unwrap();
    assert_eq!(second.progress().input_consumed(), 6);
    assert_eq!(second.progress().output_produced(), 1);
    assert_eq!(output[0], b'f');
    assert_eq!(
        decoder.finish(&mut output[1..]).unwrap().status(),
        Status::Complete
    );
}

#[test]
fn one_shot_validation_is_transactional_and_reports_original_indexes() {
    let input = b" \tZg\r!\n==";
    let mut output = [0xa5; 8];
    assert_eq!(
        ASCII_WHITESPACE.decode_into(&STRICT_STANDARD_PADDED, input, &mut output),
        Err(OneShotError::Input(InputError::InvalidByte {
            index: 5,
            byte: b'!'
        }))
    );
    assert_eq!(output, [0xa5; 8]);

    for rejected in *b"\x0b\x0c" {
        let input = [b'Z', b'g', rejected, b'=', b'='];
        assert!(
            ASCII_WHITESPACE
                .validate(&STRICT_STANDARD_PADDED, &input)
                .is_err()
        );
    }
}

#[test]
fn source_position_overflow_is_absorbing_before_whitespace_compaction() {
    let mut decoder = ASCII_WHITESPACE.decoder(&STRICT_STANDARD_PADDED);
    decoder.set_source_position_for_test(usize::MAX - 1);
    let error = decoder.update(b" \t", &mut []).unwrap_err();
    assert_eq!(error, OperationError::Failed(Failure::PositionOverflow));
    assert_eq!(decoder.update(b"Zg==", &mut []).unwrap_err(), error);
}