operation_pack.go

  1package dag
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strconv"
  7	"strings"
  8
  9	"github.com/pkg/errors"
 10	"golang.org/x/crypto/openpgp"
 11
 12	"github.com/MichaelMure/git-bug/entity"
 13	"github.com/MichaelMure/git-bug/identity"
 14	"github.com/MichaelMure/git-bug/repository"
 15	"github.com/MichaelMure/git-bug/util/lamport"
 16)
 17
 18// TODO: extra data tree
 19const extraEntryName = "extra"
 20
 21const opsEntryName = "ops"
 22const versionEntryPrefix = "version-"
 23const createClockEntryPrefix = "create-clock-"
 24const editClockEntryPrefix = "edit-clock-"
 25const packClockEntryPrefix = "pack-clock-"
 26
 27// operationPack is a wrapper structure to store multiple operations in a single git blob.
 28// Additionally, it holds and store the metadata for those operations.
 29type operationPack struct {
 30	// An identifier, taken from a hash of the serialized Operations.
 31	id entity.Id
 32
 33	// The author of the Operations. Must be the same author for all the Operations.
 34	Author identity.Interface
 35	// The list of Operation stored in the operationPack
 36	Operations []Operation
 37	// Encode the entity's logical time of creation across all entities of the same type.
 38	// Only exist on the root operationPack
 39	CreateTime lamport.Time
 40	// Encode the entity's logical time of last edition across all entities of the same type.
 41	// Exist on all operationPack
 42	EditTime lamport.Time
 43	// // Encode the operationPack's logical time of creation withing this entity.
 44	// // Exist on all operationPack
 45	// PackTime lamport.Time
 46}
 47
 48func (opp *operationPack) Id() entity.Id {
 49	if opp.id == "" || opp.id == entity.UnsetId {
 50		// This means we are trying to get the opp's Id *before* it has been stored.
 51		// As the Id is computed based on the actual bytes written on the disk, we are going to predict
 52		// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
 53
 54		data, err := json.Marshal(opp)
 55		if err != nil {
 56			panic(err)
 57		}
 58		opp.id = entity.DeriveId(data)
 59	}
 60
 61	return opp.id
 62}
 63
 64func (opp *operationPack) MarshalJSON() ([]byte, error) {
 65	return json.Marshal(struct {
 66		Author     identity.Interface `json:"author"`
 67		Operations []Operation        `json:"ops"`
 68	}{
 69		Author:     opp.Author,
 70		Operations: opp.Operations,
 71	})
 72}
 73
 74func (opp *operationPack) Validate() error {
 75	if opp.Author == nil {
 76		return fmt.Errorf("missing author")
 77	}
 78	for _, op := range opp.Operations {
 79		if op.Author() != opp.Author {
 80			return fmt.Errorf("operation has different author than the operationPack's")
 81		}
 82	}
 83	if opp.EditTime == 0 {
 84		return fmt.Errorf("lamport edit time is zero")
 85	}
 86	return nil
 87}
 88
 89func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) {
 90	if err := opp.Validate(); err != nil {
 91		return "", err
 92	}
 93
 94	// For different reason, we store the clocks and format version directly in the git tree.
 95	// Version has to be accessible before any attempt to decode to return early with a unique error.
 96	// Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and
 97	// we are storing something directly in the tree already so why not.
 98	//
 99	// To have a valid Tree, we point the "fake" entries to always the same value, the empty blob.
100	emptyBlobHash, err := repo.StoreData([]byte{})
101	if err != nil {
102		return "", err
103	}
104
105	// Write the Ops as a Git blob containing the serialized array of operations
106	data, err := json.Marshal(opp)
107	if err != nil {
108		return "", err
109	}
110
111	// compute the Id while we have the serialized data
112	opp.id = entity.DeriveId(data)
113
114	hash, err := repo.StoreData(data)
115	if err != nil {
116		return "", err
117	}
118
119	// Make a Git tree referencing this blob and encoding the other values:
120	// - format version
121	// - clocks
122	tree := []repository.TreeEntry{
123		{ObjectType: repository.Blob, Hash: emptyBlobHash,
124			Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)},
125		{ObjectType: repository.Blob, Hash: hash,
126			Name: opsEntryName},
127		{ObjectType: repository.Blob, Hash: emptyBlobHash,
128			Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)},
129		// {ObjectType: repository.Blob, Hash: emptyBlobHash,
130		// 	Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)},
131	}
132	if opp.CreateTime > 0 {
133		tree = append(tree, repository.TreeEntry{
134			ObjectType: repository.Blob,
135			Hash:       emptyBlobHash,
136			Name:       fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime),
137		})
138	}
139
140	// Store the tree
141	treeHash, err := repo.StoreTree(tree)
142	if err != nil {
143		return "", err
144	}
145
146	// Write a Git commit referencing the tree, with the previous commit as parent
147	// If we have keys, sign.
148	var commitHash repository.Hash
149
150	// Sign the commit if we have a key
151	if opp.Author.SigningKey() != nil {
152		commitHash, err = repo.StoreSignedCommit(treeHash, opp.Author.SigningKey().PGPEntity(), parentCommit...)
153	} else {
154		commitHash, err = repo.StoreCommit(treeHash, parentCommit...)
155	}
156
157	if err != nil {
158		return "", err
159	}
160
161	return commitHash, nil
162}
163
164// readOperationPack read the operationPack encoded in git at the given Tree hash.
165//
166// Validity of the Lamport clocks is left for the caller to decide.
167func readOperationPack(def Definition, repo repository.RepoData, commit repository.Commit) (*operationPack, error) {
168	entries, err := repo.ReadTree(commit.TreeHash)
169	if err != nil {
170		return nil, err
171	}
172
173	// check the format version first, fail early instead of trying to read something
174	var version uint
175	for _, entry := range entries {
176		if strings.HasPrefix(entry.Name, versionEntryPrefix) {
177			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64)
178			if err != nil {
179				return nil, errors.Wrap(err, "can't read format version")
180			}
181			if v > 1<<12 {
182				return nil, fmt.Errorf("format version too big")
183			}
184			version = uint(v)
185			break
186		}
187	}
188	if version == 0 {
189		return nil, entity.NewErrUnknowFormat(def.formatVersion)
190	}
191	if version != def.formatVersion {
192		return nil, entity.NewErrInvalidFormat(version, def.formatVersion)
193	}
194
195	var id entity.Id
196	var author identity.Interface
197	var ops []Operation
198	var createTime lamport.Time
199	var editTime lamport.Time
200	// var packTime lamport.Time
201
202	for _, entry := range entries {
203		switch {
204		case entry.Name == opsEntryName:
205			data, err := repo.ReadData(entry.Hash)
206			if err != nil {
207				return nil, errors.Wrap(err, "failed to read git blob data")
208			}
209			ops, author, err = unmarshallPack(def, data)
210			if err != nil {
211				return nil, err
212			}
213			id = entity.DeriveId(data)
214
215		case strings.HasPrefix(entry.Name, createClockEntryPrefix):
216			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64)
217			if err != nil {
218				return nil, errors.Wrap(err, "can't read creation lamport time")
219			}
220			createTime = lamport.Time(v)
221
222		case strings.HasPrefix(entry.Name, editClockEntryPrefix):
223			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64)
224			if err != nil {
225				return nil, errors.Wrap(err, "can't read edit lamport time")
226			}
227			editTime = lamport.Time(v)
228
229			// case strings.HasPrefix(entry.Name, packClockEntryPrefix):
230			// 	found &= 1 << 3
231			//
232			// 	v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64)
233			// 	if err != nil {
234			// 		return nil, errors.Wrap(err, "can't read pack lamport time")
235			// 	}
236			// 	packTime = lamport.Time(v)
237		}
238	}
239
240	// Verify signature if we expect one
241	keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime)
242	if len(keys) > 0 {
243		keyring := identity.PGPKeyring(keys)
244		_, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature)
245		if err != nil {
246			return nil, fmt.Errorf("signature failure: %v", err)
247		}
248	}
249
250	return &operationPack{
251		id:         id,
252		Author:     author,
253		Operations: ops,
254		CreateTime: createTime,
255		EditTime:   editTime,
256		// PackTime:   packTime,
257	}, nil
258}
259
260// unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding
261// function provided by the concrete entity. This gives access to the concrete type of each
262// Operation.
263func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interface, error) {
264	aux := struct {
265		Author     identity.IdentityStub `json:"author"`
266		Operations []json.RawMessage     `json:"ops"`
267	}{}
268
269	if err := json.Unmarshal(data, &aux); err != nil {
270		return nil, nil, err
271	}
272
273	if aux.Author.Id() == "" || aux.Author.Id() == entity.UnsetId {
274		return nil, nil, fmt.Errorf("missing author")
275	}
276
277	author, err := def.identityResolver.ResolveIdentity(aux.Author.Id())
278	if err != nil {
279		return nil, nil, err
280	}
281
282	ops := make([]Operation, 0, len(aux.Operations))
283
284	for _, raw := range aux.Operations {
285		// delegate to specialized unmarshal function
286		op, err := def.operationUnmarshaler(author, raw)
287		if err != nil {
288			return nil, nil, err
289		}
290		ops = append(ops, op)
291	}
292
293	return ops, author, nil
294}