version.go

  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}