detached e17db81

refactor(snapshot): separate preflight validation

commit
e17db814591f82b3d1f639c111bdc24a57fffb2a
author
brice · · 2026-07-12 20:44 UTC
parent
da79df9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +449 -441
M internal/usecase/snapshot/codec.go +0 -441
@@ -8,7 +8,6 @@ import (
88 "math"
99
1010 "github.com/bnema/vev/internal/usecase/layout"
11- "github.com/bnema/vev/pkg/vt"
1211 )
1312
1413 var (
@@ -578,446 +577,6 @@ func validateActive(active uint16, tabCount int) error {
578577 // preflightSession first validates the full structural and aggregate resource
579578 // shape without materializing strings, maps, or reference lists. Only once
580579 // that pass has accepted global budgets does it allocate to check references.
581-func preflightSession(body []byte) error {
582- if err := preflightSessionStructure(body); err != nil {
583- return err
584- }
585- return preflightSessionReferences(body)
586-}
587-
588-func preflightSessionStructure(body []byte) error {
589- r := payloadReader{b: body}
590- if err := skipString(&r); err != nil {
591- return err
592- }
593- if _, err := r.getUint64(); err != nil {
594- return err
595- }
596- active, err := r.getUint16()
597- if err != nil {
598- return err
599- }
600- tabs, err := r.getUint16()
601- if err != nil {
602- return err
603- }
604- if err := validateActive(active, int(tabs)); err != nil {
605- return err
606- }
607- if uint64(tabs) > uint64(len(r.b))/2 {
608- return ErrShortPayload
609- }
610-
611- var totals vt.DecodeStats
612- blobs := uint64(0)
613- budget := preflightBudget{}
614- for range tabs {
615- if err := preflightTabStructure(&r, &totals, &blobs, &budget); err != nil {
616- return fmt.Errorf("preflight tab: %w", err)
617- }
618- }
619- if err := r.done(); err != nil {
620- return err
621- }
622- if totals.Rows > maxSnapshotRows || totals.Cells > maxSnapshotCells || totals.Bytes > maxSnapshotBytes {
623- return fmt.Errorf("%w: global VT budget", ErrInvalidData)
624- }
625- return nil
626-}
627-
628-func skipString(r *payloadReader) error {
629- n, err := r.getUint16()
630- if err != nil {
631- return err
632- }
633- if uint64(n) > uint64(len(r.b)) {
634- return ErrShortPayload
635- }
636- r.b = r.b[n:]
637- return nil
638-}
639-
640-type preflightBudget struct {
641- nodes uint64
642- alloc uint64
643-}
644-
645-func (b *preflightBudget) addAllocation(amount uint64) bool {
646- if amount > maxSnapshotDecodedAllocation-b.alloc {
647- return false
648- }
649- b.alloc += amount
650- return true
651-}
652-
653-func (b *preflightBudget) addProduct(count, size uint64) bool {
654- if count != 0 && size > math.MaxUint64/count {
655- return false
656- }
657- return b.addAllocation(count * size)
658-}
659-
660-func (b *preflightBudget) addNode() bool {
661- if b.nodes >= maxSnapshotTreeNodes || !b.addAllocation(128) {
662- return false
663- }
664- b.nodes++
665- return true
666-}
667-
668-func preflightTabStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
669- if !budget.addAllocation(256) {
670- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
671- }
672- if err := skipString(r); err != nil {
673- return err
674- }
675- if _, err := r.getUint16(); err != nil {
676- return err
677- }
678- if _, err := r.getUint16(); err != nil {
679- return err
680- }
681- if _, err := r.getUint64(); err != nil {
682- return err
683- }
684- if err := skipString(r); err != nil {
685- return err
686- }
687- if err := preflightNodeStructure(r, 0, true, budget); err != nil {
688- return err
689- }
690- panes, err := r.getUint16()
691- if err != nil {
692- return err
693- }
694- if uint64(panes) > uint64(len(r.b))/2 {
695- return ErrShortPayload
696- }
697- for range panes {
698- if err := preflightPaneStructure(r, totals, blobs, budget); err != nil {
699- return err
700- }
701- }
702- return nil
703-}
704-
705-func preflightNodeStructure(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
706- if depth > 64 {
707- return fmt.Errorf("%w: tree depth", ErrInvalidData)
708- }
709- if !budget.addNode() {
710- return fmt.Errorf("%w: tree node or decoded allocation budget", ErrInvalidData)
711- }
712- kind, err := r.getUint8()
713- if err != nil {
714- return err
715- }
716- switch kind {
717- case 0:
718- return skipString(r)
719- case 1:
720- dir, err := r.getUint8()
721- if err != nil {
722- return err
723- }
724- if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
725- return fmt.Errorf("%w: split dir", ErrInvalidData)
726- }
727- case 2:
728- if err := skipString(r); err != nil {
729- return err
730- }
731- case 3:
732- if !root {
733- return fmt.Errorf("%w: nil child node", ErrInvalidData)
734- }
735- return nil
736- default:
737- return fmt.Errorf("%w: node kind", ErrInvalidData)
738- }
739- children, err := r.getUint16()
740- if err != nil {
741- return err
742- }
743- if uint64(children) > uint64(len(r.b)) {
744- return ErrShortPayload
745- }
746- for range children {
747- if err := preflightNodeStructure(r, depth+1, false, budget); err != nil {
748- return err
749- }
750- }
751- return nil
752-}
753-
754-func preflightPaneStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
755- if !budget.addAllocation(256) {
756- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
757- }
758- if err := skipString(r); err != nil {
759- return err
760- }
761- if err := skipString(r); err != nil {
762- return err
763- }
764- if err := skipString(r); err != nil {
765- return err
766- }
767- n, err := r.getUint32()
768- if err != nil {
769- return err
770- }
771- if n > maxSnapshotBlobs || uint64(n) > uint64(len(r.b))/4 {
772- return fmt.Errorf("%w: sealed chunks", ErrInvalidData)
773- }
774- for range n {
775- stats, err := preflightBlob(r, totals, blobs, true, budget)
776- if err != nil {
777- return err
778- }
779- if stats.Chunks != 1 || stats.Rows == 0 {
780- return fmt.Errorf("%w: sealed blob role", ErrInvalidData)
781- }
782- }
783- tail, err := preflightBlob(r, totals, blobs, true, budget)
784- if err != nil {
785- return err
786- }
787- if tail.Chunks != 0 || tail.Rows != 0 || tail.Cells != 0 {
788- return fmt.Errorf("%w: tail blob role", ErrInvalidData)
789- }
790- visible, err := preflightBlob(r, totals, blobs, false, budget)
791- if err != nil {
792- return err
793- }
794- if visible.Rows == 0 || visible.Cells == 0 {
795- return fmt.Errorf("%w: visible geometry", ErrInvalidData)
796- }
797- return preflightProcess(r)
798-}
799-
800-func preflightBlob(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, history bool, budget *preflightBudget) (vt.DecodeStats, error) {
801- n, err := r.getUint32()
802- if err != nil {
803- return vt.DecodeStats{}, err
804- }
805- if n == 0 || uint64(n) > uint64(len(r.b)) || uint64(n) > maxSnapshotBytes {
806- return vt.DecodeStats{}, fmt.Errorf("%w: VT blob length", ErrInvalidData)
807- }
808- if *blobs >= maxSnapshotBlobs {
809- return vt.DecodeStats{}, fmt.Errorf("%w: too many VT blobs", ErrInvalidData)
810- }
811- blob := r.b[:n]
812- r.b = r.b[n:]
813- var stats vt.DecodeStats
814- if history {
815- stats, err = vt.PreflightHistoryBlob(blob)
816- } else {
817- stats, err = vt.PreflightVisibleBlob(blob)
818- }
819- if err != nil {
820- return vt.DecodeStats{}, fmt.Errorf("%w: VT blob", ErrInvalidData)
821- }
822- if !totals.Add(stats) {
823- return vt.DecodeStats{}, fmt.Errorf("%w: VT budget overflow", ErrInvalidData)
824- }
825- if !budget.addAllocation(uint64(n)) || !budget.addProduct(stats.Chunks, 64) || !budget.addProduct(stats.Rows, 32) || !budget.addProduct(stats.Cells, 64) {
826- return vt.DecodeStats{}, fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
827- }
828- *blobs++
829- return stats, nil
830-}
831-
832-func preflightProcess(r *payloadReader) error {
833- present, err := r.getUint8()
834- if err != nil {
835- return err
836- }
837- if present == 0 {
838- return nil
839- }
840- if present != 1 {
841- return fmt.Errorf("%w: process presence", ErrInvalidData)
842- }
843- n, err := r.getUint16()
844- if err != nil {
845- return err
846- }
847- if n == 0 {
848- return fmt.Errorf("%w: process argv empty", ErrInvalidData)
849- }
850- for range n {
851- if err := skipString(r); err != nil {
852- return err
853- }
854- }
855- if err := skipString(r); err != nil {
856- return err
857- }
858- return skipString(r)
859-}
860-
861-// preflightSessionReferences validates duplicate, empty, and dangling pane
862-// references only after the structural pass has admitted the complete payload.
863-func preflightSessionReferences(body []byte) error {
864- r := payloadReader{b: body}
865- if err := skipString(&r); err != nil {
866- return err
867- }
868- if _, err := r.getUint64(); err != nil {
869- return err
870- }
871- if _, err := r.getUint16(); err != nil {
872- return err
873- }
874- tabs, err := r.getUint16()
875- if err != nil {
876- return err
877- }
878- for range tabs {
879- if err := preflightTabReferences(&r); err != nil {
880- return fmt.Errorf("preflight tab references: %w", err)
881- }
882- }
883- return r.done()
884-}
885-
886-func preflightTabReferences(r *payloadReader) error {
887- if err := skipString(r); err != nil {
888- return err
889- }
890- if _, err := r.getUint16(); err != nil {
891- return err
892- }
893- if _, err := r.getUint16(); err != nil {
894- return err
895- }
896- if _, err := r.getUint64(); err != nil {
897- return err
898- }
899- focus, err := r.getString()
900- if err != nil {
901- return err
902- }
903- references := preflightTreeReferences{}
904- if err := preflightNodeReferences(r, &references); err != nil {
905- return err
906- }
907- panes, err := r.getUint16()
908- if err != nil {
909- return err
910- }
911- ids := make(map[layout.PaneID]struct{}, panes)
912- for range panes {
913- id, err := r.getString()
914- if err != nil {
915- return err
916- }
917- if id == "" {
918- return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
919- }
920- paneID := layout.PaneID(id)
921- if _, exists := ids[paneID]; exists {
922- return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
923- }
924- ids[paneID] = struct{}{}
925- if err := skipString(r); err != nil {
926- return err
927- }
928- if err := skipString(r); err != nil {
929- return err
930- }
931- n, err := r.getUint32()
932- if err != nil {
933- return err
934- }
935- for range n {
936- if err := skipBlob(r); err != nil {
937- return err
938- }
939- }
940- if err := skipBlob(r); err != nil {
941- return err
942- }
943- if err := skipBlob(r); err != nil {
944- return err
945- }
946- if err := preflightProcess(r); err != nil {
947- return err
948- }
949- }
950- if focus != "" {
951- references.ids = append(references.ids, layout.PaneID(focus))
952- }
953- for _, id := range references.ids {
954- if _, ok := ids[id]; !ok {
955- return ErrUnknownPane
956- }
957- }
958- return nil
959-}
960-
961-func skipBlob(r *payloadReader) error {
962- n, err := r.getUint32()
963- if err != nil {
964- return err
965- }
966- if uint64(n) > uint64(len(r.b)) {
967- return ErrShortPayload
968- }
969- r.b = r.b[n:]
970- return nil
971-}
972-
973-type preflightTreeReferences struct{ ids []layout.PaneID }
974-
975-func (r *preflightTreeReferences) add(id layout.PaneID) {
976- r.ids = append(r.ids, id)
977-}
978-
979-func preflightNodeReferences(r *payloadReader, references *preflightTreeReferences) error {
980- kind, err := r.getUint8()
981- if err != nil {
982- return err
983- }
984- switch kind {
985- case 0:
986- leaf, err := r.getString()
987- if err != nil {
988- return err
989- }
990- references.add(layout.PaneID(leaf))
991- return nil
992- case 1:
993- if _, err := r.getUint8(); err != nil {
994- return err
995- }
996- case 2:
997- expanded, err := r.getString()
998- if err != nil {
999- return err
1000- }
1001- if expanded != "" {
1002- references.add(layout.PaneID(expanded))
1003- }
1004- case 3:
1005- return nil
1006- default:
1007- return fmt.Errorf("%w: node kind", ErrInvalidData)
1008- }
1009- children, err := r.getUint16()
1010- if err != nil {
1011- return err
1012- }
1013- for range children {
1014- if err := preflightNodeReferences(r, references); err != nil {
1015- return err
1016- }
1017- }
1018- return nil
1019-}
1020-
1021580 func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {
1022581 if t == nil || t.Root == nil {
1023582 return nil
@@ -8,7 +8,6 @@ import (
8 "math"8 "math"
9 9
10 "github.com/bnema/vev/internal/usecase/layout"10 "github.com/bnema/vev/internal/usecase/layout"
11- "github.com/bnema/vev/pkg/vt"
12 )11 )
13 12
14 var (13 var (
@@ -578,446 +577,6 @@ func validateActive(active uint16, tabCount int) error {
578 // preflightSession first validates the full structural and aggregate resource577 // preflightSession first validates the full structural and aggregate resource
579 // shape without materializing strings, maps, or reference lists. Only once578 // shape without materializing strings, maps, or reference lists. Only once
580 // that pass has accepted global budgets does it allocate to check references.579 // that pass has accepted global budgets does it allocate to check references.
581-func preflightSession(body []byte) error {
582- if err := preflightSessionStructure(body); err != nil {
583- return err
584- }
585- return preflightSessionReferences(body)
586-}
587-
588-func preflightSessionStructure(body []byte) error {
589- r := payloadReader{b: body}
590- if err := skipString(&r); err != nil {
591- return err
592- }
593- if _, err := r.getUint64(); err != nil {
594- return err
595- }
596- active, err := r.getUint16()
597- if err != nil {
598- return err
599- }
600- tabs, err := r.getUint16()
601- if err != nil {
602- return err
603- }
604- if err := validateActive(active, int(tabs)); err != nil {
605- return err
606- }
607- if uint64(tabs) > uint64(len(r.b))/2 {
608- return ErrShortPayload
609- }
610-
611- var totals vt.DecodeStats
612- blobs := uint64(0)
613- budget := preflightBudget{}
614- for range tabs {
615- if err := preflightTabStructure(&r, &totals, &blobs, &budget); err != nil {
616- return fmt.Errorf("preflight tab: %w", err)
617- }
618- }
619- if err := r.done(); err != nil {
620- return err
621- }
622- if totals.Rows > maxSnapshotRows || totals.Cells > maxSnapshotCells || totals.Bytes > maxSnapshotBytes {
623- return fmt.Errorf("%w: global VT budget", ErrInvalidData)
624- }
625- return nil
626-}
627-
628-func skipString(r *payloadReader) error {
629- n, err := r.getUint16()
630- if err != nil {
631- return err
632- }
633- if uint64(n) > uint64(len(r.b)) {
634- return ErrShortPayload
635- }
636- r.b = r.b[n:]
637- return nil
638-}
639-
640-type preflightBudget struct {
641- nodes uint64
642- alloc uint64
643-}
644-
645-func (b *preflightBudget) addAllocation(amount uint64) bool {
646- if amount > maxSnapshotDecodedAllocation-b.alloc {
647- return false
648- }
649- b.alloc += amount
650- return true
651-}
652-
653-func (b *preflightBudget) addProduct(count, size uint64) bool {
654- if count != 0 && size > math.MaxUint64/count {
655- return false
656- }
657- return b.addAllocation(count * size)
658-}
659-
660-func (b *preflightBudget) addNode() bool {
661- if b.nodes >= maxSnapshotTreeNodes || !b.addAllocation(128) {
662- return false
663- }
664- b.nodes++
665- return true
666-}
667-
668-func preflightTabStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
669- if !budget.addAllocation(256) {
670- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
671- }
672- if err := skipString(r); err != nil {
673- return err
674- }
675- if _, err := r.getUint16(); err != nil {
676- return err
677- }
678- if _, err := r.getUint16(); err != nil {
679- return err
680- }
681- if _, err := r.getUint64(); err != nil {
682- return err
683- }
684- if err := skipString(r); err != nil {
685- return err
686- }
687- if err := preflightNodeStructure(r, 0, true, budget); err != nil {
688- return err
689- }
690- panes, err := r.getUint16()
691- if err != nil {
692- return err
693- }
694- if uint64(panes) > uint64(len(r.b))/2 {
695- return ErrShortPayload
696- }
697- for range panes {
698- if err := preflightPaneStructure(r, totals, blobs, budget); err != nil {
699- return err
700- }
701- }
702- return nil
703-}
704-
705-func preflightNodeStructure(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
706- if depth > 64 {
707- return fmt.Errorf("%w: tree depth", ErrInvalidData)
708- }
709- if !budget.addNode() {
710- return fmt.Errorf("%w: tree node or decoded allocation budget", ErrInvalidData)
711- }
712- kind, err := r.getUint8()
713- if err != nil {
714- return err
715- }
716- switch kind {
717- case 0:
718- return skipString(r)
719- case 1:
720- dir, err := r.getUint8()
721- if err != nil {
722- return err
723- }
724- if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
725- return fmt.Errorf("%w: split dir", ErrInvalidData)
726- }
727- case 2:
728- if err := skipString(r); err != nil {
729- return err
730- }
731- case 3:
732- if !root {
733- return fmt.Errorf("%w: nil child node", ErrInvalidData)
734- }
735- return nil
736- default:
737- return fmt.Errorf("%w: node kind", ErrInvalidData)
738- }
739- children, err := r.getUint16()
740- if err != nil {
741- return err
742- }
743- if uint64(children) > uint64(len(r.b)) {
744- return ErrShortPayload
745- }
746- for range children {
747- if err := preflightNodeStructure(r, depth+1, false, budget); err != nil {
748- return err
749- }
750- }
751- return nil
752-}
753-
754-func preflightPaneStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
755- if !budget.addAllocation(256) {
756- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
757- }
758- if err := skipString(r); err != nil {
759- return err
760- }
761- if err := skipString(r); err != nil {
762- return err
763- }
764- if err := skipString(r); err != nil {
765- return err
766- }
767- n, err := r.getUint32()
768- if err != nil {
769- return err
770- }
771- if n > maxSnapshotBlobs || uint64(n) > uint64(len(r.b))/4 {
772- return fmt.Errorf("%w: sealed chunks", ErrInvalidData)
773- }
774- for range n {
775- stats, err := preflightBlob(r, totals, blobs, true, budget)
776- if err != nil {
777- return err
778- }
779- if stats.Chunks != 1 || stats.Rows == 0 {
780- return fmt.Errorf("%w: sealed blob role", ErrInvalidData)
781- }
782- }
783- tail, err := preflightBlob(r, totals, blobs, true, budget)
784- if err != nil {
785- return err
786- }
787- if tail.Chunks != 0 || tail.Rows != 0 || tail.Cells != 0 {
788- return fmt.Errorf("%w: tail blob role", ErrInvalidData)
789- }
790- visible, err := preflightBlob(r, totals, blobs, false, budget)
791- if err != nil {
792- return err
793- }
794- if visible.Rows == 0 || visible.Cells == 0 {
795- return fmt.Errorf("%w: visible geometry", ErrInvalidData)
796- }
797- return preflightProcess(r)
798-}
799-
800-func preflightBlob(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, history bool, budget *preflightBudget) (vt.DecodeStats, error) {
801- n, err := r.getUint32()
802- if err != nil {
803- return vt.DecodeStats{}, err
804- }
805- if n == 0 || uint64(n) > uint64(len(r.b)) || uint64(n) > maxSnapshotBytes {
806- return vt.DecodeStats{}, fmt.Errorf("%w: VT blob length", ErrInvalidData)
807- }
808- if *blobs >= maxSnapshotBlobs {
809- return vt.DecodeStats{}, fmt.Errorf("%w: too many VT blobs", ErrInvalidData)
810- }
811- blob := r.b[:n]
812- r.b = r.b[n:]
813- var stats vt.DecodeStats
814- if history {
815- stats, err = vt.PreflightHistoryBlob(blob)
816- } else {
817- stats, err = vt.PreflightVisibleBlob(blob)
818- }
819- if err != nil {
820- return vt.DecodeStats{}, fmt.Errorf("%w: VT blob", ErrInvalidData)
821- }
822- if !totals.Add(stats) {
823- return vt.DecodeStats{}, fmt.Errorf("%w: VT budget overflow", ErrInvalidData)
824- }
825- if !budget.addAllocation(uint64(n)) || !budget.addProduct(stats.Chunks, 64) || !budget.addProduct(stats.Rows, 32) || !budget.addProduct(stats.Cells, 64) {
826- return vt.DecodeStats{}, fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
827- }
828- *blobs++
829- return stats, nil
830-}
831-
832-func preflightProcess(r *payloadReader) error {
833- present, err := r.getUint8()
834- if err != nil {
835- return err
836- }
837- if present == 0 {
838- return nil
839- }
840- if present != 1 {
841- return fmt.Errorf("%w: process presence", ErrInvalidData)
842- }
843- n, err := r.getUint16()
844- if err != nil {
845- return err
846- }
847- if n == 0 {
848- return fmt.Errorf("%w: process argv empty", ErrInvalidData)
849- }
850- for range n {
851- if err := skipString(r); err != nil {
852- return err
853- }
854- }
855- if err := skipString(r); err != nil {
856- return err
857- }
858- return skipString(r)
859-}
860-
861-// preflightSessionReferences validates duplicate, empty, and dangling pane
862-// references only after the structural pass has admitted the complete payload.
863-func preflightSessionReferences(body []byte) error {
864- r := payloadReader{b: body}
865- if err := skipString(&r); err != nil {
866- return err
867- }
868- if _, err := r.getUint64(); err != nil {
869- return err
870- }
871- if _, err := r.getUint16(); err != nil {
872- return err
873- }
874- tabs, err := r.getUint16()
875- if err != nil {
876- return err
877- }
878- for range tabs {
879- if err := preflightTabReferences(&r); err != nil {
880- return fmt.Errorf("preflight tab references: %w", err)
881- }
882- }
883- return r.done()
884-}
885-
886-func preflightTabReferences(r *payloadReader) error {
887- if err := skipString(r); err != nil {
888- return err
889- }
890- if _, err := r.getUint16(); err != nil {
891- return err
892- }
893- if _, err := r.getUint16(); err != nil {
894- return err
895- }
896- if _, err := r.getUint64(); err != nil {
897- return err
898- }
899- focus, err := r.getString()
900- if err != nil {
901- return err
902- }
903- references := preflightTreeReferences{}
904- if err := preflightNodeReferences(r, &references); err != nil {
905- return err
906- }
907- panes, err := r.getUint16()
908- if err != nil {
909- return err
910- }
911- ids := make(map[layout.PaneID]struct{}, panes)
912- for range panes {
913- id, err := r.getString()
914- if err != nil {
915- return err
916- }
917- if id == "" {
918- return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
919- }
920- paneID := layout.PaneID(id)
921- if _, exists := ids[paneID]; exists {
922- return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
923- }
924- ids[paneID] = struct{}{}
925- if err := skipString(r); err != nil {
926- return err
927- }
928- if err := skipString(r); err != nil {
929- return err
930- }
931- n, err := r.getUint32()
932- if err != nil {
933- return err
934- }
935- for range n {
936- if err := skipBlob(r); err != nil {
937- return err
938- }
939- }
940- if err := skipBlob(r); err != nil {
941- return err
942- }
943- if err := skipBlob(r); err != nil {
944- return err
945- }
946- if err := preflightProcess(r); err != nil {
947- return err
948- }
949- }
950- if focus != "" {
951- references.ids = append(references.ids, layout.PaneID(focus))
952- }
953- for _, id := range references.ids {
954- if _, ok := ids[id]; !ok {
955- return ErrUnknownPane
956- }
957- }
958- return nil
959-}
960-
961-func skipBlob(r *payloadReader) error {
962- n, err := r.getUint32()
963- if err != nil {
964- return err
965- }
966- if uint64(n) > uint64(len(r.b)) {
967- return ErrShortPayload
968- }
969- r.b = r.b[n:]
970- return nil
971-}
972-
973-type preflightTreeReferences struct{ ids []layout.PaneID }
974-
975-func (r *preflightTreeReferences) add(id layout.PaneID) {
976- r.ids = append(r.ids, id)
977-}
978-
979-func preflightNodeReferences(r *payloadReader, references *preflightTreeReferences) error {
980- kind, err := r.getUint8()
981- if err != nil {
982- return err
983- }
984- switch kind {
985- case 0:
986- leaf, err := r.getString()
987- if err != nil {
988- return err
989- }
990- references.add(layout.PaneID(leaf))
991- return nil
992- case 1:
993- if _, err := r.getUint8(); err != nil {
994- return err
995- }
996- case 2:
997- expanded, err := r.getString()
998- if err != nil {
999- return err
1000- }
1001- if expanded != "" {
1002- references.add(layout.PaneID(expanded))
1003- }
1004- case 3:
1005- return nil
1006- default:
1007- return fmt.Errorf("%w: node kind", ErrInvalidData)
1008- }
1009- children, err := r.getUint16()
1010- if err != nil {
1011- return err
1012- }
1013- for range children {
1014- if err := preflightNodeReferences(r, references); err != nil {
1015- return err
1016- }
1017- }
1018- return nil
1019-}
1020-
1021 func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {580 func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {
1022 if t == nil || t.Root == nil {581 if t == nil || t.Root == nil {
1023 return nil582 return nil
A internal/usecase/snapshot/preflight.go +449 -0
@@ -0,0 +1,449 @@
1+package snapshot
2+
3+import (
4+ "fmt"
5+ "math"
6+
7+ "github.com/bnema/vev/internal/usecase/layout"
8+ "github.com/bnema/vev/pkg/vt"
9+)
10+
11+func preflightSession(body []byte) error {
12+ if err := preflightSessionStructure(body); err != nil {
13+ return err
14+ }
15+ return preflightSessionReferences(body)
16+}
17+
18+func preflightSessionStructure(body []byte) error {
19+ r := payloadReader{b: body}
20+ if err := skipString(&r); err != nil {
21+ return err
22+ }
23+ if _, err := r.getUint64(); err != nil {
24+ return err
25+ }
26+ active, err := r.getUint16()
27+ if err != nil {
28+ return err
29+ }
30+ tabs, err := r.getUint16()
31+ if err != nil {
32+ return err
33+ }
34+ if err := validateActive(active, int(tabs)); err != nil {
35+ return err
36+ }
37+ if uint64(tabs) > uint64(len(r.b))/2 {
38+ return ErrShortPayload
39+ }
40+
41+ var totals vt.DecodeStats
42+ blobs := uint64(0)
43+ budget := preflightBudget{}
44+ for range tabs {
45+ if err := preflightTabStructure(&r, &totals, &blobs, &budget); err != nil {
46+ return fmt.Errorf("preflight tab: %w", err)
47+ }
48+ }
49+ if err := r.done(); err != nil {
50+ return err
51+ }
52+ if totals.Rows > maxSnapshotRows || totals.Cells > maxSnapshotCells || totals.Bytes > maxSnapshotBytes {
53+ return fmt.Errorf("%w: global VT budget", ErrInvalidData)
54+ }
55+ return nil
56+}
57+
58+func skipString(r *payloadReader) error {
59+ n, err := r.getUint16()
60+ if err != nil {
61+ return err
62+ }
63+ if uint64(n) > uint64(len(r.b)) {
64+ return ErrShortPayload
65+ }
66+ r.b = r.b[n:]
67+ return nil
68+}
69+
70+type preflightBudget struct {
71+ nodes uint64
72+ alloc uint64
73+}
74+
75+func (b *preflightBudget) addAllocation(amount uint64) bool {
76+ if amount > maxSnapshotDecodedAllocation-b.alloc {
77+ return false
78+ }
79+ b.alloc += amount
80+ return true
81+}
82+
83+func (b *preflightBudget) addProduct(count, size uint64) bool {
84+ if count != 0 && size > math.MaxUint64/count {
85+ return false
86+ }
87+ return b.addAllocation(count * size)
88+}
89+
90+func (b *preflightBudget) addNode() bool {
91+ if b.nodes >= maxSnapshotTreeNodes || !b.addAllocation(128) {
92+ return false
93+ }
94+ b.nodes++
95+ return true
96+}
97+
98+func preflightTabStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
99+ if !budget.addAllocation(256) {
100+ return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
101+ }
102+ if err := skipString(r); err != nil {
103+ return err
104+ }
105+ if _, err := r.getUint16(); err != nil {
106+ return err
107+ }
108+ if _, err := r.getUint16(); err != nil {
109+ return err
110+ }
111+ if _, err := r.getUint64(); err != nil {
112+ return err
113+ }
114+ if err := skipString(r); err != nil {
115+ return err
116+ }
117+ if err := preflightNodeStructure(r, 0, true, budget); err != nil {
118+ return err
119+ }
120+ panes, err := r.getUint16()
121+ if err != nil {
122+ return err
123+ }
124+ if uint64(panes) > uint64(len(r.b))/2 {
125+ return ErrShortPayload
126+ }
127+ for range panes {
128+ if err := preflightPaneStructure(r, totals, blobs, budget); err != nil {
129+ return err
130+ }
131+ }
132+ return nil
133+}
134+
135+func preflightNodeStructure(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
136+ if depth > 64 {
137+ return fmt.Errorf("%w: tree depth", ErrInvalidData)
138+ }
139+ if !budget.addNode() {
140+ return fmt.Errorf("%w: tree node or decoded allocation budget", ErrInvalidData)
141+ }
142+ kind, err := r.getUint8()
143+ if err != nil {
144+ return err
145+ }
146+ switch kind {
147+ case 0:
148+ return skipString(r)
149+ case 1:
150+ dir, err := r.getUint8()
151+ if err != nil {
152+ return err
153+ }
154+ if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
155+ return fmt.Errorf("%w: split dir", ErrInvalidData)
156+ }
157+ case 2:
158+ if err := skipString(r); err != nil {
159+ return err
160+ }
161+ case 3:
162+ if !root {
163+ return fmt.Errorf("%w: nil child node", ErrInvalidData)
164+ }
165+ return nil
166+ default:
167+ return fmt.Errorf("%w: node kind", ErrInvalidData)
168+ }
169+ children, err := r.getUint16()
170+ if err != nil {
171+ return err
172+ }
173+ if uint64(children) > uint64(len(r.b)) {
174+ return ErrShortPayload
175+ }
176+ for range children {
177+ if err := preflightNodeStructure(r, depth+1, false, budget); err != nil {
178+ return err
179+ }
180+ }
181+ return nil
182+}
183+
184+func preflightPaneStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
185+ if !budget.addAllocation(256) {
186+ return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
187+ }
188+ if err := skipString(r); err != nil {
189+ return err
190+ }
191+ if err := skipString(r); err != nil {
192+ return err
193+ }
194+ if err := skipString(r); err != nil {
195+ return err
196+ }
197+ n, err := r.getUint32()
198+ if err != nil {
199+ return err
200+ }
201+ if n > maxSnapshotBlobs || uint64(n) > uint64(len(r.b))/4 {
202+ return fmt.Errorf("%w: sealed chunks", ErrInvalidData)
203+ }
204+ for range n {
205+ stats, err := preflightBlob(r, totals, blobs, true, budget)
206+ if err != nil {
207+ return err
208+ }
209+ if stats.Chunks != 1 || stats.Rows == 0 {
210+ return fmt.Errorf("%w: sealed blob role", ErrInvalidData)
211+ }
212+ }
213+ tail, err := preflightBlob(r, totals, blobs, true, budget)
214+ if err != nil {
215+ return err
216+ }
217+ if tail.Chunks != 0 || tail.Rows != 0 || tail.Cells != 0 {
218+ return fmt.Errorf("%w: tail blob role", ErrInvalidData)
219+ }
220+ visible, err := preflightBlob(r, totals, blobs, false, budget)
221+ if err != nil {
222+ return err
223+ }
224+ if visible.Rows == 0 || visible.Cells == 0 {
225+ return fmt.Errorf("%w: visible geometry", ErrInvalidData)
226+ }
227+ return preflightProcess(r)
228+}
229+
230+func preflightBlob(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, history bool, budget *preflightBudget) (vt.DecodeStats, error) {
231+ n, err := r.getUint32()
232+ if err != nil {
233+ return vt.DecodeStats{}, err
234+ }
235+ if n == 0 || uint64(n) > uint64(len(r.b)) || uint64(n) > maxSnapshotBytes {
236+ return vt.DecodeStats{}, fmt.Errorf("%w: VT blob length", ErrInvalidData)
237+ }
238+ if *blobs >= maxSnapshotBlobs {
239+ return vt.DecodeStats{}, fmt.Errorf("%w: too many VT blobs", ErrInvalidData)
240+ }
241+ blob := r.b[:n]
242+ r.b = r.b[n:]
243+ var stats vt.DecodeStats
244+ if history {
245+ stats, err = vt.PreflightHistoryBlob(blob)
246+ } else {
247+ stats, err = vt.PreflightVisibleBlob(blob)
248+ }
249+ if err != nil {
250+ return vt.DecodeStats{}, fmt.Errorf("%w: VT blob", ErrInvalidData)
251+ }
252+ if !totals.Add(stats) {
253+ return vt.DecodeStats{}, fmt.Errorf("%w: VT budget overflow", ErrInvalidData)
254+ }
255+ if !budget.addAllocation(uint64(n)) || !budget.addProduct(stats.Chunks, 64) || !budget.addProduct(stats.Rows, 32) || !budget.addProduct(stats.Cells, 64) {
256+ return vt.DecodeStats{}, fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
257+ }
258+ *blobs++
259+ return stats, nil
260+}
261+
262+func preflightProcess(r *payloadReader) error {
263+ present, err := r.getUint8()
264+ if err != nil {
265+ return err
266+ }
267+ if present == 0 {
268+ return nil
269+ }
270+ if present != 1 {
271+ return fmt.Errorf("%w: process presence", ErrInvalidData)
272+ }
273+ n, err := r.getUint16()
274+ if err != nil {
275+ return err
276+ }
277+ if n == 0 {
278+ return fmt.Errorf("%w: process argv empty", ErrInvalidData)
279+ }
280+ for range n {
281+ if err := skipString(r); err != nil {
282+ return err
283+ }
284+ }
285+ if err := skipString(r); err != nil {
286+ return err
287+ }
288+ return skipString(r)
289+}
290+
291+// preflightSessionReferences validates duplicate, empty, and dangling pane
292+// references only after the structural pass has admitted the complete payload.
293+func preflightSessionReferences(body []byte) error {
294+ r := payloadReader{b: body}
295+ if err := skipString(&r); err != nil {
296+ return err
297+ }
298+ if _, err := r.getUint64(); err != nil {
299+ return err
300+ }
301+ if _, err := r.getUint16(); err != nil {
302+ return err
303+ }
304+ tabs, err := r.getUint16()
305+ if err != nil {
306+ return err
307+ }
308+ for range tabs {
309+ if err := preflightTabReferences(&r); err != nil {
310+ return fmt.Errorf("preflight tab references: %w", err)
311+ }
312+ }
313+ return r.done()
314+}
315+
316+func preflightTabReferences(r *payloadReader) error {
317+ if err := skipString(r); err != nil {
318+ return err
319+ }
320+ if _, err := r.getUint16(); err != nil {
321+ return err
322+ }
323+ if _, err := r.getUint16(); err != nil {
324+ return err
325+ }
326+ if _, err := r.getUint64(); err != nil {
327+ return err
328+ }
329+ focus, err := r.getString()
330+ if err != nil {
331+ return err
332+ }
333+ references := preflightTreeReferences{}
334+ if err := preflightNodeReferences(r, &references); err != nil {
335+ return err
336+ }
337+ panes, err := r.getUint16()
338+ if err != nil {
339+ return err
340+ }
341+ ids := make(map[layout.PaneID]struct{}, panes)
342+ for range panes {
343+ id, err := r.getString()
344+ if err != nil {
345+ return err
346+ }
347+ if id == "" {
348+ return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
349+ }
350+ paneID := layout.PaneID(id)
351+ if _, exists := ids[paneID]; exists {
352+ return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
353+ }
354+ ids[paneID] = struct{}{}
355+ if err := skipString(r); err != nil {
356+ return err
357+ }
358+ if err := skipString(r); err != nil {
359+ return err
360+ }
361+ n, err := r.getUint32()
362+ if err != nil {
363+ return err
364+ }
365+ for range n {
366+ if err := skipBlob(r); err != nil {
367+ return err
368+ }
369+ }
370+ if err := skipBlob(r); err != nil {
371+ return err
372+ }
373+ if err := skipBlob(r); err != nil {
374+ return err
375+ }
376+ if err := preflightProcess(r); err != nil {
377+ return err
378+ }
379+ }
380+ if focus != "" {
381+ references.ids = append(references.ids, layout.PaneID(focus))
382+ }
383+ for _, id := range references.ids {
384+ if _, ok := ids[id]; !ok {
385+ return ErrUnknownPane
386+ }
387+ }
388+ return nil
389+}
390+
391+func skipBlob(r *payloadReader) error {
392+ n, err := r.getUint32()
393+ if err != nil {
394+ return err
395+ }
396+ if uint64(n) > uint64(len(r.b)) {
397+ return ErrShortPayload
398+ }
399+ r.b = r.b[n:]
400+ return nil
401+}
402+
403+type preflightTreeReferences struct{ ids []layout.PaneID }
404+
405+func (r *preflightTreeReferences) add(id layout.PaneID) {
406+ r.ids = append(r.ids, id)
407+}
408+
409+func preflightNodeReferences(r *payloadReader, references *preflightTreeReferences) error {
410+ kind, err := r.getUint8()
411+ if err != nil {
412+ return err
413+ }
414+ switch kind {
415+ case 0:
416+ leaf, err := r.getString()
417+ if err != nil {
418+ return err
419+ }
420+ references.add(layout.PaneID(leaf))
421+ return nil
422+ case 1:
423+ if _, err := r.getUint8(); err != nil {
424+ return err
425+ }
426+ case 2:
427+ expanded, err := r.getString()
428+ if err != nil {
429+ return err
430+ }
431+ if expanded != "" {
432+ references.add(layout.PaneID(expanded))
433+ }
434+ case 3:
435+ return nil
436+ default:
437+ return fmt.Errorf("%w: node kind", ErrInvalidData)
438+ }
439+ children, err := r.getUint16()
440+ if err != nil {
441+ return err
442+ }
443+ for range children {
444+ if err := preflightNodeReferences(r, references); err != nil {
445+ return err
446+ }
447+ }
448+ return nil
449+}
@@ -0,0 +1,449 @@
1+package snapshot
2+
3+import (
4+ "fmt"
5+ "math"
6+
7+ "github.com/bnema/vev/internal/usecase/layout"
8+ "github.com/bnema/vev/pkg/vt"
9+)
10+
11+func preflightSession(body []byte) error {
12+ if err := preflightSessionStructure(body); err != nil {
13+ return err
14+ }
15+ return preflightSessionReferences(body)
16+}
17+
18+func preflightSessionStructure(body []byte) error {
19+ r := payloadReader{b: body}
20+ if err := skipString(&r); err != nil {
21+ return err
22+ }
23+ if _, err := r.getUint64(); err != nil {
24+ return err
25+ }
26+ active, err := r.getUint16()
27+ if err != nil {
28+ return err
29+ }
30+ tabs, err := r.getUint16()
31+ if err != nil {
32+ return err
33+ }
34+ if err := validateActive(active, int(tabs)); err != nil {
35+ return err
36+ }
37+ if uint64(tabs) > uint64(len(r.b))/2 {
38+ return ErrShortPayload
39+ }
40+
41+ var totals vt.DecodeStats
42+ blobs := uint64(0)
43+ budget := preflightBudget{}
44+ for range tabs {
45+ if err := preflightTabStructure(&r, &totals, &blobs, &budget); err != nil {
46+ return fmt.Errorf("preflight tab: %w", err)
47+ }
48+ }
49+ if err := r.done(); err != nil {
50+ return err
51+ }
52+ if totals.Rows > maxSnapshotRows || totals.Cells > maxSnapshotCells || totals.Bytes > maxSnapshotBytes {
53+ return fmt.Errorf("%w: global VT budget", ErrInvalidData)
54+ }
55+ return nil
56+}
57+
58+func skipString(r *payloadReader) error {
59+ n, err := r.getUint16()
60+ if err != nil {
61+ return err
62+ }
63+ if uint64(n) > uint64(len(r.b)) {
64+ return ErrShortPayload
65+ }
66+ r.b = r.b[n:]
67+ return nil
68+}
69+
70+type preflightBudget struct {
71+ nodes uint64
72+ alloc uint64
73+}
74+
75+func (b *preflightBudget) addAllocation(amount uint64) bool {
76+ if amount > maxSnapshotDecodedAllocation-b.alloc {
77+ return false
78+ }
79+ b.alloc += amount
80+ return true
81+}
82+
83+func (b *preflightBudget) addProduct(count, size uint64) bool {
84+ if count != 0 && size > math.MaxUint64/count {
85+ return false
86+ }
87+ return b.addAllocation(count * size)
88+}
89+
90+func (b *preflightBudget) addNode() bool {
91+ if b.nodes >= maxSnapshotTreeNodes || !b.addAllocation(128) {
92+ return false
93+ }
94+ b.nodes++
95+ return true
96+}
97+
98+func preflightTabStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
99+ if !budget.addAllocation(256) {
100+ return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
101+ }
102+ if err := skipString(r); err != nil {
103+ return err
104+ }
105+ if _, err := r.getUint16(); err != nil {
106+ return err
107+ }
108+ if _, err := r.getUint16(); err != nil {
109+ return err
110+ }
111+ if _, err := r.getUint64(); err != nil {
112+ return err
113+ }
114+ if err := skipString(r); err != nil {
115+ return err
116+ }
117+ if err := preflightNodeStructure(r, 0, true, budget); err != nil {
118+ return err
119+ }
120+ panes, err := r.getUint16()
121+ if err != nil {
122+ return err
123+ }
124+ if uint64(panes) > uint64(len(r.b))/2 {
125+ return ErrShortPayload
126+ }
127+ for range panes {
128+ if err := preflightPaneStructure(r, totals, blobs, budget); err != nil {
129+ return err
130+ }
131+ }
132+ return nil
133+}
134+
135+func preflightNodeStructure(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
136+ if depth > 64 {
137+ return fmt.Errorf("%w: tree depth", ErrInvalidData)
138+ }
139+ if !budget.addNode() {
140+ return fmt.Errorf("%w: tree node or decoded allocation budget", ErrInvalidData)
141+ }
142+ kind, err := r.getUint8()
143+ if err != nil {
144+ return err
145+ }
146+ switch kind {
147+ case 0:
148+ return skipString(r)
149+ case 1:
150+ dir, err := r.getUint8()
151+ if err != nil {
152+ return err
153+ }
154+ if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
155+ return fmt.Errorf("%w: split dir", ErrInvalidData)
156+ }
157+ case 2:
158+ if err := skipString(r); err != nil {
159+ return err
160+ }
161+ case 3:
162+ if !root {
163+ return fmt.Errorf("%w: nil child node", ErrInvalidData)
164+ }
165+ return nil
166+ default:
167+ return fmt.Errorf("%w: node kind", ErrInvalidData)
168+ }
169+ children, err := r.getUint16()
170+ if err != nil {
171+ return err
172+ }
173+ if uint64(children) > uint64(len(r.b)) {
174+ return ErrShortPayload
175+ }
176+ for range children {
177+ if err := preflightNodeStructure(r, depth+1, false, budget); err != nil {
178+ return err
179+ }
180+ }
181+ return nil
182+}
183+
184+func preflightPaneStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
185+ if !budget.addAllocation(256) {
186+ return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
187+ }
188+ if err := skipString(r); err != nil {
189+ return err
190+ }
191+ if err := skipString(r); err != nil {
192+ return err
193+ }
194+ if err := skipString(r); err != nil {
195+ return err
196+ }
197+ n, err := r.getUint32()
198+ if err != nil {
199+ return err
200+ }
201+ if n > maxSnapshotBlobs || uint64(n) > uint64(len(r.b))/4 {
202+ return fmt.Errorf("%w: sealed chunks", ErrInvalidData)
203+ }
204+ for range n {
205+ stats, err := preflightBlob(r, totals, blobs, true, budget)
206+ if err != nil {
207+ return err
208+ }
209+ if stats.Chunks != 1 || stats.Rows == 0 {
210+ return fmt.Errorf("%w: sealed blob role", ErrInvalidData)
211+ }
212+ }
213+ tail, err := preflightBlob(r, totals, blobs, true, budget)
214+ if err != nil {
215+ return err
216+ }
217+ if tail.Chunks != 0 || tail.Rows != 0 || tail.Cells != 0 {
218+ return fmt.Errorf("%w: tail blob role", ErrInvalidData)
219+ }
220+ visible, err := preflightBlob(r, totals, blobs, false, budget)
221+ if err != nil {
222+ return err
223+ }
224+ if visible.Rows == 0 || visible.Cells == 0 {
225+ return fmt.Errorf("%w: visible geometry", ErrInvalidData)
226+ }
227+ return preflightProcess(r)
228+}
229+
230+func preflightBlob(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, history bool, budget *preflightBudget) (vt.DecodeStats, error) {
231+ n, err := r.getUint32()
232+ if err != nil {
233+ return vt.DecodeStats{}, err
234+ }
235+ if n == 0 || uint64(n) > uint64(len(r.b)) || uint64(n) > maxSnapshotBytes {
236+ return vt.DecodeStats{}, fmt.Errorf("%w: VT blob length", ErrInvalidData)
237+ }
238+ if *blobs >= maxSnapshotBlobs {
239+ return vt.DecodeStats{}, fmt.Errorf("%w: too many VT blobs", ErrInvalidData)
240+ }
241+ blob := r.b[:n]
242+ r.b = r.b[n:]
243+ var stats vt.DecodeStats
244+ if history {
245+ stats, err = vt.PreflightHistoryBlob(blob)
246+ } else {
247+ stats, err = vt.PreflightVisibleBlob(blob)
248+ }
249+ if err != nil {
250+ return vt.DecodeStats{}, fmt.Errorf("%w: VT blob", ErrInvalidData)
251+ }
252+ if !totals.Add(stats) {
253+ return vt.DecodeStats{}, fmt.Errorf("%w: VT budget overflow", ErrInvalidData)
254+ }
255+ if !budget.addAllocation(uint64(n)) || !budget.addProduct(stats.Chunks, 64) || !budget.addProduct(stats.Rows, 32) || !budget.addProduct(stats.Cells, 64) {
256+ return vt.DecodeStats{}, fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
257+ }
258+ *blobs++
259+ return stats, nil
260+}
261+
262+func preflightProcess(r *payloadReader) error {
263+ present, err := r.getUint8()
264+ if err != nil {
265+ return err
266+ }
267+ if present == 0 {
268+ return nil
269+ }
270+ if present != 1 {
271+ return fmt.Errorf("%w: process presence", ErrInvalidData)
272+ }
273+ n, err := r.getUint16()
274+ if err != nil {
275+ return err
276+ }
277+ if n == 0 {
278+ return fmt.Errorf("%w: process argv empty", ErrInvalidData)
279+ }
280+ for range n {
281+ if err := skipString(r); err != nil {
282+ return err
283+ }
284+ }
285+ if err := skipString(r); err != nil {
286+ return err
287+ }
288+ return skipString(r)
289+}
290+
291+// preflightSessionReferences validates duplicate, empty, and dangling pane
292+// references only after the structural pass has admitted the complete payload.
293+func preflightSessionReferences(body []byte) error {
294+ r := payloadReader{b: body}
295+ if err := skipString(&r); err != nil {
296+ return err
297+ }
298+ if _, err := r.getUint64(); err != nil {
299+ return err
300+ }
301+ if _, err := r.getUint16(); err != nil {
302+ return err
303+ }
304+ tabs, err := r.getUint16()
305+ if err != nil {
306+ return err
307+ }
308+ for range tabs {
309+ if err := preflightTabReferences(&r); err != nil {
310+ return fmt.Errorf("preflight tab references: %w", err)
311+ }
312+ }
313+ return r.done()
314+}
315+
316+func preflightTabReferences(r *payloadReader) error {
317+ if err := skipString(r); err != nil {
318+ return err
319+ }
320+ if _, err := r.getUint16(); err != nil {
321+ return err
322+ }
323+ if _, err := r.getUint16(); err != nil {
324+ return err
325+ }
326+ if _, err := r.getUint64(); err != nil {
327+ return err
328+ }
329+ focus, err := r.getString()
330+ if err != nil {
331+ return err
332+ }
333+ references := preflightTreeReferences{}
334+ if err := preflightNodeReferences(r, &references); err != nil {
335+ return err
336+ }
337+ panes, err := r.getUint16()
338+ if err != nil {
339+ return err
340+ }
341+ ids := make(map[layout.PaneID]struct{}, panes)
342+ for range panes {
343+ id, err := r.getString()
344+ if err != nil {
345+ return err
346+ }
347+ if id == "" {
348+ return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
349+ }
350+ paneID := layout.PaneID(id)
351+ if _, exists := ids[paneID]; exists {
352+ return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
353+ }
354+ ids[paneID] = struct{}{}
355+ if err := skipString(r); err != nil {
356+ return err
357+ }
358+ if err := skipString(r); err != nil {
359+ return err
360+ }
361+ n, err := r.getUint32()
362+ if err != nil {
363+ return err
364+ }
365+ for range n {
366+ if err := skipBlob(r); err != nil {
367+ return err
368+ }
369+ }
370+ if err := skipBlob(r); err != nil {
371+ return err
372+ }
373+ if err := skipBlob(r); err != nil {
374+ return err
375+ }
376+ if err := preflightProcess(r); err != nil {
377+ return err
378+ }
379+ }
380+ if focus != "" {
381+ references.ids = append(references.ids, layout.PaneID(focus))
382+ }
383+ for _, id := range references.ids {
384+ if _, ok := ids[id]; !ok {
385+ return ErrUnknownPane
386+ }
387+ }
388+ return nil
389+}
390+
391+func skipBlob(r *payloadReader) error {
392+ n, err := r.getUint32()
393+ if err != nil {
394+ return err
395+ }
396+ if uint64(n) > uint64(len(r.b)) {
397+ return ErrShortPayload
398+ }
399+ r.b = r.b[n:]
400+ return nil
401+}
402+
403+type preflightTreeReferences struct{ ids []layout.PaneID }
404+
405+func (r *preflightTreeReferences) add(id layout.PaneID) {
406+ r.ids = append(r.ids, id)
407+}
408+
409+func preflightNodeReferences(r *payloadReader, references *preflightTreeReferences) error {
410+ kind, err := r.getUint8()
411+ if err != nil {
412+ return err
413+ }
414+ switch kind {
415+ case 0:
416+ leaf, err := r.getString()
417+ if err != nil {
418+ return err
419+ }
420+ references.add(layout.PaneID(leaf))
421+ return nil
422+ case 1:
423+ if _, err := r.getUint8(); err != nil {
424+ return err
425+ }
426+ case 2:
427+ expanded, err := r.getString()
428+ if err != nil {
429+ return err
430+ }
431+ if expanded != "" {
432+ references.add(layout.PaneID(expanded))
433+ }
434+ case 3:
435+ return nil
436+ default:
437+ return fmt.Errorf("%w: node kind", ErrInvalidData)
438+ }
439+ children, err := r.getUint16()
440+ if err != nil {
441+ return err
442+ }
443+ for range children {
444+ if err := preflightNodeReferences(r, references); err != nil {
445+ return err
446+ }
447+ }
448+ return nil
449+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close