Skip to main content

base64_ng/v2/
alphabet.rs

1//! Owned validated alphabets for the 2.0 codec core.
2//!
3//! This module is internal until Commit 6 exposes validated codec
4//! specifications. The value contains only its immutable 64-byte table; it
5//! cannot carry caller-provided mapping functions.
6
7/// The number of symbols in every Base64 alphabet.
8pub(crate) const ALPHABET_LEN: usize = 64;
9
10/// The source-locked RFC 4648 Standard alphabet.
11pub(super) const STANDARD_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
12    table: *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
13};
14/// The source-locked RFC 4648 URL-safe alphabet.
15pub(super) const URL_SAFE_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
16    table: *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
17};
18pub(super) const BCRYPT_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
19    table: *b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
20};
21pub(super) const CRYPT_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
22    table: *b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
23};
24pub(super) const PBKDF2_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
25    table: *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./",
26};
27/// The 64-character `BinHex` 4.0 alphabet.
28///
29/// This value does not select padding and does not parse a `BinHex` container.
30pub const BINHEX_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
31    table: *b"!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr",
32};
33pub(super) const IMAP_MUTF7_ALPHABET: ValidatedAlphabet = ValidatedAlphabet {
34    table: *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,",
35};
36
37/// Failure returned while constructing a [`ValidatedAlphabet`].
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ValidatedAlphabetError {
40    /// A byte slice did not contain exactly 64 bytes.
41    InvalidLength {
42        /// Observed byte length.
43        actual: usize,
44    },
45    /// An alphabet position contains a byte outside visible ASCII.
46    InvalidByte {
47        /// Byte index in the alphabet table.
48        index: usize,
49        /// Rejected byte value.
50        byte: u8,
51    },
52    /// An alphabet position contains the reserved padding byte `=`.
53    PaddingByte {
54        /// Byte index in the alphabet table.
55        index: usize,
56    },
57    /// Two alphabet positions contain the same byte.
58    DuplicateByte {
59        /// First index containing the byte.
60        first: usize,
61        /// Second index containing the byte.
62        second: usize,
63        /// Duplicated byte value.
64        byte: u8,
65    },
66}
67
68impl core::fmt::Display for ValidatedAlphabetError {
69    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70        match self {
71            Self::InvalidLength { actual } => {
72                write!(
73                    formatter,
74                    "base64 alphabet has length {actual}; expected {ALPHABET_LEN}"
75                )
76            }
77            Self::InvalidByte { index, byte } => {
78                write!(
79                    formatter,
80                    "invalid base64 alphabet byte 0x{byte:02x} at index {index}"
81                )
82            }
83            Self::PaddingByte { index } => {
84                write!(
85                    formatter,
86                    "base64 alphabet contains padding byte at index {index}"
87                )
88            }
89            Self::DuplicateByte {
90                first,
91                second,
92                byte,
93            } => write!(
94                formatter,
95                "base64 alphabet byte 0x{byte:02x} is duplicated at indexes \
96                 {first} and {second}"
97            ),
98        }
99    }
100}
101
102/// An owned, immutable, validated 64-byte Base64 alphabet.
103///
104/// Encode and decode mappings are both derived from `table`. The type has no
105/// executable callback or overridable mapping method.
106#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
107#[repr(transparent)]
108pub struct ValidatedAlphabet {
109    table: [u8; ALPHABET_LEN],
110}
111
112impl ValidatedAlphabet {
113    /// Validates and owns a 64-byte alphabet.
114    pub const fn new(table: [u8; ALPHABET_LEN]) -> Result<Self, ValidatedAlphabetError> {
115        match validate_table(&table) {
116            Ok(()) => Ok(Self { table }),
117            Err(error) => Err(error),
118        }
119    }
120
121    /// Copies, validates, and owns a 64-byte alphabet slice.
122    pub const fn try_from_slice(bytes: &[u8]) -> Result<Self, ValidatedAlphabetError> {
123        if bytes.len() != ALPHABET_LEN {
124            return Err(ValidatedAlphabetError::InvalidLength {
125                actual: bytes.len(),
126            });
127        }
128
129        let mut table = [0u8; ALPHABET_LEN];
130        let mut index = 0;
131        while index < ALPHABET_LEN {
132            table[index] = bytes[index];
133            index += 1;
134        }
135        Self::new(table)
136    }
137
138    /// Returns the single table that defines both mappings.
139    #[must_use]
140    pub const fn as_array(&self) -> &[u8; ALPHABET_LEN] {
141        &self.table
142    }
143
144    /// Returns the encoded symbol for one six-bit value.
145    #[allow(clippy::cast_lossless)]
146    #[must_use]
147    pub const fn encode_value(&self, value: u8) -> Option<u8> {
148        if value < 64 {
149            Some(self.table[value as usize])
150        } else {
151            None
152        }
153    }
154
155    /// Returns the six-bit value represented by `byte`.
156    #[must_use]
157    pub const fn decode_byte(&self, byte: u8) -> Option<u8> {
158        let mut index = 0;
159        let mut candidate = 0u8;
160        while index < ALPHABET_LEN {
161            if self.table[index] == byte {
162                return Some(candidate);
163            }
164            index += 1;
165            candidate += 1;
166        }
167        None
168    }
169}
170
171impl TryFrom<[u8; ALPHABET_LEN]> for ValidatedAlphabet {
172    type Error = ValidatedAlphabetError;
173
174    fn try_from(table: [u8; ALPHABET_LEN]) -> Result<Self, Self::Error> {
175        Self::new(table)
176    }
177}
178
179impl TryFrom<&[u8]> for ValidatedAlphabet {
180    type Error = ValidatedAlphabetError;
181
182    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
183        Self::try_from_slice(bytes)
184    }
185}
186
187const fn validate_table(table: &[u8; ALPHABET_LEN]) -> Result<(), ValidatedAlphabetError> {
188    let mut index = 0;
189    while index < ALPHABET_LEN {
190        match validate_position(table, index) {
191            Ok(()) => {}
192            Err(error) => return Err(error),
193        }
194        index += 1;
195    }
196    Ok(())
197}
198
199const fn validate_position(
200    table: &[u8; ALPHABET_LEN],
201    index: usize,
202) -> Result<(), ValidatedAlphabetError> {
203    let byte = table[index];
204    if byte < 0x21 || byte > 0x7e {
205        return Err(ValidatedAlphabetError::InvalidByte { index, byte });
206    }
207    if byte == b'=' {
208        return Err(ValidatedAlphabetError::PaddingByte { index });
209    }
210
211    let mut duplicate = index + 1;
212    while duplicate < ALPHABET_LEN {
213        if table[duplicate] == byte {
214            return Err(ValidatedAlphabetError::DuplicateByte {
215                first: index,
216                second: duplicate,
217                byte,
218            });
219        }
220        duplicate += 1;
221    }
222    Ok(())
223}
224
225/// Exposes one constructor validation step to the bounded-index Kani harness.
226#[cfg(kani)]
227pub(crate) fn validate_position_for_proof(
228    table: &[u8; ALPHABET_LEN],
229    index: usize,
230) -> Result<(), ValidatedAlphabetError> {
231    validate_position(table, index)
232}