1package object
2
3import (
4 "bytes"
5 "encoding/binary"
6 "fmt"
7 "sort"
8 "time"
9
10 "github.com/zeebo/blake3"
11)
12
13func HashBlob(b *Blob) [32]byte {
14 var buf bytes.Buffer
15 EncodeBlob(&buf, b)
16 return blake3.Sum256(buf.Bytes())
17}
18
19func HashTree(t *Tree) [32]byte {
20 var buf bytes.Buffer
21 EncodeTree(&buf, t)
22 return blake3.Sum256(buf.Bytes())
23}
24
25func HashCommit(c *Commit) [32]byte {
26 var buf bytes.Buffer
27 EncodeCommit(&buf, c)
28 return blake3.Sum256(buf.Bytes())
29}
30
31func HashConflict(c *Conflict) [32]byte {
32 var buf bytes.Buffer
33 EncodeConflict(&buf, c)
34 return blake3.Sum256(buf.Bytes())
35}
36
37func HashObsolete(o *ObsoleteMarker) [32]byte {
38 var buf bytes.Buffer
39 EncodeObsolete(&buf, o)
40 return blake3.Sum256(buf.Bytes())
41}
42
43func EncodeBlob(w *bytes.Buffer, b *Blob) {
44 w.WriteString("arche-blob\x00")
45 writeUint64(w, uint64(len(b.Content)))
46 w.Write(b.Content)
47}
48
49func EncodeTree(w *bytes.Buffer, t *Tree) {
50 entries := make([]TreeEntry, len(t.Entries))
51 copy(entries, t.Entries)
52 sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
53
54 w.WriteString("arche-tree\x00")
55 writeUint32(w, uint32(len(entries)))
56 for _, e := range entries {
57 w.WriteByte(byte(e.Mode))
58 writeUint16(w, uint16(len(e.Name)))
59 w.WriteString(e.Name)
60 w.Write(e.ObjectID[:])
61
62 keys := make([]string, 0, len(e.Props))
63 for k := range e.Props {
64 keys = append(keys, k)
65 }
66 sort.Strings(keys)
67 writeUint16(w, uint16(len(keys)))
68 for _, k := range keys {
69 v := e.Props[k]
70 writeUint16(w, uint16(len(k)))
71 w.WriteString(k)
72 writeUint16(w, uint16(len(v)))
73 w.WriteString(v)
74 }
75 }
76}
77
78func EncodeCommit(w *bytes.Buffer, c *Commit) {
79 w.WriteString("arche-commit\x00")
80 w.Write(c.TreeID[:])
81 w.WriteByte(byte(len(c.Parents)))
82 for _, p := range c.Parents {
83 w.Write(p[:])
84 }
85 w.WriteByte(byte(len(c.ChangeID)))
86 w.WriteString(c.ChangeID)
87 w.WriteByte(byte(c.Phase))
88 encodeSignature(w, c.Author)
89 encodeSignature(w, c.Committer)
90 writeUint32(w, uint32(len(c.Message)))
91 w.WriteString(c.Message)
92 if len(c.CommitSig) > 0 {
93 w.WriteByte(1)
94 writeUint16(w, uint16(len(c.CommitSig)))
95 w.Write(c.CommitSig)
96 }
97}
98
99func CommitBodyForSigning(c *Commit) []byte {
100 tmp := *c
101 tmp.CommitSig = nil
102 var buf bytes.Buffer
103 EncodeCommit(&buf, &tmp)
104 return buf.Bytes()
105}
106
107func EncodeConflict(w *bytes.Buffer, c *Conflict) {
108 w.WriteString("arche-conflict\x00")
109 if c.Base != nil {
110 w.WriteByte(1)
111 w.Write(c.Base.CommitID[:])
112 w.Write(c.Base.BlobID[:])
113 } else {
114 w.WriteByte(0)
115 }
116 w.Write(c.Ours.CommitID[:])
117 w.Write(c.Ours.BlobID[:])
118 w.Write(c.Theirs.CommitID[:])
119 w.Write(c.Theirs.BlobID[:])
120}
121
122func EncodeObsolete(w *bytes.Buffer, o *ObsoleteMarker) {
123 w.WriteString("arche-obsolete\x00")
124 w.Write(o.Predecessor[:])
125 w.WriteByte(byte(len(o.Successors)))
126 for _, s := range o.Successors {
127 w.Write(s[:])
128 }
129 w.WriteByte(byte(len(o.Reason)))
130 w.WriteString(o.Reason)
131 writeInt64(w, o.Timestamp)
132}
133
134func DecodeBlob(data []byte) (*Blob, error) {
135 prefix := "arche-blob\x00"
136 if !bytes.HasPrefix(data, []byte(prefix)) {
137 return nil, fmt.Errorf("invalid blob prefix")
138 }
139 data = data[len(prefix):]
140 if len(data) < 8 {
141 return nil, fmt.Errorf("blob: length field truncated")
142 }
143 l := binary.BigEndian.Uint64(data[:8])
144 data = data[8:]
145 if uint64(len(data)) < l {
146 return nil, fmt.Errorf("blob: content truncated (want %d bytes, got %d)", l, len(data))
147 }
148 content := make([]byte, l)
149 copy(content, data[:l])
150 return &Blob{Content: content}, nil
151}
152
153func DecodeTree(data []byte) (*Tree, error) {
154 prefix := "arche-tree\x00"
155 if !bytes.HasPrefix(data, []byte(prefix)) {
156 return nil, fmt.Errorf("invalid tree prefix")
157 }
158 data = data[len(prefix):]
159 if len(data) < 4 {
160 return nil, fmt.Errorf("tree: entry count truncated")
161 }
162 n := binary.BigEndian.Uint32(data[:4])
163 data = data[4:]
164
165 entries := make([]TreeEntry, 0, n)
166 for i := uint32(0); i < n; i++ {
167 if len(data) < 1 {
168 return nil, fmt.Errorf("tree entry %d: truncated at mode", i)
169 }
170 mode := EntryMode(data[0])
171 data = data[1:]
172
173 if len(data) < 2 {
174 return nil, fmt.Errorf("tree entry %d: name len truncated", i)
175 }
176 nameLen := binary.BigEndian.Uint16(data[:2])
177 data = data[2:]
178 if len(data) < int(nameLen) {
179 return nil, fmt.Errorf("tree entry %d: name truncated", i)
180 }
181 name := string(data[:nameLen])
182 data = data[nameLen:]
183
184 if len(data) < 32 {
185 return nil, fmt.Errorf("tree entry %d: object ID truncated", i)
186 }
187 var oid [32]byte
188 copy(oid[:], data[:32])
189 data = data[32:]
190
191 if len(data) < 2 {
192 return nil, fmt.Errorf("tree entry %d: props count truncated", i)
193 }
194 numProps := binary.BigEndian.Uint16(data[:2])
195 data = data[2:]
196
197 props := make(map[string]string, numProps)
198 for j := uint16(0); j < numProps; j++ {
199 if len(data) < 2 {
200 return nil, fmt.Errorf("tree entry %d prop %d: key len truncated", i, j)
201 }
202 kl := binary.BigEndian.Uint16(data[:2])
203 data = data[2:]
204 if len(data) < int(kl) {
205 return nil, fmt.Errorf("tree entry %d prop %d: key truncated", i, j)
206 }
207 k := string(data[:kl])
208 data = data[kl:]
209
210 if len(data) < 2 {
211 return nil, fmt.Errorf("tree entry %d prop %d: val len truncated", i, j)
212 }
213 vl := binary.BigEndian.Uint16(data[:2])
214 data = data[2:]
215 if len(data) < int(vl) {
216 return nil, fmt.Errorf("tree entry %d prop %d: val truncated", i, j)
217 }
218 props[k] = string(data[:vl])
219 data = data[vl:]
220 }
221
222 entries = append(entries, TreeEntry{Name: name, Mode: mode, ObjectID: oid, Props: props})
223 }
224
225 for i := 1; i < len(entries); i++ {
226 if entries[i].Name <= entries[i-1].Name {
227 return nil, fmt.Errorf("tree entries not correctly sorted at index %d", i)
228 }
229 }
230 return &Tree{Entries: entries}, nil
231}
232
233func DecodeCommit(data []byte) (*Commit, error) {
234 prefix := "arche-commit\x00"
235 if !bytes.HasPrefix(data, []byte(prefix)) {
236 return nil, fmt.Errorf("invalid commit prefix")
237 }
238 data = data[len(prefix):]
239
240 if len(data) < 32 {
241 return nil, fmt.Errorf("commit: tree ID truncated")
242 }
243 var treeID [32]byte
244 copy(treeID[:], data[:32])
245 data = data[32:]
246
247 if len(data) < 1 {
248 return nil, fmt.Errorf("commit: parent count truncated")
249 }
250 np := int(data[0])
251 data = data[1:]
252 parents := make([][32]byte, np)
253 for i := range parents {
254 if len(data) < 32 {
255 return nil, fmt.Errorf("commit: parent %d truncated", i)
256 }
257 copy(parents[i][:], data[:32])
258 data = data[32:]
259 }
260
261 if len(data) < 1 {
262 return nil, fmt.Errorf("commit: change ID len truncated")
263 }
264 cidLen := int(data[0])
265 data = data[1:]
266 if len(data) < cidLen {
267 return nil, fmt.Errorf("commit: change ID truncated")
268 }
269 changeID := string(data[:cidLen])
270 data = data[cidLen:]
271
272 if len(data) < 1 {
273 return nil, fmt.Errorf("commit: phase truncated")
274 }
275 phase := Phase(data[0])
276 data = data[1:]
277
278 author, data, err := decodeSignature(data)
279 if err != nil {
280 return nil, fmt.Errorf("commit author: %w", err)
281 }
282 committer, data, err := decodeSignature(data)
283 if err != nil {
284 return nil, fmt.Errorf("commit committer: %w", err)
285 }
286
287 if len(data) < 4 {
288 return nil, fmt.Errorf("commit: message len truncated")
289 }
290 ml := binary.BigEndian.Uint32(data[:4])
291 data = data[4:]
292 if uint32(len(data)) < ml {
293 return nil, fmt.Errorf("commit: message truncated")
294 }
295 msg := string(data[:ml])
296 data = data[ml:]
297
298 var commitSig []byte
299 if len(data) >= 1 && data[0] == 1 {
300 data = data[1:]
301 if len(data) < 2 {
302 return nil, fmt.Errorf("commit: signature length truncated")
303 }
304 sigLen := binary.BigEndian.Uint16(data[:2])
305 data = data[2:]
306 if len(data) < int(sigLen) {
307 return nil, fmt.Errorf("commit: signature truncated")
308 }
309 commitSig = make([]byte, sigLen)
310 copy(commitSig, data[:sigLen])
311 }
312
313 return &Commit{
314 TreeID: treeID,
315 Parents: parents,
316 ChangeID: changeID,
317 Author: author,
318 Committer: committer,
319 Message: msg,
320 Phase: phase,
321 CommitSig: commitSig,
322 }, nil
323}
324
325func DecodeConflict(data []byte) (*Conflict, error) {
326 prefix := "arche-conflict\x00"
327 if !bytes.HasPrefix(data, []byte(prefix)) {
328 return nil, fmt.Errorf("invalid conflict prefix")
329 }
330 data = data[len(prefix):]
331
332 if len(data) < 1 {
333 return nil, fmt.Errorf("conflict: base flag truncated")
334 }
335 hasBase := data[0] == 1
336 data = data[1:]
337
338 c := &Conflict{}
339 if hasBase {
340 if len(data) < 64 {
341 return nil, fmt.Errorf("conflict: base side truncated")
342 }
343 var base ConflictSide
344 copy(base.CommitID[:], data[:32])
345 copy(base.BlobID[:], data[32:64])
346 data = data[64:]
347 c.Base = &base
348 }
349
350 if len(data) < 128 {
351 return nil, fmt.Errorf("conflict: ours/theirs truncated")
352 }
353 copy(c.Ours.CommitID[:], data[:32])
354 copy(c.Ours.BlobID[:], data[32:64])
355 copy(c.Theirs.CommitID[:], data[64:96])
356 copy(c.Theirs.BlobID[:], data[96:128])
357 return c, nil
358}
359
360func DecodeObsolete(data []byte) (*ObsoleteMarker, error) {
361 prefix := "arche-obsolete\x00"
362 if !bytes.HasPrefix(data, []byte(prefix)) {
363 return nil, fmt.Errorf("invalid obsolete prefix")
364 }
365 data = data[len(prefix):]
366
367 if len(data) < 32 {
368 return nil, fmt.Errorf("obsolete: predecessor truncated")
369 }
370 var pred [32]byte
371 copy(pred[:], data[:32])
372 data = data[32:]
373
374 if len(data) < 1 {
375 return nil, fmt.Errorf("obsolete: successor count truncated")
376 }
377 ns := int(data[0])
378 data = data[1:]
379 succs := make([][32]byte, ns)
380 for i := range succs {
381 if len(data) < 32 {
382 return nil, fmt.Errorf("obsolete: successor %d truncated", i)
383 }
384 copy(succs[i][:], data[:32])
385 data = data[32:]
386 }
387
388 if len(data) < 1 {
389 return nil, fmt.Errorf("obsolete: reason len truncated")
390 }
391 rl := int(data[0])
392 data = data[1:]
393 if len(data) < rl {
394 return nil, fmt.Errorf("obsolete: reason truncated")
395 }
396 reason := string(data[:rl])
397 data = data[rl:]
398
399 if len(data) < 8 {
400 return nil, fmt.Errorf("obsolete: timestamp truncated")
401 }
402 ts := int64(binary.BigEndian.Uint64(data[:8]))
403
404 return &ObsoleteMarker{
405 Predecessor: pred,
406 Successors: succs,
407 Reason: reason,
408 Timestamp: ts,
409 }, nil
410}
411
412func encodeSignature(w *bytes.Buffer, s Signature) {
413 writeUint16(w, uint16(len(s.Name)))
414 w.WriteString(s.Name)
415 writeUint16(w, uint16(len(s.Email)))
416 w.WriteString(s.Email)
417 writeInt64(w, s.Timestamp.UnixNano())
418}
419
420func decodeSignature(data []byte) (Signature, []byte, error) {
421 if len(data) < 2 {
422 return Signature{}, nil, fmt.Errorf("name len truncated")
423 }
424 nl := binary.BigEndian.Uint16(data[:2])
425 data = data[2:]
426 if len(data) < int(nl) {
427 return Signature{}, nil, fmt.Errorf("name truncated")
428 }
429 name := string(data[:nl])
430 data = data[nl:]
431
432 if len(data) < 2 {
433 return Signature{}, nil, fmt.Errorf("email len truncated")
434 }
435 el := binary.BigEndian.Uint16(data[:2])
436 data = data[2:]
437 if len(data) < int(el) {
438 return Signature{}, nil, fmt.Errorf("email truncated")
439 }
440 email := string(data[:el])
441 data = data[el:]
442
443 if len(data) < 8 {
444 return Signature{}, nil, fmt.Errorf("timestamp truncated")
445 }
446 unixNs := int64(binary.BigEndian.Uint64(data[:8]))
447 data = data[8:]
448
449 return Signature{
450 Name: name,
451 Email: email,
452 Timestamp: time.Unix(0, unixNs).UTC(),
453 }, data, nil
454}
455
456func writeUint64(w *bytes.Buffer, v uint64) {
457 var b [8]byte
458 binary.BigEndian.PutUint64(b[:], v)
459 w.Write(b[:])
460}
461
462func writeUint32(w *bytes.Buffer, v uint32) {
463 var b [4]byte
464 binary.BigEndian.PutUint32(b[:], v)
465 w.Write(b[:])
466}
467
468func writeUint16(w *bytes.Buffer, v uint16) {
469 var b [2]byte
470 binary.BigEndian.PutUint16(b[:], v)
471 w.Write(b[:])
472}
473
474func writeInt64(w *bytes.Buffer, v int64) {
475 var b [8]byte
476 binary.BigEndian.PutUint64(b[:], uint64(v))
477 w.Write(b[:])
478}