Skip to main content

base64/
alphabet.rs

1//! Provides [Alphabet] and constants for alphabets commonly used in the wild.
2
3use core::{array, convert, fmt};
4#[cfg(any(feature = "std", test))]
5use std::error;
6
7/// Unsurprisingly, there are 64 symbols in a Base64 alphabet.
8const ALPHABET_LEN: usize = 64;
9
10/// Pad symbol for non-weird alphabets.
11pub(crate) const PADDING_SYMBOL: Symbol = Symbol(b'=');
12
13/// An alphabet defines the 64 ASCII characters (symbols) used for base64.
14///
15/// Common alphabets are provided as constants, and custom alphabets
16/// can be made via `from_str` or the `TryFrom<str>` implementation.
17///
18/// # Examples
19///
20/// Building and using a custom Alphabet:
21///
22/// ```
23/// let custom = base64::alphabet::Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/").unwrap();
24///
25/// let engine = base64::engine::GeneralPurpose::new(
26///     &custom,
27///     base64::engine::general_purpose::PAD);
28/// ```
29///
30/// Building a const:
31///
32/// ```
33/// use base64::alphabet::Alphabet;
34///
35/// static CUSTOM: Alphabet = {
36///     // Result::unwrap() isn't const yet, but panic!() is OK
37///     match Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") {
38///         Ok(x) => x,
39///         Err(_) => panic!("creation of alphabet failed"),
40///     }
41/// };
42/// ```
43///
44/// Building lazily:
45///
46/// ```
47/// use base64::alphabet::Alphabet;
48/// use std::sync::LazyLock;
49///
50/// static CUSTOM: LazyLock<Alphabet> = LazyLock::new(||
51///     Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/").unwrap()
52/// );
53/// ```
54#[derive(Clone, Eq, PartialEq)]
55pub struct Alphabet {
56    /// All bytes are valid symbols, but left as u8 to allow `.as_str()` to work.
57    pub(crate) symbols: [u8; ALPHABET_LEN],
58    pub(crate) padding: Symbol,
59}
60
61impl Alphabet {
62    /// Performs no checks so that it can be const.
63    /// Used only for known-valid strings.
64    const fn from_str_unchecked(alphabet: &str, padding: Symbol) -> Self {
65        let mut symbols = [0_u8; ALPHABET_LEN];
66        let source_bytes = alphabet.as_bytes();
67
68        // a way to copy that's allowed in const fn
69        let mut index = 0;
70        while index < ALPHABET_LEN {
71            symbols[index] = source_bytes[index];
72            index += 1;
73        }
74
75        Self { symbols, padding }
76    }
77
78    /// Create an `Alphabet` from a string of 64 unique printable ASCII bytes with `=` as the
79    /// padding symbol.
80    ///
81    /// The padding symbol `=` is not allowed in the alphabet.
82    ///
83    /// See [`Self::new_with_padding`] if a non-default padding symbol is needed.
84    pub const fn new(alphabet: &str) -> Result<Self, ParseAlphabetError> {
85        Self::new_with_padding(alphabet, PADDING_SYMBOL)
86    }
87
88    /// Create an `Alphabet` from a string of 64 unique printable ASCII bytes, with a custom
89    /// padding symbol.
90    ///
91    /// The padding symbol must not appear in the alphabet.
92    ///
93    /// This is meant for strange alphabets that don't use `=` as the padding symbol.
94    pub const fn new_with_padding(
95        alphabet: &str,
96        padding: Symbol,
97    ) -> Result<Self, ParseAlphabetError> {
98        let bytes = alphabet.as_bytes();
99        if bytes.len() != ALPHABET_LEN {
100            return Err(ParseAlphabetError::InvalidLength);
101        }
102
103        {
104            let mut index = 0;
105            while index < ALPHABET_LEN {
106                let byte = bytes[index];
107
108                if !is_valid_b64_symbol(byte) {
109                    return Err(ParseAlphabetError::UnprintableByte(byte));
110                }
111                if byte == padding.as_u8() {
112                    return Err(ParseAlphabetError::ReservedByte(byte));
113                }
114
115                // Check for duplicates while staying within what const allows.
116                // It's n^2, but only over 64 hot bytes, and only once, so it's likely in the single digit
117                // microsecond range.
118
119                let mut probe_index = 0;
120                while probe_index < ALPHABET_LEN {
121                    if probe_index != index && byte == bytes[probe_index] {
122                        return Err(ParseAlphabetError::DuplicatedByte(byte));
123                    }
124
125                    probe_index += 1;
126                }
127
128                index += 1;
129            }
130        }
131
132        Ok(Self::from_str_unchecked(alphabet, padding))
133    }
134
135    /// A `&str` containing the symbols in the `Alphabet` (excluding padding)
136    #[must_use]
137    pub fn as_str(&self) -> &str {
138        core::str::from_utf8(&self.symbols).unwrap()
139    }
140
141    /// The 64 symbols of the alphabet (excluding padding).
142    pub fn symbols(&self) -> [Symbol; ALPHABET_LEN] {
143        array::from_fn(|i| {
144            // safe to construct Symbol since all symbol bytes have already been checked
145            Symbol(self.symbols[i])
146        })
147    }
148
149    /// The symbol used for padding.
150    pub fn padding(&self) -> Symbol {
151        self.padding
152    }
153}
154
155impl fmt::Debug for Alphabet {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(
158            f,
159            "Alphabet {{ symbols: {:?}, padding: '{:?}' }}",
160            self.as_str(),
161            self.padding
162        )
163    }
164}
165
166/// An ASCII printable byte suitable for use as a base64 symbol in an alphabet or as custom padding.
167///
168/// This doesn't mean that a particular symbol is used in any particular alphabet, just that it
169/// could be used in one.
170#[derive(Clone, Copy, PartialEq, Eq)]
171pub struct Symbol(u8);
172
173impl Symbol {
174    /// Returns `Some` if `symbol` is a valid printable ASCII symbol, otherwise `None`.
175    pub const fn new(symbol: u8) -> Option<Self> {
176        if is_valid_b64_symbol(symbol) {
177            Some(Self(symbol))
178        } else {
179            None
180        }
181    }
182
183    /// Returns the symbol as an ASCII byte.
184    pub const fn as_u8(&self) -> u8 {
185        self.0
186    }
187
188    /// Returns the symbol as a char.
189    pub fn as_char(&self) -> char {
190        // ascii u8 is the same as the code point, conveniently
191        char::from(self.0)
192    }
193}
194
195impl From<Symbol> for u8 {
196    fn from(value: Symbol) -> Self {
197        value.0
198    }
199}
200
201impl fmt::Debug for Symbol {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        write!(f, "{}", self.as_char())
204    }
205}
206
207/// Must be ascii printable. 127 (DEL) is commonly considered printable
208/// for some reason but clearly unsuitable for base64.
209pub(crate) const fn is_valid_b64_symbol(byte: u8) -> bool {
210    byte >= 32_u8 && byte <= 126_u8
211}
212
213impl convert::TryFrom<&str> for Alphabet {
214    type Error = ParseAlphabetError;
215
216    fn try_from(value: &str) -> Result<Self, Self::Error> {
217        Self::new(value)
218    }
219}
220
221/// Possible errors when constructing an [Alphabet] from a `str`.
222#[derive(Debug, Eq, PartialEq)]
223pub enum ParseAlphabetError {
224    /// Alphabets must be 64 ASCII bytes
225    InvalidLength,
226    /// All bytes must be unique
227    DuplicatedByte(u8),
228    /// All bytes must be printable (in the range `[32, 126]`).
229    UnprintableByte(u8),
230    /// Alphabet cannot contain the pad symbol (`=` by default)
231    ReservedByte(u8),
232}
233
234impl fmt::Display for ParseAlphabetError {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        match self {
237            Self::InvalidLength => write!(f, "Invalid length - must be 64 bytes"),
238            Self::DuplicatedByte(b) => write!(f, "Duplicated byte: {:#04x}", b),
239            Self::UnprintableByte(b) => write!(f, "Unprintable byte: {:#04x}", b),
240            Self::ReservedByte(b) => write!(f, "Reserved byte: {:#04x}", b),
241        }
242    }
243}
244
245#[cfg(any(feature = "std", test))]
246impl error::Error for ParseAlphabetError {}
247
248/// The standard alphabet (with `+` and `/`) specified in [RFC 4648][].
249///
250/// [RFC 4648]: https://datatracker.ietf.org/doc/html/rfc4648#section-4
251pub const STANDARD: Alphabet = Alphabet::from_str_unchecked(
252    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
253    PADDING_SYMBOL,
254);
255
256/// The URL-safe alphabet (with `-` and `_`) specified in [RFC 4648][].
257///
258/// [RFC 4648]: https://datatracker.ietf.org/doc/html/rfc4648#section-5
259pub const URL_SAFE: Alphabet = Alphabet::from_str_unchecked(
260    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
261    PADDING_SYMBOL,
262);
263
264/// The `crypt(3)` alphabet (with `.` and `/` as the _first_ two characters).
265///
266/// Not standardized, but folk wisdom on the net asserts that this alphabet is what crypt uses.
267pub const CRYPT: Alphabet = Alphabet::from_str_unchecked(
268    "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
269    PADDING_SYMBOL,
270);
271
272/// The bcrypt alphabet.
273pub const BCRYPT: Alphabet = Alphabet::from_str_unchecked(
274    "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
275    PADDING_SYMBOL,
276);
277
278/// The alphabet used in IMAP-modified UTF-7 (with `+` and `,`).
279///
280/// See [RFC 3501](https://tools.ietf.org/html/rfc3501#section-5.1.3)
281pub const IMAP_MUTF7: Alphabet = Alphabet::from_str_unchecked(
282    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,",
283    PADDING_SYMBOL,
284);
285
286/// The alphabet used in `BinHex` 4.0 files.
287///
288/// See [BinHex 4.0 Definition](http://files.stairways.com/other/binhex-40-specs-info.txt)
289pub const BIN_HEX: Alphabet = Alphabet::from_str_unchecked(
290    "!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr",
291    PADDING_SYMBOL,
292);
293
294#[cfg(test)]
295mod tests {
296    use crate::alphabet::*;
297    use core::convert::TryFrom as _;
298
299    #[test]
300    fn detects_duplicate_start() {
301        assert_eq!(
302            ParseAlphabetError::DuplicatedByte(b'A'),
303            Alphabet::new("AACDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
304                .unwrap_err()
305        );
306    }
307
308    #[test]
309    fn detects_duplicate_end() {
310        assert_eq!(
311            ParseAlphabetError::DuplicatedByte(b'/'),
312            Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789//")
313                .unwrap_err()
314        );
315    }
316
317    #[test]
318    fn detects_duplicate_middle() {
319        assert_eq!(
320            ParseAlphabetError::DuplicatedByte(b'Z'),
321            Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZZbcdefghijklmnopqrstuvwxyz0123456789+/")
322                .unwrap_err()
323        );
324    }
325
326    #[test]
327    fn detects_length() {
328        assert_eq!(
329            ParseAlphabetError::InvalidLength,
330            Alphabet::new(
331                "xxxxxxxxxABCDEFGHIJKLMNOPQRSTUVWXYZZbcdefghijklmnopqrstuvwxyz0123456789+/",
332            )
333            .unwrap_err()
334        );
335    }
336
337    #[test]
338    fn detects_padding() {
339        assert_eq!(
340            ParseAlphabetError::ReservedByte(b'='),
341            Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=")
342                .unwrap_err()
343        );
344    }
345
346    #[test]
347    fn detects_unprintable() {
348        // form feed
349        assert_eq!(
350            ParseAlphabetError::UnprintableByte(0xc),
351            Alphabet::new("\x0cBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
352                .unwrap_err()
353        );
354    }
355
356    #[test]
357    fn same_as_unchecked() {
358        assert_eq!(
359            STANDARD,
360            Alphabet::try_from("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
361                .unwrap()
362        );
363    }
364
365    #[test]
366    fn str_same_as_input() {
367        let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
368        let a = Alphabet::try_from(alphabet).unwrap();
369        assert_eq!(alphabet, a.as_str())
370    }
371
372    #[test]
373    fn symbol_matches_char_for_all_valid_symbols() {
374        for symbol in (0..=u8::MAX).filter_map(Symbol::new) {
375            // treat the byte as UTF-8
376            let bytes = &[symbol.as_u8()];
377            let s = std::str::from_utf8(bytes).unwrap();
378            assert_eq!(1, s.chars().count());
379
380            let char = s.chars().next().unwrap();
381            assert_eq!(char, symbol.as_char());
382        }
383    }
384
385    #[test]
386    fn alphabet_debug() {
387        assert_eq!(
388            r##"Alphabet { symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", padding: '=' }"##,
389            format!("{STANDARD:?}")
390        );
391    }
392
393    #[test]
394    fn alphabet_symbols() {
395        assert_eq!(
396            STANDARD.as_str(),
397            STANDARD
398                .symbols()
399                .iter()
400                .map(|s| s.as_char())
401                .collect::<String>()
402        );
403    }
404}