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