1package dag
2
3import (
4 "crypto/rand"
5 "encoding/json"
6 "fmt"
7 "time"
8
9 "github.com/pkg/errors"
10
11 "github.com/MichaelMure/git-bug/entities/identity"
12 "github.com/MichaelMure/git-bug/entity"
13)
14
15// Operation is an extended interface for an entity.Operation working with the dag package.
16type Operation interface {
17 entity.Operation
18
19 // setId allow to set the Id, used when unmarshalling only
20 setId(id entity.Id)
21 // setAuthor allow to set the author, used when unmarshalling only
22 setAuthor(author identity.Interface)
23 // setExtraMetadataImmutable add a metadata not carried by the operation itself on the operation
24 setExtraMetadataImmutable(key string, value string)
25}
26
27type OperationWithApply[SnapT entity.Snapshot] interface {
28 Operation
29
30 // Apply the operation to a Snapshot to create the final state
31 Apply(snapshot SnapT)
32}
33
34// OpBase implement the common feature that every Operation should support.
35type OpBase struct {
36 // Not serialized. Store the op's id in memory.
37 id entity.Id
38 // Not serialized
39 author identity.Interface
40
41 OperationType entity.OperationType `json:"type"`
42 UnixTime int64 `json:"timestamp"`
43
44 // mandatory random bytes to ensure a better randomness of the data used to later generate the ID
45 // len(Nonce) should be > 20 and < 64 bytes
46 // It has no functional purpose and should be ignored.
47 Nonce []byte `json:"nonce"`
48
49 Metadata map[string]string `json:"metadata,omitempty"`
50 // Not serialized. Store the extra metadata in memory,
51 // compiled from SetMetadataOperation.
52 extraMetadata map[string]string
53}
54
55func NewOpBase(opType entity.OperationType, author identity.Interface, unixTime int64) OpBase {
56 return OpBase{
57 OperationType: opType,
58 author: author,
59 UnixTime: unixTime,
60 Nonce: makeNonce(20),
61 id: entity.UnsetId,
62 }
63}
64
65func makeNonce(len int) []byte {
66 result := make([]byte, len)
67 _, err := rand.Read(result)
68 if err != nil {
69 panic(err)
70 }
71 return result
72}
73
74func IdOperation(op Operation, base *OpBase) entity.Id {
75 if base.id == "" {
76 // something went really wrong
77 panic("op's id not set")
78 }
79 if base.id == entity.UnsetId {
80 // This means we are trying to get the op's Id *before* it has been stored, for instance when
81 // adding multiple ops in one go in an OperationPack.
82 // As the Id is computed based on the actual bytes written on the disk, we are going to predict
83 // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
84
85 data, err := json.Marshal(op)
86 if err != nil {
87 panic(err)
88 }
89
90 base.id = entity.DeriveId(data)
91 }
92 return base.id
93}
94
95func (base *OpBase) Type() entity.OperationType {
96 return base.OperationType
97}
98
99// Time return the time when the operation was added
100func (base *OpBase) Time() time.Time {
101 return time.Unix(base.UnixTime, 0)
102}
103
104// Validate check the OpBase for errors
105func (base *OpBase) Validate(op entity.Operation, opType entity.OperationType) error {
106 if base.OperationType == 0 {
107 return fmt.Errorf("operation type unset")
108 }
109 if base.OperationType != opType {
110 return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
111 }
112
113 if op.Time().Unix() == 0 {
114 return fmt.Errorf("time not set")
115 }
116
117 if base.author == nil {
118 return fmt.Errorf("author not set")
119 }
120
121 if err := op.Author().Validate(); err != nil {
122 return errors.Wrap(err, "author")
123 }
124
125 if op, ok := op.(entity.OperationWithFiles); ok {
126 for _, hash := range op.GetFiles() {
127 if !hash.IsValid() {
128 return fmt.Errorf("file with invalid hash %v", hash)
129 }
130 }
131 }
132
133 if len(base.Nonce) > 64 {
134 return fmt.Errorf("nonce is too big")
135 }
136 if len(base.Nonce) < 20 {
137 return fmt.Errorf("nonce is too small")
138 }
139
140 return nil
141}
142
143// IsAuthored is a sign post method for gqlgen
144func (base *OpBase) IsAuthored() {}
145
146// Author return author identity
147func (base *OpBase) Author() identity.Interface {
148 return base.author
149}
150
151// IdIsSet returns true if the id has been set already
152func (base *OpBase) IdIsSet() bool {
153 return base.id != "" && base.id != entity.UnsetId
154}
155
156// SetMetadata store arbitrary metadata about the operation
157func (base *OpBase) SetMetadata(key string, value string) {
158 if base.IdIsSet() {
159 panic("set metadata on an operation with already an Id")
160 }
161
162 if base.Metadata == nil {
163 base.Metadata = make(map[string]string)
164 }
165 base.Metadata[key] = value
166}
167
168// GetMetadata retrieve arbitrary metadata about the operation
169func (base *OpBase) GetMetadata(key string) (string, bool) {
170 val, ok := base.Metadata[key]
171
172 if ok {
173 return val, true
174 }
175
176 // extraMetadata can't replace the original operations value if any
177 val, ok = base.extraMetadata[key]
178
179 return val, ok
180}
181
182// AllMetadata return all metadata for this operation
183func (base *OpBase) AllMetadata() map[string]string {
184 result := make(map[string]string)
185
186 for key, val := range base.extraMetadata {
187 result[key] = val
188 }
189
190 // Original metadata take precedence
191 for key, val := range base.Metadata {
192 result[key] = val
193 }
194
195 return result
196}
197
198// setId allow to set the Id, used when unmarshalling only
199func (base *OpBase) setId(id entity.Id) {
200 if base.id != "" && base.id != entity.UnsetId {
201 panic("trying to set id again")
202 }
203 base.id = id
204}
205
206// setAuthor allow to set the author, used when unmarshalling only
207func (base *OpBase) setAuthor(author identity.Interface) {
208 base.author = author
209}
210
211func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
212 if base.extraMetadata == nil {
213 base.extraMetadata = make(map[string]string)
214 }
215 if _, exist := base.extraMetadata[key]; !exist {
216 base.extraMetadata[key] = value
217 }
218}