diff options
author | Neal H. Walfield <neal@pep.foundation> | 2019-05-31 14:42:12 +0200 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2019-05-31 14:42:51 +0200 |
commit | 462dfe832f54da751dd521da185b0a468d3c4a28 (patch) | |
tree | 1dc329fbbc6c26850c546db2eb196584a417089e /rfc2822 | |
parent | be8bf91c14485a844a2c6143893105a21fd6f26e (diff) |
rfc2822: Provide a mechanism to escape display names
Diffstat (limited to 'rfc2822')
-rw-r--r-- | rfc2822/src/grammar.lalrpop | 59 | ||||
-rw-r--r-- | rfc2822/src/lib.rs | 58 |
2 files changed, 116 insertions, 1 deletions
diff --git a/rfc2822/src/grammar.lalrpop b/rfc2822/src/grammar.lalrpop index 0f6f6363..0df87445 100644 --- a/rfc2822/src/grammar.lalrpop +++ b/rfc2822/src/grammar.lalrpop @@ -246,7 +246,7 @@ atext_plus : String = { // The display-name in a name-addr production often includes a ., but -// is not quoted. The RFC even recommends supporting this variation. +// is not quoted. The RFC even recommends supporting this variant. other_or_dot : String = { <a:OTHER> => a.to_string(), <d:DOT> => d.to_string(), @@ -806,6 +806,63 @@ dtext : Token<'input> = { OTHER, } +// A production to escape a display name. +// +// Note: all characters are allowed in display names except NUL, CR and LF. +pub(crate) EscapedDisplayName : String = { + escaped_display_name +} + +escaped_display_name : String = { + escaped_display_name_token* => { + let (need_quote, last_was_space, s) = + <>.into_iter() + .fold((false, true, String::new()), + |(need_quote, last_was_space, mut s), (_need_quote, t)| { + s.push_str(&t); + let is_space = t == " "; + ((need_quote || _need_quote + || (last_was_space && is_space)), + is_space, + s) + }); + if need_quote || last_was_space { + format!("\"{}\"", s) + } else { + s + } + } +} + +// The bool means whether to put the whole thing in quotes. +escaped_display_name_token : (bool, String) = { + WSP => (false, <>.to_string()), + + // Needs to be put in quotes and escaped. + NO_WS_CTL => (true, format!("\\{}", <>)), + + // CR and LF are invalid in a display name. + + // Except for DQUOTE, specials can be put in quotes. + LPAREN => (true, <>.to_string()), + RPAREN => (true, <>.to_string()), + LANGLE => (true, <>.to_string()), + RANGLE => (true, <>.to_string()), + LBRACKET => (true, <>.to_string()), + RBRACKET => (true, <>.to_string()), + COLON => (true, <>.to_string()), + SEMICOLON => (true, <>.to_string()), + AT => (true, <>.to_string()), + BACKSLASH => (true, <>.to_string()), + COMMA => (true, <>.to_string()), + DOT => (true, <>.to_string()), + + // Needs to be put in quotes and escaped. + DQUOTE => (true, "\\\"".to_string()), + + OTHER => (false, <>.to_string()), +} + extern { type Location = usize; type Error = LexicalError; diff --git a/rfc2822/src/lib.rs b/rfc2822/src/lib.rs index 3eb6bc9d..eccbc009 100644 --- a/rfc2822/src/lib.rs +++ b/rfc2822/src/lib.rs @@ -128,6 +128,27 @@ fn parse_error_downcast<'a>(e: ParseError<usize, lexer::Token<'a>, LexicalError> } } +/// A `DisplayName`. +pub struct Name { +} + +impl Name { + /// Returns an escaped version of `name`, which is appropriate for + /// use in a `name-addr`. + /// + /// Returns an error if `name` contains characters that cannot be + /// escaped (NUL, CR and LF). + pub fn escaped<S>(name: S) -> Result<String> + where S: AsRef<str> + { + let name = name.as_ref(); + + let lexer = lexer::Lexer::new(name); + grammar::EscapedDisplayNameParser::new().parse(name, lexer) + .map_err(|e| parse_error_downcast(e).into()) + } +} + /// A parsed RFC 2822 `addr-spec`. /// /// The address must not include angle brackets. That is, this parser @@ -1597,4 +1618,41 @@ mod tests { c("<example@foo.com>", false); c("example@@foo.com", false); } + + #[test] + fn name_escape_test() { + fn c(raw: &str, escaped_expected: &str) { + eprintln!("\nInput: {:?}", raw); + eprintln!("Expecting escaped version to be: {:?}", escaped_expected); + let escaped_got = Name::escaped(raw).expect("Parse error"); + eprintln!(" Escaped version is: {:?}", escaped_got); + + // There are often multiple ways to validly escape a name. + // This check relies on knowing how a name is escaped. In + // other words: if the implementation changes and this + // test fails, then this failure may not be indicative of + // a bug; we may just need to adjust what this test + // expects. + assert_eq!(escaped_got, escaped_expected); + + // Make sure when we parse it, we get the original back. + let lexer = lexer::Lexer::new(&escaped_got); + let raw_got = grammar::DisplayNameParser::new() + .parse(&escaped_got, lexer) + .expect(&format!("Parse error: {}", escaped_got)); + + eprintln!("Parsing escaped version, got: {:?}", raw_got); + + assert_eq!(raw_got, vec![ Component::Text(raw.to_string()) ]); + } + + c("Foo Q. Bar", r#""Foo Q. Bar""#); + c(r#""Foo Q. Bar""#, r#""\"Foo Q. Bar\"""#); + c(r#""Foo Q Bar""#, r#""\"Foo Q Bar\"""#); + c("Foo, the Bar", r#""Foo, the Bar""#); + + // Make sure leading and trailing spaces are quoted. + c(" Foo Bar", r#"" Foo Bar""#); + c("Foo Bar ", r#""Foo Bar ""#); + } } |