1package identity
2
3import (
4 "crypto/rand"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "github.com/MichaelMure/git-bug/repository"
10 "github.com/MichaelMure/git-bug/util/git"
11 "github.com/MichaelMure/git-bug/util/lamport"
12 "github.com/MichaelMure/git-bug/util/text"
13 "github.com/pkg/errors"
14)
15
16const formatVersion = 1
17
18// Version is a complete set of information about an Identity at a point in time.
19type Version struct {
20 // Not serialized
21 commitHash git.Hash
22
23 // Todo: add unix timestamp for ordering with identical lamport time ?
24
25 // The lamport time at which this version become effective
26 // The reference time is the bug edition lamport clock
27 time lamport.Time
28
29 name string
30 email string
31 login string
32 avatarURL string
33
34 // The set of keys valid at that time, from this version onward, until they get removed
35 // in a new version. This allow to have multiple key for the same identity (e.g. one per
36 // device) as well as revoke key.
37 keys []Key
38
39 // This optional array is here to ensure a better randomness of the identity id to avoid collisions.
40 // It has no functional purpose and should be ignored.
41 // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys.
42 nonce []byte
43
44 // A set of arbitrary key/value to store metadata about a version or about an Identity in general.
45 metadata map[string]string
46}
47
48type VersionJSON struct {
49 // Additional field to version the data
50 FormatVersion uint `json:"version"`
51
52 Time lamport.Time `json:"time"`
53 Name string `json:"name"`
54 Email string `json:"email"`
55 Login string `json:"login"`
56 AvatarUrl string `json:"avatar_url"`
57 Keys []Key `json:"pub_keys"`
58 Nonce []byte `json:"nonce,omitempty"`
59 Metadata map[string]string `json:"metadata,omitempty"`
60}
61
62func (v *Version) MarshalJSON() ([]byte, error) {
63 return json.Marshal(VersionJSON{
64 FormatVersion: formatVersion,
65 Time: v.time,
66 Name: v.name,
67 Email: v.email,
68 Login: v.login,
69 AvatarUrl: v.avatarURL,
70 Keys: v.keys,
71 Nonce: v.nonce,
72 Metadata: v.metadata,
73 })
74}
75
76func (v *Version) UnmarshalJSON(data []byte) error {
77 var aux VersionJSON
78
79 if err := json.Unmarshal(data, &aux); err != nil {
80 return err
81 }
82
83 if aux.FormatVersion != formatVersion {
84 return fmt.Errorf("unknown format version %v", aux.FormatVersion)
85 }
86
87 v.time = aux.Time
88 v.name = aux.Name
89 v.email = aux.Email
90 v.login = aux.Login
91 v.avatarURL = aux.AvatarUrl
92 v.keys = aux.Keys
93 v.nonce = aux.Nonce
94 v.metadata = aux.Metadata
95
96 return nil
97}
98
99func (v *Version) Validate() error {
100 if text.Empty(v.name) && text.Empty(v.login) {
101 return fmt.Errorf("either name or login should be set")
102 }
103
104 if strings.Contains(v.name, "\n") {
105 return fmt.Errorf("name should be a single line")
106 }
107
108 if !text.Safe(v.name) {
109 return fmt.Errorf("name is not fully printable")
110 }
111
112 if strings.Contains(v.login, "\n") {
113 return fmt.Errorf("login should be a single line")
114 }
115
116 if !text.Safe(v.login) {
117 return fmt.Errorf("login is not fully printable")
118 }
119
120 if strings.Contains(v.email, "\n") {
121 return fmt.Errorf("email should be a single line")
122 }
123
124 if !text.Safe(v.email) {
125 return fmt.Errorf("email is not fully printable")
126 }
127
128 if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {
129 return fmt.Errorf("avatarUrl is not a valid URL")
130 }
131
132 if len(v.nonce) > 64 {
133 return fmt.Errorf("nonce is too big")
134 }
135
136 for _, k := range v.keys {
137 if err := k.Validate(); err != nil {
138 return errors.Wrap(err, "invalid key")
139 }
140 }
141
142 return nil
143}
144
145// Write will serialize and store the Version as a git blob and return
146// its hash
147func (v *Version) Write(repo repository.Repo) (git.Hash, error) {
148 // make sure we don't write invalid data
149 err := v.Validate()
150 if err != nil {
151 return "", errors.Wrap(err, "validation error")
152 }
153
154 data, err := json.Marshal(v)
155
156 if err != nil {
157 return "", err
158 }
159
160 hash, err := repo.StoreData(data)
161
162 if err != nil {
163 return "", err
164 }
165
166 return hash, nil
167}
168
169func makeNonce(len int) []byte {
170 result := make([]byte, len)
171 _, err := rand.Read(result)
172 if err != nil {
173 panic(err)
174 }
175 return result
176}
177
178// SetMetadata store arbitrary metadata about a version or an Identity in general
179// If the Version has been commit to git already, it won't be overwritten.
180func (v *Version) SetMetadata(key string, value string) {
181 if v.metadata == nil {
182 v.metadata = make(map[string]string)
183 }
184
185 v.metadata[key] = value
186}
187
188// GetMetadata retrieve arbitrary metadata about the Version
189func (v *Version) GetMetadata(key string) (string, bool) {
190 val, ok := v.metadata[key]
191 return val, ok
192}
193
194// AllMetadata return all metadata for this Identity
195func (v *Version) AllMetadata() map[string]string {
196 return v.metadata
197}