#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(any(feature = "std", test)))]
extern crate alloc;
#[cfg(not(any(feature = "std", test)))]
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
mod errors;
mod parser;
use parser::{parse_captures, parse_captures_iter, Captures};
pub use crate::errors::{PemError, Result};
use base64::Engine as _;
use core::fmt::Write;
use core::{fmt, slice, str};
const LINE_WRAP: usize = 64;
#[derive(Debug, Clone, Copy)]
pub enum LineEnding {
CRLF,
LF,
}
#[derive(Debug, Clone, Copy)]
pub struct EncodeConfig {
line_ending: LineEnding,
line_wrap: usize,
}
#[derive(PartialEq, Debug, Clone)]
pub struct Pem {
tag: String,
headers: HeaderMap,
contents: Vec<u8>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct HeaderMap(Vec<String>);
fn decode_data(raw_data: &str) -> Result<Vec<u8>> {
let data: String = raw_data.lines().map(str::trim_end).collect();
let contents = base64::engine::general_purpose::STANDARD
.decode(data)
.map_err(PemError::InvalidData)?;
Ok(contents)
}
#[derive(Debug)]
pub struct HeadersIter<'a> {
cur: slice::Iter<'a, String>,
}
impl<'a> Iterator for HeadersIter<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<Self::Item> {
self.cur.next().and_then(HeaderMap::split_header)
}
}
impl<'a> DoubleEndedIterator for HeadersIter<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
self.cur.next_back().and_then(HeaderMap::split_header)
}
}
impl HeaderMap {
#[allow(clippy::ptr_arg)]
fn split_header(header: &String) -> Option<(&str, &str)> {
header
.split_once(':')
.map(|(key, value)| (key.trim(), value.trim()))
}
fn parse(headers: Vec<String>) -> Result<HeaderMap> {
headers.iter().try_for_each(|hline| {
Self::split_header(hline)
.map(|_| ())
.ok_or_else(|| PemError::InvalidHeader(hline.to_string()))
})?;
Ok(HeaderMap(headers))
}
pub fn iter(&self) -> HeadersIter<'_> {
HeadersIter { cur: self.0.iter() }
}
pub fn get(&self, key: &str) -> Option<&str> {
self.iter().rev().find(|(k, _)| *k == key).map(|(_, v)| v)
}
pub fn add(&mut self, key: &str, value: &str) -> Result<()> {
ensure!(
!(key.contains(':') || key.contains('\n')),
PemError::InvalidHeader(key.to_string())
);
ensure!(
!(value.contains(':') || value.contains('\n')),
PemError::InvalidHeader(value.to_string())
);
self.0.push(format!("{}: {}", key.trim(), value.trim()));
Ok(())
}
}
impl EncodeConfig {
pub const fn new() -> Self {
Self {
line_ending: LineEnding::CRLF,
line_wrap: LINE_WRAP,
}
}
pub const fn set_line_ending(mut self, line_ending: LineEnding) -> Self {
self.line_ending = line_ending;
self
}
pub const fn set_line_wrap(mut self, line_wrap: usize) -> Self {
self.line_wrap = line_wrap;
self
}
}
impl Default for EncodeConfig {
fn default() -> Self {
Self::new()
}
}
impl Pem {
pub fn new(tag: impl ToString, contents: impl Into<Vec<u8>>) -> Pem {
Pem {
tag: tag.to_string(),
headers: HeaderMap::default(),
contents: contents.into(),
}
}
pub fn tag(&self) -> &str {
&self.tag
}
pub fn contents(&self) -> &[u8] {
&self.contents
}
pub fn into_contents(self) -> Vec<u8> {
self.contents
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
fn new_from_captures(caps: Captures) -> Result<Pem> {
fn as_utf8(bytes: &[u8]) -> Result<&str> {
str::from_utf8(bytes).map_err(PemError::NotUtf8)
}
let tag = as_utf8(caps.begin)?;
if tag.is_empty() {
return Err(PemError::MissingBeginTag);
}
let tag_end = as_utf8(caps.end)?;
if tag_end.is_empty() {
return Err(PemError::MissingEndTag);
}
if tag != tag_end {
return Err(PemError::MismatchedTags(tag.into(), tag_end.into()));
}
let raw_data = as_utf8(caps.data)?;
let contents = decode_data(raw_data)?;
let headers: Vec<String> = as_utf8(caps.headers)?.lines().map(str::to_string).collect();
let headers = HeaderMap::parse(headers)?;
let mut file = Pem::new(tag, contents);
file.headers = headers;
Ok(file)
}
}
impl str::FromStr for Pem {
type Err = PemError;
fn from_str(s: &str) -> Result<Pem> {
parse(s)
}
}
impl TryFrom<&[u8]> for Pem {
type Error = PemError;
fn try_from(s: &[u8]) -> Result<Pem> {
parse(s)
}
}
impl fmt::Display for Pem {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", encode(self))
}
}
pub fn parse<B: AsRef<[u8]>>(input: B) -> Result<Pem> {
parse_captures(input.as_ref())
.ok_or(PemError::MalformedFraming)
.and_then(Pem::new_from_captures)
}
pub fn parse_many<B: AsRef<[u8]>>(input: B) -> Result<Vec<Pem>> {
parse_captures_iter(input.as_ref())
.map(Pem::new_from_captures)
.collect()
}
pub fn encode(pem: &Pem) -> String {
encode_config(pem, EncodeConfig::default())
}
pub fn encode_config(pem: &Pem, config: EncodeConfig) -> String {
let line_ending = match config.line_ending {
LineEnding::CRLF => "\r\n",
LineEnding::LF => "\n",
};
let mut output = String::new();
let contents = if pem.contents.is_empty() {
String::from("")
} else {
base64::engine::general_purpose::STANDARD.encode(&pem.contents)
};
write!(output, "-----BEGIN {}-----{}", pem.tag, line_ending).unwrap();
if !pem.headers.0.is_empty() {
for line in &pem.headers.0 {
write!(output, "{}{}", line.trim(), line_ending).unwrap();
}
output.push_str(line_ending);
}
for c in contents.as_bytes().chunks(config.line_wrap) {
write!(output, "{}{}", str::from_utf8(c).unwrap(), line_ending).unwrap();
}
write!(output, "-----END {}-----{}", pem.tag, line_ending).unwrap();
output
}
pub fn encode_many(pems: &[Pem]) -> String {
pems.iter()
.map(encode)
.collect::<Vec<String>>()
.join("\r\n")
}
pub fn encode_many_config(pems: &[Pem], config: EncodeConfig) -> String {
let line_ending = match config.line_ending {
LineEnding::CRLF => "\r\n",
LineEnding::LF => "\n",
};
pems.iter()
.map(|value| encode_config(value, config))
.collect::<Vec<String>>()
.join(line_ending)
}
#[cfg(feature = "serde")]
mod serde_impl {
use super::{encode, parse, Pem};
use core::fmt;
use serde::{
de::{Error, Visitor},
Deserialize, Serialize,
};
impl Serialize for Pem {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&encode(self))
}
}
struct PemVisitor;
impl<'de> Visitor<'de> for PemVisitor {
type Value = Pem;
fn expecting(&self, _formatter: &mut fmt::Formatter) -> fmt::Result {
Ok(())
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
parse(v).map_err(Error::custom)
}
}
impl<'de> Deserialize<'de> for Pem {
fn deserialize<D>(deserializer: D) -> Result<Pem, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(PemVisitor)
}
}
}
#[cfg(test)]
mod test {
use super::*;
use proptest::prelude::*;
use std::error::Error;
const SAMPLE_CRLF: &str = "-----BEGIN RSA PRIVATE KEY-----\r
MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc\r
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO\r
2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei\r
AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un\r
DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT\r
TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh\r
ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ\r
-----END RSA PRIVATE KEY-----\r
\r
-----BEGIN RSA PUBLIC KEY-----\r
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo\r
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0\r
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI\r
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk\r
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6\r
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g\r
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg\r
-----END RSA PUBLIC KEY-----\r
";
const SAMPLE_LF: &str = "-----BEGIN RSA PRIVATE KEY-----
MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO
2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei
AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un
DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT
TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh
ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ
-----END RSA PRIVATE KEY-----
-----BEGIN RSA PUBLIC KEY-----
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg
-----END RSA PUBLIC KEY-----
";
const SAMPLE_DEFAULT_LINE_WRAP: &str = "-----BEGIN TEST-----\r
AQIDBA==\r
-----END TEST-----\r
";
const SAMPLE_CUSTOM_LINE_WRAP_4: &str = "-----BEGIN TEST-----\r
AQID\r
BA==\r
-----END TEST-----\r
";
#[test]
fn test_parse_works() {
let pem = parse(SAMPLE_CRLF).unwrap();
assert_eq!(pem.tag(), "RSA PRIVATE KEY");
}
#[test]
fn test_parse_invalid_framing() {
let input = "--BEGIN data-----
-----END data-----";
assert_eq!(parse(input), Err(PemError::MalformedFraming));
}
#[test]
fn test_parse_invalid_begin() {
let input = "-----BEGIN -----
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg
-----END RSA PUBLIC KEY-----";
assert_eq!(parse(input), Err(PemError::MissingBeginTag));
}
#[test]
fn test_parse_invalid_end() {
let input = "-----BEGIN DATA-----
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg
-----END -----";
assert_eq!(parse(input), Err(PemError::MissingEndTag));
}
#[test]
fn test_parse_invalid_data() {
let input = "-----BEGIN DATA-----
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oY?
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg
-----END DATA-----";
match parse(input) {
Err(e @ PemError::InvalidData(_)) => {
assert_eq!(
&format!("{}", e.source().unwrap()),
"Invalid byte 63, offset 63."
);
}
_ => unreachable!(),
}
}
#[test]
fn test_parse_empty_data() {
let input = "-----BEGIN DATA-----
-----END DATA-----";
let pem = parse(input).unwrap();
assert_eq!(pem.contents().len(), 0);
}
#[test]
fn test_parse_many_works() {
let pems = parse_many(SAMPLE_CRLF).unwrap();
assert_eq!(pems.len(), 2);
assert_eq!(pems[0].tag(), "RSA PRIVATE KEY");
assert_eq!(pems[1].tag(), "RSA PUBLIC KEY");
}
#[test]
fn test_parse_many_errors_on_invalid_section() {
let input = SAMPLE_LF.to_owned() + "-----BEGIN -----\n-----END -----";
assert_eq!(parse_many(input), Err(PemError::MissingBeginTag));
}
#[test]
fn test_encode_default_line_wrap() {
let pem = Pem::new("TEST", vec![1, 2, 3, 4]);
assert_eq!(encode(&pem), SAMPLE_DEFAULT_LINE_WRAP);
}
#[test]
fn test_encode_custom_line_wrap_4() {
let pem = Pem::new("TEST", vec![1, 2, 3, 4]);
assert_eq!(
encode_config(&pem, EncodeConfig::default().set_line_wrap(4)),
SAMPLE_CUSTOM_LINE_WRAP_4
);
}
#[test]
fn test_encode_empty_contents() {
let pem = Pem::new("FOO", vec![]);
let encoded = encode(&pem);
assert!(!encoded.is_empty());
let pem_out = parse(&encoded).unwrap();
assert_eq!(&pem, &pem_out);
}
#[test]
fn test_encode_contents() {
let pem = Pem::new("FOO", [1, 2, 3, 4]);
let encoded = encode(&pem);
assert!(!encoded.is_empty());
let pem_out = parse(&encoded).unwrap();
assert_eq!(&pem, &pem_out);
}
#[test]
fn test_encode_many() {
let pems = parse_many(SAMPLE_CRLF).unwrap();
let encoded = encode_many(&pems);
assert_eq!(SAMPLE_CRLF, encoded);
}
#[test]
fn test_encode_config_contents() {
let pem = Pem::new("FOO", [1, 2, 3, 4]);
let config = EncodeConfig::default().set_line_ending(LineEnding::LF);
let encoded = encode_config(&pem, config);
assert!(!encoded.is_empty());
let pem_out = parse(&encoded).unwrap();
assert_eq!(&pem, &pem_out);
}
#[test]
fn test_encode_many_config() {
let pems = parse_many(SAMPLE_LF).unwrap();
let config = EncodeConfig::default().set_line_ending(LineEnding::LF);
let encoded = encode_many_config(&pems, config);
assert_eq!(SAMPLE_LF, encoded);
}
#[cfg(feature = "serde")]
#[test]
fn test_serde() {
let pem = Pem::new("Mock tag", "Mock contents".as_bytes());
let value = serde_json::to_string_pretty(&pem).unwrap();
let result = serde_json::from_str(&value).unwrap();
assert_eq!(pem, result);
}
const HEADER_CRLF: &str = "-----BEGIN CERTIFICATE-----\r
MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc\r
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO\r
2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei\r
AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un\r
DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT\r
TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh\r
ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ\r
-----END CERTIFICATE-----\r
-----BEGIN RSA PRIVATE KEY-----\r
Proc-Type: 4,ENCRYPTED\r
DEK-Info: AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6\r
\r
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo\r
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0\r
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI\r
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk\r
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6\r
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g\r
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg\r
-----END RSA PRIVATE KEY-----\r
";
const HEADER_CRLF_DATA: [&str; 2] = [
"MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc\r
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO\r
2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei\r
AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un\r
DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT\r
TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh\r
ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ\r",
"MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo\r
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0\r
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI\r
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk\r
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6\r
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g\r
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg\r",
];
const HEADER_LF: &str = "-----BEGIN CERTIFICATE-----
MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO
2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei
AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un
DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT
TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh
ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg
-----END RSA PRIVATE KEY-----
";
const HEADER_LF_DATA: [&str; 2] = [
"MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO
2gkT00AWTSzM9Zns0HedY31yEabkuFvrMCHjscEF7u3Y6PB7An3IzooBHchsFDei
AAECIQD/JahddzR5K3A6rzTidmAf1PBtqi7296EnWv8WvpfAAQIhAOvowIXZI4Un
DXjgZ9ekuUjZN+GUQRAVlkEEohGLVy59AiEA90VtqDdQuWWpvJX0cM08V10tLXrT
TTGsEtITid1ogAECIQDAaFl90ZgS5cMrL3wCeatVKzVUmuJmB/VAmlLFFGzK0QIh
ANJGc7AFk4fyFD/OezhwGHbWmo/S+bfeAiIh2Ss2FxKJ",
"MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg",
];
fn cmp_data(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
false
} else {
left.iter()
.zip(right.iter())
.all(|(left, right)| left == right)
}
}
#[test]
fn test_parse_many_with_headers_crlf() {
let pems = parse_many(HEADER_CRLF).unwrap();
assert_eq!(pems.len(), 2);
assert_eq!(pems[0].tag(), "CERTIFICATE");
assert!(cmp_data(
pems[0].contents(),
&decode_data(HEADER_CRLF_DATA[0]).unwrap()
));
assert_eq!(pems[1].tag(), "RSA PRIVATE KEY");
assert!(cmp_data(
pems[1].contents(),
&decode_data(HEADER_CRLF_DATA[1]).unwrap()
));
}
#[test]
fn test_parse_many_with_headers_lf() {
let pems = parse_many(HEADER_LF).unwrap();
assert_eq!(pems.len(), 2);
assert_eq!(pems[0].tag(), "CERTIFICATE");
assert!(cmp_data(
pems[0].contents(),
&decode_data(HEADER_LF_DATA[0]).unwrap()
));
assert_eq!(pems[1].tag(), "RSA PRIVATE KEY");
assert!(cmp_data(
pems[1].contents(),
&decode_data(HEADER_LF_DATA[1]).unwrap()
));
}
proptest! {
#[test]
fn test_str_parse_and_display(tag in "[A-Z ]+", contents in prop::collection::vec(0..255u8, 0..200)) {
let pem = Pem::new(tag, contents);
prop_assert_eq!(&pem, &pem.to_string().parse::<Pem>().unwrap());
}
#[test]
fn test_str_parse_and_display_with_headers(tag in "[A-Z ]+",
key in "[a-zA-Z]+",
value in "[a-zA-A]+",
contents in prop::collection::vec(0..255u8, 0..200)) {
let mut pem = Pem::new(tag, contents);
pem.headers_mut().add(&key, &value).unwrap();
prop_assert_eq!(&pem, &pem.to_string().parse::<Pem>().unwrap());
}
}
#[test]
fn test_extract_headers() {
let pems = parse_many(HEADER_CRLF).unwrap();
let headers = pems[1].headers().iter().collect::<Vec<_>>();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0].0, "Proc-Type");
assert_eq!(headers[0].1, "4,ENCRYPTED");
assert_eq!(headers[1].0, "DEK-Info");
assert_eq!(headers[1].1, "AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6");
let headers = pems[1].headers().iter().rev().collect::<Vec<_>>();
assert_eq!(headers.len(), 2);
assert_eq!(headers[1].0, "Proc-Type");
assert_eq!(headers[1].1, "4,ENCRYPTED");
assert_eq!(headers[0].0, "DEK-Info");
assert_eq!(headers[0].1, "AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6");
}
#[test]
fn test_get_header() {
let pems = parse_many(HEADER_CRLF).unwrap();
let headers = pems[1].headers();
assert_eq!(headers.get("Proc-Type"), Some("4,ENCRYPTED"));
assert_eq!(
headers.get("DEK-Info"),
Some("AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6")
);
}
#[test]
fn test_only_get_latest() {
const LATEST: &str = "-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6
Proc-Type: 42,DECRYPTED
MIIBOgIBAAJBAMIeCnn9G/7g2Z6J+qHOE2XCLLuPoh5NHTO2Fm+PbzBvafBo0oYo
QVVy7frzxmOqx6iIZBxTyfAQqBPO3Br59BMCAwEAAQJAX+PjHPuxdqiwF6blTkS0
RFI1MrnzRbCmOkM6tgVO0cd6r5Z4bDGLusH9yjI9iI84gPRjK0AzymXFmBGuREHI
sQIhAPKf4pp+Prvutgq2ayygleZChBr1DC4XnnufBNtaswyvAiEAzNGVKgNvzuhk
ijoUXIDruJQEGFGvZTsi1D2RehXiT90CIQC4HOQUYKCydB7oWi1SHDokFW2yFyo6
/+lf3fgNjPI6OQIgUPmTFXciXxT1msh3gFLf3qt2Kv8wbr9Ad9SXjULVpGkCIB+g
RzHX0lkJl9Stshd/7Gbt65/QYq+v+xvAeT0CoyIg
-----END RSA PRIVATE KEY-----
";
let pem = parse(LATEST).unwrap();
let headers = pem.headers();
assert_eq!(headers.get("Proc-Type"), Some("42,DECRYPTED"));
assert_eq!(
headers.get("DEK-Info"),
Some("AES-256-CBC,975C518B7D2CCD1164A3354D1F89C5A6")
);
}
}