summaryrefslogtreecommitdiffstats
path: root/openpgp/src/packet/userid.rs
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2020-09-17 15:22:22 +0200
committerJustus Winter <justus@sequoia-pgp.org>2020-09-17 15:22:22 +0200
commit00d389f47c66f14740b89a62944a5169b0f4b9a7 (patch)
tree89cac3cf058c54a74acd1e35e4f61b403bce7ad8 /openpgp/src/packet/userid.rs
parent9310ec08bd0a46f6b128678cefa4ff861eaa3bfe (diff)
openpgp: Rename file.
Diffstat (limited to 'openpgp/src/packet/userid.rs')
-rw-r--r--openpgp/src/packet/userid.rs1217
1 files changed, 1217 insertions, 0 deletions
diff --git a/openpgp/src/packet/userid.rs b/openpgp/src/packet/userid.rs
new file mode 100644
index 00000000..17267a5b
--- /dev/null
+++ b/openpgp/src/packet/userid.rs
@@ -0,0 +1,1217 @@
+use std::fmt;
+use std::str;
+use std::hash::{Hash, Hasher};
+use std::cell::RefCell;
+use std::cmp::Ordering;
+use std::sync::Mutex;
+
+#[cfg(any(test, feature = "quickcheck"))]
+use quickcheck::{Arbitrary, Gen};
+
+use anyhow::Context;
+use regex::Regex;
+
+use crate::Result;
+use crate::packet;
+use crate::Packet;
+use crate::Error;
+
+/// A conventionally parsed UserID.
+///
+/// Informally, conventional UserIDs are of the form:
+///
+/// - First Last (Comment) <name@example.org>
+/// - First Last <name@example.org>
+/// - First Last
+/// - name@example.org <name@example.org>
+/// - <name@example.org>
+/// - name@example.org
+///
+/// - Name (Comment) <scheme://hostname/path>
+/// - Name (Comment) <mailto:user@example.org>
+/// - Name <scheme://hostname/path>
+/// - <scheme://hostname/path>
+/// - scheme://hostname/path
+///
+/// Names consist of UTF-8 non-control characters and may include
+/// punctuation. For instance, the following names are valid:
+///
+/// - Acme Industries, Inc.
+/// - Michael O'Brian
+/// - Smith, John
+/// - e.e. cummings
+///
+/// (Note: according to RFC 2822 and its successors, all of these
+/// would need to be quoted. Conventionally, no implementation quotes
+/// names.)
+///
+/// Conventional User IDs are UTF-8. RFC 2822 only covers US-ASCII
+/// and allows character set switching using RFC 2047. For example,
+/// an RFC 2822 parser would parse:
+///
+/// - Bj=?utf-8?q?=C3=B6?=rn Bj=?utf-8?q?=C3=B6?=rnson
+///
+/// "Björn Björnson". Nobody uses this in practice, and, as such,
+/// this extension is not supported by this parser.
+///
+/// Comments can include any UTF-8 text except parentheses. Thus, the
+/// following is not a valid comment even though the parentheses are
+/// balanced:
+///
+/// - (foo (bar))
+///
+/// URIs
+/// ----
+///
+/// The URI parser recognizes URIs using a regular expression similar
+/// to the one recommended in [RFC 3986] with the following extensions
+/// and restrictions:
+///
+/// - UTF-8 characters are in the range \u{80}-\u{10ffff} are
+/// allowed wherever percent-encoded characters are allowed (i.e.,
+/// everywhere but the schema).
+///
+/// - The scheme component and its trailing ":" are required.
+///
+/// - The URI must have an authority component ("//domain") or a
+/// path component ("/path/to/resource").
+///
+/// - Although the RFC does not allow it, in practice, the '[' and
+/// ']' characters are allowed wherever percent-encoded characters
+/// are allowed (i.e., everywhere but the schema).
+///
+/// URIs are neither normalized nor interpreted. For instance, dot
+/// segments are not removed, escape sequences are not decoded, etc.
+///
+/// Note: the recommended regular expression is less strict than the
+/// grammar. For instance, a percent encoded character must consist
+/// of three characters: the percent character followed by two hex
+/// digits. The parser that we use does not enforce this either.
+///
+/// [RFC 3986]: https://tools.ietf.org/html/rfc3986
+///
+/// Formal Grammar
+/// --------------
+///
+/// Formally, the following grammar is used to decompose a User ID:
+///
+/// WS = 0x20 (space character)
+///
+/// comment-specials = "<" / ">" / ; RFC 2822 specials - "(" and ")"
+/// "[" / "]" /
+/// ":" / ";" /
+/// "@" / "\" /
+/// "," / "." /
+/// DQUOTE
+///
+/// atext-specials = "(" / ")" / ; RFC 2822 specials - "<" and ">".
+/// "[" / "]" /
+/// ":" / ";" /
+/// "@" / "\" /
+/// "," / "." /
+/// DQUOTE
+///
+/// atext = ALPHA / DIGIT / ; Any character except controls,
+/// "!" / "#" / ; SP, and specials.
+/// "$" / "%" / ; Used for atoms
+/// "&" / "'" /
+/// "*" / "+" /
+/// "-" / "/" /
+/// "=" / "?" /
+/// "^" / "_" /
+/// "`" / "{" /
+/// "|" / "}" /
+/// "~" /
+/// \u{80}-\u{10ffff} ; Non-ascii, non-control UTF-8
+///
+/// dot_atom_text = 1*atext *("." *atext)
+///
+/// name-char-start = atext / atext-specials
+///
+/// name-char-rest = atext / atext-specials / WS
+///
+/// name = name-char-start *name-char-rest
+///
+/// comment-char = atext / comment-specials / WS
+///
+/// comment-content = *comment-char
+///
+/// comment = "(" *WS comment-content *WS ")"
+///
+/// addr-spec = dot-atom-text "@" dot-atom-text
+///
+/// uri = See [RFC 3986] and the note on URIs above.
+///
+/// pgp-uid-convention = addr-spec /
+/// uri /
+/// *WS [name] *WS [comment] *WS "<" addr-spec ">" /
+/// *WS [name] *WS [comment] *WS "<" uri ">" /
+/// *WS name *WS [comment] *WS
+#[derive(Clone, Debug)]
+pub struct ConventionallyParsedUserID {
+ userid: String,
+
+ name: Option<(usize, usize)>,
+ comment: Option<(usize, usize)>,
+ email: Option<(usize, usize)>,
+ uri: Option<(usize, usize)>,
+}
+
+impl ConventionallyParsedUserID {
+ /// Parses the userid according to the usual conventions.
+ pub fn new<S>(userid: S) -> Result<Self>
+ where S: Into<String>
+ {
+ Self::parse(userid.into())
+ }
+
+ /// Returns the User ID's name component, if any.
+ pub fn name(&self) -> Option<&str> {
+ self.name.map(|(s, e)| &self.userid[s..e])
+ }
+
+ /// Returns the User ID's comment field, if any.
+ pub fn comment(&self) -> Option<&str> {
+ self.comment.map(|(s, e)| &self.userid[s..e])
+ }
+
+ /// Returns the User ID's email component, if any.
+ pub fn email(&self) -> Option<&str> {
+ self.email.map(|(s, e)| &self.userid[s..e])
+ }
+
+ /// Returns the User ID's URI component, if any.
+ ///
+ /// Note: the URI is returned as is; dot segments are not removed,
+ /// escape sequences are not unescaped, etc.
+ pub fn uri(&self) -> Option<&str> {
+ self.uri.map(|(s, e)| &self.userid[s..e])
+ }
+
+ fn parse(userid: String) -> Result<Self> {
+ lazy_static!{
+ static ref USER_ID_PARSER: Regex = {
+ // Whitespace.
+ let ws_bare = " ";
+ let ws = format!("[{}]", ws_bare);
+ let optional_ws = format!("(?:{}*)", ws);
+
+ // Specials minus ( and ).
+ let comment_specials_bare = r#"<>\[\]:;@\\,.""#;
+ let _comment_specials
+ = format!("[{}]", comment_specials_bare);
+
+ let atext_specials_bare = r#"()\[\]:;@\\,.""#;
+ let _atext_specials =
+ format!("[{}]", atext_specials_bare);
+
+ // "Text"
+ let atext_bare
+ = "-A-Za-z0-9!#$%&'*+/=?^_`{|}~\u{80}-\u{10ffff}";
+ let atext = format!("[{}]", atext_bare);
+
+ // An atext with dots and the added restriction that
+ // it may not start or end with a dot.
+ 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);
+ // We need to minimize the match as otherwise we
+ // swallow any comment.
+ 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| {
+ // The regex suggested from the RFC:
+ //
+ // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+ // ^schema ^authority ^path ^query ^fragment
+ //
+ // Since only the path component is required, and
+ // the path matches everything but the '?' and '#'
+ // characters, this regular expression will match
+ // almost any string.
+ //
+ // This regular expression is good for picking
+ // apart strings that are known to be URIs. But,
+ // we want to detect URIs and distinguish them
+ // from things that are almost certainly not URIs,
+ // like email addresses.
+ //
+ // As such, we require the URI to have a
+ // well-formed schema, and the schema must be
+ // followed by a non-empty component. Further, we
+ // restrict the alphabet to approximately what the
+ // grammar permits.
+
+ // Looking at the productions for the schema,
+ // authority, path, query, and fragment
+ // components, we can distil the following useful
+ // alphabets (the symbols are drawn from the
+ // following pct-encoded, unreserved, gen-delims,
+ // sub-delims, pchar, and IP-literal productions):
+ let symbols = "-{}0-9._~%!$&'()*+,;=:@\\[\\]";
+ let ascii_alpha = "a-zA-Z";
+ let utf8_alpha = "a-zA-Z\u{80}-\u{10ffff}";
+
+ // We strictly match the schema production:
+ //
+ // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+ let schema
+ = format!("(?:[{}][-+.{}0-9]*:)",
+ ascii_alpha, ascii_alpha);
+
+ // The symbols that can occur in a fragment are a
+ // superset of those that can occur in a query and
+ // its delimiters. Likewise, the symbols that can
+ // occur in a query are a superset of those that
+ // can occur in a path and its delimiters. The
+ // symbols that can occur in a path are *almost* a
+ // subset of those that can occur in an authority:
+ // '[' and ']' can occur in an authority component
+ // (via the IP-literal production, e.g.,
+ // '[2001:db8::7]'), but not in a path. But, URI
+ // parsers appear to accept '[' and ']' as part of
+ // a path. So, we accept them too.
+ //
+ // Given this, a fragment matches all components
+ // and everything that precedes it. Since we
+ // don't need to distinguish the individual parts
+ // here, matching what follows the schema in a URI
+ // is straightforward:
+ 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"));
+
+ // whitespace is ignored. It is allowed (but not
+ // required) at the start and between components, but
+ // it is not allowed after the closing '>'. space is
+ // not allowed.
+ 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);
+
+ // Note: bare-name has to come after addr-spec-raw as
+ // prefer addr-spec-raw to bare-name when the match is
+ // ambiguous.
+ let pgp_uid_convention
+ = format!("^(?:{}|{}|{}|{}|{})$",
+ raw_addr_spec, raw_uri,
+ wrapped_addr_spec, wrapped_uri,
+ bare_name);
+
+ Regex::new(&pgp_uid_convention).unwrap()
+ };
+ }
+
+ // The regex is anchored at the start and at the end so we
+ // have either 0 or 1 matches.
+ if let Some(cap) = USER_ID_PARSER.captures_iter(&userid).nth(0) {
+ let to_range = |m: regex::Match| (m.start(), m.end());
+
+ // We need to figure out which branch matched. Match on a
+ // required capture for each branch.
+
+ if let Some(email) = cap.name("raw_addr_spec") {
+ // 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") {
+ // 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") {
+ // 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") {
+ // uri-wrapped
+ 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") {
+ // name-bare
+ 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 {
+ return Err(Error::InvalidArgument(
+ "Failed to parse UserID".into()).into());
+ }
+ }
+}
+
+/// Holds a UserID 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
+pub struct UserID {
+ /// CTB packet header fields.
+ pub(crate) common: packet::Common,
+ /// The user id.
+ ///
+ /// According to [RFC 4880], the text is by convention UTF-8 encoded
+ /// and in "mail name-addr" form, i.e., "Name (Comment)
+ /// <email@example.com>".
+ ///
+ /// [RFC 4880]: https://tools.ietf.org/html/rfc4880#section-5.11
+ ///
+ /// Use `UserID::default()` to get a UserID with a default settings.
+ value: Vec<u8>,
+
+ parsed: Mutex<RefCell<Option<ConventionallyParsedUserID>>>,
+}
+
+impl From<Vec<u8>> for UserID {
+ fn from(u: Vec<u8>) -> Self {
+ UserID {
+ common: Default::default(),
+ value: u,
+ parsed: Mutex::new(RefCell::new(None)),
+ }
+ }
+}
+
+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 {
+ let b = u.as_bytes();
+ let mut v = Vec::with_capacity(b.len());
+ v.extend_from_slice(b);
+ v.into()
+ }
+}
+
+impl From<String> for UserID {
+ fn from(u: String) -> Self {
+ let u = &u[..];
+ u.into()
+ }
+}
+
+impl<'a> From<::std::borrow::Cow<'a, str>> for UserID {
+ fn from(u: ::std::borrow::Cow<'a, str>) -> Self {
+ let b = u.as_bytes();
+ let mut v = Vec::with_capacity(b.len());
+ v.extend_from_slice(b);
+ v.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) {
+ // 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: Mutex::new(RefCell::new(None)),
+ }
+ }
+}
+
+impl UserID {
+ fn assemble<S>(name: Option<S>, comment: Option<S>,
+ address: S, check_address: bool)
+ -> Result<Self>
+ where S: AsRef<str>,
+ {
+ let mut value = String::with_capacity(64);
+
+ // Make sure the individual components are valid.
+ if let Some(ref name) = name {
+ let name = name.as_ref();
+ match ConventionallyParsedUserID::new(name.to_string()) {
+ Err(err) =>
+ return Err(err.context(format!(
+ "Validating name ({:?})",
+ name)).into()),
+ Ok(p) => {
+ if !(p.name().is_some()
+ && p.comment().is_none()
+ && p.email().is_none()) {
+ return Err(Error::InvalidArgument(
+ format!("Invalid name ({:?})", name)
+ .into()).into());
+ }
+ }
+ }
+
+ value.push_str(name);
+ }
+
+ if let Some(ref comment) = comment {
+ let comment = comment.as_ref();
+ match ConventionallyParsedUserID::new(
+ format!("x ({})", comment))
+ {
+ Err(err) =>
+ return Err(err.context(format!(
+ "Validating comment ({:?})",
+ comment)).into()),
+ Ok(p) => {
+ if !(p.name().is_none()
+ && p.comment().is_some()
+ && p.email().is_none()) {
+ return Err(Error::InvalidArgument(
+ format!("Invalid comment ({:?})", comment)
+ .into()).into());
+ }
+ }
+ }
+
+ if value.len() > 0 {
+ value.push_str(" ");
+ }
+ value.push_str("(");
+ value.push_str(comment);
+ value.push_str(")");
+ }
+
+ if check_address {
+ let address = address.as_ref();
+ match ConventionallyParsedUserID::new(
+ format!("<{}>", address))
+ {
+ Err(err) =>
+ return Err(err.context(format!(
+ "Validating address ({:?})",
+ address)).into()),
+ 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()).into());
+ }
+ }
+ }
+ }
+
+ let something = value.len() > 0;
+ if something {
+ value.push_str(" <");
+ }
+ value.push_str(address.as_ref());
+ if something {
+ value.push_str(">");
+ }
+
+ if check_address {
+ // Make sure the combined thing is valid.
+ match ConventionallyParsedUserID::new(value.clone())
+ {
+ Err(err) =>
+ return Err(err.context(format!(
+ "Validating User ID ({:?})",
+ value)).into()),
+ 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()).into());
+ }
+ }
+ }
+ }
+
+ Ok(UserID::from(value))
+ }
+
+ /// Constructs a User ID.
+ ///
+ /// This does a basic check and any necessary escaping to form a de
+ /// facto User ID.
+ ///
+ /// Only the address is required. If a comment is supplied, then
+ /// a name is also required.
+ ///
+ /// If you already have a User ID value, then you can just
+ /// use `UserID::from()`.
+ ///
+ /// ```
+ /// # extern crate sequoia_openpgp as openpgp;
+ /// # use openpgp::packet::UserID;
+ /// assert_eq!(UserID::from_address(
+ /// "John Smith".into(),
+ /// None, "boat@example.org").unwrap().value(),
+ /// &b"John Smith <boat@example.org>"[..]);
+ /// ```
+ pub fn from_address<O, S>(name: O, comment: O, email: S)
+ -> Result<Self>
+ where S: AsRef<str>,
+ O: Into<Option<S>>
+ {
+ Self::assemble(name.into(), comment.into(), email, true)
+ }
+
+ /// Constructs a User ID.
+ ///
+ /// This does a basic check and any necessary escaping to form a de
+ /// facto User ID modulo the address, which is not checked.
+ ///
+ /// This is useful when you want to specify a URI instead of an
+ /// email address.
+ ///
+ /// If you already have a User ID value, then you can just
+ /// use `UserID::from()`.
+ ///
+ /// ```
+ /// # extern crate sequoia_openpgp as openpgp;
+ /// # use openpgp::packet::UserID;
+ /// assert_eq!(UserID::from_unchecked_address(
+ /// "NAS".into(),
+ /// None, "ssh://host.example.org").unwrap().value(),
+ /// &b"NAS <ssh://host.example.org>"[..]);
+ /// ```
+ pub fn from_unchecked_address<O, S>(name: O, comment: O, address: S)
+ -> Result<Self>
+ where S: AsRef<str>,
+ O: Into<Option<S>>
+ {
+ Self::assemble(name.into(), comment.into(), address, false)
+ }
+
+ /// Gets the user ID packet's value.
+ pub fn value(&self) -> &[u8] {
+ self.value.as_slice()
+ }
+
+ fn do_parse(&self) -> Result<()> {
+ if self.parsed.lock().unwrap().borrow().is_none() {
+ let s = str::from_utf8(&self.value)?;
+
+ *self.parsed.lock().unwrap().borrow_mut() =
+ Some(match ConventionallyParsedUserID::new(s) {
+ Ok(puid) => puid,
+ Err(err) => {
+ // Return the error from the NameAddrOrOther parser.
+ let err : anyhow::Error = err.into();
+ return Err(err).context(format!(
+ "Failed to parse User ID: {:?}", s))?;
+ }
+ });
+ }
+ Ok(())
+ }
+
+ /// Parses the User ID according to de facto conventions, and
+ /// returns the name component, if any.
+ pub fn name(&self) -> Result<Option<String>> {
+ self.do_parse()?;
+ match *self.parsed.lock().unwrap().borrow() {
+ Some(ref puid) => Ok(puid.name().map(|s| s.to_string())),
+ None => unreachable!(),
+ }
+ }
+
+ /// Parses the User ID according to de facto conventions, and
+ /// returns the comment field, if any.
+ pub fn comment(&self) -> Result<Option<String>> {
+ self.do_parse()?;
+ match *self.parsed.lock().unwrap().borrow() {
+ Some(ref puid) => Ok(puid.comment().map(|s| s.to_string())),
+ None => unreachable!(),
+ }
+ }
+
+ /// Parses the User ID according to de facto conventions, and
+ /// returns the em