base64-ng 2.0.1

no_std-first Base64 encoding and decoding with strict RFC 4648 APIs and optional SIMD
Documentation
use core::num::NonZeroUsize;

use super::contracts::{
    AssuranceClass, Atomicity, BackendClass, BackendFault, Failure, InputError, InputErrorKind,
    Lifecycle, OperationError, Progress, ProtocolScope, Status, TerminalError,
};

struct StackText {
    bytes: [u8; 128],
    len: usize,
}

impl StackText {
    const fn new() -> Self {
        Self {
            bytes: [0; 128],
            len: 0,
        }
    }

    fn as_str(&self) -> &str {
        core::str::from_utf8(&self.bytes[..self.len]).unwrap()
    }
}

impl core::fmt::Write for StackText {
    fn write_str(&mut self, text: &str) -> core::fmt::Result {
        let Some(end) = self.len.checked_add(text.len()) else {
            return Err(core::fmt::Error);
        };
        let Some(destination) = self.bytes.get_mut(self.len..end) else {
            return Err(core::fmt::Error);
        };
        destination.copy_from_slice(text.as_bytes());
        self.len = end;
        Ok(())
    }
}

#[test]
fn output_full_is_retryable_and_finish_is_idempotent() {
    let mut state = Lifecycle::new();
    let span = state.reserve_input(3).unwrap();
    assert_eq!(span.index(0), Some(0));
    assert_eq!(span.index(2), Some(2));
    assert_eq!(span.index(3), None);
    state.commit_input(span, 2).unwrap();

    let full = state
        .output_full(Progress::new(2, 0), NonZeroUsize::new(4).unwrap())
        .unwrap();
    assert_eq!(full.progress().input_consumed(), 2);
    assert_eq!(full.progress().output_produced(), 0);
    let Status::OutputFull(requirement) = full.status() else {
        panic!("expected output-full status");
    };
    assert_eq!(requirement.minimum_output().get(), 4);

    let retry = state.reserve_input(1).unwrap();
    assert_eq!(retry.index(0), Some(2));
    state.commit_input(retry, 1).unwrap();
    assert_eq!(
        state.need_input(Progress::new(1, 4)).unwrap().status(),
        Status::NeedInput
    );
    assert_eq!(
        state.finish(Progress::new(0, 2)).unwrap().status(),
        Status::Complete
    );
    assert_eq!(
        state.finish(Progress::new(9, 9)).unwrap().progress(),
        Progress::ZERO
    );
    assert_eq!(
        state.reserve_input(1),
        Err(OperationError::Terminal(TerminalError::InputAfterComplete))
    );

    state.reset();
    assert_eq!(state.source_position(), 0);
    assert_eq!(
        state.need_input(Progress::ZERO).unwrap().status(),
        Status::NeedInput
    );
}

#[test]
fn beginning_finish_closes_input_while_output_remains_retryable() {
    let mut state = Lifecycle::new();
    let span = state.reserve_input(2).unwrap();
    state.commit_input(span, 2).unwrap();
    assert!(!state.begin_finish().unwrap());
    assert!(!state.begin_finish().unwrap());
    assert_eq!(
        state.reserve_input(0),
        Err(OperationError::Terminal(TerminalError::InputAfterFinish))
    );
    assert_eq!(
        state
            .output_full(Progress::ZERO, NonZeroUsize::MIN)
            .unwrap()
            .status(),
        Status::OutputFull(super::contracts::OutputFull::new(NonZeroUsize::MIN))
    );
    assert_eq!(
        state.finish(Progress::ZERO).unwrap().status(),
        Status::Complete
    );
    assert!(state.begin_finish().unwrap());
}

#[test]
fn malformed_and_backend_failures_are_absorbing_until_reset() {
    let malformed = Failure::Input(InputError::InvalidByte {
        index: 7,
        byte: b'!',
    });
    let mut state = Lifecycle::new();
    assert_eq!(state.fail(malformed), OperationError::Failed(malformed));
    assert_eq!(
        state.need_input(Progress::ZERO),
        Err(OperationError::Failed(malformed))
    );
    assert_eq!(
        state.finish(Progress::ZERO),
        Err(OperationError::Failed(malformed))
    );
    assert_eq!(
        state.fail(Failure::Backend(BackendFault::ImpossibleState)),
        OperationError::Failed(malformed)
    );

    state.reset();
    let backend = Failure::Backend(BackendFault::OutputMismatch);
    assert_eq!(state.fail(backend), OperationError::Failed(backend));
    assert_eq!(backend.as_str(), "backend-output-mismatch");
}

#[test]
fn truncation_is_a_distinct_absorbing_input_failure() {
    let mut state = Lifecycle::new();
    let span = state.reserve_input(3).unwrap();
    state.commit_input(span, 3).unwrap();
    let truncated = Failure::Input(InputError::TruncatedInput {
        index: state.source_position(),
    });
    assert_eq!(truncated.as_str(), "truncated-input");
    assert_eq!(state.fail(truncated), OperationError::Failed(truncated));
    assert_eq!(
        state.output_full(Progress::ZERO, NonZeroUsize::MIN),
        Err(OperationError::Failed(truncated))
    );
}

#[test]
fn compacted_and_whitespace_only_chunks_keep_original_indexes() {
    let chunks: &[&[u8]] = &[b" \t", b"Z \n", b"g", b"=="];
    let mut state = Lifecycle::new();
    let mut compacted = [0_u8; 4];
    let mut indexes = [0_usize; 4];
    let mut compacted_len = 0;

    for chunk in chunks {
        let span = state.reserve_input(chunk.len()).unwrap();
        for (local, byte) in chunk.iter().copied().enumerate() {
            if !matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
                compacted[compacted_len] = byte;
                indexes[compacted_len] = span.index(local).unwrap();
                compacted_len += 1;
            }
        }
        state.commit_input(span, chunk.len()).unwrap();
    }

    assert_eq!(compacted_len, compacted.len());
    assert_eq!(compacted, *b"Zg==");
    assert_eq!(indexes, [2, 5, 6, 7]);
    assert_eq!(state.source_position(), 8);
}

#[test]
fn source_position_overflow_is_checked_before_processing_and_absorbing() {
    let mut state = Lifecycle::at_source_position(usize::MAX - 3);
    let span = state.reserve_input(3).unwrap();
    assert_eq!(span.index(0), Some(usize::MAX - 3));
    assert_eq!(span.index(2), Some(usize::MAX - 1));
    assert_eq!(state.source_position(), usize::MAX - 3);
    state.commit_input(span, 3).unwrap();
    assert_eq!(state.source_position(), usize::MAX);

    assert_eq!(
        state.reserve_input(1),
        Err(OperationError::Failed(Failure::PositionOverflow))
    );
    assert_eq!(state.source_position(), usize::MAX);
    assert_eq!(
        state.reserve_input(0),
        Err(OperationError::Failed(Failure::PositionOverflow))
    );
    assert_eq!(
        state.finish(Progress::ZERO),
        Err(OperationError::Failed(Failure::PositionOverflow))
    );
}

#[test]
fn invalid_internal_source_commits_fail_closed_without_panicking() {
    let mut oversized = Lifecycle::new();
    let span = oversized.reserve_input(2).unwrap();
    assert_eq!(
        oversized.commit_input(span, 3),
        Err(OperationError::Failed(Failure::Backend(
            BackendFault::ImpossibleState
        )))
    );

    let mut stale = Lifecycle::new();
    let span = stale.reserve_input(2).unwrap();
    stale.commit_input(span, 1).unwrap();
    assert_eq!(
        stale.commit_input(span, 1),
        Err(OperationError::Failed(Failure::Backend(
            BackendFault::ImpossibleState
        )))
    );
}

#[test]
fn detailed_input_debug_is_redacted_and_kind_is_stable() {
    let error = InputError::InvalidByte {
        index: 123_456,
        byte: 0xfe,
    };
    let mut debug = StackText::new();
    let mut display = StackText::new();
    core::fmt::write(&mut debug, format_args!("{error:?}")).unwrap();
    core::fmt::write(&mut display, format_args!("{error}")).unwrap();
    let debug = debug.as_str();
    let display = display.as_str();

    assert_eq!(error.kind(), InputErrorKind::InvalidByte);
    assert_eq!(error.kind().as_str(), "invalid-byte");
    assert!(!debug.contains("123456"));
    assert!(!debug.contains("fe"));
    assert!(display.contains("123456"));
    assert!(display.contains("fe"));
}

#[test]
fn reporting_and_atomicity_identifiers_are_stable() {
    assert_eq!(Status::NeedInput.as_str(), "need-input");
    assert_eq!(BackendClass::Scalar.as_str(), "scalar");
    assert_eq!(BackendClass::Accelerated.as_str(), "accelerated");
    assert_eq!(BackendClass::Checked.as_str(), "checked");
    assert_eq!(AssuranceClass::Ordinary.as_str(), "ordinary");
    assert_eq!(AssuranceClass::CheckedBackend.as_str(), "checked-backend");
    assert_eq!(AssuranceClass::Secret.as_str(), "secret");
    assert_eq!(AssuranceClass::HighAssurance.as_str(), "high-assurance");
    assert_eq!(ProtocolScope::Core.as_str(), "core");
    assert_eq!(ProtocolScope::Compatibility.as_str(), "compatibility");
    assert_eq!(ProtocolScope::Companion.as_str(), "companion");
    assert_eq!(Atomicity::Unchanged.as_str(), "unchanged");
    assert_eq!(Atomicity::Rollback.as_str(), "rollback");
    assert_eq!(Atomicity::CommittedPrefix.as_str(), "committed-prefix");
    assert_eq!(Atomicity::IrrevocableSink.as_str(), "irrevocable-sink");
}