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