1package snapshot
2
3import (
4 "crypto/sha256"
5 "encoding/binary"
6 "fmt"
7 "hash/crc32"
8 "math"
9
10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"
12 "github.com/bnema/vev/internal/usecase/layout"
13)
14
15const (
16 manifestMagic = "VEVM"
17 headMagic = "VEVH"
18 objectMagic = "VEVO"
19 ManifestVersion = uint16(4) // Reject dense VT checkpoints before loading their objects.
20 manifestHeaderSize = 16
21 objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length
22 minObjectEnvelopeSize = manifestHeaderSize + objectEnvelopeBodyPrefixSize + 1
23 objectRefDigestSize = sha256.Size
24 objectRefSizeFieldSize = 4
25
26 manifestNodeLeaf uint8 = 0
27 manifestNodeSplit uint8 = 1
28 manifestNodeStack uint8 = 2
29 manifestNodeNil uint8 = 3
30
31 processAbsent uint8 = 0
32 processPresent uint8 = 1
33
34 manifestWeightTag = "WGT1"
35 manifestWeightSize = 8
36 // maxDecodedBodySize includes the object kind and payload-length prefix.
37 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize
38 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize
39)
40
41// SnapshotDigest is the shared content-address type used by the repository.
42type SnapshotDigest = ports.SnapshotDigest
43
44func ManifestDigest(encoded []byte) SnapshotDigest { return sha256.Sum256(encoded) }
45
46type ObjectKind uint8
47
48const (
49 HistoryChunk ObjectKind = iota + 1
50 HistoryTail
51 RecoveryTranscript
52)
53
54type ObjectRef struct {
55 Kind ObjectKind
56 Digest SnapshotDigest
57 Size uint32
58}
59
60// Head is the repository-owned generation pointer. It is deliberately not a
61// VEVM payload; adapters produce and atomically manage HEAD later.
62type Head struct {
63 Generation uint64
64 ManifestDigest SnapshotDigest
65}
66
67// MarshalHead encodes the deterministic, strict VEVH generation pointer.
68func MarshalHead(h Head) ([]byte, error) {
69 if h.Generation == 0 || isZeroDigestBytes(h.ManifestDigest[:]) {
70 return nil, fmt.Errorf("%w: head", ErrInvalidData)
71 }
72 var w payloadWriter
73 w.putUint64(h.Generation)
74 w.b = append(w.b, h.ManifestDigest[:]...)
75 return marshalManifestEnvelope(headMagic, w.b)
76}
77
78// UnmarshalHead decodes a complete VEVH generation pointer.
79func UnmarshalHead(encoded []byte) (Head, error) {
80 body, err := unmarshalManifestEnvelope(encoded, headMagic)
81 if err != nil {
82 return Head{}, err
83 }
84 if len(body) < 8+sha256.Size {
85 return Head{}, ErrShortPayload
86 }
87 if len(body) != 8+sha256.Size {
88 return Head{}, ErrTrailingBytes
89 }
90 head := Head{Generation: binary.BigEndian.Uint64(body[:8])}
91 copy(head.ManifestDigest[:], body[8:])
92 if head.Generation == 0 || isZeroDigestBytes(head.ManifestDigest[:]) {
93 return Head{}, fmt.Errorf("%w: head", ErrInvalidData)
94 }
95 return head, nil
96}
97
98// Manifest is the complete VEVM payload carried directly in a publication.
99type Manifest struct {
100 Generation uint64
101 IncarnationID domain.IncarnationID
102 ParentCheckpoint *domain.CheckpointRef
103 Name string
104 CreatedAt uint64
105 Active uint16
106 Tabs []ManifestTab
107}
108
109type ManifestTab struct {
110 StableID string
111 Cols uint16
112 Rows uint16
113 NextPaneID uint64
114 Focus layout.PaneID
115 Tree *layout.Tree
116 Panes []ManifestPane
117}
118
119type ManifestPane struct {
120 ID layout.PaneID
121 StableID string
122 Cwd string
123 Sealed []ObjectRef
124 Tail ObjectRef
125 Transcript ObjectRef
126 Process *Process
127}
128
129func MarshalManifest(m Manifest) ([]byte, error) {
130 if err := validateManifest(m); err != nil {
131 return nil, err
132 }
133 var w payloadWriter
134 w.putUint64(m.Generation)
135 w.b = append(w.b, m.IncarnationID[:]...)
136 if m.ParentCheckpoint == nil {
137 w.putUint8(0)
138 } else {
139 w.putUint8(1)
140 w.putUint64(m.ParentCheckpoint.Generation)
141 w.b = append(w.b, m.ParentCheckpoint.ManifestDigest[:]...)
142 }
143 if err := w.putString(m.Name); err != nil {
144 return nil, err
145 }
146 w.putUint64(m.CreatedAt)
147 w.putUint16(m.Active)
148 if len(m.Tabs) > math.MaxUint16 {
149 return nil, fmt.Errorf("%w: too many tabs", ErrInvalidData)
150 }
151 w.putUint16(uint16(len(m.Tabs)))
152 for _, tab := range m.Tabs {
153 if err := writeManifestTab(&w, tab); err != nil {
154 return nil, err
155 }
156 }
157 if err := writeManifestWeights(&w, manifestNodes(m)); err != nil {
158 return nil, err
159 }
160 return marshalManifestEnvelope(manifestMagic, w.b)
161}
162
163func UnmarshalManifest(encoded []byte) (Manifest, error) {
164 body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
165 if err != nil {
166 return Manifest{}, err
167 }
168 if err := preflightManifest(body); err != nil {
169 return Manifest{}, err
170 }
171 return decodeManifest(body)
172}
173
174// decodeManifest decodes an already preflighted payload. Keeping this separate
175// allows the preflight grammar to be differentially tested against decoding.
176func decodeManifest(body []byte) (Manifest, error) {
177 r := payloadReader{b: body}
178 generation, err := r.getUint64()
179 if err != nil {
180 return Manifest{}, err
181 }
182 incarnationBytes, err := r.getBytes(len(domain.IncarnationID{}))
183 if err != nil {
184 return Manifest{}, err
185 }
186 var incarnationID domain.IncarnationID
187 copy(incarnationID[:], incarnationBytes)
188 parentPresent, err := r.getUint8()
189 if err != nil {
190 return Manifest{}, err
191 }
192 var parent *domain.CheckpointRef
193 switch parentPresent {
194 case 0:
195 case 1:
196 parent = &domain.CheckpointRef{}
197 parent.Generation, err = r.getUint64()
198 if err != nil {
199 return Manifest{}, err
200 }
201 digest, err := r.getBytes(len(parent.ManifestDigest))
202 if err != nil {
203 return Manifest{}, err
204 }
205 copy(parent.ManifestDigest[:], digest)
206 default:
207 return Manifest{}, fmt.Errorf("%w: parent checkpoint presence", ErrInvalidData)
208 }
209 name, err := r.getString()
210 if err != nil {
211 return Manifest{}, err
212 }
213 createdAt, err := r.getUint64()
214 if err != nil {
215 return Manifest{}, err
216 }
217 active, err := r.getUint16()
218 if err != nil {
219 return Manifest{}, err
220 }
221 n, err := r.getUint16()
222 if err != nil {
223 return Manifest{}, err
224 }
225 m := Manifest{Generation: generation, IncarnationID: incarnationID, ParentCheckpoint: parent, Name: name, CreatedAt: createdAt, Active: active, Tabs: make([]ManifestTab, n)}
226 for i := range m.Tabs {
227 if m.Tabs[i], err = readManifestTab(&r); err != nil {
228 return Manifest{}, err
229 }
230 }
231 if err := readManifestWeights(&r, manifestNodes(m)); err != nil {
232 return Manifest{}, err
233 }
234 if err := validateManifest(m); err != nil {
235 return Manifest{}, err
236 }
237 return m, nil
238}
239
240// MarshalObject frames payload as VEVO and returns the bytes together with the
241// SHA-256 address of those complete bytes.
242func MarshalObject(kind ObjectKind, payload []byte) (ports.SnapshotObject, error) {
243 if !validObjectKind(kind) || len(payload) == 0 || len(payload) > maxObjectPayloadSize {
244 return ports.SnapshotObject{}, fmt.Errorf("%w: object", ErrInvalidData)
245 }
246 var w payloadWriter
247 w.putUint8(uint8(kind))
248 w.putUint32(uint32(len(payload)))
249 w.b = append(w.b, payload...)
250 data, err := marshalManifestEnvelope(objectMagic, w.b)
251 if err != nil {
252 return ports.SnapshotObject{}, err
253 }
254 return ports.SnapshotObject{Digest: sha256.Sum256(data), Data: data}, nil
255}
256
257func UnmarshalObject(encoded []byte) (ObjectKind, []byte, error) {
258 kind, payload, err := PreflightObject(encoded)
259 if err != nil {
260 return 0, nil, err
261 }
262 return kind, append([]byte(nil), payload...), nil
263}
264
265// PreflightObject verifies VEVO framing and declared payload size without
266// copying; the returned payload aliases encoded and is only valid while it is.
267func PreflightObject(encoded []byte) (ObjectKind, []byte, error) {
268 body, err := unmarshalManifestEnvelope(encoded, objectMagic)
269 if err != nil {
270 return 0, nil, err
271 }
272 if len(body) < 5 {
273 return 0, nil, ErrShortPayload
274 }
275 kind := ObjectKind(body[0])
276 if !validObjectKind(kind) {
277 return 0, nil, fmt.Errorf("%w: object kind", ErrInvalidData)
278 }
279 n := binary.BigEndian.Uint32(body[1:5])
280 if n == 0 || n > maxObjectPayloadSize {
281 return 0, nil, fmt.Errorf("%w: object length", ErrInvalidData)
282 }
283 if uint64(n) > uint64(len(body)-5) {
284 return 0, nil, ErrShortPayload
285 }
286 if uint64(n) != uint64(len(body)-5) {
287 return 0, nil, ErrTrailingBytes
288 }
289 return kind, body[5:], nil
290}
291
292func marshalManifestEnvelope(magic string, body []byte) ([]byte, error) {
293 if len(body) > maxDecodedBodySize || len(body) > math.MaxUint32 {
294 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
295 }
296 out := make([]byte, manifestHeaderSize+len(body))
297 copy(out[:4], magic)
298 binary.BigEndian.PutUint16(out[4:6], envelopeVersion(magic))
299 binary.BigEndian.PutUint32(out[8:12], uint32(len(body)))
300 binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
301 copy(out[16:], body)
302 return out, nil
303}
304
305func envelopeVersion(magic string) uint16 {
306 if magic == manifestMagic {
307 return ManifestVersion
308 }
309 return 1
310}
311
312func unmarshalManifestEnvelope(encoded []byte, expectedMagic string) ([]byte, error) {
313 if len(encoded) < manifestHeaderSize {
314 return nil, ErrShortPayload
315 }
316 if string(encoded[:4]) != expectedMagic {
317 return nil, ErrBadMagic
318 }
319 if binary.BigEndian.Uint16(encoded[4:6]) != envelopeVersion(expectedMagic) {
320 return nil, ErrBadVersion
321 }
322 if binary.BigEndian.Uint16(encoded[6:8]) != 0 {
323 return nil, fmt.Errorf("%w: flags", ErrInvalidData)
324 }
325 n := binary.BigEndian.Uint32(encoded[8:12])
326 if n > maxDecodedBodySize {
327 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
328 }
329 if uint64(len(encoded)) < uint64(manifestHeaderSize)+uint64(n) {
330 return nil, ErrShortPayload
331 }
332 if uint64(len(encoded)) != uint64(manifestHeaderSize)+uint64(n) {
333 return nil, ErrTrailingBytes
334 }
335 body := encoded[manifestHeaderSize:]
336 if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(encoded[12:16]) {
337 return nil, ErrBadCRC
338 }
339 return body, nil
340}
341
342func writeManifestTab(w *payloadWriter, tab ManifestTab) error {
343 if err := w.putString(tab.StableID); err != nil {
344 return err
345 }
346 w.putUint16(tab.Cols)
347 w.putUint16(tab.Rows)
348 w.putUint64(tab.NextPaneID)
349 if err := w.putString(string(tab.Focus)); err != nil {
350 return err
351 }
352 var root *layout.Node
353 if tab.Tree != nil {
354 root = tab.Tree.Root
355 }
356 if err := writeNode(w, root); err != nil {
357 return err
358 }
359 if len(tab.Panes) > math.MaxUint16 {
360 return fmt.Errorf("%w: too many panes", ErrInvalidData)
361 }
362 w.putUint16(uint16(len(tab.Panes)))
363 for _, pane := range tab.Panes {
364 if err := writeManifestPane(w, pane); err != nil {
365 return err
366 }
367 }
368 return nil
369}
370func writeManifestWeights(w *payloadWriter, nodes []*layout.Node) error {
371 hasWeights := false
372 for _, node := range nodes {
373 if node.Weight != 0 {
374 hasWeights = true
375 break
376 }
377 }
378 if !hasWeights {
379 return nil
380 }
381 if len(nodes) > math.MaxUint32 {
382 return fmt.Errorf("%w: too many weighted nodes", ErrInvalidData)
383 }
384 w.b = append(w.b, manifestWeightTag...)
385 w.putUint32(uint32(len(nodes)))
386 for _, node := range nodes {
387 w.putUint64(math.Float64bits(node.Weight))
388 }
389 return nil
390}
391
392func readManifestWeights(r *payloadReader, nodes []*layout.Node) error {
393 return readManifestWeightExtension(r, uint64(len(nodes)), func(i int, weight float64) {
394 nodes[i].Weight = weight
395 })
396}
397
398// readManifestWeightExtension validates the optional weight extension and
399// calls assign for each decoded weight when assignment is required.
400func readManifestWeightExtension(r *payloadReader, wantNodes uint64, assign func(int, float64)) error {
401 if len(r.b) == 0 {
402 return nil
403 }
404 if len(r.b) < len(manifestWeightTag) {
405 return ErrShortPayload
406 }
407 if string(r.b[:len(manifestWeightTag)]) != manifestWeightTag {
408 return fmt.Errorf("%w: weight extension tag", ErrInvalidData)
409 }
410 r.b = r.b[len(manifestWeightTag):]
411 count, err := r.getUint32()
412 if err != nil {
413 return err
414 }
415 if uint64(count) != wantNodes {
416 return fmt.Errorf("%w: weight node count", ErrInvalidData)
417 }
418 if uint64(count) > uint64(len(r.b))/manifestWeightSize {
419 return ErrShortPayload
420 }
421 if uint64(len(r.b)) != uint64(count)*manifestWeightSize {
422 return ErrTrailingBytes
423 }
424 for i := range count {
425 bits, err := r.getUint64()
426 if err != nil {
427 return err
428 }
429 weight := math.Float64frombits(bits)
430 if !validManifestWeight(weight) {
431 return fmt.Errorf("%w: node weight", ErrInvalidData)
432 }
433 if assign != nil {
434 assign(int(i), weight)
435 }
436 }
437 return r.done()
438}
439
440func manifestNodes(m Manifest) []*layout.Node {
441 var nodes []*layout.Node
442 var visit func(*layout.Node)
443 visit = func(node *layout.Node) {
444 if node == nil {
445 return
446 }
447 nodes = append(nodes, node)
448 for _, child := range node.Children {
449 visit(child)
450 }
451 }
452 for _, tab := range m.Tabs {
453 if tab.Tree != nil {
454 visit(tab.Tree.Root)
455 }
456 }
457 return nodes
458}
459
460func validManifestWeight(weight float64) bool {
461 return weight >= 0 && !math.IsNaN(weight) && !math.IsInf(weight, 0)
462}
463
464func writeManifestPane(w *payloadWriter, pane ManifestPane) error {
465 if err := w.putString(string(pane.ID)); err != nil {
466 return err
467 }
468 if err := w.putString(pane.StableID); err != nil {
469 return err
470 }
471 if err := w.putString(pane.Cwd); err != nil {
472 return err
473 }
474 if len(pane.Sealed) > math.MaxUint16 {
475 return fmt.Errorf("%w: too many sealed references", ErrInvalidData)
476 }
477 w.putUint16(uint16(len(pane.Sealed)))
478 for _, ref := range pane.Sealed {
479 writeObjectRef(w, ref)
480 }
481 writeObjectRef(w, pane.Tail)
482 writeObjectRef(w, pane.Transcript)
483 return writeProcess(w, pane.Process)
484}
485func writeObjectRef(w *payloadWriter, ref ObjectRef) {
486 w.putUint8(uint8(ref.Kind))
487 w.b = append(w.b, ref.Digest[:]...)
488 w.putUint32(ref.Size)
489}
490
491func readManifestTab(r *payloadReader) (ManifestTab, error) {
492 stableID, err := r.getString()
493 if err != nil {
494 return ManifestTab{}, err
495 }
496 cols, err := r.getUint16()
497 if err != nil {
498 return ManifestTab{}, err
499 }
500 rows, err := r.getUint16()
501 if err != nil {
502 return ManifestTab{}, err
503 }
504 next, err := r.getUint64()
505 if err != nil {
506 return ManifestTab{}, err
507 }
508 focus, err := r.getString()
509 if err != nil {
510 return ManifestTab{}, err
511 }
512 root, err := readNode(r)
513 if err != nil {
514 return ManifestTab{}, err
515 }
516 n, err := r.getUint16()
517 if err != nil {
518 return ManifestTab{}, err
519 }
520 tab := ManifestTab{StableID: stableID, Cols: cols, Rows: rows, NextPaneID: next, Focus: layout.PaneID(focus), Tree: &layout.Tree{Root: root, Focus: layout.PaneID(focus)}, Panes: make([]ManifestPane, n)}
521 for i := range tab.Panes {
522 if tab.Panes[i], err = readManifestPane(r); err != nil {
523 return ManifestTab{}, err
524 }
525 }
526 return tab, nil
527}
528func readManifestPane(r *payloadReader) (ManifestPane, error) {
529 id, err := r.getString()
530 if err != nil {
531 return ManifestPane{}, err
532 }
533 stableID, err := r.getString()
534 if err != nil {
535 return ManifestPane{}, err
536 }
537 cwd, err := r.getString()
538 if err != nil {
539 return ManifestPane{}, err
540 }
541 n, err := r.getUint16()
542 if err != nil {
543 return ManifestPane{}, err
544 }
545 pane := ManifestPane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, Sealed: make([]ObjectRef, n)}
546 for i := range pane.Sealed {
547 if pane.Sealed[i], err = readObjectRef(r); err != nil {
548 return ManifestPane{}, err
549 }
550 }
551 if pane.Tail, err = readObjectRef(r); err != nil {
552 return ManifestPane{}, err
553 }
554 if pane.Transcript, err = readObjectRef(r); err != nil {
555 return ManifestPane{}, err
556 }
557 pane.Process, err = readProcess(r)
558 return pane, err
559}
560func readObjectRef(r *payloadReader) (ObjectRef, error) {
561 kind, digestBytes, size, err := parseObjectRef(r)
562 if err != nil {
563 return ObjectRef{}, err
564 }
565 var digest SnapshotDigest
566 copy(digest[:], digestBytes)
567 return ObjectRef{Kind: kind, Digest: digest, Size: size}, nil
568}
569
570// parseObjectRef advances r over an encoded object reference without copying
571// its digest. Callers that retain the digest must copy it before reusing input.
572func parseObjectRef(r *payloadReader) (ObjectKind, []byte, uint32, error) {
573 kind, err := r.getUint8()
574 if err != nil {
575 return 0, nil, 0, err
576 }
577 if len(r.b) < objectRefDigestSize+objectRefSizeFieldSize {
578 return 0, nil, 0, ErrShortPayload
579 }
580 digest := r.b[:objectRefDigestSize]
581 r.b = r.b[objectRefDigestSize:]
582 size, err := r.getUint32()
583 if err != nil {
584 return 0, nil, 0, err
585 }
586 return ObjectKind(kind), digest, size, nil
587}
588
589func validateManifest(m Manifest) error {
590 if m.Generation == 0 {
591 return fmt.Errorf("%w: manifest generation", ErrInvalidData)
592 }
593 if m.IncarnationID == (domain.IncarnationID{}) {
594 return fmt.Errorf("%w: manifest incarnation", ErrInvalidData)
595 }
596 if m.ParentCheckpoint != nil {
597 if m.ParentCheckpoint.Generation == 0 || m.ParentCheckpoint.Generation >= m.Generation || isZeroDigestBytes(m.ParentCheckpoint.ManifestDigest[:]) {
598 return fmt.Errorf("%w: parent checkpoint", ErrInvalidData)
599 }
600 }
601 if m.Name == "" {
602 return fmt.Errorf("%w: manifest name", ErrInvalidData)
603 }
604 if err := validateActive(m.Active, len(m.Tabs)); err != nil {
605 return err
606 }
607 for _, node := range manifestNodes(m) {
608 if !validManifestWeight(node.Weight) {
609 return fmt.Errorf("%w: node weight", ErrInvalidData)
610 }
611 }
612 for _, tab := range m.Tabs {
613 if !safeDimensions(tab.Cols, tab.Rows) {
614 return fmt.Errorf("%w: dimensions", ErrInvalidData)
615 }
616 ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
617 for _, pane := range tab.Panes {
618 if pane.ID == "" {
619 return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
620 }
621 if _, exists := ids[pane.ID]; exists {
622 return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
623 }
624 if err := validatePaneRefs(pane); err != nil {
625 return err
626 }
627 ids[pane.ID] = struct{}{}
628 }
629 if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
630 return ErrUnknownPane
631 }
632 if err := validateTree(tab.Tree, ids); err != nil {
633 return err
634 }
635 }
636 return nil
637}
638func safeDimensions(cols, rows uint16) bool {
639 return cols != 0 && rows != 0 && uint64(cols)*uint64(rows) <= maxSnapshotCells
640}
641func validatePaneRefs(p ManifestPane) error {
642 for _, ref := range p.Sealed {
643 if err := validateObjectRef(ref, HistoryChunk); err != nil {
644 return err
645 }
646 }
647 if err := validateObjectRef(p.Tail, HistoryTail); err != nil {
648 return err
649 }
650 return validateObjectRef(p.Transcript, RecoveryTranscript)
651}
652func validateObjectRef(ref ObjectRef, want ObjectKind) error {
653 return validateObjectRefFields(ref.Kind, ref.Digest[:], ref.Size, want)
654}
655
656func validateObjectRefFields(kind ObjectKind, digest []byte, size uint32, want ObjectKind) error {
657 if kind != want || !validObjectEnvelopeSize(size) || isZeroDigestBytes(digest) {
658 return fmt.Errorf("%w: object reference", ErrInvalidData)
659 }
660 return nil
661}
662func validObjectEnvelopeSize(size uint32) bool {
663 return size >= minObjectEnvelopeSize && size <= maxObjectEnvelopeSize
664}
665
666func validObjectKind(kind ObjectKind) bool {
667 return kind == HistoryChunk || kind == HistoryTail || kind == RecoveryTranscript
668}
669func isZeroDigestBytes(d []byte) bool {
670 for _, b := range d {
671 if b != 0 {
672 return false
673 }
674 }
675 return true
676}