geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::super::schema::{BitArrayError, BitArrayOk, BitArrayResult};
use super::BitArrayProvider;
use crate::gleam_stdlib::GleamStdlibHostProfile;
use crate::{BitArrayValue, HostCall, HostCallCompletion, HostCallError};
use base64::Engine;
use base64::alphabet;
use base64::engine::general_purpose::{
    GeneralPurpose, GeneralPurposeConfig, STANDARD, STANDARD_NO_PAD,
};
use ecow::EcoString;

const ERLANG_BASE64: GeneralPurpose = GeneralPurpose::new(
    &alphabet::STANDARD,
    GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
);

pub(in crate::gleam_stdlib::bit_array) fn base64_encode(
    value: BitArrayValue,
    padding: bool,
) -> EcoString {
    let value = value.pad_to_bytes();
    if padding {
        STANDARD.encode(value.bytes()).into()
    } else {
        STANDARD_NO_PAD.encode(value.bytes()).into()
    }
}

pub(in crate::gleam_stdlib::bit_array) fn decode64<'call, Profile>(
    call: HostCall<'call, Profile, BitArrayProvider<Profile>, BitArrayResult>,
    value: EcoString,
) -> Result<HostCallCompletion<'call, BitArrayResult>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    Ok(match decode_base64(&value) {
        Ok(bytes) => call.return_custom::<BitArrayOk>((BitArrayValue::from_bytes(bytes), ())),
        Err(_) => call.return_custom::<BitArrayError>(((), ())),
    })
}

pub(in crate::gleam_stdlib::bit_array) fn base16_encode(value: BitArrayValue) -> EcoString {
    hex::encode_upper(value.pad_to_bytes().bytes()).into()
}

pub(in crate::gleam_stdlib::bit_array) fn base16_decode<'call, Profile>(
    call: HostCall<'call, Profile, BitArrayProvider<Profile>, BitArrayResult>,
    value: EcoString,
) -> Result<HostCallCompletion<'call, BitArrayResult>, HostCallError>
where
    Profile: GleamStdlibHostProfile,
{
    Ok(match hex::decode(value.as_bytes()) {
        Ok(bytes) => call.return_custom::<BitArrayOk>((BitArrayValue::from_bytes(bytes), ())),
        Err(_) => call.return_custom::<BitArrayError>(((), ())),
    })
}

fn decode_base64(value: &str) -> Result<Vec<u8>, base64::DecodeError> {
    let encoded = value
        .bytes()
        .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
        .collect::<Vec<_>>();
    ERLANG_BASE64.decode(encoded)
}

#[cfg(test)]
mod tests {
    use super::{base16_encode, base64_encode, decode_base64};
    use crate::BitArrayValue;

    #[test]
    fn matches_erlang_base64_whitespace_and_trailing_bit_acceptance() {
        assert_eq!(decode_base64("aG  \t\nVsbG8="), Ok(b"hello".to_vec()));
        assert_eq!(decode_base64("AB=="), Ok(vec![0]));
        for invalid in [
            "=", "A===", "AAAA====", "AA=A", "AA==junk", "A", "AA", "AAA",
        ] {
            assert!(decode_base64(invalid).is_err(), "{invalid:?}");
        }
        assert!(decode_base64("aG\u{000c}VsbG8=").is_err());
        assert!(decode_base64("aG\u{000b}VsbG8=").is_err());
    }

    #[test]
    fn zero_pads_unaligned_values_for_encoding() {
        let value = BitArrayValue::try_from_parts(vec![0b1010_0000], 3)
            .expect("three supplied bits should be valid");

        assert_eq!(base64_encode(value.clone(), true), "oA==");
        assert_eq!(base64_encode(value.clone(), false), "oA");
        assert_eq!(base16_encode(value), "A0");
    }
}