seed_encoder/
lib.rs

1use std::{collections::HashMap, num::ParseIntError};
2
3use once_cell::sync::Lazy;
4use substring::Substring;
5use thiserror::Error;
6
7const ENGLISH: &str = include_str!("../words/english.txt");
8
9static ENGLISH_NUM: Lazy<HashMap<String, u16>> = Lazy::new(|| {
10    let mut num_map = HashMap::new();
11
12    for (i, word) in ENGLISH.split_ascii_whitespace().enumerate() {
13        num_map.insert(word.to_owned(), i as u16 + 1);
14    }
15
16    num_map
17});
18
19static NUM_ENGLISH: Lazy<HashMap<u16, String>> = Lazy::new(|| {
20    let mut num_map = HashMap::new();
21
22    for (i, word) in ENGLISH.split_ascii_whitespace().enumerate() {
23        num_map.insert(i as u16 + 1, word.to_owned());
24    }
25
26    num_map
27});
28
29static ENGLISH_ALPHA: Lazy<HashMap<String, String>> = Lazy::new(|| {
30    let mut alpha_map = HashMap::new();
31
32    for word in ENGLISH.split_ascii_whitespace() {
33        alpha_map.insert(word.to_owned(), word.substring(0, 4).to_owned());
34    }
35
36    alpha_map
37});
38
39static ALPHA_ENGLISH: Lazy<HashMap<String, String>> = Lazy::new(|| {
40    let mut alpha_map = HashMap::new();
41
42    for word in ENGLISH.split_ascii_whitespace() {
43        alpha_map.insert(word.substring(0, 4).to_owned(), word.to_owned());
44    }
45
46    alpha_map
47});
48
49pub enum Plate {
50    Alpha,
51    Num,
52    Unknown,
53}
54
55pub fn detect(input: &str) -> Plate {
56    let words = input.split(' ');
57    
58    if !matches!(words.clone().count(), 12 | 18 | 24) {
59        return Plate::Unknown;
60    }
61    
62    let mut alphas = 0;
63    let mut nums = 0;
64    
65    for word in words.clone() {
66            match word.parse::<u16>() {
67                Ok(result) => {
68                    if result <= 2048 {
69                        nums += 1;
70                    } else {
71                        break;
72                    }
73                }
74                Err(_) => {
75                    break;
76                }
77            };
78        }
79
80    if !matches!(nums, 12 | 18 | 24) {
81        for word in words.clone() {
82            if word.len() <= 4 {
83                alphas += 1;
84            } else {
85                break;
86            }
87        } 
88    }
89
90    if matches!(alphas, 12 | 18 | 24) {
91        Plate::Alpha
92    } else if matches!(nums, 12 | 18 | 24) {
93        Plate::Num
94    } else {
95        Plate::Unknown
96    }
97}
98
99#[derive(Error, Debug)]
100pub enum Error {
101    #[error("Word not found in BIP-39 wordlist when encoding words")]
102    EncodeWordNotFound,
103    #[error("Number not between 1-2048, which is the size of the BIP-39 wordlist")]
104    DecodeNumNotFound,
105    #[error("First 4 letters not found in BIP-39 wordlist when encoding words")]
106    DecodeAlphaNotFound,
107    #[error(transparent)]
108    ParseIntError(#[from] ParseIntError),
109}
110
111/// Take words from word list and return numbers of those words
112pub fn encode_num(words: &str) -> Result<String, Error> {
113    let mut nums = vec![];
114
115    for word in words.split_ascii_whitespace() {
116        let result = ENGLISH_NUM.get(word);
117
118        if let Some(num) = result {
119            nums.push(num.to_string());
120        } else {
121            return Err(Error::EncodeWordNotFound);
122        }
123    }
124
125    Ok(nums.join(" "))
126}
127
128/// Take words from word list and return first 4 letters of those words
129pub fn encode_alpha(words: &str) -> Result<String, Error> {
130    let mut alphas = vec![];
131
132    for word in words.split_ascii_whitespace() {
133        let result = ENGLISH_ALPHA.get(word);
134
135        if let Some(alpha) = result {
136            alphas.push(alpha.to_string());
137        } else {
138            return Err(Error::EncodeWordNotFound);
139        }
140    }
141
142    Ok(alphas.join(" "))
143}
144
145/// Take number of word from word list and return full word
146pub fn decode_num(nums: &str) -> Result<String, Error> {
147    let mut words = vec![];
148
149    for num in nums.split_ascii_whitespace() {
150        let result = NUM_ENGLISH.get(&num.parse::<u16>()?);
151
152        if let Some(word) = result {
153            words.push(word.to_owned());
154        } else {
155            return Err(Error::DecodeNumNotFound);
156        }
157    }
158
159    Ok(words.join(" "))
160}
161
162/// Take first 4 letters from word and return full word
163pub fn decode_alpha(alphas: &str) -> Result<String, Error> {
164    let mut words = vec![];
165
166    for alpha in alphas.split_ascii_whitespace() {
167        let result = ALPHA_ENGLISH.get(alpha);
168
169        if let Some(word) = result {
170            words.push(word.to_string());
171        } else {
172            return Err(Error::DecodeAlphaNotFound);
173        }
174    }
175
176    Ok(words.join(" "))
177}
178
179/// Automatic decode method
180pub fn decode(input: &str) -> Result<String, Error> {
181    match detect(input) {
182        Plate::Alpha => decode_alpha(input),
183        Plate::Num => decode_num(input),
184        Plate::Unknown => Ok(input.to_owned()),
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    const WORDS: &str = "evidence gate beef bright sample lounge flower culture strategy begin thought thumb start ask river olive joy pause purchase absorb mad jacket error elevator";
193
194    const ALPHAS: &str = "evid gate beef brig samp loun flow cult stra begi thou thum star ask rive oliv joy paus purc abso mad jack erro elev";
195
196    const NUMS: &str = "623 771 161 225 1529 1059 717 429 1719 163 1800 1804 1702 107 1495 1234 965 1292 1394 7 1070 953 615 576";
197
198    #[test]
199    fn encodes_nums() {
200        let result = encode_num(WORDS).unwrap();
201        assert_eq!(result, "623 771 161 225 1529 1059 717 429 1719 163 1800 1804 1702 107 1495 1234 965 1292 1394 7 1070 953 615 576");
202    }
203
204    #[test]
205    fn encodes_alphas() {
206        let result = encode_alpha(WORDS).unwrap();
207        assert_eq!(result, "evid gate beef brig samp loun flow cult stra begi thou thum star ask rive oliv joy paus purc abso mad jack erro elev");
208    }
209
210    #[test]
211    fn decodes_nums() {
212        let result = decode_num(NUMS).unwrap();
213        assert_eq!(result, "evidence gate beef bright sample lounge flower culture strategy begin thought thumb start ask river olive joy pause purchase absorb mad jacket error elevator");
214    }
215
216    #[test]
217    fn decodes_alphas() {
218        let result = decode_alpha(ALPHAS).unwrap();
219        assert_eq!(result, "evidence gate beef bright sample lounge flower culture strategy begin thought thumb start ask river olive joy pause purchase absorb mad jacket error elevator");
220    }
221
222    #[test]
223    fn decodes() {
224        let result = decode(ALPHAS).unwrap();
225        assert_eq!(result, "evidence gate beef bright sample lounge flower culture strategy begin thought thumb start ask river olive joy pause purchase absorb mad jacket error elevator");
226        let result = decode(NUMS).unwrap();
227        assert_eq!(result, "evidence gate beef bright sample lounge flower culture strategy begin thought thumb start ask river olive joy pause purchase absorb mad jacket error elevator");
228        let result = decode(WORDS).unwrap();
229        assert_eq!(result, "evidence gate beef bright sample lounge flower culture strategy begin thought thumb start ask river olive joy pause purchase absorb mad jacket error elevator");
230    }
231}