1use itertools::Itertools as _;
2
3pub struct Base64;
4
5const CHARACTERS: &[u8] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes();
6
7impl Base64 {
8 pub fn encrypt(plaintext: &str) -> String {
9 plaintext
10 .replace("\r", "")
11 .chars()
12 .chunks(3)
13 .into_iter()
14 .map(|triplet| {
15 let mut quadruplet = triplet
16 .map(|character| format!("{:08b}", character as u8))
17 .join("")
18 .chars()
19 .chunks(6)
20 .into_iter()
21 .map(|chunk| {
22 let mut string = chunk.collect::<String>();
23 while string.len() < 6 {
24 string = string + "0";
25 }
26 (*CHARACTERS.get(usize::from_str_radix(&string, 2).unwrap()).unwrap() as char).to_string()
27 })
28 .collect::<String>();
29 while quadruplet.len() % 4 != 0 {
30 quadruplet += "=";
31 }
32 quadruplet
33 })
34 .collect()
35 }
36
37 pub fn decrypt(ciphertext: &str) -> String {
38 ciphertext
39 .chars()
40 .filter(|character| !character.is_whitespace())
41 .chunks(4)
42 .into_iter()
43 .map(|quadruplet| {
44 quadruplet
45 .map(|character| {
46 if character == '=' {
47 "2".to_owned()
48 } else {
49 format!("{:06b}", CHARACTERS.iter().position(|other| *other as char == character).unwrap())
50 }
51 })
52 .join("")
53 .chars()
54 .chunks(8)
55 .into_iter()
56 .filter_map(|chunk| {
57 let string = chunk.collect::<String>().trim_end_matches("2").to_owned();
58 (string.len() == 8).then_some(u8::from_str_radix(&string, 2).unwrap() as char)
59 })
60 .collect::<String>()
61 })
62 .collect()
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use crate::Base64;
69
70 #[test]
71 fn encrypt_decrypt() {
72 let letter = include_str!("../tests/letter.txt").trim().replace("\r", "");
73 let encrypted_letter = include_str!("../tests/encrypted_letter.txt").trim().replace("\r", "");
74
75 let ciphertext = Base64::encrypt(&letter);
76 let plaintext = Base64::decrypt(&encrypted_letter);
77
78 assert_eq!(letter, plaintext);
79 assert_eq!(encrypted_letter, ciphertext);
80 }
81}