|
| 1 | +//! The Trifid cipher uses a table to fractionate each plaintext letter into a trigram, |
| 2 | +//! mixes the constituents of the trigrams, and then applies the table in reverse to turn |
| 3 | +//! these mixed trigrams into ciphertext letters. |
| 4 | +//! |
| 5 | +//! [Wikipedia reference](https://en.wikipedia.org/wiki/Trifid_cipher) |
| 6 | +
|
| 7 | +use std::collections::HashMap; |
| 8 | + |
| 9 | +type CharToNum = HashMap<char, String>; |
| 10 | +type NumToChar = HashMap<String, char>; |
| 11 | +type PrepareResult = Result<(String, String, CharToNum, NumToChar), String>; |
| 12 | + |
| 13 | +const TRIGRAM_VALUES: [&str; 27] = [ |
| 14 | + "111", "112", "113", "121", "122", "123", "131", "132", "133", "211", "212", "213", "221", |
| 15 | + "222", "223", "231", "232", "233", "311", "312", "313", "321", "322", "323", "331", "332", |
| 16 | + "333", |
| 17 | +]; |
| 18 | + |
| 19 | +/// Encrypts a message using the Trifid cipher. |
| 20 | +/// |
| 21 | +/// # Arguments |
| 22 | +/// |
| 23 | +/// * `message` - The message to encrypt |
| 24 | +/// * `alphabet` - The characters to be used for the cipher (must be 27 characters) |
| 25 | +/// * `period` - The number of characters in a group whilst encrypting |
| 26 | +pub fn trifid_encrypt(message: &str, alphabet: &str, period: usize) -> Result<String, String> { |
| 27 | + let (message, _alphabet, char_to_num, num_to_char) = prepare(message, alphabet)?; |
| 28 | + |
| 29 | + let mut encrypted_numeric = String::new(); |
| 30 | + let chars: Vec<char> = message.chars().collect(); |
| 31 | + |
| 32 | + for chunk in chars.chunks(period) { |
| 33 | + let chunk_str: String = chunk.iter().collect(); |
| 34 | + encrypted_numeric.push_str(&encrypt_part(&chunk_str, &char_to_num)); |
| 35 | + } |
| 36 | + |
| 37 | + let mut encrypted = String::new(); |
| 38 | + let numeric_chars: Vec<char> = encrypted_numeric.chars().collect(); |
| 39 | + |
| 40 | + for chunk in numeric_chars.chunks(3) { |
| 41 | + let trigram: String = chunk.iter().collect(); |
| 42 | + if let Some(ch) = num_to_char.get(&trigram) { |
| 43 | + encrypted.push(*ch); |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + Ok(encrypted) |
| 48 | +} |
| 49 | + |
| 50 | +/// Decrypts a Trifid cipher encrypted message. |
| 51 | +/// |
| 52 | +/// # Arguments |
| 53 | +/// |
| 54 | +/// * `message` - The message to decrypt |
| 55 | +/// * `alphabet` - The characters used for the cipher (must be 27 characters) |
| 56 | +/// * `period` - The number of characters used in grouping when it was encrypted |
| 57 | +pub fn trifid_decrypt(message: &str, alphabet: &str, period: usize) -> Result<String, String> { |
| 58 | + let (message, _alphabet, char_to_num, num_to_char) = prepare(message, alphabet)?; |
| 59 | + |
| 60 | + let mut decrypted_numeric = Vec::new(); |
| 61 | + let chars: Vec<char> = message.chars().collect(); |
| 62 | + |
| 63 | + for chunk in chars.chunks(period) { |
| 64 | + let chunk_str: String = chunk.iter().collect(); |
| 65 | + let (a, b, c) = decrypt_part(&chunk_str, &char_to_num); |
| 66 | + |
| 67 | + for i in 0..a.len() { |
| 68 | + let trigram = format!( |
| 69 | + "{}{}{}", |
| 70 | + a.chars().nth(i).unwrap(), |
| 71 | + b.chars().nth(i).unwrap(), |
| 72 | + c.chars().nth(i).unwrap() |
| 73 | + ); |
| 74 | + decrypted_numeric.push(trigram); |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + let mut decrypted = String::new(); |
| 79 | + for trigram in decrypted_numeric { |
| 80 | + if let Some(ch) = num_to_char.get(&trigram) { |
| 81 | + decrypted.push(*ch); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + Ok(decrypted) |
| 86 | +} |
| 87 | + |
| 88 | +/// Arranges the trigram value of each letter of message_part vertically and joins |
| 89 | +/// them horizontally. |
| 90 | +fn encrypt_part(message_part: &str, char_to_num: &CharToNum) -> String { |
| 91 | + let mut one = String::new(); |
| 92 | + let mut two = String::new(); |
| 93 | + let mut three = String::new(); |
| 94 | + |
| 95 | + for ch in message_part.chars() { |
| 96 | + if let Some(trigram) = char_to_num.get(&ch) { |
| 97 | + let chars: Vec<char> = trigram.chars().collect(); |
| 98 | + one.push(chars[0]); |
| 99 | + two.push(chars[1]); |
| 100 | + three.push(chars[2]); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + format!("{one}{two}{three}") |
| 105 | +} |
| 106 | + |
| 107 | +/// Converts each letter of the input string into their respective trigram values, |
| 108 | +/// joins them and splits them into three equal groups of strings which are returned. |
| 109 | +fn decrypt_part(message_part: &str, char_to_num: &CharToNum) -> (String, String, String) { |
| 110 | + let mut this_part = String::new(); |
| 111 | + |
| 112 | + for ch in message_part.chars() { |
| 113 | + if let Some(trigram) = char_to_num.get(&ch) { |
| 114 | + this_part.push_str(trigram); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + let part_len = message_part.len(); |
| 119 | + |
| 120 | + if part_len == 0 { |
| 121 | + return (String::new(), String::new(), String::new()); |
| 122 | + } |
| 123 | + |
| 124 | + let chars: Vec<char> = this_part.chars().collect(); |
| 125 | + |
| 126 | + let mut result = Vec::new(); |
| 127 | + for chunk in chars.chunks(part_len) { |
| 128 | + result.push(chunk.iter().collect::<String>()); |
| 129 | + } |
| 130 | + |
| 131 | + // Ensure we have exactly 3 parts, pad with empty strings if necessary |
| 132 | + while result.len() < 3 { |
| 133 | + result.push(String::new()); |
| 134 | + } |
| 135 | + |
| 136 | + (result[0].clone(), result[1].clone(), result[2].clone()) |
| 137 | +} |
| 138 | + |
| 139 | +/// Prepares the message and alphabet for encryption/decryption. |
| 140 | +/// Validates inputs and creates the character-to-number and number-to-character mappings. |
| 141 | +fn prepare(message: &str, alphabet: &str) -> PrepareResult { |
| 142 | + // Remove spaces and convert to uppercase |
| 143 | + let alphabet: String = alphabet.chars().filter(|c| !c.is_whitespace()).collect(); |
| 144 | + let alphabet = alphabet.to_uppercase(); |
| 145 | + let message: String = message.chars().filter(|c| !c.is_whitespace()).collect(); |
| 146 | + let message = message.to_uppercase(); |
| 147 | + |
| 148 | + // Validate alphabet length |
| 149 | + if alphabet.len() != 27 { |
| 150 | + return Err("Length of alphabet has to be 27.".to_string()); |
| 151 | + } |
| 152 | + |
| 153 | + // Validate that all message characters are in the alphabet |
| 154 | + for ch in message.chars() { |
| 155 | + if !alphabet.contains(ch) { |
| 156 | + return Err("Each message character has to be included in alphabet!".to_string()); |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + // Create character-to-number mapping |
| 161 | + let mut char_to_num = HashMap::new(); |
| 162 | + let mut num_to_char = HashMap::new(); |
| 163 | + |
| 164 | + for (i, ch) in alphabet.chars().enumerate() { |
| 165 | + let trigram = TRIGRAM_VALUES[i].to_string(); |
| 166 | + char_to_num.insert(ch, trigram.clone()); |
| 167 | + num_to_char.insert(trigram, ch); |
| 168 | + } |
| 169 | + |
| 170 | + Ok((message, alphabet, char_to_num, num_to_char)) |
| 171 | +} |
| 172 | + |
| 173 | +#[cfg(test)] |
| 174 | +mod tests { |
| 175 | + use super::*; |
| 176 | + |
| 177 | + const DEFAULT_ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ."; |
| 178 | + |
| 179 | + #[test] |
| 180 | + fn test_encrypt_basic() { |
| 181 | + let result = trifid_encrypt("I am a boy", DEFAULT_ALPHABET, 5); |
| 182 | + assert_eq!(result, Ok("BCDGBQY".to_string())); |
| 183 | + } |
| 184 | + |
| 185 | + #[test] |
| 186 | + fn test_encrypt_empty() { |
| 187 | + let result = trifid_encrypt(" ", DEFAULT_ALPHABET, 5); |
| 188 | + assert_eq!(result, Ok("".to_string())); |
| 189 | + } |
| 190 | + |
| 191 | + #[test] |
| 192 | + fn test_encrypt_custom_alphabet() { |
| 193 | + let result = trifid_encrypt( |
| 194 | + "aide toi le c iel ta id era", |
| 195 | + "FELIXMARDSTBCGHJKNOPQUVWYZ+", |
| 196 | + 5, |
| 197 | + ); |
| 198 | + assert_eq!(result, Ok("FMJFVOISSUFTFPUFEQQC".to_string())); |
| 199 | + } |
| 200 | + |
| 201 | + #[test] |
| 202 | + fn test_decrypt_basic() { |
| 203 | + let result = trifid_decrypt("BCDGBQY", DEFAULT_ALPHABET, 5); |
| 204 | + assert_eq!(result, Ok("IAMABOY".to_string())); |
| 205 | + } |
| 206 | + |
| 207 | + #[test] |
| 208 | + fn test_decrypt_custom_alphabet() { |
| 209 | + let result = trifid_decrypt("FMJFVOISSUFTFPUFEQQC", "FELIXMARDSTBCGHJKNOPQUVWYZ+", 5); |
| 210 | + assert_eq!(result, Ok("AIDETOILECIELTAIDERA".to_string())); |
| 211 | + } |
| 212 | + |
| 213 | + #[test] |
| 214 | + fn test_encrypt_decrypt_roundtrip() { |
| 215 | + let msg = "DEFEND THE EAST WALL OF THE CASTLE."; |
| 216 | + let alphabet = "EPSDUCVWYM.ZLKXNBTFGORIJHAQ"; |
| 217 | + let encrypted = trifid_encrypt(msg, alphabet, 5).unwrap(); |
| 218 | + let decrypted = trifid_decrypt(&encrypted, alphabet, 5).unwrap(); |
| 219 | + assert_eq!(decrypted, msg.replace(' ', "")); |
| 220 | + } |
| 221 | + |
| 222 | + #[test] |
| 223 | + fn test_invalid_alphabet_length() { |
| 224 | + let result = trifid_encrypt("test", "ABCDEFGHIJKLMNOPQRSTUVW", 5); |
| 225 | + assert!(result.is_err()); |
| 226 | + assert_eq!(result.unwrap_err(), "Length of alphabet has to be 27."); |
| 227 | + } |
| 228 | + |
| 229 | + #[test] |
| 230 | + fn test_invalid_character_in_message() { |
| 231 | + let result = trifid_encrypt("am i a boy?", "ABCDEFGHIJKLMNOPQRSTUVWXYZ+", 5); |
| 232 | + assert!(result.is_err()); |
| 233 | + assert_eq!( |
| 234 | + result.unwrap_err(), |
| 235 | + "Each message character has to be included in alphabet!" |
| 236 | + ); |
| 237 | + } |
| 238 | + |
| 239 | + #[test] |
| 240 | + fn test_encrypt_part() { |
| 241 | + let mut char_to_num = HashMap::new(); |
| 242 | + char_to_num.insert('A', "111".to_string()); |
| 243 | + char_to_num.insert('S', "311".to_string()); |
| 244 | + char_to_num.insert('K', "212".to_string()); |
| 245 | + |
| 246 | + let result = encrypt_part("ASK", &char_to_num); |
| 247 | + assert_eq!(result, "132111112"); |
| 248 | + } |
| 249 | + |
| 250 | + #[test] |
| 251 | + fn test_decrypt_part() { |
| 252 | + let mut char_to_num = HashMap::new(); |
| 253 | + for (i, ch) in DEFAULT_ALPHABET.chars().enumerate() { |
| 254 | + char_to_num.insert(ch, TRIGRAM_VALUES[i].to_string()); |
| 255 | + } |
| 256 | + |
| 257 | + let (a, b, c) = decrypt_part("ABCDE", &char_to_num); |
| 258 | + assert_eq!(a, "11111"); |
| 259 | + assert_eq!(b, "21131"); |
| 260 | + assert_eq!(c, "21122"); |
| 261 | + } |
| 262 | + |
| 263 | + #[test] |
| 264 | + fn test_decrypt_part_single_char() { |
| 265 | + let mut char_to_num = HashMap::new(); |
| 266 | + char_to_num.insert('A', "111".to_string()); |
| 267 | + |
| 268 | + let (a, b, c) = decrypt_part("A", &char_to_num); |
| 269 | + assert_eq!(a, "1"); |
| 270 | + assert_eq!(b, "1"); |
| 271 | + assert_eq!(c, "1"); |
| 272 | + } |
| 273 | + |
| 274 | + #[test] |
| 275 | + fn test_decrypt_part_empty() { |
| 276 | + let char_to_num = HashMap::new(); |
| 277 | + let (a, b, c) = decrypt_part("", &char_to_num); |
| 278 | + assert_eq!(a, ""); |
| 279 | + assert_eq!(b, ""); |
| 280 | + assert_eq!(c, ""); |
| 281 | + } |
| 282 | + |
| 283 | + #[test] |
| 284 | + fn test_decrypt_part_with_unmapped_chars() { |
| 285 | + let mut char_to_num = HashMap::new(); |
| 286 | + char_to_num.insert('A', "111".to_string()); |
| 287 | + // 'B' and 'C' are not in the mapping, so this_part will only contain A's trigram |
| 288 | + // With message_part length of 3, chunks will be size 3, giving us one chunk "111" |
| 289 | + // The padding logic will add two empty strings |
| 290 | + let (a, b, c) = decrypt_part("ABC", &char_to_num); |
| 291 | + assert_eq!(a, "111"); |
| 292 | + assert_eq!(b, ""); |
| 293 | + assert_eq!(c, ""); |
| 294 | + } |
| 295 | +} |
0 commit comments