qubit-codec-misc 0.8.4

Miscellaneous byte and text format codecs for Rust applications
Documentation
// =============================================================================
//    Copyright (c) 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Tests for Base64 encoding variants.

use std::error::Error;

use base64::DecodeError;
use qubit_codec::ValueDecoder;
use qubit_codec::ValueEncoder;
use qubit_codec_misc::Base64Codec;
use qubit_codec_misc::Base64ErrorKind;
use qubit_codec_misc::MiscCodecError;

type Base64Mode = (&'static str, fn() -> Base64Codec, bool, bool);
type Base64PaddingCase = (&'static str, fn() -> Base64Codec, &'static str);

#[test]
fn test_standard_base64_roundtrip_with_padding() {
    let codec = Base64Codec::standard();
    let encoded = codec.encode(b"hello world");

    assert_eq!("aGVsbG8gd29ybGQ=", encoded);
    assert_eq!(
        b"hello world".to_vec(),
        codec.decode(&encoded).expect("base64 should decode")
    );
}

#[test]
fn test_url_safe_base64_roundtrip_without_padding() {
    let codec = Base64Codec::url_safe_no_pad();
    let bytes = [0xfb, 0xff, 0xee, 0x00];
    let encoded = codec.encode(&bytes);

    assert_eq!("-__uAA", encoded);
    assert_eq!(
        bytes.to_vec(),
        codec.decode(&encoded).expect("url-safe base64 should decode")
    );
}

#[test]
fn test_base64_constructors_cover_padding_and_alphabet_variants() {
    assert_eq!("aGk", Base64Codec::standard_no_pad().encode(b"hi"));
    assert_eq!("++8=", Base64Codec::standard().encode(&[0xfb, 0xef]));
    assert_eq!("--8=", Base64Codec::url_safe().encode(&[0xfb, 0xef]));
    assert_eq!(
        b"hi".to_vec(),
        Base64Codec::standard_no_pad()
            .decode("aGk")
            .expect("standard no-pad base64 should decode")
    );
    assert_eq!(
        vec![0xfb, 0xef],
        Base64Codec::url_safe()
            .decode("--8=")
            .expect("url-safe padded base64 should decode")
    );
    assert_eq!(
        b"hi".to_vec(),
        Base64Codec::default()
            .decode("aGk=")
            .expect("default base64 should decode")
    );
}

#[test]
fn test_base64_facades_accept_only_their_padding_and_alphabet_variants() {
    let modes: [Base64Mode; 4] = [
        ("standard padded", Base64Codec::standard, true, true),
        ("standard unpadded", Base64Codec::standard_no_pad, false, true),
        ("URL-safe padded", Base64Codec::url_safe, true, false),
        ("URL-safe unpadded", Base64Codec::url_safe_no_pad, false, false),
    ];

    for (name, factory, has_padding, is_standard) in modes {
        let codec = factory();
        for (input, expected) in [("", b"".as_slice()), ("TWFu", b"Man".as_slice())] {
            assert_eq!(
                expected,
                codec
                    .decode(input)
                    .unwrap_or_else(|error| { panic!("{name} should accept {input:?}: {error}") }),
                "{name} decoding {input:?}"
            );
        }

        for (padded, unpadded, expected) in [("Zg==", "Zg", b"f".as_slice()), ("Zm8=", "Zm8", b"fo".as_slice())] {
            let accepted = if has_padding { padded } else { unpadded };
            let rejected = if has_padding { unpadded } else { padded };
            assert_eq!(
                expected,
                codec
                    .decode(accepted)
                    .unwrap_or_else(|error| { panic!("{name} should accept {accepted:?}: {error}") }),
                "{name} decoding {accepted:?}"
            );
            assert!(
                codec.decode(rejected).is_err(),
                "{name} should reject padding mismatch {rejected:?}"
            );
        }

        let (own_padded, own_unpadded, foreign_padded, foreign_unpadded) = if is_standard {
            ("+/8=", "+/8", "-_8=", "-_8")
        } else {
            ("-_8=", "-_8", "+/8=", "+/8")
        };
        let own = if has_padding { own_padded } else { own_unpadded };
        let foreign = if has_padding { foreign_padded } else { foreign_unpadded };
        assert_eq!(
            vec![0xfb, 0xff],
            codec
                .decode(own)
                .unwrap_or_else(|error| panic!("{name} should accept {own:?}: {error}")),
            "{name} decoding its alphabet"
        );
        assert!(
            codec.decode(foreign).is_err(),
            "{name} should reject the foreign alphabet {foreign:?}"
        );

        let malformed = ["Zg===", "Z=g=", "Zg=", "Z g==", "Zg==\n", "Zh==", "Zm9=", "Zh", "Zm9"];
        for input in malformed {
            assert!(
                codec.decode(input).is_err(),
                "{name} should reject malformed input {input:?}"
            );
        }
    }
}

#[test]
fn test_base64_facades_preserve_invalid_padding_source() {
    let mismatched_padding: [Base64PaddingCase; 4] = [
        ("standard padded", Base64Codec::standard, "Zg"),
        ("standard unpadded", Base64Codec::standard_no_pad, "Zg=="),
        ("URL-safe padded", Base64Codec::url_safe, "Zg"),
        ("URL-safe unpadded", Base64Codec::url_safe_no_pad, "Zg=="),
    ];

    for (name, factory, input) in mismatched_padding {
        let error = factory()
            .decode(input)
            .expect_err(&format!("{name} should reject {input:?}"));
        assert!(matches!(
            &error,
            MiscCodecError::InvalidBase64 {
                kind: Base64ErrorKind::InvalidPadding,
                input_index: None,
                symbol: None,
                input_length: None,
                ..
            }
        ));
        let source = Error::source(&error).expect("invalid padding should retain its dependency source");
        assert_eq!(
            Some(&DecodeError::InvalidPadding),
            source.downcast_ref::<DecodeError>(),
            "{name} source error"
        );
    }
}

#[test]
fn test_base64_codec_equality_distinguishes_engine_variants() {
    assert_eq!(Base64Codec::standard(), Base64Codec::default());
    assert_eq!(Base64Codec::url_safe_no_pad(), Base64Codec::url_safe_no_pad());
    assert_ne!(Base64Codec::standard(), Base64Codec::standard_no_pad());
    assert_ne!(Base64Codec::standard(), Base64Codec::url_safe());
}

#[test]
fn test_base64_mode_identity_is_stable() {
    let factories: [fn() -> Base64Codec; 4] = [
        Base64Codec::standard,
        Base64Codec::standard_no_pad,
        Base64Codec::url_safe,
        Base64Codec::url_safe_no_pad,
    ];
    for (left_index, left_factory) in factories.iter().enumerate() {
        let left = left_factory();
        let copied = left;
        assert_eq!(left, copied);
        assert_eq!(left, left_factory());
        for (right_index, right_factory) in factories.iter().enumerate() {
            assert_eq!(left_index == right_index, left == right_factory());
        }
    }
    assert_eq!(Base64Codec::default(), Base64Codec::standard());
}

#[test]
fn test_decode_rejects_invalid_base64() {
    let error = Base64Codec::standard()
        .decode("@")
        .expect_err("invalid base64 should fail");

    assert!(matches!(
        error,
        MiscCodecError::InvalidBase64 {
            kind: Base64ErrorKind::InvalidByte,
            input_index: Some(0),
            symbol: Some(b'@'),
            input_length: None,
            ..
        }
    ));
    let source = Error::source(&error).expect("Base64 error should retain its dependency source");
    assert_eq!(
        Some(&DecodeError::InvalidByte(0, b'@')),
        source.downcast_ref::<DecodeError>()
    );
}

#[test]
fn test_decode_reports_base64_error_category_and_index() {
    let invalid_last_symbol = Base64Codec::standard()
        .decode("YR==")
        .expect_err("non-canonical Base64 should fail");
    assert!(matches!(
        invalid_last_symbol,
        MiscCodecError::InvalidBase64 {
            kind: Base64ErrorKind::InvalidLastSymbol,
            input_index: Some(1),
            symbol: Some(b'R'),
            input_length: None,
            ..
        }
    ));

    let invalid_length = Base64Codec::standard()
        .decode("Y")
        .expect_err("short Base64 should fail");
    assert!(matches!(
        invalid_length,
        MiscCodecError::InvalidBase64 {
            kind: Base64ErrorKind::InvalidLength,
            input_index: None,
            symbol: None,
            input_length: Some(1),
            ..
        }
    ));
}

#[test]
fn test_base64_codec_can_be_used_through_traits() {
    let mut codec = Base64Codec::standard();
    let encoded = ValueEncoder::<[u8]>::encode(&mut codec, b"abc").expect("base64 encode should succeed");
    let decoded = ValueDecoder::<str>::decode(&mut codec, &encoded).expect("base64 decode should succeed");

    assert_eq!("YWJj", encoded);
    assert_eq!(b"abc".to_vec(), decoded);
}