diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/base64ct/src/encoder.rs | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/base64ct/src/encoder.rs')
-rw-r--r-- | vendor/base64ct/src/encoder.rs | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/vendor/base64ct/src/encoder.rs b/vendor/base64ct/src/encoder.rs new file mode 100644 index 000000000..74d457efc --- /dev/null +++ b/vendor/base64ct/src/encoder.rs @@ -0,0 +1,364 @@ +//! 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<LineWrapper>, + + /// Phantom parameter for the Base64 encoding in use. + encoding: PhantomData<E>, +} + +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<Self, Error> { + 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<Self, Error> { + 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<usize, Error> { + 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")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl<'o, E: Encoding> io::Write for Encoder<'o, E> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + 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<Self, Error> { + 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::<Base64>(PADDED_BIN, PADDED_BASE64, None); + } + + #[test] + fn encode_unpadded() { + encode_test::<Base64Unpadded>(UNPADDED_BIN, UNPADDED_BASE64, None); + } + + #[test] + fn encode_multiline_padded() { + encode_test::<Base64>(MULTILINE_PADDED_BIN, MULTILINE_PADDED_BASE64, Some(70)); + } + + #[test] + fn encode_multiline_unpadded() { + encode_test::<Base64Unpadded>(MULTILINE_UNPADDED_BIN, MULTILINE_UNPADDED_BASE64, Some(70)); + } + + #[test] + fn no_trailing_newline_when_aligned() { + let mut buffer = [0u8; 64]; + let mut encoder = Encoder::<Base64>::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<V: Alphabet>(input: &[u8], expected: &str, wrapped: Option<usize>) { + let mut buffer = [0u8; 1024]; + + for chunk_size in 1..input.len() { + let mut encoder = match wrapped { + Some(line_width) => { + Encoder::<V>::new_wrapped(&mut buffer, line_width, LineEnding::LF) + } + None => Encoder::<V>::new(&mut buffer), + } + .unwrap(); + + for chunk in input.chunks(chunk_size) { + encoder.encode(chunk).unwrap(); + } + + assert_eq!(expected, encoder.finish().unwrap()); + } + } +} |