//! Buffered Base64 encoder. use crate::{ Encoding, Error::{self, InvalidLength}, LineEnding, MIN_LINE_WIDTH, }; use core::{cmp, marker::PhantomData, str}; #[cfg(feature = "std")] use std::io; #[cfg(doc)] use crate::{Base64, Base64Unpadded}; /// Stateful Base64 encoder with support for buffered, incremental encoding. /// /// The `E` type parameter can be any type which impls [`Encoding`] such as /// [`Base64`] or [`Base64Unpadded`]. pub struct Encoder<'o, E: Encoding> { /// Output buffer. output: &'o mut [u8], /// Cursor within the output buffer. position: usize, /// Block buffer used for non-block-aligned data. block_buffer: BlockBuffer, /// Configuration and state for line-wrapping the output at a specified /// column. line_wrapper: Option, /// Phantom parameter for the Base64 encoding in use. encoding: PhantomData, } impl<'o, E: Encoding> Encoder<'o, E> { /// Create a new encoder which writes output to the given byte slice. /// /// Output constructed using this method is not line-wrapped. pub fn new(output: &'o mut [u8]) -> Result { if output.is_empty() { return Err(InvalidLength); } Ok(Self { output, position: 0, block_buffer: BlockBuffer::default(), line_wrapper: None, encoding: PhantomData, }) } /// Create a new encoder which writes line-wrapped output to the given byte /// slice. /// /// Output will be wrapped at the specified interval, using the provided /// line ending. Use [`LineEnding::default()`] to use the conventional line /// ending for the target OS. /// /// Minimum allowed line width is 4. pub fn new_wrapped( output: &'o mut [u8], width: usize, ending: LineEnding, ) -> Result { let mut encoder = Self::new(output)?; encoder.line_wrapper = Some(LineWrapper::new(width, ending)?); Ok(encoder) } /// Encode the provided buffer as Base64, writing it to the output buffer. /// /// # Returns /// - `Ok(bytes)` if the expected amount of data was read /// - `Err(Error::InvalidLength)` if there is insufficient space in the output buffer pub fn encode(&mut self, mut input: &[u8]) -> Result<(), Error> { // If there's data in the block buffer, fill it if !self.block_buffer.is_empty() { self.process_buffer(&mut input)?; } while !input.is_empty() { // Attempt to encode a stride of block-aligned data let in_blocks = input.len() / 3; let out_blocks = self.remaining().len() / 4; let mut blocks = cmp::min(in_blocks, out_blocks); // When line wrapping, cap the block-aligned stride at near/at line length if let Some(line_wrapper) = &self.line_wrapper { line_wrapper.wrap_blocks(&mut blocks)?; } if blocks > 0 { let len = blocks.checked_mul(3).ok_or(InvalidLength)?; let (in_aligned, in_rem) = input.split_at(len); input = in_rem; self.perform_encode(in_aligned)?; } // If there's remaining non-aligned data, fill the block buffer if !input.is_empty() { self.process_buffer(&mut input)?; } } Ok(()) } /// Get the position inside of the output buffer where the write cursor /// is currently located. pub fn position(&self) -> usize { self.position } /// Finish encoding data, returning the resulting Base64 as a `str`. pub fn finish(self) -> Result<&'o str, Error> { self.finish_with_remaining().map(|(base64, _)| base64) } /// Finish encoding data, returning the resulting Base64 as a `str` /// along with the remaining space in the output buffer. pub fn finish_with_remaining(mut self) -> Result<(&'o str, &'o mut [u8]), Error> { if !self.block_buffer.is_empty() { let buffer_len = self.block_buffer.position; let block = self.block_buffer.bytes; self.perform_encode(&block[..buffer_len])?; } let (base64, remaining) = self.output.split_at_mut(self.position); Ok((str::from_utf8(base64)?, remaining)) } /// Borrow the remaining data in the buffer. fn remaining(&mut self) -> &mut [u8] { &mut self.output[self.position..] } /// Fill the block buffer with data, consuming and encoding it when the /// buffer is full. fn process_buffer(&mut self, input: &mut &[u8]) -> Result<(), Error> { self.block_buffer.fill(input)?; if self.block_buffer.is_full() { let block = self.block_buffer.take(); self.perform_encode(&block)?; } Ok(()) } /// Perform Base64 encoding operation. fn perform_encode(&mut self, input: &[u8]) -> Result { let mut len = E::encode(input, self.remaining())?.as_bytes().len(); // Insert newline characters into the output as needed if let Some(line_wrapper) = &mut self.line_wrapper { line_wrapper.insert_newlines(&mut self.output[self.position..], &mut len)?; } self.position = self.position.checked_add(len).ok_or(InvalidLength)?; Ok(len) } } #[cfg(feature = "std")] impl<'o, E: Encoding> io::Write for Encoder<'o, E> { fn write(&mut self, buf: &[u8]) -> io::Result { self.encode(buf)?; Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { // TODO(tarcieri): return an error if there's still data remaining in the buffer? Ok(()) } } /// Base64 encode buffer for a 1-block output. /// /// This handles a partial block of data, i.e. data which hasn't been #[derive(Clone, Default, Debug)] struct BlockBuffer { /// 3 decoded bytes to be encoded to a 4-byte Base64-encoded input. bytes: [u8; Self::SIZE], /// Position within the buffer. position: usize, } impl BlockBuffer { /// Size of the buffer in bytes: 3-bytes of unencoded input which /// Base64 encode to 4-bytes of output. const SIZE: usize = 3; /// Fill the remaining space in the buffer with the input data. fn fill(&mut self, input: &mut &[u8]) -> Result<(), Error> { let remaining = Self::SIZE.checked_sub(self.position).ok_or(InvalidLength)?; let len = cmp::min(input.len(), remaining); self.bytes[self.position..][..len].copy_from_slice(&input[..len]); self.position = self.position.checked_add(len).ok_or(InvalidLength)?; *input = &input[len..]; Ok(()) } /// Take the output buffer, resetting the position to 0. fn take(&mut self) -> [u8; Self::SIZE] { debug_assert!(self.is_full()); let result = self.bytes; *self = Default::default(); result } /// Is the buffer empty? fn is_empty(&self) -> bool { self.position == 0 } /// Is the buffer full? fn is_full(&self) -> bool { self.position == Self::SIZE } } /// Helper for wrapping Base64 at a given line width. #[derive(Debug)] struct LineWrapper { /// Number of bytes remaining in the current line. remaining: usize, /// Column at which Base64 should be wrapped. width: usize, /// Newline characters to use at the end of each line. ending: LineEnding, } impl LineWrapper { /// Create a new linewrapper. fn new(width: usize, ending: LineEnding) -> Result { if width < MIN_LINE_WIDTH { return Err(InvalidLength); } Ok(Self { remaining: width, width, ending, }) } /// Wrap the number of blocks to encode near/at EOL. fn wrap_blocks(&self, blocks: &mut usize) -> Result<(), Error> { if blocks.checked_mul(4).ok_or(InvalidLength)? >= self.remaining { *blocks = self.remaining / 4; } Ok(()) } /// Insert newlines into the output buffer as needed. fn insert_newlines(&mut self, mut buffer: &mut [u8], len: &mut usize) -> Result<(), Error> { let mut buffer_len = *len; if buffer_len <= self.remaining { self.remaining = self .remaining .checked_sub(buffer_len) .ok_or(InvalidLength)?; return Ok(()); } buffer = &mut buffer[self.remaining..]; buffer_len = buffer_len .checked_sub(self.remaining) .ok_or(InvalidLength)?; // The `wrap_blocks` function should ensure the buffer is no larger than a Base64 block debug_assert!(buffer_len <= 4, "buffer too long: {}", buffer_len); // Ensure space in buffer to add newlines let buffer_end = buffer_len .checked_add(self.ending.len()) .ok_or(InvalidLength)?; if buffer_end >= buffer.len() { return Err(InvalidLength); } // Shift the buffer contents to make space for the line ending for i in (0..buffer_len).rev() { buffer[i.checked_add(self.ending.len()).ok_or(InvalidLength)?] = buffer[i]; } buffer[..self.ending.len()].copy_from_slice(self.ending.as_bytes()); *len = (*len).checked_add(self.ending.len()).ok_or(InvalidLength)?; self.remaining = self.width.checked_sub(buffer_len).ok_or(InvalidLength)?; Ok(()) } } #[cfg(test)] mod tests { use crate::{alphabet::Alphabet, test_vectors::*, Base64, Base64Unpadded, Encoder, LineEnding}; #[test] fn encode_padded() { encode_test::(PADDED_BIN, PADDED_BASE64, None); } #[test] fn encode_unpadded() { encode_test::(UNPADDED_BIN, UNPADDED_BASE64, None); } #[test] fn encode_multiline_padded() { encode_test::(MULTILINE_PADDED_BIN, MULTILINE_PADDED_BASE64, Some(70)); } #[test] fn encode_multiline_unpadded() { encode_test::(MULTILINE_UNPADDED_BIN, MULTILINE_UNPADDED_BASE64, Some(70)); } #[test] fn no_trailing_newline_when_aligned() { let mut buffer = [0u8; 64]; let mut encoder = Encoder::::new_wrapped(&mut buffer, 64, LineEnding::LF).unwrap(); encoder.encode(&[0u8; 48]).unwrap(); // Ensure no newline character is present in this case assert_eq!( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", encoder.finish().unwrap() ); } /// Core functionality of an encoding test. fn encode_test(input: &[u8], expected: &str, wrapped: Option) { let mut buffer = [0u8; 1024]; for chunk_size in 1..input.len() { let mut encoder = match wrapped { Some(line_width) => { Encoder::::new_wrapped(&mut buffer, line_width, LineEnding::LF) } None => Encoder::::new(&mut buffer), } .unwrap(); for chunk in input.chunks(chunk_size) { encoder.encode(chunk).unwrap(); } assert_eq!(expected, encoder.finish().unwrap()); } } }