use std::borrow::Cow;
use std::fmt;
use std::str;
use std::hash::{Hash, Hasher};
use std::cmp::Ordering;
use std::sync::OnceLock;
#[cfg(test)]
use quickcheck::{Arbitrary, Gen};
use regex::Regex;
use crate::Result;
use crate::packet;
use crate::Packet;
use crate::Error;
use crate::policy::HashAlgoSecurity;
#[derive(Clone, Debug)]
pub struct ConventionallyParsedUserID {
userid: String,
name: Option<(usize, usize)>,
comment: Option<(usize, usize)>,
email: Option<(usize, usize)>,
uri: Option<(usize, usize)>,
}
assert_send_and_sync!(ConventionallyParsedUserID);
impl ConventionallyParsedUserID {
pub fn new<S>(userid: S) -> Result<Self>
where S: Into<String>
{
Ok(Self::parse(userid.into())?)
}
pub fn name(&self) -> Option<&str> {
self.name.map(|(s, e)| &self.userid[s..e])
}
pub fn comment(&self) -> Option<&str> {
self.comment.map(|(s, e)| &self.userid[s..e])
}
pub fn email(&self) -> Option<&str> {
self.email.map(|(s, e)| &self.userid[s..e])
}
pub fn uri(&self) -> Option<&str> {
self.uri.map(|(s, e)| &self.userid[s..e])
}
fn parse(userid: String) -> Result<Self> {
fn user_id_parser() -> &'static Regex {
use std::sync::OnceLock;
static USER_ID_PARSER: OnceLock<Regex> = OnceLock::new();
USER_ID_PARSER.get_or_init(|| {
let ws_bare = " ";
let ws = format!("[{}]", ws_bare);
let optional_ws = format!("(?:{}*)", ws);
let comment_specials_bare = r#"<>\[\]:;@\\,.""#;
let _comment_specials
= format!("[{}]", comment_specials_bare);
let atext_specials_bare = r#"()\[\]:;@\\,.""#;
let _atext_specials =
format!("[{}]", atext_specials_bare);
let atext_bare
= "-A-Za-z0-9!#$%&'*+/=?^_`{|}~\u{80}-\u{10ffff}";
let atext = format!("[{}]", atext_bare);
let dot_atom_text
= format!(r"(?:{}+(?:\.{}+)*)", atext, atext);
let name_char_start
= format!("[{}{}]",
atext_bare, atext_specials_bare);
let name_char_rest
= format!("[{}{}{}]",
atext_bare, atext_specials_bare, ws_bare);
let name
= format!("(?:{}{}*?)", name_char_start, name_char_rest);
let comment_char
= format!("[{}{}{}]",
atext_bare, comment_specials_bare, ws_bare);
let comment = |prefix| {
format!(r#"(?:\({}(?P<{}_comment>{}*?){}\))"#,
optional_ws, prefix, comment_char, optional_ws)
};
let addr_spec
= format!("(?:{}@{})", dot_atom_text, dot_atom_text);
let uri = |prefix| {
let symbols = "-{}0-9._~%!$&'()*+,;=:@\\[\\]";
let ascii_alpha = "a-zA-Z";
let utf8_alpha = "a-zA-Z\u{80}-\u{10ffff}";
let schema
= format!("(?:[{}][-+.{}0-9]*:)",
ascii_alpha, ascii_alpha);
let rest = format!("(?:[{}{}/\\?#]+)",
symbols, utf8_alpha);
format!("(?P<{}_uri>{}{})",
prefix, schema, rest)
};
let raw_addr_spec
= format!("(?P<raw_addr_spec>{})", addr_spec);
let raw_uri = format!("(?:{})", uri("raw"));
let wrapped_addr_spec
= format!("{}(?P<wrapped_addr_spec_name>{})?{}\
(?:{})?{}\
<(?P<wrapped_addr_spec>{})>",
optional_ws, name, optional_ws,
comment("wrapped_addr_spec"), optional_ws,
addr_spec);
let wrapped_uri
= format!("{}(?P<wrapped_uri_name>{})?{}\
(?:{})?{}\
<(?:{})>",
optional_ws, name, optional_ws,
comment("wrapped_uri"), optional_ws,
uri("wrapped"));
let bare_name
= format!("{}(?P<bare_name>{}){}\
(?:{})?{}",
optional_ws, name, optional_ws,
comment("bare"), optional_ws);
let pgp_uid_convention
= format!("^(?:{}|{}|{}|{}|{})$",
raw_addr_spec, raw_uri,
wrapped_addr_spec, wrapped_uri,
bare_name);
Regex::new(&pgp_uid_convention).unwrap()
})
}
if let Some(cap) = user_id_parser().captures_iter(&userid).next() {
let to_range = |m: regex::Match| (m.start(), m.end());
if let Some(email) = cap.name("raw_addr_spec") {
let email = Some(to_range(email));
Ok(ConventionallyParsedUserID {
userid,
name: None,
comment: None,
email,
uri: None,
})
} else if let Some(uri) = cap.name("raw_uri") {
let uri = Some(to_range(uri));
Ok(ConventionallyParsedUserID {
userid,
name: None,
comment: None,
email: None,
uri,
})
} else if let Some(email) = cap.name("wrapped_addr_spec") {
let name = cap.name("wrapped_addr_spec_name").map(to_range);
let comment = cap.name("wrapped_addr_spec_comment").map(to_range);
let email = Some(to_range(email));
Ok(ConventionallyParsedUserID {
userid,
name,
comment,
email,
uri: None,
})
} else if let Some(uri) = cap.name("wrapped_uri") {
let name = cap.name("wrapped_uri_name").map(to_range);
let comment = cap.name("wrapped_uri_comment").map(to_range);
let uri = Some(to_range(uri));
Ok(ConventionallyParsedUserID {
userid,
name,
comment,
email: None,
uri,
})
} else if let Some(name) = cap.name("bare_name") {
let name = to_range(name);
let comment = cap.name("bare_comment").map(to_range);
Ok(ConventionallyParsedUserID {
userid,
name: Some(name),
comment,
email: None,
uri: None,
})
} else {
panic!("Unexpected result");
}
} else {
Err(Error::InvalidArgument(
"Failed to parse UserID".into()).into())
}
}
}
pub struct UserID {
pub(crate) common: packet::Common,
value: Cow<'static, [u8]>,
hash_algo_security: OnceLock<HashAlgoSecurity>,
parsed: OnceLock<ConventionallyParsedUserID>,
}
assert_send_and_sync!(UserID);
impl UserID {
pub const fn from_static_bytes(u: &'static [u8]) -> Self {
UserID {
common: packet::Common::new(),
hash_algo_security: OnceLock::new(),
value: Cow::Borrowed(u),
parsed: OnceLock::new(),
}
}
}
impl From<Vec<u8>> for UserID {
fn from(u: Vec<u8>) -> Self {
UserID {
common: Default::default(),
hash_algo_security: Default::default(),
value: Cow::Owned(u),
parsed: Default::default(),
}
}
}
impl From<&[u8]> for UserID {
fn from(u: &[u8]) -> Self {
u.to_vec().into()
}
}
impl<'a> From<&'a str> for UserID {
fn from(u: &'a str) -> Self {
u.as_bytes().into()
}
}
impl From<String> for UserID {
fn from(u: String) -> Self {
u.into_bytes().into()
}
}
impl<'a> From<Cow<'a, str>> for UserID {
fn from(u: Cow<'a, str>) -> Self {
match u {
Cow::Owned(u) => u.into(),
Cow::Borrowed(u) => u.into(),
}
}
}
impl fmt::Display for UserID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let userid = String::from_utf8_lossy(&self.value[..]);
write!(f, "{}", userid)
}
}
impl fmt::Debug for UserID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let userid = String::from_utf8_lossy(&self.value[..]);
f.debug_struct("UserID")
.field("value", &userid)
.finish()
}
}
impl PartialEq for UserID {
fn eq(&self, other: &UserID) -> bool {
self.common == other.common
&& self.value == other.value
}
}
impl Eq for UserID {
}
impl PartialOrd for UserID {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for UserID {
fn cmp(&self, other: &Self) -> Ordering {
self.common.cmp(&other.common).then_with(
|| self.value.cmp(&other.value))
}
}
impl Hash for UserID {
fn hash<H: Hasher>(&self, state: &mut H) {
self.common.hash(state);
self.value.hash(state);
}
}
impl Clone for UserID {
fn clone(&self) -> Self {
UserID {
common: self.common.clone(),
hash_algo_security: self.hash_algo_security.clone(),
value: self.value.clone(),
parsed: if let Some(p) = self.parsed.get() {
p.clone().into()
} else {
OnceLock::new()
},
}
}
}
impl UserID {
fn assemble(name: Option<&str>, comment: Option<&str>,
address: &str, check_address: bool)
-> Result<Self>
{
let mut value = String::with_capacity(64);
if let Some(ref name) = name {
match ConventionallyParsedUserID::new(name.to_string()) {
Err(err) =>
return Err(err.context(format!(
"Validating name ({:?})",
name))),
Ok(p) => {
if !(p.name().is_some()
&& p.comment().is_none()
&& p.email().is_none()) {
return Err(Error::InvalidArgument(
format!("Invalid name ({:?})", name)).into());
}
}
}
value.push_str(name);
}
if let Some(ref comment) = comment {
match ConventionallyParsedUserID::new(
format!("x ({})", comment))
{
Err(err) =>
return Err(err.context(format!(
"Validating comment ({:?})",
comment))),
Ok(p) => {
if !(p.name().is_some()
&& p.comment().is_some()
&& p.email().is_none()) {
return Err(Error::InvalidArgument(
format!("Invalid comment ({:?})", comment)).into());
}
}
}
if !value.is_empty() {
value.push(' ');
}
value.push('(');
value.push_str(comment);
value.push(')');
}
if check_address {
match ConventionallyParsedUserID::new(
format!("<{}>", address))
{
Err(err) =>
return Err(err.context(format!(
"Validating address ({:?})",
address))),
Ok(p) => {
if !(p.name().is_none()
&& p.comment().is_none()
&& p.email().is_some()) {
return Err(Error::InvalidArgument(
format!("Invalid address address ({:?})", address))
.into());
}
}
}
}
if !value.is_empty() {
value.push(' ');
}
value.push('<');
value.push_str(address);
value.push('>');
if check_address {
match ConventionallyParsedUserID::new(value.clone())
{
Err(err) =>
return Err(err.context(format!(
"Validating User ID ({:?})",
value))),
Ok(p) => {
if !(p.name().is_none() == name.is_none()
&& p.comment().is_none() == comment.is_none()
&& p.email().is_some()) {
return Err(Error::InvalidArgument(
format!("Invalid User ID ({:?})", value)).into());
}
}
}
}
Ok(UserID::from(value))
}
pub fn hash_algo_security(&self) -> HashAlgoSecurity {
self.hash_algo_security
.get_or_init(|| {
UserID::determine_hash_algo_security(&self.value)
})
.clone()
}
fn determine_hash_algo_security(u: &[u8]) -> HashAlgoSecurity {
if u.len() > 96 {
return HashAlgoSecurity::CollisionResistance;
}
match str::from_utf8(u) {
Ok(s) => {
if s.chars().any(char::is_control) {
return HashAlgoSecurity::CollisionResistance;
}
}
Err(_err) => {
return HashAlgoSecurity::CollisionResistance;
}
}
HashAlgoSecurity::SecondPreImageResistance
}
pub fn from_address<'a, N, C, E>(name: N, comment: C, email: E)
-> Result<Self>
where
N: Into<Option<&'a str>>,
C: Into<Option<&'a str>>,
E: AsRef<str>,
{
Self::assemble(name.into().as_ref().map(|s| s.as_ref()),
comment.into().as_ref().map(|s| s.as_ref()),
email.as_ref(),
true)
}
pub fn from_unchecked_address<'a, N, C, E>(name: N, comment: C, address: E)
-> Result<Self>
where
N: Into<Option<&'a str>>,
C: Into<Option<&'a str>>,
E: AsRef<str>,
{
Self::assemble(name.into().as_ref().map(|s| s.as_ref()),
comment.into().as_ref().map(|s| s.as_ref()),
address.as_ref(),
false)
}
pub fn value(&self) -> &[u8] {
&self.value
}
fn do_parse(&self) -> Result<&ConventionallyParsedUserID> {
if let Some(p) = self.parsed.get() {
return Ok(p);
}
let s = str::from_utf8(&self.value)?;
let p = ConventionallyParsedUserID::parse(s.to_string())?;
let _lost_race = self.parsed.set(p.clone());
Ok(self.parsed.get().expect("just set"))
}
pub fn name(&self) -> Result<Option<&str>> {
Ok(self.do_parse()?.name())
}
pub fn comment(&self) -> Result<Option<&str>> {
Ok(self.do_parse()?.comment())
}
pub fn email(&self) -> Result<Option<&str>> {
Ok(self.do_parse()?.email())
}
pub fn uri(&self) -> Result<Option<&str>> {
Ok(self.do_parse()?.uri())
}
pub fn email_normalized(&self) -> Result<Option<String>> {
match self.email()? {
None => Ok(None),
Some(address) => {
let mut iter = address.split('@');
let localpart = iter.next().expect("Invalid email address");
let domain = iter.next().expect("Invalid email address");
assert!(iter.next().is_none(), "Invalid email address");
let domain = idna::domain_to_ascii(domain)
.map_err(|e| anyhow::Error::from(e)
.context("punycode conversion failed"))?;
let address = format!("{}@{}", localpart, domain);
let address = address.to_lowercase();
Ok(Some(address))
}
}
}
}
impl From<UserID> for Packet {
fn from(s: UserID) -> Self {
Packet::UserID(s)
}
}
#[cfg(test)]
impl Arbitrary for UserID {
fn arbitrary(g: &mut Gen) -> Self {
Vec::<u8>::arbitrary(g).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::Parse;
use crate::serialize::MarshalInto;
quickcheck! {
fn roundtrip(p: UserID) -> bool {
let q = UserID::from_bytes(&p.to_vec().unwrap()).unwrap();
assert_eq!(p, q);
true
}
}
#[test]
fn decompose() {
tracer!(true, "decompose", 0);
fn c(userid: &str,
name: Option<&str>, comment: Option<&str>,
email: Option<&str>, uri: Option<&str>)
-> bool
{
assert!(email.is_none() || uri.is_none());
t!("userid: {}, name: {:?}, comment: {:?}, email: {:?}, uri: {:?}",
userid, name, comment, email, uri);
match ConventionallyParsedUserID::new(userid) {
Ok(puid) => {
let good = puid.name() == name
&& puid.comment() == comment
&& puid.email() == email
&& puid.uri() == uri;
if ! good {
t!("userid: {}", userid);
t!(" -> {:?}", puid);
t!(" {:?} {}= {:?}",
puid.name(),
if puid.name() == name { "=" } else { "!" },
name);
t!(" {:?} {}= {:?}",
puid.comment(),
if puid.comment() == comment { "=" } else { "!" },
comment);
t!(" {:?} {}= {:?}",
puid.email(),
if puid.email() == email { "=" } else { "!" },
email);
t!(" {:?} {}= {:?}",
puid.uri(),
if puid.uri() == uri { "=" } else { "!" },
uri);
t!(" -> BAD PARSE");
}
good
}
Err(err) => {
t!("userid: {} -> PARSE ERROR: {:?}", userid, err);
false
}
}
}
let mut g = true;
g &= c("First Last (Comment) <name@example.org>",
Some("First Last"), Some("Comment"), Some("name@example.org"), None);
g &= c("First Last <name@example.org>",
Some("First Last"), None, Some("name@example.org"), None);
g &= c("First Last", Some("First Last"), None, None, None);
g &= c("name@example.org <name@example.org>",
Some("name@example.org"), None, Some("name@example.org"), None);
g &= c("<name@example.org>",
None, None, Some("name@example.org"), None);
g &= c("name@example.org",
None, None, Some("name@example.org"), None);
g &= c("Björn Björnson <bjoern@example.net>",
Some("Björn Björnson"), None, Some("bjoern@example.net"), None);
g &= c("Bj=?utf-8?q?=C3=B6?=rn Bj=?utf-8?q?=C3=B6?=rnson \
<bjoern@example.net>",
Some("Bj=?utf-8?q?=C3=B6?=rn Bj=?utf-8?q?=C3=B6?=rnson"),
None, Some("bjoern@example.net"), None);
g &= c("Acme Industries, Inc. <info@acme.example>",
Some("Acme Industries, Inc."), None, Some("info@acme.example"), None);
g &= c("Michael O'Brian <obrian@example.biz>",
Some("Michael O'Brian"), None, Some("obrian@example.biz"), None);
g &= c("Smith, John <jsmith@example.com>",
Some("Smith, John"), None, Some("jsmith@example.com"), None);
g &= c("mariag@example.org",
None, None, Some("mariag@example.org"), None);
g &= c("joe@example.net <joe@example.net>",
Some("joe@example.net"), None, Some("joe@example.net"), None);
g &= c("иван.сергеев@пример.рф",
None, None, Some("иван.сергеев@пример.рф"), None);
g &= c("Dörte@Sörensen.example.com",
None, None, Some("Dörte@Sörensen.example.com"), None);
g &= c("Vorname Nachname, Dr.",
Some("Vorname Nachname, Dr."), None, None, None);
g &= c("Vorname Nachname, Dr. <dr@example.org>",
Some("Vorname Nachname, Dr."), None, Some("dr@example.org"), None);
g &= c("Foo (Bar) (Baz)",
Some("Foo (Bar)"), Some("Baz"), None, None);
g &= c("Foo (Bar) (Baz)",
Some("Foo (Bar)"), Some("Baz"), None, None);
g &= c("Foo (Bar (Baz)",
Some("Foo (Bar"), Some("Baz"), None, None);
g &= c(" Name Last ( some comment ) <name@example.org>",
Some("Name Last"), Some("some comment"),
Some("name@example.org"), None);
g &= c(" Name Last (email@example.org)",
Some("Name Last"), Some("email@example.org"), None, None);
g &= c("\"user\"@example.org",
Some("\"user\"@example.org"), None, None, None);
g &= c("\"user@example.org",
Some("\"user@example.org"), None, None, None);
g &= c("Henry Ford (CEO) <henry@ford.com>",
Some("Henry Ford"), Some("CEO"), Some("henry@ford.com"), None);
g &= c("Thomas \"Tomakin\" (DHC) <thomas@clh.co.uk>",
Some("Thomas \"Tomakin\""), Some("DHC"),
Some("thomas@clh.co.uk"), None);
g &= c("Aldous L. Huxley <huxley@old-world.org>",
Some("Aldous L. Huxley"), None,
Some("huxley@old-world.org"), None);
g &= c("<ftp://ftp.is.co.za/rfc/rfc1808.txt>",
None, None,
None, Some("ftp://ftp.is.co.za/rfc/rfc1808.txt"));
g &= c("<http://www.ietf.org/rfc/rfc2396.txt>",
None, None,
None, Some("http://www.ietf.org/rfc/rfc2396.txt"));
g &= c("<ldap://[2001:db8::7]/c=GB?objectClass?one>",
None, None,
None, Some("ldap://[2001:db8::7]/c=GB?objectClass?one"));
g &= c("<mailto:John.Doe@example.com>",
None, None,
None, Some("mailto:John.Doe@example.com"));
g &= c("<news:comp.infosystems.www.servers.unix>",
None, None,
None, Some("news:comp.infosystems.www.servers.unix"));
g &= c("<tel:+1-816-555-1212>",
None, None,
None, Some("tel:+1-816-555-1212"));
g &= c("<telnet://192.0.2.16:80/>",
None, None,
None, Some("telnet://192.0.2.16:80/"));
g &= c("<urn:oasis:names:specification:docbook:dtd:xml:4.1.2>",
None, None,
None, Some("urn:oasis:names:specification:docbook:dtd:xml:4.1.2"));
g &= c("Foo's ssh server <ssh://hostname>",
Some("Foo's ssh server"), None,
None, Some("ssh://hostname"));
g &= c("Foo (ssh server) <ssh://hostname>",
Some("Foo"), Some("ssh server"),
None, Some("ssh://hostname"));
g &= c("<ssh://hostname>",
None, None,
None, Some("ssh://hostname"));
g &= c("Warez <ftp://127.0.0.1>",
Some("Warez"), None,
None, Some("ftp://127.0.0.1"));
g &= c("ssh://hostname",
None, None,
None, Some("ssh://hostname"));
g &= c("ssh:hostname",
None, None,
None, Some("ssh:hostname"));
g &= c("Frank Füber <ssh://ïntérnätïònál.eu>",
Some("Frank Füber"), None,
None, Some("ssh://ïntérnätïònál.eu"));
g &= c("ssh://ïntérnätïònál.eu",
None, None,
None, Some("ssh://ïntérnätïònál.eu"));
g &= c("<foo://domain.org>",
None, None,
None, Some("foo://domain.org"));
g &= c("<foo-bar://domain.org>",
None, None,
None, Some("foo-bar://domain.org"));
g &= c("<foo+bar://domain.org>",
None, None,
None, Some("foo+bar://domain.org"));
g &= c("<foo.bar://domain.org>",
None, None,
None, Some("foo.bar://domain.org"));
g &= c("<foo.bar://domain.org#anchor?query>",
None, None,
None, Some("foo.bar://domain.org#anchor?query"));
g &= c("<foo://user:password@domain.org>",
None, None,
None, Some("foo://user:password@domain.org"));
g &= c("<foo://domain.org:348>",
None, None,
None, Some("foo://domain.org:348"));
g &= c("<foo://domain.org:348/>",
None, None,
None, Some("foo://domain.org:348/"));
g &= c("<http://[:]>", None, None, None, Some("http://[:]"));
g &= c("<http://2001:db8::1>", None, None, None, Some("http://2001:db8::1"));
g &= c("<http://[www.google.com]/>", None, None, None, Some("http://[www.google.com]/"));
g &= c("<http:////////user:@google.com:99?foo>", None, None, None, Some("http:////////user:@google.com:99?foo"));
g &= c("<http:path>", None, None, None, Some("http:path"));
g &= c("<http:/path>", None, None, None, Some("http:/path"));
g &= c("<http:host>", None, None, None, Some("http:host"));
g &= c("<http://user:pass@foo:21/bar;par?b#c>", None, None, None,
Some("http://user:pass@foo:21/bar;par?b#c"));
g &= c("<http:foo.com>", None, None, None, Some("http:foo.com"));
g &= c("<http://f:/c>", None, None, None, Some("http://f:/c"));
g &= c("<http://f:0/c>", None, None, None, Some("http://f:0/c"));
g &= c("<http://f:00000000000000/c>", None, None, None, Some("http://f:00000000000000/c"));
g &= c("<http://f:
/c>", None, None, None, Some("http://f:
/c"));
g &= c("<http://f:fifty-two/c>", None, None, None, Some("http://f:fifty-two/c"));
g &= c("<foo://>", None, None, None, Some("foo://"));
g &= c("<http://a:b@c:29/d>", None, None, None, Some("http://a:b@c:29/d"));
g &= c("<http::@c:29>", None, None, None, Some("http::@c:29"));
g &= c("<http://&a:foo(b]c@d:2/>", None, None, None, Some("http://&a:foo(b]c@d:2/"));
g &= c("<http://iris.test.ing/résumé/résumé.html>", None, None, None, Some("http://iris.test.ing/résumé/résumé.html"));
g &= c("<http://google.com/foo[bar]>", None, None, None, Some("http://google.com/foo[bar]"));
if !g {
panic!("Parse error");
}
}
#[test]
fn compose() {
tracer!(true, "compose", 0);
fn c(userid: &str,
name: Option<&str>, comment: Option<&str>,
email: Option<&str>, uri: Option<&str>)
{
assert!(email.xor(uri).is_some());
t!("userid: {}, name: {:?}, comment: {:?}, email: {:?}, uri: {:?}",
userid, name, comment, email, uri);
if let Some(email) = email {
let uid = UserID::from_address(name, comment, email).unwrap();
assert_eq!(userid, String::from_utf8_lossy(uid.value()));
}
if let Some(uri) = uri {
let uid =
UserID::from_unchecked_address(name, comment, uri).unwrap();
assert_eq!(userid, String::from_utf8_lossy(uid.value()));
}
}
c("First Last (Comment) <name@example.org>",
Some("First Last"), Some("Comment"), Some("name@example.org"), None);
c("First Last <name@example.org>",
Some("First Last"), None, Some("name@example.org"), None);
c("<name@example.org>",
None, None, Some("name@example.org"), None);
}
#[test]
fn decompose_non_conventional() {
assert!(ConventionallyParsedUserID::new("").is_err());
assert!(ConventionallyParsedUserID::new(" ").is_err());
assert!(ConventionallyParsedUserID::new(" ").is_err());
assert!(ConventionallyParsedUserID::new(
"<a..b@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<dr.@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<.drb@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<hallo> <hello@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<hallo <hello@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"hallo> <hello@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"foo <example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"Huxley <huxley@@old-world.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"foo <@example.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<huxley@.old-world.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<huxley@old-world.org.>").is_err());
assert!(ConventionallyParsedUserID::new(
"<@old-world.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<über://domain.org>").is_err());
assert!(ConventionallyParsedUserID::new(
"<http://some domain.org>").is_err());
}
#[test]
fn email_normalized() {
fn c(value: &str, expected: &str) {
let u = UserID::from(value);
let got = u.email_normalized().unwrap().unwrap();
assert_eq!(expected, got);
}
c("Henry Ford (CEO) <henry@ford.com>", "henry@ford.com");
c("Henry Ford (CEO) <Henry@Ford.com>", "henry@ford.com");
c("Henry Ford (CEO) <Henry@Ford.com>", "henry@ford.com");
c("hans@bücher.tld", "hans@xn--bcher-kva.tld");
c("hANS@bücher.tld", "hans@xn--bcher-kva.tld");
}
#[test]
fn from_address() {
assert_eq!(UserID::from_address(None, None, "foo@bar.com")
.unwrap().value(),
b"<foo@bar.com>");
assert!(UserID::from_address(None, None, "foo@@bar.com").is_err());
assert_eq!(UserID::from_address("Foo Q. Bar", None, "foo@bar.com")
.unwrap().value(),
b"Foo Q. Bar <foo@bar.com>");
}
#[test]
fn hash_algo_security() {
assert_eq!(UserID::from("Alice Lovelace <alice@lovelace.org>")
.hash_algo_security(),
HashAlgoSecurity::SecondPreImageResistance);
assert_eq!(UserID::from(&b"Alice Lovelace <alice@lovelace.org>\0"[..])
.hash_algo_security(),
HashAlgoSecurity::CollisionResistance);
assert_eq!(
UserID::from(
&b"Alice Lovelace <alice@lovelace.org>\0Hidden!"[..])
.hash_algo_security(),
HashAlgoSecurity::CollisionResistance);
assert_eq!(
UserID::from(String::from_utf8(vec![b'a'; 90]).unwrap())
.hash_algo_security(),
HashAlgoSecurity::SecondPreImageResistance);
assert_eq!(
UserID::from(String::from_utf8(vec![b'a'; 100]).unwrap())
.hash_algo_security(),
HashAlgoSecurity::CollisionResistance);
}
}