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}