pw_base64/
lib.rs

1// Copyright 2023 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14#![cfg_attr(not(feature = "std"), no_std)]
15#![deny(missing_docs)]
16
17//! `pw_base64` provides simple encoding of data into base64.
18//!
19//! ```
20//! const INPUT: &'static [u8] = "I 💖 Pigweed".as_bytes();
21//!
22//! // [`encoded_size`] can be used to calculate the size of the output buffer.
23//! let mut output = [0u8; pw_base64::encoded_size(INPUT.len())];
24//!
25//! // Data can be encoded to a `&mut [u8]`.
26//! let output_size = pw_base64::encode(INPUT, &mut output).unwrap();
27//! assert_eq!(&output[0..output_size], b"SSDwn5KWIFBpZ3dlZWQ=");
28//!
29//! // The output buffer can also be automatically converted to a `&str`.
30//! let output_str = pw_base64::encode_str(INPUT, &mut output).unwrap();
31//! assert_eq!(output_str, "SSDwn5KWIFBpZ3dlZWQ=");
32//! ```
33
34use pw_status::{Error, Result};
35use pw_stream::{Cursor, ReadInteger, Seek, Write};
36
37// Helper macro to make declaring the base 64 encode table more concise.
38macro_rules! b {
39    ($char:tt) => {
40        stringify!($char).as_bytes()[0]
41    };
42}
43
44// We use `u8`s in our encoding table instead of `char`s in order to avoid the
45// overhead of 1) storing each entry as 4 bytes and 2) overhead of converting
46// from `char` to `u8` while building the output.
47//
48// When constructing this table, the `b!` macro makes the assumption that
49// all the characters are a single byte in utf8.  This is true as base64
50// only outputs ASCII characters.
51#[rustfmt::skip]
52const BASE64_ENCODE_TABLE: [u8; 64] = [
53    b!(A), b!(B), b!(C), b!(D), b!(E), b!(F), b!(G), b!(H),
54    b!(I), b!(J), b!(K), b!(L), b!(M), b!(N), b!(O), b!(P),
55    b!(Q), b!(R), b!(S), b!(T), b!(U), b!(V), b!(W), b!(X),
56    b!(Y), b!(Z), b!(a), b!(b), b!(c), b!(d), b!(e), b!(f),
57    b!(g), b!(h), b!(i), b!(j), b!(k), b!(l), b!(m), b!(n),
58    b!(o), b!(p), b!(q), b!(r), b!(s), b!(t), b!(u), b!(v),
59    b!(w), b!(x), b!(y), b!(z), b!(0), b!(1), b!(2), b!(3),
60    b!(4), b!(5), b!(6), b!(7), b!(8), b!(9), b!(+), b!(/),
61];
62const BASE64_PADDING: u8 = b!(=);
63
64/// Returns the size of the output buffer needed to encode an input buffer of
65/// size `input_size`.
66pub const fn encoded_size(input_size: usize) -> usize {
67    input_size.div_ceil(3) * 4 // round up to a 3-byte group
68}
69
70// Base 64 encoding represents every 3 bytes with 4 ascii characters.  Each
71// of these 4 ascii characters represents 6 bits of data from the 3 bytes of
72// input.  The below helpers calculate each of the 4 characters form the 3 bytes
73// of input.
74const fn char_0(b: &[u8; 3]) -> u8 {
75    BASE64_ENCODE_TABLE[((b[0] & 0b11111100) >> 2) as usize]
76}
77
78const fn char_1(b: &[u8; 3]) -> u8 {
79    BASE64_ENCODE_TABLE[(((b[0] & 0b00000011) << 4) | ((b[1] & 0b11110000) >> 4)) as usize]
80}
81
82const fn char_2(b: &[u8; 3]) -> u8 {
83    BASE64_ENCODE_TABLE[(((b[1] & 0b00001111) << 2) | ((b[2] & 0b11000000) >> 6)) as usize]
84}
85
86const fn char_3(b: &[u8; 3]) -> u8 {
87    BASE64_ENCODE_TABLE[(b[2] & 0b00111111) as usize]
88}
89
90/// Encode `input` as base64 into the `output_buffer`.
91///
92/// Returns the number of bytes written to `output_buffer` on success or
93/// `Error::OutOfRange` if `output_buffer` is not large enough.
94pub fn encode(input: &[u8], output: &mut [u8]) -> Result<usize> {
95    if output.len() < encoded_size(input.len()) {
96        return Err(Error::OutOfRange);
97    }
98    let mut input = Cursor::new(input);
99    let mut output = Cursor::new(output);
100
101    let mut remaining_bytes = input.len();
102    while remaining_bytes > 0 {
103        let bytes = [
104            input.read_u8_le().unwrap_or(0),
105            input.read_u8_le().unwrap_or(0),
106            input.read_u8_le().unwrap_or(0),
107        ];
108
109        output.write(&[
110            char_0(&bytes),
111            char_1(&bytes),
112            if remaining_bytes > 1 {
113                char_2(&bytes)
114            } else {
115                BASE64_PADDING
116            },
117            if remaining_bytes > 2 {
118                char_3(&bytes)
119            } else {
120                BASE64_PADDING
121            },
122        ])?;
123        remaining_bytes = remaining_bytes.saturating_add_signed(-3);
124    }
125
126    output.stream_position().map(|len| len as usize)
127}
128
129/// Encode `input` as base64 into `output_buffer` and interprets it as a
130/// string.
131///
132/// Returns a `&str` referencing the `output_buffer` buffer on success or
133/// `Error::OutOfRange` if `output_buffer` is not large enough.
134///
135/// Using this method avoids having to do unicode checking as it can guarantee
136/// that the data written to `output_buffer` is only valid ASCII bytes.
137pub fn encode_str<'a>(input: &[u8], output_buffer: &'a mut [u8]) -> Result<&'a str> {
138    let encode_len = encode(input, output_buffer)?;
139    // Safety: Since we are building the output buffer strictly from ASCII
140    // characters, it is guaranteed to be a valid string.
141    unsafe {
142        Ok(core::str::from_utf8_unchecked(
143            &output_buffer[0..encode_len],
144        ))
145    }
146}
147
148#[cfg(test)]
149mod tests;