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
111pub 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
128pub 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
145pub 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
162pub 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
179pub 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}