simple_base64/engine/general_purpose/
mod.rs

1//! Provides the [GeneralPurpose] engine and associated config types.
2use crate::{
3    alphabet,
4    alphabet::Alphabet,
5    engine::{Config, DecodeMetadata, DecodePaddingMode},
6    DecodeError,
7};
8use core::convert::TryInto;
9
10mod decode;
11pub(crate) mod decode_suffix;
12
13pub use decode::GeneralPurposeEstimate;
14
15pub(crate) const INVALID_VALUE: u8 = 255;
16
17/// A general-purpose base64 engine.
18///
19/// - It uses no vector CPU instructions, so it will work on any system.
20/// - It is reasonably fast (~2-3GiB/s).
21/// - It is not constant-time, though, so it is vulnerable to timing side-channel attacks. For loading cryptographic keys, etc, it is suggested to use the forthcoming constant-time implementation.
22pub struct GeneralPurpose {
23    encode_table: [u8; 64],
24    decode_table: [u8; 256],
25    config: GeneralPurposeConfig,
26}
27
28impl GeneralPurpose {
29    /// Create a `GeneralPurpose` engine from an [Alphabet].
30    ///
31    /// While not very expensive to initialize, ideally these should be cached
32    /// if the engine will be used repeatedly.
33    pub const fn new(alphabet: &Alphabet, config: GeneralPurposeConfig) -> Self {
34        Self {
35            encode_table: encode_table(alphabet),
36            decode_table: decode_table(alphabet),
37            config,
38        }
39    }
40}
41
42impl super::Engine for GeneralPurpose {
43    type Config = GeneralPurposeConfig;
44    type DecodeEstimate = GeneralPurposeEstimate;
45
46    fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
47        let mut input_index: usize = 0;
48
49        const BLOCKS_PER_FAST_LOOP: usize = 4;
50        const LOW_SIX_BITS: u64 = 0x3F;
51
52        // we read 8 bytes at a time (u64) but only actually consume 6 of those bytes. Thus, we need
53        // 2 trailing bytes to be available to read..
54        let last_fast_index = input.len().saturating_sub(BLOCKS_PER_FAST_LOOP * 6 + 2);
55        let mut output_index = 0;
56
57        if last_fast_index > 0 {
58            while input_index <= last_fast_index {
59                // Major performance wins from letting the optimizer do the bounds check once, mostly
60                // on the output side
61                let input_chunk =
62                    &input[input_index..(input_index + (BLOCKS_PER_FAST_LOOP * 6 + 2))];
63                let output_chunk =
64                    &mut output[output_index..(output_index + BLOCKS_PER_FAST_LOOP * 8)];
65
66                // Hand-unrolling for 32 vs 16 or 8 bytes produces yields performance about equivalent
67                // to unsafe pointer code on a Xeon E5-1650v3. 64 byte unrolling was slightly better for
68                // large inputs but significantly worse for 50-byte input, unsurprisingly. I suspect
69                // that it's a not uncommon use case to encode smallish chunks of data (e.g. a 64-byte
70                // SHA-512 digest), so it would be nice if that fit in the unrolled loop at least once.
71                // Plus, single-digit percentage performance differences might well be quite different
72                // on different hardware.
73
74                let input_u64 = read_u64(&input_chunk[0..]);
75
76                output_chunk[0] = self.encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
77                output_chunk[1] = self.encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
78                output_chunk[2] = self.encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
79                output_chunk[3] = self.encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
80                output_chunk[4] = self.encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
81                output_chunk[5] = self.encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
82                output_chunk[6] = self.encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
83                output_chunk[7] = self.encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
84
85                let input_u64 = read_u64(&input_chunk[6..]);
86
87                output_chunk[8] = self.encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
88                output_chunk[9] = self.encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
89                output_chunk[10] = self.encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
90                output_chunk[11] = self.encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
91                output_chunk[12] = self.encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
92                output_chunk[13] = self.encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
93                output_chunk[14] = self.encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
94                output_chunk[15] = self.encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
95
96                let input_u64 = read_u64(&input_chunk[12..]);
97
98                output_chunk[16] = self.encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
99                output_chunk[17] = self.encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
100                output_chunk[18] = self.encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
101                output_chunk[19] = self.encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
102                output_chunk[20] = self.encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
103                output_chunk[21] = self.encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
104                output_chunk[22] = self.encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
105                output_chunk[23] = self.encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
106
107                let input_u64 = read_u64(&input_chunk[18..]);
108
109                output_chunk[24] = self.encode_table[((input_u64 >> 58) & LOW_SIX_BITS) as usize];
110                output_chunk[25] = self.encode_table[((input_u64 >> 52) & LOW_SIX_BITS) as usize];
111                output_chunk[26] = self.encode_table[((input_u64 >> 46) & LOW_SIX_BITS) as usize];
112                output_chunk[27] = self.encode_table[((input_u64 >> 40) & LOW_SIX_BITS) as usize];
113                output_chunk[28] = self.encode_table[((input_u64 >> 34) & LOW_SIX_BITS) as usize];
114                output_chunk[29] = self.encode_table[((input_u64 >> 28) & LOW_SIX_BITS) as usize];
115                output_chunk[30] = self.encode_table[((input_u64 >> 22) & LOW_SIX_BITS) as usize];
116                output_chunk[31] = self.encode_table[((input_u64 >> 16) & LOW_SIX_BITS) as usize];
117
118                output_index += BLOCKS_PER_FAST_LOOP * 8;
119                input_index += BLOCKS_PER_FAST_LOOP * 6;
120            }
121        }
122
123        // Encode what's left after the fast loop.
124
125        const LOW_SIX_BITS_U8: u8 = 0x3F;
126
127        let rem = input.len() % 3;
128        let start_of_rem = input.len() - rem;
129
130        // start at the first index not handled by fast loop, which may be 0.
131
132        while input_index < start_of_rem {
133            let input_chunk = &input[input_index..(input_index + 3)];
134            let output_chunk = &mut output[output_index..(output_index + 4)];
135
136            output_chunk[0] = self.encode_table[(input_chunk[0] >> 2) as usize];
137            output_chunk[1] = self.encode_table
138                [((input_chunk[0] << 4 | input_chunk[1] >> 4) & LOW_SIX_BITS_U8) as usize];
139            output_chunk[2] = self.encode_table
140                [((input_chunk[1] << 2 | input_chunk[2] >> 6) & LOW_SIX_BITS_U8) as usize];
141            output_chunk[3] = self.encode_table[(input_chunk[2] & LOW_SIX_BITS_U8) as usize];
142
143            input_index += 3;
144            output_index += 4;
145        }
146
147        if rem == 2 {
148            output[output_index] = self.encode_table[(input[start_of_rem] >> 2) as usize];
149            output[output_index + 1] =
150                self.encode_table[((input[start_of_rem] << 4 | input[start_of_rem + 1] >> 4)
151                    & LOW_SIX_BITS_U8) as usize];
152            output[output_index + 2] =
153                self.encode_table[((input[start_of_rem + 1] << 2) & LOW_SIX_BITS_U8) as usize];
154            output_index += 3;
155        } else if rem == 1 {
156            output[output_index] = self.encode_table[(input[start_of_rem] >> 2) as usize];
157            output[output_index + 1] =
158                self.encode_table[((input[start_of_rem] << 4) & LOW_SIX_BITS_U8) as usize];
159            output_index += 2;
160        }
161
162        output_index
163    }
164
165    fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate {
166        GeneralPurposeEstimate::new(input_len)
167    }
168
169    fn internal_decode(
170        &self,
171        input: &[u8],
172        output: &mut [u8],
173        estimate: Self::DecodeEstimate,
174    ) -> Result<DecodeMetadata, DecodeError> {
175        decode::decode_helper(
176            input,
177            estimate,
178            output,
179            &self.decode_table,
180            self.config.decode_allow_trailing_bits,
181            self.config.decode_padding_mode,
182        )
183    }
184
185    fn config(&self) -> &Self::Config {
186        &self.config
187    }
188}
189
190/// Returns a table mapping a 6-bit index to the ASCII byte encoding of the index
191pub(crate) const fn encode_table(alphabet: &Alphabet) -> [u8; 64] {
192    // the encode table is just the alphabet:
193    // 6-bit index lookup -> printable byte
194    let mut encode_table = [0_u8; 64];
195    {
196        let mut index = 0;
197        while index < 64 {
198            encode_table[index] = alphabet.symbols[index];
199            index += 1;
200        }
201    }
202
203    encode_table
204}
205
206/// Returns a table mapping base64 bytes as the lookup index to either:
207/// - [INVALID_VALUE] for bytes that aren't members of the alphabet
208/// - a byte whose lower 6 bits are the value that was encoded into the index byte
209pub(crate) const fn decode_table(alphabet: &Alphabet) -> [u8; 256] {
210    let mut decode_table = [INVALID_VALUE; 256];
211
212    // Since the table is full of `INVALID_VALUE` already, we only need to overwrite
213    // the parts that are valid.
214    let mut index = 0;
215    while index < 64 {
216        // The index in the alphabet is the 6-bit value we care about.
217        // Since the index is in 0-63, it is safe to cast to u8.
218        decode_table[alphabet.symbols[index] as usize] = index as u8;
219        index += 1;
220    }
221
222    decode_table
223}
224
225#[inline]
226fn read_u64(s: &[u8]) -> u64 {
227    u64::from_be_bytes(s[..8].try_into().unwrap())
228}
229
230/// Contains configuration parameters for base64 encoding and decoding.
231///
232/// ```
233/// # use simple_base64::engine::GeneralPurposeConfig;
234/// let config = GeneralPurposeConfig::new()
235///     .with_encode_padding(false);
236///     // further customize using `.with_*` methods as needed
237/// ```
238///
239/// The constants [PAD] and [NO_PAD] cover most use cases.
240///
241/// To specify the characters used, see [Alphabet].
242#[derive(Clone, Copy, Debug)]
243pub struct GeneralPurposeConfig {
244    encode_padding: bool,
245    decode_allow_trailing_bits: bool,
246    decode_padding_mode: DecodePaddingMode,
247}
248
249impl GeneralPurposeConfig {
250    /// Create a new config with `padding` = `true`, `decode_allow_trailing_bits` = `false`, and
251    /// `decode_padding_mode = DecodePaddingMode::RequireCanonicalPadding`.
252    ///
253    /// This probably matches most people's expectations, but consider disabling padding to save
254    /// a few bytes unless you specifically need it for compatibility with some legacy system.
255    pub const fn new() -> Self {
256        Self {
257            // RFC states that padding must be applied by default
258            encode_padding: true,
259            decode_allow_trailing_bits: false,
260            decode_padding_mode: DecodePaddingMode::RequireCanonical,
261        }
262    }
263
264    /// Create a new config based on `self` with an updated `padding` setting.
265    ///
266    /// If `padding` is `true`, encoding will append either 1 or 2 `=` padding characters as needed
267    /// to produce an output whose length is a multiple of 4.
268    ///
269    /// Padding is not needed for correct decoding and only serves to waste bytes, but it's in the
270    /// [spec](https://datatracker.ietf.org/doc/html/rfc4648#section-3.2).
271    ///
272    /// For new applications, consider not using padding if the decoders you're using don't require
273    /// padding to be present.
274    pub const fn with_encode_padding(self, padding: bool) -> Self {
275        Self {
276            encode_padding: padding,
277            ..self
278        }
279    }
280
281    /// Create a new config based on `self` with an updated `decode_allow_trailing_bits` setting.
282    ///
283    /// Most users will not need to configure this. It's useful if you need to decode base64
284    /// produced by a buggy encoder that has bits set in the unused space on the last base64
285    /// character as per [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode).
286    /// If invalid trailing bits are present and this is `true`, those bits will
287    /// be silently ignored, else `DecodeError::InvalidLastSymbol` will be emitted.
288    pub const fn with_decode_allow_trailing_bits(self, allow: bool) -> Self {
289        Self {
290            decode_allow_trailing_bits: allow,
291            ..self
292        }
293    }
294
295    /// Create a new config based on `self` with an updated `decode_padding_mode` setting.
296    ///
297    /// Padding is not useful in terms of representing encoded data -- it makes no difference to
298    /// the decoder if padding is present or not, so if you have some un-padded input to decode, it
299    /// is perfectly fine to use `DecodePaddingMode::Indifferent` to prevent errors from being
300    /// emitted.
301    ///
302    /// However, since in practice
303    /// [people who learned nothing from BER vs DER seem to expect base64 to have one canonical encoding](https://eprint.iacr.org/2022/361),
304    /// the default setting is the stricter `DecodePaddingMode::RequireCanonicalPadding`.
305    ///
306    /// Or, if "canonical" in your circumstance means _no_ padding rather than padding to the
307    /// next multiple of four, there's `DecodePaddingMode::RequireNoPadding`.
308    pub const fn with_decode_padding_mode(self, mode: DecodePaddingMode) -> Self {
309        Self {
310            decode_padding_mode: mode,
311            ..self
312        }
313    }
314}
315
316impl Default for GeneralPurposeConfig {
317    /// Delegates to [GeneralPurposeConfig::new].
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323impl Config for GeneralPurposeConfig {
324    fn encode_padding(&self) -> bool {
325        self.encode_padding
326    }
327}
328
329/// A [GeneralPurpose] engine using the [alphabet::STANDARD] base64 alphabet and [PAD] config.
330pub const STANDARD: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, PAD);
331
332/// A [GeneralPurpose] engine using the [alphabet::STANDARD] base64 alphabet and [NO_PAD] config.
333pub const STANDARD_NO_PAD: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, NO_PAD);
334
335/// A [GeneralPurpose] engine using the [alphabet::URL_SAFE] base64 alphabet and [PAD] config.
336pub const URL_SAFE: GeneralPurpose = GeneralPurpose::new(&alphabet::URL_SAFE, PAD);
337
338/// A [GeneralPurpose] engine using the [alphabet::URL_SAFE] base64 alphabet and [NO_PAD] config.
339pub const URL_SAFE_NO_PAD: GeneralPurpose = GeneralPurpose::new(&alphabet::URL_SAFE, NO_PAD);
340
341/// Include padding bytes when encoding, and require that they be present when decoding.
342///
343/// This is the standard per the base64 RFC, but consider using [NO_PAD] instead as padding serves
344/// little purpose in practice.
345pub const PAD: GeneralPurposeConfig = GeneralPurposeConfig::new();
346
347/// Don't add padding when encoding, and require no padding when decoding.
348pub const NO_PAD: GeneralPurposeConfig = GeneralPurposeConfig::new()
349    .with_encode_padding(false)
350    .with_decode_padding_mode(DecodePaddingMode::RequireNone);