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}