Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions base64ct/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,28 @@ impl<'o, E: Variant> Encoder<'o, E> {
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(mut self) -> Result<&'o str, Error> {
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])?;
}

Ok(str::from_utf8(&self.output[..self.position])?)
let (base64, remaining) = self.output.split_at_mut(self.position);
Ok((str::from_utf8(base64)?, remaining))
}

/// Borrow the remaining data in the buffer.
Expand Down Expand Up @@ -253,7 +266,7 @@ impl LineWrapper {
fn insert_newlines(&mut self, mut buffer: &mut [u8], len: &mut usize) -> Result<(), Error> {
let mut buffer_len = *len;

if buffer_len < self.remaining {
if buffer_len <= self.remaining {
self.remaining = self
.remaining
.checked_sub(buffer_len)
Expand All @@ -267,8 +280,8 @@ impl LineWrapper {
.checked_sub(self.remaining)
.ok_or(InvalidLength)?;

// The `wrap_blocks` function should ensure the buffer is smaller than a Base64 block
debug_assert!(buffer_len < 4, "buffer exceeds 4-bytes");
// 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);

if buffer_len + self.ending.len() >= buffer.len() {
// Not enough space in buffer to add newlines
Expand Down Expand Up @@ -312,6 +325,19 @@ mod tests {
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: Variant>(input: &[u8], expected: &str, wrapped: Option<usize>) {
let mut buffer = [0u8; 1024];
Expand Down
4 changes: 2 additions & 2 deletions pem-rfc7468/src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn check_for_headers(pem: &[u8], err: Error) -> Error {
}
}

/// PEM decoder.
/// Buffered PEM decoder.
///
/// Stateful buffered decoder type which decodes an input PEM document according
/// to RFC 7468's "Strict" grammar.
Expand Down Expand Up @@ -133,7 +133,7 @@ impl<'i> Decoder<'i> {
self.base64.is_finished()
}

/// Convert into the inner [`base64::Decoder`].
/// Convert into the inner [`Base64Decoder`].
pub fn into_base64_decoder(self) -> Base64Decoder<'i> {
self.base64
}
Expand Down
203 changes: 124 additions & 79 deletions pem-rfc7468/src/encoder.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,37 @@
//! PEM encoder.

use crate::{
grammar, Error, LineEnding, Result, BASE64_WRAP_WIDTH, ENCAPSULATION_BOUNDARY_DELIMITER,
POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY,
grammar, Base64Encoder, Error, LineEnding, Result, BASE64_WRAP_WIDTH,
ENCAPSULATION_BOUNDARY_DELIMITER, POST_ENCAPSULATION_BOUNDARY, PRE_ENCAPSULATION_BOUNDARY,
};
use base64ct::{Base64, Encoding};

#[cfg(feature = "alloc")]
use alloc::string::String;

/// Encode a PEM document according to RFC 7468's "Strict" grammar.
pub fn encode<'a>(
label: &str,
pub fn encode<'o>(
type_label: &str,
line_ending: LineEnding,
input: &[u8],
buf: &'a mut [u8],
) -> Result<&'a [u8]> {
grammar::validate_label(label.as_bytes())?;

let mut buf = Buffer::new(buf, line_ending);
buf.write(PRE_ENCAPSULATION_BOUNDARY)?;
buf.write(label.as_bytes())?;
buf.writeln(ENCAPSULATION_BOUNDARY_DELIMITER)?;

for chunk in input.chunks((BASE64_WRAP_WIDTH * 3) / 4) {
buf.write_base64ln(chunk)?;
}

buf.write(POST_ENCAPSULATION_BOUNDARY)?;
buf.write(label.as_bytes())?;
buf.writeln(ENCAPSULATION_BOUNDARY_DELIMITER)?;
buf.finish()
buf: &'o mut [u8],
) -> Result<&'o [u8]> {
let mut encoder = Encoder::new(type_label, line_ending, buf)?;
encoder.encode(input)?;
let encoded_len = encoder.finish()?;
Ok(&buf[..encoded_len])
}

/// Get the length of a PEM encoded document with the given bytes and label.
pub fn encoded_len(label: &str, line_ending: LineEnding, input: &[u8]) -> usize {
// TODO(tarcieri): use checked arithmetic
PRE_ENCAPSULATION_BOUNDARY.len()
+ label.as_bytes().len()
+ ENCAPSULATION_BOUNDARY_DELIMITER.len()
+ line_ending.len()
+ input
.chunks((BASE64_WRAP_WIDTH * 3) / 4)
.fold(0, |acc, chunk| {
acc + Base64::encoded_len(chunk) + line_ending.len()
})
+ POST_ENCAPSULATION_BOUNDARY.len()
+ label.as_bytes().len()
+ ENCAPSULATION_BOUNDARY_DELIMITER.len()
+ line_ending.len()
let base64_len = input
.chunks((BASE64_WRAP_WIDTH * 3) / 4)
.fold(0, |acc, chunk| {
acc + Base64::encoded_len(chunk) + line_ending.len()
});

encoded_len_inner(label, line_ending, base64_len)
}

/// Encode a PEM document according to RFC 7468's "Strict" grammar, returning
Expand All @@ -61,66 +44,128 @@ pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Resu
String::from_utf8(buf).map_err(|_| Error::CharacterEncoding)
}

/// Output buffer for writing encoded PEM output.
struct Buffer<'a> {
/// Backing byte slice where PEM output is being written.
bytes: &'a mut [u8],
/// Buffered PEM encoder.
///
/// Stateful buffered encoder type which encodes an input PEM document according
/// to RFC 7468's "Strict" grammar.
pub struct Encoder<'l, 'o> {
/// PEM type label.
type_label: &'l str,

/// Total number of bytes written into the buffer so far.
position: usize,

/// Line ending to use
/// Line ending used to wrap Base64.
line_ending: LineEnding,

/// Buffered Base64 encoder.
base64: Base64Encoder<'o>,
}

impl<'a> Buffer<'a> {
/// Initialize buffer.
pub fn new(bytes: &'a mut [u8], line_ending: LineEnding) -> Self {
Self {
bytes,
position: 0,
line_ending,
}
impl<'l, 'o> Encoder<'l, 'o> {
/// Create a new PEM [`Encoder`] with the default options which
/// writes output into the provided buffer.
///
/// Uses the default 64-character line wrapping.
pub fn new(type_label: &'l str, line_ending: LineEnding, out: &'o mut [u8]) -> Result<Self> {
Self::new_wrapped(type_label, BASE64_WRAP_WIDTH, line_ending, out)
}

/// Write a byte slice to the buffer.
pub fn write(&mut self, slice: &[u8]) -> Result<()> {
let reserved = self.reserve(slice.len())?;
reserved.copy_from_slice(slice);
Ok(())
/// Create a new PEM [`Encoder`] which wraps at the given line width.
pub fn new_wrapped(
type_label: &'l str,
line_width: usize,
line_ending: LineEnding,
mut out: &'o mut [u8],
) -> Result<Self> {
grammar::validate_label(type_label.as_bytes())?;

for boundary_part in [
PRE_ENCAPSULATION_BOUNDARY,
type_label.as_bytes(),
ENCAPSULATION_BOUNDARY_DELIMITER,
line_ending.as_bytes(),
] {
if out.len() < boundary_part.len() {
return Err(Error::Length);
}

let (part, rest) = out.split_at_mut(boundary_part.len());
out = rest;

part.copy_from_slice(boundary_part);
}

let base64 = Base64Encoder::new_wrapped(out, line_width, line_ending)?;

Ok(Self {
type_label,
line_ending,
base64,
})
}

/// Write a byte slice to the buffer with a newline at the end.
pub fn writeln(&mut self, slice: &[u8]) -> Result<()> {
self.write(slice)?;
self.write(self.line_ending.as_bytes())
/// Get the PEM type label used for this document.
pub fn type_label(&self) -> &'l str {
self.type_label
}

/// Write Base64-encoded data to the buffer.
/// Encode the provided input data.
///
/// Automatically adds a newline at the end.
pub fn write_base64ln(&mut self, bytes: &[u8]) -> Result<()> {
let reserved = self.reserve(Base64::encoded_len(bytes))?;
Base64::encode(bytes, reserved)?;
self.write(self.line_ending.as_bytes())
/// This method can be called as many times as needed with any sized input
/// to write data encoded data into the output buffer, so long as there is
/// sufficient space in the buffer to handle the resulting Base64 encoded
/// data.
pub fn encode(&mut self, input: &[u8]) -> Result<()> {
self.base64.encode(input)?;
Ok(())
}

/// Finish writing to the buffer, returning the portion that has been
/// written to.
pub fn finish(self) -> Result<&'a [u8]> {
self.bytes.get(..self.position).ok_or(Error::Length)
/// Borrow the inner [`Base64Encoder`].
pub fn base64_encoder(&mut self) -> &mut Base64Encoder<'o> {
&mut self.base64
}

/// Reserve space in the encoding buffer, returning a mutable slice.
fn reserve(&mut self, nbytes: usize) -> Result<&mut [u8]> {
let new_position = self.position.checked_add(nbytes).ok_or(Error::Length)?;

let reserved = self
.bytes
.get_mut(self.position..new_position)
.ok_or(Error::Length)?;
/// Finish encoding PEM, writing the post-encapsulation boundary.
///
/// On success, returns the total number of bytes written to the output
/// buffer.
pub fn finish(self) -> Result<usize> {
let (base64, mut out) = self.base64.finish_with_remaining()?;

for boundary_part in [
self.line_ending.as_bytes(),
POST_ENCAPSULATION_BOUNDARY,
self.type_label.as_bytes(),
ENCAPSULATION_BOUNDARY_DELIMITER,
self.line_ending.as_bytes(),
] {
if out.len() < boundary_part.len() {
return Err(Error::Length);
}

let (part, rest) = out.split_at_mut(boundary_part.len());
out = rest;

part.copy_from_slice(boundary_part);
}

self.position = new_position;
Ok(reserved)
Ok(encoded_len_inner(
self.type_label,
self.line_ending,
base64.len() + self.line_ending.len(),
))
}
}

/// Compute the length of a PEM encoded document with a Base64-encoded body of
/// the given length.
fn encoded_len_inner(label: &str, line_ending: LineEnding, base64_len: usize) -> usize {
// TODO(tarcieri): use checked arithmetic
PRE_ENCAPSULATION_BOUNDARY.len()
+ label.as_bytes().len()
+ ENCAPSULATION_BOUNDARY_DELIMITER.len()
+ line_ending.len()
+ base64_len
+ POST_ENCAPSULATION_BOUNDARY.len()
+ label.as_bytes().len()
+ ENCAPSULATION_BOUNDARY_DELIMITER.len()
+ line_ending.len()
}
4 changes: 2 additions & 2 deletions pem-rfc7468/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

use core::fmt;

/// Result type.
/// Result type with the `pem-rfc7468` crate's [`Error`] type.
pub type Result<T> = core::result::Result<T, Error>;

/// Error type.
/// PEM errors.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Error {
Expand Down
Loading