Skip to main content

crypto/encoding/
pem.rs

1use alloc::{string::String, vec::Vec};
2use core::fmt;
3
4use memchr::{memchr, memchr2, memmem};
5
6const BEGIN_MARKER: &[u8] = b"-----BEGIN ";
7const END_MARKER: &[u8] = b"-----END ";
8const MARKER_END: &[u8] = b"-----";
9const LINE_WIDTH: usize = 64;
10
11#[derive(Clone, PartialEq, Eq)]
12pub struct Block<'a> {
13    pub r#type: &'a str,
14    pub headers: Headers,
15    pub contents: Vec<u8>,
16}
17
18impl fmt::Debug for Block<'_> {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        f.debug_struct("Block")
21            .field("type", &self.r#type)
22            .field("headers", &self.headers)
23            .field("contents.len", &self.contents.len())
24            .finish()
25    }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum PemError<'a> {
30    InvalidEncoding(&'static str),
31    Base64(base64::DecodeError),
32    LabelMismatch { expected: &'a str, actual: &'a str },
33}
34
35impl<'a> From<base64::DecodeError> for PemError<'a> {
36    fn from(err: base64::DecodeError) -> Self {
37        PemError::Base64(err)
38    }
39}
40
41#[cfg(feature = "alloc")]
42impl core::fmt::Display for PemError<'_> {
43    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44        match self {
45            PemError::InvalidEncoding(str) => write!(f, "{str}"),
46            PemError::Base64(err) => write!(f, "base64 decode error: {err}"),
47            PemError::LabelMismatch {
48                expected,
49                actual,
50            } => {
51                write!(f, "label mismatch: expected '{expected}', got '{actual}'")
52            }
53        }
54    }
55}
56
57pub fn encode(blocks: &[Block<'_>]) -> Vec<u8> {
58    let mut capacity = 0usize;
59    let mut base64_capacity = 0;
60
61    for block in blocks {
62        capacity += 11 + block.r#type.len() + 6;
63        for (k, v) in block.headers.iter() {
64            capacity += k.len() + 2 + v.len() + 1;
65        }
66        if !block.headers.is_empty() {
67            capacity += 1;
68        }
69        let b64_len = base64::encoded_length(block.contents.len(), true).unwrap_or_default();
70        base64_capacity += b64_len;
71        capacity += b64_len + b64_len / 64 + 1;
72        capacity += 9 + block.r#type.len() + 6;
73    }
74
75    let mut output = Vec::with_capacity(capacity);
76    let mut b64_buf = String::with_capacity(base64_capacity);
77
78    for block in blocks {
79        output.extend_from_slice(b"-----BEGIN ");
80        output.extend_from_slice(block.r#type.as_bytes());
81        output.extend_from_slice(b"-----\n");
82
83        for (key, value) in block.headers.iter() {
84            output.extend_from_slice(key.as_bytes());
85            output.extend_from_slice(b": ");
86            output.extend_from_slice(value.as_bytes());
87            output.push(b'\n');
88        }
89
90        if !block.headers.is_empty() {
91            output.push(b'\n');
92        }
93
94        base64::encode_into_string(&mut b64_buf, &block.contents, base64::Alphabet::Standard);
95        for chunk in b64_buf.as_bytes().chunks(LINE_WIDTH) {
96            output.extend_from_slice(chunk);
97            output.push(b'\n');
98        }
99        b64_buf.clear();
100
101        output.extend_from_slice(b"-----END ");
102        output.extend_from_slice(block.r#type.as_bytes());
103        output.extend_from_slice(b"-----\n");
104    }
105    return output;
106}
107
108pub fn decode<'a>(pem: &'a [u8]) -> Blocks<'a> {
109    Blocks {
110        input: pem,
111        pos: 0,
112    }
113}
114
115pub struct Blocks<'a> {
116    input: &'a [u8],
117    pos: usize,
118}
119
120impl<'a> Iterator for Blocks<'a> {
121    type Item = Result<Block<'a>, PemError<'a>>;
122
123    fn next(&mut self) -> Option<Self::Item> {
124        if self.pos >= self.input.len() {
125            return None;
126        }
127
128        let result = parse_one_block(self.input, &mut self.pos);
129
130        return match result {
131            Ok(block) => Some(Ok(block)),
132            Err(err) => {
133                self.pos = self.input.len();
134                Some(Err(err))
135            }
136        };
137    }
138}
139
140#[derive(Clone, Copy)]
141struct Header {
142    key_start: usize,
143    key_len: usize,
144    value_start: usize,
145    value_len: usize,
146}
147
148#[derive(Clone)]
149pub struct Headers {
150    buf: String,
151    pairs: Vec<Header>,
152}
153
154impl Headers {
155    pub fn new() -> Self {
156        Headers {
157            buf: String::new(),
158            pairs: Vec::new(),
159        }
160    }
161
162    pub fn with_capacity(buf: usize, headers: usize) -> Self {
163        Headers {
164            buf: String::with_capacity(buf),
165            pairs: Vec::with_capacity(headers),
166        }
167    }
168
169    pub fn from_pairs(pairs: &[(&str, &str)]) -> Self {
170        let buf_capacity = pairs.iter().fold(0, |acc, pair| acc + pair.0.len() + pair.1.len());
171        let mut headers = Headers::with_capacity(buf_capacity, pairs.len());
172
173        for (k, v) in pairs {
174            headers.buf.reserve(k.len() + v.len());
175            let key_start = headers.buf.len();
176            headers.buf.push_str(k);
177            let value_start = headers.buf.len();
178            headers.buf.push_str(v);
179            headers.pairs.push(Header {
180                key_start,
181                key_len: value_start - key_start,
182                value_start,
183                value_len: headers.buf.len() - value_start,
184            });
185        }
186
187        headers
188    }
189
190    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
191        self.pairs.iter().map(move |span| {
192            let key = &self.buf[span.key_start..span.key_start + span.key_len];
193            let value = &self.buf[span.value_start..span.value_start + span.value_len];
194            (key, value)
195        })
196    }
197
198    pub fn len(&self) -> usize {
199        self.pairs.len()
200    }
201
202    pub fn is_empty(&self) -> bool {
203        self.pairs.is_empty()
204    }
205
206    pub fn push(&mut self, key: &str, value: &str) {
207        let key_start = self.buf.len();
208        self.buf.push_str(key);
209        let value_start = self.buf.len();
210        self.buf.push_str(value);
211        self.pairs.push(Header {
212            key_start,
213            key_len: value_start - key_start,
214            value_start,
215            value_len: self.buf.len() - value_start,
216        });
217    }
218}
219
220impl PartialEq for Headers {
221    fn eq(&self, other: &Self) -> bool {
222        self.len() == other.len() && self.iter().zip(other.iter()).all(|(a, b)| a == b)
223    }
224}
225
226impl Eq for Headers {}
227
228impl fmt::Debug for Headers {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        f.debug_list().entries(self.iter()).finish()
231    }
232}
233
234fn parse_one_block<'a>(input: &'a [u8], pos: &mut usize) -> Result<Block<'a>, PemError<'a>> {
235    let remaining = &input[*pos..];
236    let begin_offset = find_pattern(remaining, BEGIN_MARKER).ok_or(PemError::InvalidEncoding("no BEGIN line found"))?;
237
238    let label_start = *pos + begin_offset + BEGIN_MARKER.len();
239    let (r#type, advance) = {
240        let remaining = &input[label_start..];
241        let line_end = find_line_end(remaining).ok_or(PemError::InvalidEncoding("unexpected end of PEM data"))?;
242        let label_bytes = &remaining[..line_end];
243
244        if label_bytes.len() < MARKER_END.len()
245            || label_bytes[label_bytes.len() - MARKER_END.len()..] != *MARKER_END
246            || (label_bytes.len() > MARKER_END.len() && label_bytes[label_bytes.len() - MARKER_END.len() - 1] == b'-')
247        {
248            return Err(PemError::InvalidEncoding("malformed BEGIN line"));
249        }
250
251        let label = &label_bytes[..label_bytes.len() - MARKER_END.len()];
252        let r#type = core::str::from_utf8(label).map_err(|_| PemError::InvalidEncoding("non-UTF-8 label"))?;
253        (r#type, line_advance(remaining))
254    };
255
256    let mut cursor = label_start + advance;
257
258    let mut header_buf = String::new();
259    let mut pairs: Vec<Header> = Vec::new();
260
261    loop {
262        if cursor >= input.len() {
263            return Err(PemError::InvalidEncoding("unexpected end of PEM data"));
264        }
265        let remaining = &input[cursor..];
266        let line_end = find_line_end(remaining).ok_or(PemError::InvalidEncoding("unexpected end of PEM data"))?;
267        let line = &remaining[..line_end];
268
269        if line.starts_with(END_MARKER) {
270            let contents = Vec::new();
271            *pos = cursor + line_advance(remaining);
272            return Ok(Block {
273                r#type,
274                headers: Headers {
275                    buf: header_buf,
276                    pairs,
277                },
278                contents,
279            });
280        }
281
282        if line.is_empty() || line.iter().all(|&b| b.is_ascii_whitespace()) {
283            cursor += line_advance(remaining);
284            break;
285        }
286
287        if let Some(colon_pos) = memchr(b':', line) {
288            let key = core::str::from_utf8(&line[..colon_pos])
289                .map_err(|_| PemError::InvalidEncoding("non-UTF-8 header key"))?;
290            let value_start = colon_pos + 1;
291            let value = if value_start < line.len() && line[value_start] == b' ' {
292                &line[value_start + 1..]
293            } else {
294                &line[value_start..]
295            };
296            let value_str =
297                core::str::from_utf8(value).map_err(|_| PemError::InvalidEncoding("non-UTF-8 header value"))?;
298
299            cursor += line_advance(remaining);
300
301            let key_start = header_buf.len();
302            header_buf.push_str(key);
303            let key_len = header_buf.len() - key_start;
304
305            let has_continuation = cursor < input.len() && {
306                let rest = &input[cursor..];
307                let cont_line_end = find_line_end(rest).unwrap_or(rest.len());
308                let cont_line = &rest[..cont_line_end];
309                !cont_line.is_empty() && (cont_line[0] == b' ' || cont_line[0] == b'\t')
310            };
311
312            if has_continuation {
313                let mut full_value = String::from(value_str);
314                loop {
315                    if cursor >= input.len() {
316                        break;
317                    }
318                    let rest = &input[cursor..];
319                    let cont_line_end =
320                        find_line_end(rest).ok_or(PemError::InvalidEncoding("unexpected end of PEM data"))?;
321                    let cont_line = &rest[..cont_line_end];
322                    if cont_line.is_empty() {
323                        break;
324                    }
325                    if cont_line[0] != b' ' && cont_line[0] != b'\t' {
326                        break;
327                    }
328                    let cont_trimmed = cont_line.trim_ascii_start();
329                    if !cont_trimmed.is_empty() {
330                        if !full_value.is_empty() {
331                            full_value.push(' ');
332                        }
333                        if let Ok(s) = core::str::from_utf8(cont_trimmed) {
334                            full_value.push_str(s);
335                        }
336                    }
337                    cursor += line_advance(rest);
338                }
339                let val_start = header_buf.len();
340                header_buf.push_str(&full_value);
341                let val_len = header_buf.len() - val_start;
342                pairs.push(Header {
343                    key_start,
344                    key_len,
345                    value_start: val_start,
346                    value_len: val_len,
347                });
348            } else {
349                let val_start = header_buf.len();
350                header_buf.push_str(value_str);
351                let val_len = header_buf.len() - val_start;
352                pairs.push(Header {
353                    key_start,
354                    key_len,
355                    value_start: val_start,
356                    value_len: val_len,
357                });
358            }
359        } else {
360            break;
361        }
362    }
363
364    let base64_start = cursor;
365
366    let b64_data_end;
367    let mut search_pos = base64_start;
368
369    loop {
370        if search_pos >= input.len() {
371            return Err(PemError::InvalidEncoding("missing END line"));
372        }
373        let remaining = &input[search_pos..];
374        let end_offset = find_pattern(remaining, END_MARKER);
375        match end_offset {
376            Some(eo) => {
377                let candidate = search_pos + eo;
378                let end_rest = &input[candidate..];
379                let end_line_end =
380                    find_line_end(end_rest).ok_or(PemError::InvalidEncoding("unexpected end of data"))?;
381                let end_line = &end_rest[..end_line_end];
382
383                let end_label_bytes = &end_line[END_MARKER.len()..];
384                if end_label_bytes.len() >= MARKER_END.len()
385                    && end_label_bytes[end_label_bytes.len() - MARKER_END.len()..] == *MARKER_END
386                    && (end_label_bytes.len() == MARKER_END.len()
387                        || end_label_bytes[end_label_bytes.len() - MARKER_END.len() - 1] != b'-')
388                {
389                    let end_label = &end_label_bytes[..end_label_bytes.len() - MARKER_END.len()];
390                    if end_label == r#type.as_bytes() {
391                        b64_data_end = candidate;
392                        break;
393                    }
394                    let actual = core::str::from_utf8(end_label).unwrap_or("(invalid UTF-8)");
395                    return Err(PemError::LabelMismatch {
396                        expected: r#type,
397                        actual,
398                    });
399                }
400                search_pos = candidate + 1;
401            }
402            None => {
403                return Err(PemError::InvalidEncoding("missing END line"));
404            }
405        }
406    }
407
408    let b64_text = &input[base64_start..b64_data_end];
409
410    let end_remaining = &input[b64_data_end..];
411    *pos = b64_data_end + line_advance(end_remaining);
412
413    let b64_clean: Vec<u8> = b64_text
414        .iter()
415        .copied()
416        .filter(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/' || b == b'=')
417        .collect();
418
419    let contents = base64::decode(&b64_clean, base64::Alphabet::Standard)?;
420
421    return Ok(Block {
422        r#type,
423        headers: Headers {
424            buf: header_buf,
425            pairs,
426        },
427        contents,
428    });
429}
430
431fn find_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
432    if pattern.is_empty() || data.len() < pattern.len() {
433        return None;
434    }
435    memmem::find(data, pattern)
436}
437
438fn find_line_end(data: &[u8]) -> Option<usize> {
439    if data.is_empty() {
440        return None;
441    }
442    Some(memchr2(b'\n', b'\r', data).unwrap_or(data.len()))
443}
444
445fn line_advance(data: &[u8]) -> usize {
446    if data.is_empty() {
447        return 0;
448    }
449    if let Some(i) = memchr2(b'\n', b'\r', data) {
450        if data[i] == b'\n' {
451            return i + 1;
452        }
453        // data[i] == b'\r'
454        if i + 1 < data.len() && data[i + 1] == b'\n' {
455            return i + 2;
456        }
457        return i + 1;
458    }
459    data.len()
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    fn roundtrip(blocks: &[Block<'_>]) {
467        let pem = encode(blocks);
468        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem).collect();
469        for (i, result) in decoded.iter().enumerate() {
470            let decoded_block = result.as_ref().unwrap();
471            assert_eq!(decoded_block.r#type, blocks[i].r#type, "block {} type mismatch", i);
472            assert_eq!(decoded_block.contents, blocks[i].contents, "block {} contents mismatch", i);
473            assert_eq!(decoded_block.headers, blocks[i].headers, "block {} headers mismatch", i);
474        }
475        assert_eq!(decoded.len(), blocks.len(), "block count mismatch");
476    }
477
478    #[test]
479    fn empty_content() {
480        let blocks = [Block {
481            r#type: "CERTIFICATE".into(),
482            headers: Headers::new(),
483            contents: Vec::new(),
484        }];
485        roundtrip(&blocks);
486    }
487
488    #[test]
489    fn simple_certificate() {
490        let blocks = [Block {
491            r#type: "CERTIFICATE".into(),
492            headers: Headers::new(),
493            contents: b"hello world".to_vec(),
494        }];
495        roundtrip(&blocks);
496    }
497
498    #[test]
499    fn binary_content() {
500        let contents: Vec<u8> = (0u8..255).collect();
501        let blocks = [Block {
502            r#type: "PRIVATE KEY".into(),
503            headers: Headers::new(),
504            contents,
505        }];
506        roundtrip(&blocks);
507    }
508
509    #[test]
510    fn exact_48_bytes() {
511        let contents = b"1234567890abcdef1234567890abcdef1234567890abcdef"; // 48 bytes
512        let blocks = [Block {
513            r#type: "CERTIFICATE".into(),
514            headers: Headers::new(),
515            contents: contents.to_vec(),
516        }];
517        roundtrip(&blocks);
518    }
519
520    #[test]
521    fn multiple_blocks() {
522        let blocks = [
523            Block {
524                r#type: "CERTIFICATE".into(),
525                headers: Headers::new(),
526                contents: b"first certificate data".to_vec(),
527            },
528            Block {
529                r#type: "CERTIFICATE".into(),
530                headers: Headers::new(),
531                contents: b"second certificate data".to_vec(),
532            },
533        ];
534        roundtrip(&blocks);
535    }
536
537    #[test]
538    fn different_labels() {
539        let blocks = [
540            Block {
541                r#type: "CERTIFICATE".into(),
542                headers: Headers::new(),
543                contents: b"cert data".to_vec(),
544            },
545            Block {
546                r#type: "PRIVATE KEY".into(),
547                headers: Headers::new(),
548                contents: b"key data".to_vec(),
549            },
550            Block {
551                r#type: "PUBLIC KEY".into(),
552                headers: Headers::new(),
553                contents: b"pubkey data".to_vec(),
554            },
555        ];
556        roundtrip(&blocks);
557    }
558
559    #[test]
560    fn with_rfc1421_headers() {
561        let blocks = [Block {
562            r#type: "PRIVACY-ENHANCED MESSAGE".into(),
563            headers: Headers::from_pairs(&[
564                ("Proc-Type", "4,ENCRYPTED"),
565                ("Content-Domain", "RFC822"),
566                ("DEK-Info", "DES-CBC,F8143EDE5960C597"),
567            ]),
568            contents: b"encrypted message data".to_vec(),
569        }];
570        roundtrip(&blocks);
571    }
572
573    #[test]
574    fn header_with_folded_value() {
575        let pem: &[u8] = b"-----BEGIN PRIVACY-ENHANCED MESSAGE-----\n\
576Proc-Type: 4,ENCRYPTED\n\
577Originator-Certificate:\n MIIBlTCCAScCAWUw\n\
578\n\
579SGVsbG8gV29ybGQ=\n\
580-----END PRIVACY-ENHANCED MESSAGE-----\n";
581
582        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
583        assert_eq!(decoded.len(), 1);
584        let block = decoded[0].as_ref().unwrap();
585        assert_eq!(block.r#type, "PRIVACY-ENHANCED MESSAGE");
586        assert!(block.headers.len() >= 1);
587
588        let proc_type = block.headers.iter().find(|&(k, _)| k == "Proc-Type");
589        assert!(proc_type.is_some(), "Proc-Type not found. Headers: {:?}", block.headers);
590        assert_eq!(proc_type.unwrap().1, "4,ENCRYPTED");
591
592        let originator = block.headers.iter().find(|&(k, _)| k == "Originator-Certificate");
593        assert!(
594            originator.is_some(),
595            "Originator-Certificate not found. Headers: {:?}",
596            block.headers
597        );
598        assert_eq!(
599            originator.unwrap().1,
600            "MIIBlTCCAScCAWUw",
601            "Originator-Certificate value mismatch. Got: '{}'",
602            originator.unwrap().1
603        );
604
605        assert_eq!(block.contents, b"Hello World");
606    }
607
608    #[test]
609    fn decode_from_rfc7468_certificate() {
610        let pem = b"-----BEGIN CERTIFICATE-----\n\
611                     MIICLDCCAdKgAwIBAgIBADAKBggqhkjOPQQDAjB9MQswCQYDVQQGEwJCRTEPMA0G\n\
612                     A1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2VydGlmaWNhdGUgYXV0aG9y\n\
613                     aXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdudVRMUyBjZXJ0aWZpY2F0\n\
614                     ZSBhdXRob3JpdHkwHhcNMTEwNTIzMjAzODIxWhcNMTIxMjIyMDc0MTUxWjB9MQsw\n\
615                     CQYDVQQGEwJCRTEPMA0GA1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2Vy\n\
616                     dGlmaWNhdGUgYXV0aG9yaXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdu\n\
617                     dVRMUyBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMB\n\
618                     BwNCAARS2I0jiuNn14Y2sSALCX3IybqiIJUvxUpj+oNfzngvj/Niyv2394BWnW4X\n\
619                     uQ4RTEiywK87WRcWMGgJB5kX/t2no0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1Ud\n\
620                     DwEB/wQFAwMHBgAwHQYDVR0OBBYEFPC0gf6YEr+1KLlkQAPLzB9mTigDMAoGCCqG\n\
621                     SM49BAMCA0gAMEUCIDGuwD1KPyG+hRf88MeyMQcqOFZD0TbVleF+UsAGQ4enAiEA\n\
622                     l4wOuDwKQa+upc8GftXE2C//4mKANBC6It01gUaTIpo=\n\
623                     -----END CERTIFICATE-----\n";
624
625        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
626        assert_eq!(decoded.len(), 1);
627        let block = decoded[0].as_ref().unwrap();
628        assert_eq!(block.r#type, "CERTIFICATE");
629        assert!(block.headers.is_empty());
630        assert!(!block.contents.is_empty());
631    }
632
633    #[test]
634    fn decode_rfc7468_private_key() {
635        let pem = b"-----BEGIN PRIVATE KEY-----\n\
636                     MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgVcB/UNPxalR9zDYAjQIf\n\
637                     jojUDiQuGnSJrFEEzZPT/92hRANCAASc7UJtgnF/abqWM60T3XNJEzBv5ez9TdwK\n\
638                     H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ\n\
639                     -----END PRIVATE KEY-----\n";
640
641        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
642        assert_eq!(decoded.len(), 1);
643        let block = decoded[0].as_ref().unwrap();
644        assert_eq!(block.r#type, "PRIVATE KEY");
645        assert!(!block.contents.is_empty());
646    }
647
648    #[test]
649    fn decode_crlf_line_endings() {
650        let pem = b"-----BEGIN CERTIFICATE-----\r\n\
651                     SGVsbG8gV29ybGQ=\r\n\
652                     -----END CERTIFICATE-----\r\n";
653
654        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
655        assert_eq!(decoded.len(), 1);
656        let block = decoded[0].as_ref().unwrap();
657        assert_eq!(block.r#type, "CERTIFICATE");
658        assert_eq!(block.contents, b"Hello World");
659    }
660
661    #[test]
662    fn decode_mac_line_endings() {
663        let pem = b"-----BEGIN CERTIFICATE-----\r\
664                     SGVsbG8gV29ybGQ=\r\
665                     -----END CERTIFICATE-----\r";
666
667        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
668        assert_eq!(decoded.len(), 1);
669        let block = decoded[0].as_ref().unwrap();
670        assert_eq!(block.contents, b"Hello World");
671    }
672
673    #[test]
674    fn decode_multiple_blocks() {
675        let pem = b"-----BEGIN CERTIFICATE-----\n\
676                     Rmlyc3Q=\n\
677                     -----END CERTIFICATE-----\n\
678                     -----BEGIN CERTIFICATE-----\n\
679                     U2Vjb25k\n\
680                     -----END CERTIFICATE-----\n";
681
682        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
683        assert_eq!(decoded.len(), 2);
684        assert_eq!(decoded[0].as_ref().unwrap().contents, b"First");
685        assert_eq!(decoded[1].as_ref().unwrap().contents, b"Second");
686    }
687
688    #[test]
689    fn decode_with_leading_text() {
690        let pem = b"Some explanatory text\n\
691                     Subject: CN=Test\n\
692                     Issuer: CN=Test\n\
693                     -----BEGIN CERTIFICATE-----\n\
694                     SGVsbG8=\n\
695                     -----END CERTIFICATE-----\n";
696
697        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
698        assert_eq!(decoded.len(), 1);
699        assert_eq!(decoded[0].as_ref().unwrap().contents, b"Hello");
700    }
701
702    #[test]
703    fn decode_empty_pem() {
704        let pem = b"";
705        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
706        assert!(decoded.is_empty());
707    }
708
709    #[test]
710    fn decode_label_mismatch() {
711        let pem = b"-----BEGIN CERTIFICATE-----\n\
712                     SGVsbG8=\n\
713                     -----END PRIVATE KEY-----\n";
714
715        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
716        assert_eq!(decoded.len(), 1);
717        assert!(decoded[0].is_err());
718        match &decoded[0] {
719            Err(PemError::LabelMismatch {
720                expected,
721                actual,
722            }) => {
723                assert_eq!(*expected, "CERTIFICATE");
724                assert_eq!(*actual, "PRIVATE KEY");
725            }
726            other => panic!("expected LabelMismatch, got {:?}", other),
727        }
728    }
729
730    #[test]
731    fn decode_missing_end_line() {
732        let pem = b"-----BEGIN CERTIFICATE-----\n\
733                     SGVsbG8=\n";
734
735        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
736        assert_eq!(decoded.len(), 1);
737        assert!(decoded[0].is_err());
738    }
739
740    #[test]
741    fn decode_invalid_base64() {
742        let pem = b"-----BEGIN CERTIFICATE-----\n\
743                     !!!invalid!!!\n\
744                     -----END CERTIFICATE-----\n";
745
746        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
747        assert_eq!(decoded.len(), 1);
748        assert!(decoded[0].is_err());
749    }
750
751    #[test]
752    fn decode_multiline_base64() {
753        let contents = b"This is a test message that is long enough to span multiple lines when base64 encoded with 64 character line width";
754        let blocks = [Block {
755            r#type: "MESSAGE".into(),
756            headers: Headers::new(),
757            contents: contents.to_vec(),
758        }];
759        let pem = encode(&blocks);
760        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem).collect();
761        assert_eq!(decoded.len(), 1);
762        assert_eq!(decoded[0].as_ref().unwrap().contents, contents);
763    }
764
765    #[test]
766    fn encode_produces_valid_pem() {
767        let block = Block {
768            r#type: "TEST".into(),
769            headers: Headers::new(),
770            contents: b"data".to_vec(),
771        };
772        let pem = encode(&[block]);
773        let pem_str = core::str::from_utf8(&pem).unwrap();
774        assert!(pem_str.starts_with("-----BEGIN TEST-----\n"));
775        assert!(pem_str.contains("\n-----END TEST-----\n"));
776    }
777
778    #[test]
779    fn encode_line_wrapping() {
780        let contents = vec![b'A'; 200];
781        let block = Block {
782            r#type: "DATA".into(),
783            headers: Headers::new(),
784            contents,
785        };
786        let pem = encode(&[block]);
787        let pem_str = core::str::from_utf8(&pem).unwrap();
788        let body = pem_str
789            .strip_prefix("-----BEGIN DATA-----\n")
790            .unwrap()
791            .strip_suffix("\n-----END DATA-----\n")
792            .unwrap();
793        for line in body.lines() {
794            if !line.is_empty() {
795                assert!(line.len() <= 64, "line too long: {} > 64", line.len());
796            }
797        }
798    }
799
800    #[test]
801    fn decode_no_trailing_newline() {
802        let pem = b"-----BEGIN CERTIFICATE-----\n\
803                     SGVsbG8=\n\
804                     -----END CERTIFICATE-----";
805        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
806        assert_eq!(decoded.len(), 1);
807        assert_eq!(decoded[0].as_ref().unwrap().contents, b"Hello");
808    }
809
810    #[test]
811    fn decode_whitespace_in_base64() {
812        let pem = b"-----BEGIN CERTIFICATE-----\n\
813                     SGVs\tbG8g\n\
814                     V29y bGQ=\n\
815                     -----END CERTIFICATE-----\n";
816
817        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
818        assert_eq!(decoded.len(), 1);
819        assert_eq!(decoded[0].as_ref().unwrap().contents, b"Hello World");
820    }
821
822    #[test]
823    fn decode_interleaved_comment_lines() {
824        let pem = b"-----BEGIN CERTIFICATE-----\n\
825                     Proc-Type: 4,ENCRYPTED\n\
826                     \n\
827                     SGVsbG8=\n\
828                     -----END CERTIFICATE-----\n";
829
830        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
831        assert_eq!(decoded.len(), 1);
832        let block = decoded[0].as_ref().unwrap();
833        assert_eq!(block.contents, b"Hello");
834        assert_eq!(block.r#type, "CERTIFICATE");
835    }
836
837    #[test]
838    fn decode_header_no_space_after_colon() {
839        let pem = b"-----BEGIN PRIVACY-ENHANCED MESSAGE-----\n\
840                     Proc-Type:4,ENCRYPTED\n\
841                     \n\
842                     SGVsbG8=\n\
843                     -----END PRIVACY-ENHANCED MESSAGE-----\n";
844
845        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
846        assert_eq!(decoded.len(), 1);
847        let block = decoded[0].as_ref().unwrap();
848        assert_eq!(block.contents, b"Hello");
849        assert_eq!(block.headers.iter().next().unwrap().1, "4,ENCRYPTED");
850    }
851
852    #[test]
853    fn openssl_test_vector() {
854        let der: Vec<u8> = (0u8..48).collect();
855
856        let b64 = base64::encode(&der, base64::Alphabet::Standard);
857        let mut lines = Vec::new();
858        for chunk in b64.as_bytes().chunks(64) {
859            lines.push(core::str::from_utf8(chunk).unwrap().to_string());
860        }
861
862        let mut pem = String::from("-----BEGIN CERTIFICATE-----\n");
863        for line in &lines {
864            pem.push_str(line);
865            pem.push('\n');
866        }
867        pem.push_str("-----END CERTIFICATE-----\n");
868
869        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem.as_bytes()).collect();
870        assert_eq!(decoded.len(), 1);
871        assert_eq!(decoded[0].as_ref().unwrap().contents, der);
872    }
873
874    #[test]
875    fn openssl_ec_private_key() {
876        let pem = b"-----BEGIN EC PRIVATE KEY-----\n\
877                     MHQCAQEEIIm3VYFh8WkH4lA2KJ6tC3R0H3G7LgZc1Y0Z5Q7sZq6oBwYFK4EE\n\
878                     AaahRANCAAQrR4q6kQ8V5lY6Lq3XZ0gG5f7J2sLvG8kH7X4KxVc5oBwYFK4E\n\
879                     AaE=\n\
880                     -----END EC PRIVATE KEY-----\n";
881
882        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
883        assert_eq!(decoded.len(), 1);
884        let block = decoded[0].as_ref().unwrap();
885        assert_eq!(block.r#type, "EC PRIVATE KEY");
886        assert!(!block.contents.is_empty());
887    }
888
889    #[test]
890    fn python_generated_vector() {
891        let data = b"Hello from Python!";
892        let b64 = base64::encode(data, base64::Alphabet::Standard);
893        let pem = alloc::format!("-----BEGIN PYTHON DATA-----\n{}\n-----END PYTHON DATA-----\n", b64);
894
895        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem.as_bytes()).collect();
896        assert_eq!(decoded.len(), 1);
897        assert_eq!(decoded[0].as_ref().unwrap().contents, data);
898    }
899
900    #[test]
901    fn python_generated_with_headers() {
902        let mut pem = String::from("-----BEGIN PYTHON DATA-----\n");
903        pem.push_str("Content-Type: application/octet-stream\n");
904        pem.push_str("Content-Transfer-Encoding: base64\n");
905        pem.push('\n');
906        let b64 = base64::encode(b"Python header data", base64::Alphabet::Standard);
907        pem.push_str(&b64);
908        pem.push('\n');
909        pem.push_str("-----END PYTHON DATA-----\n");
910
911        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem.as_bytes()).collect();
912        assert_eq!(decoded.len(), 1);
913        let block = decoded[0].as_ref().unwrap();
914        assert_eq!(block.r#type, "PYTHON DATA");
915        assert_eq!(block.contents, b"Python header data");
916        let h_vec: Vec<(&str, &str)> = block.headers.iter().collect();
917        assert_eq!(h_vec.len(), 2);
918    }
919
920    #[test]
921    fn label_with_spaces() {
922        let blocks = [Block {
923            r#type: "CERTIFICATE REQUEST".into(),
924            headers: Headers::new(),
925            contents: b"csr data".to_vec(),
926        }];
927        roundtrip(&blocks);
928    }
929
930    #[test]
931    fn label_empty() {
932        let blocks = [Block {
933            r#type: "",
934            headers: Headers::new(),
935            contents: b"data".to_vec(),
936        }];
937        roundtrip(&blocks);
938    }
939
940    #[test]
941    fn large_content() {
942        let contents = vec![0xABu8; 10000];
943        let blocks = [Block {
944            r#type: "LARGE DATA".into(),
945            headers: Headers::new(),
946            contents,
947        }];
948        roundtrip(&blocks);
949    }
950
951    #[test]
952    fn decode_block_without_base64() {
953        let pem = b"-----BEGIN EMPTY-----\n\
954                     -----END EMPTY-----\n";
955
956        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
957        assert_eq!(decoded.len(), 1);
958        let block = decoded[0].as_ref().unwrap();
959        assert_eq!(block.r#type, "EMPTY");
960        assert!(block.contents.is_empty());
961    }
962
963    #[test]
964    fn roundtrip_rfc7468_certificate() {
965        let der: Vec<u8> = (0u8..128).collect();
966        let original = Block {
967            r#type: "CERTIFICATE".into(),
968            headers: Headers::new(),
969            contents: der,
970        };
971        roundtrip(&[original]);
972    }
973
974    #[test]
975    fn roundtrip_rfc1421_encrypted_message() {
976        let original = Block {
977            r#type: "PRIVACY-ENHANCED MESSAGE".into(),
978            headers: Headers::from_pairs(&[
979                ("Proc-Type", "4,ENCRYPTED"),
980                ("Content-Domain", "RFC822"),
981                ("DEK-Info", "DES-CBC,BFF968AA74691AC1"),
982            ]),
983            contents: b"encrypted content here".to_vec(),
984        };
985        roundtrip(&[original]);
986    }
987
988    #[test]
989    fn encode_skip_headers_when_empty() {
990        let block = Block {
991            r#type: "TEST".into(),
992            headers: Headers::new(),
993            contents: b"data".to_vec(),
994        };
995        let pem = encode(&[block]);
996        let pem_str = core::str::from_utf8(&pem).unwrap();
997        let lines: Vec<&str> = pem_str.lines().collect();
998        assert_eq!(lines[0], "-----BEGIN TEST-----");
999        assert_eq!(lines[1], "ZGF0YQ==");
1000        assert_eq!(lines[2], "-----END TEST-----");
1001    }
1002
1003    #[test]
1004    fn encode_include_headers() {
1005        let block = Block {
1006            r#type: "MESSAGE".into(),
1007            headers: Headers::from_pairs(&[("X-Custom", "value123")]),
1008            contents: b"data".to_vec(),
1009        };
1010        let pem = encode(&[block]);
1011        let pem_str = core::str::from_utf8(&pem).unwrap();
1012        assert!(pem_str.contains("X-Custom: value123\n"));
1013        assert!(pem_str.contains("\n\nZGF0YQ=="));
1014    }
1015
1016    #[test]
1017    fn decode_no_newline_before_end_marker() {
1018        let pem = b"-----BEGIN DATA-----\n\
1019                     ZGF0YQ==\n\
1020                     -----END DATA-----\n";
1021
1022        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1023        assert_eq!(decoded.len(), 1);
1024        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1025    }
1026
1027    #[test]
1028    fn decode_header_values_with_colons() {
1029        let pem = b"-----BEGIN MESSAGE-----\n\
1030                     Proc-Type: 4,ENCRYPTED\n\
1031                     Key-Info: RSA,abc123\n\
1032                     \n\
1033                     ZGF0YQ==\n\
1034                     -----END MESSAGE-----\n";
1035
1036        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1037        assert_eq!(decoded.len(), 1);
1038        let block = decoded[0].as_ref().unwrap();
1039        let h_vec: Vec<(&str, &str)> = block.headers.iter().collect();
1040        assert_eq!(h_vec.len(), 2);
1041        assert_eq!(h_vec[1].0, "Key-Info");
1042        assert!(h_vec[1].1.contains("RSA,abc123"));
1043    }
1044
1045    #[test]
1046    fn decode_only_first_block_on_error() {
1047        let pem = b"-----BEGIN GOOD-----\n\
1048                     R29vZEF0YQ==\n\
1049                     -----END GOOD-----\n\
1050                     -----BEGIN BAD-----\n\
1051                     !!!invalid!!!\n\
1052                     -----END BAD-----\n";
1053
1054        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1055        assert_eq!(decoded.len(), 2);
1056        assert!(decoded[0].is_ok());
1057        assert!(decoded[1].is_err());
1058    }
1059
1060    #[cfg(feature = "std")]
1061    #[test]
1062    fn match_openssl_output() {
1063        use std::process::Command;
1064
1065        let result = Command::new("sh").arg("-c").arg("which openssl").output();
1066
1067        if result.map(|r| r.status.success()).unwrap_or(false) {
1068            let output = Command::new("openssl")
1069                .args([
1070                    "req",
1071                    "-x509",
1072                    "-newkey",
1073                    "rsa:2048",
1074                    "-keyout",
1075                    "/dev/null",
1076                    "-out",
1077                    "/dev/stdout",
1078                    "-days",
1079                    "365",
1080                    "-nodes",
1081                    "-subj",
1082                    "/CN=TestCert",
1083                ])
1084                .output()
1085                .expect("openssl failed");
1086
1087            assert!(output.status.success());
1088            let pem_bytes = output.stdout;
1089
1090            let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem_bytes).collect();
1091            assert_eq!(decoded.len(), 1);
1092            let block = decoded[0].as_ref().unwrap();
1093            assert_eq!(block.r#type, "CERTIFICATE");
1094            assert!(!block.contents.is_empty());
1095
1096            let reencoded = encode(&[block.clone()]);
1097            let re_decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&reencoded).collect();
1098            assert_eq!(re_decoded.len(), 1);
1099            assert_eq!(re_decoded[0].as_ref().unwrap().contents, block.contents);
1100        }
1101    }
1102
1103    #[cfg(feature = "std")]
1104    #[test]
1105    fn match_python_output() {
1106        use std::process::Command;
1107
1108        let result = Command::new("sh").arg("-c").arg("which python3").output();
1109
1110        if result.map(|r| r.status.success()).unwrap_or(false) {
1111            let py_script = "\
1112import base64
1113data = b'Python PEM test data'
1114b64 = base64.b64encode(data).decode('ascii')
1115pem = f\"-----BEGIN PYTHON DATA-----\\n{b64}\\n-----END PYTHON DATA-----\\n\"
1116print(pem, end='')
1117";
1118            let output = Command::new("python3")
1119                .arg("-c")
1120                .arg(py_script)
1121                .output()
1122                .expect("python3 failed");
1123
1124            assert!(output.status.success());
1125            let pem_bytes = output.stdout;
1126
1127            let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem_bytes).collect();
1128            assert_eq!(decoded.len(), 1);
1129            let block = decoded[0].as_ref().unwrap();
1130            assert_eq!(block.r#type, "PYTHON DATA");
1131            assert_eq!(block.contents, b"Python PEM test data");
1132        }
1133    }
1134
1135    #[cfg(feature = "std")]
1136    #[test]
1137    fn match_openssl_ec_key_roundtrip() {
1138        use std::process::Command;
1139
1140        let result = Command::new("sh").arg("-c").arg("which openssl").output();
1141
1142        if result.map(|r| r.status.success()).unwrap_or(false) {
1143            let output = Command::new("openssl")
1144                .args(["ecparam", "-genkey", "-name", "prime256v1", "-outform", "PEM"])
1145                .output()
1146                .expect("openssl ecparam failed");
1147
1148            assert!(output.status.success());
1149            let pem_bytes = output.stdout;
1150
1151            let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem_bytes).collect();
1152            assert!(decoded.len() >= 1);
1153            let first_block = decoded[0].as_ref().unwrap();
1154            assert!(!first_block.contents.is_empty());
1155
1156            let reencoded = encode(&[first_block.clone()]);
1157            let re_decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&reencoded).collect();
1158            assert_eq!(re_decoded.len(), 1);
1159            assert_eq!(re_decoded[0].as_ref().unwrap().contents, first_block.contents);
1160        }
1161    }
1162
1163    #[test]
1164    fn encode_with_special_chars_in_label() {
1165        let label = "X.509 CERTIFICATE";
1166        let block = Block {
1167            r#type: label,
1168            headers: Headers::new(),
1169            contents: b"data".to_vec(),
1170        };
1171        let pem = encode(&[block.clone()]);
1172        assert!(pem.starts_with(b"-----BEGIN X.509 CERTIFICATE-----\n"));
1173        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem).collect();
1174        assert_eq!(decoded.len(), 1);
1175        assert_eq!(decoded[0].as_ref().unwrap().r#type, label);
1176    }
1177
1178    #[test]
1179    fn decode_non_ascii_ignored() {
1180        let mut pem = b"-----BEGIN CERTIFICATE-----\n".to_vec();
1181        pem.push(0x80);
1182        pem.extend_from_slice(b"\n");
1183        pem.extend_from_slice(b"SGVsbG8=\n");
1184        pem.extend_from_slice(b"-----END CERTIFICATE-----\n");
1185
1186        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem).collect();
1187        assert_eq!(decoded.len(), 1);
1188        assert!(decoded[0].is_ok());
1189        assert_eq!(decoded[0].as_ref().unwrap().contents, b"Hello");
1190    }
1191
1192    // ─── RFC 7468 Section 5-13: Label-specific examples ───
1193
1194    #[test]
1195    fn decode_rfc7468_section6_x509_crl() {
1196        let pem = b"-----BEGIN X509 CRL-----\n\
1197                     MIIB9DCCAV8CAQEwCwYJKoZIhvcNAQEFMIIBCDEXMBUGA1UEChMOVmVyaVNpZ24s\n\
1198                     IEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdvcmsxRjBEBgNVBAsT\n\
1199                     PXd3dy52ZXJpc2lnbi5jb20vcmVwb3NpdG9yeS9SUEEgSW5jb3JwLiBieSBSZWYu\n\
1200                     LExJQUIuTFREKGMpOTgxHjAcBgNVBAsTFVBlcnNvbmEgTm90IFZhbGlkYXRlZDEm\n\
1201                     MCQGA1UECxMdRGlnaXRhbCBJRCBDbGFzcyAxIC0gTmV0c2NhcGUxGDAWBgNVBAMU\n\
1202                     D1NpbW9uIEpvc2Vmc3NvbjEiMCAGCSqGSIb3DQEJARYTc2ltb25Aam9zZWZzc29u\n\
1203                     Lm9yZxcNMDYxMjI3MDgwMjM0WhcNMDcwMjA3MDgwMjM1WjAjMCECEC4QNwPfRoWd\n\
1204                     elUNpllhhTgXDTA2MTIyNzA4MDIzNFowCwYJKoZIhvcNAQEFA4GBAD0zX+J2hkcc\n\
1205                     Nbrq1Dn5IKL8nXLgPGcHv1I/le1MNo9t1ohGQxB5HnFUkRPAY82fR6Epor4aHgVy\n\
1206                     b+5y+neKN9Kn2mPF4iiun+a4o26CjJ0pArojCL1p8T0yyi9Xxvyc/ezaZ98HiIyP\n\
1207                     c3DGMNR+oUmSjKZ0jIhAYmeLxaPHfQwR\n\
1208                     -----END X509 CRL-----\n";
1209
1210        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1211        assert_eq!(decoded.len(), 1);
1212        let block = decoded[0].as_ref().unwrap();
1213        assert_eq!(block.r#type, "X509 CRL");
1214        assert!(block.headers.is_empty());
1215        assert!(!block.contents.is_empty());
1216    }
1217
1218    #[test]
1219    fn decode_rfc7468_section7_certificate_request() {
1220        let pem = b"-----BEGIN CERTIFICATE REQUEST-----\n\
1221                     MIIBWDCCAQcCAQAwTjELMAkGA1UEBhMCU0UxJzAlBgNVBAoTHlNpbW9uIEpvc2Vm\n\
1222                     c3NvbiBEYXRha29uc3VsdCBBQjEWMBQGA1UEAxMNam9zZWZzc29uLm9yZzBOMBAG\n\
1223                     ByqGSM49AgEGBSuBBAAhAzoABLLPSkuXY0l66MbxVJ3Mot5FCFuqQfn6dTs+9/CM\n\
1224                     EOlSwVej77tj56kj9R/j9Q+LfysX8FO9I5p3oGIwYAYJKoZIhvcNAQkOMVMwUTAY\n\
1225                     BgNVHREEETAPgg1qb3NlZnNzb24ub3JnMAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/\n\
1226                     BAUDAwegADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAKBggqhkjOPQQDAgM/ADA8\n\
1227                     AhxBvfhxPFfbBbsE1NoFmCUczOFApEuQVUw3ZP69AhwWXk3dgSUsKnuwL5g/ftAY\n\
1228                     dEQc8B8jAcnuOrfU\n\
1229                     -----END CERTIFICATE REQUEST-----\n";
1230
1231        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1232        assert_eq!(decoded.len(), 1);
1233        let block = decoded[0].as_ref().unwrap();
1234        assert_eq!(block.r#type, "CERTIFICATE REQUEST");
1235        assert!(!block.contents.is_empty());
1236    }
1237
1238    #[test]
1239    fn decode_rfc7468_section8_pkcs7() {
1240        let pem = b"-----BEGIN PKCS7-----\n\
1241                     MIHjBgsqhkiG9w0BCRABF6CB0zCB0AIBADFho18CAQCgGwYJKoZIhvcNAQUMMA4E\n\
1242                     CLfrI6dr0gUWAgITiDAjBgsqhkiG9w0BCRADCTAUBggqhkiG9w0DBwQIZpECRWtz\n\
1243                     u5kEGDCjerXY8odQ7EEEromZJvAurk/j81IrozBSBgkqhkiG9w0BBwEwMwYLKoZI\n\
1244                     hvcNAQkQAw8wJDAUBggqhkiG9w0DBwQI0tCBcU09nxEwDAYIKwYBBQUIAQIFAIAQ\n\
1245                     OsYGYUFdAH0RNc1p4VbKEAQUM2Xo8PMHBoYdqEcsbTodlCFAZH4=\n\
1246                     -----END PKCS7-----\n";
1247
1248        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1249        assert_eq!(decoded.len(), 1);
1250        let block = decoded[0].as_ref().unwrap();
1251        assert_eq!(block.r#type, "PKCS7");
1252        assert!(!block.contents.is_empty());
1253    }
1254
1255    #[test]
1256    fn decode_rfc7468_section9_cms() {
1257        let pem = b"-----BEGIN CMS-----\n\
1258                     MIGDBgsqhkiG9w0BCRABCaB0MHICAQAwDQYLKoZIhvcNAQkQAwgwXgYJKoZIhvcN\n\
1259                     AQcBoFEET3icc87PK0nNK9ENqSxItVIoSa0o0S/ISczMs1ZIzkgsKk4tsQ0N1nUM\n\
1260                     dvb05OXi5XLPLEtViMwvLVLwSE0sKlFIVHAqSk3MBkkBAJv0Fx0=\n\
1261                     -----END CMS-----\n";
1262
1263        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1264        assert_eq!(decoded.len(), 1);
1265        let block = decoded[0].as_ref().unwrap();
1266        assert_eq!(block.r#type, "CMS");
1267        assert!(!block.contents.is_empty());
1268    }
1269
1270    #[test]
1271    fn decode_rfc7468_section11_encrypted_private_key() {
1272        let pem = b"-----BEGIN ENCRYPTED PRIVATE KEY-----\n\
1273                     MIHNMEAGCSqGSIb3DQEFDTAzMBsGCSqGSIb3DQEFDDAOBAghhICA6T/51QICCAAw\n\
1274                     FAYIKoZIhvcNAwcECBCxDgvI59i9BIGIY3CAqlMNBgaSI5QiiWVNJ3IpfLnEiEsW\n\
1275                     Z0JIoHyRmKK/+cr9QPLnzxImm0TR9s4JrG3CilzTWvb0jIvbG3hu0zyFPraoMkap\n\
1276                     8eRzWsIvC5SVel+CSjoS2mVS87cyjlD+txrmrXOVYDE+eTgMLbrLmsWh3QkCTRtF\n\
1277                     QC7k0NNzUHTV9yGDwfqMbw==\n\
1278                     -----END ENCRYPTED PRIVATE KEY-----\n";
1279
1280        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1281        assert_eq!(decoded.len(), 1);
1282        let block = decoded[0].as_ref().unwrap();
1283        assert_eq!(block.r#type, "ENCRYPTED PRIVATE KEY");
1284        assert!(!block.contents.is_empty());
1285    }
1286
1287    #[test]
1288    fn decode_rfc7468_section12_attribute_certificate() {
1289        let pem = b"-----BEGIN ATTRIBUTE CERTIFICATE-----\n\
1290                     MIICKzCCAZQCAQEwgZeggZQwgYmkgYYwgYMxCzAJBgNVBAYTAlVTMREwDwYDVQQI\n\
1291                     DAhOZXcgWW9yazEUMBIGA1UEBwwLU3RvbnkgQnJvb2sxDzANBgNVBAoMBkNTRTU5\n\
1292                     MjE6MDgGA1UEAwwxU2NvdHQgU3RhbGxlci9lbWFpbEFkZHJlc3M9c3N0YWxsZXJA\n\
1293                     aWMuc3VueXNiLmVkdQIGARWrgUUSoIGMMIGJpIGGMIGDMQswCQYDVQQGEwJVUzER\n\
1294                     MA8GA1UECAwITmV3IFlvcmsxFDASBgNVBAcMC1N0b255IEJyb29rMQ8wDQYDVQQK\n\
1295                     DAZDU0U1OTIxOjA4BgNVBAMMMVNjb3R0IFN0YWxsZXIvZW1haWxBZGRyZXNzPXNz\n\
1296                     dGFsbGVyQGljLnN1bnlzYi5lZHUwDQYJKoZIhvcNAQEFBQACBgEVq4FFSjAiGA8z\n\
1297                     OTA3MDIwMTA1MDAwMFoYDzM5MTEwMTMxMDUwMDAwWjArMCkGA1UYSDEiMCCGHmh0\n\
1298                     dHA6Ly9pZGVyYXNobi5vcmcvaW5kZXguaHRtbDANBgkqhkiG9w0BAQUFAAOBgQAV\n\
1299                     M9axFPXXozEFcer06bj9MCBBCQLtAM7ZXcZjcxyva7xCBDmtZXPYUluHf5OcWPJz\n\
1300                     5XPus/xS9wBgtlM3fldIKNyNO8RsMp6Ocx+PGlICc7zpZiGmCYLl64lAEGPO/bsw\n\
1301                     Smluak1aZIttePeTAHeJJs8izNJ5aR3Wcd3A5gLztQ==\n\
1302                     -----END ATTRIBUTE CERTIFICATE-----\n";
1303
1304        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1305        assert_eq!(decoded.len(), 1);
1306        let block = decoded[0].as_ref().unwrap();
1307        assert_eq!(block.r#type, "ATTRIBUTE CERTIFICATE");
1308        assert!(!block.contents.is_empty());
1309    }
1310
1311    #[test]
1312    fn decode_rfc7468_section13_public_key() {
1313        let pem = b"-----BEGIN PUBLIC KEY-----\n\
1314                     MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEn1LlwLN/KBYQRVH6HfIMTzfEqJOVztLe\n\
1315                     kLchp2hi78cCaMY81FBlYs8J9l7krc+M4aBeCGYFjba+hiXttJWPL7ydlE+5UG4U\n\
1316                     Nkn3Eos8EiZByi9DVsyfy9eejh+8AXgp\n\
1317                     -----END PUBLIC KEY-----\n";
1318
1319        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1320        assert_eq!(decoded.len(), 1);
1321        let block = decoded[0].as_ref().unwrap();
1322        assert_eq!(block.r#type, "PUBLIC KEY");
1323        assert!(!block.contents.is_empty());
1324    }
1325
1326    // ─── RFC 7468 Appendix A: Non-standard label variants ───
1327
1328    #[test]
1329    fn decode_rfc7468_appendix_x509_certificate() {
1330        let pem = b"-----BEGIN X509 CERTIFICATE-----\n\
1331                     MIIBHDCBxaADAgECAgIcxzAJBgcqhkjOPQQBMBAxDjAMBgNVBAMUBVBLSVghMB4X\n\
1332                     DTE0MDkxNDA2MTU1MFoXDTI0MDkxNDA2MTU1MFowEDEOMAwGA1UEAxQFUEtJWCEw\n\
1333                     WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATwoQSr863QrR0PoRIYQ96H7WykDePH\n\
1334                     Wa0eVAE24bth43wCNc+U5aZ761dhGhSSJkVWRgVH5+prLIr+nzfIq+X4oxAwDjAM\n\
1335                     BgNVHRMBAf8EAjAAMAkGByqGSM49BAEDRwAwRAIfMdKS5F63lMnWVhi7uaKJzKCs\n\
1336                     NnY/OKgBex6MIEAv2AIhAI2GdvfL+mGvhyPZE+JxRxWChmggb5/9eHdUcmW/jkOH\n\
1337                     -----END X509 CERTIFICATE-----\n";
1338
1339        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1340        assert_eq!(decoded.len(), 1);
1341        let block = decoded[0].as_ref().unwrap();
1342        assert_eq!(block.r#type, "X509 CERTIFICATE");
1343        assert!(!block.contents.is_empty());
1344    }
1345
1346    #[test]
1347    fn decode_rfc7468_appendix_dot_x509_certificate() {
1348        let pem = b"-----BEGIN X.509 CERTIFICATE-----\n\
1349                     MIIBHDCBxaADAgECAgIcxzAJBgcqhkjOPQQBMBAxDjAMBgNVBAMUBVBLSVghMB4X\n\
1350                     DTE0MDkxNDA2MTU1MFoXDTI0MDkxNDA2MTU1MFowEDEOMAwGA1UEAxQFUEtJWCEw\n\
1351                     WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATwoQSr863QrR0PoRIYQ96H7WykDePH\n\
1352                     Wa0eVAE24bth43wCNc+U5aZ761dhGhSSJkVWRgVH5+prLIr+nzfIq+X4oxAwDjAM\n\
1353                     BgNVHRMBAf8EAjAAMAkGByqGSM49BAEDRwAwRAIfMdKS5F63lMnWVhi7uaKJzKCs\n\
1354                     NnY/OKgBex6MIEAv2AIhAI2GdvfL+mGvhyPZE+JxRxWChmggb5/9eHdUcmW/jkOH\n\
1355                     -----END X.509 CERTIFICATE-----\n";
1356
1357        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1358        assert_eq!(decoded.len(), 1);
1359        let block = decoded[0].as_ref().unwrap();
1360        assert_eq!(block.r#type, "X.509 CERTIFICATE");
1361        assert!(!block.contents.is_empty());
1362    }
1363
1364    #[test]
1365    fn decode_rfc7468_appendix_new_certificate_request() {
1366        let pem = b"-----BEGIN NEW CERTIFICATE REQUEST-----\n\
1367                     MIIBWDCCAQcCAQAwTjELMAkGA1UEBhMCU0UxJzAlBgNVBAoTHlNpbW9uIEpvc2Vm\n\
1368                     c3NvbiBEYXRha29uc3VsdCBBQjEWMBQGA1UEAxMNam9zZWZzc29uLm9yZzBOMBAG\n\
1369                     ByqGSM49AgEGBSuBBAAhAzoABLLPSkuXY0l66MbxVJ3Mot5FCFuqQfn6dTs+9/CM\n\
1370                     EOlSwVej77tj56kj9R/j9Q+LfysX8FO9I5p3oGIwYAYJKoZIhvcNAQkOMVMwUTAY\n\
1371                     BgNVHREEETAPgg1qb3NlZnNzb24ub3JnMAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/\n\
1372                     BAUDAwegADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAKBggqhkjOPQQDAgM/ADA8\n\
1373                     AhxBvfhxPFfbBbsE1NoFmCUczOFApEuQVUw3ZP69AhwWXk3dgSUsKnuwL5g/ftAY\n\
1374                     dEQc8B8jAcnuOrfU\n\
1375                     -----END NEW CERTIFICATE REQUEST-----\n";
1376
1377        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1378        assert_eq!(decoded.len(), 1);
1379        let block = decoded[0].as_ref().unwrap();
1380        assert_eq!(block.r#type, "NEW CERTIFICATE REQUEST");
1381        assert!(!block.contents.is_empty());
1382    }
1383
1384    #[test]
1385    fn decode_rfc7468_appendix_certificate_chain() {
1386        let pem = b"-----BEGIN CERTIFICATE CHAIN-----\n\
1387                     MIHjBgsqhkiG9w0BCRABF6CB0zCB0AIBADFho18CAQCgGwYJKoZIhvcNAQUMMA4E\n\
1388                     CLfrI6dr0gUWAgITiDAjBgsqhkiG9w0BCRADCTAUBggqhkiG9w0DBwQIZpECRWtz\n\
1389                     u5kEGDCjerXY8odQ7EEEromZJvAurk/j81IrozBSBgkqhkiG9w0BBwEwMwYLKoZI\n\
1390                     hvcNAQkQAw8wJDAUBggqhkiG9w0DBwQI0tCBcU09nxEwDAYIKwYBBQUIAQIFAIAQ\n\
1391                     OsYGYUFdAH0RNc1p4VbKEAQUM2Xo8PMHBoYdqEcsbTodlCFAZH4=\n\
1392                     -----END CERTIFICATE CHAIN-----\n";
1393
1394        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1395        assert_eq!(decoded.len(), 1);
1396        let block = decoded[0].as_ref().unwrap();
1397        assert_eq!(block.r#type, "CERTIFICATE CHAIN");
1398        assert!(!block.contents.is_empty());
1399    }
1400
1401    // ─── Bad PEM rejection (matching Go's badPEMTests) ───
1402
1403    #[test]
1404    fn reject_too_few_trailing_dashes_begin() {
1405        let pem = b"-----BEGIN FOO----\ndGVzdA==\n-----END FOO-----\n";
1406        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1407        assert!(
1408            decoded.is_empty() || decoded[0].is_err(),
1409            "expected error for BEGIN with 4 trailing dashes"
1410        );
1411    }
1412
1413    #[test]
1414    fn reject_too_many_trailing_dashes_begin() {
1415        let pem = b"-----BEGIN FOO-------\ndGVzdA==\n-----END FOO-------\n";
1416        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1417        assert!(
1418            decoded.is_empty() || decoded[0].is_err(),
1419            "expected error for BEGIN with 7 trailing dashes"
1420        );
1421    }
1422
1423    #[test]
1424    fn reject_too_few_trailing_dashes_end() {
1425        let pem = b"-----BEGIN FOO-----\ndGVzdA==\n-----END FOO----\n";
1426        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1427        assert!(
1428            decoded.is_empty() || decoded[0].is_err(),
1429            "expected error for END with 4 trailing dashes"
1430        );
1431    }
1432
1433    #[test]
1434    fn reject_too_many_trailing_dashes_end() {
1435        let pem = b"-----BEGIN FOO-----\ndGVzdA==\n-----END FOO------\n";
1436        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1437        assert!(
1438            decoded.is_empty() || decoded[0].is_err(),
1439            "expected error for END with 6 trailing dashes"
1440        );
1441    }
1442
1443    #[test]
1444    fn reject_missing_ending_space() {
1445        let pem = b"-----BEGIN FOO-----\ndGVzdA==\n-----ENDBAR-----\n";
1446        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1447        assert!(
1448            decoded.is_empty() || decoded[0].is_err(),
1449            "expected error for missing space between END and label"
1450        );
1451    }
1452
1453    #[test]
1454    fn reject_trailing_non_whitespace_on_end() {
1455        let pem = b"-----BEGIN FOO-----\ndGVzdA==\n-----END FOO----- .\n";
1456        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1457        assert_eq!(decoded.len(), 1);
1458        assert!(decoded[0].is_err(), "expected error for trailing '. ' on END line");
1459    }
1460
1461    #[test]
1462    fn reject_repeating_begin_no_end() {
1463        let input = b"-----BEGIN \n".repeat(100);
1464        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&input).collect();
1465        assert_eq!(decoded.len(), 1);
1466        assert!(decoded[0].is_err(), "expected error for 100 repeated BEGIN lines with no END");
1467    }
1468
1469    #[test]
1470    fn reject_only_end_marker() {
1471        let pem = b"-----END PUBLIC KEY-----\n";
1472        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1473        assert!(
1474            decoded.is_empty() || decoded[0].is_err(),
1475            "expected no valid block from input containing only END marker"
1476        );
1477    }
1478
1479    // ─── Go-equivalence: Strange cases ───
1480
1481    #[test]
1482    fn decode_empty_lines_before_end() {
1483        let pem = b"-----BEGIN DATA-----\n\
1484                     ZGF0YQ==\n\
1485                     \n\
1486                     \n\
1487                     -----END DATA-----\n";
1488        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1489        assert_eq!(decoded.len(), 1);
1490        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1491    }
1492
1493    #[test]
1494    fn decode_blank_lines_between_begin_and_end() {
1495        let pem = b"-----BEGIN EMPTY-----\n\
1496                     \n\
1497                     \n\
1498                     \n\
1499                     -----END EMPTY-----\n";
1500        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1501        assert_eq!(decoded.len(), 1);
1502        assert!(decoded[0].as_ref().unwrap().contents.is_empty());
1503    }
1504
1505    #[test]
1506    fn decode_header_key_only_no_value() {
1507        let pem = b"-----BEGIN DATA-----\n\
1508                     Key-Only:\n\
1509                     \n\
1510                     ZGF0YQ==\n\
1511                     -----END DATA-----\n";
1512        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1513        assert_eq!(decoded.len(), 1);
1514        let block = decoded[0].as_ref().unwrap();
1515        assert_eq!(block.contents, b"data");
1516        let header = block.headers.iter().find(|&(k, _)| k == "Key-Only");
1517        assert!(header.is_some());
1518        assert_eq!(header.unwrap().1, "");
1519    }
1520
1521    #[test]
1522    fn decode_multiple_blank_lines_before_body() {
1523        let pem = b"-----BEGIN DATA-----\n\
1524                     \n\
1525                     \n\
1526                     \n\
1527                     \n\
1528                     ZGF0YQ==\n\
1529                     -----END DATA-----\n";
1530        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1531        assert_eq!(decoded.len(), 1);
1532        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1533    }
1534
1535    #[test]
1536    fn decode_header_value_with_spaces_only() {
1537        let pem = b"-----BEGIN DATA-----\n\
1538                     Key:   \n\
1539                     \n\
1540                     ZGF0YQ==\n\
1541                     -----END DATA-----\n";
1542        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1543        assert_eq!(decoded.len(), 1);
1544        let block = decoded[0].as_ref().unwrap();
1545        assert_eq!(block.contents, b"data");
1546    }
1547
1548    #[test]
1549    fn decode_header_continuation_empty_line() {
1550        let pem = b"-----BEGIN DATA-----\n\
1551                     Key: start\n\
1552                     \t\n\
1553                     \n\
1554                     ZGF0YQ==\n\
1555                     -----END DATA-----\n";
1556        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1557        assert_eq!(decoded.len(), 1);
1558        let block = decoded[0].as_ref().unwrap();
1559        assert_eq!(block.contents, b"data");
1560    }
1561
1562    #[test]
1563    fn decode_header_with_no_blank_line_before_body() {
1564        let pem = b"-----BEGIN FOO-----\n\
1565                     Header: 1\n\
1566                     -----END FOO-----\n";
1567        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1568        assert_eq!(decoded.len(), 1);
1569        let block = decoded[0].as_ref().unwrap();
1570        assert_eq!(block.r#type, "FOO");
1571        assert_eq!(block.headers.len(), 1);
1572        assert_eq!(block.headers.iter().next().unwrap().0, "Header");
1573        assert_eq!(block.headers.iter().next().unwrap().1, "1");
1574        assert!(block.contents.is_empty());
1575    }
1576
1577    // ─── Roundtrip: various content sizes ───
1578
1579    #[test]
1580    fn roundtrip_0_bytes() {
1581        let blocks = [Block {
1582            r#type: "DATA".into(),
1583            headers: Headers::new(),
1584            contents: Vec::new(),
1585        }];
1586        roundtrip(&blocks);
1587    }
1588
1589    #[test]
1590    fn roundtrip_1_byte() {
1591        let blocks = [Block {
1592            r#type: "DATA".into(),
1593            headers: Headers::new(),
1594            contents: vec![0x00],
1595        }];
1596        roundtrip(&blocks);
1597    }
1598
1599    #[test]
1600    fn roundtrip_2_bytes() {
1601        let blocks = [Block {
1602            r#type: "DATA".into(),
1603            headers: Headers::new(),
1604            contents: vec![0x00, 0x01],
1605        }];
1606        roundtrip(&blocks);
1607    }
1608
1609    #[test]
1610    fn roundtrip_3_bytes() {
1611        let blocks = [Block {
1612            r#type: "DATA".into(),
1613            headers: Headers::new(),
1614            contents: vec![0x00, 0x01, 0x02],
1615        }];
1616        roundtrip(&blocks);
1617    }
1618
1619    #[test]
1620    fn roundtrip_64_bytes() {
1621        let contents: Vec<u8> = (0u8..64).collect();
1622        let blocks = [Block {
1623            r#type: "DATA".into(),
1624            headers: Headers::new(),
1625            contents,
1626        }];
1627        roundtrip(&blocks);
1628    }
1629
1630    #[test]
1631    fn roundtrip_65_bytes() {
1632        let contents: Vec<u8> = (0u8..65).collect();
1633        let blocks = [Block {
1634            r#type: "DATA".into(),
1635            headers: Headers::new(),
1636            contents,
1637        }];
1638        roundtrip(&blocks);
1639    }
1640
1641    #[test]
1642    fn roundtrip_128_bytes() {
1643        let contents: Vec<u8> = (0u8..128).collect();
1644        let blocks = [Block {
1645            r#type: "DATA".into(),
1646            headers: Headers::new(),
1647            contents,
1648        }];
1649        roundtrip(&blocks);
1650    }
1651
1652    #[test]
1653    fn roundtrip_255_bytes() {
1654        let contents: Vec<u8> = (0u8..255).collect();
1655        let blocks = [Block {
1656            r#type: "DATA".into(),
1657            headers: Headers::new(),
1658            contents,
1659        }];
1660        roundtrip(&blocks);
1661    }
1662
1663    #[test]
1664    fn roundtrip_256_bytes() {
1665        let contents: Vec<u8> = (0u8..=255).collect();
1666        let blocks = [Block {
1667            r#type: "DATA".into(),
1668            headers: Headers::new(),
1669            contents,
1670        }];
1671        roundtrip(&blocks);
1672    }
1673
1674    // ─── Encode line wrapping edge cases ───
1675
1676    #[test]
1677    fn encode_line_wrapping_exact_multiple_of_64() {
1678        let contents = vec![b'x'; 48];
1679        let blocks = [Block {
1680            r#type: "TEST".into(),
1681            headers: Headers::new(),
1682            contents,
1683        }];
1684        let pem = encode(&blocks);
1685        let pem_str = core::str::from_utf8(&pem).unwrap();
1686        let body = pem_str
1687            .strip_prefix("-----BEGIN TEST-----\n")
1688            .unwrap()
1689            .strip_suffix("\n-----END TEST-----\n")
1690            .unwrap();
1691        assert_eq!(body.lines().count(), 1);
1692        assert!(body.len() <= 64, "single line should be <= 64 chars");
1693    }
1694
1695    #[test]
1696    fn encode_line_wrapping_exactly_64_per_line() {
1697        let contents = vec![0u8; 64];
1698        let blocks = [Block {
1699            r#type: "TEST".into(),
1700            headers: Headers::new(),
1701            contents,
1702        }];
1703        let pem = encode(&blocks);
1704        let pem_str = core::str::from_utf8(&pem).unwrap();
1705        let body = pem_str
1706            .strip_prefix("-----BEGIN TEST-----\n")
1707            .unwrap()
1708            .strip_suffix("\n-----END TEST-----\n")
1709            .unwrap();
1710        assert!(!body.is_empty());
1711    }
1712
1713    // ─── Label edge cases ───
1714
1715    #[test]
1716    fn roundtrip_label_with_hyphen() {
1717        let blocks = [Block {
1718            r#type: "TEST-DATA".into(),
1719            headers: Headers::new(),
1720            contents: b"hyphen test".to_vec(),
1721        }];
1722        roundtrip(&blocks);
1723    }
1724
1725    #[test]
1726    fn roundtrip_label_with_numbers() {
1727        let blocks = [Block {
1728            r#type: "AES-256-CBC".into(),
1729            headers: Headers::new(),
1730            contents: b"number test".to_vec(),
1731        }];
1732        roundtrip(&blocks);
1733    }
1734
1735    #[test]
1736    fn roundtrip_label_with_underscores() {
1737        let blocks = [Block {
1738            r#type: "MY_CUSTOM_LABEL".into(),
1739            headers: Headers::new(),
1740            contents: b"underscore test".to_vec(),
1741        }];
1742        roundtrip(&blocks);
1743    }
1744
1745    #[test]
1746    fn roundtrip_label_with_at_sign() {
1747        let blocks = [Block {
1748            r#type: "KEY@DOMAIN.COM".into(),
1749            headers: Headers::new(),
1750            contents: b"at sign test".to_vec(),
1751        }];
1752        roundtrip(&blocks);
1753    }
1754
1755    #[test]
1756    fn roundtrip_single_char_label() {
1757        let blocks = [Block {
1758            r#type: "X".into(),
1759            headers: Headers::new(),
1760            contents: b"single char".to_vec(),
1761        }];
1762        roundtrip(&blocks);
1763    }
1764
1765    #[test]
1766    fn roundtrip_long_label() {
1767        let label = "A".repeat(100);
1768        let blocks = [Block {
1769            r#type: &label,
1770            headers: Headers::new(),
1771            contents: b"long label test".to_vec(),
1772        }];
1773        roundtrip(&blocks);
1774    }
1775
1776    // ─── Decoding: whitespace robustness ───
1777
1778    #[test]
1779    fn decode_leading_whitespace_before_begin() {
1780        let pem = b"\n\n\n-----BEGIN DATA-----\n\
1781                     ZGF0YQ==\n\
1782                     -----END DATA-----\n";
1783        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1784        assert_eq!(decoded.len(), 1);
1785        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1786    }
1787
1788    #[test]
1789    fn decode_tabs_in_body_only() {
1790        let pem = b"-----BEGIN DATA-----\n\
1791                     ZGF0\tYQ==\n\
1792                     -----END DATA-----\n";
1793        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1794        assert_eq!(decoded.len(), 1);
1795        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1796    }
1797
1798    #[test]
1799    fn decode_line_breaks_within_base64_line() {
1800        let pem = b"-----BEGIN DATA-----\n\
1801                     ZGF0\n\
1802                     YQ==\n\
1803                     -----END DATA-----\n";
1804        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1805        assert_eq!(decoded.len(), 1);
1806        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1807    }
1808
1809    // ─── Encode: header format precision ───
1810
1811    #[test]
1812    fn encode_multiple_header_pairs() {
1813        let block = Block {
1814            r#type: "DATA".into(),
1815            headers: Headers::from_pairs(&[("Key1", "val1"), ("Key2", "val2"), ("Key3", "val3")]),
1816            contents: b"multi header".to_vec(),
1817        };
1818        let pem = encode(&[block.clone()]);
1819        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem).collect();
1820        assert_eq!(decoded.len(), 1);
1821        assert_eq!(decoded[0].as_ref().unwrap().headers, block.headers);
1822        assert_eq!(decoded[0].as_ref().unwrap().contents, block.contents);
1823    }
1824
1825    #[test]
1826    fn encode_header_with_empty_value() {
1827        let block = Block {
1828            r#type: "DATA".into(),
1829            headers: Headers::from_pairs(&[("Empty-Key", "")]),
1830            contents: b"empty value".to_vec(),
1831        };
1832        let pem = encode(&[block.clone()]);
1833        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(&pem).collect();
1834        assert_eq!(decoded.len(), 1);
1835        assert_eq!(decoded[0].as_ref().unwrap().headers.iter().next().unwrap().1, "");
1836    }
1837
1838    // ─── Multiple blocks roundtrip ───
1839
1840    #[test]
1841    fn roundtrip_five_blocks() {
1842        let type_strs: Vec<String> = (0..5).map(|i| alloc::format!("BLOCK{}", i)).collect();
1843        let mut blocks = Vec::new();
1844        for i in 0..5 {
1845            blocks.push(Block {
1846                r#type: &type_strs[i],
1847                headers: Headers::new(),
1848                contents: alloc::format!("content {}", i).into_bytes(),
1849            });
1850        }
1851        roundtrip(&blocks);
1852    }
1853
1854    #[test]
1855    fn decode_block_with_explanatory_text() {
1856        let pem = b"Subject: CN=Atlantis\n\
1857                     Issuer: CN=Atlantis\n\
1858                     Validity: from 7/9/2012 3:10:38 AM UTC to 7/9/2013 3:10:37 AM UTC\n\
1859                     -----BEGIN CERTIFICATE-----\n\
1860                     SGVsbG8=\n\
1861                     -----END CERTIFICATE-----\n";
1862        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1863        assert_eq!(decoded.len(), 1);
1864        assert_eq!(decoded[0].as_ref().unwrap().contents, b"Hello");
1865    }
1866
1867    // ─── Fuzz-style: random roundtrip ───
1868
1869    #[test]
1870    fn fuzz_random_roundtrip() {
1871        let mut state: u64 = 42;
1872        for i in 0..20 {
1873            state = state
1874                .wrapping_mul(6364136223846793005)
1875                .wrapping_add(1442695040888963407);
1876            let len = (state % 512) as usize;
1877            let contents: Vec<u8> = (0..len)
1878                .map(|j| {
1879                    ((state
1880                        .wrapping_add(j as u64)
1881                        .wrapping_mul(1103515245)
1882                        .wrapping_add(12345))
1883                        >> 16) as u8
1884                })
1885                .collect();
1886
1887            let type_str = alloc::format!("TEST{}", i);
1888            let mut headers = Headers::new();
1889            for hi in 0..(state % 3) as usize {
1890                headers.push(&alloc::format!("H{}{}", i, hi), &alloc::format!("V{}{}", i, hi));
1891            }
1892            let blocks = [Block {
1893                r#type: &type_str,
1894                headers,
1895                contents,
1896            }];
1897            roundtrip(&blocks);
1898        }
1899    }
1900
1901    // ─── RFC 1421: Encrypted message format with header continuation ───
1902
1903    #[test]
1904    fn decode_rfc1421_full_encrypted_message() {
1905        let pem: &[u8] = b"-----BEGIN PRIVACY-ENHANCED MESSAGE-----\n\
1906                     Proc-Type: 4,ENCRYPTED\n\
1907                     Content-Domain: RFC822\n\
1908                     DEK-Info: DES-CBC,F8143EDE5960C597\n\
1909                     Originator-ID-Symmetric: linn@zendia.enet.dec.com,,\n\
1910                     Recipient-ID-Symmetric: linn@zendia.enet.dec.com,ptf-kmc,3\n\
1911                     Key-Info: DES-ECB,RSA-MD2,9FD3AAD2F2691B9A,\n\x20B70665BB9BF7CBCDA60195DB94F727D3\n\
1912                     \n\
1913                     SGVsbG8=\n\
1914                     -----END PRIVACY-ENHANCED MESSAGE-----\n";
1915
1916        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1917        assert_eq!(decoded.len(), 1);
1918        let block = decoded[0].as_ref().unwrap();
1919        assert_eq!(block.r#type, "PRIVACY-ENHANCED MESSAGE");
1920        assert_eq!(block.contents, b"Hello");
1921        assert!(block.headers.len() >= 4);
1922    }
1923
1924    // ─── Edge: base64 body with only padding ───
1925
1926    #[test]
1927    fn decode_body_only_padding() {
1928        let pem = b"-----BEGIN DATA-----\n=\n-----END DATA-----\n";
1929        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1930        assert_eq!(decoded.len(), 1);
1931        assert!(decoded[0].is_err(), "a single = is invalid base64");
1932    }
1933
1934    #[test]
1935    fn decode_body_only_padding_pair() {
1936        let pem = b"-----BEGIN DATA-----\n==\n-----END DATA-----\n";
1937        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1938        assert_eq!(decoded.len(), 1);
1939        assert!(decoded[0].is_err(), "just == is invalid base64");
1940    }
1941
1942    // ─── Edge: END line with no newline and matching label ───
1943
1944    #[test]
1945    fn decode_end_no_newline_matching() {
1946        let pem = b"-----BEGIN FOO-----\nZGF0YQ==\n-----END FOO-----";
1947        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1948        assert_eq!(decoded.len(), 1);
1949        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1950    }
1951
1952    // ─── End-to-end: Read PEM from RFC 7468 Section 5 explanatory text example ───
1953
1954    #[test]
1955    fn decode_rfc7468_section5_explanatory_text() {
1956        let pem = b"Subject: CN=Atlantis\n\
1957                     Issuer: CN=Atlantis\n\
1958                     Validity: from 7/9/2012 3:10:38 AM UTC to 7/9/2013 3:10:37 AM UTC\n\
1959                     -----BEGIN CERTIFICATE-----\n\
1960                     MIIBmTCCAUegAwIBAgIBKjAJBgUrDgMCHQUAMBMxETAPBgNVBAMTCEF0bGFudGlz\n\
1961                     MB4XDTEyMDcwOTAzMTAzOFoXDTEzMDcwOTAzMTAzN1owEzERMA8GA1UEAxMIQXRs\n\
1962                     YW50aXMwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAu+BXo+miabDIHHx+yquqzqNh\n\
1963                     Ryn/XtkJIIHVcYtHvIX+S1x5ErgMoHehycpoxbErZmVR4GCq1S2diNmRFZCRtQID\n\
1964                     AQABo4GJMIGGMAwGA1UdEwEB/wQCMAAwIAYDVR0EAQH/BBYwFDAOMAwGCisGAQQB\n\
1965                     gjcCARUDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDAzA1BgNVHQEE\n\
1966                     LjAsgBA0jOnSSuIHYmnVryHAdywMoRUwEzERMA8GA1UEAxMIQXRsYW50aXOCASow\n\
1967                     CQYFKw4DAh0FAANBAKi6HRBaNEL5R0n56nvfclQNaXiDT174uf+lojzA4lhVInc0\n\
1968                     ILwpnZ1izL4MlI9eCSHhVQBHEp2uQdXJB+d5Byg=\n\
1969                     -----END CERTIFICATE-----\n";
1970
1971        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1972        assert_eq!(decoded.len(), 1);
1973        let block = decoded[0].as_ref().unwrap();
1974        assert_eq!(block.r#type, "CERTIFICATE");
1975        assert!(!block.contents.is_empty());
1976    }
1977
1978    // ─── decode_base64: trailing whitespace before END marker ───
1979
1980    #[test]
1981    fn decode_base64_with_trailing_whitespace_in_line() {
1982        let pem = b"-----BEGIN DATA-----\n\
1983                     ZGF0YQ==   \n\
1984                     -----END DATA-----\n";
1985        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1986        assert_eq!(decoded.len(), 1);
1987        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
1988    }
1989
1990    // ─── decode_base64: leading whitespace in body line ───
1991
1992    #[test]
1993    fn decode_base64_with_leading_whitespace() {
1994        let pem = b"-----BEGIN DATA-----\n\
1995                        ZGF0YQ==\n\
1996                     -----END DATA-----\n";
1997        let decoded: Vec<Result<Block<'_>, PemError<'_>>> = decode(pem).collect();
1998        assert_eq!(decoded.len(), 1);
1999        assert_eq!(decoded[0].as_ref().unwrap().contents, b"data");
2000    }
2001}