Skip to main content

ct_codecs/
base64.rs

1use crate::error::*;
2use crate::{Decoder, Encoder};
3
4struct Base64Impl;
5
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7enum Base64Variant {
8    Original = 1,
9    OriginalNoPadding = 3,
10    UrlSafe = 5,
11    UrlSafeNoPadding = 7,
12}
13
14enum VariantMask {
15    NoPadding = 2,
16    UrlSafe = 4,
17}
18
19impl Base64Impl {
20    #[inline]
21    fn _eq(x: u8, y: u8) -> u8 {
22        !(((0u16.wrapping_sub((x as u16) ^ (y as u16))) >> 8) as u8)
23    }
24
25    #[inline]
26    fn _gt(x: u8, y: u8) -> u8 {
27        (((y as u16).wrapping_sub(x as u16)) >> 8) as u8
28    }
29
30    #[inline]
31    fn _ge(x: u8, y: u8) -> u8 {
32        !Self::_gt(y, x)
33    }
34
35    #[inline]
36    fn _lt(x: u8, y: u8) -> u8 {
37        Self::_gt(y, x)
38    }
39
40    #[inline]
41    fn _le(x: u8, y: u8) -> u8 {
42        Self::_ge(y, x)
43    }
44
45    #[inline]
46    fn b64_byte_to_char(x: u8) -> u8 {
47        (Self::_lt(x, 26) & (x.wrapping_add(b'A')))
48            | (Self::_ge(x, 26) & Self::_lt(x, 52) & (x.wrapping_add(b'a'.wrapping_sub(26))))
49            | (Self::_ge(x, 52) & Self::_lt(x, 62) & (x.wrapping_add(b'0'.wrapping_sub(52))))
50            | (Self::_eq(x, 62) & b'+')
51            | (Self::_eq(x, 63) & b'/')
52    }
53
54    #[inline]
55    fn b64_char_to_byte(c: u8) -> u8 {
56        let x = (Self::_ge(c, b'A') & Self::_le(c, b'Z') & (c.wrapping_sub(b'A')))
57            | (Self::_ge(c, b'a') & Self::_le(c, b'z') & (c.wrapping_sub(b'a'.wrapping_sub(26))))
58            | (Self::_ge(c, b'0') & Self::_le(c, b'9') & (c.wrapping_sub(b'0'.wrapping_sub(52))))
59            | (Self::_eq(c, b'+') & 62)
60            | (Self::_eq(c, b'/') & 63);
61        x | (Self::_eq(x, 0) & (Self::_eq(c, b'A') ^ 0xff))
62    }
63
64    #[inline]
65    fn b64_byte_to_urlsafe_char(x: u8) -> u8 {
66        (Self::_lt(x, 26) & (x.wrapping_add(b'A')))
67            | (Self::_ge(x, 26) & Self::_lt(x, 52) & (x.wrapping_add(b'a'.wrapping_sub(26))))
68            | (Self::_ge(x, 52) & Self::_lt(x, 62) & (x.wrapping_add(b'0'.wrapping_sub(52))))
69            | (Self::_eq(x, 62) & b'-')
70            | (Self::_eq(x, 63) & b'_')
71    }
72
73    #[inline]
74    fn b64_urlsafe_char_to_byte(c: u8) -> u8 {
75        let x = (Self::_ge(c, b'A') & Self::_le(c, b'Z') & (c.wrapping_sub(b'A')))
76            | (Self::_ge(c, b'a') & Self::_le(c, b'z') & (c.wrapping_sub(b'a'.wrapping_sub(26))))
77            | (Self::_ge(c, b'0') & Self::_le(c, b'9') & (c.wrapping_sub(b'0'.wrapping_sub(52))))
78            | (Self::_eq(c, b'-') & 62)
79            | (Self::_eq(c, b'_') & 63);
80        x | (Self::_eq(x, 0) & (Self::_eq(c, b'A') ^ 0xff))
81    }
82
83    #[inline]
84    fn encoded_len(bin_len: usize, variant: Base64Variant) -> Result<usize, Error> {
85        let nibbles = bin_len / 3;
86        let rounded = nibbles * 3;
87        let pad = bin_len - rounded;
88        let mut b64_len = nibbles.checked_mul(4).ok_or(Error::Overflow)?;
89        if pad != 0 {
90            let remainder_len = if (variant as u16 & VariantMask::NoPadding as u16) == 0 {
91                4
92            } else {
93                2 + (pad >> 1)
94            };
95            b64_len = b64_len.checked_add(remainder_len).ok_or(Error::Overflow)?;
96        }
97        Ok(b64_len)
98    }
99
100    pub fn encode<'t>(
101        b64: &'t mut [u8],
102        bin: &[u8],
103        variant: Base64Variant,
104    ) -> Result<&'t [u8], Error> {
105        let bin_len = bin.len();
106        let b64_maxlen = b64.len();
107        let mut acc_len = 0usize;
108        let mut b64_pos = 0usize;
109        let mut acc = 0u16;
110
111        let nibbles = bin_len / 3;
112        let remainder = bin_len - 3 * nibbles;
113        let mut b64_len = nibbles * 4;
114        if remainder != 0 {
115            if (variant as u16 & VariantMask::NoPadding as u16) == 0 {
116                b64_len += 4;
117            } else {
118                b64_len += 2 + (remainder >> 1);
119            }
120        }
121        if b64_maxlen < b64_len {
122            return Err(Error::Overflow);
123        }
124        if (variant as u16 & VariantMask::UrlSafe as u16) != 0 {
125            for &v in bin {
126                acc = (acc << 8) + v as u16;
127                acc_len += 8;
128                while acc_len >= 6 {
129                    acc_len -= 6;
130                    b64[b64_pos] = Self::b64_byte_to_urlsafe_char(((acc >> acc_len) & 0x3f) as u8);
131                    b64_pos += 1;
132                }
133            }
134            if acc_len > 0 {
135                b64[b64_pos] =
136                    Self::b64_byte_to_urlsafe_char(((acc << (6 - acc_len)) & 0x3f) as u8);
137                b64_pos += 1;
138            }
139        } else {
140            for &v in bin {
141                acc = (acc << 8) + v as u16;
142                acc_len += 8;
143                while acc_len >= 6 {
144                    acc_len -= 6;
145                    b64[b64_pos] = Self::b64_byte_to_char(((acc >> acc_len) & 0x3f) as u8);
146                    b64_pos += 1;
147                }
148            }
149            if acc_len > 0 {
150                b64[b64_pos] = Self::b64_byte_to_char(((acc << (6 - acc_len)) & 0x3f) as u8);
151                b64_pos += 1;
152            }
153        }
154        while b64_pos < b64_len {
155            b64[b64_pos] = b'=';
156            b64_pos += 1
157        }
158        Ok(&b64[..b64_pos])
159    }
160
161    fn skip_padding<'t>(
162        b64: &'t [u8],
163        mut padding_len: usize,
164        ignore: Option<&[u8]>,
165    ) -> Result<&'t [u8], Error> {
166        let b64_len = b64.len();
167        let mut b64_pos = 0usize;
168        while padding_len > 0 {
169            if b64_pos >= b64_len {
170                return Err(Error::InvalidInput);
171            }
172            let c = b64[b64_pos];
173            if c == b'=' {
174                padding_len -= 1
175            } else {
176                match ignore {
177                    Some(ignore) if ignore.contains(&c) => {}
178                    _ => return Err(Error::InvalidInput),
179                }
180            }
181            b64_pos += 1
182        }
183        Ok(&b64[b64_pos..])
184    }
185
186    pub fn decode<'t>(
187        bin: &'t mut [u8],
188        b64: &[u8],
189        ignore: Option<&[u8]>,
190        variant: Base64Variant,
191    ) -> Result<&'t [u8], Error> {
192        let bin_maxlen = bin.len();
193        let is_urlsafe = (variant as u16 & VariantMask::UrlSafe as u16) != 0;
194        let mut acc = 0u16;
195        let mut acc_len = 0usize;
196        let mut bin_pos = 0usize;
197        let mut premature_end = None;
198        for (b64_pos, &c) in b64.iter().enumerate() {
199            let d = if is_urlsafe {
200                Self::b64_urlsafe_char_to_byte(c)
201            } else {
202                Self::b64_char_to_byte(c)
203            };
204            if d == 0xff {
205                match ignore {
206                    Some(ignore) if ignore.contains(&c) => continue,
207                    _ => {
208                        premature_end = Some(b64_pos);
209                        break;
210                    }
211                }
212            }
213            acc = (acc << 6) + d as u16;
214            acc_len += 6;
215            if acc_len >= 8 {
216                acc_len -= 8;
217                if bin_pos >= bin_maxlen {
218                    return Err(Error::Overflow);
219                }
220                bin[bin_pos] = (acc >> acc_len) as u8;
221                bin_pos += 1;
222            }
223        }
224        if acc_len > 4 || (acc & ((1u16 << acc_len).wrapping_sub(1))) != 0 {
225            return Err(Error::InvalidInput);
226        }
227        let padding_len = acc_len / 2;
228        if let Some(premature_end) = premature_end {
229            let remaining = if variant as u16 & VariantMask::NoPadding as u16 == 0 {
230                Self::skip_padding(&b64[premature_end..], padding_len, ignore)?
231            } else {
232                &b64[premature_end..]
233            };
234            match ignore {
235                None => {
236                    if !remaining.is_empty() {
237                        return Err(Error::InvalidInput);
238                    }
239                }
240                Some(ignore) => {
241                    for &c in remaining {
242                        if !ignore.contains(&c) {
243                            return Err(Error::InvalidInput);
244                        }
245                    }
246                }
247            }
248        } else if variant as u16 & VariantMask::NoPadding as u16 == 0 && padding_len != 0 {
249            return Err(Error::InvalidInput);
250        }
251        Ok(&bin[..bin_pos])
252    }
253}
254
255/// Standard Base64 encoder and decoder with padding.
256///
257/// This implementation follows the standard Base64 encoding as defined in RFC 4648,
258/// and includes padding characters ('=') when needed.
259///
260/// # Standard Base64 Alphabet
261///
262/// The standard Base64 alphabet uses characters:
263/// - 'A' to 'Z' (26 characters)
264/// - 'a' to 'z' (26 characters)
265/// - '0' to '9' (10 characters)
266/// - '+' and '/' (2 characters)
267/// - '=' (padding character)
268///
269/// # Examples
270///
271/// ```
272/// use ct_codecs::{Base64, Encoder, Decoder};
273///
274/// fn example() -> Result<(), ct_codecs::Error> {
275///     let data = b"Hello, world!";
276///     let encoded = Base64::encode_to_string(data)?;
277///     assert_eq!(encoded, "SGVsbG8sIHdvcmxkIQ==");
278///
279///     let decoded = Base64::decode_to_vec(&encoded, None)?;
280///     assert_eq!(decoded, data);
281///     Ok(())
282/// }
283/// # example().unwrap();
284/// ```
285pub struct Base64;
286
287/// Standard Base64 encoder and decoder without padding.
288///
289/// This implementation follows the standard Base64 encoding as defined in RFC 4648,
290/// but omits padding characters ('=').
291///
292/// # Examples
293///
294/// ```
295/// use ct_codecs::{Base64NoPadding, Encoder, Decoder};
296///
297/// fn example() -> Result<(), ct_codecs::Error> {
298///     let data = b"Hello, world!";
299///     let encoded = Base64NoPadding::encode_to_string(data)?;
300///     assert_eq!(encoded, "SGVsbG8sIHdvcmxkIQ");
301///
302///     let decoded = Base64NoPadding::decode_to_vec(&encoded, None)?;
303///     assert_eq!(decoded, data);
304///     Ok(())
305/// }
306/// # example().unwrap();
307/// ```
308pub struct Base64NoPadding;
309
310/// URL-safe Base64 encoder and decoder with padding.
311///
312/// This implementation follows the URL-safe Base64 encoding variant as defined in RFC 4648.
313/// It replaces the '+' and '/' characters with '-' and '_' to make the output URL and
314/// filename safe. Padding characters ('=') are included when needed.
315///
316/// # URL-safe Base64 Alphabet
317///
318/// The URL-safe Base64 alphabet uses characters:
319/// - 'A' to 'Z' (26 characters)
320/// - 'a' to 'z' (26 characters)
321/// - '0' to '9' (10 characters)
322/// - '-' and '_' (2 characters)
323/// - '=' (padding character)
324///
325/// # Examples
326///
327/// ```
328/// use ct_codecs::{Base64UrlSafe, Encoder, Decoder};
329///
330/// fn example() -> Result<(), ct_codecs::Error> {
331///     let data = b"Hello, world!";
332///     let encoded = Base64UrlSafe::encode_to_string(data)?;
333///     assert_eq!(encoded, "SGVsbG8sIHdvcmxkIQ==");
334///
335///     // If the input contains characters that would be escaped in URLs
336///     let binary_data = &[251, 239, 190, 222];
337///     let encoded = Base64UrlSafe::encode_to_string(binary_data)?;
338///     assert_eq!(encoded, "----3g==");
339///     Ok(())
340/// }
341/// # example().unwrap();
342/// ```
343pub struct Base64UrlSafe;
344
345/// URL-safe Base64 encoder and decoder without padding.
346///
347/// This implementation follows the URL-safe Base64 encoding variant as defined in RFC 4648,
348/// but omits padding characters ('='). This is particularly useful for URLs, where the
349/// padding character may need to be percent-encoded.
350///
351/// # Examples
352///
353/// ```
354/// use ct_codecs::{Base64UrlSafeNoPadding, Encoder, Decoder};
355///
356/// fn example() -> Result<(), ct_codecs::Error> {
357///     let data = b"Hello, world!";
358///     let encoded = Base64UrlSafeNoPadding::encode_to_string(data)?;
359///     assert_eq!(encoded, "SGVsbG8sIHdvcmxkIQ");
360///
361///     // With binary data containing characters that would be escaped in URLs
362///     let binary_data = &[251, 239, 190, 222];
363///     let encoded = Base64UrlSafeNoPadding::encode_to_string(binary_data)?;
364///     assert_eq!(encoded, "----3g");
365///     Ok(())
366/// }
367/// # example().unwrap();
368/// ```
369pub struct Base64UrlSafeNoPadding;
370
371impl Encoder for Base64 {
372    #[inline]
373    fn encoded_len(bin_len: usize) -> Result<usize, Error> {
374        Base64Impl::encoded_len(bin_len, Base64Variant::Original)
375    }
376
377    #[inline]
378    fn encode<IN: AsRef<[u8]>>(b64: &mut [u8], bin: IN) -> Result<&[u8], Error> {
379        Base64Impl::encode(b64, bin.as_ref(), Base64Variant::Original)
380    }
381}
382
383impl Decoder for Base64 {
384    #[inline]
385    fn decode<'t, IN: AsRef<[u8]>>(
386        bin: &'t mut [u8],
387        b64: IN,
388        ignore: Option<&[u8]>,
389    ) -> Result<&'t [u8], Error> {
390        Base64Impl::decode(bin, b64.as_ref(), ignore, Base64Variant::Original)
391    }
392}
393
394impl Encoder for Base64NoPadding {
395    #[inline]
396    fn encoded_len(bin_len: usize) -> Result<usize, Error> {
397        Base64Impl::encoded_len(bin_len, Base64Variant::OriginalNoPadding)
398    }
399
400    #[inline]
401    fn encode<IN: AsRef<[u8]>>(b64: &mut [u8], bin: IN) -> Result<&[u8], Error> {
402        Base64Impl::encode(b64, bin.as_ref(), Base64Variant::OriginalNoPadding)
403    }
404}
405
406impl Decoder for Base64NoPadding {
407    #[inline]
408    fn decode<'t, IN: AsRef<[u8]>>(
409        bin: &'t mut [u8],
410        b64: IN,
411        ignore: Option<&[u8]>,
412    ) -> Result<&'t [u8], Error> {
413        Base64Impl::decode(bin, b64.as_ref(), ignore, Base64Variant::OriginalNoPadding)
414    }
415}
416
417impl Encoder for Base64UrlSafe {
418    #[inline]
419    fn encoded_len(bin_len: usize) -> Result<usize, Error> {
420        Base64Impl::encoded_len(bin_len, Base64Variant::UrlSafe)
421    }
422
423    #[inline]
424    fn encode<IN: AsRef<[u8]>>(b64: &mut [u8], bin: IN) -> Result<&[u8], Error> {
425        Base64Impl::encode(b64, bin.as_ref(), Base64Variant::UrlSafe)
426    }
427}
428
429impl Decoder for Base64UrlSafe {
430    #[inline]
431    fn decode<'t, IN: AsRef<[u8]>>(
432        bin: &'t mut [u8],
433        b64: IN,
434        ignore: Option<&[u8]>,
435    ) -> Result<&'t [u8], Error> {
436        Base64Impl::decode(bin, b64.as_ref(), ignore, Base64Variant::UrlSafe)
437    }
438}
439
440impl Encoder for Base64UrlSafeNoPadding {
441    #[inline]
442    fn encoded_len(bin_len: usize) -> Result<usize, Error> {
443        Base64Impl::encoded_len(bin_len, Base64Variant::UrlSafeNoPadding)
444    }
445
446    #[inline]
447    fn encode<IN: AsRef<[u8]>>(b64: &mut [u8], bin: IN) -> Result<&[u8], Error> {
448        Base64Impl::encode(b64, bin.as_ref(), Base64Variant::UrlSafeNoPadding)
449    }
450}
451
452impl Decoder for Base64UrlSafeNoPadding {
453    #[inline]
454    fn decode<'t, IN: AsRef<[u8]>>(
455        bin: &'t mut [u8],
456        b64: IN,
457        ignore: Option<&[u8]>,
458    ) -> Result<&'t [u8], Error> {
459        Base64Impl::decode(bin, b64.as_ref(), ignore, Base64Variant::UrlSafeNoPadding)
460    }
461}
462
463#[cfg(feature = "std")]
464#[test]
465fn test_base64() {
466    let bin = [1u8, 5, 11, 15, 19, 131, 122];
467    let expected = "AQULDxODeg==";
468    let b64 = Base64::encode_to_string(bin).unwrap();
469    assert_eq!(b64, expected);
470    let bin2 = Base64::decode_to_vec(&b64, None).unwrap();
471    assert_eq!(bin, &bin2[..]);
472}
473
474#[cfg(feature = "std")]
475#[test]
476fn test_base64_mising_padding() {
477    let missing_padding = "AA";
478    assert!(Base64::decode_to_vec(missing_padding, None).is_err());
479    assert!(Base64NoPadding::decode_to_vec(missing_padding, None).is_ok());
480    let missing_padding = "AAA";
481    assert!(Base64::decode_to_vec(missing_padding, None).is_err());
482    assert!(Base64NoPadding::decode_to_vec(missing_padding, None).is_ok());
483}
484
485#[test]
486fn test_base64_no_std() {
487    let bin = [1u8, 5, 11, 15, 19, 131, 122];
488    let expected = [65, 81, 85, 76, 68, 120, 79, 68, 101, 103, 61, 61];
489    let mut b64 = [0u8; 12];
490    let b64 = Base64::encode(&mut b64, bin).unwrap();
491    assert_eq!(b64, expected);
492    let mut bin2 = [0u8; 7];
493    let bin2 = Base64::decode(&mut bin2, b64, None).unwrap();
494    assert_eq!(bin, bin2);
495}
496
497#[cfg(feature = "std")]
498#[test]
499fn test_base64_invalid_padding() {
500    let valid_padding = "AA==";
501    assert_eq!(Base64::decode_to_vec(valid_padding, None), Ok(vec![0u8; 1]));
502    let invalid_padding = "AA=";
503    assert_eq!(
504        Base64::decode_to_vec(invalid_padding, None),
505        Err(Error::InvalidInput)
506    );
507}
508
509#[cfg(feature = "std")]
510#[test]
511fn test_base64_non_canonical() {
512    assert!(Base64::decode_to_vec("AB==", None).is_err());
513    assert!(Base64NoPadding::decode_to_vec("AB", None).is_err());
514}
515
516#[cfg(feature = "std")]
517#[test]
518fn test_base64_no_padding_rejects_padding() {
519    assert!(Base64NoPadding::decode_to_vec("AA==", None).is_err());
520    assert!(Base64NoPadding::decode_to_vec("AAA=", None).is_err());
521}