base64-const 0.0.6

Base64 encode/decode with const-friendly decode and URL-safe macros
Documentation
use std::str;
pub mod url_safe_util;
pub mod macros;

const BITS_DIFF: usize = 8 - 6;
const PADDING_B4: u8 = 0x4d;


pub fn encode(bytes: &[u8]) -> String {
    let bytes: &[u8] = &encode_bytes(bytes);
    str::from_utf8(bytes).unwrap().to_string()
}

pub fn encode_bytes(bytes: &[u8]) -> Box<[u8]> {
    let result_length = encoded_length_for(bytes);

    let mut result = Vec::with_capacity(result_length);
    for index in 0..result_length {
        let start_bit = index * 6;
        let start_byte = start_bit >> 3;

        let start_bit = (start_bit % 8) as u8;
        let b64 = if start_byte >= bytes.len() {
            PADDING_B4
        } else if start_bit <= BITS_DIFF as u8 {
            bytes[start_byte] >> (BITS_DIFF as u8 - start_bit) & 0b0011_1111
        } else {
            let mut result = (bytes[start_byte] & (0xff >> start_bit)) << start_bit - BITS_DIFF as u8;
            if start_byte + 1 < bytes.len() {
                result |= bytes[start_byte + 1] >> 10 - start_bit;
            }
            result
        };

        result.push(b64_to_ascii(b64));
    }

    result.into_boxed_slice()
}

pub fn decode(bytes: &[u8]) -> Box<[u8]> {
    let potential_length = (bytes.len() * 6) >> 3;
    let mut start_bit: u8 = 0;
    let mut byte: u8 = 0;
    let mut result: Vec<u8> = Vec::with_capacity(potential_length);

    for index in 0..bytes.len() {
        let ascii = bytes[index];

        if ascii == b'=' {
            break;
        }

        let b64 = ascii_to_b64(ascii);

        if start_bit <= BITS_DIFF as u8 {
            byte |= b64 << (BITS_DIFF as u8 - start_bit);
            if start_bit == BITS_DIFF as u8 {
                result.push(byte);
                byte = 0;
            }
        } else {
            byte |= b64 >> start_bit - BITS_DIFF as u8;
            result.push(byte);
            byte = b64 << (8 - start_bit + BITS_DIFF as u8);
        }
        start_bit = (start_bit + 6) % 8;
    }

    result.into_boxed_slice()
}

const fn b64_to_ascii(b64: u8) -> u8 {
    if b64 < 26 {
        b'A' + b64
    } else if b64 < 52 {
        b'a' + b64 - 26
    } else if b64 < 62 {
        b'0' + b64 - 52
    } else if b64 == 62 {
        b'+'
    } else if b64 == 63 {
        b'/'
    } else {
        b'='
    }
}

const fn ascii_to_b64(ascii: u8) -> u8 {
    if ascii >= b'a' {
        26 + ascii - b'a'
    } else if ascii >= b'A' {
        ascii - b'A'
    } else if ascii >= b'0' {
        52 + ascii - b'0'
    } else if ascii == b'+' {
        62
    } else if ascii == b'/' {
        63
    } else {
        0xff
    }
}

pub fn encoded_length_for(bytes: &[u8]) -> usize {
    let bits_length = bytes.len() << 3;

    let mut result_length = bits_length / 6;
    if bits_length % 6 > 0 {
        result_length += (8 - (bits_length % 6)) / BITS_DIFF;
    }
    result_length
}


#[cfg(test)]
mod tests {
    use std::str::from_utf8;

    use super::*;

    #[test]
    fn test_result_length() {
        assert_eq!(encoded_length_for(b"a"), 4);
        assert_eq!(encoded_length_for(b"ab"), 4);
        assert_eq!(encoded_length_for(b"abc"), 4);
        assert_eq!(encoded_length_for(b"abcd"), 8);
        assert_eq!(encoded_length_for(b"abcde"), 8);
        assert_eq!(encoded_length_for(b"abcdef"), 8);
    }

    #[test]
    fn test_encode() {
        let base64 = encode("Ma".as_bytes());
        assert_eq!(base64, "TWE=");

        let base64 = encode("Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.".as_bytes());
        assert_eq!(base64, "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=");

        assert_eq!(
            "XC5WGfrtRruAKSncMk+InwNcbkIX/v+jyz3YAqKMwiCtv9O5BbqsvlSDErnoEoyJCljGAWvqOHLLgP1BxxuZsg==",
            encode([
                0x5c, 0x2e, 0x56, 0x19, 0xfa, 0xed, 0x46, 0xbb, 0x80, 0x29, 0x29, 0xdc, 0x32, 0x4f, 0x88, 0x9f, 0x03, 0x5c, 
                0x6e, 0x42, 0x17, 0xfe, 0xff, 0xa3, 0xcb, 0x3d, 0xd8, 0x02, 0xa2, 0x8c, 0xc2, 0x20, 0xad, 0xbf, 0xd3, 0xb9, 
                0x05, 0xba, 0xac, 0xbe, 0x54, 0x83, 0x12, 0xb9, 0xe8, 0x12, 0x8c, 0x89, 0x0a, 0x58, 0xc6, 0x01, 0x6b, 0xea, 
                0x38, 0x72, 0xcb, 0x80, 0xfd, 0x41, 0xc7, 0x1b, 0x99, 0xb2
            ].as_slice())
        )
    }

    #[test]
    fn test_b64_to_ascii() {
        assert_eq!(b64_to_ascii(0), b'A');
        assert_eq!(b64_to_ascii(9), b'J');
        assert_eq!(b64_to_ascii(40), b'o');
        assert_eq!(b64_to_ascii(51), b'z');
        assert_eq!(b64_to_ascii(52), b'0');
        assert_eq!(b64_to_ascii(61), b'9');
        assert_eq!(b64_to_ascii(62), b'+');
        assert_eq!(b64_to_ascii(63), b'/');
        assert_eq!(b64_to_ascii(PADDING_B4), b'=');
    }

    #[test]
    fn test_ascii_to_b64() {
        assert_eq!(ascii_to_b64(b'A'), 0);
        assert_eq!(ascii_to_b64(b'J'), 9);
        assert_eq!(ascii_to_b64(b'o'), 40);
        assert_eq!(ascii_to_b64(b'z'), 51);
        assert_eq!(ascii_to_b64(b'0'), 52);
        assert_eq!(ascii_to_b64(b'9'), 61);
        assert_eq!(ascii_to_b64(b'+'), 62);
        assert_eq!(ascii_to_b64(b'/'), 63);
        //assert_eq!(ascii_to_b64(b'='), PADDING_B4);
    }

    #[test]
    fn test_decode() {
        let message = decode("TWE=".as_bytes());
        assert_eq!(message.as_ref(), b"Ma");

        let message = base64_decode!("TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=");
        assert_eq!(from_utf8(&message).unwrap(), "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.");

        const DECODED: &[u8] = &base64_decode!("TWE=");
        assert_eq!(DECODED, b"Ma");
    
        const LONG_DECODED: &[u8] = &base64_decode!("TWFu");
        assert_eq!(LONG_DECODED, b"Man");
        assert_eq!(
            decode(b"XC5WGfrtRruAKSncMk+InwNcbkIX/v+jyz3YAqKMwiCtv9O5BbqsvlSDErnoEoyJCljGAWvqOHLLgP1BxxuZsg==").as_ref(),
            [
                0x5c, 0x2e, 0x56, 0x19, 0xfa, 0xed, 0x46, 0xbb, 0x80, 0x29, 0x29, 0xdc, 0x32, 0x4f, 0x88, 0x9f, 0x03, 0x5c, 
                0x6e, 0x42, 0x17, 0xfe, 0xff, 0xa3, 0xcb, 0x3d, 0xd8, 0x02, 0xa2, 0x8c, 0xc2, 0x20, 0xad, 0xbf, 0xd3, 0xb9, 
                0x05, 0xba, 0xac, 0xbe, 0x54, 0x83, 0x12, 0xb9, 0xe8, 0x12, 0x8c, 0x89, 0x0a, 0x58, 0xc6, 0x01, 0x6b, 0xea, 
                0x38, 0x72, 0xcb, 0x80, 0xfd, 0x41, 0xc7, 0x1b, 0x99, 0xb2
            ].as_slice()
        )
    }
}