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