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