diff --git a/DIRECTORY.md b/DIRECTORY.md index c123cf19d26..5bbc6c752c8 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -59,6 +59,7 @@ * [Tea](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/tea.rs) * [Theoretical ROT13](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/theoretical_rot13.rs) * [Transposition](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/transposition.rs) + * [Trifid](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/trifid.rs) * [Vernam](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/vernam.rs) * [Vigenere](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/vigenere.rs) * [XOR](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/xor.rs) diff --git a/src/ciphers/mod.rs b/src/ciphers/mod.rs index 1cd823f16e7..89bf353bc81 100644 --- a/src/ciphers/mod.rs +++ b/src/ciphers/mod.rs @@ -23,6 +23,7 @@ mod sha3; mod tea; mod theoretical_rot13; mod transposition; +mod trifid; mod vernam; mod vigenere; mod xor; @@ -55,6 +56,7 @@ pub use self::sha3::{sha3_224, sha3_256, sha3_384, sha3_512}; pub use self::tea::{tea_decrypt, tea_encrypt}; pub use self::theoretical_rot13::theoretical_rot13; pub use self::transposition::transposition; +pub use self::trifid::{trifid_decrypt, trifid_encrypt}; pub use self::vernam::{vernam_decrypt, vernam_encrypt}; pub use self::vigenere::vigenere; pub use self::xor::xor; diff --git a/src/ciphers/trifid.rs b/src/ciphers/trifid.rs new file mode 100644 index 00000000000..59b0d6014be --- /dev/null +++ b/src/ciphers/trifid.rs @@ -0,0 +1,295 @@ +//! The Trifid cipher uses a table to fractionate each plaintext letter into a trigram, +//! mixes the constituents of the trigrams, and then applies the table in reverse to turn +//! these mixed trigrams into ciphertext letters. +//! +//! [Wikipedia reference](https://en.wikipedia.org/wiki/Trifid_cipher) + +use std::collections::HashMap; + +type CharToNum = HashMap; +type NumToChar = HashMap; +type PrepareResult = Result<(String, String, CharToNum, NumToChar), String>; + +const TRIGRAM_VALUES: [&str; 27] = [ + "111", "112", "113", "121", "122", "123", "131", "132", "133", "211", "212", "213", "221", + "222", "223", "231", "232", "233", "311", "312", "313", "321", "322", "323", "331", "332", + "333", +]; + +/// Encrypts a message using the Trifid cipher. +/// +/// # Arguments +/// +/// * `message` - The message to encrypt +/// * `alphabet` - The characters to be used for the cipher (must be 27 characters) +/// * `period` - The number of characters in a group whilst encrypting +pub fn trifid_encrypt(message: &str, alphabet: &str, period: usize) -> Result { + let (message, _alphabet, char_to_num, num_to_char) = prepare(message, alphabet)?; + + let mut encrypted_numeric = String::new(); + let chars: Vec = message.chars().collect(); + + for chunk in chars.chunks(period) { + let chunk_str: String = chunk.iter().collect(); + encrypted_numeric.push_str(&encrypt_part(&chunk_str, &char_to_num)); + } + + let mut encrypted = String::new(); + let numeric_chars: Vec = encrypted_numeric.chars().collect(); + + for chunk in numeric_chars.chunks(3) { + let trigram: String = chunk.iter().collect(); + if let Some(ch) = num_to_char.get(&trigram) { + encrypted.push(*ch); + } + } + + Ok(encrypted) +} + +/// Decrypts a Trifid cipher encrypted message. +/// +/// # Arguments +/// +/// * `message` - The message to decrypt +/// * `alphabet` - The characters used for the cipher (must be 27 characters) +/// * `period` - The number of characters used in grouping when it was encrypted +pub fn trifid_decrypt(message: &str, alphabet: &str, period: usize) -> Result { + let (message, _alphabet, char_to_num, num_to_char) = prepare(message, alphabet)?; + + let mut decrypted_numeric = Vec::new(); + let chars: Vec = message.chars().collect(); + + for chunk in chars.chunks(period) { + let chunk_str: String = chunk.iter().collect(); + let (a, b, c) = decrypt_part(&chunk_str, &char_to_num); + + for i in 0..a.len() { + let trigram = format!( + "{}{}{}", + a.chars().nth(i).unwrap(), + b.chars().nth(i).unwrap(), + c.chars().nth(i).unwrap() + ); + decrypted_numeric.push(trigram); + } + } + + let mut decrypted = String::new(); + for trigram in decrypted_numeric { + if let Some(ch) = num_to_char.get(&trigram) { + decrypted.push(*ch); + } + } + + Ok(decrypted) +} + +/// Arranges the trigram value of each letter of message_part vertically and joins +/// them horizontally. +fn encrypt_part(message_part: &str, char_to_num: &CharToNum) -> String { + let mut one = String::new(); + let mut two = String::new(); + let mut three = String::new(); + + for ch in message_part.chars() { + if let Some(trigram) = char_to_num.get(&ch) { + let chars: Vec = trigram.chars().collect(); + one.push(chars[0]); + two.push(chars[1]); + three.push(chars[2]); + } + } + + format!("{one}{two}{three}") +} + +/// Converts each letter of the input string into their respective trigram values, +/// joins them and splits them into three equal groups of strings which are returned. +fn decrypt_part(message_part: &str, char_to_num: &CharToNum) -> (String, String, String) { + let mut this_part = String::new(); + + for ch in message_part.chars() { + if let Some(trigram) = char_to_num.get(&ch) { + this_part.push_str(trigram); + } + } + + let part_len = message_part.len(); + + if part_len == 0 { + return (String::new(), String::new(), String::new()); + } + + let chars: Vec = this_part.chars().collect(); + + let mut result = Vec::new(); + for chunk in chars.chunks(part_len) { + result.push(chunk.iter().collect::()); + } + + // Ensure we have exactly 3 parts, pad with empty strings if necessary + while result.len() < 3 { + result.push(String::new()); + } + + (result[0].clone(), result[1].clone(), result[2].clone()) +} + +/// Prepares the message and alphabet for encryption/decryption. +/// Validates inputs and creates the character-to-number and number-to-character mappings. +fn prepare(message: &str, alphabet: &str) -> PrepareResult { + // Remove spaces and convert to uppercase + let alphabet: String = alphabet.chars().filter(|c| !c.is_whitespace()).collect(); + let alphabet = alphabet.to_uppercase(); + let message: String = message.chars().filter(|c| !c.is_whitespace()).collect(); + let message = message.to_uppercase(); + + // Validate alphabet length + if alphabet.len() != 27 { + return Err("Length of alphabet has to be 27.".to_string()); + } + + // Validate that all message characters are in the alphabet + for ch in message.chars() { + if !alphabet.contains(ch) { + return Err("Each message character has to be included in alphabet!".to_string()); + } + } + + // Create character-to-number mapping + let mut char_to_num = HashMap::new(); + let mut num_to_char = HashMap::new(); + + for (i, ch) in alphabet.chars().enumerate() { + let trigram = TRIGRAM_VALUES[i].to_string(); + char_to_num.insert(ch, trigram.clone()); + num_to_char.insert(trigram, ch); + } + + Ok((message, alphabet, char_to_num, num_to_char)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const DEFAULT_ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ."; + + #[test] + fn test_encrypt_basic() { + let result = trifid_encrypt("I am a boy", DEFAULT_ALPHABET, 5); + assert_eq!(result, Ok("BCDGBQY".to_string())); + } + + #[test] + fn test_encrypt_empty() { + let result = trifid_encrypt(" ", DEFAULT_ALPHABET, 5); + assert_eq!(result, Ok("".to_string())); + } + + #[test] + fn test_encrypt_custom_alphabet() { + let result = trifid_encrypt( + "aide toi le c iel ta id era", + "FELIXMARDSTBCGHJKNOPQUVWYZ+", + 5, + ); + assert_eq!(result, Ok("FMJFVOISSUFTFPUFEQQC".to_string())); + } + + #[test] + fn test_decrypt_basic() { + let result = trifid_decrypt("BCDGBQY", DEFAULT_ALPHABET, 5); + assert_eq!(result, Ok("IAMABOY".to_string())); + } + + #[test] + fn test_decrypt_custom_alphabet() { + let result = trifid_decrypt("FMJFVOISSUFTFPUFEQQC", "FELIXMARDSTBCGHJKNOPQUVWYZ+", 5); + assert_eq!(result, Ok("AIDETOILECIELTAIDERA".to_string())); + } + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let msg = "DEFEND THE EAST WALL OF THE CASTLE."; + let alphabet = "EPSDUCVWYM.ZLKXNBTFGORIJHAQ"; + let encrypted = trifid_encrypt(msg, alphabet, 5).unwrap(); + let decrypted = trifid_decrypt(&encrypted, alphabet, 5).unwrap(); + assert_eq!(decrypted, msg.replace(' ', "")); + } + + #[test] + fn test_invalid_alphabet_length() { + let result = trifid_encrypt("test", "ABCDEFGHIJKLMNOPQRSTUVW", 5); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Length of alphabet has to be 27."); + } + + #[test] + fn test_invalid_character_in_message() { + let result = trifid_encrypt("am i a boy?", "ABCDEFGHIJKLMNOPQRSTUVWXYZ+", 5); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + "Each message character has to be included in alphabet!" + ); + } + + #[test] + fn test_encrypt_part() { + let mut char_to_num = HashMap::new(); + char_to_num.insert('A', "111".to_string()); + char_to_num.insert('S', "311".to_string()); + char_to_num.insert('K', "212".to_string()); + + let result = encrypt_part("ASK", &char_to_num); + assert_eq!(result, "132111112"); + } + + #[test] + fn test_decrypt_part() { + let mut char_to_num = HashMap::new(); + for (i, ch) in DEFAULT_ALPHABET.chars().enumerate() { + char_to_num.insert(ch, TRIGRAM_VALUES[i].to_string()); + } + + let (a, b, c) = decrypt_part("ABCDE", &char_to_num); + assert_eq!(a, "11111"); + assert_eq!(b, "21131"); + assert_eq!(c, "21122"); + } + + #[test] + fn test_decrypt_part_single_char() { + let mut char_to_num = HashMap::new(); + char_to_num.insert('A', "111".to_string()); + + let (a, b, c) = decrypt_part("A", &char_to_num); + assert_eq!(a, "1"); + assert_eq!(b, "1"); + assert_eq!(c, "1"); + } + + #[test] + fn test_decrypt_part_empty() { + let char_to_num = HashMap::new(); + let (a, b, c) = decrypt_part("", &char_to_num); + assert_eq!(a, ""); + assert_eq!(b, ""); + assert_eq!(c, ""); + } + + #[test] + fn test_decrypt_part_with_unmapped_chars() { + let mut char_to_num = HashMap::new(); + char_to_num.insert('A', "111".to_string()); + // 'B' and 'C' are not in the mapping, so this_part will only contain A's trigram + // With message_part length of 3, chunks will be size 3, giving us one chunk "111" + // The padding logic will add two empty strings + let (a, b, c) = decrypt_part("ABC", &char_to_num); + assert_eq!(a, "111"); + assert_eq!(b, ""); + assert_eq!(c, ""); + } +}