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 // The lamport time at which this version become effective
21 // The reference time is the bug edition lamport clock
22 // It must be the first field in this struct due to https://github.com/golang/go/issues/599
23 //
24 // TODO: BREAKING CHANGE - this need to actually be one edition lamport time **per entity**
25 // This is not a problem right now but will be when more entities are added (pull-request, config ...)
26 time lamport.Time
27 unixTime int64
28
29 name string
30 email string // as defined in git or from a bridge when importing the identity
31 login string // from a bridge when importing the identity
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 // Not serialized
48 commitHash git.Hash
49}
50
51type VersionJSON struct {
52 // Additional field to version the data
53 FormatVersion uint `json:"version"`
54
55 Time lamport.Time `json:"time"`
56 UnixTime int64 `json:"unix_time"`
57 Name string `json:"name,omitempty"`
58 Email string `json:"email,omitempty"`
59 Login string `json:"login,omitempty"`
60 AvatarUrl string `json:"avatar_url,omitempty"`
61 Keys []*Key `json:"pub_keys,omitempty"`
62 Nonce []byte `json:"nonce,omitempty"`
63 Metadata map[string]string `json:"metadata,omitempty"`
64}
65
66// Make a deep copy
67func (v *Version) Clone() *Version {
68 clone := &Version{
69 name: v.name,
70 email: v.email,
71 avatarURL: v.avatarURL,
72 keys: make([]*Key, len(v.keys)),
73 }
74
75 for i, key := range v.keys {
76 clone.keys[i] = key.Clone()
77 }
78
79 return clone
80}
81
82func (v *Version) MarshalJSON() ([]byte, error) {
83 return json.Marshal(VersionJSON{
84 FormatVersion: formatVersion,
85 Time: v.time,
86 UnixTime: v.unixTime,
87 Name: v.name,
88 Email: v.email,
89 Login: v.login,
90 AvatarUrl: v.avatarURL,
91 Keys: v.keys,
92 Nonce: v.nonce,
93 Metadata: v.metadata,
94 })
95}
96
97func (v *Version) UnmarshalJSON(data []byte) error {
98 var aux VersionJSON
99
100 if err := json.Unmarshal(data, &aux); err != nil {
101 return err
102 }
103
104 if aux.FormatVersion != formatVersion {
105 return fmt.Errorf("unknown format version %v", aux.FormatVersion)
106 }
107
108 v.time = aux.Time
109 v.unixTime = aux.UnixTime
110 v.name = aux.Name
111 v.email = aux.Email
112 v.login = aux.Login
113 v.avatarURL = aux.AvatarUrl
114 v.keys = aux.Keys
115 v.nonce = aux.Nonce
116 v.metadata = aux.Metadata
117
118 return nil
119}
120
121func (v *Version) Validate() error {
122 // time must be set after a commit
123 if v.commitHash != "" && v.unixTime == 0 {
124 return fmt.Errorf("unix time not set")
125 }
126 if v.commitHash != "" && v.time == 0 {
127 return fmt.Errorf("lamport time not set")
128 }
129
130 if text.Empty(v.name) && text.Empty(v.login) {
131 return fmt.Errorf("either name or login should be set")
132 }
133
134 if strings.Contains(v.name, "\n") {
135 return fmt.Errorf("name should be a single line")
136 }
137
138 if !text.Safe(v.name) {
139 return fmt.Errorf("name is not fully printable")
140 }
141
142 if strings.Contains(v.login, "\n") {
143 return fmt.Errorf("login should be a single line")
144 }
145
146 if !text.Safe(v.login) {
147 return fmt.Errorf("login is not fully printable")
148 }
149
150 if strings.Contains(v.email, "\n") {
151 return fmt.Errorf("email should be a single line")
152 }
153
154 if !text.Safe(v.email) {
155 return fmt.Errorf("email is not fully printable")
156 }
157
158 if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {
159 return fmt.Errorf("avatarUrl is not a valid URL")
160 }
161
162 if len(v.nonce) > 64 {
163 return fmt.Errorf("nonce is too big")
164 }
165
166 for _, k := range v.keys {
167 if err := k.Validate(); err != nil {
168 return errors.Wrap(err, "invalid key")
169 }
170 }
171
172 return nil
173}
174
175// Write will serialize and store the Version as a git blob and return
176// its hash
177func (v *Version) Write(repo repository.Repo) (git.Hash, error) {
178 // make sure we don't write invalid data
179 err := v.Validate()
180 if err != nil {
181 return "", errors.Wrap(err, "validation error")
182 }
183
184 data, err := json.Marshal(v)
185
186 if err != nil {
187 return "", err
188 }
189
190 hash, err := repo.StoreData(data)
191
192 if err != nil {
193 return "", err
194 }
195
196 return hash, nil
197}
198
199func makeNonce(len int) []byte {
200 result := make([]byte, len)
201 _, err := rand.Read(result)
202 if err != nil {
203 panic(err)
204 }
205 return result
206}
207
208// SetMetadata store arbitrary metadata about a version or an Identity in general
209// If the Version has been commit to git already, it won't be overwritten.
210func (v *Version) SetMetadata(key string, value string) {
211 if v.metadata == nil {
212 v.metadata = make(map[string]string)
213 }
214
215 v.metadata[key] = value
216}
217
218// GetMetadata retrieve arbitrary metadata about the Version
219func (v *Version) GetMetadata(key string) (string, bool) {
220 val, ok := v.metadata[key]
221 return val, ok
222}
223
224// AllMetadata return all metadata for this Version
225func (v *Version) AllMetadata() map[string]string {
226 return v.metadata
227}