Skip to content

Commit 8ef48a0

Browse files
feat: add Trifid cipher implementation (#1009)
1 parent fb5784f commit 8ef48a0

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
* [Tea](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/tea.rs)
6060
* [Theoretical ROT13](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/theoretical_rot13.rs)
6161
* [Transposition](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/transposition.rs)
62+
* [Trifid](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/trifid.rs)
6263
* [Vernam](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/vernam.rs)
6364
* [Vigenere](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/vigenere.rs)
6465
* [XOR](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/xor.rs)

src/ciphers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod sha3;
2323
mod tea;
2424
mod theoretical_rot13;
2525
mod transposition;
26+
mod trifid;
2627
mod vernam;
2728
mod vigenere;
2829
mod xor;
@@ -55,6 +56,7 @@ pub use self::sha3::{sha3_224, sha3_256, sha3_384, sha3_512};
5556
pub use self::tea::{tea_decrypt, tea_encrypt};
5657
pub use self::theoretical_rot13::theoretical_rot13;
5758
pub use self::transposition::transposition;
59+
pub use self::trifid::{trifid_decrypt, trifid_encrypt};
5860
pub use self::vernam::{vernam_decrypt, vernam_encrypt};
5961
pub use self::vigenere::vigenere;
6062
pub use self::xor::xor;

src/ciphers/trifid.rs

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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

Comments
 (0)