sub_solver/
solve.rs

1use base64::{prelude::BASE64_STANDARD, Engine};
2use std::{
3    collections::{HashMap, HashSet},
4    fmt::Display,
5    sync::mpsc,
6};
7
8use crate::Word;
9
10fn intersect(a: HashSet<char>, b: HashSet<char>) -> HashSet<char> {
11    let mut result = HashSet::new();
12
13    for i in a {
14        if b.contains(&i) {
15            result.insert(i);
16        }
17    }
18
19    result
20}
21
22/// Remove certain words from the candidates that are not possible
23pub fn prune(cipher_words: &mut [Word]) {
24    // Initialize with all possible letters
25    let mut pruner: HashMap<char, HashSet<char>> = HashMap::new();
26    for i in 'a'..='z' {
27        for j in 'a'..='z' {
28            pruner.entry(i).or_default().insert(j);
29        }
30    }
31
32    // Remove letters that are not possible
33    for word in cipher_words.iter() {
34        for j in word.word.chars() {
35            pruner.entry(j).and_modify(|e| {
36                *e = intersect(e.clone(), word.letter_map.get(&j).unwrap().clone())
37            });
38        }
39    }
40
41    // Remove candidates that are not possible
42    for word in cipher_words.iter_mut() {
43        for j in 0..word.word.len() {
44            word.candidates.retain(|k| {
45                pruner
46                    .get(&word.word.chars().nth(j).unwrap())
47                    .unwrap()
48                    .contains(&k.chars().nth(j).unwrap())
49            });
50        }
51    }
52}
53
54pub fn order_by_possible_words(cipher_words: &mut [Word]) {
55    cipher_words.sort_by(|a, b| {
56        let a = a.word.len();
57        let b = b.word.len();
58
59        b.cmp(&a)
60    });
61}
62
63fn is_consistent(map: &HashMap<char, char>) -> bool {
64    let mut counter: HashMap<char, char> = HashMap::new();
65
66    for (&first, &second) in map.iter() {
67        counter.insert(second, first);
68    }
69
70    map.len() == counter.len()
71}
72
73fn apply_map(cipher: &str, plain: &str, map: &HashMap<char, char>) -> String {
74    let mut result = String::new();
75
76    for (i, c) in cipher.chars().enumerate() {
77        let c = *map.get(&c).unwrap_or(&plain.chars().nth(i).unwrap());
78        result.push(c);
79    }
80
81    result
82}
83fn update_map(cipher: &str, plain: &str, map: &HashMap<char, char>) -> HashMap<char, char> {
84    let mut map = map.to_owned();
85
86    for (i, c) in cipher.chars().enumerate() {
87        map.entry(c)
88            .or_insert_with(|| plain.chars().nth(i).unwrap());
89    }
90
91    map
92}
93
94pub struct Solver {
95    pub cipher_words: Vec<Word>,
96}
97impl Solver {
98    pub fn new(cipher_words: &Vec<Word>) -> Self {
99        Solver {
100            cipher_words: cipher_words.to_owned(),
101        }
102    }
103
104    pub fn solve(
105        &mut self,
106        mut starting_key: HashMap<char, char>,
107        tx: Option<&mpsc::Sender<Solution>>,
108    ) {
109        self.solve_recursive(0, &mut starting_key, tx);
110    }
111
112    fn solve_recursive(
113        &mut self,
114        depth: usize,
115        map: &mut HashMap<char, char>,
116        tx: Option<&mpsc::Sender<Solution>>,
117    ) {
118        if is_consistent(map) {
119            if depth >= self.cipher_words.len() {
120                // Solution found
121                let solution = Solution::new(map.to_owned());
122                if let Some(tx) = tx {
123                    tx.send(solution).unwrap();
124                }
125            } else {
126                // Explore all candidates
127                for i in self.cipher_words[depth].candidates.to_owned().iter() {
128                    if &apply_map(&self.cipher_words[depth].word, i, map) == i {
129                        self.solve_recursive(
130                            depth + 1,
131                            &mut update_map(&self.cipher_words[depth].word, i, map),
132                            tx,
133                        );
134                    }
135                }
136            }
137        }
138    }
139}
140
141pub struct Solution {
142    pub key: HashMap<char, char>,
143}
144impl Solution {
145    pub fn new(key: HashMap<char, char>) -> Self {
146        Solution { key }
147    }
148
149    /// Fill unknowns in the key with unused letters
150    pub fn fill_key(&mut self) {
151        let mut unused: Vec<char> = ('a'..='z').rev().collect();
152        // Filter used letters
153        for value in self.key.values() {
154            unused.retain(|x| x != value);
155        }
156
157        // Fill unknown letters with unused letters
158        for c in 'a'..='z' {
159            self.key.entry(c).or_insert_with(|| unused.pop().unwrap());
160        }
161    }
162
163    pub fn apply(&self, ciphertext: &str) -> String {
164        let mut result = String::new();
165
166        for c in ciphertext.chars() {
167            // Show unknown letters as '?', but keep punctuation
168            result.push(*self.key.get(&c).unwrap_or_else(|| {
169                if c.is_alphabetic() {
170                    &'?'
171                } else {
172                    &c
173                }
174            }));
175        }
176
177        result
178    }
179
180    pub fn format_hyperlink(&self, ciphertext: &str) -> String {
181        let ciphertext = BASE64_STANDARD.encode(ciphertext);
182        let url = format!("https://gchq.github.io/CyberChef/#recipe=Substitute('ABCDEFGHIJKLMNOPQRSTUVWXYZ','{self}',true)&input={ciphertext}");
183        format!("\x1b]8;;{url}\x1b\\{self}\x1b]8;;\x1b\\")
184    }
185}
186impl Display for Solution {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        ('a'..='z')
189            .map(|c| *self.key.get(&c).unwrap_or(&'?'))
190            .collect::<String>()
191            .fmt(f)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use crate::{input::input_to_words, load_wordlist};
198
199    use super::*;
200
201    #[test]
202    fn can_solve() {
203        let ciphertext = "x cbt tloap";
204        let wordlist = [
205            "many", "words", "here", "to", "test", "the", "solver", "also", "few", "a", "ok",
206            "now", "all", "words", "should", "be", "good",
207        ]
208        .join("\n");
209        let dictionary = load_wordlist(&wordlist);
210
211        let cipher_words = input_to_words(ciphertext, &dictionary).unwrap();
212        let mut solver = Solver::new(&cipher_words);
213
214        let (tx, rx) = mpsc::channel();
215        solver.solve(HashMap::new(), Some(&tx));
216
217        let solution = rx.recv().unwrap();
218        let plaintext = solution.apply(ciphertext);
219        assert_eq!(plaintext, "a few words");
220    }
221}