base_d/core/alternating_dictionary.rs
1//! Alternating word dictionary for PGP-style biometric encoding.
2//!
3//! Provides a dictionary that alternates between multiple sub-dictionaries based on byte position.
4//! This is used for PGP biometric word lists where even and odd bytes use different word sets.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use base_d::{WordDictionary, AlternatingWordDictionary};
10//!
11//! let even = WordDictionary::builder()
12//! .words(vec!["aardvark", "absurd", "accrue", "acme"])
13//! .build()
14//! .unwrap();
15//!
16//! let odd = WordDictionary::builder()
17//! .words(vec!["adroitness", "adviser", "aftermath", "aggregate"])
18//! .build()
19//! .unwrap();
20//!
21//! let alternating = AlternatingWordDictionary::new(
22//! vec![even, odd],
23//! "-".to_string(),
24//! );
25//!
26//! // Byte at position 0 uses even dictionary, position 1 uses odd dictionary, etc.
27//! assert_eq!(alternating.encode_byte(0, 0), Some("aardvark")); // Even position
28//! assert_eq!(alternating.encode_byte(0, 1), Some("adroitness")); // Odd position
29//! ```
30
31use super::word_dictionary::WordDictionary;
32
33/// A word dictionary that alternates between multiple sub-dictionaries.
34///
35/// Used for PGP biometric word lists where even/odd bytes use different words.
36/// Each byte position determines which sub-dictionary is used for encoding/decoding.
37#[derive(Debug, Clone)]
38pub struct AlternatingWordDictionary {
39 dictionaries: Vec<WordDictionary>,
40 delimiter: String,
41}
42
43impl AlternatingWordDictionary {
44 /// Creates a new AlternatingWordDictionary.
45 ///
46 /// # Parameters
47 ///
48 /// - `dictionaries`: Vector of WordDictionary instances to alternate between (must be non-empty)
49 /// - `delimiter`: String to join encoded words (e.g., "-" or " ")
50 ///
51 /// # Panics
52 ///
53 /// Panics if:
54 /// - `dictionaries` is empty
55 /// - Any dictionary does not have exactly 256 words (required for full byte coverage)
56 ///
57 /// # Example
58 ///
59 /// ```
60 /// use base_d::{WordDictionary, AlternatingWordDictionary};
61 ///
62 /// // Create dictionaries with 256 words each for full byte coverage
63 /// let dict1 = WordDictionary::builder()
64 /// .words((0..256).map(|i| format!("even{}", i)).collect::<Vec<_>>())
65 /// .build()
66 /// .unwrap();
67 ///
68 /// let dict2 = WordDictionary::builder()
69 /// .words((0..256).map(|i| format!("odd{}", i)).collect::<Vec<_>>())
70 /// .build()
71 /// .unwrap();
72 ///
73 /// let alternating = AlternatingWordDictionary::new(
74 /// vec![dict1, dict2],
75 /// " ".to_string(),
76 /// );
77 /// ```
78 pub fn new(dictionaries: Vec<WordDictionary>, delimiter: String) -> Self {
79 if dictionaries.is_empty() {
80 panic!("AlternatingWordDictionary requires at least one sub-dictionary");
81 }
82
83 // Validate that all dictionaries have exactly 256 words for full byte coverage
84 for (i, dict) in dictionaries.iter().enumerate() {
85 if dict.base() != 256 {
86 panic!(
87 "Dictionary at index {} has {} words, but exactly 256 words are required for full byte coverage (0-255)",
88 i,
89 dict.base()
90 );
91 }
92 }
93
94 Self {
95 dictionaries,
96 delimiter,
97 }
98 }
99
100 /// Returns which dictionary index to use for a given byte position.
101 ///
102 /// Uses modulo arithmetic to cycle through available dictionaries.
103 ///
104 /// # Example
105 ///
106 /// ```
107 /// # use base_d::{WordDictionary, AlternatingWordDictionary};
108 /// # let dict1 = WordDictionary::builder().words((0..256).map(|i| format!("a{}", i)).collect::<Vec<_>>()).build().unwrap();
109 /// # let dict2 = WordDictionary::builder().words((0..256).map(|i| format!("b{}", i)).collect::<Vec<_>>()).build().unwrap();
110 /// # let alternating = AlternatingWordDictionary::new(vec![dict1, dict2], " ".to_string());
111 /// assert_eq!(alternating.dict_index(0), 0);
112 /// assert_eq!(alternating.dict_index(1), 1);
113 /// assert_eq!(alternating.dict_index(2), 0);
114 /// assert_eq!(alternating.dict_index(3), 1);
115 /// ```
116 pub fn dict_index(&self, byte_position: usize) -> usize {
117 byte_position % self.dictionaries.len()
118 }
119
120 /// Get the dictionary for a given byte position.
121 ///
122 /// # Example
123 ///
124 /// ```
125 /// # use base_d::{WordDictionary, AlternatingWordDictionary};
126 /// # let dict1 = WordDictionary::builder().words((0..256).map(|i| format!("a{}", i)).collect::<Vec<_>>()).build().unwrap();
127 /// # let dict2 = WordDictionary::builder().words((0..256).map(|i| format!("b{}", i)).collect::<Vec<_>>()).build().unwrap();
128 /// # let alternating = AlternatingWordDictionary::new(vec![dict1, dict2], " ".to_string());
129 /// let dict_at_0 = alternating.dict_at(0);
130 /// let dict_at_1 = alternating.dict_at(1);
131 /// // dict_at_0 and dict_at_1 are different dictionaries
132 /// ```
133 pub fn dict_at(&self, position: usize) -> &WordDictionary {
134 &self.dictionaries[self.dict_index(position)]
135 }
136
137 /// Encode a single byte at a given position.
138 ///
139 /// The dictionary used depends on the byte position.
140 ///
141 /// # Parameters
142 ///
143 /// - `byte`: The byte value to encode (0-255)
144 /// - `position`: The position of this byte in the input stream
145 ///
146 /// # Returns
147 ///
148 /// The word corresponding to this byte at this position, or None if the byte value
149 /// exceeds the dictionary size.
150 ///
151 /// # Example
152 ///
153 /// ```
154 /// # use base_d::{WordDictionary, AlternatingWordDictionary};
155 /// # let even = WordDictionary::builder().words((0..256).map(|i| format!("even{}", i)).collect::<Vec<_>>()).build().unwrap();
156 /// # let odd = WordDictionary::builder().words((0..256).map(|i| format!("odd{}", i)).collect::<Vec<_>>()).build().unwrap();
157 /// # let alternating = AlternatingWordDictionary::new(vec![even, odd], " ".to_string());
158 /// assert_eq!(alternating.encode_byte(42, 0), Some("even42")); // Even position
159 /// assert_eq!(alternating.encode_byte(42, 1), Some("odd42")); // Odd position
160 /// ```
161 pub fn encode_byte(&self, byte: u8, position: usize) -> Option<&str> {
162 self.dict_at(position).encode_word(byte as usize)
163 }
164
165 /// Decode a word at a given position back to a byte.
166 ///
167 /// The dictionary used depends on the word position. Case sensitivity
168 /// is determined by the sub-dictionary's case_sensitive setting.
169 ///
170 /// # Parameters
171 ///
172 /// - `word`: The word to decode
173 /// - `position`: The position of this word in the encoded sequence
174 ///
175 /// # Returns
176 ///
177 /// The byte value (0-255) corresponding to this word at this position,
178 /// or None if the word is not found in the appropriate dictionary.
179 ///
180 /// # Example
181 ///
182 /// ```
183 /// # use base_d::{WordDictionary, AlternatingWordDictionary};
184 /// # let even = WordDictionary::builder().words((0..256).map(|i| format!("even{}", i)).collect::<Vec<_>>()).build().unwrap();
185 /// # let odd = WordDictionary::builder().words((0..256).map(|i| format!("odd{}", i)).collect::<Vec<_>>()).build().unwrap();
186 /// # let alternating = AlternatingWordDictionary::new(vec![even, odd], " ".to_string());
187 /// // "even0" is index 0 in even dictionary (position 0)
188 /// assert_eq!(alternating.decode_word("even0", 0), Some(0));
189 /// // "odd1" is index 1 in odd dictionary (position 1)
190 /// assert_eq!(alternating.decode_word("odd1", 1), Some(1));
191 /// ```
192 pub fn decode_word(&self, word: &str, position: usize) -> Option<u8> {
193 self.dict_at(position)
194 .decode_word(word)
195 .map(|idx| idx as u8)
196 }
197
198 /// Returns the delimiter used between encoded words.
199 pub fn delimiter(&self) -> &str {
200 &self.delimiter
201 }
202
203 /// Returns the number of sub-dictionaries.
204 pub fn num_dicts(&self) -> usize {
205 self.dictionaries.len()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn create_test_dictionaries() -> Vec<WordDictionary> {
214 // Create dictionaries with 256 words each (required by AlternatingWordDictionary)
215 let even_words: Vec<String> = (0..256).map(|i| format!("even{}", i)).collect();
216 let odd_words: Vec<String> = (0..256).map(|i| format!("odd{}", i)).collect();
217
218 let even = WordDictionary::builder().words(even_words).build().unwrap();
219
220 let odd = WordDictionary::builder().words(odd_words).build().unwrap();
221
222 vec![even, odd]
223 }
224
225 #[test]
226 fn test_dict_index() {
227 let dicts = create_test_dictionaries();
228 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
229
230 assert_eq!(alternating.dict_index(0), 0);
231 assert_eq!(alternating.dict_index(1), 1);
232 assert_eq!(alternating.dict_index(2), 0);
233 assert_eq!(alternating.dict_index(3), 1);
234 }
235
236 #[test]
237 fn test_encode_byte() {
238 let dicts = create_test_dictionaries();
239 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
240
241 // Position 0 (even) - byte 0
242 assert_eq!(alternating.encode_byte(0, 0), Some("even0"));
243 // Position 1 (odd) - byte 0
244 assert_eq!(alternating.encode_byte(0, 1), Some("odd0"));
245 // Position 2 (even) - byte 1
246 assert_eq!(alternating.encode_byte(1, 2), Some("even1"));
247 // Position 3 (odd) - byte 1
248 assert_eq!(alternating.encode_byte(1, 3), Some("odd1"));
249 }
250
251 #[test]
252 fn test_decode_word() {
253 let dicts = create_test_dictionaries();
254 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
255
256 // Position 0 (even)
257 assert_eq!(alternating.decode_word("even0", 0), Some(0));
258 assert_eq!(alternating.decode_word("even1", 0), Some(1));
259
260 // Position 1 (odd)
261 assert_eq!(alternating.decode_word("odd0", 1), Some(0));
262 assert_eq!(alternating.decode_word("odd1", 1), Some(1));
263 }
264
265 #[test]
266 fn test_decode_word_case_insensitive() {
267 let dicts = create_test_dictionaries();
268 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
269
270 assert_eq!(alternating.decode_word("EVEN0", 0), Some(0));
271 assert_eq!(alternating.decode_word("EvEn0", 0), Some(0));
272 assert_eq!(alternating.decode_word("ODD0", 1), Some(0));
273 }
274
275 #[test]
276 fn test_decode_word_case_sensitive() {
277 // Create 256-word dictionaries with case sensitivity
278 let even_words: Vec<String> = (0..256).map(|i| format!("Even{}", i)).collect();
279 let odd_words: Vec<String> = (0..256).map(|i| format!("Odd{}", i)).collect();
280
281 let even = WordDictionary::builder()
282 .words(even_words)
283 .case_sensitive(true)
284 .build()
285 .unwrap();
286
287 let odd = WordDictionary::builder()
288 .words(odd_words)
289 .case_sensitive(true)
290 .build()
291 .unwrap();
292
293 let alternating = AlternatingWordDictionary::new(vec![even, odd], "-".to_string());
294
295 assert_eq!(alternating.decode_word("Even0", 0), Some(0));
296 assert_eq!(alternating.decode_word("even0", 0), None);
297 assert_eq!(alternating.decode_word("EVEN0", 0), None);
298 }
299
300 #[test]
301 fn test_delimiter() {
302 let dicts = create_test_dictionaries();
303 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
304
305 assert_eq!(alternating.delimiter(), "-");
306 }
307
308 #[test]
309 fn test_num_dicts() {
310 let dicts = create_test_dictionaries();
311 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
312
313 assert_eq!(alternating.num_dicts(), 2);
314 }
315
316 #[test]
317 fn test_encode_byte_all_values() {
318 let dicts = create_test_dictionaries();
319 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
320
321 // Dictionary has 256 words, so all byte values should work
322 assert_eq!(alternating.encode_byte(0, 0), Some("even0"));
323 assert_eq!(alternating.encode_byte(128, 0), Some("even128"));
324 assert_eq!(alternating.encode_byte(255, 0), Some("even255"));
325 }
326
327 #[test]
328 fn test_decode_word_not_found() {
329 let dicts = create_test_dictionaries();
330 let alternating = AlternatingWordDictionary::new(dicts, "-".to_string());
331
332 assert_eq!(alternating.decode_word("unknown", 0), None);
333 assert_eq!(alternating.decode_word("unknown", 1), None);
334 }
335
336 #[test]
337 #[should_panic(expected = "AlternatingWordDictionary requires at least one sub-dictionary")]
338 fn test_empty_dictionaries_panics() {
339 AlternatingWordDictionary::new(vec![], "-".to_string());
340 }
341
342 #[test]
343 #[should_panic(expected = "has 4 words, but exactly 256 words are required")]
344 fn test_undersized_dictionary_panics() {
345 // Create dictionaries with only 4 words each (should panic)
346 let even = WordDictionary::builder()
347 .words(vec!["aardvark", "absurd", "accrue", "acme"])
348 .build()
349 .unwrap();
350
351 let odd = WordDictionary::builder()
352 .words(vec!["adroitness", "adviser", "aftermath", "aggregate"])
353 .build()
354 .unwrap();
355
356 // This should panic because dictionaries don't have 256 words
357 AlternatingWordDictionary::new(vec![even, odd], "-".to_string());
358 }
359
360 #[test]
361 fn test_valid_256_word_dictionaries() {
362 // Create dictionaries with exactly 256 words - should not panic
363 let even_words: Vec<String> = (0..256).map(|i| format!("even{}", i)).collect();
364 let odd_words: Vec<String> = (0..256).map(|i| format!("odd{}", i)).collect();
365
366 let even = WordDictionary::builder().words(even_words).build().unwrap();
367 let odd = WordDictionary::builder().words(odd_words).build().unwrap();
368
369 let alternating = AlternatingWordDictionary::new(vec![even, odd], "-".to_string());
370 assert_eq!(alternating.num_dicts(), 2);
371 }
372}