1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package packet
6
7import (
8	"io"
9	"io/ioutil"
10	"strings"
11)
12
13// UserId contains text that is intended to represent the name and email
14// address of the key holder. See RFC 4880, section 5.11. By convention, this
15// takes the form "Full Name (Comment) <email@example.com>"
16type UserId struct {
17	Id string // By convention, this takes the form "Full Name (Comment) <email@example.com>" which is split out in the fields below.
18
19	Name, Comment, Email string
20}
21
22func hasInvalidCharacters(s string) bool {
23	for _, c := range s {
24		switch c {
25		case '(', ')', '<', '>', 0:
26			return true
27		}
28	}
29	return false
30}
31
32// NewUserId returns a UserId or nil if any of the arguments contain invalid
33// characters. The invalid characters are '\x00', '(', ')', '<' and '>'
34func NewUserId(name, comment, email string) *UserId {
35	// RFC 4880 doesn't deal with the structure of userid strings; the
36	// name, comment and email form is just a convention. However, there's
37	// no convention about escaping the metacharacters and GPG just refuses
38	// to create user ids where, say, the name contains a '('. We mirror
39	// this behaviour.
40
41	if hasInvalidCharacters(name) || hasInvalidCharacters(comment) || hasInvalidCharacters(email) {
42		return nil
43	}
44
45	uid := new(UserId)
46	uid.Name, uid.Comment, uid.Email = name, comment, email
47	uid.Id = name
48	if len(comment) > 0 {
49		if len(uid.Id) > 0 {
50			uid.Id += " "
51		}
52		uid.Id += "("
53		uid.Id += comment
54		uid.Id += ")"
55	}
56	if len(email) > 0 {
57		if len(uid.Id) > 0 {
58			uid.Id += " "
59		}
60		uid.Id += "<"
61		uid.Id += email
62		uid.Id += ">"
63	}
64	return uid
65}
66
67func (uid *UserId) parse(r io.Reader) (err error) {
68	// RFC 4880, section 5.11
69	b, err := ioutil.ReadAll(r)
70	if err != nil {
71		return
72	}
73	uid.Id = string(b)
74	uid.Name, uid.Comment, uid.Email = parseUserId(uid.Id)
75	return
76}
77
78// Serialize marshals uid to w in the form of an OpenPGP packet, including
79// header.
80func (uid *UserId) Serialize(w io.Writer) error {
81	err := serializeHeader(w, packetTypeUserId, len(uid.Id))
82	if err != nil {
83		return err
84	}
85	_, err = w.Write([]byte(uid.Id))
86	return err
87}
88
89// parseUserId extracts the name, comment and email from a user id string that
90// is formatted as "Full Name (Comment) <email@example.com>".
91func parseUserId(id string) (name, comment, email string) {
92	var n, c, e struct {
93		start, end int
94	}
95	var state int
96
97	for offset, rune := range id {
98		switch state {
99		case 0:
100			// Entering name
101			n.start = offset
102			state = 1
103			fallthrough
104		case 1:
105			// In name
106			if rune == '(' {
107				state = 2
108				n.end = offset
109			} else if rune == '<' {
110				state = 5
111				n.end = offset
112			}
113		case 2:
114			// Entering comment
115			c.start = offset
116			state = 3
117			fallthrough
118		case 3:
119			// In comment
120			if rune == ')' {
121				state = 4
122				c.end = offset
123			}
124		case 4:
125			// Between comment and email
126			if rune == '<' {
127				state = 5
128			}
129		case 5:
130			// Entering email
131			e.start = offset
132			state = 6
133			fallthrough
134		case 6:
135			// In email
136			if rune == '>' {
137				state = 7
138				e.end = offset
139			}
140		default:
141			// After email
142		}
143	}
144	switch state {
145	case 1:
146		// ended in the name
147		n.end = len(id)
148	case 3:
149		// ended in comment
150		c.end = len(id)
151	case 6:
152		// ended in email
153		e.end = len(id)
154	}
155
156	name = strings.TrimSpace(id[n.start:n.end])
157	comment = strings.TrimSpace(id[c.start:c.end])
158	email = strings.TrimSpace(id[e.start:e.end])
159	return
160}
161