blob: 6fe30503df72942357f8fbbe88475e286f90fdda [file] [log] [blame]
//! PEM encoder.
use crate::{
grammar::{self, CHAR_CR, CHAR_LF},
Error, 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,
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()
}
/// 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()
}
/// Encode a PEM document according to RFC 7468's "Strict" grammar, returning
/// the result as a [`String`].
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn encode_string(label: &str, line_ending: LineEnding, input: &[u8]) -> Result<String> {
let mut buf = vec![0u8; encoded_len(label, line_ending, input)];
encode(label, line_ending, input, &mut buf)?;
String::from_utf8(buf).map_err(|_| Error::CharacterEncoding)
}
/// Line endings.
///
/// Use [`LineEnding::default`] to get an appropriate line ending for the
/// current operating system.
#[allow(clippy::upper_case_acronyms)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum LineEnding {
/// Carriage return: `\r` (Pre-OS X Macintosh)
CR,
/// Line feed: `\n` (Unix OSes)
LF,
/// Carriage return + line feed: `\r\n` (Windows)
CRLF,
}
impl Default for LineEnding {
/// Use the line ending for the current OS
#[cfg(windows)]
fn default() -> LineEnding {
LineEnding::CRLF
}
#[cfg(not(windows))]
fn default() -> LineEnding {
LineEnding::LF
}
}
#[allow(clippy::len_without_is_empty)]
impl LineEnding {
/// Get the byte serialization of this [`LineEnding`].
pub fn as_bytes(self) -> &'static [u8] {
match self {
LineEnding::CR => &[CHAR_CR],
LineEnding::LF => &[CHAR_LF],
LineEnding::CRLF => &[CHAR_CR, CHAR_LF],
}
}
/// Get the encoded length of this [`LineEnding`].
pub fn len(self) -> usize {
self.as_bytes().len()
}
}
/// Output buffer for writing encoded PEM output.
struct Buffer<'a> {
/// Backing byte slice where PEM output is being written.
bytes: &'a mut [u8],
/// Total number of bytes written into the buffer so far.
position: usize,
/// Line ending to use
line_ending: LineEnding,
}
impl<'a> Buffer<'a> {
/// Initialize buffer.
pub fn new(bytes: &'a mut [u8], line_ending: LineEnding) -> Self {
Self {
bytes,
position: 0,
line_ending,
}
}
/// 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(())
}
/// 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())
}
/// Write Base64-encoded data to the buffer.
///
/// 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())
}
/// 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)
}
/// 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)?;
self.position = new_position;
Ok(reserved)
}
}