Skip to content

Commit a0f5ad0

Browse files
committed
pemfile: also trim leading contiguous content whitespace
RFC 7468 §2[0] says: > parsers SHOULD ignore whitespace Previously we only stripped the trailing whitespace from the base64 content lines within the PEM section boundaries. This branch updates our processing to additionally trim leading contiguous whitespace. This felt more sensible than outright ignoring whitespace (e.g. from the middle of content) and will be sufficient to resolve the real world PEM files we've seen with leading whitespace. We base our implementation on the stdlib's unstable `[u8]::trim_ascii` fn, which we can't (yet) use directly due to our MSRV/stable rust requirements. A TODO is left for future cleanup. [0]: https://www.rfc-editor.org/rfc/rfc7468#section-2
1 parent dad014e commit a0f5ad0

File tree

3 files changed

+92
-8
lines changed

3 files changed

+92
-8
lines changed

src/pemfile.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,8 @@ fn read_one_impl(
215215
}
216216

217217
if section.is_some() {
218-
let mut trim = 0;
219-
for &b in line.iter().rev() {
220-
match b {
221-
b'\n' | b'\r' | b' ' => trim += 1,
222-
_ => break,
223-
}
224-
}
225-
b64buf.extend(&line[..line.len() - trim]);
218+
// Extend b64buf without leading or trailing whitespace
219+
b64buf.extend(trim_ascii(line));
226220
}
227221

228222
Ok(ControlFlow::Continue(()))
@@ -266,6 +260,35 @@ fn read_until_newline<R: io::BufRead + ?Sized>(
266260
}
267261
}
268262

263+
/// Trim contiguous leading and trailing whitespace from `line`.
264+
///
265+
/// We use [u8::is_ascii_whitespace] to determine what is whitespace.
266+
// TODO(XXX): Replace with `[u8]::trim_ascii` once stabilized[0] and available in our MSRV.
267+
// [0]: https://github.com/rust-lang/rust/issues/94035
268+
const fn trim_ascii(line: &[u8]) -> &[u8] {
269+
let mut bytes = line;
270+
271+
// Note: A pattern matching based approach (instead of indexing) allows
272+
// making the function const.
273+
while let [first, rest @ ..] = bytes {
274+
if first.is_ascii_whitespace() {
275+
bytes = rest;
276+
} else {
277+
break;
278+
}
279+
}
280+
281+
while let [rest @ .., last] = bytes {
282+
if last.is_ascii_whitespace() {
283+
bytes = rest;
284+
} else {
285+
break;
286+
}
287+
}
288+
289+
bytes
290+
}
291+
269292
/// Extract and return all PEM sections by reading `rd`.
270293
#[cfg(feature = "std")]
271294
pub fn read_all(rd: &mut dyn io::BufRead) -> impl Iterator<Item = Result<Item, io::Error>> + '_ {
@@ -284,3 +307,31 @@ mod base64 {
284307
);
285308
}
286309
use self::base64::Engine;
310+
311+
#[cfg(test)]
312+
mod tests {
313+
#[test]
314+
fn test_trim_ascii() {
315+
let tests: &[(&[u8], &[u8])] = &[
316+
(b"", b""),
317+
(b" hello world ", b"hello world"),
318+
(b" hello\t\r\nworld ", b"hello\t\r\nworld"),
319+
(b"\n\r \ttest\t \r\n", b"test"),
320+
(b" \r\n ", b""),
321+
(b"no trimming needed", b"no trimming needed"),
322+
(
323+
b"\n\n content\n\n more content\n\n",
324+
b"content\n\n more content",
325+
),
326+
];
327+
328+
for &(input, expected) in tests {
329+
assert_eq!(
330+
super::trim_ascii(input),
331+
expected,
332+
"Failed for input: {:?}",
333+
input,
334+
);
335+
}
336+
}
337+
}

tests/data/whitespace-prefix.crt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDaTCCAlGgAwIBAgIJAOq/zL+84IswMA0GCSqGSIb3DQEBCwUAMFoxCzAJBgNV
3+
BAYTAlVTMQswCQYDVQQIDAJOQzEMMAoGA1UEBwwDUlRQMQ8wDQYDVQQKDAZOZXRB
4+
cHAxDTALBgNVBAsMBEVTSVMxEDAOBgNVBAMMB1NTRk1DQ0EwHhcNMTcxMTAxMjEw
5+
OTQyWhcNMjcxMDMwMjEwOTQyWjBaMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMx
6+
DDAKBgNVBAcMA1JUUDEPMA0GA1UECgwGTmV0QXBwMQ0wCwYDVQQLDARFU0lTMRAw
7+
DgYDVQQDDAdTU0ZNQ0NBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
8+
iaD9Ee0Yrdka0I+9GTJBIW/Fp5JU6kyjaxfOldW/R9lEubegXQFhDD2Xi1HZ+fTM
9+
f224glB9xLJXAHhipRK01C2MgC4kSH75WL1iAiYeOBloExqmK6OCX+sdyO7RXm/H
10+
Ra9tN2INWdvyO2pnmxsSnq56mCMsUZLtrRKp89FWgcxLg5r8QxH7xwfh5k54rxjE
11+
144TD9yrIiQOgRSIRHUrVJ9l/F/gnwzP8wcNABeXwN71Mzl7mliPA703kONQIAyU
12+
0E0tLpmy/U8dZdMmTBZGB7jI9f95Hl1RunfwhR371a6z38kgkvwrLzl4qflfsPjw
13+
K9n4omNk9rCH9H9tWkxxjwIDAQABozIwMDAdBgNVHQ4EFgQU/bFyCCnqdDFKlQBJ
14+
ExtV6wcMYkEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOQMs
15+
Pz2iBD1+3RcSOsahB36WAwPCjgPiXXXpU+Zri11+m6I0Lq+OWtf+YgaQ8ylLmCQd
16+
0p1wHlYA4qo896SycrhTQfy9GlS/aQqN192k3oBGoJcMIUnGUBGuEvyZ2aDUfkzy
17+
JUqBe+0KaT7pkvvbRL7VUz34I7ouq9fQIRZ26vUDLTY3KM1n/DXBj3e30GHGMV3K
18+
NN2twuLXPNjnryfgpliHU1rwV7r1WvrCVn4StjimP2bO5HGqD/SbiYUL2M9LOuLK
19+
6mqY4OHumYXq3k7CHrvt0FepsN0L14LYEt1LvpPDFWP3SdN4z4KqT9AGqBaJnhhl
20+
Qiq8GWnAChspdBLxCg==
21+
-----END CERTIFICATE-----

tests/integration.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,15 @@ fn different_line_endings() {
196196
assert!(matches!(cert, rustls_pemfile::Item::X509Certificate(_)));
197197
}
198198
}
199+
200+
#[test]
201+
fn whitespace_prefix() {
202+
let items = rustls_pemfile::read_all(&mut BufReader::new(
203+
&include_bytes!("data/whitespace-prefix.crt")[..],
204+
))
205+
.collect::<Result<Vec<_>, _>>()
206+
.unwrap();
207+
208+
assert_eq!(items.len(), 1);
209+
assert!(matches!(items[0], rustls_pemfile::Item::X509Certificate(_)));
210+
}

0 commit comments

Comments
 (0)