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