version.go

  1package identity
  2
  3import (
  4	"crypto/rand"
  5	"crypto/sha256"
  6	"encoding/json"
  7	"fmt"
  8	"strings"
  9	"time"
 10
 11	"github.com/pkg/errors"
 12
 13	"github.com/MichaelMure/git-bug/entity"
 14	"github.com/MichaelMure/git-bug/repository"
 15	"github.com/MichaelMure/git-bug/util/lamport"
 16	"github.com/MichaelMure/git-bug/util/text"
 17)
 18
 19// 1: original format
 20// 2: Identity Ids are generated from the first version serialized data instead of from the first git commit
 21//    + Identity hold multiple lamport clocks from other entities, instead of just bug edit
 22const formatVersion = 2
 23
 24// version is a complete set of information about an Identity at a point in time.
 25type version struct {
 26	name      string
 27	email     string // as defined in git or from a bridge when importing the identity
 28	login     string // from a bridge when importing the identity
 29	avatarURL string
 30
 31	// The lamport times of the other entities at which this version become effective
 32	times    map[string]lamport.Time
 33	unixTime int64
 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	// mandatory random bytes to ensure a better randomness of the data of the first
 41	// version of a bug, used to later generate the ID
 42	// len(Nonce) should be > 20 and < 64 bytes
 43	// It has no functional purpose and should be ignored.
 44	// TODO: optional after first version?
 45	nonce []byte
 46
 47	// A set of arbitrary key/value to store metadata about a version or about an Identity in general.
 48	metadata map[string]string
 49
 50	// Not serialized. Store the version's id in memory.
 51	id entity.Id
 52	// Not serialized
 53	commitHash repository.Hash
 54}
 55
 56func newVersion(repo repository.RepoClock, name string, email string, login string, avatarURL string, keys []*Key) (*version, error) {
 57	clocks, err := repo.AllClocks()
 58	if err != nil {
 59		return nil, err
 60	}
 61
 62	times := make(map[string]lamport.Time)
 63	for name, clock := range clocks {
 64		times[name] = clock.Time()
 65	}
 66
 67	return &version{
 68		id:        entity.UnsetId,
 69		name:      name,
 70		email:     email,
 71		login:     login,
 72		avatarURL: avatarURL,
 73		times:     times,
 74		unixTime:  time.Now().Unix(),
 75		keys:      keys,
 76		nonce:     makeNonce(20),
 77	}, nil
 78}
 79
 80type versionJSON struct {
 81	// Additional field to version the data
 82	FormatVersion uint `json:"version"`
 83
 84	Times     map[string]lamport.Time `json:"times"`
 85	UnixTime  int64                   `json:"unix_time"`
 86	Name      string                  `json:"name,omitempty"`
 87	Email     string                  `json:"email,omitempty"`
 88	Login     string                  `json:"login,omitempty"`
 89	AvatarUrl string                  `json:"avatar_url,omitempty"`
 90	Keys      []*Key                  `json:"pub_keys,omitempty"`
 91	Nonce     []byte                  `json:"nonce"`
 92	Metadata  map[string]string       `json:"metadata,omitempty"`
 93}
 94
 95// Id return the identifier of the version
 96func (v *version) Id() entity.Id {
 97	if v.id == "" {
 98		// something went really wrong
 99		panic("version's id not set")
100	}
101	if v.id == entity.UnsetId {
102		// This means we are trying to get the version's Id *before* it has been stored.
103		// As the Id is computed based on the actual bytes written on the disk, we are going to predict
104		// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
105		data, err := json.Marshal(v)
106		if err != nil {
107			panic(err)
108		}
109		v.id = deriveId(data)
110	}
111	return v.id
112}
113
114func deriveId(data []byte) entity.Id {
115	sum := sha256.Sum256(data)
116	return entity.Id(fmt.Sprintf("%x", sum))
117}
118
119// Make a deep copy
120func (v *version) Clone() *version {
121	// copy direct fields
122	clone := *v
123
124	// reset some fields
125	clone.commitHash = ""
126	clone.id = entity.UnsetId
127
128	clone.times = make(map[string]lamport.Time)
129	for name, t := range v.times {
130		clone.times[name] = t
131	}
132
133	clone.keys = make([]*Key, len(v.keys))
134	for i, key := range v.keys {
135		clone.keys[i] = key.Clone()
136	}
137
138	clone.nonce = make([]byte, len(v.nonce))
139	copy(clone.nonce, v.nonce)
140
141	// not copying metadata
142
143	return &clone
144}
145
146func (v *version) MarshalJSON() ([]byte, error) {
147	return json.Marshal(versionJSON{
148		FormatVersion: formatVersion,
149		Times:         v.times,
150		UnixTime:      v.unixTime,
151		Name:          v.name,
152		Email:         v.email,
153		Login:         v.login,
154		AvatarUrl:     v.avatarURL,
155		Keys:          v.keys,
156		Nonce:         v.nonce,
157		Metadata:      v.metadata,
158	})
159}
160
161func (v *version) UnmarshalJSON(data []byte) error {
162	var aux versionJSON
163
164	if err := json.Unmarshal(data, &aux); err != nil {
165		return err
166	}
167
168	if aux.FormatVersion < formatVersion {
169		return entity.NewErrOldFormatVersion(aux.FormatVersion)
170	}
171	if aux.FormatVersion > formatVersion {
172		return entity.NewErrNewFormatVersion(aux.FormatVersion)
173	}
174
175	v.id = deriveId(data)
176	v.times = aux.Times
177	v.unixTime = aux.UnixTime
178	v.name = aux.Name
179	v.email = aux.Email
180	v.login = aux.Login
181	v.avatarURL = aux.AvatarUrl
182	v.keys = aux.Keys
183	v.nonce = aux.Nonce
184	v.metadata = aux.Metadata
185
186	return nil
187}
188
189func (v *version) Validate() error {
190	// time must be set after a commit
191	if v.commitHash != "" && v.unixTime == 0 {
192		return fmt.Errorf("unix time not set")
193	}
194
195	if text.Empty(v.name) && text.Empty(v.login) {
196		return fmt.Errorf("either name or login should be set")
197	}
198	if strings.Contains(v.name, "\n") {
199		return fmt.Errorf("name should be a single line")
200	}
201	if !text.Safe(v.name) {
202		return fmt.Errorf("name is not fully printable")
203	}
204
205	if strings.Contains(v.login, "\n") {
206		return fmt.Errorf("login should be a single line")
207	}
208	if !text.Safe(v.login) {
209		return fmt.Errorf("login is not fully printable")
210	}
211
212	if strings.Contains(v.email, "\n") {
213		return fmt.Errorf("email should be a single line")
214	}
215	if !text.Safe(v.email) {
216		return fmt.Errorf("email is not fully printable")
217	}
218
219	if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {
220		return fmt.Errorf("avatarUrl is not a valid URL")
221	}
222
223	if len(v.nonce) > 64 {
224		return fmt.Errorf("nonce is too big")
225	}
226	if len(v.nonce) < 20 {
227		return fmt.Errorf("nonce is too small")
228	}
229
230	for _, k := range v.keys {
231		if err := k.Validate(); err != nil {
232			return errors.Wrap(err, "invalid key")
233		}
234	}
235
236	return nil
237}
238
239// Write will serialize and store the version as a git blob and return
240// its hash
241func (v *version) Write(repo repository.Repo) (repository.Hash, error) {
242	// make sure we don't write invalid data
243	err := v.Validate()
244	if err != nil {
245		return "", errors.Wrap(err, "validation error")
246	}
247
248	data, err := json.Marshal(v)
249	if err != nil {
250		return "", err
251	}
252
253	hash, err := repo.StoreData(data)
254	if err != nil {
255		return "", err
256	}
257
258	// make sure we set the Id when writing in the repo
259	v.id = deriveId(data)
260
261	return hash, nil
262}
263
264func makeNonce(len int) []byte {
265	result := make([]byte, len)
266	_, err := rand.Read(result)
267	if err != nil {
268		panic(err)
269	}
270	return result
271}
272
273// SetMetadata store arbitrary metadata about a version or an Identity in general
274// If the version has been commit to git already, it won't be overwritten.
275// Beware: changing the metadata on a version will change it's ID
276func (v *version) SetMetadata(key string, value string) {
277	if v.metadata == nil {
278		v.metadata = make(map[string]string)
279	}
280	v.metadata[key] = value
281}
282
283// GetMetadata retrieve arbitrary metadata about the version
284func (v *version) GetMetadata(key string) (string, bool) {
285	val, ok := v.metadata[key]
286	return val, ok
287}
288
289// AllMetadata return all metadata for this version
290func (v *version) AllMetadata() map[string]string {
291	return v.metadata
292}