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