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