1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! 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)
    }
}