ender-eye 1.0.0

Encrypt and decrypt messages using SGA encoding + AES-256-GCM, inspired by the Standard Galactic Alphabet from Minecraft.
Documentation
use crate::error::ValidationErrors;

const SGA_TABLE: [(char, &str); 26] = [
    ('a', ""),
    ('b', "ʖ"),
    ('c', "ϟ"),
    ('d', ""),
    ('e', ""),
    ('f', ""),
    ('g', ""),
    ('h', ""),
    ('i', ""),
    ('j', ""),
    ('k', ""),
    ('l', ""),
    ('m', ""),
    ('n', ""),
    ('o', "𝙹"),
    ('p', ""),
    ('q', ""),
    ('r', ""),
    ('s', ""),
    ('t', "ʇ"),
    ('u', ""),
    ('v', ""),
    ('w', ""),
    ('x', ""),
    ('y', ""),
    ('z', ""),
];

pub fn encode(text: &str) -> String {
    text.chars()
        .map(|c| {
            let lower = c.to_ascii_lowercase();
            match SGA_TABLE.iter().find(|(latin, _)| *latin == lower) {
                Some((_, char)) => char.to_string(),
                None => c.to_string(),
            }
        })
        .collect::<String>()
}

pub fn decode(text: &str) -> Result<String, ValidationErrors> {
    if text.is_empty() {
        return Err(ValidationErrors::EmptyCharacters);
    }

    let mut result = String::new();
    for c in text.chars() {
        match SGA_TABLE
            .iter()
            .find(|(_, symbol)| *symbol == c.to_string().as_str())
        {
            Some((latin, _)) => result.push_str(&latin.to_string()),
            None => {
                if c.is_ascii_alphanumeric() || c.is_ascii_punctuation() || c.is_ascii_whitespace()
                {
                    result.push(c);
                } else {
                    return Err(ValidationErrors::NonAllowedCharacters);
                }
            }
        }
    }
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn encode_to_correct_char() {
        assert_eq!(encode("a"), "");
    }

    #[test]
    fn non_encode_to_same_string() {
        assert_ne!(encode("hi"), "hi");
    }

    #[test]
    fn encode_preserves_additional_char() {
        let result = encode("Hello World 123");
        assert!(result.contains(' '));
        assert!(result.contains('1'));
    }

    #[test]
    fn decode_to_correct_symbol() {
        assert_eq!(decode("").unwrap(), "a")
    }

    #[test]
    fn non_decode_to_same_string() {
        assert_ne!(decode("ʖᔑʖᔑ").unwrap(), "ʖᔑʖᔑ");
    }

    #[test]
    fn decode_preserves_whole_string() {
        let phrase: String = "hello world".to_string();

        let encode_result = encode(&phrase);

        assert_eq!(decode(&encode_result).unwrap(), phrase);
    }
}