diff options
author | Neal H. Walfield <neal@pep.foundation> | 2019-04-11 23:33:15 +0200 |
---|---|---|
committer | Neal H. Walfield <neal@pep.foundation> | 2019-04-12 11:44:32 +0200 |
commit | 6b8e5c59d44047e4c462235c17753187254bf052 (patch) | |
tree | 861c8c44b1e03901881ba18d9be375417de78224 | |
parent | 5c7e4274102748b287e81ffb195a918f442e2e13 (diff) |
openpgp: Parse User IDs.
- Provide an interface to query the name, comment and email address
of RFC 2822 name-addr and addr-spec encoded User IDs.
-rw-r--r-- | openpgp/Cargo.toml | 1 | ||||
-rw-r--r-- | openpgp/src/lib.rs | 2 | ||||
-rw-r--r-- | openpgp/src/packet/userid.rs | 167 |
3 files changed, 169 insertions, 1 deletions
diff --git a/openpgp/Cargo.toml b/openpgp/Cargo.toml index 740e3495..560af969 100644 --- a/openpgp/Cargo.toml +++ b/openpgp/Cargo.toml @@ -32,6 +32,7 @@ nettle = "5.0" quickcheck = "0.8" rand = "0.6" time = "0.1.40" +sequoia-rfc2822 = { path = "../rfc2822", version = "*" } [build-dependencies] lalrpop = "0.16" diff --git a/openpgp/src/lib.rs b/openpgp/src/lib.rs index 7f1e4b9b..584ae406 100644 --- a/openpgp/src/lib.rs +++ b/openpgp/src/lib.rs @@ -65,6 +65,8 @@ extern crate quickcheck; extern crate rand; extern crate time; + +extern crate sequoia_rfc2822 as rfc2822; #[macro_use] mod macros; diff --git a/openpgp/src/packet/userid.rs b/openpgp/src/packet/userid.rs index 99cb2a0d..4a1ecec4 100644 --- a/openpgp/src/packet/userid.rs +++ b/openpgp/src/packet/userid.rs @@ -1,6 +1,11 @@ use std::fmt; +use std::str; +use std::hash::{Hash, Hasher}; +use std::cell::RefCell; use quickcheck::{Arbitrary, Gen}; +use rfc2822::{NameAddr, AddrSpec}; +use Result; use packet; use Packet; @@ -9,7 +14,6 @@ use Packet; /// See [Section 5.11 of RFC 4880] for details. /// /// [Section 5.11 of RFC 4880]: https://tools.ietf.org/html/rfc4880#section-5.11 -#[derive(PartialEq, Eq, Hash, Clone)] pub struct UserID { /// CTB packet header fields. pub(crate) common: packet::Common, @@ -23,6 +27,8 @@ pub struct UserID { /// /// Use `UserID::default()` to get a UserID with a default settings. value: Vec<u8>, + + parsed: RefCell<Option<(Option<String>, Option<String>, Option<String>)>>, } impl From<Vec<u8>> for UserID { @@ -30,6 +36,7 @@ impl From<Vec<u8>> for UserID { UserID { common: Default::default(), value: u, + parsed: RefCell::new(None), } } } @@ -60,11 +67,95 @@ impl fmt::Debug for UserID { } } +impl PartialEq for UserID { + fn eq(&self, other: &UserID) -> bool { + self.common == other.common + && self.value == other.value + } +} + +impl Eq for UserID { +} + + +impl Hash for UserID { + fn hash<H: Hasher>(&self, state: &mut H) { + // We hash only the data; the cache does not implement hash. + self.common.hash(state); + self.value.hash(state); + } +} + +impl Clone for UserID { + fn clone(&self) -> Self { + UserID { + common: self.common.clone(), + value: self.value.clone(), + parsed: RefCell::new(None), + } + } +} + impl UserID { /// Gets the user ID packet's value. pub fn value(&self) -> &[u8] { self.value.as_slice() } + + fn do_parse(&self) -> Result<()> { + if self.parsed.borrow().is_none() { + let s = str::from_utf8(&self.value)?; + + *self.parsed.borrow_mut() = Some(match NameAddr::parse(s) { + Ok(na) => (na.name().map(|s| s.to_string()), + na.comment().map(|s| s.to_string()), + na.address().map(|s| s.to_string())), + Err(err) => { + // Try with the addr-spec parser. + if let Ok(a) = AddrSpec::parse(s) { + (None, None, Some(a.address().to_string())) + } else { + // Return the error from the NameAddr parser. + return Err(err.into()); + } + } + }); + } + Ok(()) + } + + /// Treats the user ID as an RFC 2822 name-addr and extracts the + /// display name, if any. + pub fn name(&self) -> Result<Option<String>> { + self.do_parse()?; + match *self.parsed.borrow() { + Some((ref name, ref _comment, ref _address)) => + Ok(name.as_ref().map(|s| s.clone())), + None => unreachable!(), + } + } + + /// Treats the user ID as an RFC 2822 name-addr and extracts the + /// first comment, if any. + pub fn comment(&self) -> Result<Option<String>> { + self.do_parse()?; + match *self.parsed.borrow() { + Some((ref _name, ref comment, ref _address)) => + Ok(comment.as_ref().map(|s| s.clone())), + None => unreachable!(), + } + } + + /// Treats the user ID as an RFC 2822 name-addr and extracts the + /// address, if any. + pub fn address(&self) -> Result<Option<String>> { + self.do_parse()?; + match *self.parsed.borrow() { + Some((ref _name, ref _comment, ref address)) => + Ok(address.as_ref().map(|s| s.clone())), + None => unreachable!(), + } + } } impl From<UserID> for Packet { @@ -92,4 +183,78 @@ mod tests { true } } + + #[test] + fn name_addr() { + fn c(value: &str, ok: bool, + name: Option<&str>, comment: Option<&str>, address: Option<&str>) + { + let name = name.map(|s| s.to_string()); + let comment = comment.map(|s| s.to_string()); + let address = address.map(|s| s.to_string()); + + let u = UserID::from(value); + for _ in 0..2 { + match u.name() { + Ok(ref v) if ok => + assert_eq!(v, &name), + Ok(_) if !ok => + panic!("Expected parse to fail."), + Err(ref err) if ok => + panic!("Expected parse to succeed: {:?}", err), + Err(_) if !ok => + (), + _ => unreachable!(), + }; + match u.comment() { + Ok(ref v) if ok => + assert_eq!(v, &comment), + Ok(_) if !ok => + panic!("Expected parse to fail."), + Err(ref err) if ok => + panic!("Expected parse to succeed: {:?}", err), + Err(_) if !ok => + (), + _ => unreachable!(), + }; + match u.address() { + Ok(ref v) if ok => + assert_eq!(v, &address), + Ok(_) if !ok => + panic!("Expected parse to fail."), + Err(ref err) if ok => + panic!("Expected parse to succeed: {:?}", err), + Err(_) if !ok => + (), + _ => unreachable!(), + }; + } + } + + c("Henry Ford (CEO) <henry@ford.com>", true, + Some("Henry Ford"), Some("CEO"), Some("henry@ford.com")); + + // The quotes disappear. Unexpected, but true. + c("Thomas \"Tomakin\" (DHC) <thomas@clh.co.uk>", true, + Some("Thomas Tomakin"), Some("DHC"), Some("thomas@clh.co.uk")); + + c("Aldous L. Huxley <huxley@old-world.org>", true, + Some("Aldous L. Huxley"), None, Some("huxley@old-world.org")); + + // Make sure bare email addresses work. This is an extension + // to 2822 where addresses normally have to be in angle + // brackets. + c("huxley@old-world.org", true, + None, None, Some("huxley@old-world.org")); + + // Tricky... + c("\"<loki@bar.com>\" <foo@bar.com>", true, + Some("<loki@bar.com>"), None, Some("foo@bar.com")); + + // Invalid. + c("<huxley@@old-world.org>", false, None, None, None); + c("huxley@@old-world.org", false, None, None, None); + c("huxley@old-world.org.", false, None, None, None); + c("@old-world.org", false, None, None, None); + } } |