Skip to main content

base64_ng/v2/
const_transforms.rs

1//! Const-capable transforms over validated codec settings.
2
3use super::{
4    contracts::InputError,
5    specifications::{
6        Base64, CodecSettings, DecodePadding, EncodePadding, RuntimeSpec, StrictStandardPadded,
7        StrictStandardUnpadded, StrictUrlSafePadded, StrictUrlSafeUnpadded, TrailingBits,
8    },
9};
10
11/// Error returned by an exact const transform.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum ConstTransformError {
15    /// The exact output length cannot be represented by `usize`.
16    LengthOverflow,
17    /// The encoded input is malformed under the selected codec settings.
18    Input(InputError),
19    /// The output array length is not the exact result length.
20    OutputLengthMismatch {
21        /// Exact required output bytes.
22        required: usize,
23        /// Compile-time output array length supplied by the caller.
24        actual: usize,
25    },
26}
27
28impl core::fmt::Display for ConstTransformError {
29    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30        match self {
31            Self::LengthOverflow => formatter.write_str("base64 output length overflows usize"),
32            Self::Input(error) => error.fmt(formatter),
33            Self::OutputLengthMismatch { required, actual } => write!(
34                formatter,
35                "base64 exact output length mismatch: required {required}, actual {actual}"
36            ),
37        }
38    }
39}
40
41#[cfg(feature = "std")]
42impl std::error::Error for ConstTransformError {}
43
44impl CodecSettings {
45    /// Returns the exact encoded length in const and runtime contexts.
46    pub const fn encoded_len(self, input_len: usize) -> Result<usize, ConstTransformError> {
47        let Some(complete) = (input_len / 3).checked_mul(4) else {
48            return Err(ConstTransformError::LengthOverflow);
49        };
50        let remainder = input_len % 3;
51        let tail = match (remainder, self.encode_padding()) {
52            (0, _) => 0,
53            (_, EncodePadding::Padded) => 4,
54            (value, EncodePadding::Unpadded) => value + 1,
55        };
56        match complete.checked_add(tail) {
57            Some(value) => Ok(value),
58            None => Err(ConstTransformError::LengthOverflow),
59        }
60    }
61
62    /// Validates encoded input and returns its exact decoded length.
63    pub const fn decoded_len(self, input: &[u8]) -> Result<usize, ConstTransformError> {
64        validate_and_measure(self, input)
65    }
66
67    /// Encodes into an array whose length must equal the exact result length.
68    pub const fn encode_array<const INPUT: usize, const OUTPUT: usize>(
69        self,
70        input: &[u8; INPUT],
71    ) -> Result<[u8; OUTPUT], ConstTransformError> {
72        let required = match self.encoded_len(INPUT) {
73            Ok(value) => value,
74            Err(error) => return Err(error),
75        };
76        if required != OUTPUT {
77            return Err(ConstTransformError::OutputLengthMismatch {
78                required,
79                actual: OUTPUT,
80            });
81        }
82
83        let mut output = [0u8; OUTPUT];
84        encode_exact(self, input, &mut output);
85        Ok(output)
86    }
87
88    /// Decodes into an array whose length must equal the exact result length.
89    pub const fn decode_array<const INPUT: usize, const OUTPUT: usize>(
90        self,
91        input: &[u8; INPUT],
92    ) -> Result<[u8; OUTPUT], ConstTransformError> {
93        let required = match self.decoded_len(input) {
94            Ok(value) => value,
95            Err(error) => return Err(error),
96        };
97        if required != OUTPUT {
98            return Err(ConstTransformError::OutputLengthMismatch {
99                required,
100                actual: OUTPUT,
101            });
102        }
103
104        let mut output = [0u8; OUTPUT];
105        decode_exact(self, input, &mut output);
106        Ok(output)
107    }
108}
109
110macro_rules! const_codec_methods {
111    ($specification:ty, $settings:expr) => {
112        impl Base64<$specification> {
113            /// Returns const-capable immutable settings for this codec.
114            #[must_use]
115            pub const fn const_settings(&self) -> CodecSettings {
116                $settings
117            }
118
119            /// Encodes into an array whose length must equal the exact result.
120            pub const fn encode_array<const INPUT: usize, const OUTPUT: usize>(
121                &self,
122                input: &[u8; INPUT],
123            ) -> Result<[u8; OUTPUT], ConstTransformError> {
124                self.const_settings().encode_array(input)
125            }
126
127            /// Decodes into an array whose length must equal the exact result.
128            pub const fn decode_array<const INPUT: usize, const OUTPUT: usize>(
129                &self,
130                input: &[u8; INPUT],
131            ) -> Result<[u8; OUTPUT], ConstTransformError> {
132                self.const_settings().decode_array(input)
133            }
134        }
135    };
136}
137
138const_codec_methods!(StrictStandardPadded, StrictStandardPadded::const_settings());
139const_codec_methods!(
140    StrictStandardUnpadded,
141    StrictStandardUnpadded::const_settings()
142);
143const_codec_methods!(StrictUrlSafePadded, StrictUrlSafePadded::const_settings());
144const_codec_methods!(
145    StrictUrlSafeUnpadded,
146    StrictUrlSafeUnpadded::const_settings()
147);
148
149impl Base64<RuntimeSpec> {
150    /// Returns const-capable immutable settings for this runtime codec.
151    #[must_use]
152    pub const fn const_settings(&self) -> CodecSettings {
153        self.specification().const_settings()
154    }
155
156    /// Encodes into an array whose length must equal the exact result.
157    pub const fn encode_array<const INPUT: usize, const OUTPUT: usize>(
158        &self,
159        input: &[u8; INPUT],
160    ) -> Result<[u8; OUTPUT], ConstTransformError> {
161        self.const_settings().encode_array(input)
162    }
163
164    /// Decodes into an array whose length must equal the exact result.
165    pub const fn decode_array<const INPUT: usize, const OUTPUT: usize>(
166        &self,
167        input: &[u8; INPUT],
168    ) -> Result<[u8; OUTPUT], ConstTransformError> {
169        self.const_settings().decode_array(input)
170    }
171}
172
173#[allow(clippy::manual_is_multiple_of)]
174const fn validate_and_measure(
175    settings: CodecSettings,
176    input: &[u8],
177) -> Result<usize, ConstTransformError> {
178    if matches!(settings.decode_padding(), DecodePadding::RequireCanonical) && input.len() % 4 != 0
179    {
180        return Err(ConstTransformError::Input(InputError::TruncatedInput {
181            index: input.len(),
182        }));
183    }
184
185    let mut read = 0;
186    let mut written = 0usize;
187    while input.len() - read >= 4 {
188        let (produced, terminal) = match validate_quantum(settings, input, read) {
189            Ok(value) => value,
190            Err(error) => return Err(ConstTransformError::Input(error)),
191        };
192        written = match written.checked_add(produced) {
193            Some(value) => value,
194            None => return Err(ConstTransformError::LengthOverflow),
195        };
196        read += 4;
197        if terminal && read != input.len() {
198            return Err(ConstTransformError::Input(InputError::TrailingData {
199                index: read,
200            }));
201        }
202    }
203
204    if read == input.len() {
205        return Ok(written);
206    }
207    if matches!(settings.decode_padding(), DecodePadding::RequireCanonical) {
208        return Err(ConstTransformError::Input(InputError::TruncatedInput {
209            index: input.len(),
210        }));
211    }
212
213    match input.len() - read {
214        2 => {
215            if let Err(error) = decode_symbol(settings, input[read], read) {
216                return Err(ConstTransformError::Input(error));
217            }
218            let second = match decode_symbol(settings, input[read + 1], read + 1) {
219                Ok(value) => value,
220                Err(error) => return Err(ConstTransformError::Input(error)),
221            };
222            if second & 0x0f != 0
223                && matches!(settings.trailing_bits(), TrailingBits::RequireCanonical)
224            {
225                return Err(ConstTransformError::Input(
226                    InputError::NonCanonicalTrailingBits { index: read + 1 },
227                ));
228            }
229            add_decoded_len(written, 1)
230        }
231        3 => validate_three_symbol_tail(settings, input, read, written),
232        _ => Err(ConstTransformError::Input(InputError::InvalidLength)),
233    }
234}
235
236const fn validate_three_symbol_tail(
237    settings: CodecSettings,
238    input: &[u8],
239    read: usize,
240    written: usize,
241) -> Result<usize, ConstTransformError> {
242    let first = match decode_symbol(settings, input[read], read) {
243        Ok(value) => value,
244        Err(error) => return Err(ConstTransformError::Input(error)),
245    };
246    let second = match decode_symbol(settings, input[read + 1], read + 1) {
247        Ok(value) => value,
248        Err(error) => return Err(ConstTransformError::Input(error)),
249    };
250    let _ = first;
251    if input[read + 2] == b'=' {
252        if matches!(settings.decode_padding(), DecodePadding::Forbid) {
253            return Err(ConstTransformError::Input(InputError::InvalidPadding {
254                index: read + 2,
255            }));
256        }
257        if second & 0x0f != 0 && matches!(settings.trailing_bits(), TrailingBits::RequireCanonical)
258        {
259            return Err(ConstTransformError::Input(
260                InputError::NonCanonicalTrailingBits { index: read + 1 },
261            ));
262        }
263        return add_decoded_len(written, 1);
264    }
265
266    let third = match decode_symbol(settings, input[read + 2], read + 2) {
267        Ok(value) => value,
268        Err(error) => return Err(ConstTransformError::Input(error)),
269    };
270    if third & 0x03 != 0 && matches!(settings.trailing_bits(), TrailingBits::RequireCanonical) {
271        return Err(ConstTransformError::Input(
272            InputError::NonCanonicalTrailingBits { index: read + 2 },
273        ));
274    }
275    add_decoded_len(written, 2)
276}
277
278const fn validate_quantum(
279    settings: CodecSettings,
280    input: &[u8],
281    read: usize,
282) -> Result<(usize, bool), InputError> {
283    let first = match decode_symbol(settings, input[read], read) {
284        Ok(value) => value,
285        Err(error) => return Err(error),
286    };
287    let second = match decode_symbol(settings, input[read + 1], read + 1) {
288        Ok(value) => value,
289        Err(error) => return Err(error),
290    };
291    let _ = first;
292    match (input[read + 2], input[read + 3]) {
293        (b'=', b'=') => {
294            if matches!(settings.decode_padding(), DecodePadding::Forbid) {
295                return Err(InputError::InvalidPadding { index: read + 2 });
296            }
297            if second & 0x0f != 0
298                && matches!(settings.trailing_bits(), TrailingBits::RequireCanonical)
299            {
300                return Err(InputError::NonCanonicalTrailingBits { index: read + 1 });
301            }
302            Ok((1, true))
303        }
304        (b'=', _) => Err(InputError::InvalidPadding { index: read + 2 }),
305        (third, b'=') => {
306            if matches!(settings.decode_padding(), DecodePadding::Forbid) {
307                return Err(InputError::InvalidPadding { index: read + 3 });
308            }
309            let third = match decode_symbol(settings, third, read + 2) {
310                Ok(value) => value,
311                Err(error) => return Err(error),
312            };
313            if third & 0x03 != 0
314                && matches!(settings.trailing_bits(), TrailingBits::RequireCanonical)
315            {
316                return Err(InputError::NonCanonicalTrailingBits { index: read + 2 });
317            }
318            Ok((2, true))
319        }
320        (third, fourth) => {
321            if let Err(error) = decode_symbol(settings, third, read + 2) {
322                return Err(error);
323            }
324            if let Err(error) = decode_symbol(settings, fourth, read + 3) {
325                return Err(error);
326            }
327            Ok((3, false))
328        }
329    }
330}
331
332const fn decode_symbol(settings: CodecSettings, byte: u8, index: usize) -> Result<u8, InputError> {
333    match settings.alphabet().decode_byte(byte) {
334        Some(value) => Ok(value),
335        None if byte == b'=' => Err(InputError::InvalidPadding { index }),
336        None => Err(InputError::InvalidByte { index, byte }),
337    }
338}
339
340const fn add_decoded_len(written: usize, additional: usize) -> Result<usize, ConstTransformError> {
341    match written.checked_add(additional) {
342        Some(value) => Ok(value),
343        None => Err(ConstTransformError::LengthOverflow),
344    }
345}
346
347#[allow(clippy::cast_lossless)]
348const fn encode_exact(settings: CodecSettings, input: &[u8], output: &mut [u8]) {
349    let alphabet = settings.alphabet().as_array();
350    let mut read = 0;
351    let mut write = 0;
352    while input.len() - read >= 3 {
353        output[write] = alphabet[(input[read] >> 2) as usize];
354        output[write + 1] = alphabet[(((input[read] & 3) << 4) | (input[read + 1] >> 4)) as usize];
355        output[write + 2] =
356            alphabet[(((input[read + 1] & 15) << 2) | (input[read + 2] >> 6)) as usize];
357        output[write + 3] = alphabet[(input[read + 2] & 63) as usize];
358        read += 3;
359        write += 4;
360    }
361
362    let remaining = input.len() - read;
363    if remaining != 0 {
364        output[write] = alphabet[(input[read] >> 2) as usize];
365        output[write + 1] = alphabet[((input[read] & 3) << 4) as usize];
366        if remaining == 2 {
367            output[write + 1] =
368                alphabet[(((input[read] & 3) << 4) | (input[read + 1] >> 4)) as usize];
369            output[write + 2] = alphabet[((input[read + 1] & 15) << 2) as usize];
370            if matches!(settings.encode_padding(), EncodePadding::Padded) {
371                output[write + 3] = b'=';
372            }
373        } else if matches!(settings.encode_padding(), EncodePadding::Padded) {
374            output[write + 2] = b'=';
375            output[write + 3] = b'=';
376        }
377    }
378}
379
380const fn decode_exact(settings: CodecSettings, input: &[u8], output: &mut [u8]) {
381    let mut read = 0;
382    let mut write = 0;
383    while input.len() - read >= 4 {
384        let first = decode_value(settings, input[read]);
385        let second = decode_value(settings, input[read + 1]);
386        output[write] = (first << 2) | (second >> 4);
387        write += 1;
388        if input[read + 2] != b'=' {
389            let third = decode_value(settings, input[read + 2]);
390            output[write] = (second << 4) | (third >> 2);
391            write += 1;
392            if input[read + 3] != b'=' {
393                output[write] = (third << 6) | decode_value(settings, input[read + 3]);
394                write += 1;
395            }
396        }
397        read += 4;
398    }
399
400    if read + 2 <= input.len() {
401        let first = decode_value(settings, input[read]);
402        let second = decode_value(settings, input[read + 1]);
403        output[write] = (first << 2) | (second >> 4);
404        if read + 3 == input.len() && input[read + 2] != b'=' {
405            output[write + 1] = (second << 4) | (decode_value(settings, input[read + 2]) >> 2);
406        }
407    }
408}
409
410const fn decode_value(settings: CodecSettings, byte: u8) -> u8 {
411    match settings.alphabet().decode_byte(byte) {
412        Some(value) => value,
413        None => 0,
414    }
415}