Skip to main content

structured_base64/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
8/// Failure of a Base64 stream operation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Base64Error {
11    /// Invalid input, a finished encoder given more data, or invalid state.
12    Invalid,
13    /// The output buffer is too small for this call.
14    OutputFull,
15    /// The decoder reached the end with an incomplete group.
16    Truncated,
17}
18
19impl fmt::Display for Base64Error {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        f.write_str(match self {
22            Self::Invalid => "invalid Base64 input or state",
23            Self::OutputFull => "Base64 output buffer is too small",
24            Self::Truncated => "incomplete Base64 group",
25        })
26    }
27}
28
29impl core::error::Error for Base64Error {}
30
31/// Plain state of one Base64 stream, restorable between successful calls.
32///
33/// Each call checks the state before reading input or writing output. The
34/// fields describe a logical state, with no Rust or C layout guarantee.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct Base64Codec {
37    /// True for encoding and false for decoding.
38    pub encode: bool,
39    /// Encoded line width, or zero for a single line. Ignored by the decoder.
40    pub columns: usize,
41    /// Previous input byte (encoding) or sextet (decoding).
42    pub carry: u8,
43    /// Bytes carried by the encoder or characters seen in the decoder group.
44    pub position: usize,
45    /// Number of characters on the current encoded line. Zero for decoding.
46    pub line_position: usize,
47    /// The stream has finished, or the decoder has started reading padding.
48    pub finished: bool,
49}
50
51impl Base64Codec {
52    /// Start a stream with the specified direction and encoded line width.
53    #[must_use]
54    pub const fn new(encode: bool, columns: usize) -> Self {
55        Self {
56            encode,
57            columns,
58            carry: 0,
59            position: 0,
60            line_position: 0,
61            finished: false,
62        }
63    }
64
65    /// Upper bound for encoding a chunk and finalizing, including spare room
66    /// for a caller's terminator. Returns `None` on arithmetic overflow.
67    #[must_use]
68    pub fn encoded_capacity(columns: usize, size: usize) -> Option<usize> {
69        let symbols = (size.checked_mul(4)? / 3).checked_add(4)?;
70        let with_breaks = if let Some(breaks) = symbols.checked_div(columns) {
71            symbols.checked_add(breaks)?.checked_add(4)?
72        } else {
73            symbols
74        };
75        with_breaks.checked_add(1)
76    }
77
78    /// Upper bound for decoding a chunk, including spare room to consume
79    /// padding. Returns `None` on arithmetic overflow.
80    #[must_use]
81    pub fn decoded_capacity(size: usize) -> Option<usize> {
82        (size.checked_mul(3)? / 4).checked_add(8)
83    }
84
85    /// Consume all input and return the number of output bytes written.
86    ///
87    /// After an error, discard the stream and any output from this call.
88    /// A successful call can be followed by another with a different slice.
89    ///
90    /// # Errors
91    /// Returns [`Base64Error::Invalid`] for invalid input or state, or
92    /// [`Base64Error::OutputFull`] when the output has insufficient space.
93    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, Base64Error> {
94        self.validate()?;
95        if self.encode {
96            self.encode_input(input, output)
97        } else {
98            self.decode_input(input, output)
99        }
100    }
101
102    /// Complete the stream, writing any final encoded characters and padding.
103    ///
104    /// A nonempty output slice is required. Encoding leaves one spare byte
105    /// for the caller. Decoding returns zero and rejects an incomplete group.
106    /// After an error, discard the stream and any output from this call.
107    /// A successful call closes the stream.
108    ///
109    /// # Errors
110    /// Returns [`Base64Error::Invalid`] for invalid state,
111    /// [`Base64Error::OutputFull`] when the output has insufficient space,
112    /// or [`Base64Error::Truncated`] for an incomplete decoder group.
113    pub fn finish(&mut self, output: &mut [u8]) -> Result<usize, Base64Error> {
114        self.validate()?;
115        let (_, output) = output.split_last_mut().ok_or(Base64Error::OutputFull)?;
116        let mut written = 0;
117        if self.position != 0 {
118            if !self.encode {
119                return Err(Base64Error::Truncated);
120            }
121            let shift = if self.position == 1 { 4 } else { 2 };
122            self.emit(
123                ALPHABET[usize::from((self.carry << shift) & 63)],
124                output,
125                &mut written,
126            )?;
127            for _ in self.position..3 {
128                self.emit(b'=', output, &mut written)?;
129            }
130        }
131        self.carry = 0;
132        self.position = 0;
133        self.finished = true;
134        Ok(written)
135    }
136
137    fn validate(&self) -> Result<(), Base64Error> {
138        let valid = if self.encode {
139            self.valid_encoder()
140        } else {
141            self.line_position == 0
142                && self.carry < 64
143                && match (self.finished, self.position) {
144                    (false, 0) => self.carry == 0,
145                    (false, 1..=3) | (true, 0 | 3) => true,
146                    _ => false,
147                }
148        };
149        if valid {
150            Ok(())
151        } else {
152            Err(Base64Error::Invalid)
153        }
154    }
155
156    fn valid_encoder(&self) -> bool {
157        if self.position > 2
158            || (self.position == 0 && self.carry != 0)
159            || (self.finished && self.position != 0)
160        {
161            return false;
162        }
163        if self.columns == 0 {
164            return self.line_position % 4 == self.position;
165        }
166        if self.line_position == 0 {
167            return self.position == 0;
168        }
169        if self.line_position > self.columns {
170            return false;
171        }
172        // Each previous line contributes columns symbols. Its residue modulo
173        // four must be a multiple of gcd(columns, 4).
174        let mask = match self.columns % 4 {
175            0 => 3,
176            2 => 1,
177            _ => 0,
178        };
179        self.line_position & mask == self.position & mask
180    }
181
182    fn encode_input(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, Base64Error> {
183        if self.finished && !input.is_empty() {
184            return Err(Base64Error::Invalid);
185        }
186        let mut written = 0;
187        for &byte in input {
188            let sextet = match self.position {
189                0 => byte >> 2,
190                1 => (self.carry & 3) << 4 | byte >> 4,
191                _ => (self.carry & 15) << 2 | byte >> 6,
192            };
193            self.emit(ALPHABET[usize::from(sextet)], output, &mut written)?;
194            if self.position == 2 {
195                self.emit(ALPHABET[usize::from(byte & 63)], output, &mut written)?;
196                self.position = 0;
197                self.carry = 0;
198            } else {
199                self.position += 1;
200                self.carry = byte;
201            }
202        }
203        Ok(written)
204    }
205
206    fn emit(
207        &mut self,
208        byte: u8,
209        output: &mut [u8],
210        written: &mut usize,
211    ) -> Result<(), Base64Error> {
212        if self.columns != 0 && self.line_position == self.columns {
213            write_byte(output, written, b'\n')?;
214            self.line_position = 0;
215        }
216        let next = self
217            .line_position
218            .checked_add(1)
219            .ok_or(Base64Error::Invalid)?;
220        write_byte(output, written, byte)?;
221        self.line_position = next;
222        Ok(())
223    }
224
225    fn decode_input(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, Base64Error> {
226        let mut written = 0;
227        for &byte in input {
228            if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
229                continue;
230            }
231            // libxmlsec 1.3.13 stops decoding at a full output buffer before
232            // inspecting another non-space byte, including padding.
233            if written == output.len() {
234                return Err(Base64Error::OutputFull);
235            }
236            if byte == b'=' {
237                self.position = match (self.finished, self.position) {
238                    (false, 2) => 3,
239                    (_, 3) => 0,
240                    _ => return Err(Base64Error::Invalid),
241                };
242                self.finished = true;
243                continue;
244            }
245            if self.finished {
246                return Err(Base64Error::Invalid);
247            }
248            let value = sextet(byte).ok_or(Base64Error::Invalid)?;
249            if self.position != 0 {
250                let shift = self.position * 2;
251                write_byte(
252                    output,
253                    &mut written,
254                    (self.carry << shift) | (value >> (6 - shift)),
255                )?;
256            }
257            self.position = (self.position + 1) % 4;
258            self.carry = if self.position == 0 { 0 } else { value };
259        }
260        Ok(written)
261    }
262}
263
264fn sextet(byte: u8) -> Option<u8> {
265    Some(match byte {
266        b'A'..=b'Z' => byte - b'A',
267        b'a'..=b'z' => byte - b'a' + 26,
268        b'0'..=b'9' => byte - b'0' + 52,
269        b'+' => 62,
270        b'/' => 63,
271        _ => return None,
272    })
273}
274
275fn write_byte(output: &mut [u8], written: &mut usize, byte: u8) -> Result<(), Base64Error> {
276    *output.get_mut(*written).ok_or(Base64Error::OutputFull)? = byte;
277    *written += 1;
278    Ok(())
279}