Skip to main content

base24/
lib.rs

1pub mod errors;
2
3use errors::Base24Error;
4use std::collections::BTreeMap;
5
6type Result<T> = std::result::Result<T, Base24Error>;
7
8const ALPHABET: &str = "ZAC2B3EF4GH5TK67P8RS9WXY";
9const ALPHABET_LENGTH: usize = ALPHABET.len();
10
11struct Base24 {
12    encode_map: BTreeMap<usize, char>,
13    decode_map: BTreeMap<char, usize>,
14}
15
16impl Base24 {
17    pub fn new() -> Base24 {
18        Base24 {
19            encode_map: ALPHABET.char_indices().collect(),
20            decode_map: ALPHABET
21                .char_indices()
22                .map(|(idx, kar)| (kar, idx))
23                .chain(
24                    ALPHABET
25                        .to_lowercase()
26                        .char_indices()
27                        .map(|(idx, kar)| (kar, idx)),
28                )
29                .collect(),
30        }
31    }
32
33    pub fn encode(&self, data: &[u8]) -> Result<String> {
34        if data.len() % 4 != 0 {
35            return Err(Base24Error::EncodeInputLengthInvalid);
36        }
37
38        let res = data
39            .chunks(4)
40            .map(|chunk| u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
41            .map(|mut value| {
42                (0..7)
43                    .into_iter()
44                    .map(|_| {
45                        let idx: usize = value as usize % ALPHABET_LENGTH;
46                        value = value / ALPHABET_LENGTH as u32;
47
48                        self.encode_map[&idx].clone()
49                    })
50                    .collect::<Vec<char>>()
51                    .iter()
52                    .rev()
53                    .collect::<String>()
54            })
55            .collect();
56
57        Ok(res)
58    }
59
60    pub fn decode(&self, data: &str) -> Result<Vec<u8>> {
61        let char_vec: Vec<char> = data.chars().collect();
62
63        if char_vec.len() % 7 != 0 {
64            return Err(Base24Error::DecodeInputLengthInvalid);
65        }
66
67        // Pessimistically check whether the input contains any invalid characters
68        for kar in &char_vec {
69            if !self.decode_map.contains_key(kar) {
70                return Err(Base24Error::DecodeUnsupportedCharacter(kar.clone()));
71            }
72        }
73
74        let res = char_vec
75            .chunks(7)
76            .map(|chunks| {
77                chunks.iter().fold(0u32, |acc, kar| {
78                    let idx = self.decode_map.get(kar).unwrap_or_else(|| {
79                        unreachable!("We checked for invalid chars before. Something is wrong!")
80                    });
81
82                    (ALPHABET_LENGTH as u32) * acc + (*idx as u32)
83                })
84            })
85            .flat_map(|value| value.to_be_bytes().to_vec())
86            .collect();
87
88        Ok(res)
89    }
90}
91
92pub fn encode(data: &[u8]) -> Result<String> {
93    Base24::new().encode(data)
94}
95
96pub fn decode(data: &str) -> Result<Vec<u8>> {
97    Base24::new().decode(data)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_all() {
106        // A few hard coded values
107        let values: Vec<(Vec<u8>, _)> = vec![
108            ("00000000", "ZZZZZZZ"),
109            ("000000000000000000000000", "ZZZZZZZZZZZZZZZZZZZZZ"),
110            ("00000001", "ZZZZZZA"),
111            ("000000010000000100000001", "ZZZZZZAZZZZZZAZZZZZZA"),
112            ("00000010", "ZZZZZZP"),
113            ("00000030", "ZZZZZCZ"),
114            ("88553311", "5YEATXA"),
115            ("FFFFFFFF", "X5GGBH7"),
116            ("FFFFFFFFFFFFFFFFFFFFFFFF", "X5GGBH7X5GGBH7X5GGBH7"),
117            ("FFFFFFFFFFFFFFFFFFFFFFFF", "x5ggbh7x5ggbh7x5ggbh7"),
118            ("1234567887654321", "A64KHWZ5WEPAGG"),
119            ("1234567887654321", "a64khwz5wepagg"),
120            ("FF0001FF001101FF01023399", "XGES63FZZ247C7ZC2ZA6G"),
121            ("FF0001FF001101FF01023399", "xges63fzz247c7zc2za6g"),
122            (
123                "25896984125478546598563251452658",
124                "2FC28KTA66WRST4XAHRRCF237S8Z",
125            ),
126            (
127                "25896984125478546598563251452658",
128                "2fc28kta66wrst4xahrrcf237s8z",
129            ),
130            ("00000001", "ZZZZZZA"),
131            ("00000002", "ZZZZZZC"),
132            ("00000004", "ZZZZZZB"),
133            ("00000008", "ZZZZZZ4"),
134            ("00000010", "ZZZZZZP"),
135            ("00000020", "ZZZZZA4"),
136            ("00000040", "ZZZZZCP"),
137            ("00000080", "ZZZZZ34"),
138            ("00000100", "ZZZZZHP"),
139            ("00000200", "ZZZZZW4"),
140            ("00000400", "ZZZZARP"),
141            ("00000800", "ZZZZ2K4"),
142            ("00001000", "ZZZZFCP"),
143            ("00002000", "ZZZZ634"),
144            ("00004000", "ZZZABHP"),
145            ("00008000", "ZZZC4W4"),
146            ("00010000", "ZZZB8RP"),
147            ("00020000", "ZZZG5K4"),
148            ("00040000", "ZZZRYCP"),
149            ("00080000", "ZZAKX34"),
150            ("00100000", "ZZ229HP"),
151            ("00200000", "ZZEFPW4"),
152            ("00400000", "ZZT7GRP"),
153            ("00800000", "ZAAESK4"),
154            ("01000000", "ZCCK7CP"),
155            ("02000000", "ZB32E34"),
156            ("04000000", "Z4HETHP"),
157            ("08000000", "ZP9KZW4"),
158            ("10000000", "AG8CARP"),
159            ("20000000", "CSHB2K4"),
160            ("40000000", "3694FCP"),
161            ("80000000", "53PP634"),
162        ]
163        .iter()
164        .map(|(str_data, b24_str)| {
165            let char_vec: Vec<_> = str_data.chars().collect();
166
167            let data: Vec<u8> = char_vec
168                .chunks(2)
169                .map(|chunk| {
170                    let byte_str = format!("{}{}", chunk[0], chunk[1]);
171
172                    u8::from_str_radix(&byte_str, 16)
173                })
174                .filter_map(|res| res.ok())
175                .collect();
176
177            (data, b24_str.to_string())
178        })
179        .collect();
180
181        for (data, b24_str) in values {
182            let decoded = decode(&b24_str).expect("error during test decode");
183            assert_eq!(decoded, data);
184            assert_eq!(
185                encode(&decoded).expect("error during test encode"),
186                b24_str.to_uppercase()
187            );
188        }
189    }
190
191    #[test]
192    fn random_test() {
193        use rand::distributions::Standard;
194        use rand::{thread_rng, Rng};
195
196        let rng = thread_rng();
197
198        for _ in 0..100 {
199            let original_data: Vec<u8> = rng.sample_iter(Standard).take(64).collect();
200
201            let encoded_data = encode(&original_data).expect("error during test encode");
202            let decoded_data = decode(&encoded_data).expect("error during test decode");
203
204            assert_eq!(decoded_data, original_data);
205        }
206    }
207
208    #[test]
209    fn test_failures() {
210        let test_data: [u8; 5] = [1, 2, 3, 4, 5];
211
212        assert_eq!(
213            encode(&test_data),
214            Err(Base24Error::EncodeInputLengthInvalid)
215        );
216
217        let test_data: &str = "ZZZ";
218
219        assert_eq!(
220            decode(&test_data),
221            Err(Base24Error::DecodeInputLengthInvalid)
222        );
223
224        let test_data: &str = "ZZZZZZO";
225
226        assert_eq!(
227            decode(&test_data),
228            Err(Base24Error::DecodeUnsupportedCharacter('O'))
229        );
230
231        let test_data: &str = "ABC😘EFG";
232
233        assert_eq!(
234            decode(&test_data),
235            Err(Base24Error::DecodeUnsupportedCharacter('😘'))
236        );
237    }
238}