version.go

  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}