Skip to main content

tsumiki_pem/
lib.rs

1//! # tsumiki-pem
2//!
3//! PEM (Privacy Enhanced Mail) format handling for cryptographic data.
4//!
5//! This crate implements [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468)
6//! for encoding and decoding PEM format.
7//!
8//! ## What is PEM?
9//!
10//! PEM is a text-based format for encoding binary data. It wraps base64-encoded
11//! data with boundary markers like:
12//! ```text
13//! -----BEGIN CERTIFICATE-----
14//! MIIBkTCB+wIJAKHHCgVZU...
15//! -----END CERTIFICATE-----
16//! ```
17//!
18//! ## Supported Labels
19//!
20//! - `CERTIFICATE` - X.509 certificates
21//! - `PRIVATE KEY` - PKCS#8 private keys
22//! - `ENCRYPTED PRIVATE KEY` - PKCS#8 encrypted private keys
23//! - `RSA PRIVATE KEY` - PKCS#1 RSA private keys
24//! - `EC PRIVATE KEY` - SEC1 EC private keys
25//! - `PUBLIC KEY` - X.509 SubjectPublicKeyInfo
26//! - `RSA PUBLIC KEY` - PKCS#1 RSA public keys
27//!
28//! ## Example
29//!
30//! ```
31//! use std::str::FromStr;
32//! use tsumiki_pem::{Pem, Label};
33//!
34//! let pem_text = "-----BEGIN CERTIFICATE-----\nAAA=\n-----END CERTIFICATE-----";
35//! let pem = Pem::from_str(pem_text)?;
36//!
37//! assert_eq!(pem.label(), Label::Certificate);
38//! # Ok::<(), Box<dyn std::error::Error>>(())
39//! ```
40//!
41//! ## Decoding to Raw Bytes
42//!
43//! ```
44//! use std::str::FromStr;
45//! use tsumiki::decoder::Decoder;
46//! use tsumiki_pem::Pem;
47//!
48//! let pem_text = "-----BEGIN CERTIFICATE-----\nAAA=\n-----END CERTIFICATE-----";
49//! let pem = Pem::from_str(pem_text)?;
50//!
51//! // Decode to raw bytes
52//! let bytes: Vec<u8> = pem.decode()?;
53//! # Ok::<(), Box<dyn std::error::Error>>(())
54//! ```
55
56#![forbid(unsafe_code)]
57
58pub mod error;
59
60use std::{
61    fmt::{Display, Formatter},
62    str::FromStr,
63};
64
65use base64::{Engine, engine::general_purpose::STANDARD};
66use error::Error;
67use regex::Regex;
68use tsumiki::decoder::{DecodableFrom, Decoder};
69
70const PRIVATE_KEY_LABEL: &str = "PRIVATE KEY";
71const ENCRYPTED_PRIVATE_KEY_LABEL: &str = "ENCRYPTED PRIVATE KEY";
72const RSA_PRIVATE_KEY_LABEL: &str = "RSA PRIVATE KEY";
73const EC_PRIVATE_KEY_LABEL: &str = "EC PRIVATE KEY";
74const PUBLIC_KEY_LABEL: &str = "PUBLIC KEY";
75const RSA_PUBLIC_KEY_LABEL: &str = "RSA PUBLIC KEY";
76const CERTIFICATE_LABEL: &str = "CERTIFICATE";
77
78/// PEM label identifying the type of data enclosed.
79///
80/// Labels appear in the boundary markers of PEM format:
81/// ```text
82/// -----BEGIN <LABEL>-----
83/// ...
84/// -----END <LABEL>-----
85/// ```
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Label {
88    /// PKCS#8 private key (non-encrypted)
89    PrivateKey,
90    /// PKCS#8 encrypted private key
91    EncryptedPrivateKey,
92    /// PKCS#1 RSA private key
93    RSAPrivateKey,
94    /// SEC1 EC private key
95    ECPrivateKey,
96    /// X.509 SubjectPublicKeyInfo
97    PublicKey,
98    /// PKCS#1 RSA public key
99    RSAPublicKey,
100    /// X.509 Certificate
101    Certificate,
102    /// Unknown or unrecognized label (for internal use)
103    Unknown,
104}
105
106impl Display for Label {
107    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Label::PrivateKey => write!(f, "{}", PRIVATE_KEY_LABEL),
110            Label::EncryptedPrivateKey => write!(f, "{}", ENCRYPTED_PRIVATE_KEY_LABEL),
111            Label::RSAPrivateKey => write!(f, "{}", RSA_PRIVATE_KEY_LABEL),
112            Label::ECPrivateKey => write!(f, "{}", EC_PRIVATE_KEY_LABEL),
113            Label::PublicKey => write!(f, "{}", PUBLIC_KEY_LABEL),
114            Label::RSAPublicKey => write!(f, "{}", RSA_PUBLIC_KEY_LABEL),
115            Label::Certificate => write!(f, "{}", CERTIFICATE_LABEL),
116            Label::Unknown => write!(f, "UNKNOWN"),
117        }
118    }
119}
120
121impl FromStr for Label {
122    type Err = Error;
123
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        match s {
126            PRIVATE_KEY_LABEL => Ok(Label::PrivateKey),
127            ENCRYPTED_PRIVATE_KEY_LABEL => Ok(Label::EncryptedPrivateKey),
128            RSA_PRIVATE_KEY_LABEL => Ok(Label::RSAPrivateKey),
129            EC_PRIVATE_KEY_LABEL => Ok(Label::ECPrivateKey),
130            PUBLIC_KEY_LABEL => Ok(Label::PublicKey),
131            RSA_PUBLIC_KEY_LABEL => Ok(Label::RSAPublicKey),
132            CERTIFICATE_LABEL => Ok(Label::Certificate),
133            _ => Err(Error::InvalidLabel),
134        }
135    }
136}
137
138impl Label {
139    fn get_label(line: &str) -> Result<Label, Error> {
140        let re = Regex::new(r"-----(?:BEGIN|END) ([A-Z ]+)-----\s*")
141            .map_err(|_| Error::InvalidEncapsulationBoundary)?;
142        if let Some(captured) = re.captures(line) {
143            if captured.len() != 2 {
144                return Err(Error::InvalidEncapsulationBoundary);
145            }
146            return captured
147                .get(1)
148                .ok_or(Error::InvalidEncapsulationBoundary)
149                .map(|c| Label::from_str(c.as_str()))?;
150        }
151
152        Err(Error::InvalidEncapsulationBoundary)
153    }
154}
155
156/// A PEM-encoded data structure.
157///
158/// Represents data in PEM (Privacy Enhanced Mail) format, consisting of
159/// a label and base64-encoded content wrapped in boundary markers.
160///
161/// # Example
162/// ```
163/// use tsumiki_pem::{Pem, Label};
164/// use std::str::FromStr;
165///
166/// let pem_text = "-----BEGIN CERTIFICATE-----\nAAA=\n-----END CERTIFICATE-----";
167/// let pem = Pem::from_str(pem_text)?;
168/// assert_eq!(pem.label(), Label::Certificate);
169/// # Ok::<(), Box<dyn std::error::Error>>(())
170/// ```
171///
172/// Reference: [RFC 7468](https://www.rfc-editor.org/rfc/rfc7468.html#section-3)
173#[derive(Debug, Clone)]
174pub struct Pem {
175    label: Label,
176    base64_data: String, // base64 encoded data
177}
178
179impl Pem {
180    /// Creates a new PEM structure from a label and base64-encoded data.
181    ///
182    /// # Example
183    /// ```
184    /// use tsumiki_pem::{Pem, Label};
185    ///
186    /// let pem = Pem::new(Label::Certificate, "AAAA".to_string());
187    /// assert_eq!(pem.label(), Label::Certificate);
188    /// ```
189    pub fn new(label: Label, base64_data: String) -> Self {
190        Pem { label, base64_data }
191    }
192
193    /// Creates a PEM structure from raw bytes, encoding them as base64.
194    ///
195    /// # Example
196    /// ```
197    /// use tsumiki_pem::{Pem, Label};
198    ///
199    /// let data = vec![0u8, 1, 2, 3];
200    /// let pem = Pem::from_bytes(Label::Certificate, &data);
201    /// ```
202    pub fn from_bytes(label: Label, data: &[u8]) -> Self {
203        let base64_data = STANDARD.encode(data);
204        Pem { label, base64_data }
205    }
206
207    /// Returns the PEM label identifying the type of data.
208    pub fn label(&self) -> Label {
209        self.label
210    }
211
212    /// Returns the base64-encoded data as a string.
213    pub fn data(&self) -> &str {
214        &self.base64_data
215    }
216}
217
218impl Display for Pem {
219    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
220        writeln!(f, "-----BEGIN {}-----", self.label)?;
221        // RFC 7468: base64 text should be wrapped at 64 characters
222        for chunk in self.base64_data.as_bytes().chunks(64) {
223            let line = std::str::from_utf8(chunk).map_err(|_| std::fmt::Error)?;
224            writeln!(f, "{}", line)?;
225        }
226        write!(f, "-----END {}-----", self.label)
227    }
228}
229
230/// Trait for types that can be converted to PEM format
231pub trait ToPem {
232    /// The error type returned by to_pem
233    type Error;
234
235    /// Get the PEM label for this type
236    fn pem_label(&self) -> Label;
237
238    /// Convert to PEM format
239    fn to_pem(&self) -> Result<Pem, Self::Error>;
240}
241
242/// Trait for types that can be constructed from PEM format
243pub trait FromPem: Sized {
244    /// The error type returned by from_pem
245    type Error;
246
247    /// Get the expected PEM label for this type
248    fn expected_label() -> Label;
249
250    /// Construct from PEM format
251    fn from_pem(pem: &Pem) -> Result<Self, Self::Error>;
252}
253
254impl DecodableFrom<Pem> for Vec<u8> {}
255
256impl Decoder<Pem, Vec<u8>> for Pem {
257    type Error = Error;
258
259    fn decode(&self) -> Result<Vec<u8>, Self::Error> {
260        // This discards label information from Pem format.
261        let decoded = STANDARD.decode(self.data()).map_err(Error::Base64Decode)?;
262        Ok(decoded)
263    }
264}
265
266impl DecodableFrom<String> for Pem {}
267
268impl Decoder<String, Pem> for String {
269    type Error = Error;
270
271    fn decode(&self) -> Result<Pem, Self::Error> {
272        Pem::from_str(self)
273    }
274}
275
276impl DecodableFrom<&str> for Pem {}
277
278impl Decoder<&str, Pem> for &str {
279    type Error = Error;
280
281    fn decode(&self) -> Result<Pem, Self::Error> {
282        Pem::from_str(self)
283    }
284}
285
286/// Parse multiple PEM blocks from a string.
287///
288/// Returns a vector of all PEM blocks found in the input.
289/// This is useful for parsing certificate chains or files containing
290/// multiple certificates/keys.
291///
292/// # Example
293/// ```
294/// use tsumiki_pem::parse_many;
295///
296/// let pem_data = "-----BEGIN CERTIFICATE-----\nAAA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nBBB=\n-----END CERTIFICATE-----";
297/// let pems = parse_many(pem_data).unwrap();
298/// assert_eq!(pems.len(), 2);
299/// ```
300pub fn parse_many(s: &str) -> Result<Vec<Pem>, Error> {
301    // Normalize input: ensure each boundary marker is on its own line
302    let normalized = s.replace("----------", "-----\n-----");
303
304    let mut pems = Vec::new();
305    let mut current_block: Option<(Label, Vec<&str>)> = None;
306
307    for line in normalized.lines() {
308        if let Ok(label) = Label::get_label(line) {
309            if line.contains("BEGIN") {
310                // Start a new block
311                current_block = Some((label, vec![line]));
312            } else if line.contains("END") {
313                // End current block
314                if let Some((begin_label, mut lines)) = current_block.take() {
315                    if begin_label == label {
316                        lines.push(line);
317                        let block = lines.join("\n") + "\n";
318                        pems.push(Pem::from_str(&block)?);
319                    } else {
320                        return Err(Error::LabelMissMatch);
321                    }
322                } else {
323                    return Err(Error::MissingPreEncapsulationBoundary);
324                }
325            }
326        } else if let Some((_, ref mut lines)) = current_block {
327            // Inside a block, collect data lines
328            lines.push(line);
329        }
330        // Ignore lines outside of PEM blocks
331    }
332
333    if current_block.is_some() {
334        return Err(Error::MissingPostEncapsulationBoundary);
335    }
336
337    if pems.is_empty() {
338        return Err(Error::MissingPreEncapsulationBoundary);
339    }
340
341    Ok(pems)
342}
343
344impl FromStr for Pem {
345    type Err = Error;
346
347    fn from_str(s: &str) -> Result<Self, Self::Err> {
348        let mut state = PemParsingState::default();
349        let mut label = Label::Unknown;
350        let mut base64_lines = vec![];
351        let mut base64_finl_lines = vec![];
352        let mut lines = s.lines();
353        loop {
354            match state {
355                PemParsingState::Init => match lines.next() {
356                    Some(line) => {
357                        if line.is_empty() {
358                            return Err(Error::MissingPreEncapsulationBoundary);
359                        }
360                        if let Ok(l) = Label::get_label(line) {
361                            label = l;
362                            state = PemParsingState::PreEncapsulationBoundary;
363                        } else {
364                            // TODO: correctly handle explanatory text
365                            // https://www.rfc-editor.org/rfc/rfc7468.html#section-5.2
366                            // Now. we simply ignore explanatory text.
367                        }
368                    }
369                    None => return Err(Error::MissingPreEncapsulationBoundary),
370                },
371                PemParsingState::PreEncapsulationBoundary => match lines.next() {
372                    Some(line) => {
373                        if line.is_empty() || Label::get_label(line).is_ok() {
374                            return Err(Error::MissingData);
375                        }
376                        if is_base64_finl(line) {
377                            base64_finl_lines.push(line);
378                            state = PemParsingState::Base64Finl;
379                        } else {
380                            base64_lines.push(line);
381                            state = PemParsingState::Base64Lines;
382                        }
383                    }
384                    None => return Err(Error::MissingData),
385                },
386                PemParsingState::Base64Lines => match lines.next() {
387                    Some(line) => {
388                        if line.is_empty() {
389                            return Err(Error::InvalidBase64Line);
390                        }
391                        if let Ok(l) = Label::get_label(line) {
392                            // reach postdb
393                            if l.ne(&label) {
394                                return Err(Error::LabelMissMatch);
395                            }
396                            state = PemParsingState::PostEncapsulationBoundary;
397                        } else if is_base64_finl(line) {
398                            base64_finl_lines.push(line);
399                            state = PemParsingState::Base64Finl;
400                        } else {
401                            base64_lines.push(line);
402                        }
403                    }
404                    None => return Err(Error::MissingPostEncapsulationBoundary),
405                },
406                PemParsingState::Base64Finl => match lines.next() {
407                    Some(line) => {
408                        if line.is_empty() {
409                            return Err(Error::InvalidBase64Finl);
410                        }
411                        if let Ok(l) = Label::get_label(line) {
412                            // reach postdb
413                            if l.ne(&label) {
414                                return Err(Error::LabelMissMatch);
415                            }
416                            state = PemParsingState::PostEncapsulationBoundary;
417                        } else {
418                            if !is_base64_finl(line) {
419                                return Err(Error::InvalidBase64Finl);
420                            }
421                            base64_finl_lines.push(line);
422                        }
423                    }
424                    None => return Err(Error::MissingPostEncapsulationBoundary),
425                },
426                PemParsingState::PostEncapsulationBoundary => break,
427            }
428        }
429        let finl = base64_finl(&base64_finl_lines)?;
430        base64_lines.push(&finl);
431
432        Ok(Pem {
433            label,
434            base64_data: base64_lines.join(""),
435        })
436    }
437}
438
439/*
440* pre-eb ->          base64finl -> post-eb
441*        -> base64lines-|---------->
442*            |_|
443 */
444#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
445enum PemParsingState {
446    #[default]
447    Init,
448    PreEncapsulationBoundary,
449    Base64Lines,
450    Base64Finl,
451    PostEncapsulationBoundary,
452}
453
454fn base64_finl(lines: &[&str]) -> Result<String, Error> {
455    // base64finl = *base64char (base64pad *WSP eol base64pad / *2base64pad) *WSP eol
456    // exp-1)
457    // ..AB=\s\s\s\n
458    // =\s\s\n
459    // exp-2)
460    // ..AB==\s\s\n
461    if lines.iter().any(|l| l.is_empty()) {
462        return Err(Error::InvalidBase64Finl);
463    }
464    let lines = lines.iter().map(|l| l.trim()).collect::<Vec<&str>>();
465    let content = lines.join("");
466    Ok(content)
467}
468
469fn is_base64_finl(line: &str) -> bool {
470    if line.contains("=") {
471        return true;
472    }
473    false
474}
475
476#[cfg(test)]
477mod tests {
478    use rstest::rstest;
479
480    use crate::Error;
481    use crate::Label;
482    use crate::Pem;
483    use std::str::FromStr;
484    use tsumiki::decoder::Decoder;
485
486    #[rstest(
487        input,
488        expected,
489        case("-----BEGIN PRIVATE KEY-----", Label::PrivateKey),
490        case("-----END PUBLIC KEY-----", Label::PublicKey),
491        case("-----END PUBLIC KEY-----     ", Label::PublicKey),
492        case("-----END PUBLIC KEY-----  ", Label::PublicKey)
493    )]
494    fn test_get_label(input: &str, expected: Label) {
495        let got = Label::get_label(input).unwrap();
496        assert_eq!(expected, got);
497    }
498
499    const TEST_PEM1: &str = r"-----BEGIN PRIVATE KEY-----
500AAA
501-----END PRIVATE KEY-----
502";
503    const TEST_PEM2: &str = r"-----BEGIN PRIVATE KEY-----
504AAA
505BBB==
506-----END PRIVATE KEY-----
507";
508    const TEST_PEM3: &str = r"-----BEGIN PRIVATE KEY-----
509AAA
510BBB=
511=
512-----END PRIVATE KEY-----
513";
514    const TEST_PEM4: &str = r"Subject: CN=Atlantis
515Issuer: CN=Atlantis
516-----BEGIN PRIVATE KEY-----
517AAA=
518-----END PRIVATE KEY-----
519";
520    const TEST_PEM_CERT1: &str = r"-----BEGIN CERTIFICATE-----
521MIICLDCCAdKgAwIBAgIBADAKBggqhkjOPQQDAjB9MQswCQYDVQQGEwJCRTEPMA0G
522A1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2VydGlmaWNhdGUgYXV0aG9y
523aXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdudVRMUyBjZXJ0aWZpY2F0
524ZSBhdXRob3JpdHkwHhcNMTEwNTIzMjAzODIxWhcNMTIxMjIyMDc0MTUxWjB9MQsw
525CQYDVQQGEwJCRTEPMA0GA1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2Vy
526dGlmaWNhdGUgYXV0aG9yaXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdu
527dVRMUyBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMB
528BwNCAARS2I0jiuNn14Y2sSALCX3IybqiIJUvxUpj+oNfzngvj/Niyv2394BWnW4X
529uQ4RTEiywK87WRcWMGgJB5kX/t2no0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud
530DwEB/wQFAwMHBgAwHQYDVR0OBBYEFPC0gf6YEr+1KLlkQAPLzB9mTigDMAoGCCqG
531SM49BAMCA0gAMEUCIDGuwD1KPyG+hRf88MeyMQcqOFZD0TbVleF+UsAGQ4enAiEA
532l4wOuDwKQa+upc8GftXE2C//4mKANBC6It01gUaTIpo=
533-----END CERTIFICATE-----";
534
535    const TEST_PEM_CERT2: &str = r"-----BEGIN CERTIFICATE-----
536MIIDXTCCAkWgAwIBAgIJAKL0UG+mRkmSMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
537BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
538aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjIxMTYzMDA1WhcNMjYxMjE5MTYzMDA1WjBF
539MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
540ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
541CgKCAQEAw3khLOKBaKp0I+rkfpJH6i1KBmfEpuCrzK5LMZaFZiVgW/SxXU31N1ee
5424WMrNkfxbI4UlGhPmvlTjP7bvC5V0U28kCZ5s9PQb1FvkPvEJhw9aJVf3zr5wZRb
5438PyBwP3qUfYYWdJmHAHSKb3wDTl4m9wW0i3BNJxW2FLCQU0hRGiCBnW3hEMCH8m2
544P+kQhUITjy9VfNJmKi5dL3RDXZHN+9gYvwHAabMh8qdWKaJCxAiLN4AO9dVXqOJd
545e1TuZ/Vl6qJ3hYT3T3DdVCJ7vHXLqXBnGMxbFhD8rJ4f5V7QRQVbKl1fWZRGtqzB
546YaKyMMoHCMLa3qJvGDEJGTCKB1LEawIDAQABo1AwTjAdBgNVHQ4EFgQUo2hUXWzw
547BI1kxA1WFCLKjWHHwdQwHwYDVR0jBBgwFoAUo2hUXWzwBI1kxA1WFCLKjWHHwdQw
548DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAaDQl2e0vqOCqGNzYqZyY
549S7RJVYW6WIoq7KdQ0m2Bz2NKRvh2KCqCLZvOuDWoOqMHIQM3FnOFv2FIzTT6sqLv
550njRKYAx9Vd4NeMkPq3QHJU7RMkr3EGqFPB8/Zr/p8lZL5DsHKAQv0P9fxbLPxEqw
551Db4tBf4sFjflSF5g3yD4UwmQvSvYGDW8LqhpSL0FZ8thCR4Ii9L9vGBr5fqB3pFM
552uS6eN4Ck5fC4VaZuPKpCj6c7L5i8BDvPbZV4h6FJZFGpd7qPrCJUvYJH0u5MiLJh
553H6Z2F5qzxFr3dVOYlTUQPYJGBZBpXgXL5fBnPWnPPuLFBNLNNqCpM5cY+c5dS9YE
554pg==
555-----END CERTIFICATE-----";
556
557    #[rstest(
558        input,
559        expected_label,
560        expected_data,
561        case(TEST_PEM1, Label::PrivateKey, "AAA"),
562        case(TEST_PEM2, Label::PrivateKey, "AAABBB=="),
563        case(TEST_PEM3, Label::PrivateKey, "AAABBB=="),
564        case(TEST_PEM4, Label::PrivateKey, "AAA="),
565        case(
566            TEST_PEM_CERT1,
567            Label::Certificate,
568            "MIICLDCCAdKgAwIBAgIBADAKBggqhkjOPQQDAjB9MQswCQYDVQQGEwJCRTEPMA0GA1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2VydGlmaWNhdGUgYXV0aG9yaXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdudVRMUyBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkwHhcNMTEwNTIzMjAzODIxWhcNMTIxMjIyMDc0MTUxWjB9MQswCQYDVQQGEwJCRTEPMA0GA1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2VydGlmaWNhdGUgYXV0aG9yaXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdudVRMUyBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARS2I0jiuNn14Y2sSALCX3IybqiIJUvxUpj+oNfzngvj/Niyv2394BWnW4XuQ4RTEiywK87WRcWMGgJB5kX/t2no0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1UdDwEB/wQFAwMHBgAwHQYDVR0OBBYEFPC0gf6YEr+1KLlkQAPLzB9mTigDMAoGCCqGSM49BAMCA0gAMEUCIDGuwD1KPyG+hRf88MeyMQcqOFZD0TbVleF+UsAGQ4enAiEAl4wOuDwKQa+upc8GftXE2C//4mKANBC6It01gUaTIpo="
569        )
570    )]
571    fn test_pem_from_str(input: &str, expected_label: Label, expected_data: &str) {
572        let pem = Pem::from_str(input).unwrap();
573        assert_eq!(expected_label, pem.label());
574        assert_eq!(expected_data, pem.data());
575    }
576
577    const INVALID_TEST_PEM1: &str = r"";
578    const INVALID_TEST_PEM2: &str = r"-----BEGIN PRIVATE KEY-----
579
580-----END PRIVATE KEY-----
581";
582    const INVALID_TEST_PEM3: &str = r"-----BEGIN PRIVATE KEY-----
583AAA
584";
585    const INVALID_TEST_PEM4: &str = r"-----BEGIN PRIVATE KEY-----
586AAA
587
588-----END PRIVATE KEY-----
589";
590    const INVALID_TEST_PEM5: &str = r"-----BEGIN PRIVATE KEY-----
591AAA==
592-----END PUBLIC KEY-----
593";
594    #[rstest(
595        input,
596        expected,
597        case(INVALID_TEST_PEM1, Error::MissingPreEncapsulationBoundary),
598        case(INVALID_TEST_PEM2, Error::MissingData),
599        case(INVALID_TEST_PEM3, Error::MissingPostEncapsulationBoundary),
600        case(INVALID_TEST_PEM4, Error::InvalidBase64Line),
601        case(INVALID_TEST_PEM5, Error::LabelMissMatch)
602    )]
603    fn test_pem_from_str_with_error(input: &str, expected: Error) {
604        if let Err(e) = Pem::from_str(input) {
605            assert_eq!(expected, e);
606        } else {
607            panic!("this test should return an error");
608        }
609    }
610
611    #[rstest(
612        pem_str,
613        label,
614        case(TEST_PEM_CERT1, Label::Certificate),
615        case(TEST_PEM_CERT2, Label::Certificate)
616    )]
617    fn test_pem_roundtrip(pem_str: &str, label: Label) {
618        let original_pem: Pem = pem_str.parse().unwrap();
619        let decoded: Vec<u8> = original_pem.decode().unwrap();
620        let re_encoded_pem = Pem::from_bytes(label, &decoded);
621
622        // Verify the content is the same
623        let re_decoded: Vec<u8> = re_encoded_pem.decode().unwrap();
624        assert_eq!(decoded, re_decoded);
625    }
626
627    #[rstest]
628    #[case::single(vec![TEST_PEM_CERT1], "\n", 1)]
629    #[case::multiple(vec![TEST_PEM_CERT1, TEST_PEM_CERT2], "\n", 2)]
630    #[case::with_whitespace(vec![TEST_PEM_CERT1, TEST_PEM_CERT2], "\n\n\n", 2)]
631    #[case::no_trailing_newline(vec![TEST_PEM_CERT1, TEST_PEM_CERT2], "", 2)]
632    fn test_parse_many(#[case] certs: Vec<&str>, #[case] sep: &str, #[case] expected_count: usize) {
633        let input = certs
634            .iter()
635            .map(|c| c.trim_end())
636            .collect::<Vec<_>>()
637            .join(sep);
638        let pems = crate::parse_many(&input).unwrap();
639        assert_eq!(pems.len(), expected_count);
640    }
641
642    #[test]
643    fn test_parse_many_empty() {
644        let result = crate::parse_many("");
645        assert!(result.is_err());
646    }
647}