1pub mod error;
2
3use std::{
4 borrow::{Borrow, Cow},
5 str::{FromStr, Utf8Error},
6 string::FromUtf8Error,
7};
8
9pub use error::XfccError;
10use nom::{
11 branch::alt,
12 bytes::complete::{escaped_transform, is_not, tag, take, take_till},
13 character::complete::char,
14 combinator::{eof, map, map_res, value},
15 multi::{separated_list0, separated_list1},
16 sequence::{delimited, tuple},
17 AsChar, IResult,
18};
19use strum::EnumString;
20
21const DOUBLE_QUOTE: u8 = b'"';
22const SLASH: u8 = b'\\';
23const COMMA: u8 = b',';
24const SEMICOLON: u8 = b';';
25const EQUAL: u8 = b'=';
26
27#[derive(Debug, PartialEq, Eq, EnumString, strum::Display)]
29pub enum PairKey {
30 By,
32 Hash,
34 Cert,
36 Chain,
38 Subject,
40 #[strum(serialize = "URI")]
42 Uri,
43 #[strum(serialize = "DNS")]
45 Dns,
46}
47
48pub type ElementRaw<'a> = Vec<(PairKey, Cow<'a, str>)>;
50
51#[derive(Debug, PartialEq, Eq, Default)]
53pub struct Element<'a> {
54 pub by: Vec<Cow<'a, str>>,
55 pub hash: Option<Cow<'a, str>>,
56 pub cert: Option<Cow<'a, str>>,
57 pub chain: Option<Cow<'a, str>>,
58 pub subject: Option<Cow<'a, str>>,
59 pub uri: Vec<Cow<'a, str>>,
60 pub dns: Vec<Cow<'a, str>>,
61}
62
63impl<'a> TryFrom<ElementRaw<'a>> for Element<'a> {
64 type Error = XfccError<'a>;
65
66 fn try_from(element_raw: ElementRaw<'a>) -> Result<Self, Self::Error> {
67 let mut element = Self::default();
68 for (key, value) in element_raw {
69 if value.is_empty() {
70 continue;
71 }
72 macro_rules! error_if_duplicate {
73 ($key_type:expr, $key_field:ident) => {
74 if element.$key_field.is_some() {
75 return Err(XfccError::DuplicatePairKey($key_type));
76 } else {
77 element.$key_field = Some(value);
78 }
79 };
80 }
81 match key {
82 PairKey::By => element.by.push(value),
83 PairKey::Hash => error_if_duplicate!(PairKey::Hash, hash),
84 PairKey::Cert => error_if_duplicate!(PairKey::Cert, cert),
85 PairKey::Chain => error_if_duplicate!(PairKey::Chain, chain),
86 PairKey::Subject => error_if_duplicate!(PairKey::Subject, subject),
87 PairKey::Uri => element.uri.push(value),
88 PairKey::Dns => element.dns.push(value),
89 }
90 }
91 Ok(element)
92 }
93}
94
95fn to_cow_str(s: &[u8]) -> Result<Cow<str>, Utf8Error> {
96 std::str::from_utf8(s).map(Cow::from)
97}
98
99fn to_owned_cow_str(s: Vec<u8>) -> Result<Cow<'static, str>, FromUtf8Error> {
100 String::from_utf8(s).map(Cow::from)
101}
102
103fn empty_quoted_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
104 map(value("", tag([DOUBLE_QUOTE, DOUBLE_QUOTE])), Cow::from)(s)
105}
106
107fn escaped_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
108 map_res(
109 escaped_transform(
110 is_not(&[DOUBLE_QUOTE, SLASH][..]),
111 SLASH.as_char(),
112 take(1u8),
113 ),
114 to_owned_cow_str,
115 )(s)
116}
117
118fn quoted_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
119 alt((
120 empty_quoted_value,
121 delimited(
122 char(DOUBLE_QUOTE.as_char()),
123 escaped_value,
124 char(DOUBLE_QUOTE.as_char()),
125 ),
126 ))(s)
127}
128
129fn unquoted_value(s: &[u8]) -> IResult<&[u8], Cow<str>> {
130 map_res(
131 alt((
132 take_till(|c| c == COMMA || c == SEMICOLON || c == EQUAL),
133 eof,
134 )),
135 to_cow_str,
136 )(s)
137}
138
139fn pair_key(s: &[u8]) -> IResult<&[u8], PairKey> {
140 map_res(
141 alt((
142 tag("By"),
143 tag("Hash"),
144 tag("Cert"),
145 tag("Chain"),
146 tag("Subject"),
147 tag("URI"),
148 tag("DNS"),
149 )),
150 |name| {
151 to_cow_str(name).map(|name| match PairKey::from_str(name.borrow()) {
152 Ok(key) => key,
153 Err(_) => unreachable!("Failed to parse PairKey while nom succeeded"),
154 })
155 },
156 )(s)
157}
158
159fn pair(s: &[u8]) -> IResult<&[u8], (PairKey, Cow<str>)> {
160 let (s, (key, _, value)) =
161 tuple((pair_key, char('='), alt((quoted_value, unquoted_value))))(s)?;
162 Ok((s, (key, value)))
163}
164
165fn element(s: &[u8]) -> IResult<&[u8], ElementRaw> {
166 separated_list1(char(';'), pair)(s)
167}
168
169pub fn element_raw_list(s: &[u8]) -> IResult<&[u8], Vec<ElementRaw>> {
192 separated_list0(char(','), element)(s)
193}
194
195pub fn element_list(s: &[u8]) -> Result<Vec<Element>, XfccError> {
224 let (trailing, raw_list) = element_raw_list(s)?;
225
226 if !trailing.is_empty() {
227 return Err(XfccError::TrailingSequence(trailing));
228 }
229
230 let mut elements = vec![];
231 raw_list
232 .into_iter()
233 .try_for_each(|element_raw| -> Result<(), XfccError> {
234 elements.push(Element::try_from(element_raw)?);
235 Ok(())
236 })?;
237
238 Ok(elements)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn basic_escaped_value_test() {
247 let input = br#"hello, \"world\"!"#;
248 assert_eq!(
249 escaped_value(input),
250 Ok((&[][..], Cow::from(r#"hello, "world"!"#)))
251 );
252 }
253
254 #[test]
255 fn unnecessarily_escaped_value_test() {
256 let input = br#"\h\e\l\l\o, \"world\"!"#;
257 assert_eq!(
258 escaped_value(input),
259 Ok((&[][..], Cow::from(r#"hello, "world"!"#)))
260 );
261 }
262
263 #[test]
264 fn utf8_escaped_value_test() {
265 let input: Vec<u8> = "こんにちは"
266 .bytes()
267 .flat_map(|b| [b'\\', b].into_iter())
268 .collect();
269 assert_eq!(
270 escaped_value(&input),
271 Ok((&[][..], Cow::from("こんにちは")))
272 );
273 }
274
275 #[test]
276 fn invalid_utf8_escaped_value_test() {
277 let mut input: Vec<u8> = "こんにちは".bytes().collect();
278 input.pop().unwrap();
279 assert_eq!(
280 escaped_value(&input),
281 Err(nom::Err::Error(nom::error::Error {
282 input: &input[..],
283 code: nom::error::ErrorKind::MapRes
284 }))
285 );
286 }
287
288 #[test]
289 fn basic_quoted_value_test() {
290 let input = br#""hello, \"world\"!""#;
291 assert_eq!(
292 quoted_value(input),
293 Ok((&[][..], Cow::from(r#"hello, "world"!"#)))
294 );
295 }
296
297 #[test]
298 fn empty_quoted_value_test() {
299 let input = br#""""#;
300 assert_eq!(empty_quoted_value(input), Ok((&[][..], Cow::from(""))));
301 assert_eq!(quoted_value(input), Ok((&[][..], Cow::from(""))));
302 }
303
304 #[test]
305 fn basic_unquoted_value_test() {
306 let input = b"hello! world!;";
307 let parsed = unquoted_value(input).unwrap();
308 assert_eq!(parsed, (&[SEMICOLON][..], Cow::from("hello! world!")));
309 assert!(matches!(parsed.1, Cow::Borrowed(_)));
310
311 let input = b"hello! world!";
312 let parsed = unquoted_value(input).unwrap();
313 assert_eq!(parsed, (&[][..], Cow::from("hello! world!")));
314 assert!(matches!(parsed.1, Cow::Borrowed(_)));
315 }
316
317 #[test]
318 fn must_be_quoted_in_unquoted_value_test() {
319 let input = b"hello, world!;";
320 assert_eq!(
321 unquoted_value(input),
322 Ok((&b", world!;"[..], Cow::from("hello")))
323 );
324 }
325
326 #[test]
327 fn basic_pair_key_test() {
328 let input = b"Chain";
329 assert_eq!(pair_key(input), Ok((&[][..], PairKey::Chain)));
330 }
331
332 #[test]
333 fn invalid_pair_key_test() {
334 let input = b"Example";
335 assert_eq!(
336 pair_key(input),
337 Err(nom::Err::Error(nom::error::Error {
338 input: &input[..],
339 code: nom::error::ErrorKind::Tag
340 }))
341 );
342 }
343
344 #[test]
345 fn basic_pair_test() {
346 let input = br#"Chain=hello! world!;"#;
347 let parsed = pair(input).unwrap();
348 assert_eq!(
349 parsed,
350 (&b";"[..], (PairKey::Chain, Cow::from("hello! world!")))
351 );
352 assert!(matches!(parsed.1 .1, Cow::Borrowed(_)));
353
354 let input = br#"Chain=hello! world!"#;
355 let parsed = pair(input).unwrap();
356 assert_eq!(
357 parsed,
358 (&[][..], (PairKey::Chain, Cow::from("hello! world!")))
359 );
360 assert!(matches!(parsed.1 .1, Cow::Borrowed(_)));
361 }
362
363 #[test]
364 fn quoted_value_pair_test() {
365 let input = br#"Chain="hello! world!";"#;
366 let parsed = pair(input).unwrap();
367 assert_eq!(
368 parsed,
369 (&b";"[..], (PairKey::Chain, Cow::from("hello! world!")))
370 );
371 assert!(matches!(parsed.1 .1, Cow::Owned(_)));
372
373 let input = br#"Chain="hello! world!""#;
374 let parsed = pair(input).unwrap();
375 assert_eq!(
376 parsed,
377 (&[][..], (PairKey::Chain, Cow::from("hello! world!")))
378 );
379 assert!(matches!(parsed.1 .1, Cow::Owned(_)));
380 }
381
382 #[test]
383 fn basic_element_test() {
384 let input = br#"By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com"#;
385 assert_eq!(
386 element(input),
387 Ok((
388 &[][..],
389 (vec![
390 (PairKey::By, Cow::from("http://frontend.lyft.com")),
391 (
392 PairKey::Hash,
393 Cow::from(
394 "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
395 )
396 ),
397 (
398 PairKey::Subject,
399 Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")
400 ),
401 (PairKey::Uri, Cow::from("http://testclient.lyft.com"))
402 ])
403 ))
404 );
405 }
406
407 #[test]
408 fn empty_value_in_element_test() {
409 let input = br#"By=;By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com"#;
410 assert_eq!(
411 element(input),
412 Ok((
413 &[][..],
414 (vec![
415 (PairKey::By, Cow::from("")),
416 (PairKey::By, Cow::from("http://frontend.lyft.com")),
417 (
418 PairKey::Hash,
419 Cow::from(
420 "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
421 )
422 ),
423 (
424 PairKey::Subject,
425 Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")
426 ),
427 (PairKey::Uri, Cow::from("http://testclient.lyft.com"))
428 ])
429 ))
430 );
431 }
432
433 #[test]
434 fn basic_element_raw_list_test() {
435 let input = br#"By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com,By=http://example.com;By=http://instance.com"#;
436 assert_eq!(
437 element_raw_list(input),
438 Ok((
439 &[][..],
440 vec![
441 vec![
442 (PairKey::By, Cow::from("http://frontend.lyft.com")),
443 (
444 PairKey::Hash,
445 Cow::from(
446 "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
447 )
448 ),
449 (
450 PairKey::Subject,
451 Cow::from("/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client")
452 ),
453 (PairKey::Uri, Cow::from("http://testclient.lyft.com"))
454 ],
455 vec![
456 (PairKey::By, Cow::from("http://example.com")),
457 (PairKey::By, Cow::from("http://instance.com"))
458 ]
459 ]
460 ))
461 );
462
463 let input = br#"Hash=hash;Cert="-----BEGIN%20CERTIFICATE-----%0cert%0A-----END%20CERTIFICATE-----%0A";Subject="CN=hello,OU=hello,O=Acme\, Inc.";URI=;DNS=hello.west.example.com;DNS=hello.east.example.com,By=spiffe://mesh.example.com/ns/hellons/sa/hellosa;Hash=again;Subject="";URI=spiffe://mesh.example.com/ns/otherns/sa/othersa"#;
465 assert_eq!(
466 element_raw_list(input),
467 Ok((
468 &[][..],
469 vec![
470 vec![
471 (PairKey::Hash, Cow::from("hash")),
472 (
473 PairKey::Cert,
474 Cow::from("-----BEGIN%20CERTIFICATE-----%0cert%0A-----END%20CERTIFICATE-----%0A")
475 ),
476 (PairKey::Subject, Cow::from("CN=hello,OU=hello,O=Acme, Inc.")),
477 (PairKey::Uri, Cow::from("")),
478 (PairKey::Dns, Cow::from("hello.west.example.com")),
479 (PairKey::Dns, Cow::from("hello.east.example.com"))
480 ],
481 vec![
482 (
483 PairKey::By,
484 Cow::from("spiffe://mesh.example.com/ns/hellons/sa/hellosa")
485 ),
486 (PairKey::Hash, Cow::from("again")),
487 (PairKey::Subject, Cow::from("")),
488 (
489 PairKey::Uri,
490 Cow::from("spiffe://mesh.example.com/ns/otherns/sa/othersa")
491 )
492 ]
493 ]
494 ))
495 );
496 }
497
498 #[test]
499 fn basic_element_list_test() {
500 let input = br#"By=http://frontend.lyft.com;Hash=468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688;Subject="/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client";URI=http://testclient.lyft.com,By=http://example.com;By=http://instance.com"#;
501 let certificates = element_list(input).unwrap();
502 assert_eq!(certificates.len(), 2);
503 assert_eq!(
504 certificates[0],
505 Element {
506 by: vec![Cow::from("http://frontend.lyft.com")],
507 hash: Some(Cow::from(
508 "468ed33be74eee6556d90c0149c1309e9ba61d6425303443c0748a02dd8de688"
509 )),
510 cert: None,
511 chain: None,
512 subject: Some(Cow::from(
513 "/C=US/ST=CA/L=San Francisco/OU=Lyft/CN=Test Client"
514 )),
515 uri: vec![Cow::from("http://testclient.lyft.com")],
516 dns: vec![],
517 }
518 );
519 assert_eq!(
520 certificates[1],
521 Element {
522 by: vec![
523 Cow::from("http://example.com"),
524 Cow::from("http://instance.com")
525 ],
526 hash: None,
527 cert: None,
528 chain: None,
529 subject: None,
530 uri: vec![],
531 dns: vec![],
532 }
533 );
534 }
535
536 #[test]
537 fn empty_subject_element_list_test() {
538 let input = br#"By=http://example.com;Subject="""#;
539 let certificates = element_list(input).unwrap();
540 assert_eq!(certificates.len(), 1);
541 assert_eq!(
542 certificates[0],
543 Element {
544 by: vec![Cow::from("http://example.com"),],
545 hash: None,
546 cert: None,
547 chain: None,
548 subject: None,
549 uri: vec![],
550 dns: vec![],
551 }
552 );
553 }
554
555 #[test]
556 fn duplicate_pair_key_test() {
557 let input = br#"By=http://example.com;Hash=hash1;Hash=hash2"#;
558 assert_eq!(
559 element_raw_list(input),
560 Ok((
561 &[][..],
562 vec![vec![
563 (PairKey::By, Cow::from("http://example.com")),
564 (PairKey::Hash, Cow::from("hash1")),
565 (PairKey::Hash, Cow::from("hash2")),
566 ]]
567 ))
568 );
569 assert_eq!(
570 element_list(input),
571 Err(XfccError::DuplicatePairKey(PairKey::Hash))
572 );
573 }
574
575 #[test]
576 fn trailing_characters_test() {
577 let input = br#"By=http://example.com;Hash=hash,URI"#;
578 assert_eq!(
579 element_raw_list(input),
580 Ok((
581 &b",URI"[..],
582 vec![vec![
583 (PairKey::By, Cow::from("http://example.com")),
584 (PairKey::Hash, Cow::from("hash")),
585 ]]
586 ))
587 );
588 assert_eq!(
589 element_list(input),
590 Err(XfccError::TrailingSequence(&b",URI"[..]))
591 );
592 }
593}