summaryrefslogtreecommitdiffstats
path: root/rfc2822
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@pep.foundation>2019-05-31 14:42:12 +0200
committerNeal H. Walfield <neal@pep.foundation>2019-05-31 14:42:51 +0200
commit462dfe832f54da751dd521da185b0a468d3c4a28 (patch)
tree1dc329fbbc6c26850c546db2eb196584a417089e /rfc2822
parentbe8bf91c14485a844a2c6143893105a21fd6f26e (diff)
rfc2822: Provide a mechanism to escape display names
Diffstat (limited to 'rfc2822')
-rw-r--r--rfc2822/src/grammar.lalrpop59
-rw-r--r--rfc2822/src/lib.rs58
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 ""#);
+ }
}