detached dd9f60c

fix(snapshot): align incremental generation contracts

commit
dd9f60c5aa8f2cfcc694de787aced78e15090183
author
brice · · 2026-07-21 19:25 UTC
committer
brice · · 2026-07-21 19:26 UTC
parent
871c141
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +877 -754
M internal/ports/mocks/store_mock.go +261 -59
@@ -5,6 +5,8 @@
55 package portsmocks
66
77 import (
8+ "context"
9+
810 "github.com/bnema/vev/internal/ports"
911 mock "github.com/stretchr/testify/mock"
1012 )
@@ -552,16 +554,16 @@ func (_m *MockSnapshotRepository) EXPECT() *MockSnapshotRepository_Expecter {
552554 }
553555
554556 // Delete provides a mock function for the type MockSnapshotRepository
555-func (_mock *MockSnapshotRepository) Delete(name string) error {
556- ret := _mock.Called(name)
557+func (_mock *MockSnapshotRepository) Delete(context1 context.Context, s string) error {
558+ ret := _mock.Called(context1, s)
557559
558560 if len(ret) == 0 {
559561 panic("no return value specified for Delete")
560562 }
561563
562564 var r0 error
563- if returnFunc, ok := ret.Get(0).(func(string) error); ok {
564- r0 = returnFunc(name)
565+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
566+ r0 = returnFunc(context1, s)
565567 } else {
566568 r0 = ret.Error(0)
567569 }
@@ -574,19 +576,25 @@ type MockSnapshotRepository_Delete_Call struct {
574576 }
575577
576578 // Delete is a helper method to define mock.On call
577-// - name string
578-func (_e *MockSnapshotRepository_Expecter) Delete(name any) *MockSnapshotRepository_Delete_Call {
579- return &MockSnapshotRepository_Delete_Call{Call: _e.mock.On("Delete", name)}
579+// - context1 context.Context
580+// - s string
581+func (_e *MockSnapshotRepository_Expecter) Delete(context1 any, s any) *MockSnapshotRepository_Delete_Call {
582+ return &MockSnapshotRepository_Delete_Call{Call: _e.mock.On("Delete", context1, s)}
580583 }
581584
582-func (_c *MockSnapshotRepository_Delete_Call) Run(run func(name string)) *MockSnapshotRepository_Delete_Call {
585+func (_c *MockSnapshotRepository_Delete_Call) Run(run func(context1 context.Context, s string)) *MockSnapshotRepository_Delete_Call {
583586 _c.Call.Run(func(args mock.Arguments) {
584- var arg0 string
587+ var arg0 context.Context
585588 if args[0] != nil {
586- arg0 = args[0].(string)
589+ arg0 = args[0].(context.Context)
590+ }
591+ var arg1 string
592+ if args[1] != nil {
593+ arg1 = args[1].(string)
587594 }
588595 run(
589596 arg0,
597+ arg1,
590598 )
591599 })
592600 return _c
@@ -597,33 +605,93 @@ func (_c *MockSnapshotRepository_Delete_Call) Return(err error) *MockSnapshotRep
597605 return _c
598606 }
599607
600-func (_c *MockSnapshotRepository_Delete_Call) RunAndReturn(run func(name string) error) *MockSnapshotRepository_Delete_Call {
608+func (_c *MockSnapshotRepository_Delete_Call) RunAndReturn(run func(context1 context.Context, s string) error) *MockSnapshotRepository_Delete_Call {
601609 _c.Call.Return(run)
602610 return _c
603611 }
604612
605-// Load provides a mock function for the type MockSnapshotRepository
606-func (_mock *MockSnapshotRepository) Load() ([]ports.SnapshotGeneration, error) {
607- ret := _mock.Called()
613+// List provides a mock function for the type MockSnapshotRepository
614+func (_mock *MockSnapshotRepository) List(context1 context.Context) ([]string, error) {
615+ ret := _mock.Called(context1)
608616
609617 if len(ret) == 0 {
610- panic("no return value specified for Load")
618+ panic("no return value specified for List")
611619 }
612620
613- var r0 []ports.SnapshotGeneration
621+ var r0 []string
614622 var r1 error
615- if returnFunc, ok := ret.Get(0).(func() ([]ports.SnapshotGeneration, error)); ok {
616- return returnFunc()
623+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok {
624+ return returnFunc(context1)
617625 }
618- if returnFunc, ok := ret.Get(0).(func() []ports.SnapshotGeneration); ok {
619- r0 = returnFunc()
626+ if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok {
627+ r0 = returnFunc(context1)
620628 } else {
621629 if ret.Get(0) != nil {
622- r0 = ret.Get(0).([]ports.SnapshotGeneration)
630+ r0 = ret.Get(0).([]string)
623631 }
624632 }
625- if returnFunc, ok := ret.Get(1).(func() error); ok {
626- r1 = returnFunc()
633+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
634+ r1 = returnFunc(context1)
635+ } else {
636+ r1 = ret.Error(1)
637+ }
638+ return r0, r1
639+}
640+
641+// MockSnapshotRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
642+type MockSnapshotRepository_List_Call struct {
643+ *mock.Call
644+}
645+
646+// List is a helper method to define mock.On call
647+// - context1 context.Context
648+func (_e *MockSnapshotRepository_Expecter) List(context1 any) *MockSnapshotRepository_List_Call {
649+ return &MockSnapshotRepository_List_Call{Call: _e.mock.On("List", context1)}
650+}
651+
652+func (_c *MockSnapshotRepository_List_Call) Run(run func(context1 context.Context)) *MockSnapshotRepository_List_Call {
653+ _c.Call.Run(func(args mock.Arguments) {
654+ var arg0 context.Context
655+ if args[0] != nil {
656+ arg0 = args[0].(context.Context)
657+ }
658+ run(
659+ arg0,
660+ )
661+ })
662+ return _c
663+}
664+
665+func (_c *MockSnapshotRepository_List_Call) Return(strings []string, err error) *MockSnapshotRepository_List_Call {
666+ _c.Call.Return(strings, err)
667+ return _c
668+}
669+
670+func (_c *MockSnapshotRepository_List_Call) RunAndReturn(run func(context1 context.Context) ([]string, error)) *MockSnapshotRepository_List_Call {
671+ _c.Call.Return(run)
672+ return _c
673+}
674+
675+// Load provides a mock function for the type MockSnapshotRepository
676+func (_mock *MockSnapshotRepository) Load(context1 context.Context, s string) (ports.SnapshotGeneration, error) {
677+ ret := _mock.Called(context1, s)
678+
679+ if len(ret) == 0 {
680+ panic("no return value specified for Load")
681+ }
682+
683+ var r0 ports.SnapshotGeneration
684+ var r1 error
685+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (ports.SnapshotGeneration, error)); ok {
686+ return returnFunc(context1, s)
687+ }
688+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ports.SnapshotGeneration); ok {
689+ r0 = returnFunc(context1, s)
690+ } else {
691+ r0 = ret.Get(0).(ports.SnapshotGeneration)
692+ }
693+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
694+ r1 = returnFunc(context1, s)
627695 } else {
628696 r1 = ret.Error(1)
629697 }
@@ -636,38 +704,102 @@ type MockSnapshotRepository_Load_Call struct {
636704 }
637705
638706 // Load is a helper method to define mock.On call
639-func (_e *MockSnapshotRepository_Expecter) Load() *MockSnapshotRepository_Load_Call {
640- return &MockSnapshotRepository_Load_Call{Call: _e.mock.On("Load")}
707+// - context1 context.Context
708+// - s string
709+func (_e *MockSnapshotRepository_Expecter) Load(context1 any, s any) *MockSnapshotRepository_Load_Call {
710+ return &MockSnapshotRepository_Load_Call{Call: _e.mock.On("Load", context1, s)}
641711 }
642712
643-func (_c *MockSnapshotRepository_Load_Call) Run(run func()) *MockSnapshotRepository_Load_Call {
713+func (_c *MockSnapshotRepository_Load_Call) Run(run func(context1 context.Context, s string)) *MockSnapshotRepository_Load_Call {
644714 _c.Call.Run(func(args mock.Arguments) {
645- run()
715+ var arg0 context.Context
716+ if args[0] != nil {
717+ arg0 = args[0].(context.Context)
718+ }
719+ var arg1 string
720+ if args[1] != nil {
721+ arg1 = args[1].(string)
722+ }
723+ run(
724+ arg0,
725+ arg1,
726+ )
646727 })
647728 return _c
648729 }
649730
650-func (_c *MockSnapshotRepository_Load_Call) Return(snapshotGenerations []ports.SnapshotGeneration, err error) *MockSnapshotRepository_Load_Call {
651- _c.Call.Return(snapshotGenerations, err)
731+func (_c *MockSnapshotRepository_Load_Call) Return(snapshotGeneration ports.SnapshotGeneration, err error) *MockSnapshotRepository_Load_Call {
732+ _c.Call.Return(snapshotGeneration, err)
652733 return _c
653734 }
654735
655-func (_c *MockSnapshotRepository_Load_Call) RunAndReturn(run func() ([]ports.SnapshotGeneration, error)) *MockSnapshotRepository_Load_Call {
736+func (_c *MockSnapshotRepository_Load_Call) RunAndReturn(run func(context1 context.Context, s string) (ports.SnapshotGeneration, error)) *MockSnapshotRepository_Load_Call {
737+ _c.Call.Return(run)
738+ return _c
739+}
740+
741+// Maintain provides a mock function for the type MockSnapshotRepository
742+func (_mock *MockSnapshotRepository) Maintain(context1 context.Context) error {
743+ ret := _mock.Called(context1)
744+
745+ if len(ret) == 0 {
746+ panic("no return value specified for Maintain")
747+ }
748+
749+ var r0 error
750+ if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok {
751+ r0 = returnFunc(context1)
752+ } else {
753+ r0 = ret.Error(0)
754+ }
755+ return r0
756+}
757+
758+// MockSnapshotRepository_Maintain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Maintain'
759+type MockSnapshotRepository_Maintain_Call struct {
760+ *mock.Call
761+}
762+
763+// Maintain is a helper method to define mock.On call
764+// - context1 context.Context
765+func (_e *MockSnapshotRepository_Expecter) Maintain(context1 any) *MockSnapshotRepository_Maintain_Call {
766+ return &MockSnapshotRepository_Maintain_Call{Call: _e.mock.On("Maintain", context1)}
767+}
768+
769+func (_c *MockSnapshotRepository_Maintain_Call) Run(run func(context1 context.Context)) *MockSnapshotRepository_Maintain_Call {
770+ _c.Call.Run(func(args mock.Arguments) {
771+ var arg0 context.Context
772+ if args[0] != nil {
773+ arg0 = args[0].(context.Context)
774+ }
775+ run(
776+ arg0,
777+ )
778+ })
779+ return _c
780+}
781+
782+func (_c *MockSnapshotRepository_Maintain_Call) Return(err error) *MockSnapshotRepository_Maintain_Call {
783+ _c.Call.Return(err)
784+ return _c
785+}
786+
787+func (_c *MockSnapshotRepository_Maintain_Call) RunAndReturn(run func(context1 context.Context) error) *MockSnapshotRepository_Maintain_Call {
656788 _c.Call.Return(run)
657789 return _c
658790 }
659791
660792 // Publish provides a mock function for the type MockSnapshotRepository
661-func (_mock *MockSnapshotRepository) Publish(snapshotPublication ports.SnapshotPublication) error {
662- ret := _mock.Called(snapshotPublication)
793+func (_mock *MockSnapshotRepository) Publish(context1 context.Context, snapshotPublication ports.SnapshotPublication) error {
794+ ret := _mock.Called(context1, snapshotPublication)
663795
664796 if len(ret) == 0 {
665797 panic("no return value specified for Publish")
666798 }
667799
668800 var r0 error
669- if returnFunc, ok := ret.Get(0).(func(ports.SnapshotPublication) error); ok {
670- r0 = returnFunc(snapshotPublication)
801+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.SnapshotPublication) error); ok {
802+ r0 = returnFunc(context1, snapshotPublication)
671803 } else {
672804 r0 = ret.Error(0)
673805 }
@@ -680,19 +812,25 @@ type MockSnapshotRepository_Publish_Call struct {
680812 }
681813
682814 // Publish is a helper method to define mock.On call
815+// - context1 context.Context
683816 // - snapshotPublication ports.SnapshotPublication
684-func (_e *MockSnapshotRepository_Expecter) Publish(snapshotPublication any) *MockSnapshotRepository_Publish_Call {
685- return &MockSnapshotRepository_Publish_Call{Call: _e.mock.On("Publish", snapshotPublication)}
817+func (_e *MockSnapshotRepository_Expecter) Publish(context1 any, snapshotPublication any) *MockSnapshotRepository_Publish_Call {
818+ return &MockSnapshotRepository_Publish_Call{Call: _e.mock.On("Publish", context1, snapshotPublication)}
686819 }
687820
688-func (_c *MockSnapshotRepository_Publish_Call) Run(run func(snapshotPublication ports.SnapshotPublication)) *MockSnapshotRepository_Publish_Call {
821+func (_c *MockSnapshotRepository_Publish_Call) Run(run func(context1 context.Context, snapshotPublication ports.SnapshotPublication)) *MockSnapshotRepository_Publish_Call {
689822 _c.Call.Run(func(args mock.Arguments) {
690- var arg0 ports.SnapshotPublication
823+ var arg0 context.Context
691824 if args[0] != nil {
692- arg0 = args[0].(ports.SnapshotPublication)
825+ arg0 = args[0].(context.Context)
826+ }
827+ var arg1 ports.SnapshotPublication
828+ if args[1] != nil {
829+ arg1 = args[1].(ports.SnapshotPublication)
693830 }
694831 run(
695832 arg0,
833+ arg1,
696834 )
697835 })
698836 return _c
@@ -703,7 +841,7 @@ func (_c *MockSnapshotRepository_Publish_Call) Return(err error) *MockSnapshotRe
703841 return _c
704842 }
705843
706-func (_c *MockSnapshotRepository_Publish_Call) RunAndReturn(run func(snapshotPublication ports.SnapshotPublication) error) *MockSnapshotRepository_Publish_Call {
844+func (_c *MockSnapshotRepository_Publish_Call) RunAndReturn(run func(context1 context.Context, snapshotPublication ports.SnapshotPublication) error) *MockSnapshotRepository_Publish_Call {
707845 _c.Call.Return(run)
708846 return _c
709847 }
@@ -735,57 +873,121 @@ func (_m *MockLegacySnapshotSource) EXPECT() *MockLegacySnapshotSource_Expecter
735873 return &MockLegacySnapshotSource_Expecter{mock: &_m.Mock}
736874 }
737875
738-// LoadLegacySnapshots provides a mock function for the type MockLegacySnapshotSource
739-func (_mock *MockLegacySnapshotSource) LoadLegacySnapshots() ([]ports.LegacySnapshot, error) {
740- ret := _mock.Called()
876+// DeleteLegacy provides a mock function for the type MockLegacySnapshotSource
877+func (_mock *MockLegacySnapshotSource) DeleteLegacy(context1 context.Context, s string) error {
878+ ret := _mock.Called(context1, s)
879+
880+ if len(ret) == 0 {
881+ panic("no return value specified for DeleteLegacy")
882+ }
883+
884+ var r0 error
885+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
886+ r0 = returnFunc(context1, s)
887+ } else {
888+ r0 = ret.Error(0)
889+ }
890+ return r0
891+}
892+
893+// MockLegacySnapshotSource_DeleteLegacy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteLegacy'
894+type MockLegacySnapshotSource_DeleteLegacy_Call struct {
895+ *mock.Call
896+}
897+
898+// DeleteLegacy is a helper method to define mock.On call
899+// - context1 context.Context
900+// - s string
901+func (_e *MockLegacySnapshotSource_Expecter) DeleteLegacy(context1 any, s any) *MockLegacySnapshotSource_DeleteLegacy_Call {
902+ return &MockLegacySnapshotSource_DeleteLegacy_Call{Call: _e.mock.On("DeleteLegacy", context1, s)}
903+}
904+
905+func (_c *MockLegacySnapshotSource_DeleteLegacy_Call) Run(run func(context1 context.Context, s string)) *MockLegacySnapshotSource_DeleteLegacy_Call {
906+ _c.Call.Run(func(args mock.Arguments) {
907+ var arg0 context.Context
908+ if args[0] != nil {
909+ arg0 = args[0].(context.Context)
910+ }
911+ var arg1 string
912+ if args[1] != nil {
913+ arg1 = args[1].(string)
914+ }
915+ run(
916+ arg0,
917+ arg1,
918+ )
919+ })
920+ return _c
921+}
922+
923+func (_c *MockLegacySnapshotSource_DeleteLegacy_Call) Return(err error) *MockLegacySnapshotSource_DeleteLegacy_Call {
924+ _c.Call.Return(err)
925+ return _c
926+}
927+
928+func (_c *MockLegacySnapshotSource_DeleteLegacy_Call) RunAndReturn(run func(context1 context.Context, s string) error) *MockLegacySnapshotSource_DeleteLegacy_Call {
929+ _c.Call.Return(run)
930+ return _c
931+}
932+
933+// LoadLegacy provides a mock function for the type MockLegacySnapshotSource
934+func (_mock *MockLegacySnapshotSource) LoadLegacy(context1 context.Context) ([]ports.LegacySnapshot, error) {
935+ ret := _mock.Called(context1)
741936
742937 if len(ret) == 0 {
743- panic("no return value specified for LoadLegacySnapshots")
938+ panic("no return value specified for LoadLegacy")
744939 }
745940
746941 var r0 []ports.LegacySnapshot
747942 var r1 error
748- if returnFunc, ok := ret.Get(0).(func() ([]ports.LegacySnapshot, error)); ok {
749- return returnFunc()
943+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]ports.LegacySnapshot, error)); ok {
944+ return returnFunc(context1)
750945 }
751- if returnFunc, ok := ret.Get(0).(func() []ports.LegacySnapshot); ok {
752- r0 = returnFunc()
946+ if returnFunc, ok := ret.Get(0).(func(context.Context) []ports.LegacySnapshot); ok {
947+ r0 = returnFunc(context1)
753948 } else {
754949 if ret.Get(0) != nil {
755950 r0 = ret.Get(0).([]ports.LegacySnapshot)
756951 }
757952 }
758- if returnFunc, ok := ret.Get(1).(func() error); ok {
759- r1 = returnFunc()
953+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
954+ r1 = returnFunc(context1)
760955 } else {
761956 r1 = ret.Error(1)
762957 }
763958 return r0, r1
764959 }
765960
766-// MockLegacySnapshotSource_LoadLegacySnapshots_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadLegacySnapshots'
767-type MockLegacySnapshotSource_LoadLegacySnapshots_Call struct {
961+// MockLegacySnapshotSource_LoadLegacy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadLegacy'
962+type MockLegacySnapshotSource_LoadLegacy_Call struct {
768963 *mock.Call
769964 }
770965
771-// LoadLegacySnapshots is a helper method to define mock.On call
772-func (_e *MockLegacySnapshotSource_Expecter) LoadLegacySnapshots() *MockLegacySnapshotSource_LoadLegacySnapshots_Call {
773- return &MockLegacySnapshotSource_LoadLegacySnapshots_Call{Call: _e.mock.On("LoadLegacySnapshots")}
966+// LoadLegacy is a helper method to define mock.On call
967+// - context1 context.Context
968+func (_e *MockLegacySnapshotSource_Expecter) LoadLegacy(context1 any) *MockLegacySnapshotSource_LoadLegacy_Call {
969+ return &MockLegacySnapshotSource_LoadLegacy_Call{Call: _e.mock.On("LoadLegacy", context1)}
774970 }
775971
776-func (_c *MockLegacySnapshotSource_LoadLegacySnapshots_Call) Run(run func()) *MockLegacySnapshotSource_LoadLegacySnapshots_Call {
972+func (_c *MockLegacySnapshotSource_LoadLegacy_Call) Run(run func(context1 context.Context)) *MockLegacySnapshotSource_LoadLegacy_Call {
777973 _c.Call.Run(func(args mock.Arguments) {
778- run()
974+ var arg0 context.Context
975+ if args[0] != nil {
976+ arg0 = args[0].(context.Context)
977+ }
978+ run(
979+ arg0,
980+ )
779981 })
780982 return _c
781983 }
782984
783-func (_c *MockLegacySnapshotSource_LoadLegacySnapshots_Call) Return(legacySnapshots []ports.LegacySnapshot, err error) *MockLegacySnapshotSource_LoadLegacySnapshots_Call {
985+func (_c *MockLegacySnapshotSource_LoadLegacy_Call) Return(legacySnapshots []ports.LegacySnapshot, err error) *MockLegacySnapshotSource_LoadLegacy_Call {
784986 _c.Call.Return(legacySnapshots, err)
785987 return _c
786988 }
787989
788-func (_c *MockLegacySnapshotSource_LoadLegacySnapshots_Call) RunAndReturn(run func() ([]ports.LegacySnapshot, error)) *MockLegacySnapshotSource_LoadLegacySnapshots_Call {
990+func (_c *MockLegacySnapshotSource_LoadLegacy_Call) RunAndReturn(run func(context1 context.Context) ([]ports.LegacySnapshot, error)) *MockLegacySnapshotSource_LoadLegacy_Call {
789991 _c.Call.Return(run)
790992 return _c
791993 }
@@ -5,6 +5,8 @@
5 package portsmocks5 package portsmocks
6 6
7 import (7 import (
8+ "context"
9+
8 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
9 mock "github.com/stretchr/testify/mock"11 mock "github.com/stretchr/testify/mock"
10 )12 )
@@ -552,16 +554,16 @@ func (_m *MockSnapshotRepository) EXPECT() *MockSnapshotRepository_Expecter {
552 }554 }
553 555
554 // Delete provides a mock function for the type MockSnapshotRepository556 // Delete provides a mock function for the type MockSnapshotRepository
555-func (_mock *MockSnapshotRepository) Delete(name string) error {557+func (_mock *MockSnapshotRepository) Delete(context1 context.Context, s string) error {
556- ret := _mock.Called(name)558+ ret := _mock.Called(context1, s)
557 559
558 if len(ret) == 0 {560 if len(ret) == 0 {
559 panic("no return value specified for Delete")561 panic("no return value specified for Delete")
560 }562 }
561 563
562 var r0 error564 var r0 error
563- if returnFunc, ok := ret.Get(0).(func(string) error); ok {565+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
564- r0 = returnFunc(name)566+ r0 = returnFunc(context1, s)
565 } else {567 } else {
566 r0 = ret.Error(0)568 r0 = ret.Error(0)
567 }569 }
@@ -574,19 +576,25 @@ type MockSnapshotRepository_Delete_Call struct {
574 }576 }
575 577
576 // Delete is a helper method to define mock.On call578 // Delete is a helper method to define mock.On call
577-// - name string579+// - context1 context.Context
578-func (_e *MockSnapshotRepository_Expecter) Delete(name any) *MockSnapshotRepository_Delete_Call {580+// - s string
579- return &MockSnapshotRepository_Delete_Call{Call: _e.mock.On("Delete", name)}581+func (_e *MockSnapshotRepository_Expecter) Delete(context1 any, s any) *MockSnapshotRepository_Delete_Call {
582+ return &MockSnapshotRepository_Delete_Call{Call: _e.mock.On("Delete", context1, s)}
580 }583 }
581 584
582-func (_c *MockSnapshotRepository_Delete_Call) Run(run func(name string)) *MockSnapshotRepository_Delete_Call {585+func (_c *MockSnapshotRepository_Delete_Call) Run(run func(context1 context.Context, s string)) *MockSnapshotRepository_Delete_Call {
583 _c.Call.Run(func(args mock.Arguments) {586 _c.Call.Run(func(args mock.Arguments) {
584- var arg0 string587+ var arg0 context.Context
585 if args[0] != nil {588 if args[0] != nil {
586- arg0 = args[0].(string)589+ arg0 = args[0].(context.Context)
590+ }
591+ var arg1 string
592+ if args[1] != nil {
593+ arg1 = args[1].(string)
587 }594 }
588 run(595 run(
589 arg0,596 arg0,
597+ arg1,
590 )598 )
591 })599 })
592 return _c600 return _c
@@ -597,33 +605,93 @@ func (_c *MockSnapshotRepository_Delete_Call) Return(err error) *MockSnapshotRep
597 return _c605 return _c
598 }606 }
599 607
600-func (_c *MockSnapshotRepository_Delete_Call) RunAndReturn(run func(name string) error) *MockSnapshotRepository_Delete_Call {608+func (_c *MockSnapshotRepository_Delete_Call) RunAndReturn(run func(context1 context.Context, s string) error) *MockSnapshotRepository_Delete_Call {
601 _c.Call.Return(run)609 _c.Call.Return(run)
602 return _c610 return _c
603 }611 }
604 612
605-// Load provides a mock function for the type MockSnapshotRepository613+// List provides a mock function for the type MockSnapshotRepository
606-func (_mock *MockSnapshotRepository) Load() ([]ports.SnapshotGeneration, error) {614+func (_mock *MockSnapshotRepository) List(context1 context.Context) ([]string, error) {
607- ret := _mock.Called()615+ ret := _mock.Called(context1)
608 616
609 if len(ret) == 0 {617 if len(ret) == 0 {
610- panic("no return value specified for Load")618+ panic("no return value specified for List")
611 }619 }
612 620
613- var r0 []ports.SnapshotGeneration621+ var r0 []string
614 var r1 error622 var r1 error
615- if returnFunc, ok := ret.Get(0).(func() ([]ports.SnapshotGeneration, error)); ok {623+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok {
616- return returnFunc()624+ return returnFunc(context1)
617 }625 }
618- if returnFunc, ok := ret.Get(0).(func() []ports.SnapshotGeneration); ok {626+ if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok {
619- r0 = returnFunc()627+ r0 = returnFunc(context1)
620 } else {628 } else {
621 if ret.Get(0) != nil {629 if ret.Get(0) != nil {
622- r0 = ret.Get(0).([]ports.SnapshotGeneration)630+ r0 = ret.Get(0).([]string)
623 }631 }
624 }632 }
625- if returnFunc, ok := ret.Get(1).(func() error); ok {633+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
626- r1 = returnFunc()634+ r1 = returnFunc(context1)
635+ } else {
636+ r1 = ret.Error(1)
637+ }
638+ return r0, r1
639+}
640+
641+// MockSnapshotRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
642+type MockSnapshotRepository_List_Call struct {
643+ *mock.Call
644+}
645+
646+// List is a helper method to define mock.On call
647+// - context1 context.Context
648+func (_e *MockSnapshotRepository_Expecter) List(context1 any) *MockSnapshotRepository_List_Call {
649+ return &MockSnapshotRepository_List_Call{Call: _e.mock.On("List", context1)}
650+}
651+
652+func (_c *MockSnapshotRepository_List_Call) Run(run func(context1 context.Context)) *MockSnapshotRepository_List_Call {
653+ _c.Call.Run(func(args mock.Arguments) {
654+ var arg0 context.Context
655+ if args[0] != nil {
656+ arg0 = args[0].(context.Context)
657+ }
658+ run(
659+ arg0,
660+ )
661+ })
662+ return _c
663+}
664+
665+func (_c *MockSnapshotRepository_List_Call) Return(strings []string, err error) *MockSnapshotRepository_List_Call {
666+ _c.Call.Return(strings, err)
667+ return _c
668+}
669+
670+func (_c *MockSnapshotRepository_List_Call) RunAndReturn(run func(context1 context.Context) ([]string, error)) *MockSnapshotRepository_List_Call {
671+ _c.Call.Return(run)
672+ return _c
673+}
674+
675+// Load provides a mock function for the type MockSnapshotRepository
676+func (_mock *MockSnapshotRepository) Load(context1 context.Context, s string) (ports.SnapshotGeneration, error) {
677+ ret := _mock.Called(context1, s)
678+
679+ if len(ret) == 0 {
680+ panic("no return value specified for Load")
681+ }
682+
683+ var r0 ports.SnapshotGeneration
684+ var r1 error
685+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (ports.SnapshotGeneration, error)); ok {
686+ return returnFunc(context1, s)
687+ }
688+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ports.SnapshotGeneration); ok {
689+ r0 = returnFunc(context1, s)
690+ } else {
691+ r0 = ret.Get(0).(ports.SnapshotGeneration)
692+ }
693+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
694+ r1 = returnFunc(context1, s)
627 } else {695 } else {
628 r1 = ret.Error(1)696 r1 = ret.Error(1)
629 }697 }
@@ -636,38 +704,102 @@ type MockSnapshotRepository_Load_Call struct {
636 }704 }
637 705
638 // Load is a helper method to define mock.On call706 // Load is a helper method to define mock.On call
639-func (_e *MockSnapshotRepository_Expecter) Load() *MockSnapshotRepository_Load_Call {707+// - context1 context.Context
640- return &MockSnapshotRepository_Load_Call{Call: _e.mock.On("Load")}708+// - s string
709+func (_e *MockSnapshotRepository_Expecter) Load(context1 any, s any) *MockSnapshotRepository_Load_Call {
710+ return &MockSnapshotRepository_Load_Call{Call: _e.mock.On("Load", context1, s)}
641 }711 }
642 712
643-func (_c *MockSnapshotRepository_Load_Call) Run(run func()) *MockSnapshotRepository_Load_Call {713+func (_c *MockSnapshotRepository_Load_Call) Run(run func(context1 context.Context, s string)) *MockSnapshotRepository_Load_Call {
644 _c.Call.Run(func(args mock.Arguments) {714 _c.Call.Run(func(args mock.Arguments) {
645- run()715+ var arg0 context.Context
716+ if args[0] != nil {
717+ arg0 = args[0].(context.Context)
718+ }
719+ var arg1 string
720+ if args[1] != nil {
721+ arg1 = args[1].(string)
722+ }
723+ run(
724+ arg0,
725+ arg1,
726+ )
646 })727 })
647 return _c728 return _c
648 }729 }
649 730
650-func (_c *MockSnapshotRepository_Load_Call) Return(snapshotGenerations []ports.SnapshotGeneration, err error) *MockSnapshotRepository_Load_Call {731+func (_c *MockSnapshotRepository_Load_Call) Return(snapshotGeneration ports.SnapshotGeneration, err error) *MockSnapshotRepository_Load_Call {
651- _c.Call.Return(snapshotGenerations, err)732+ _c.Call.Return(snapshotGeneration, err)
652 return _c733 return _c
653 }734 }
654 735
655-func (_c *MockSnapshotRepository_Load_Call) RunAndReturn(run func() ([]ports.SnapshotGeneration, error)) *MockSnapshotRepository_Load_Call {736+func (_c *MockSnapshotRepository_Load_Call) RunAndReturn(run func(context1 context.Context, s string) (ports.SnapshotGeneration, error)) *MockSnapshotRepository_Load_Call {
737+ _c.Call.Return(run)
738+ return _c
739+}
740+
741+// Maintain provides a mock function for the type MockSnapshotRepository
742+func (_mock *MockSnapshotRepository) Maintain(context1 context.Context) error {
743+ ret := _mock.Called(context1)
744+
745+ if len(ret) == 0 {
746+ panic("no return value specified for Maintain")
747+ }
748+
749+ var r0 error
750+ if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok {
751+ r0 = returnFunc(context1)
752+ } else {
753+ r0 = ret.Error(0)
754+ }
755+ return r0
756+}
757+
758+// MockSnapshotRepository_Maintain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Maintain'
759+type MockSnapshotRepository_Maintain_Call struct {
760+ *mock.Call
761+}
762+
763+// Maintain is a helper method to define mock.On call
764+// - context1 context.Context
765+func (_e *MockSnapshotRepository_Expecter) Maintain(context1 any) *MockSnapshotRepository_Maintain_Call {
766+ return &MockSnapshotRepository_Maintain_Call{Call: _e.mock.On("Maintain", context1)}
767+}
768+
769+func (_c *MockSnapshotRepository_Maintain_Call) Run(run func(context1 context.Context)) *MockSnapshotRepository_Maintain_Call {
770+ _c.Call.Run(func(args mock.Arguments) {
771+ var arg0 context.Context
772+ if args[0] != nil {
773+ arg0 = args[0].(context.Context)
774+ }
775+ run(
776+ arg0,
777+ )
778+ })
779+ return _c
780+}
781+
782+func (_c *MockSnapshotRepository_Maintain_Call) Return(err error) *MockSnapshotRepository_Maintain_Call {
783+ _c.Call.Return(err)
784+ return _c
785+}
786+
787+func (_c *MockSnapshotRepository_Maintain_Call) RunAndReturn(run func(context1 context.Context) error) *MockSnapshotRepository_Maintain_Call {
656 _c.Call.Return(run)788 _c.Call.Return(run)
657 return _c789 return _c
658 }790 }
659 791
660 // Publish provides a mock function for the type MockSnapshotRepository792 // Publish provides a mock function for the type MockSnapshotRepository
661-func (_mock *MockSnapshotRepository) Publish(snapshotPublication ports.SnapshotPublication) error {793+func (_mock *MockSnapshotRepository) Publish(context1 context.Context, snapshotPublication ports.SnapshotPublication) error {
662- ret := _mock.Called(snapshotPublication)794+ ret := _mock.Called(context1, snapshotPublication)
663 795
664 if len(ret) == 0 {796 if len(ret) == 0 {
665 panic("no return value specified for Publish")797 panic("no return value specified for Publish")
666 }798 }
667 799
668 var r0 error800 var r0 error
669- if returnFunc, ok := ret.Get(0).(func(ports.SnapshotPublication) error); ok {801+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.SnapshotPublication) error); ok {
670- r0 = returnFunc(snapshotPublication)802+ r0 = returnFunc(context1, snapshotPublication)
671 } else {803 } else {
672 r0 = ret.Error(0)804 r0 = ret.Error(0)
673 }805 }
@@ -680,19 +812,25 @@ type MockSnapshotRepository_Publish_Call struct {
680 }812 }
681 813
682 // Publish is a helper method to define mock.On call814 // Publish is a helper method to define mock.On call
815+// - context1 context.Context
683 // - snapshotPublication ports.SnapshotPublication816 // - snapshotPublication ports.SnapshotPublication
684-func (_e *MockSnapshotRepository_Expecter) Publish(snapshotPublication any) *MockSnapshotRepository_Publish_Call {817+func (_e *MockSnapshotRepository_Expecter) Publish(context1 any, snapshotPublication any) *MockSnapshotRepository_Publish_Call {
685- return &MockSnapshotRepository_Publish_Call{Call: _e.mock.On("Publish", snapshotPublication)}818+ return &MockSnapshotRepository_Publish_Call{Call: _e.mock.On("Publish", context1, snapshotPublication)}
686 }819 }
687 820
688-func (_c *MockSnapshotRepository_Publish_Call) Run(run func(snapshotPublication ports.SnapshotPublication)) *MockSnapshotRepository_Publish_Call {821+func (_c *MockSnapshotRepository_Publish_Call) Run(run func(context1 context.Context, snapshotPublication ports.SnapshotPublication)) *MockSnapshotRepository_Publish_Call {
689 _c.Call.Run(func(args mock.Arguments) {822 _c.Call.Run(func(args mock.Arguments) {
690- var arg0 ports.SnapshotPublication823+ var arg0 context.Context
691 if args[0] != nil {824 if args[0] != nil {
692- arg0 = args[0].(ports.SnapshotPublication)825+ arg0 = args[0].(context.Context)
826+ }
827+ var arg1 ports.SnapshotPublication
828+ if args[1] != nil {
829+ arg1 = args[1].(ports.SnapshotPublication)
693 }830 }
694 run(831 run(
695 arg0,832 arg0,
833+ arg1,
696 )834 )
697 })835 })
698 return _c836 return _c
@@ -703,7 +841,7 @@ func (_c *MockSnapshotRepository_Publish_Call) Return(err error) *MockSnapshotRe
703 return _c841 return _c
704 }842 }
705 843
706-func (_c *MockSnapshotRepository_Publish_Call) RunAndReturn(run func(snapshotPublication ports.SnapshotPublication) error) *MockSnapshotRepository_Publish_Call {844+func (_c *MockSnapshotRepository_Publish_Call) RunAndReturn(run func(context1 context.Context, snapshotPublication ports.SnapshotPublication) error) *MockSnapshotRepository_Publish_Call {
707 _c.Call.Return(run)845 _c.Call.Return(run)
708 return _c846 return _c
709 }847 }
@@ -735,57 +873,121 @@ func (_m *MockLegacySnapshotSource) EXPECT() *MockLegacySnapshotSource_Expecter
735 return &MockLegacySnapshotSource_Expecter{mock: &_m.Mock}873 return &MockLegacySnapshotSource_Expecter{mock: &_m.Mock}
736 }874 }
737 875
738-// LoadLegacySnapshots provides a mock function for the type MockLegacySnapshotSource876+// DeleteLegacy provides a mock function for the type MockLegacySnapshotSource
739-func (_mock *MockLegacySnapshotSource) LoadLegacySnapshots() ([]ports.LegacySnapshot, error) {877+func (_mock *MockLegacySnapshotSource) DeleteLegacy(context1 context.Context, s string) error {
740- ret := _mock.Called()878+ ret := _mock.Called(context1, s)
879+
880+ if len(ret) == 0 {
881+ panic("no return value specified for DeleteLegacy")
882+ }
883+
884+ var r0 error
885+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
886+ r0 = returnFunc(context1, s)
887+ } else {
888+ r0 = ret.Error(0)
889+ }
890+ return r0
891+}
892+
893+// MockLegacySnapshotSource_DeleteLegacy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteLegacy'
894+type MockLegacySnapshotSource_DeleteLegacy_Call struct {
895+ *mock.Call
896+}
897+
898+// DeleteLegacy is a helper method to define mock.On call
899+// - context1 context.Context
900+// - s string
901+func (_e *MockLegacySnapshotSource_Expecter) DeleteLegacy(context1 any, s any) *MockLegacySnapshotSource_DeleteLegacy_Call {
902+ return &MockLegacySnapshotSource_DeleteLegacy_Call{Call: _e.mock.On("DeleteLegacy", context1, s)}
903+}
904+
905+func (_c *MockLegacySnapshotSource_DeleteLegacy_Call) Run(run func(context1 context.Context, s string)) *MockLegacySnapshotSource_DeleteLegacy_Call {
906+ _c.Call.Run(func(args mock.Arguments) {
907+ var arg0 context.Context
908+ if args[0] != nil {
909+ arg0 = args[0].(context.Context)
910+ }
911+ var arg1 string
912+ if args[1] != nil {
913+ arg1 = args[1].(string)
914+ }
915+ run(
916+ arg0,
917+ arg1,
918+ )
919+ })
920+ return _c
921+}
922+
923+func (_c *MockLegacySnapshotSource_DeleteLegacy_Call) Return(err error) *MockLegacySnapshotSource_DeleteLegacy_Call {
924+ _c.Call.Return(err)
925+ return _c
926+}
927+
928+func (_c *MockLegacySnapshotSource_DeleteLegacy_Call) RunAndReturn(run func(context1 context.Context, s string) error) *MockLegacySnapshotSource_DeleteLegacy_Call {
929+ _c.Call.Return(run)
930+ return _c
931+}
932+
933+// LoadLegacy provides a mock function for the type MockLegacySnapshotSource
934+func (_mock *MockLegacySnapshotSource) LoadLegacy(context1 context.Context) ([]ports.LegacySnapshot, error) {
935+ ret := _mock.Called(context1)
741 936
742 if len(ret) == 0 {937 if len(ret) == 0 {
743- panic("no return value specified for LoadLegacySnapshots")938+ panic("no return value specified for LoadLegacy")
744 }939 }
745 940
746 var r0 []ports.LegacySnapshot941 var r0 []ports.LegacySnapshot
747 var r1 error942 var r1 error
748- if returnFunc, ok := ret.Get(0).(func() ([]ports.LegacySnapshot, error)); ok {943+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]ports.LegacySnapshot, error)); ok {
749- return returnFunc()944+ return returnFunc(context1)
750 }945 }
751- if returnFunc, ok := ret.Get(0).(func() []ports.LegacySnapshot); ok {946+ if returnFunc, ok := ret.Get(0).(func(context.Context) []ports.LegacySnapshot); ok {
752- r0 = returnFunc()947+ r0 = returnFunc(context1)
753 } else {948 } else {
754 if ret.Get(0) != nil {949 if ret.Get(0) != nil {
755 r0 = ret.Get(0).([]ports.LegacySnapshot)950 r0 = ret.Get(0).([]ports.LegacySnapshot)
756 }951 }
757 }952 }
758- if returnFunc, ok := ret.Get(1).(func() error); ok {953+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
759- r1 = returnFunc()954+ r1 = returnFunc(context1)
760 } else {955 } else {
761 r1 = ret.Error(1)956 r1 = ret.Error(1)
762 }957 }
763 return r0, r1958 return r0, r1
764 }959 }
765 960
766-// MockLegacySnapshotSource_LoadLegacySnapshots_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadLegacySnapshots'961+// MockLegacySnapshotSource_LoadLegacy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadLegacy'
767-type MockLegacySnapshotSource_LoadLegacySnapshots_Call struct {962+type MockLegacySnapshotSource_LoadLegacy_Call struct {
768 *mock.Call963 *mock.Call
769 }964 }
770 965
771-// LoadLegacySnapshots is a helper method to define mock.On call966+// LoadLegacy is a helper method to define mock.On call
772-func (_e *MockLegacySnapshotSource_Expecter) LoadLegacySnapshots() *MockLegacySnapshotSource_LoadLegacySnapshots_Call {967+// - context1 context.Context
773- return &MockLegacySnapshotSource_LoadLegacySnapshots_Call{Call: _e.mock.On("LoadLegacySnapshots")}968+func (_e *MockLegacySnapshotSource_Expecter) LoadLegacy(context1 any) *MockLegacySnapshotSource_LoadLegacy_Call {
969+ return &MockLegacySnapshotSource_LoadLegacy_Call{Call: _e.mock.On("LoadLegacy", context1)}
774 }970 }
775 971
776-func (_c *MockLegacySnapshotSource_LoadLegacySnapshots_Call) Run(run func()) *MockLegacySnapshotSource_LoadLegacySnapshots_Call {972+func (_c *MockLegacySnapshotSource_LoadLegacy_Call) Run(run func(context1 context.Context)) *MockLegacySnapshotSource_LoadLegacy_Call {
777 _c.Call.Run(func(args mock.Arguments) {973 _c.Call.Run(func(args mock.Arguments) {
778- run()974+ var arg0 context.Context
975+ if args[0] != nil {
976+ arg0 = args[0].(context.Context)
977+ }
978+ run(
979+ arg0,
980+ )
779 })981 })
780 return _c982 return _c
781 }983 }
782 984
783-func (_c *MockLegacySnapshotSource_LoadLegacySnapshots_Call) Return(legacySnapshots []ports.LegacySnapshot, err error) *MockLegacySnapshotSource_LoadLegacySnapshots_Call {985+func (_c *MockLegacySnapshotSource_LoadLegacy_Call) Return(legacySnapshots []ports.LegacySnapshot, err error) *MockLegacySnapshotSource_LoadLegacy_Call {
784 _c.Call.Return(legacySnapshots, err)986 _c.Call.Return(legacySnapshots, err)
785 return _c987 return _c
786 }988 }
787 989
788-func (_c *MockLegacySnapshotSource_LoadLegacySnapshots_Call) RunAndReturn(run func() ([]ports.LegacySnapshot, error)) *MockLegacySnapshotSource_LoadLegacySnapshots_Call {990+func (_c *MockLegacySnapshotSource_LoadLegacy_Call) RunAndReturn(run func(context1 context.Context) ([]ports.LegacySnapshot, error)) *MockLegacySnapshotSource_LoadLegacy_Call {
789 _c.Call.Return(run)991 _c.Call.Return(run)
790 return _c992 return _c
791 }993 }
M internal/ports/snapshot_test.go +30 -21
@@ -1,33 +1,50 @@
11 package ports
22
3-import "testing"
3+import (
4+ "context"
5+ "crypto/sha256"
6+ "testing"
7+)
48
59 type snapshotRepositoryContract struct{}
610
7-func (snapshotRepositoryContract) Publish(SnapshotPublication) error { return nil }
8-func (snapshotRepositoryContract) Load() ([]SnapshotGeneration, error) { return nil, nil }
9-func (snapshotRepositoryContract) Delete(string) error { return nil }
11+func (snapshotRepositoryContract) Publish(context.Context, SnapshotPublication) error { return nil }
12+func (snapshotRepositoryContract) List(context.Context) ([]string, error) { return nil, nil }
13+func (snapshotRepositoryContract) Load(context.Context, string) (SnapshotGeneration, error) {
14+ return SnapshotGeneration{}, nil
15+}
16+func (snapshotRepositoryContract) Delete(context.Context, string) error { return nil }
17+func (snapshotRepositoryContract) Maintain(context.Context) error { return nil }
1018
1119 type legacySnapshotSourceContract struct{}
1220
13-func (legacySnapshotSourceContract) LoadLegacySnapshots() ([]LegacySnapshot, error) { return nil, nil }
21+func (legacySnapshotSourceContract) LoadLegacy(context.Context) ([]LegacySnapshot, error) {
22+ return nil, nil
23+}
24+func (legacySnapshotSourceContract) DeleteLegacy(context.Context, string) error { return nil }
1425
1526 func TestSnapshotRepositoryContracts(t *testing.T) {
1627 var _ SnapshotRepository = snapshotRepositoryContract{}
1728 var _ LegacySnapshotSource = legacySnapshotSourceContract{}
1829
1930 publication := SnapshotPublication{
20- Name: "named-session",
21- Head: []byte("head-envelope"),
22- Objects: []SnapshotObject{{Digest: SnapshotDigest{1}, Data: []byte("object-envelope")}},
31+ Name: "named-session",
32+ Generation: 7,
33+ Manifest: []byte("VEVM"),
34+ Objects: []SnapshotObject{{Digest: SnapshotDigest{1}, Data: []byte("VEVO")}},
35+ }
36+ generation := SnapshotGeneration{
37+ Name: publication.Name,
38+ Generation: publication.Generation,
39+ Manifest: publication.Manifest,
40+ Objects: map[SnapshotDigest][]byte{publication.Objects[0].Digest: publication.Objects[0].Data},
41+ Fallback: true,
2342 }
24- generation := SnapshotGeneration{Publication: publication, Manifest: []byte("manifest-envelope")}
25- if publication.Name != "named-session" || string(generation.Manifest) != "manifest-envelope" {
43+ if publication.Name != "named-session" || generation.Generation != 7 || !generation.Fallback {
2644 t.Fatalf("snapshot publication/generation = %#v", generation)
2745 }
28- legacy := LegacySnapshot{Name: "old", Data: []byte("v3")}
29- if legacy.Name != "old" || string(legacy.Data) != "v3" {
30- t.Fatalf("legacy snapshot = %#v", legacy)
46+ if len(SnapshotDigest{}) != sha256.Size {
47+ t.Fatalf("SnapshotDigest has length %d, want %d", len(SnapshotDigest{}), sha256.Size)
3148 }
3249 }
3350
@@ -39,12 +56,4 @@ func (snapshotStoreContract) Delete(name string) error { return nil
3956
4057 func TestSnapshotStoreContract(t *testing.T) {
4158 var _ SnapshotStore = snapshotStoreContract{}
42-
43- blob := SnapshotBlob{Name: "named-session", Data: []byte("snapshot-bytes")}
44- if blob.Name != "named-session" {
45- t.Fatalf("SnapshotBlob.Name = %q, want %q", blob.Name, "named-session")
46- }
47- if string(blob.Data) != "snapshot-bytes" {
48- t.Fatalf("SnapshotBlob.Data = %q, want %q", string(blob.Data), "snapshot-bytes")
49- }
5059 }
@@ -1,33 +1,50 @@
1 package ports1 package ports
2 2
3-import "testing"3+import (
4+ "context"
5+ "crypto/sha256"
6+ "testing"
7+)
4 8
5 type snapshotRepositoryContract struct{}9 type snapshotRepositoryContract struct{}
6 10
7-func (snapshotRepositoryContract) Publish(SnapshotPublication) error { return nil }11+func (snapshotRepositoryContract) Publish(context.Context, SnapshotPublication) error { return nil }
8-func (snapshotRepositoryContract) Load() ([]SnapshotGeneration, error) { return nil, nil }12+func (snapshotRepositoryContract) List(context.Context) ([]string, error) { return nil, nil }
9-func (snapshotRepositoryContract) Delete(string) error { return nil }13+func (snapshotRepositoryContract) Load(context.Context, string) (SnapshotGeneration, error) {
14+ return SnapshotGeneration{}, nil
15+}
16+func (snapshotRepositoryContract) Delete(context.Context, string) error { return nil }
17+func (snapshotRepositoryContract) Maintain(context.Context) error { return nil }
10 18
11 type legacySnapshotSourceContract struct{}19 type legacySnapshotSourceContract struct{}
12 20
13-func (legacySnapshotSourceContract) LoadLegacySnapshots() ([]LegacySnapshot, error) { return nil, nil }21+func (legacySnapshotSourceContract) LoadLegacy(context.Context) ([]LegacySnapshot, error) {
22+ return nil, nil
23+}
24+func (legacySnapshotSourceContract) DeleteLegacy(context.Context, string) error { return nil }
14 25
15 func TestSnapshotRepositoryContracts(t *testing.T) {26 func TestSnapshotRepositoryContracts(t *testing.T) {
16 var _ SnapshotRepository = snapshotRepositoryContract{}27 var _ SnapshotRepository = snapshotRepositoryContract{}
17 var _ LegacySnapshotSource = legacySnapshotSourceContract{}28 var _ LegacySnapshotSource = legacySnapshotSourceContract{}
18 29
19 publication := SnapshotPublication{30 publication := SnapshotPublication{
20- Name: "named-session",31+ Name: "named-session",
21- Head: []byte("head-envelope"),32+ Generation: 7,
22- Objects: []SnapshotObject{{Digest: SnapshotDigest{1}, Data: []byte("object-envelope")}},33+ Manifest: []byte("VEVM"),
34+ Objects: []SnapshotObject{{Digest: SnapshotDigest{1}, Data: []byte("VEVO")}},
35+ }
36+ generation := SnapshotGeneration{
37+ Name: publication.Name,
38+ Generation: publication.Generation,
39+ Manifest: publication.Manifest,
40+ Objects: map[SnapshotDigest][]byte{publication.Objects[0].Digest: publication.Objects[0].Data},
41+ Fallback: true,
23 }42 }
24- generation := SnapshotGeneration{Publication: publication, Manifest: []byte("manifest-envelope")}43+ if publication.Name != "named-session" || generation.Generation != 7 || !generation.Fallback {
25- if publication.Name != "named-session" || string(generation.Manifest) != "manifest-envelope" {
26 t.Fatalf("snapshot publication/generation = %#v", generation)44 t.Fatalf("snapshot publication/generation = %#v", generation)
27 }45 }
28- legacy := LegacySnapshot{Name: "old", Data: []byte("v3")}46+ if len(SnapshotDigest{}) != sha256.Size {
29- if legacy.Name != "old" || string(legacy.Data) != "v3" {47+ t.Fatalf("SnapshotDigest has length %d, want %d", len(SnapshotDigest{}), sha256.Size)
30- t.Fatalf("legacy snapshot = %#v", legacy)
31 }48 }
32 }49 }
33 50
@@ -39,12 +56,4 @@ func (snapshotStoreContract) Delete(name string) error { return nil
39 56
40 func TestSnapshotStoreContract(t *testing.T) {57 func TestSnapshotStoreContract(t *testing.T) {
41 var _ SnapshotStore = snapshotStoreContract{}58 var _ SnapshotStore = snapshotStoreContract{}
42-
43- blob := SnapshotBlob{Name: "named-session", Data: []byte("snapshot-bytes")}
44- if blob.Name != "named-session" {
45- t.Fatalf("SnapshotBlob.Name = %q, want %q", blob.Name, "named-session")
46- }
47- if string(blob.Data) != "snapshot-bytes" {
48- t.Fatalf("SnapshotBlob.Data = %q, want %q", string(blob.Data), "snapshot-bytes")
49- }
50 }59 }
M internal/ports/store.go +31 -23
@@ -1,5 +1,10 @@
11 package ports
22
3+import (
4+ "context"
5+ "crypto/sha256"
6+)
7+
38 // Store is a small byte-key/value persistence port.
49 //
510 // Implementations may buffer writes; Sync is the durability barrier.
@@ -30,40 +35,42 @@ type SnapshotStore interface {
3035 Delete(name string) error
3136 }
3237
33-// SnapshotDigest is the SHA-256 content address of a complete snapshot object
34-// or manifest envelope. The fixed-width value cannot be mutated by a caller.
35-type SnapshotDigest [32]byte
38+// SnapshotDigest is the SHA-256 content address of a complete snapshot object.
39+type SnapshotDigest [sha256.Size]byte
3640
37-// SnapshotObject is one complete, typed VEVO envelope and its content address.
38-// Data is caller-owned: repository implementations must not retain it after
39-// Publish returns, and Load returns a fresh copy that callers may mutate.
4041 type SnapshotObject struct {
4142 Digest SnapshotDigest
4243 Data []byte
4344 }
4445
45-// SnapshotPublication is the atomic data submitted for a named generation.
46-// Head and every object Data slice are caller-owned.
46+// SnapshotPublication atomically publishes a complete VEVM manifest and any
47+// newly reachable content-addressed VEVO objects for one named generation.
4748 type SnapshotPublication struct {
48- Name string
49- Head []byte
50- Objects []SnapshotObject
49+ Name string
50+ Generation uint64
51+ Manifest []byte
52+ Objects []SnapshotObject
5153 }
5254
53-// SnapshotGeneration is a published head together with its manifest and all
54-// reachable objects. Every byte slice is caller-owned by the recipient.
55+// SnapshotGeneration is the caller-owned material needed to restore one
56+// named generation. Fallback indicates that the repository selected an older
57+// valid generation after the requested/current generation was unavailable.
5558 type SnapshotGeneration struct {
56- Publication SnapshotPublication
57- Manifest []byte
59+ Name string
60+ Generation uint64
61+ Manifest []byte
62+ Objects map[SnapshotDigest][]byte
63+ Fallback bool
5864 }
5965
6066 // SnapshotRepository persists content-addressed incremental snapshot
61-// generations. Load returns caller-owned copies, so callers may retain or
62-// mutate all returned bytes without affecting the repository.
67+// generations. All returned bytes are owned by the caller.
6368 type SnapshotRepository interface {
64- Publish(SnapshotPublication) error
65- Load() ([]SnapshotGeneration, error)
66- Delete(name string) error
69+ Publish(context.Context, SnapshotPublication) error
70+ List(context.Context) ([]string, error)
71+ Load(context.Context, string) (SnapshotGeneration, error)
72+ Delete(context.Context, string) error
73+ Maintain(context.Context) error
6774 }
6875
6976 // LegacySnapshot is the pre-incremental named-session blob retained only for
@@ -73,8 +80,9 @@ type LegacySnapshot struct {
7380 Data []byte
7481 }
7582
76-// LegacySnapshotSource exposes v3 snapshot blobs without coupling the new
77-// repository to the legacy SnapshotStore lifecycle.
83+// LegacySnapshotSource exposes the v3 bridge separately from the new write
84+// contract.
7885 type LegacySnapshotSource interface {
79- LoadLegacySnapshots() ([]LegacySnapshot, error)
86+ LoadLegacy(context.Context) ([]LegacySnapshot, error)
87+ DeleteLegacy(context.Context, string) error
8088 }
@@ -1,5 +1,10 @@
1 package ports1 package ports
2 2
3+import (
4+ "context"
5+ "crypto/sha256"
6+)
7+
3 // Store is a small byte-key/value persistence port.8 // Store is a small byte-key/value persistence port.
4 //9 //
5 // Implementations may buffer writes; Sync is the durability barrier.10 // Implementations may buffer writes; Sync is the durability barrier.
@@ -30,40 +35,42 @@ type SnapshotStore interface {
30 Delete(name string) error35 Delete(name string) error
31 }36 }
32 37
33-// SnapshotDigest is the SHA-256 content address of a complete snapshot object38+// SnapshotDigest is the SHA-256 content address of a complete snapshot object.
34-// or manifest envelope. The fixed-width value cannot be mutated by a caller.39+type SnapshotDigest [sha256.Size]byte
35-type SnapshotDigest [32]byte
36 40
37-// SnapshotObject is one complete, typed VEVO envelope and its content address.
38-// Data is caller-owned: repository implementations must not retain it after
39-// Publish returns, and Load returns a fresh copy that callers may mutate.
40 type SnapshotObject struct {41 type SnapshotObject struct {
41 Digest SnapshotDigest42 Digest SnapshotDigest
42 Data []byte43 Data []byte
43 }44 }
44 45
45-// SnapshotPublication is the atomic data submitted for a named generation.46+// SnapshotPublication atomically publishes a complete VEVM manifest and any
46-// Head and every object Data slice are caller-owned.47+// newly reachable content-addressed VEVO objects for one named generation.
47 type SnapshotPublication struct {48 type SnapshotPublication struct {
48- Name string49+ Name string
49- Head []byte50+ Generation uint64
50- Objects []SnapshotObject51+ Manifest []byte
52+ Objects []SnapshotObject
51 }53 }
52 54
53-// SnapshotGeneration is a published head together with its manifest and all55+// SnapshotGeneration is the caller-owned material needed to restore one
54-// reachable objects. Every byte slice is caller-owned by the recipient.56+// named generation. Fallback indicates that the repository selected an older
57+// valid generation after the requested/current generation was unavailable.
55 type SnapshotGeneration struct {58 type SnapshotGeneration struct {
56- Publication SnapshotPublication59+ Name string
57- Manifest []byte60+ Generation uint64
61+ Manifest []byte
62+ Objects map[SnapshotDigest][]byte
63+ Fallback bool
58 }64 }
59 65
60 // SnapshotRepository persists content-addressed incremental snapshot66 // SnapshotRepository persists content-addressed incremental snapshot
61-// generations. Load returns caller-owned copies, so callers may retain or67+// generations. All returned bytes are owned by the caller.
62-// mutate all returned bytes without affecting the repository.
63 type SnapshotRepository interface {68 type SnapshotRepository interface {
64- Publish(SnapshotPublication) error69+ Publish(context.Context, SnapshotPublication) error
65- Load() ([]SnapshotGeneration, error)70+ List(context.Context) ([]string, error)
66- Delete(name string) error71+ Load(context.Context, string) (SnapshotGeneration, error)
72+ Delete(context.Context, string) error
73+ Maintain(context.Context) error
67 }74 }
68 75
69 // LegacySnapshot is the pre-incremental named-session blob retained only for76 // LegacySnapshot is the pre-incremental named-session blob retained only for
@@ -73,8 +80,9 @@ type LegacySnapshot struct {
73 Data []byte80 Data []byte
74 }81 }
75 82
76-// LegacySnapshotSource exposes v3 snapshot blobs without coupling the new83+// LegacySnapshotSource exposes the v3 bridge separately from the new write
77-// repository to the legacy SnapshotStore lifecycle.84+// contract.
78 type LegacySnapshotSource interface {85 type LegacySnapshotSource interface {
79- LoadLegacySnapshots() ([]LegacySnapshot, error)86+ LoadLegacy(context.Context) ([]LegacySnapshot, error)
87+ DeleteLegacy(context.Context, string) error
80 }88 }
M internal/usecase/snapshot/manifest.go +204 -581
@@ -12,73 +12,93 @@ import (
1212 )
1313
1414 const (
15- manifestMagic = "VEVM"
16- headMagic = "VEVH"
17- objectMagic = "VEVO"
18- codecVersion = uint16(1)
15+ manifestMagic = "VEVM"
16+ objectMagic = "VEVO"
17+ ManifestVersion = uint16(1)
18+ manifestHeaderSize = 16
19+ maxObjectEnvelopeSize = maxSnapshotBytes + manifestHeaderSize + 5
1920 )
2021
2122 // SnapshotDigest is the shared content-address type used by the repository.
2223 type SnapshotDigest = ports.SnapshotDigest
2324
24-// ManifestDigest returns the content address of a complete VEVM manifest.
2525 func ManifestDigest(encoded []byte) SnapshotDigest { return sha256.Sum256(encoded) }
2626
27-// ObjectRole binds a referenced object to its position in a pane snapshot.
28-type ObjectRole uint8
27+type ObjectKind uint8
2928
3029 const (
31- ObjectRoleHistory ObjectRole = iota + 1
32- ObjectRoleTail
33- ObjectRoleVisible
30+ HistoryChunk ObjectKind = iota + 1
31+ HistoryTail
32+ Visible
3433 )
3534
36-// ObjectRef is a content-addressed object used by a reference-only manifest.
3735 type ObjectRef struct {
38- Role ObjectRole
36+ Kind ObjectKind
3937 Digest SnapshotDigest
38+ Size uint32
4039 }
4140
42-// Manifest is the incremental durable representation of a named session. Its
43-// panes contain references only; terminal bytes belong exclusively in VEVO
44-// object envelopes.
45-type Manifest struct {
46- Name string
47- CreatedAt uint64
48- Active uint16
49- Tabs []Tab
41+// Head is the repository-owned generation pointer. It is deliberately not a
42+// VEVM payload; adapters produce and atomically manage HEAD later.
43+type Head struct {
44+ Generation uint64
45+ ManifestDigest SnapshotDigest
5046 }
5147
52-// Head is the atomically published name-to-manifest pointer.
53-type Head struct {
54- Name string
48+// Manifest is the complete VEVM payload carried directly in a publication.
49+type Manifest struct {
5550 Generation uint64
56- Manifest SnapshotDigest
51+ Name string
52+ CreatedAt uint64
53+ Active uint16
54+ Tabs []ManifestTab
5755 }
5856
59-// Object is a complete typed object payload before VEVO framing.
60-type Object struct {
61- Role ObjectRole
62- Data []byte
57+type ManifestTab struct {
58+ StableID string
59+ Cols uint16
60+ Rows uint16
61+ NextPaneID uint64
62+ Focus layout.PaneID
63+ Tree *layout.Tree
64+ Panes []ManifestPane
65+}
66+
67+type ManifestPane struct {
68+ ID layout.PaneID
69+ StableID string
70+ Cwd string
71+ Sealed []ObjectRef
72+ Tail ObjectRef
73+ Visible ObjectRef
74+ Process *Process
6375 }
6476
65-// MarshalManifest encodes a deterministic, strict VEVM v1 envelope.
6677 func MarshalManifest(m Manifest) ([]byte, error) {
6778 if err := validateManifest(m); err != nil {
6879 return nil, err
6980 }
7081 var w payloadWriter
71- if err := writeManifest(&w, m); err != nil {
82+ w.putUint64(m.Generation)
83+ if err := w.putString(m.Name); err != nil {
7284 return nil, err
7385 }
74- return marshalEnvelope(manifestMagic, w.b)
86+ w.putUint64(m.CreatedAt)
87+ w.putUint16(m.Active)
88+ if len(m.Tabs) > math.MaxUint16 {
89+ return nil, fmt.Errorf("%w: too many tabs", ErrInvalidData)
90+ }
91+ w.putUint16(uint16(len(m.Tabs)))
92+ for _, tab := range m.Tabs {
93+ if err := writeManifestTab(&w, tab); err != nil {
94+ return nil, err
95+ }
96+ }
97+ return marshalManifestEnvelope(manifestMagic, w.b)
7598 }
7699
77-// UnmarshalManifest validates a whole VEVM envelope before allocating its
78-// decoded graph. It rejects non-reference pane data, malformed layouts and
79-// noncanonical object roles.
80100 func UnmarshalManifest(encoded []byte) (Manifest, error) {
81- body, err := unmarshalEnvelope(encoded, manifestMagic)
101+ body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
82102 if err != nil {
83103 return Manifest{}, err
84104 }
@@ -86,241 +106,137 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) {
86106 return Manifest{}, err
87107 }
88108 r := payloadReader{b: body}
89- m, err := readManifest(&r)
109+ generation, err := r.getUint64()
90110 if err != nil {
91111 return Manifest{}, err
92112 }
93- if err := r.done(); err != nil {
113+ name, err := r.getString()
114+ if err != nil {
94115 return Manifest{}, err
95116 }
96- return m, nil
97-}
98-
99-// MarshalHead encodes a deterministic VEVH v1 publication pointer.
100-func MarshalHead(h Head) ([]byte, error) {
101- if h.Name == "" || isZeroDigest(h.Manifest) {
102- return nil, fmt.Errorf("%w: head name or manifest digest", ErrInvalidData)
103- }
104- var w payloadWriter
105- if err := w.putString(h.Name); err != nil {
106- return nil, err
107- }
108- w.putUint64(h.Generation)
109- w.b = append(w.b, h.Manifest[:]...)
110- return marshalEnvelope(headMagic, w.b)
111-}
112-
113-// UnmarshalHead decodes a complete VEVH envelope.
114-func UnmarshalHead(encoded []byte) (Head, error) {
115- body, err := unmarshalEnvelope(encoded, headMagic)
117+ createdAt, err := r.getUint64()
116118 if err != nil {
117- return Head{}, err
119+ return Manifest{}, err
118120 }
119- r := payloadReader{b: body}
120- name, err := r.getString()
121+ active, err := r.getUint16()
121122 if err != nil {
122- return Head{}, err
123+ return Manifest{}, err
123124 }
124- generation, err := r.getUint64()
125+ n, err := r.getUint16()
125126 if err != nil {
126- return Head{}, err
127+ return Manifest{}, err
127128 }
128- if len(r.b) < sha256.Size {
129- return Head{}, ErrShortPayload
129+ m := Manifest{Generation: generation, Name: name, CreatedAt: createdAt, Active: active, Tabs: make([]ManifestTab, n)}
130+ for i := range m.Tabs {
131+ if m.Tabs[i], err = readManifestTab(&r); err != nil {
132+ return Manifest{}, err
133+ }
130134 }
131- var digest SnapshotDigest
132- copy(digest[:], r.b[:sha256.Size])
133- r.b = r.b[sha256.Size:]
134135 if err := r.done(); err != nil {
135- return Head{}, err
136+ return Manifest{}, err
136137 }
137- if name == "" || isZeroDigest(digest) {
138- return Head{}, fmt.Errorf("%w: head name or manifest digest", ErrInvalidData)
138+ if err := validateManifest(m); err != nil {
139+ return Manifest{}, err
139140 }
140- return Head{Name: name, Generation: generation, Manifest: digest}, nil
141+ return m, nil
141142 }
142143
143-// MarshalObject is the sole API that creates VEVO object envelopes.
144-func MarshalObject(object Object) ([]byte, error) {
145- if !validObjectRole(object.Role) || len(object.Data) == 0 || len(object.Data) > maxSnapshotBytes {
146- return nil, fmt.Errorf("%w: object", ErrInvalidData)
144+// MarshalObject frames payload as VEVO and returns the bytes together with the
145+// SHA-256 address of those complete bytes.
146+func MarshalObject(kind ObjectKind, payload []byte) (ports.SnapshotObject, error) {
147+ if !validObjectKind(kind) || len(payload) == 0 || len(payload) > maxSnapshotBytes {
148+ return ports.SnapshotObject{}, fmt.Errorf("%w: object", ErrInvalidData)
147149 }
148150 var w payloadWriter
149- w.putUint8(uint8(object.Role))
150- w.putUint32(uint32(len(object.Data)))
151- w.b = append(w.b, object.Data...)
152- return marshalEnvelope(objectMagic, w.b)
151+ w.putUint8(uint8(kind))
152+ w.putUint32(uint32(len(payload)))
153+ w.b = append(w.b, payload...)
154+ data, err := marshalManifestEnvelope(objectMagic, w.b)
155+ if err != nil {
156+ return ports.SnapshotObject{}, err
157+ }
158+ return ports.SnapshotObject{Digest: sha256.Sum256(data), Data: data}, nil
153159 }
154160
155-// PreflightObject validates VEVO framing and all sizes without copying object
156-// bytes. Callers loading untrusted storage can use it before retaining data.
157-func PreflightObject(encoded []byte) error {
158- body, err := unmarshalEnvelope(encoded, objectMagic)
161+func UnmarshalObject(encoded []byte) (ObjectKind, []byte, error) {
162+ kind, payload, err := PreflightObject(encoded)
159163 if err != nil {
160- return err
164+ return 0, nil, err
165+ }
166+ return kind, append([]byte(nil), payload...), nil
167+}
168+
169+// PreflightObject verifies VEVO framing and declared payload size without
170+// copying; the returned payload aliases encoded and is only valid while it is.
171+func PreflightObject(encoded []byte) (ObjectKind, []byte, error) {
172+ body, err := unmarshalManifestEnvelope(encoded, objectMagic)
173+ if err != nil {
174+ return 0, nil, err
161175 }
162176 if len(body) < 5 {
163- return ErrShortPayload
177+ return 0, nil, ErrShortPayload
164178 }
165- if !validObjectRole(ObjectRole(body[0])) {
166- return fmt.Errorf("%w: object role", ErrInvalidData)
179+ kind := ObjectKind(body[0])
180+ if !validObjectKind(kind) {
181+ return 0, nil, fmt.Errorf("%w: object kind", ErrInvalidData)
167182 }
168183 n := binary.BigEndian.Uint32(body[1:5])
169184 if n == 0 || n > maxSnapshotBytes {
170- return fmt.Errorf("%w: object length", ErrInvalidData)
185+ return 0, nil, fmt.Errorf("%w: object length", ErrInvalidData)
171186 }
172187 if uint64(n) > uint64(len(body)-5) {
173- return ErrShortPayload
188+ return 0, nil, ErrShortPayload
174189 }
175190 if uint64(n) != uint64(len(body)-5) {
176- return ErrTrailingBytes
177- }
178- return nil
179-}
180-
181-// UnmarshalObject validates and returns a caller-owned object payload.
182-func UnmarshalObject(encoded []byte) (Object, error) {
183- if err := PreflightObject(encoded); err != nil {
184- return Object{}, err
191+ return 0, nil, ErrTrailingBytes
185192 }
186- body := encoded[16:]
187- return Object{Role: ObjectRole(body[0]), Data: append([]byte(nil), body[5:]...)}, nil
193+ return kind, body[5:], nil
188194 }
189195
190-func marshalEnvelope(magic string, body []byte) ([]byte, error) {
196+func marshalManifestEnvelope(magic string, body []byte) ([]byte, error) {
191197 if len(body) > maxDecodedBodySize || len(body) > math.MaxUint32 {
192198 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
193199 }
194- out := make([]byte, 16+len(body))
200+ out := make([]byte, manifestHeaderSize+len(body))
195201 copy(out[:4], magic)
196- binary.BigEndian.PutUint16(out[4:6], codecVersion)
197- // Flags [6:8] remain zero.
202+ binary.BigEndian.PutUint16(out[4:6], ManifestVersion)
198203 binary.BigEndian.PutUint32(out[8:12], uint32(len(body)))
199204 binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
200205 copy(out[16:], body)
201206 return out, nil
202207 }
203208
204-func unmarshalEnvelope(encoded []byte, expectedMagic string) ([]byte, error) {
205- if len(encoded) < 16 {
209+func unmarshalManifestEnvelope(encoded []byte, expectedMagic string) ([]byte, error) {
210+ if len(encoded) < manifestHeaderSize {
206211 return nil, ErrShortPayload
207212 }
208213 if string(encoded[:4]) != expectedMagic {
209214 return nil, ErrBadMagic
210215 }
211- if binary.BigEndian.Uint16(encoded[4:6]) != codecVersion {
216+ if binary.BigEndian.Uint16(encoded[4:6]) != ManifestVersion {
212217 return nil, ErrBadVersion
213218 }
214219 if binary.BigEndian.Uint16(encoded[6:8]) != 0 {
215220 return nil, fmt.Errorf("%w: flags", ErrInvalidData)
216221 }
217- bodyLen := binary.BigEndian.Uint32(encoded[8:12])
218- if bodyLen > maxDecodedBodySize {
222+ n := binary.BigEndian.Uint32(encoded[8:12])
223+ if n > maxDecodedBodySize {
219224 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
220225 }
221- if uint64(len(encoded)) < 16+uint64(bodyLen) {
226+ if uint64(len(encoded)) < uint64(manifestHeaderSize)+uint64(n) {
222227 return nil, ErrShortPayload
223228 }
224- if uint64(len(encoded)) != 16+uint64(bodyLen) {
229+ if uint64(len(encoded)) != uint64(manifestHeaderSize)+uint64(n) {
225230 return nil, ErrTrailingBytes
226231 }
227- body := encoded[16:]
232+ body := encoded[manifestHeaderSize:]
228233 if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(encoded[12:16]) {
229234 return nil, ErrBadCRC
230235 }
231236 return body, nil
232237 }
233238
234-func validateManifest(m Manifest) error {
235- if m.Name == "" {
236- return fmt.Errorf("%w: manifest name", ErrInvalidData)
237- }
238- if err := validateActive(m.Active, len(m.Tabs)); err != nil {
239- return err
240- }
241- for _, tab := range m.Tabs {
242- if tab.Cols == 0 || tab.Rows == 0 {
243- return fmt.Errorf("%w: dimensions", ErrInvalidData)
244- }
245- ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
246- for _, pane := range tab.Panes {
247- if pane.ID == "" {
248- return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
249- }
250- if _, exists := ids[pane.ID]; exists {
251- return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
252- }
253- if len(pane.SealedChunks) != 0 || len(pane.Tail) != 0 || len(pane.Visible) != 0 {
254- return fmt.Errorf("%w: manifest pane contains inline object data", ErrInvalidData)
255- }
256- if err := validateObjectRefs(pane.Objects); err != nil {
257- return err
258- }
259- ids[pane.ID] = struct{}{}
260- }
261- if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
262- return ErrUnknownPane
263- }
264- if err := validateTree(tab.Tree, ids); err != nil {
265- return err
266- }
267- }
268- return nil
269-}
270-
271-func validateObjectRefs(refs []ObjectRef) error {
272- if len(refs) < 2 {
273- return fmt.Errorf("%w: missing tail or visible object", ErrInvalidData)
274- }
275- seenTail := false
276- for i, ref := range refs {
277- if isZeroDigest(ref.Digest) {
278- return fmt.Errorf("%w: object digest", ErrInvalidData)
279- }
280- switch ref.Role {
281- case ObjectRoleHistory:
282- if seenTail {
283- return fmt.Errorf("%w: object role order", ErrInvalidData)
284- }
285- case ObjectRoleTail:
286- if seenTail || i+1 == len(refs) {
287- return fmt.Errorf("%w: object role order", ErrInvalidData)
288- }
289- seenTail = true
290- case ObjectRoleVisible:
291- if !seenTail || i+1 != len(refs) {
292- return fmt.Errorf("%w: object role order", ErrInvalidData)
293- }
294- return nil
295- default:
296- return fmt.Errorf("%w: object role", ErrInvalidData)
297- }
298- }
299- return fmt.Errorf("%w: missing visible object", ErrInvalidData)
300-}
301-
302-func writeManifest(w *payloadWriter, m Manifest) error {
303- if len(m.Tabs) > math.MaxUint16 {
304- return fmt.Errorf("%w: too many tabs", ErrInvalidData)
305- }
306- if err := validateActive(m.Active, len(m.Tabs)); err != nil {
307- return err
308- }
309- if err := w.putString(m.Name); err != nil {
310- return err
311- }
312- w.putUint64(m.CreatedAt)
313- w.putUint16(m.Active)
314- w.putUint16(uint16(len(m.Tabs)))
315- for _, tab := range m.Tabs {
316- if err := writeManifestTab(w, tab); err != nil {
317- return err
318- }
319- }
320- return nil
321-}
322-
323-func writeManifestTab(w *payloadWriter, tab Tab) error {
239+func writeManifestTab(w *payloadWriter, tab ManifestTab) error {
324240 if err := w.putString(tab.StableID); err != nil {
325241 return err
326242 }
@@ -348,8 +264,7 @@ func writeManifestTab(w *payloadWriter, tab Tab) error {
348264 }
349265 return nil
350266 }
351-
352-func writeManifestPane(w *payloadWriter, pane Pane) error {
267+func writeManifestPane(w *payloadWriter, pane ManifestPane) error {
353268 if err := w.putString(string(pane.ID)); err != nil {
354269 return err
355270 }
@@ -359,458 +274,166 @@ func writeManifestPane(w *payloadWriter, pane Pane) error {
359274 if err := w.putString(pane.Cwd); err != nil {
360275 return err
361276 }
362- if len(pane.Objects) > math.MaxUint16 {
363- return fmt.Errorf("%w: too many object references", ErrInvalidData)
277+ if len(pane.Sealed) > math.MaxUint16 {
278+ return fmt.Errorf("%w: too many sealed references", ErrInvalidData)
364279 }
365- w.putUint16(uint16(len(pane.Objects)))
366- for _, ref := range pane.Objects {
367- w.putUint8(uint8(ref.Role))
368- w.b = append(w.b, ref.Digest[:]...)
280+ w.putUint16(uint16(len(pane.Sealed)))
281+ for _, ref := range pane.Sealed {
282+ writeObjectRef(w, ref)
369283 }
284+ writeObjectRef(w, pane.Tail)
285+ writeObjectRef(w, pane.Visible)
370286 return writeProcess(w, pane.Process)
371287 }
372-
373-func readManifest(r *payloadReader) (Manifest, error) {
374- name, err := r.getString()
375- if err != nil {
376- return Manifest{}, err
377- }
378- created, err := r.getUint64()
379- if err != nil {
380- return Manifest{}, err
381- }
382- active, err := r.getUint16()
383- if err != nil {
384- return Manifest{}, err
385- }
386- n, err := r.getUint16()
387- if err != nil {
388- return Manifest{}, err
389- }
390- m := Manifest{Name: name, CreatedAt: created, Active: active, Tabs: make([]Tab, n)}
391- for i := range m.Tabs {
392- if m.Tabs[i], err = readManifestTab(r); err != nil {
393- return Manifest{}, err
394- }
395- }
396- return m, nil
288+func writeObjectRef(w *payloadWriter, ref ObjectRef) {
289+ w.putUint8(uint8(ref.Kind))
290+ w.b = append(w.b, ref.Digest[:]...)
291+ w.putUint32(ref.Size)
397292 }
398293
399-func readManifestTab(r *payloadReader) (Tab, error) {
294+func readManifestTab(r *payloadReader) (ManifestTab, error) {
400295 stableID, err := r.getString()
401296 if err != nil {
402- return Tab{}, err
297+ return ManifestTab{}, err
403298 }
404299 cols, err := r.getUint16()
405300 if err != nil {
406- return Tab{}, err
301+ return ManifestTab{}, err
407302 }
408303 rows, err := r.getUint16()
409304 if err != nil {
410- return Tab{}, err
305+ return ManifestTab{}, err
411306 }
412307 next, err := r.getUint64()
413308 if err != nil {
414- return Tab{}, err
309+ return ManifestTab{}, err
415310 }
416311 focus, err := r.getString()
417312 if err != nil {
418- return Tab{}, err
313+ return ManifestTab{}, err
419314 }
420315 root, err := readNode(r)
421316 if err != nil {
422- return Tab{}, err
317+ return ManifestTab{}, err
423318 }
424319 n, err := r.getUint16()
425320 if err != nil {
426- return Tab{}, err
321+ return ManifestTab{}, err
427322 }
428- tab := Tab{StableID: stableID, Cols: cols, Rows: rows, NextPaneID: next, Focus: layout.PaneID(focus), Tree: &layout.Tree{Root: root, Focus: layout.PaneID(focus)}, Panes: make([]Pane, n)}
323+ 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)}
429324 for i := range tab.Panes {
430325 if tab.Panes[i], err = readManifestPane(r); err != nil {
431- return Tab{}, err
326+ return ManifestTab{}, err
432327 }
433328 }
434329 return tab, nil
435330 }
436-
437-func readManifestPane(r *payloadReader) (Pane, error) {
331+func readManifestPane(r *payloadReader) (ManifestPane, error) {
438332 id, err := r.getString()
439333 if err != nil {
440- return Pane{}, err
334+ return ManifestPane{}, err
441335 }
442336 stableID, err := r.getString()
443337 if err != nil {
444- return Pane{}, err
338+ return ManifestPane{}, err
445339 }
446340 cwd, err := r.getString()
447341 if err != nil {
448- return Pane{}, err
342+ return ManifestPane{}, err
449343 }
450344 n, err := r.getUint16()
451345 if err != nil {
452- return Pane{}, err
453- }
454- objects := make([]ObjectRef, n)
455- for i := range objects {
456- role, err := r.getUint8()
457- if err != nil {
458- return Pane{}, err
459- }
460- if len(r.b) < sha256.Size {
461- return Pane{}, ErrShortPayload
462- }
463- objects[i].Role = ObjectRole(role)
464- copy(objects[i].Digest[:], r.b[:sha256.Size])
465- r.b = r.b[sha256.Size:]
466- }
467- process, err := readProcess(r)
468- if err != nil {
469- return Pane{}, err
470- }
471- return Pane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, Objects: objects, Process: process}, nil
472-}
473-
474-func preflightManifest(body []byte) error {
475- if len(body) < 2 {
476- return ErrShortPayload
477- }
478- if binary.BigEndian.Uint16(body[:2]) == 0 {
479- return fmt.Errorf("%w: manifest name", ErrInvalidData)
480- }
481- r := payloadReader{b: body}
482- budget := preflightBudget{}
483- if err := skipManifestString(&r, &budget); err != nil {
484- return err
485- }
486- if _, err := r.getUint64(); err != nil {
487- return err
488- }
489- active, err := r.getUint16()
490- if err != nil {
491- return err
492- }
493- tabs, err := r.getUint16()
494- if err != nil {
495- return err
496- }
497- if err := validateActive(active, int(tabs)); err != nil {
498- return err
499- }
500- if uint64(tabs) > uint64(len(r.b))/2 {
501- return ErrShortPayload
502- }
503- for range tabs {
504- if err := preflightManifestTab(&r, &budget); err != nil {
505- return err
506- }
507- }
508- if err := r.done(); err != nil {
509- return err
510- }
511- return preflightManifestReferences(body)
512-}
513-
514-func preflightManifestTab(r *payloadReader, budget *preflightBudget) error {
515- if !budget.addAllocation(256) {
516- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
517- }
518- if err := skipManifestString(r, budget); err != nil {
519- return err
520- }
521- cols, err := r.getUint16()
522- if err != nil {
523- return err
524- }
525- rows, err := r.getUint16()
526- if err != nil {
527- return err
346+ return ManifestPane{}, err
528347 }
529- if cols == 0 || rows == 0 {
530- return fmt.Errorf("%w: dimensions", ErrInvalidData)
531- }
532- if _, err := r.getUint64(); err != nil {
533- return err
534- }
535- if err := skipManifestString(r, budget); err != nil {
536- return err
537- }
538- if err := preflightManifestNode(r, 0, true, budget); err != nil {
539- return err
540- }
541- panes, err := r.getUint16()
542- if err != nil {
543- return err
544- }
545- if uint64(panes) > uint64(len(r.b))/2 {
546- return ErrShortPayload
547- }
548- for range panes {
549- if err := preflightManifestPane(r, budget); err != nil {
550- return err
551- }
552- }
553- return nil
554-}
555-
556-func preflightManifestPane(r *payloadReader, budget *preflightBudget) error {
557- if !budget.addAllocation(256) {
558- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
559- }
560- for range 3 {
561- if err := skipManifestString(r, budget); err != nil {
562- return err
348+ pane := ManifestPane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, Sealed: make([]ObjectRef, n)}
349+ for i := range pane.Sealed {
350+ if pane.Sealed[i], err = readObjectRef(r); err != nil {
351+ return ManifestPane{}, err
563352 }
564353 }
565- n, err := r.getUint16()
566- if err != nil {
567- return err
568- }
569- if uint64(n) > uint64(len(r.b))/33 || !budget.addProduct(uint64(n), 48) {
570- return fmt.Errorf("%w: object references or decoded allocation budget", ErrInvalidData)
354+ if pane.Tail, err = readObjectRef(r); err != nil {
355+ return ManifestPane{}, err
571356 }
572- for range n {
573- role, err := r.getUint8()
574- if err != nil {
575- return err
576- }
577- if !validObjectRole(ObjectRole(role)) {
578- return fmt.Errorf("%w: object role", ErrInvalidData)
579- }
580- if len(r.b) < sha256.Size {
581- return ErrShortPayload
582- }
583- if isZeroDigestBytes(r.b[:sha256.Size]) {
584- return fmt.Errorf("%w: object digest", ErrInvalidData)
585- }
586- r.b = r.b[sha256.Size:]
357+ if pane.Visible, err = readObjectRef(r); err != nil {
358+ return ManifestPane{}, err
587359 }
588- return preflightManifestProcess(r, budget)
360+ pane.Process, err = readProcess(r)
361+ return pane, err
589362 }
590-
591-func skipManifestString(r *payloadReader, budget *preflightBudget) error {
592- n, err := r.getUint16()
363+func readObjectRef(r *payloadReader) (ObjectRef, error) {
364+ kind, err := r.getUint8()
593365 if err != nil {
594- return err
595- }
596- if uint64(n) > uint64(len(r.b)) {
597- return ErrShortPayload
366+ return ObjectRef{}, err
598367 }
599- if !budget.addAllocation(uint64(n) + 16) {
600- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
368+ if len(r.b) < sha256.Size+4 {
369+ return ObjectRef{}, ErrShortPayload
601370 }
602- r.b = r.b[n:]
603- return nil
371+ var digest SnapshotDigest
372+ copy(digest[:], r.b[:sha256.Size])
373+ r.b = r.b[sha256.Size:]
374+ size := binary.BigEndian.Uint32(r.b)
375+ r.b = r.b[4:]
376+ return ObjectRef{Kind: ObjectKind(kind), Digest: digest, Size: size}, nil
604377 }
605378
606-func preflightManifestNode(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
607- if depth > 64 || !budget.addNode() {
608- return fmt.Errorf("%w: tree depth, node, or decoded allocation budget", ErrInvalidData)
379+func validateManifest(m Manifest) error {
380+ if m.Name == "" {
381+ return fmt.Errorf("%w: manifest name", ErrInvalidData)
609382 }
610- kind, err := r.getUint8()
611- if err != nil {
383+ if err := validateActive(m.Active, len(m.Tabs)); err != nil {
612384 return err
613385 }
614- switch kind {
615- case 0:
616- return skipManifestString(r, budget)
617- case 1:
618- dir, err := r.getUint8()
619- if err != nil {
620- return err
621- }
622- if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
623- return fmt.Errorf("%w: split dir", ErrInvalidData)
386+ for _, tab := range m.Tabs {
387+ if !safeDimensions(tab.Cols, tab.Rows) {
388+ return fmt.Errorf("%w: dimensions", ErrInvalidData)
624389 }
625- case 2:
626- if err := skipManifestString(r, budget); err != nil {
627- return err
390+ ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
391+ for _, pane := range tab.Panes {
392+ if pane.ID == "" {
393+ return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
394+ }
395+ if _, exists := ids[pane.ID]; exists {
396+ return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
397+ }
398+ if err := validatePaneRefs(pane); err != nil {
399+ return err
400+ }
401+ ids[pane.ID] = struct{}{}
628402 }
629- case 3:
630- if !root {
631- return fmt.Errorf("%w: nil child node", ErrInvalidData)
403+ if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
404+ return ErrUnknownPane
632405 }
633- return nil
634- default:
635- return fmt.Errorf("%w: node kind", ErrInvalidData)
636- }
637- children, err := r.getUint16()
638- if err != nil {
639- return err
640- }
641- if uint64(children) > uint64(len(r.b)) {
642- return ErrShortPayload
643- }
644- for range children {
645- if err := preflightManifestNode(r, depth+1, false, budget); err != nil {
406+ if err := validateTree(tab.Tree, ids); err != nil {
646407 return err
647408 }
648409 }
649410 return nil
650411 }
651-
652-func preflightManifestProcess(r *payloadReader, budget *preflightBudget) error {
653- present, err := r.getUint8()
654- if err != nil {
655- return err
656- }
657- if present == 0 {
658- return nil
659- }
660- if present != 1 {
661- return fmt.Errorf("%w: process presence", ErrInvalidData)
662- }
663- n, err := r.getUint16()
664- if err != nil {
665- return err
666- }
667- if n == 0 || !budget.addProduct(uint64(n), 16) {
668- return fmt.Errorf("%w: process argv or decoded allocation budget", ErrInvalidData)
669- }
670- for range n {
671- if err := skipManifestString(r, budget); err != nil {
672- return err
673- }
674- }
675- if err := skipManifestString(r, budget); err != nil {
676- return err
677- }
678- return skipManifestString(r, budget)
412+func safeDimensions(cols, rows uint16) bool {
413+ return cols != 0 && rows != 0 && uint64(cols)*uint64(rows) <= maxSnapshotCells
679414 }
680-
681-func preflightManifestReferences(body []byte) error {
682- r := payloadReader{b: body}
683- if err := skipString(&r); err != nil {
684- return err
685- }
686- if _, err := r.getUint64(); err != nil {
687- return err
688- }
689- if _, err := r.getUint16(); err != nil {
690- return err
691- }
692- tabs, err := r.getUint16()
693- if err != nil {
694- return err
695- }
696- for range tabs {
697- if err := preflightManifestTabReferences(&r); err != nil {
415+func validatePaneRefs(p ManifestPane) error {
416+ for _, ref := range p.Sealed {
417+ if err := validateObjectRef(ref, HistoryChunk); err != nil {
698418 return err
699419 }
700420 }
701- return r.done()
702-}
703-
704-func preflightManifestTabReferences(r *payloadReader) error {
705- if err := skipString(r); err != nil {
706- return err
707- }
708- if _, err := r.getUint16(); err != nil {
709- return err
710- }
711- if _, err := r.getUint16(); err != nil {
712- return err
713- }
714- if _, err := r.getUint64(); err != nil {
421+ if err := validateObjectRef(p.Tail, HistoryTail); err != nil {
715422 return err
716423 }
717- focus, err := r.getString()
718- if err != nil {
719- return err
720- }
721- references := preflightTreeReferences{}
722- if err := preflightNodeReferences(r, &references); err != nil {
723- return err
724- }
725- n, err := r.getUint16()
726- if err != nil {
727- return err
728- }
729- ids := make(map[layout.PaneID]struct{}, n)
730- for range n {
731- id, err := r.getString()
732- if err != nil {
733- return err
734- }
735- if id == "" {
736- return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
737- }
738- paneID := layout.PaneID(id)
739- if _, exists := ids[paneID]; exists {
740- return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
741- }
742- ids[paneID] = struct{}{}
743- if err := skipString(r); err != nil {
744- return err
745- }
746- if err := skipString(r); err != nil {
747- return err
748- }
749- objects, err := r.getUint16()
750- if err != nil {
751- return err
752- }
753- if err := preflightObjectOrder(r, objects); err != nil {
754- return err
755- }
756- if err := preflightProcess(r); err != nil {
757- return err
758- }
759- }
760- if focus != "" {
761- references.add(layout.PaneID(focus))
762- }
763- for _, id := range references.ids {
764- if _, ok := ids[id]; !ok {
765- return ErrUnknownPane
766- }
767- }
768- return nil
424+ return validateObjectRef(p.Visible, Visible)
769425 }
770-
771-func preflightObjectOrder(r *payloadReader, n uint16) error {
772- if n < 2 {
773- return fmt.Errorf("%w: missing tail or visible object", ErrInvalidData)
774- }
775- seenTail := false
776- for i := uint16(0); i < n; i++ {
777- role, err := r.getUint8()
778- if err != nil {
779- return err
780- }
781- if len(r.b) < sha256.Size {
782- return ErrShortPayload
783- }
784- r.b = r.b[sha256.Size:]
785- switch ObjectRole(role) {
786- case ObjectRoleHistory:
787- if seenTail {
788- return fmt.Errorf("%w: object role order", ErrInvalidData)
789- }
790- case ObjectRoleTail:
791- if seenTail || i+1 >= n {
792- return fmt.Errorf("%w: object role order", ErrInvalidData)
793- }
794- seenTail = true
795- case ObjectRoleVisible:
796- if !seenTail || i+1 != n {
797- return fmt.Errorf("%w: object role order", ErrInvalidData)
798- }
799- return nil
800- default:
801- return fmt.Errorf("%w: object role", ErrInvalidData)
802- }
426+func validateObjectRef(ref ObjectRef, want ObjectKind) error {
427+ if ref.Kind != want || ref.Size < manifestHeaderSize+5 || ref.Size > maxObjectEnvelopeSize || isZeroDigest(ref.Digest) {
428+ return fmt.Errorf("%w: object reference", ErrInvalidData)
803429 }
804- return fmt.Errorf("%w: missing visible object", ErrInvalidData)
430+ return nil
805431 }
806-
807-func validObjectRole(role ObjectRole) bool {
808- return role == ObjectRoleHistory || role == ObjectRoleTail || role == ObjectRoleVisible
432+func validObjectKind(kind ObjectKind) bool {
433+ return kind == HistoryChunk || kind == HistoryTail || kind == Visible
809434 }
810-
811-func isZeroDigest(digest SnapshotDigest) bool { return isZeroDigestBytes(digest[:]) }
812-func isZeroDigestBytes(digest []byte) bool {
813- for _, b := range digest {
435+func isZeroDigest(d SnapshotDigest) bool {
436+ for _, b := range d {
814437 if b != 0 {
815438 return false
816439 }
@@ -12,73 +12,93 @@ import (
12 )12 )
13 13
14 const (14 const (
15- manifestMagic = "VEVM"15+ manifestMagic = "VEVM"
16- headMagic = "VEVH"16+ objectMagic = "VEVO"
17- objectMagic = "VEVO"17+ ManifestVersion = uint16(1)
18- codecVersion = uint16(1)18+ manifestHeaderSize = 16
19+ maxObjectEnvelopeSize = maxSnapshotBytes + manifestHeaderSize + 5
19 )20 )
20 21
21 // SnapshotDigest is the shared content-address type used by the repository.22 // SnapshotDigest is the shared content-address type used by the repository.
22 type SnapshotDigest = ports.SnapshotDigest23 type SnapshotDigest = ports.SnapshotDigest
23 24
24-// ManifestDigest returns the content address of a complete VEVM manifest.
25 func ManifestDigest(encoded []byte) SnapshotDigest { return sha256.Sum256(encoded) }25 func ManifestDigest(encoded []byte) SnapshotDigest { return sha256.Sum256(encoded) }
26 26
27-// ObjectRole binds a referenced object to its position in a pane snapshot.27+type ObjectKind uint8
28-type ObjectRole uint8
29 28
30 const (29 const (
31- ObjectRoleHistory ObjectRole = iota + 130+ HistoryChunk ObjectKind = iota + 1
32- ObjectRoleTail31+ HistoryTail
33- ObjectRoleVisible32+ Visible
34 )33 )
35 34
36-// ObjectRef is a content-addressed object used by a reference-only manifest.
37 type ObjectRef struct {35 type ObjectRef struct {
38- Role ObjectRole36+ Kind ObjectKind
39 Digest SnapshotDigest37 Digest SnapshotDigest
38+ Size uint32
40 }39 }
41 40
42-// Manifest is the incremental durable representation of a named session. Its41+// Head is the repository-owned generation pointer. It is deliberately not a
43-// panes contain references only; terminal bytes belong exclusively in VEVO42+// VEVM payload; adapters produce and atomically manage HEAD later.
44-// object envelopes.43+type Head struct {
45-type Manifest struct {44+ Generation uint64
46- Name string45+ ManifestDigest SnapshotDigest
47- CreatedAt uint64
48- Active uint16
49- Tabs []Tab
50 }46 }
51 47
52-// Head is the atomically published name-to-manifest pointer.48+// Manifest is the complete VEVM payload carried directly in a publication.
53-type Head struct {49+type Manifest struct {
54- Name string
55 Generation uint6450 Generation uint64
56- Manifest SnapshotDigest51+ Name string
52+ CreatedAt uint64
53+ Active uint16
54+ Tabs []ManifestTab
57 }55 }
58 56
59-// Object is a complete typed object payload before VEVO framing.57+type ManifestTab struct {
60-type Object struct {58+ StableID string
61- Role ObjectRole59+ Cols uint16
62- Data []byte60+ Rows uint16
61+ NextPaneID uint64
62+ Focus layout.PaneID
63+ Tree *layout.Tree
64+ Panes []ManifestPane
65+}
66+
67+type ManifestPane struct {
68+ ID layout.PaneID
69+ StableID string
70+ Cwd string
71+ Sealed []ObjectRef
72+ Tail ObjectRef
73+ Visible ObjectRef
74+ Process *Process
63 }75 }
64 76
65-// MarshalManifest encodes a deterministic, strict VEVM v1 envelope.
66 func MarshalManifest(m Manifest) ([]byte, error) {77 func MarshalManifest(m Manifest) ([]byte, error) {
67 if err := validateManifest(m); err != nil {78 if err := validateManifest(m); err != nil {
68 return nil, err79 return nil, err
69 }80 }
70 var w payloadWriter81 var w payloadWriter
71- if err := writeManifest(&w, m); err != nil {82+ w.putUint64(m.Generation)
83+ if err := w.putString(m.Name); err != nil {
72 return nil, err84 return nil, err
73 }85 }
74- return marshalEnvelope(manifestMagic, w.b)86+ w.putUint64(m.CreatedAt)
87+ w.putUint16(m.Active)
88+ if len(m.Tabs) > math.MaxUint16 {
89+ return nil, fmt.Errorf("%w: too many tabs", ErrInvalidData)
90+ }
91+ w.putUint16(uint16(len(m.Tabs)))
92+ for _, tab := range m.Tabs {
93+ if err := writeManifestTab(&w, tab); err != nil {
94+ return nil, err
95+ }
96+ }
97+ return marshalManifestEnvelope(manifestMagic, w.b)
75 }98 }
76 99
77-// UnmarshalManifest validates a whole VEVM envelope before allocating its
78-// decoded graph. It rejects non-reference pane data, malformed layouts and
79-// noncanonical object roles.
80 func UnmarshalManifest(encoded []byte) (Manifest, error) {100 func UnmarshalManifest(encoded []byte) (Manifest, error) {
81- body, err := unmarshalEnvelope(encoded, manifestMagic)101+ body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
82 if err != nil {102 if err != nil {
83 return Manifest{}, err103 return Manifest{}, err
84 }104 }
@@ -86,241 +106,137 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) {
86 return Manifest{}, err106 return Manifest{}, err
87 }107 }
88 r := payloadReader{b: body}108 r := payloadReader{b: body}
89- m, err := readManifest(&r)109+ generation, err := r.getUint64()
90 if err != nil {110 if err != nil {
91 return Manifest{}, err111 return Manifest{}, err
92 }112 }
93- if err := r.done(); err != nil {113+ name, err := r.getString()
114+ if err != nil {
94 return Manifest{}, err115 return Manifest{}, err
95 }116 }
96- return m, nil117+ createdAt, err := r.getUint64()
97-}
98-
99-// MarshalHead encodes a deterministic VEVH v1 publication pointer.
100-func MarshalHead(h Head) ([]byte, error) {
101- if h.Name == "" || isZeroDigest(h.Manifest) {
102- return nil, fmt.Errorf("%w: head name or manifest digest", ErrInvalidData)
103- }
104- var w payloadWriter
105- if err := w.putString(h.Name); err != nil {
106- return nil, err
107- }
108- w.putUint64(h.Generation)
109- w.b = append(w.b, h.Manifest[:]...)
110- return marshalEnvelope(headMagic, w.b)
111-}
112-
113-// UnmarshalHead decodes a complete VEVH envelope.
114-func UnmarshalHead(encoded []byte) (Head, error) {
115- body, err := unmarshalEnvelope(encoded, headMagic)
116 if err != nil {118 if err != nil {
117- return Head{}, err119+ return Manifest{}, err
118 }120 }
119- r := payloadReader{b: body}121+ active, err := r.getUint16()
120- name, err := r.getString()
121 if err != nil {122 if err != nil {
122- return Head{}, err123+ return Manifest{}, err
123 }124 }
124- generation, err := r.getUint64()125+ n, err := r.getUint16()
125 if err != nil {126 if err != nil {
126- return Head{}, err127+ return Manifest{}, err
127 }128 }
128- if len(r.b) < sha256.Size {129+ m := Manifest{Generation: generation, Name: name, CreatedAt: createdAt, Active: active, Tabs: make([]ManifestTab, n)}
129- return Head{}, ErrShortPayload130+ for i := range m.Tabs {
131+ if m.Tabs[i], err = readManifestTab(&r); err != nil {
132+ return Manifest{}, err
133+ }
130 }134 }
131- var digest SnapshotDigest
132- copy(digest[:], r.b[:sha256.Size])
133- r.b = r.b[sha256.Size:]
134 if err := r.done(); err != nil {135 if err := r.done(); err != nil {
135- return Head{}, err136+ return Manifest{}, err
136 }137 }
137- if name == "" || isZeroDigest(digest) {138+ if err := validateManifest(m); err != nil {
138- return Head{}, fmt.Errorf("%w: head name or manifest digest", ErrInvalidData)139+ return Manifest{}, err
139 }140 }
140- return Head{Name: name, Generation: generation, Manifest: digest}, nil141+ return m, nil
141 }142 }
142 143
143-// MarshalObject is the sole API that creates VEVO object envelopes.144+// MarshalObject frames payload as VEVO and returns the bytes together with the
144-func MarshalObject(object Object) ([]byte, error) {145+// SHA-256 address of those complete bytes.
145- if !validObjectRole(object.Role) || len(object.Data) == 0 || len(object.Data) > maxSnapshotBytes {146+func MarshalObject(kind ObjectKind, payload []byte) (ports.SnapshotObject, error) {
146- return nil, fmt.Errorf("%w: object", ErrInvalidData)147+ if !validObjectKind(kind) || len(payload) == 0 || len(payload) > maxSnapshotBytes {
148+ return ports.SnapshotObject{}, fmt.Errorf("%w: object", ErrInvalidData)
147 }149 }
148 var w payloadWriter150 var w payloadWriter
149- w.putUint8(uint8(object.Role))151+ w.putUint8(uint8(kind))
150- w.putUint32(uint32(len(object.Data)))152+ w.putUint32(uint32(len(payload)))
151- w.b = append(w.b, object.Data...)153+ w.b = append(w.b, payload...)
152- return marshalEnvelope(objectMagic, w.b)154+ data, err := marshalManifestEnvelope(objectMagic, w.b)
155+ if err != nil {
156+ return ports.SnapshotObject{}, err
157+ }
158+ return ports.SnapshotObject{Digest: sha256.Sum256(data), Data: data}, nil
153 }159 }
154 160
155-// PreflightObject validates VEVO framing and all sizes without copying object161+func UnmarshalObject(encoded []byte) (ObjectKind, []byte, error) {
156-// bytes. Callers loading untrusted storage can use it before retaining data.162+ kind, payload, err := PreflightObject(encoded)
157-func PreflightObject(encoded []byte) error {
158- body, err := unmarshalEnvelope(encoded, objectMagic)
159 if err != nil {163 if err != nil {
160- return err164+ return 0, nil, err
165+ }
166+ return kind, append([]byte(nil), payload...), nil
167+}
168+
169+// PreflightObject verifies VEVO framing and declared payload size without
170+// copying; the returned payload aliases encoded and is only valid while it is.
171+func PreflightObject(encoded []byte) (ObjectKind, []byte, error) {
172+ body, err := unmarshalManifestEnvelope(encoded, objectMagic)
173+ if err != nil {
174+ return 0, nil, err
161 }175 }
162 if len(body) < 5 {176 if len(body) < 5 {
163- return ErrShortPayload177+ return 0, nil, ErrShortPayload
164 }178 }
165- if !validObjectRole(ObjectRole(body[0])) {179+ kind := ObjectKind(body[0])
166- return fmt.Errorf("%w: object role", ErrInvalidData)180+ if !validObjectKind(kind) {
181+ return 0, nil, fmt.Errorf("%w: object kind", ErrInvalidData)
167 }182 }
168 n := binary.BigEndian.Uint32(body[1:5])183 n := binary.BigEndian.Uint32(body[1:5])
169 if n == 0 || n > maxSnapshotBytes {184 if n == 0 || n > maxSnapshotBytes {
170- return fmt.Errorf("%w: object length", ErrInvalidData)185+ return 0, nil, fmt.Errorf("%w: object length", ErrInvalidData)
171 }186 }
172 if uint64(n) > uint64(len(body)-5) {187 if uint64(n) > uint64(len(body)-5) {
173- return ErrShortPayload188+ return 0, nil, ErrShortPayload
174 }189 }
175 if uint64(n) != uint64(len(body)-5) {190 if uint64(n) != uint64(len(body)-5) {
176- return ErrTrailingBytes191+ return 0, nil, ErrTrailingBytes
177- }
178- return nil
179-}
180-
181-// UnmarshalObject validates and returns a caller-owned object payload.
182-func UnmarshalObject(encoded []byte) (Object, error) {
183- if err := PreflightObject(encoded); err != nil {
184- return Object{}, err
185 }192 }
186- body := encoded[16:]193+ return kind, body[5:], nil
187- return Object{Role: ObjectRole(body[0]), Data: append([]byte(nil), body[5:]...)}, nil
188 }194 }
189 195
190-func marshalEnvelope(magic string, body []byte) ([]byte, error) {196+func marshalManifestEnvelope(magic string, body []byte) ([]byte, error) {
191 if len(body) > maxDecodedBodySize || len(body) > math.MaxUint32 {197 if len(body) > maxDecodedBodySize || len(body) > math.MaxUint32 {
192 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)198 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
193 }199 }
194- out := make([]byte, 16+len(body))200+ out := make([]byte, manifestHeaderSize+len(body))
195 copy(out[:4], magic)201 copy(out[:4], magic)
196- binary.BigEndian.PutUint16(out[4:6], codecVersion)202+ binary.BigEndian.PutUint16(out[4:6], ManifestVersion)
197- // Flags [6:8] remain zero.
198 binary.BigEndian.PutUint32(out[8:12], uint32(len(body)))203 binary.BigEndian.PutUint32(out[8:12], uint32(len(body)))
199 binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))204 binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
200 copy(out[16:], body)205 copy(out[16:], body)
201 return out, nil206 return out, nil
202 }207 }
203 208
204-func unmarshalEnvelope(encoded []byte, expectedMagic string) ([]byte, error) {209+func unmarshalManifestEnvelope(encoded []byte, expectedMagic string) ([]byte, error) {
205- if len(encoded) < 16 {210+ if len(encoded) < manifestHeaderSize {
206 return nil, ErrShortPayload211 return nil, ErrShortPayload
207 }212 }
208 if string(encoded[:4]) != expectedMagic {213 if string(encoded[:4]) != expectedMagic {
209 return nil, ErrBadMagic214 return nil, ErrBadMagic
210 }215 }
211- if binary.BigEndian.Uint16(encoded[4:6]) != codecVersion {216+ if binary.BigEndian.Uint16(encoded[4:6]) != ManifestVersion {
212 return nil, ErrBadVersion217 return nil, ErrBadVersion
213 }218 }
214 if binary.BigEndian.Uint16(encoded[6:8]) != 0 {219 if binary.BigEndian.Uint16(encoded[6:8]) != 0 {
215 return nil, fmt.Errorf("%w: flags", ErrInvalidData)220 return nil, fmt.Errorf("%w: flags", ErrInvalidData)
216 }221 }
217- bodyLen := binary.BigEndian.Uint32(encoded[8:12])222+ n := binary.BigEndian.Uint32(encoded[8:12])
218- if bodyLen > maxDecodedBodySize {223+ if n > maxDecodedBodySize {
219 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)224 return nil, fmt.Errorf("%w: body too large", ErrInvalidData)
220 }225 }
221- if uint64(len(encoded)) < 16+uint64(bodyLen) {226+ if uint64(len(encoded)) < uint64(manifestHeaderSize)+uint64(n) {
222 return nil, ErrShortPayload227 return nil, ErrShortPayload
223 }228 }
224- if uint64(len(encoded)) != 16+uint64(bodyLen) {229+ if uint64(len(encoded)) != uint64(manifestHeaderSize)+uint64(n) {
225 return nil, ErrTrailingBytes230 return nil, ErrTrailingBytes
226 }231 }
227- body := encoded[16:]232+ body := encoded[manifestHeaderSize:]
228 if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(encoded[12:16]) {233 if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(encoded[12:16]) {
229 return nil, ErrBadCRC234 return nil, ErrBadCRC
230 }235 }
231 return body, nil236 return body, nil
232 }237 }
233 238
234-func validateManifest(m Manifest) error {239+func writeManifestTab(w *payloadWriter, tab ManifestTab) error {
235- if m.Name == "" {
236- return fmt.Errorf("%w: manifest name", ErrInvalidData)
237- }
238- if err := validateActive(m.Active, len(m.Tabs)); err != nil {
239- return err
240- }
241- for _, tab := range m.Tabs {
242- if tab.Cols == 0 || tab.Rows == 0 {
243- return fmt.Errorf("%w: dimensions", ErrInvalidData)
244- }
245- ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
246- for _, pane := range tab.Panes {
247- if pane.ID == "" {
248- return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
249- }
250- if _, exists := ids[pane.ID]; exists {
251- return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
252- }
253- if len(pane.SealedChunks) != 0 || len(pane.Tail) != 0 || len(pane.Visible) != 0 {
254- return fmt.Errorf("%w: manifest pane contains inline object data", ErrInvalidData)
255- }
256- if err := validateObjectRefs(pane.Objects); err != nil {
257- return err
258- }
259- ids[pane.ID] = struct{}{}
260- }
261- if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
262- return ErrUnknownPane
263- }
264- if err := validateTree(tab.Tree, ids); err != nil {
265- return err
266- }
267- }
268- return nil
269-}
270-
271-func validateObjectRefs(refs []ObjectRef) error {
272- if len(refs) < 2 {
273- return fmt.Errorf("%w: missing tail or visible object", ErrInvalidData)
274- }
275- seenTail := false
276- for i, ref := range refs {
277- if isZeroDigest(ref.Digest) {
278- return fmt.Errorf("%w: object digest", ErrInvalidData)
279- }
280- switch ref.Role {
281- case ObjectRoleHistory:
282- if seenTail {
283- return fmt.Errorf("%w: object role order", ErrInvalidData)
284- }
285- case ObjectRoleTail:
286- if seenTail || i+1 == len(refs) {
287- return fmt.Errorf("%w: object role order", ErrInvalidData)
288- }
289- seenTail = true
290- case ObjectRoleVisible:
291- if !seenTail || i+1 != len(refs) {
292- return fmt.Errorf("%w: object role order", ErrInvalidData)
293- }
294- return nil
295- default:
296- return fmt.Errorf("%w: object role", ErrInvalidData)
297- }
298- }
299- return fmt.Errorf("%w: missing visible object", ErrInvalidData)
300-}
301-
302-func writeManifest(w *payloadWriter, m Manifest) error {
303- if len(m.Tabs) > math.MaxUint16 {
304- return fmt.Errorf("%w: too many tabs", ErrInvalidData)
305- }
306- if err := validateActive(m.Active, len(m.Tabs)); err != nil {
307- return err
308- }
309- if err := w.putString(m.Name); err != nil {
310- return err
311- }
312- w.putUint64(m.CreatedAt)
313- w.putUint16(m.Active)
314- w.putUint16(uint16(len(m.Tabs)))
315- for _, tab := range m.Tabs {
316- if err := writeManifestTab(w, tab); err != nil {
317- return err
318- }
319- }
320- return nil
321-}
322-
323-func writeManifestTab(w *payloadWriter, tab Tab) error {
324 if err := w.putString(tab.StableID); err != nil {240 if err := w.putString(tab.StableID); err != nil {
325 return err241 return err
326 }242 }
@@ -348,8 +264,7 @@ func writeManifestTab(w *payloadWriter, tab Tab) error {
348 }264 }
349 return nil265 return nil
350 }266 }
351-267+func writeManifestPane(w *payloadWriter, pane ManifestPane) error {
352-func writeManifestPane(w *payloadWriter, pane Pane) error {
353 if err := w.putString(string(pane.ID)); err != nil {268 if err := w.putString(string(pane.ID)); err != nil {
354 return err269 return err
355 }270 }
@@ -359,458 +274,166 @@ func writeManifestPane(w *payloadWriter, pane Pane) error {
359 if err := w.putString(pane.Cwd); err != nil {274 if err := w.putString(pane.Cwd); err != nil {
360 return err275 return err
361 }276 }
362- if len(pane.Objects) > math.MaxUint16 {277+ if len(pane.Sealed) > math.MaxUint16 {
363- return fmt.Errorf("%w: too many object references", ErrInvalidData)278+ return fmt.Errorf("%w: too many sealed references", ErrInvalidData)
364 }279 }
365- w.putUint16(uint16(len(pane.Objects)))280+ w.putUint16(uint16(len(pane.Sealed)))
366- for _, ref := range pane.Objects {281+ for _, ref := range pane.Sealed {
367- w.putUint8(uint8(ref.Role))282+ writeObjectRef(w, ref)
368- w.b = append(w.b, ref.Digest[:]...)
369 }283 }
284+ writeObjectRef(w, pane.Tail)
285+ writeObjectRef(w, pane.Visible)
370 return writeProcess(w, pane.Process)286 return writeProcess(w, pane.Process)
371 }287 }
372-288+func writeObjectRef(w *payloadWriter, ref ObjectRef) {
373-func readManifest(r *payloadReader) (Manifest, error) {289+ w.putUint8(uint8(ref.Kind))
374- name, err := r.getString()290+ w.b = append(w.b, ref.Digest[:]...)
375- if err != nil {291+ w.putUint32(ref.Size)
376- return Manifest{}, err
377- }
378- created, err := r.getUint64()
379- if err != nil {
380- return Manifest{}, err
381- }
382- active, err := r.getUint16()
383- if err != nil {
384- return Manifest{}, err
385- }
386- n, err := r.getUint16()
387- if err != nil {
388- return Manifest{}, err
389- }
390- m := Manifest{Name: name, CreatedAt: created, Active: active, Tabs: make([]Tab, n)}
391- for i := range m.Tabs {
392- if m.Tabs[i], err = readManifestTab(r); err != nil {
393- return Manifest{}, err
394- }
395- }
396- return m, nil
397 }292 }
398 293
399-func readManifestTab(r *payloadReader) (Tab, error) {294+func readManifestTab(r *payloadReader) (ManifestTab, error) {
400 stableID, err := r.getString()295 stableID, err := r.getString()
401 if err != nil {296 if err != nil {
402- return Tab{}, err297+ return ManifestTab{}, err
403 }298 }
404 cols, err := r.getUint16()299 cols, err := r.getUint16()
405 if err != nil {300 if err != nil {
406- return Tab{}, err301+ return ManifestTab{}, err
407 }302 }
408 rows, err := r.getUint16()303 rows, err := r.getUint16()
409 if err != nil {304 if err != nil {
410- return Tab{}, err305+ return ManifestTab{}, err
411 }306 }
412 next, err := r.getUint64()307 next, err := r.getUint64()
413 if err != nil {308 if err != nil {
414- return Tab{}, err309+ return ManifestTab{}, err
415 }310 }
416 focus, err := r.getString()311 focus, err := r.getString()
417 if err != nil {312 if err != nil {
418- return Tab{}, err313+ return ManifestTab{}, err
419 }314 }
420 root, err := readNode(r)315 root, err := readNode(r)
421 if err != nil {316 if err != nil {
422- return Tab{}, err317+ return ManifestTab{}, err
423 }318 }
424 n, err := r.getUint16()319 n, err := r.getUint16()
425 if err != nil {320 if err != nil {
426- return Tab{}, err321+ return ManifestTab{}, err
427 }322 }
428- tab := Tab{StableID: stableID, Cols: cols, Rows: rows, NextPaneID: next, Focus: layout.PaneID(focus), Tree: &layout.Tree{Root: root, Focus: layout.PaneID(focus)}, Panes: make([]Pane, n)}323+ 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)}
429 for i := range tab.Panes {324 for i := range tab.Panes {
430 if tab.Panes[i], err = readManifestPane(r); err != nil {325 if tab.Panes[i], err = readManifestPane(r); err != nil {
431- return Tab{}, err326+ return ManifestTab{}, err
432 }327 }
433 }328 }
434 return tab, nil329 return tab, nil
435 }330 }
436-331+func readManifestPane(r *payloadReader) (ManifestPane, error) {
437-func readManifestPane(r *payloadReader) (Pane, error) {
438 id, err := r.getString()332 id, err := r.getString()
439 if err != nil {333 if err != nil {
440- return Pane{}, err334+ return ManifestPane{}, err
441 }335 }
442 stableID, err := r.getString()336 stableID, err := r.getString()
443 if err != nil {337 if err != nil {
444- return Pane{}, err338+ return ManifestPane{}, err
445 }339 }
446 cwd, err := r.getString()340 cwd, err := r.getString()
447 if err != nil {341 if err != nil {
448- return Pane{}, err342+ return ManifestPane{}, err
449 }343 }
450 n, err := r.getUint16()344 n, err := r.getUint16()
451 if err != nil {345 if err != nil {
452- return Pane{}, err346+ return ManifestPane{}, err
453- }
454- objects := make([]ObjectRef, n)
455- for i := range objects {
456- role, err := r.getUint8()
457- if err != nil {
458- return Pane{}, err
459- }
460- if len(r.b) < sha256.Size {
461- return Pane{}, ErrShortPayload
462- }
463- objects[i].Role = ObjectRole(role)
464- copy(objects[i].Digest[:], r.b[:sha256.Size])
465- r.b = r.b[sha256.Size:]
466- }
467- process, err := readProcess(r)
468- if err != nil {
469- return Pane{}, err
470- }
471- return Pane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, Objects: objects, Process: process}, nil
472-}
473-
474-func preflightManifest(body []byte) error {
475- if len(body) < 2 {
476- return ErrShortPayload
477- }
478- if binary.BigEndian.Uint16(body[:2]) == 0 {
479- return fmt.Errorf("%w: manifest name", ErrInvalidData)
480- }
481- r := payloadReader{b: body}
482- budget := preflightBudget{}
483- if err := skipManifestString(&r, &budget); err != nil {
484- return err
485- }
486- if _, err := r.getUint64(); err != nil {
487- return err
488- }
489- active, err := r.getUint16()
490- if err != nil {
491- return err
492- }
493- tabs, err := r.getUint16()
494- if err != nil {
495- return err
496- }
497- if err := validateActive(active, int(tabs)); err != nil {
498- return err
499- }
500- if uint64(tabs) > uint64(len(r.b))/2 {
501- return ErrShortPayload
502- }
503- for range tabs {
504- if err := preflightManifestTab(&r, &budget); err != nil {
505- return err
506- }
507- }
508- if err := r.done(); err != nil {
509- return err
510- }
511- return preflightManifestReferences(body)
512-}
513-
514-func preflightManifestTab(r *payloadReader, budget *preflightBudget) error {
515- if !budget.addAllocation(256) {
516- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
517- }
518- if err := skipManifestString(r, budget); err != nil {
519- return err
520- }
521- cols, err := r.getUint16()
522- if err != nil {
523- return err
524- }
525- rows, err := r.getUint16()
526- if err != nil {
527- return err
528 }347 }
529- if cols == 0 || rows == 0 {348+ pane := ManifestPane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, Sealed: make([]ObjectRef, n)}
530- return fmt.Errorf("%w: dimensions", ErrInvalidData)349+ for i := range pane.Sealed {
531- }350+ if pane.Sealed[i], err = readObjectRef(r); err != nil {
532- if _, err := r.getUint64(); err != nil {351+ return ManifestPane{}, err
533- return err
534- }
535- if err := skipManifestString(r, budget); err != nil {
536- return err
537- }
538- if err := preflightManifestNode(r, 0, true, budget); err != nil {
539- return err
540- }
541- panes, err := r.getUint16()
542- if err != nil {
543- return err
544- }
545- if uint64(panes) > uint64(len(r.b))/2 {
546- return ErrShortPayload
547- }
548- for range panes {
549- if err := preflightManifestPane(r, budget); err != nil {
550- return err
551- }
552- }
553- return nil
554-}
555-
556-func preflightManifestPane(r *payloadReader, budget *preflightBudget) error {
557- if !budget.addAllocation(256) {
558- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
559- }
560- for range 3 {
561- if err := skipManifestString(r, budget); err != nil {
562- return err
563 }352 }
564 }353 }
565- n, err := r.getUint16()354+ if pane.Tail, err = readObjectRef(r); err != nil {
566- if err != nil {355+ return ManifestPane{}, err
567- return err
568- }
569- if uint64(n) > uint64(len(r.b))/33 || !budget.addProduct(uint64(n), 48) {
570- return fmt.Errorf("%w: object references or decoded allocation budget", ErrInvalidData)
571 }356 }
572- for range n {357+ if pane.Visible, err = readObjectRef(r); err != nil {
573- role, err := r.getUint8()358+ return ManifestPane{}, err
574- if err != nil {
575- return err
576- }
577- if !validObjectRole(ObjectRole(role)) {
578- return fmt.Errorf("%w: object role", ErrInvalidData)
579- }
580- if len(r.b) < sha256.Size {
581- return ErrShortPayload
582- }
583- if isZeroDigestBytes(r.b[:sha256.Size]) {
584- return fmt.Errorf("%w: object digest", ErrInvalidData)
585- }
586- r.b = r.b[sha256.Size:]
587 }359 }
588- return preflightManifestProcess(r, budget)360+ pane.Process, err = readProcess(r)
361+ return pane, err
589 }362 }
590-363+func readObjectRef(r *payloadReader) (ObjectRef, error) {
591-func skipManifestString(r *payloadReader, budget *preflightBudget) error {364+ kind, err := r.getUint8()
592- n, err := r.getUint16()
593 if err != nil {365 if err != nil {
594- return err366+ return ObjectRef{}, err
595- }
596- if uint64(n) > uint64(len(r.b)) {
597- return ErrShortPayload
598 }367 }
599- if !budget.addAllocation(uint64(n) + 16) {368+ if len(r.b) < sha256.Size+4 {
600- return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)369+ return ObjectRef{}, ErrShortPayload
601 }370 }
602- r.b = r.b[n:]371+ var digest SnapshotDigest
603- return nil372+ copy(digest[:], r.b[:sha256.Size])
373+ r.b = r.b[sha256.Size:]
374+ size := binary.BigEndian.Uint32(r.b)
375+ r.b = r.b[4:]
376+ return ObjectRef{Kind: ObjectKind(kind), Digest: digest, Size: size}, nil
604 }377 }
605 378
606-func preflightManifestNode(r *payloadReader, depth int, root bool, budget *preflightBudget) error {379+func validateManifest(m Manifest) error {
607- if depth > 64 || !budget.addNode() {380+ if m.Name == "" {
608- return fmt.Errorf("%w: tree depth, node, or decoded allocation budget", ErrInvalidData)381+ return fmt.Errorf("%w: manifest name", ErrInvalidData)
609 }382 }
610- kind, err := r.getUint8()383+ if err := validateActive(m.Active, len(m.Tabs)); err != nil {
611- if err != nil {
612 return err384 return err
613 }385 }
614- switch kind {386+ for _, tab := range m.Tabs {
615- case 0:387+ if !safeDimensions(tab.Cols, tab.Rows) {
616- return skipManifestString(r, budget)388+ return fmt.Errorf("%w: dimensions", ErrInvalidData)
617- case 1:
618- dir, err := r.getUint8()
619- if err != nil {
620- return err
621- }
622- if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
623- return fmt.Errorf("%w: split dir", ErrInvalidData)
624 }389 }
625- case 2:390+ ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
626- if err := skipManifestString(r, budget); err != nil {391+ for _, pane := range tab.Panes {
627- return err392+ if pane.ID == "" {
393+ return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
394+ }
395+ if _, exists := ids[pane.ID]; exists {
396+ return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
397+ }
398+ if err := validatePaneRefs(pane); err != nil {
399+ return err
400+ }
401+ ids[pane.ID] = struct{}{}
628 }402 }
629- case 3:403+ if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
630- if !root {404+ return ErrUnknownPane
631- return fmt.Errorf("%w: nil child node", ErrInvalidData)
632 }405 }
633- return nil406+ if err := validateTree(tab.Tree, ids); err != nil {
634- default:
635- return fmt.Errorf("%w: node kind", ErrInvalidData)
636- }
637- children, err := r.getUint16()
638- if err != nil {
639- return err
640- }
641- if uint64(children) > uint64(len(r.b)) {
642- return ErrShortPayload
643- }
644- for range children {
645- if err := preflightManifestNode(r, depth+1, false, budget); err != nil {
646 return err407 return err
647 }408 }
648 }409 }
649 return nil410 return nil
650 }411 }
651-412+func safeDimensions(cols, rows uint16) bool {
652-func preflightManifestProcess(r *payloadReader, budget *preflightBudget) error {413+ return cols != 0 && rows != 0 && uint64(cols)*uint64(rows) <= maxSnapshotCells
653- present, err := r.getUint8()
654- if err != nil {
655- return err
656- }
657- if present == 0 {
658- return nil
659- }
660- if present != 1 {
661- return fmt.Errorf("%w: process presence", ErrInvalidData)
662- }
663- n, err := r.getUint16()
664- if err != nil {
665- return err
666- }
667- if n == 0 || !budget.addProduct(uint64(n), 16) {
668- return fmt.Errorf("%w: process argv or decoded allocation budget", ErrInvalidData)
669- }
670- for range n {
671- if err := skipManifestString(r, budget); err != nil {
672- return err
673- }
674- }
675- if err := skipManifestString(r, budget); err != nil {
676- return err
677- }
678- return skipManifestString(r, budget)
679 }414 }
680-415+func validatePaneRefs(p ManifestPane) error {
681-func preflightManifestReferences(body []byte) error {416+ for _, ref := range p.Sealed {
682- r := payloadReader{b: body}417+ if err := validateObjectRef(ref, HistoryChunk); err != nil {
683- if err := skipString(&r); err != nil {
684- return err
685- }
686- if _, err := r.getUint64(); err != nil {
687- return err
688- }
689- if _, err := r.getUint16(); err != nil {
690- return err
691- }
692- tabs, err := r.getUint16()
693- if err != nil {
694- return err
695- }
696- for range tabs {
697- if err := preflightManifestTabReferences(&r); err != nil {
698 return err418 return err
699 }419 }
700 }420 }
701- return r.done()421+ if err := validateObjectRef(p.Tail, HistoryTail); err != nil {
702-}
703-
704-func preflightManifestTabReferences(r *payloadReader) error {
705- if err := skipString(r); err != nil {
706- return err
707- }
708- if _, err := r.getUint16(); err != nil {
709- return err
710- }
711- if _, err := r.getUint16(); err != nil {
712- return err
713- }
714- if _, err := r.getUint64(); err != nil {
715 return err422 return err
716 }423 }
717- focus, err := r.getString()424+ return validateObjectRef(p.Visible, Visible)
718- if err != nil {
719- return err
720- }
721- references := preflightTreeReferences{}
722- if err := preflightNodeReferences(r, &references); err != nil {
723- return err
724- }
725- n, err := r.getUint16()
726- if err != nil {
727- return err
728- }
729- ids := make(map[layout.PaneID]struct{}, n)
730- for range n {
731- id, err := r.getString()
732- if err != nil {
733- return err
734- }
735- if id == "" {
736- return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
737- }
738- paneID := layout.PaneID(id)
739- if _, exists := ids[paneID]; exists {
740- return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
741- }
742- ids[paneID] = struct{}{}
743- if err := skipString(r); err != nil {
744- return err
745- }
746- if err := skipString(r); err != nil {
747- return err
748- }
749- objects, err := r.getUint16()
750- if err != nil {
751- return err
752- }
753- if err := preflightObjectOrder(r, objects); err != nil {
754- return err
755- }
756- if err := preflightProcess(r); err != nil {
757- return err
758- }
759- }
760- if focus != "" {
761- references.add(layout.PaneID(focus))
762- }
763- for _, id := range references.ids {
764- if _, ok := ids[id]; !ok {
765- return ErrUnknownPane
766- }
767- }
768- return nil
769 }425 }
770-426+func validateObjectRef(ref ObjectRef, want ObjectKind) error {
771-func preflightObjectOrder(r *payloadReader, n uint16) error {427+ if ref.Kind != want || ref.Size < manifestHeaderSize+5 || ref.Size > maxObjectEnvelopeSize || isZeroDigest(ref.Digest) {
772- if n < 2 {428+ return fmt.Errorf("%w: object reference", ErrInvalidData)
773- return fmt.Errorf("%w: missing tail or visible object", ErrInvalidData)
774- }
775- seenTail := false
776- for i := uint16(0); i < n; i++ {
777- role, err := r.getUint8()
778- if err != nil {
779- return err
780- }
781- if len(r.b) < sha256.Size {
782- return ErrShortPayload
783- }
784- r.b = r.b[sha256.Size:]
785- switch ObjectRole(role) {
786- case ObjectRoleHistory:
787- if seenTail {
788- return fmt.Errorf("%w: object role order", ErrInvalidData)
789- }
790- case ObjectRoleTail:
791- if seenTail || i+1 >= n {
792- return fmt.Errorf("%w: object role order", ErrInvalidData)
793- }
794- seenTail = true
795- case ObjectRoleVisible:
796- if !seenTail || i+1 != n {
797- return fmt.Errorf("%w: object role order", ErrInvalidData)
798- }
799- return nil
800- default:
801- return fmt.Errorf("%w: object role", ErrInvalidData)
802- }
803 }429 }
804- return fmt.Errorf("%w: missing visible object", ErrInvalidData)430+ return nil
805 }431 }
806-432+func validObjectKind(kind ObjectKind) bool {
807-func validObjectRole(role ObjectRole) bool {433+ return kind == HistoryChunk || kind == HistoryTail || kind == Visible
808- return role == ObjectRoleHistory || role == ObjectRoleTail || role == ObjectRoleVisible
809 }434 }
810-435+func isZeroDigest(d SnapshotDigest) bool {
811-func isZeroDigest(digest SnapshotDigest) bool { return isZeroDigestBytes(digest[:]) }436+ for _, b := range d {
812-func isZeroDigestBytes(digest []byte) bool {
813- for _, b := range digest {
814 if b != 0 {437 if b != 0 {
815 return false438 return false
816 }439 }
A internal/usecase/snapshot/manifest_preflight.go +286 -0
@@ -0,0 +1,286 @@
1+package snapshot
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/vev/internal/usecase/layout"
7+)
8+
9+// preflightManifest rejects hostile declarations before UnmarshalManifest
10+// allocates tab, pane, string, or reference slices.
11+func preflightManifest(body []byte) error {
12+ r := payloadReader{b: body}
13+ budget := manifestPreflightBudget{}
14+ if _, err := r.getUint64(); err != nil {
15+ return err
16+ }
17+ name, err := preflightString(&r, &budget)
18+ if err != nil {
19+ return err
20+ }
21+ if name == "" {
22+ return fmt.Errorf("%w: manifest name", ErrInvalidData)
23+ }
24+ if _, err := r.getUint64(); err != nil {
25+ return err
26+ }
27+ active, err := r.getUint16()
28+ if err != nil {
29+ return err
30+ }
31+ tabs, err := r.getUint16()
32+ if err != nil {
33+ return err
34+ }
35+ if err := validateActive(active, int(tabs)); err != nil {
36+ return err
37+ }
38+ if uint64(tabs) > uint64(len(r.b))/2 || !budget.add(uint64(tabs)*128) {
39+ return fmt.Errorf("%w: tab allocation", ErrInvalidData)
40+ }
41+ for range tabs {
42+ if err := preflightManifestTab(&r, &budget); err != nil {
43+ return err
44+ }
45+ }
46+ return r.done()
47+}
48+
49+type manifestPreflightBudget struct{ allocation uint64 }
50+
51+func (b *manifestPreflightBudget) add(n uint64) bool {
52+ if n > maxSnapshotDecodedAllocation-b.allocation {
53+ return false
54+ }
55+ b.allocation += n
56+ return true
57+}
58+
59+func preflightString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {
60+ n, err := r.getUint16()
61+ if err != nil {
62+ return "", err
63+ }
64+ if uint64(n) > uint64(len(r.b)) {
65+ return "", ErrShortPayload
66+ }
67+ if !budget.add(uint64(n) + 16) {
68+ return "", fmt.Errorf("%w: string allocation", ErrInvalidData)
69+ }
70+ s := string(r.b[:n])
71+ r.b = r.b[n:]
72+ return s, nil
73+}
74+
75+func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) error {
76+ if _, err := preflightString(r, budget); err != nil {
77+ return err
78+ }
79+ cols, err := r.getUint16()
80+ if err != nil {
81+ return err
82+ }
83+ rows, err := r.getUint16()
84+ if err != nil {
85+ return err
86+ }
87+ if !safeDimensions(cols, rows) {
88+ return fmt.Errorf("%w: dimensions", ErrInvalidData)
89+ }
90+ if _, err := r.getUint64(); err != nil {
91+ return err
92+ }
93+ focus, err := preflightString(r, budget)
94+ if err != nil {
95+ return err
96+ }
97+ references := make([]layout.PaneID, 0, 8)
98+ if err := preflightManifestNode(r, 0, true, budget, &references); err != nil {
99+ return err
100+ }
101+ panes, err := r.getUint16()
102+ if err != nil {
103+ return err
104+ }
105+ if uint64(panes) > uint64(len(r.b))/2 || !budget.add(uint64(panes)*256) {
106+ return fmt.Errorf("%w: pane allocation", ErrInvalidData)
107+ }
108+ ids := make(map[layout.PaneID]struct{}, panes)
109+ for range panes {
110+ id, err := preflightManifestPane(r, budget)
111+ if err != nil {
112+ return err
113+ }
114+ if id == "" {
115+ return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
116+ }
117+ if _, duplicate := ids[id]; duplicate {
118+ return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
119+ }
120+ ids[id] = struct{}{}
121+ }
122+ if focus != "" {
123+ references = append(references, layout.PaneID(focus))
124+ }
125+ for _, id := range references {
126+ if _, ok := ids[id]; !ok {
127+ return ErrUnknownPane
128+ }
129+ }
130+ return nil
131+}
132+
133+func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manifestPreflightBudget, refs *[]layout.PaneID) error {
134+ if depth > 64 || !budget.add(128) {
135+ return fmt.Errorf("%w: tree resource declaration", ErrInvalidData)
136+ }
137+ kind, err := r.getUint8()
138+ if err != nil {
139+ return err
140+ }
141+ switch kind {
142+ case 0:
143+ leaf, err := preflightString(r, budget)
144+ if err != nil {
145+ return err
146+ }
147+ *refs = append(*refs, layout.PaneID(leaf))
148+ return nil
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+ expanded, err := preflightString(r, budget)
159+ if err != nil {
160+ return err
161+ }
162+ if expanded != "" {
163+ *refs = append(*refs, layout.PaneID(expanded))
164+ }
165+ case 3:
166+ if !root {
167+ return fmt.Errorf("%w: nil child node", ErrInvalidData)
168+ }
169+ return nil
170+ default:
171+ return fmt.Errorf("%w: node kind", ErrInvalidData)
172+ }
173+ n, err := r.getUint16()
174+ if err != nil {
175+ return err
176+ }
177+ if uint64(n) > uint64(len(r.b)) {
178+ return ErrShortPayload
179+ }
180+ for range n {
181+ if err := preflightManifestNode(r, depth+1, false, budget, refs); err != nil {
182+ return err
183+ }
184+ }
185+ return nil
186+}
187+
188+func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
189+ id, err := preflightString(r, budget)
190+ if err != nil {
191+ return "", err
192+ }
193+ if _, err := preflightString(r, budget); err != nil {
194+ return "", err
195+ }
196+ if _, err := preflightString(r, budget); err != nil {
197+ return "", err
198+ }
199+ sealed, err := r.getUint16()
200+ if err != nil {
201+ return "", err
202+ }
203+ if uint64(sealed) > uint64(len(r.b))/(sha256Size+5) || !budget.add(uint64(sealed)*48) {
204+ return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData)
205+ }
206+ for range sealed {
207+ if err := preflightObjectRef(r, HistoryChunk); err != nil {
208+ return "", err
209+ }
210+ }
211+ if err := preflightObjectRef(r, HistoryTail); err != nil {
212+ return "", err
213+ }
214+ if err := preflightObjectRef(r, Visible); err != nil {
215+ return "", err
216+ }
217+ if err := preflightManifestProcess(r, budget); err != nil {
218+ return "", err
219+ }
220+ return layout.PaneID(id), nil
221+}
222+
223+const sha256Size = 32
224+
225+func preflightObjectRef(r *payloadReader, want ObjectKind) error {
226+ kind, err := r.getUint8()
227+ if err != nil {
228+ return err
229+ }
230+ if ObjectKind(kind) != want || len(r.b) < sha256Size+4 {
231+ if len(r.b) < sha256Size+4 {
232+ return ErrShortPayload
233+ }
234+ return fmt.Errorf("%w: object kind", ErrInvalidData)
235+ }
236+ digest := r.b[:sha256Size]
237+ r.b = r.b[sha256Size:]
238+ if isZeroDigestBytes(digest) {
239+ return fmt.Errorf("%w: object digest", ErrInvalidData)
240+ }
241+ size, err := r.getUint32()
242+ if err != nil {
243+ return err
244+ }
245+ if size < manifestHeaderSize+5 || size > maxObjectEnvelopeSize {
246+ return fmt.Errorf("%w: object size", ErrInvalidData)
247+ }
248+ return nil
249+}
250+func isZeroDigestBytes(d []byte) bool {
251+ for _, b := range d {
252+ if b != 0 {
253+ return false
254+ }
255+ }
256+ return true
257+}
258+func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error {
259+ present, err := r.getUint8()
260+ if err != nil {
261+ return err
262+ }
263+ if present == 0 {
264+ return nil
265+ }
266+ if present != 1 {
267+ return fmt.Errorf("%w: process presence", ErrInvalidData)
268+ }
269+ n, err := r.getUint16()
270+ if err != nil {
271+ return err
272+ }
273+ if n == 0 || !budget.add(uint64(n)*16) {
274+ return fmt.Errorf("%w: process argv", ErrInvalidData)
275+ }
276+ for range n {
277+ if _, err := preflightString(r, budget); err != nil {
278+ return err
279+ }
280+ }
281+ if _, err := preflightString(r, budget); err != nil {
282+ return err
283+ }
284+ _, err = preflightString(r, budget)
285+ return err
286+}
@@ -0,0 +1,286 @@
1+package snapshot
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/vev/internal/usecase/layout"
7+)
8+
9+// preflightManifest rejects hostile declarations before UnmarshalManifest
10+// allocates tab, pane, string, or reference slices.
11+func preflightManifest(body []byte) error {
12+ r := payloadReader{b: body}
13+ budget := manifestPreflightBudget{}
14+ if _, err := r.getUint64(); err != nil {
15+ return err
16+ }
17+ name, err := preflightString(&r, &budget)
18+ if err != nil {
19+ return err
20+ }
21+ if name == "" {
22+ return fmt.Errorf("%w: manifest name", ErrInvalidData)
23+ }
24+ if _, err := r.getUint64(); err != nil {
25+ return err
26+ }
27+ active, err := r.getUint16()
28+ if err != nil {
29+ return err
30+ }
31+ tabs, err := r.getUint16()
32+ if err != nil {
33+ return err
34+ }
35+ if err := validateActive(active, int(tabs)); err != nil {
36+ return err
37+ }
38+ if uint64(tabs) > uint64(len(r.b))/2 || !budget.add(uint64(tabs)*128) {
39+ return fmt.Errorf("%w: tab allocation", ErrInvalidData)
40+ }
41+ for range tabs {
42+ if err := preflightManifestTab(&r, &budget); err != nil {
43+ return err
44+ }
45+ }
46+ return r.done()
47+}
48+
49+type manifestPreflightBudget struct{ allocation uint64 }
50+
51+func (b *manifestPreflightBudget) add(n uint64) bool {
52+ if n > maxSnapshotDecodedAllocation-b.allocation {
53+ return false
54+ }
55+ b.allocation += n
56+ return true
57+}
58+
59+func preflightString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {
60+ n, err := r.getUint16()
61+ if err != nil {
62+ return "", err
63+ }
64+ if uint64(n) > uint64(len(r.b)) {
65+ return "", ErrShortPayload
66+ }
67+ if !budget.add(uint64(n) + 16) {
68+ return "", fmt.Errorf("%w: string allocation", ErrInvalidData)
69+ }
70+ s := string(r.b[:n])
71+ r.b = r.b[n:]
72+ return s, nil
73+}
74+
75+func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) error {
76+ if _, err := preflightString(r, budget); err != nil {
77+ return err
78+ }
79+ cols, err := r.getUint16()
80+ if err != nil {
81+ return err
82+ }
83+ rows, err := r.getUint16()
84+ if err != nil {
85+ return err
86+ }
87+ if !safeDimensions(cols, rows) {
88+ return fmt.Errorf("%w: dimensions", ErrInvalidData)
89+ }
90+ if _, err := r.getUint64(); err != nil {
91+ return err
92+ }
93+ focus, err := preflightString(r, budget)
94+ if err != nil {
95+ return err
96+ }
97+ references := make([]layout.PaneID, 0, 8)
98+ if err := preflightManifestNode(r, 0, true, budget, &references); err != nil {
99+ return err
100+ }
101+ panes, err := r.getUint16()
102+ if err != nil {
103+ return err
104+ }
105+ if uint64(panes) > uint64(len(r.b))/2 || !budget.add(uint64(panes)*256) {
106+ return fmt.Errorf("%w: pane allocation", ErrInvalidData)
107+ }
108+ ids := make(map[layout.PaneID]struct{}, panes)
109+ for range panes {
110+ id, err := preflightManifestPane(r, budget)
111+ if err != nil {
112+ return err
113+ }
114+ if id == "" {
115+ return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
116+ }
117+ if _, duplicate := ids[id]; duplicate {
118+ return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
119+ }
120+ ids[id] = struct{}{}
121+ }
122+ if focus != "" {
123+ references = append(references, layout.PaneID(focus))
124+ }
125+ for _, id := range references {
126+ if _, ok := ids[id]; !ok {
127+ return ErrUnknownPane
128+ }
129+ }
130+ return nil
131+}
132+
133+func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manifestPreflightBudget, refs *[]layout.PaneID) error {
134+ if depth > 64 || !budget.add(128) {
135+ return fmt.Errorf("%w: tree resource declaration", ErrInvalidData)
136+ }
137+ kind, err := r.getUint8()
138+ if err != nil {
139+ return err
140+ }
141+ switch kind {
142+ case 0:
143+ leaf, err := preflightString(r, budget)
144+ if err != nil {
145+ return err
146+ }
147+ *refs = append(*refs, layout.PaneID(leaf))
148+ return nil
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+ expanded, err := preflightString(r, budget)
159+ if err != nil {
160+ return err
161+ }
162+ if expanded != "" {
163+ *refs = append(*refs, layout.PaneID(expanded))
164+ }
165+ case 3:
166+ if !root {
167+ return fmt.Errorf("%w: nil child node", ErrInvalidData)
168+ }
169+ return nil
170+ default:
171+ return fmt.Errorf("%w: node kind", ErrInvalidData)
172+ }
173+ n, err := r.getUint16()
174+ if err != nil {
175+ return err
176+ }
177+ if uint64(n) > uint64(len(r.b)) {
178+ return ErrShortPayload
179+ }
180+ for range n {
181+ if err := preflightManifestNode(r, depth+1, false, budget, refs); err != nil {
182+ return err
183+ }
184+ }
185+ return nil
186+}
187+
188+func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
189+ id, err := preflightString(r, budget)
190+ if err != nil {
191+ return "", err
192+ }
193+ if _, err := preflightString(r, budget); err != nil {
194+ return "", err
195+ }
196+ if _, err := preflightString(r, budget); err != nil {
197+ return "", err
198+ }
199+ sealed, err := r.getUint16()
200+ if err != nil {
201+ return "", err
202+ }
203+ if uint64(sealed) > uint64(len(r.b))/(sha256Size+5) || !budget.add(uint64(sealed)*48) {
204+ return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData)
205+ }
206+ for range sealed {
207+ if err := preflightObjectRef(r, HistoryChunk); err != nil {
208+ return "", err
209+ }
210+ }
211+ if err := preflightObjectRef(r, HistoryTail); err != nil {
212+ return "", err
213+ }
214+ if err := preflightObjectRef(r, Visible); err != nil {
215+ return "", err
216+ }
217+ if err := preflightManifestProcess(r, budget); err != nil {
218+ return "", err
219+ }
220+ return layout.PaneID(id), nil
221+}
222+
223+const sha256Size = 32
224+
225+func preflightObjectRef(r *payloadReader, want ObjectKind) error {
226+ kind, err := r.getUint8()
227+ if err != nil {
228+ return err
229+ }
230+ if ObjectKind(kind) != want || len(r.b) < sha256Size+4 {
231+ if len(r.b) < sha256Size+4 {
232+ return ErrShortPayload
233+ }
234+ return fmt.Errorf("%w: object kind", ErrInvalidData)
235+ }
236+ digest := r.b[:sha256Size]
237+ r.b = r.b[sha256Size:]
238+ if isZeroDigestBytes(digest) {
239+ return fmt.Errorf("%w: object digest", ErrInvalidData)
240+ }
241+ size, err := r.getUint32()
242+ if err != nil {
243+ return err
244+ }
245+ if size < manifestHeaderSize+5 || size > maxObjectEnvelopeSize {
246+ return fmt.Errorf("%w: object size", ErrInvalidData)
247+ }
248+ return nil
249+}
250+func isZeroDigestBytes(d []byte) bool {
251+ for _, b := range d {
252+ if b != 0 {
253+ return false
254+ }
255+ }
256+ return true
257+}
258+func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error {
259+ present, err := r.getUint8()
260+ if err != nil {
261+ return err
262+ }
263+ if present == 0 {
264+ return nil
265+ }
266+ if present != 1 {
267+ return fmt.Errorf("%w: process presence", ErrInvalidData)
268+ }
269+ n, err := r.getUint16()
270+ if err != nil {
271+ return err
272+ }
273+ if n == 0 || !budget.add(uint64(n)*16) {
274+ return fmt.Errorf("%w: process argv", ErrInvalidData)
275+ }
276+ for range n {
277+ if _, err := preflightString(r, budget); err != nil {
278+ return err
279+ }
280+ }
281+ if _, err := preflightString(r, budget); err != nil {
282+ return err
283+ }
284+ _, err = preflightString(r, budget)
285+ return err
286+}
M internal/usecase/snapshot/manifest_test.go +65 -65
@@ -2,45 +2,46 @@ package snapshot
22
33 import (
44 "bytes"
5+ "crypto/sha256"
56 "errors"
7+ "math"
68 "testing"
79
10+ "github.com/bnema/vev/internal/ports"
811 "github.com/bnema/vev/internal/usecase/layout"
912 )
1013
11-func TestManifestAndHeadCodecsAreDeterministicAndStrict(t *testing.T) {
12- digest := SnapshotDigest{1}
13- manifest := Manifest{
14- Name: "named",
15- CreatedAt: 9,
16- Active: 0,
17- Tabs: []Tab{{
18- StableID: "tab", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "pane",
19- Tree: layout.NewTree("pane"),
20- Panes: []Pane{{ID: "pane", StableID: "pane-stable", Cwd: "/tmp", Objects: []ObjectRef{
21- {Role: ObjectRoleHistory, Digest: digest},
22- {Role: ObjectRoleTail, Digest: SnapshotDigest{2}},
23- {Role: ObjectRoleVisible, Digest: SnapshotDigest{3}},
24- }}},
25- }},
14+func TestManifestCodecUsesGenerationAndOrderedTypedReferences(t *testing.T) {
15+ chunk, err := MarshalObject(HistoryChunk, []byte("history"))
16+ if err != nil {
17+ t.Fatal(err)
2618 }
27- encoded, err := MarshalManifest(manifest)
19+ tail, err := MarshalObject(HistoryTail, []byte("tail"))
2820 if err != nil {
2921 t.Fatal(err)
3022 }
31- encodedAgain, err := MarshalManifest(manifest)
23+ visible, err := MarshalObject(Visible, []byte("visible"))
3224 if err != nil {
3325 t.Fatal(err)
3426 }
35- if !bytes.Equal(encoded, encodedAgain) {
36- t.Fatal("manifest encoding is not deterministic")
27+ manifest := Manifest{Generation: 7, Name: "named", CreatedAt: 9, Active: 0, Tabs: []ManifestTab{{
28+ StableID: "tab", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "pane", Tree: layout.NewTree("pane"),
29+ Panes: []ManifestPane{{ID: "pane", StableID: "pane-stable", Cwd: "/tmp",
30+ Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: chunk.Digest, Size: uint32(len(chunk.Data))}},
31+ Tail: ObjectRef{Kind: HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))},
32+ Visible: ObjectRef{Kind: Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))},
33+ }},
34+ }}}
35+ encoded, err := MarshalManifest(manifest)
36+ if err != nil {
37+ t.Fatal(err)
3738 }
3839 got, err := UnmarshalManifest(encoded)
3940 if err != nil {
4041 t.Fatal(err)
4142 }
42- if got.Name != manifest.Name || len(got.Tabs) != 1 || len(got.Tabs[0].Panes[0].Objects) != 3 {
43- t.Fatalf("manifest round trip = %#v", got)
43+ if got.Generation != manifest.Generation || got.Tabs[0].Panes[0].Tail != manifest.Tabs[0].Panes[0].Tail {
44+ t.Fatalf("round trip = %#v", got)
4445 }
4546 for n := range len(encoded) {
4647 if _, err := UnmarshalManifest(encoded[:n]); err == nil {
@@ -51,68 +52,67 @@ func TestManifestAndHeadCodecsAreDeterministicAndStrict(t *testing.T) {
5152 t.Fatalf("trailing data error = %v", err)
5253 }
5354
54- head := Head{Name: "named", Generation: 7, Manifest: ManifestDigest(encoded)}
55- headBytes, err := MarshalHead(head)
56- if err != nil {
57- t.Fatal(err)
58- }
59- if got, err := UnmarshalHead(headBytes); err != nil || got != head {
60- t.Fatalf("head round trip = %#v, %v", got, err)
61- }
62- for n := range len(headBytes) {
63- if _, err := UnmarshalHead(headBytes[:n]); err == nil {
64- t.Fatalf("head prefix %d accepted", n)
65- }
66- }
67- if _, err := UnmarshalHead(append(headBytes, 0)); !errors.Is(err, ErrTrailingBytes) {
68- t.Fatalf("head trailing data error = %v", err)
55+ head := Head{Generation: 7, ManifestDigest: sha256.Sum256(encoded)}
56+ if head.Generation != manifest.Generation || head.ManifestDigest != ManifestDigest(encoded) {
57+ t.Fatalf("head = %#v", head)
6958 }
7059 }
7160
72-func TestObjectCodecIsTypedAndStrict(t *testing.T) {
73- object := Object{Role: ObjectRoleVisible, Data: []byte("canonical-vt")}
74- encoded, err := MarshalObject(object)
61+func TestObjectCodecReturnsContentAddressedObjectAndPayload(t *testing.T) {
62+ object, err := MarshalObject(Visible, []byte("canonical-vt"))
7563 if err != nil {
7664 t.Fatal(err)
7765 }
78- got, err := UnmarshalObject(encoded)
79- if err != nil || got.Role != object.Role || !bytes.Equal(got.Data, object.Data) {
80- t.Fatalf("object round trip = %#v, %v", got, err)
66+ if object.Digest != sha256.Sum256(object.Data) {
67+ t.Fatalf("digest does not address VEVO bytes")
8168 }
82- if err := PreflightObject(encoded); err != nil {
83- t.Fatal(err)
69+ kind, payload, err := UnmarshalObject(object.Data)
70+ if err != nil || kind != Visible || !bytes.Equal(payload, []byte("canonical-vt")) {
71+ t.Fatalf("round trip = %v %q %v", kind, payload, err)
8472 }
85- for n := range len(encoded) {
86- if _, err := UnmarshalObject(encoded[:n]); err == nil {
87- t.Fatalf("object prefix %d accepted", n)
73+ kind, payload, err = PreflightObject(object.Data)
74+ if err != nil || kind != Visible || !bytes.Equal(payload, []byte("canonical-vt")) {
75+ t.Fatalf("preflight = %v %q %v", kind, payload, err)
76+ }
77+ for n := range len(object.Data) {
78+ if _, _, err := UnmarshalObject(object.Data[:n]); err == nil {
79+ t.Fatalf("prefix %d accepted", n)
8880 }
8981 }
90- if _, err := UnmarshalObject(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) {
91- t.Fatalf("object trailing data error = %v", err)
82+ if _, _, err := UnmarshalObject(append(object.Data, 0)); !errors.Is(err, ErrTrailingBytes) {
83+ t.Fatalf("trailing data error = %v", err)
9284 }
9385 }
9486
95-func TestManifestPreflightRejectsReferencesBeforeDecode(t *testing.T) {
96- valid := Manifest{Name: "valid", Tabs: []Tab{{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", Objects: []ObjectRef{{Role: ObjectRoleTail, Digest: SnapshotDigest{1}}, {Role: ObjectRoleVisible, Digest: SnapshotDigest{2}}}}}}}}
87+func TestManifestDimensionBoundaries(t *testing.T) {
88+ ref := ObjectRef{Kind: HistoryTail, Digest: ports.SnapshotDigest{1}, Size: manifestHeaderSize + 5}
89+ visible := ObjectRef{Kind: Visible, Digest: ports.SnapshotDigest{2}, Size: manifestHeaderSize + 5}
9790 for _, tc := range []struct {
98- name string
99- mutate func(*Manifest)
91+ name string
92+ cols, rows uint16
93+ wantErr bool
10094 }{
101- {"zero digest", func(m *Manifest) { m.Tabs[0].Panes[0].Objects[0].Digest = SnapshotDigest{} }},
102- {"unordered role", func(m *Manifest) { m.Tabs[0].Panes[0].Objects[0].Role = ObjectRoleVisible }},
103- {"duplicate pane", func(m *Manifest) { m.Tabs[0].Panes = append(m.Tabs[0].Panes, m.Tabs[0].Panes[0]) }},
104- {"dangling layout", func(m *Manifest) { m.Tabs[0].Tree = layout.NewTree("missing") }},
105- {"zero dimensions", func(m *Manifest) { m.Tabs[0].Cols = 0 }},
95+ {"max cols safe rows", math.MaxUint16, 1, false},
96+ {"max rows safe cols", 1, math.MaxUint16, false},
97+ {"hostile product", math.MaxUint16, math.MaxUint16, true},
10698 } {
10799 t.Run(tc.name, func(t *testing.T) {
108- m := valid
109- m.Tabs = append([]Tab(nil), valid.Tabs...)
110- m.Tabs[0].Panes = append([]Pane(nil), valid.Tabs[0].Panes...)
111- m.Tabs[0].Panes[0].Objects = append([]ObjectRef(nil), valid.Tabs[0].Panes[0].Objects...)
112- tc.mutate(&m)
113- if _, err := MarshalManifest(m); !errors.Is(err, ErrInvalidData) && !errors.Is(err, ErrUnknownPane) {
114- t.Fatalf("MarshalManifest() error = %v", err)
100+ m := Manifest{Name: "safe", Tabs: []ManifestTab{{Cols: tc.cols, Rows: tc.rows, Panes: []ManifestPane{{ID: "p", Tail: ref, Visible: visible}}}}}
101+ _, err := MarshalManifest(m)
102+ if (err != nil) != tc.wantErr {
103+ t.Fatalf("MarshalManifest() error = %v, want error=%v", err, tc.wantErr)
115104 }
116105 })
117106 }
118107 }
108+
109+func TestManifestRejectsImpossibleObjectEnvelopeSize(t *testing.T) {
110+ manifest := Manifest{Name: "named", Tabs: []ManifestTab{{Cols: 1, Rows: 1, Panes: []ManifestPane{{
111+ ID: "pane",
112+ Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: 1},
113+ Visible: ObjectRef{Kind: Visible, Digest: SnapshotDigest{2}, Size: manifestHeaderSize + 5},
114+ }}}}}
115+ if _, err := MarshalManifest(manifest); !errors.Is(err, ErrInvalidData) {
116+ t.Fatalf("MarshalManifest() error = %v, want invalid data", err)
117+ }
118+}
@@ -2,45 +2,46 @@ package snapshot
2 2
3 import (3 import (
4 "bytes"4 "bytes"
5+ "crypto/sha256"
5 "errors"6 "errors"
7+ "math"
6 "testing"8 "testing"
7 9
10+ "github.com/bnema/vev/internal/ports"
8 "github.com/bnema/vev/internal/usecase/layout"11 "github.com/bnema/vev/internal/usecase/layout"
9 )12 )
10 13
11-func TestManifestAndHeadCodecsAreDeterministicAndStrict(t *testing.T) {14+func TestManifestCodecUsesGenerationAndOrderedTypedReferences(t *testing.T) {
12- digest := SnapshotDigest{1}15+ chunk, err := MarshalObject(HistoryChunk, []byte("history"))
13- manifest := Manifest{16+ if err != nil {
14- Name: "named",17+ t.Fatal(err)
15- CreatedAt: 9,
16- Active: 0,
17- Tabs: []Tab{{
18- StableID: "tab", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "pane",
19- Tree: layout.NewTree("pane"),
20- Panes: []Pane{{ID: "pane", StableID: "pane-stable", Cwd: "/tmp", Objects: []ObjectRef{
21- {Role: ObjectRoleHistory, Digest: digest},
22- {Role: ObjectRoleTail, Digest: SnapshotDigest{2}},
23- {Role: ObjectRoleVisible, Digest: SnapshotDigest{3}},
24- }}},
25- }},
26 }18 }
27- encoded, err := MarshalManifest(manifest)19+ tail, err := MarshalObject(HistoryTail, []byte("tail"))
28 if err != nil {20 if err != nil {
29 t.Fatal(err)21 t.Fatal(err)
30 }22 }
31- encodedAgain, err := MarshalManifest(manifest)23+ visible, err := MarshalObject(Visible, []byte("visible"))
32 if err != nil {24 if err != nil {
33 t.Fatal(err)25 t.Fatal(err)
34 }26 }
35- if !bytes.Equal(encoded, encodedAgain) {27+ manifest := Manifest{Generation: 7, Name: "named", CreatedAt: 9, Active: 0, Tabs: []ManifestTab{{
36- t.Fatal("manifest encoding is not deterministic")28+ StableID: "tab", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "pane", Tree: layout.NewTree("pane"),
29+ Panes: []ManifestPane{{ID: "pane", StableID: "pane-stable", Cwd: "/tmp",
30+ Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: chunk.Digest, Size: uint32(len(chunk.Data))}},
31+ Tail: ObjectRef{Kind: HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))},
32+ Visible: ObjectRef{Kind: Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))},
33+ }},
34+ }}}
35+ encoded, err := MarshalManifest(manifest)
36+ if err != nil {
37+ t.Fatal(err)
37 }38 }
38 got, err := UnmarshalManifest(encoded)39 got, err := UnmarshalManifest(encoded)
39 if err != nil {40 if err != nil {
40 t.Fatal(err)41 t.Fatal(err)
41 }42 }
42- if got.Name != manifest.Name || len(got.Tabs) != 1 || len(got.Tabs[0].Panes[0].Objects) != 3 {43+ if got.Generation != manifest.Generation || got.Tabs[0].Panes[0].Tail != manifest.Tabs[0].Panes[0].Tail {
43- t.Fatalf("manifest round trip = %#v", got)44+ t.Fatalf("round trip = %#v", got)
44 }45 }
45 for n := range len(encoded) {46 for n := range len(encoded) {
46 if _, err := UnmarshalManifest(encoded[:n]); err == nil {47 if _, err := UnmarshalManifest(encoded[:n]); err == nil {
@@ -51,68 +52,67 @@ func TestManifestAndHeadCodecsAreDeterministicAndStrict(t *testing.T) {
51 t.Fatalf("trailing data error = %v", err)52 t.Fatalf("trailing data error = %v", err)
52 }53 }
53 54
54- head := Head{Name: "named", Generation: 7, Manifest: ManifestDigest(encoded)}55+ head := Head{Generation: 7, ManifestDigest: sha256.Sum256(encoded)}
55- headBytes, err := MarshalHead(head)56+ if head.Generation != manifest.Generation || head.ManifestDigest != ManifestDigest(encoded) {
56- if err != nil {57+ t.Fatalf("head = %#v", head)
57- t.Fatal(err)
58- }
59- if got, err := UnmarshalHead(headBytes); err != nil || got != head {
60- t.Fatalf("head round trip = %#v, %v", got, err)
61- }
62- for n := range len(headBytes) {
63- if _, err := UnmarshalHead(headBytes[:n]); err == nil {
64- t.Fatalf("head prefix %d accepted", n)
65- }
66- }
67- if _, err := UnmarshalHead(append(headBytes, 0)); !errors.Is(err, ErrTrailingBytes) {
68- t.Fatalf("head trailing data error = %v", err)
69 }58 }
70 }59 }
71 60
72-func TestObjectCodecIsTypedAndStrict(t *testing.T) {61+func TestObjectCodecReturnsContentAddressedObjectAndPayload(t *testing.T) {
73- object := Object{Role: ObjectRoleVisible, Data: []byte("canonical-vt")}62+ object, err := MarshalObject(Visible, []byte("canonical-vt"))
74- encoded, err := MarshalObject(object)
75 if err != nil {63 if err != nil {
76 t.Fatal(err)64 t.Fatal(err)
77 }65 }
78- got, err := UnmarshalObject(encoded)66+ if object.Digest != sha256.Sum256(object.Data) {
79- if err != nil || got.Role != object.Role || !bytes.Equal(got.Data, object.Data) {67+ t.Fatalf("digest does not address VEVO bytes")
80- t.Fatalf("object round trip = %#v, %v", got, err)
81 }68 }
82- if err := PreflightObject(encoded); err != nil {69+ kind, payload, err := UnmarshalObject(object.Data)
83- t.Fatal(err)70+ if err != nil || kind != Visible || !bytes.Equal(payload, []byte("canonical-vt")) {
71+ t.Fatalf("round trip = %v %q %v", kind, payload, err)
84 }72 }
85- for n := range len(encoded) {73+ kind, payload, err = PreflightObject(object.Data)
86- if _, err := UnmarshalObject(encoded[:n]); err == nil {74+ if err != nil || kind != Visible || !bytes.Equal(payload, []byte("canonical-vt")) {
87- t.Fatalf("object prefix %d accepted", n)75+ t.Fatalf("preflight = %v %q %v", kind, payload, err)
76+ }
77+ for n := range len(object.Data) {
78+ if _, _, err := UnmarshalObject(object.Data[:n]); err == nil {
79+ t.Fatalf("prefix %d accepted", n)
88 }80 }
89 }81 }
90- if _, err := UnmarshalObject(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) {82+ if _, _, err := UnmarshalObject(append(object.Data, 0)); !errors.Is(err, ErrTrailingBytes) {
91- t.Fatalf("object trailing data error = %v", err)83+ t.Fatalf("trailing data error = %v", err)
92 }84 }
93 }85 }
94 86
95-func TestManifestPreflightRejectsReferencesBeforeDecode(t *testing.T) {87+func TestManifestDimensionBoundaries(t *testing.T) {
96- valid := Manifest{Name: "valid", Tabs: []Tab{{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", Objects: []ObjectRef{{Role: ObjectRoleTail, Digest: SnapshotDigest{1}}, {Role: ObjectRoleVisible, Digest: SnapshotDigest{2}}}}}}}}88+ ref := ObjectRef{Kind: HistoryTail, Digest: ports.SnapshotDigest{1}, Size: manifestHeaderSize + 5}
89+ visible := ObjectRef{Kind: Visible, Digest: ports.SnapshotDigest{2}, Size: manifestHeaderSize + 5}
97 for _, tc := range []struct {90 for _, tc := range []struct {
98- name string91+ name string
99- mutate func(*Manifest)92+ cols, rows uint16
93+ wantErr bool
100 }{94 }{
101- {"zero digest", func(m *Manifest) { m.Tabs[0].Panes[0].Objects[0].Digest = SnapshotDigest{} }},95+ {"max cols safe rows", math.MaxUint16, 1, false},
102- {"unordered role", func(m *Manifest) { m.Tabs[0].Panes[0].Objects[0].Role = ObjectRoleVisible }},96+ {"max rows safe cols", 1, math.MaxUint16, false},
103- {"duplicate pane", func(m *Manifest) { m.Tabs[0].Panes = append(m.Tabs[0].Panes, m.Tabs[0].Panes[0]) }},97+ {"hostile product", math.MaxUint16, math.MaxUint16, true},
104- {"dangling layout", func(m *Manifest) { m.Tabs[0].Tree = layout.NewTree("missing") }},
105- {"zero dimensions", func(m *Manifest) { m.Tabs[0].Cols = 0 }},
106 } {98 } {
107 t.Run(tc.name, func(t *testing.T) {99 t.Run(tc.name, func(t *testing.T) {
108- m := valid100+ m := Manifest{Name: "safe", Tabs: []ManifestTab{{Cols: tc.cols, Rows: tc.rows, Panes: []ManifestPane{{ID: "p", Tail: ref, Visible: visible}}}}}
109- m.Tabs = append([]Tab(nil), valid.Tabs...)101+ _, err := MarshalManifest(m)
110- m.Tabs[0].Panes = append([]Pane(nil), valid.Tabs[0].Panes...)102+ if (err != nil) != tc.wantErr {
111- m.Tabs[0].Panes[0].Objects = append([]ObjectRef(nil), valid.Tabs[0].Panes[0].Objects...)103+ t.Fatalf("MarshalManifest() error = %v, want error=%v", err, tc.wantErr)
112- tc.mutate(&m)
113- if _, err := MarshalManifest(m); !errors.Is(err, ErrInvalidData) && !errors.Is(err, ErrUnknownPane) {
114- t.Fatalf("MarshalManifest() error = %v", err)
115 }104 }
116 })105 })
117 }106 }
118 }107 }
108+
109+func TestManifestRejectsImpossibleObjectEnvelopeSize(t *testing.T) {
110+ manifest := Manifest{Name: "named", Tabs: []ManifestTab{{Cols: 1, Rows: 1, Panes: []ManifestPane{{
111+ ID: "pane",
112+ Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: 1},
113+ Visible: ObjectRef{Kind: Visible, Digest: SnapshotDigest{2}, Size: manifestHeaderSize + 5},
114+ }}}}}
115+ if _, err := MarshalManifest(manifest); !errors.Is(err, ErrInvalidData) {
116+ t.Fatalf("MarshalManifest() error = %v, want invalid data", err)
117+ }
118+}
M internal/usecase/snapshot/snapshot.go +0 -5
@@ -31,11 +31,6 @@ type Pane struct {
3131 Tail []byte // mandatory nonempty canonical VT blob
3232 Visible []byte // mandatory nonempty canonical VT blob
3333 Process *Process
34-
35- // Objects is used only by the v1 incremental manifest codec. It must be
36- // ordered history objects, then exactly one tail and one visible object.
37- // The v3 codec deliberately ignores it to preserve its wire format.
38- Objects []ObjectRef
3934 }
4035
4136 // Process captures restorable foreground process metadata.
@@ -31,11 +31,6 @@ type Pane struct {
31 Tail []byte // mandatory nonempty canonical VT blob31 Tail []byte // mandatory nonempty canonical VT blob
32 Visible []byte // mandatory nonempty canonical VT blob32 Visible []byte // mandatory nonempty canonical VT blob
33 Process *Process33 Process *Process
34-
35- // Objects is used only by the v1 incremental manifest codec. It must be
36- // ordered history objects, then exactly one tail and one visible object.
37- // The v3 codec deliberately ignores it to preserve its wire format.
38- Objects []ObjectRef
39 }34 }
40 35
41 // Process captures restorable foreground process metadata.36 // Process captures restorable foreground process metadata.

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