Skip to main content

crypto/encoding/
pem.rs

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