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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +877 -754
M internal/ports/mocks/store_mock.go +261 -59
| @@ -5,6 +5,8 @@ | |||
| 5 | 5 | package portsmocks | |
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | + | "context" | |
| 9 | + | ||
| 8 | 10 | "github.com/bnema/vev/internal/ports" | |
| 9 | 11 | mock "github.com/stretchr/testify/mock" | |
| 10 | 12 | ) | |
| @@ -552,16 +554,16 @@ func (_m *MockSnapshotRepository) EXPECT() *MockSnapshotRepository_Expecter { | |||
| 552 | 554 | } | |
| 553 | 555 | ||
| 554 | 556 | // 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) | |
| 557 | 559 | ||
| 558 | 560 | if len(ret) == 0 { | |
| 559 | 561 | panic("no return value specified for Delete") | |
| 560 | 562 | } | |
| 561 | 563 | ||
| 562 | 564 | 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) | |
| 565 | 567 | } else { | |
| 566 | 568 | r0 = ret.Error(0) | |
| 567 | 569 | } | |
| @@ -574,19 +576,25 @@ type MockSnapshotRepository_Delete_Call struct { | |||
| 574 | 576 | } | |
| 575 | 577 | ||
| 576 | 578 | // 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)} | |
| 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 | 586 | _c.Call.Run(func(args mock.Arguments) { | |
| 584 | - | var arg0 string | |
| 587 | + | var arg0 context.Context | |
| 585 | 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 | 595 | run( | |
| 589 | 596 | arg0, | |
| 597 | + | arg1, | |
| 590 | 598 | ) | |
| 591 | 599 | }) | |
| 592 | 600 | return _c | |
| @@ -597,33 +605,93 @@ func (_c *MockSnapshotRepository_Delete_Call) Return(err error) *MockSnapshotRep | |||
| 597 | 605 | 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 | 609 | _c.Call.Return(run) | |
| 602 | 610 | return _c | |
| 603 | 611 | } | |
| 604 | 612 | ||
| 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) | |
| 608 | 616 | ||
| 609 | 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.SnapshotGeneration | |
| 621 | + | var r0 []string | |
| 614 | 622 | 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) | |
| 617 | 625 | } | |
| 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) | |
| 620 | 628 | } else { | |
| 621 | 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 { | |
| 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) | |
| 627 | 695 | } else { | |
| 628 | 696 | r1 = ret.Error(1) | |
| 629 | 697 | } | |
| @@ -636,38 +704,102 @@ type MockSnapshotRepository_Load_Call struct { | |||
| 636 | 704 | } | |
| 637 | 705 | ||
| 638 | 706 | // 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)} | |
| 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 | 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 | 728 | return _c | |
| 648 | 729 | } | |
| 649 | 730 | ||
| 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) | |
| 652 | 733 | 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 | 788 | _c.Call.Return(run) | |
| 657 | 789 | return _c | |
| 658 | 790 | } | |
| 659 | 791 | ||
| 660 | 792 | // 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) | |
| 663 | 795 | ||
| 664 | 796 | if len(ret) == 0 { | |
| 665 | 797 | panic("no return value specified for Publish") | |
| 666 | 798 | } | |
| 667 | 799 | ||
| 668 | 800 | 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) | |
| 671 | 803 | } else { | |
| 672 | 804 | r0 = ret.Error(0) | |
| 673 | 805 | } | |
| @@ -680,19 +812,25 @@ type MockSnapshotRepository_Publish_Call struct { | |||
| 680 | 812 | } | |
| 681 | 813 | ||
| 682 | 814 | // Publish is a helper method to define mock.On call | |
| 815 | + | // - context1 context.Context | |
| 683 | 816 | // - 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)} | |
| 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 | 822 | _c.Call.Run(func(args mock.Arguments) { | |
| 690 | - | var arg0 ports.SnapshotPublication | |
| 823 | + | var arg0 context.Context | |
| 691 | 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 | 831 | run( | |
| 695 | 832 | arg0, | |
| 833 | + | arg1, | |
| 696 | 834 | ) | |
| 697 | 835 | }) | |
| 698 | 836 | return _c | |
| @@ -703,7 +841,7 @@ func (_c *MockSnapshotRepository_Publish_Call) Return(err error) *MockSnapshotRe | |||
| 703 | 841 | 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 | 845 | _c.Call.Return(run) | |
| 708 | 846 | return _c | |
| 709 | 847 | } | |
| @@ -735,57 +873,121 @@ func (_m *MockLegacySnapshotSource) EXPECT() *MockLegacySnapshotSource_Expecter | |||
| 735 | 873 | return &MockLegacySnapshotSource_Expecter{mock: &_m.Mock} | |
| 736 | 874 | } | |
| 737 | 875 | ||
| 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) | |
| 741 | 936 | ||
| 742 | 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 | 941 | var r0 []ports.LegacySnapshot | |
| 747 | 942 | 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) | |
| 750 | 945 | } | |
| 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) | |
| 753 | 948 | } else { | |
| 754 | 949 | if ret.Get(0) != nil { | |
| 755 | 950 | r0 = ret.Get(0).([]ports.LegacySnapshot) | |
| 756 | 951 | } | |
| 757 | 952 | } | |
| 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) | |
| 760 | 955 | } else { | |
| 761 | 956 | r1 = ret.Error(1) | |
| 762 | 957 | } | |
| 763 | 958 | 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' | |
| 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 { | |
| 768 | 963 | *mock.Call | |
| 769 | 964 | } | |
| 770 | 965 | ||
| 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)} | |
| 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 | 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 | 982 | 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 | 986 | _c.Call.Return(legacySnapshots, err) | |
| 785 | 987 | 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 | 991 | _c.Call.Return(run) | |
| 790 | 992 | return _c | |
| 791 | 993 | } | |
| @@ -5,6 +5,8 @@ | |||||
| 5 | package portsmocks | 5 | 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 MockSnapshotRepository | 556 | // 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 error | 564 | 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 call | 578 | // Delete is a helper method to define mock.On call | ||
| 577 | - | // - name string | 579 | + | // - 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 string | 587 | + | 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 _c | 600 | return _c | ||
| @@ -597,33 +605,93 @@ func (_c *MockSnapshotRepository_Delete_Call) Return(err error) *MockSnapshotRep | |||||
| 597 | return _c | 605 | 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 _c | 610 | return _c | ||
| 603 | } | 611 | } | ||
| 604 | 612 | ||||
| 605 | - | // Load provides a mock function for the type MockSnapshotRepository | 613 | + | // 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.SnapshotGeneration | 621 | + | var r0 []string |
| 614 | var r1 error | 622 | 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 call | 706 | // 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 _c | 728 | 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 _c | 733 | 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 _c | 789 | return _c | ||
| 658 | } | 790 | } | ||
| 659 | 791 | ||||
| 660 | // Publish provides a mock function for the type MockSnapshotRepository | 792 | // 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 error | 800 | 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 call | 814 | // Publish is a helper method to define mock.On call | ||
| 815 | + | // - context1 context.Context | |||
| 683 | // - snapshotPublication ports.SnapshotPublication | 816 | // - 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.SnapshotPublication | 823 | + | 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 _c | 836 | return _c | ||
| @@ -703,7 +841,7 @@ func (_c *MockSnapshotRepository_Publish_Call) Return(err error) *MockSnapshotRe | |||||
| 703 | return _c | 841 | 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 _c | 846 | 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 MockLegacySnapshotSource | 876 | + | // 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.LegacySnapshot | 941 | var r0 []ports.LegacySnapshot | ||
| 747 | var r1 error | 942 | 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, r1 | 958 | 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.Call | 963 | *mock.Call | ||
| 769 | } | 964 | } | ||
| 770 | 965 | ||||
| 771 | - | // LoadLegacySnapshots is a helper method to define mock.On call | 966 | + | // 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 _c | 982 | 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 _c | 987 | 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 _c | 992 | return _c | ||
| 791 | } | 993 | } | ||
M internal/ports/snapshot_test.go +30 -21
| @@ -1,33 +1,50 @@ | |||
| 1 | 1 | package ports | |
| 2 | 2 | ||
| 3 | - | import "testing" | |
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "crypto/sha256" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 4 | 8 | ||
| 5 | 9 | type snapshotRepositoryContract struct{} | |
| 6 | 10 | ||
| 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 } | |
| 10 | 18 | ||
| 11 | 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 | 26 | func TestSnapshotRepositoryContracts(t *testing.T) { | |
| 16 | 27 | var _ SnapshotRepository = snapshotRepositoryContract{} | |
| 17 | 28 | var _ LegacySnapshotSource = legacySnapshotSourceContract{} | |
| 18 | 29 | ||
| 19 | 30 | 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, | |
| 23 | 42 | } | |
| 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 { | |
| 26 | 44 | t.Fatalf("snapshot publication/generation = %#v", generation) | |
| 27 | 45 | } | |
| 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) | |
| 31 | 48 | } | |
| 32 | 49 | } | |
| 33 | 50 | ||
| @@ -39,12 +56,4 @@ func (snapshotStoreContract) Delete(name string) error { return nil | |||
| 39 | 56 | ||
| 40 | 57 | func TestSnapshotStoreContract(t *testing.T) { | |
| 41 | 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 | } | |
| @@ -1,33 +1,50 @@ | |||||
| 1 | package ports | 1 | 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 @@ | |||
| 1 | 1 | package ports | |
| 2 | 2 | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "crypto/sha256" | |
| 6 | + | ) | |
| 7 | + | ||
| 3 | 8 | // Store is a small byte-key/value persistence port. | |
| 4 | 9 | // | |
| 5 | 10 | // Implementations may buffer writes; Sync is the durability barrier. | |
| @@ -30,40 +35,42 @@ type SnapshotStore interface { | |||
| 30 | 35 | Delete(name string) error | |
| 31 | 36 | } | |
| 32 | 37 | ||
| 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 | |
| 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 | 41 | type SnapshotObject struct { | |
| 41 | 42 | Digest SnapshotDigest | |
| 42 | 43 | Data []byte | |
| 43 | 44 | } | |
| 44 | 45 | ||
| 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. | |
| 47 | 48 | type SnapshotPublication struct { | |
| 48 | - | Name string | |
| 49 | - | Head []byte | |
| 50 | - | Objects []SnapshotObject | |
| 49 | + | Name string | |
| 50 | + | Generation uint64 | |
| 51 | + | Manifest []byte | |
| 52 | + | Objects []SnapshotObject | |
| 51 | 53 | } | |
| 52 | 54 | ||
| 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. | |
| 55 | 58 | 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 | |
| 58 | 64 | } | |
| 59 | 65 | ||
| 60 | 66 | // 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. | |
| 63 | 68 | 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 | |
| 67 | 74 | } | |
| 68 | 75 | ||
| 69 | 76 | // LegacySnapshot is the pre-incremental named-session blob retained only for | |
| @@ -73,8 +80,9 @@ type LegacySnapshot struct { | |||
| 73 | 80 | Data []byte | |
| 74 | 81 | } | |
| 75 | 82 | ||
| 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. | |
| 78 | 85 | type LegacySnapshotSource interface { | |
| 79 | - | LoadLegacySnapshots() ([]LegacySnapshot, error) | |
| 86 | + | LoadLegacy(context.Context) ([]LegacySnapshot, error) | |
| 87 | + | DeleteLegacy(context.Context, string) error | |
| 80 | 88 | } | |
| @@ -1,5 +1,10 @@ | |||||
| 1 | package ports | 1 | 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) error | 35 | Delete(name string) error | ||
| 31 | } | 36 | } | ||
| 32 | 37 | ||||
| 33 | - | // SnapshotDigest is the SHA-256 content address of a complete snapshot object | 38 | + | // 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 SnapshotDigest | 42 | Digest SnapshotDigest | ||
| 42 | Data []byte | 43 | 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 string | 49 | + | Name string |
| 49 | - | Head []byte | 50 | + | Generation uint64 |
| 50 | - | Objects []SnapshotObject | 51 | + | Manifest []byte |
| 52 | + | Objects []SnapshotObject | |||
| 51 | } | 53 | } | ||
| 52 | 54 | ||||
| 53 | - | // SnapshotGeneration is a published head together with its manifest and all | 55 | + | // 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 SnapshotPublication | 59 | + | Name string |
| 57 | - | Manifest []byte | 60 | + | Generation uint64 |
| 61 | + | Manifest []byte | |||
| 62 | + | Objects map[SnapshotDigest][]byte | |||
| 63 | + | Fallback bool | |||
| 58 | } | 64 | } | ||
| 59 | 65 | ||||
| 60 | // SnapshotRepository persists content-addressed incremental snapshot | 66 | // SnapshotRepository persists content-addressed incremental snapshot | ||
| 61 | - | // generations. Load returns caller-owned copies, so callers may retain or | 67 | + | // 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) error | 69 | + | Publish(context.Context, SnapshotPublication) error |
| 65 | - | Load() ([]SnapshotGeneration, error) | 70 | + | List(context.Context) ([]string, error) |
| 66 | - | Delete(name string) error | 71 | + | 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 for | 76 | // LegacySnapshot is the pre-incremental named-session blob retained only for | ||
| @@ -73,8 +80,9 @@ type LegacySnapshot struct { | |||||
| 73 | Data []byte | 80 | Data []byte | ||
| 74 | } | 81 | } | ||
| 75 | 82 | ||||
| 76 | - | // LegacySnapshotSource exposes v3 snapshot blobs without coupling the new | 83 | + | // 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 ( | |||
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | 14 | 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 | |
| 19 | 20 | ) | |
| 20 | 21 | ||
| 21 | 22 | // SnapshotDigest is the shared content-address type used by the repository. | |
| 22 | 23 | type SnapshotDigest = ports.SnapshotDigest | |
| 23 | 24 | ||
| 24 | - | // ManifestDigest returns the content address of a complete VEVM manifest. | |
| 25 | 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. | |
| 28 | - | type ObjectRole uint8 | |
| 27 | + | type ObjectKind uint8 | |
| 29 | 28 | ||
| 30 | 29 | const ( | |
| 31 | - | ObjectRoleHistory ObjectRole = iota + 1 | |
| 32 | - | ObjectRoleTail | |
| 33 | - | ObjectRoleVisible | |
| 30 | + | HistoryChunk ObjectKind = iota + 1 | |
| 31 | + | HistoryTail | |
| 32 | + | Visible | |
| 34 | 33 | ) | |
| 35 | 34 | ||
| 36 | - | // ObjectRef is a content-addressed object used by a reference-only manifest. | |
| 37 | 35 | type ObjectRef struct { | |
| 38 | - | Role ObjectRole | |
| 36 | + | Kind ObjectKind | |
| 39 | 37 | Digest SnapshotDigest | |
| 38 | + | Size uint32 | |
| 40 | 39 | } | |
| 41 | 40 | ||
| 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 | |
| 50 | 46 | } | |
| 51 | 47 | ||
| 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 { | |
| 55 | 50 | Generation uint64 | |
| 56 | - | Manifest SnapshotDigest | |
| 51 | + | 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. | |
| 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 | |
| 63 | 75 | } | |
| 64 | 76 | ||
| 65 | - | // MarshalManifest encodes a deterministic, strict VEVM v1 envelope. | |
| 66 | 77 | func MarshalManifest(m Manifest) ([]byte, error) { | |
| 67 | 78 | if err := validateManifest(m); err != nil { | |
| 68 | 79 | return nil, err | |
| 69 | 80 | } | |
| 70 | 81 | 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 | 84 | 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 | 100 | func UnmarshalManifest(encoded []byte) (Manifest, error) { | |
| 81 | - | body, err := unmarshalEnvelope(encoded, manifestMagic) | |
| 101 | + | body, err := unmarshalManifestEnvelope(encoded, manifestMagic) | |
| 82 | 102 | if err != nil { | |
| 83 | 103 | return Manifest{}, err | |
| 84 | 104 | } | |
| @@ -86,241 +106,137 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) { | |||
| 86 | 106 | return Manifest{}, err | |
| 87 | 107 | } | |
| 88 | 108 | r := payloadReader{b: body} | |
| 89 | - | m, err := readManifest(&r) | |
| 109 | + | generation, err := r.getUint64() | |
| 90 | 110 | if err != nil { | |
| 91 | 111 | return Manifest{}, err | |
| 92 | 112 | } | |
| 93 | - | if err := r.done(); err != nil { | |
| 113 | + | name, err := r.getString() | |
| 114 | + | if err != nil { | |
| 94 | 115 | return Manifest{}, err | |
| 95 | 116 | } | |
| 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() | |
| 116 | 118 | if err != nil { | |
| 117 | - | return Head{}, err | |
| 119 | + | return Manifest{}, err | |
| 118 | 120 | } | |
| 119 | - | r := payloadReader{b: body} | |
| 120 | - | name, err := r.getString() | |
| 121 | + | active, err := r.getUint16() | |
| 121 | 122 | if err != nil { | |
| 122 | - | return Head{}, err | |
| 123 | + | return Manifest{}, err | |
| 123 | 124 | } | |
| 124 | - | generation, err := r.getUint64() | |
| 125 | + | n, err := r.getUint16() | |
| 125 | 126 | if err != nil { | |
| 126 | - | return Head{}, err | |
| 127 | + | return Manifest{}, err | |
| 127 | 128 | } | |
| 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 | + | } | |
| 130 | 134 | } | |
| 131 | - | var digest SnapshotDigest | |
| 132 | - | copy(digest[:], r.b[:sha256.Size]) | |
| 133 | - | r.b = r.b[sha256.Size:] | |
| 134 | 135 | if err := r.done(); err != nil { | |
| 135 | - | return Head{}, err | |
| 136 | + | return Manifest{}, err | |
| 136 | 137 | } | |
| 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 | |
| 139 | 140 | } | |
| 140 | - | return Head{Name: name, Generation: generation, Manifest: digest}, nil | |
| 141 | + | return m, nil | |
| 141 | 142 | } | |
| 142 | 143 | ||
| 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) | |
| 147 | 149 | } | |
| 148 | 150 | 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 | |
| 153 | 159 | } | |
| 154 | 160 | ||
| 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) | |
| 159 | 163 | 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 | |
| 161 | 175 | } | |
| 162 | 176 | if len(body) < 5 { | |
| 163 | - | return ErrShortPayload | |
| 177 | + | return 0, nil, ErrShortPayload | |
| 164 | 178 | } | |
| 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) | |
| 167 | 182 | } | |
| 168 | 183 | n := binary.BigEndian.Uint32(body[1:5]) | |
| 169 | 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 | 187 | if uint64(n) > uint64(len(body)-5) { | |
| 173 | - | return ErrShortPayload | |
| 188 | + | return 0, nil, ErrShortPayload | |
| 174 | 189 | } | |
| 175 | 190 | 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 | |
| 185 | 192 | } | |
| 186 | - | body := encoded[16:] | |
| 187 | - | return Object{Role: ObjectRole(body[0]), Data: append([]byte(nil), body[5:]...)}, nil | |
| 193 | + | return kind, 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 | 197 | if len(body) > maxDecodedBodySize || len(body) > math.MaxUint32 { | |
| 192 | 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 | 201 | 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) | |
| 198 | 203 | binary.BigEndian.PutUint32(out[8:12], uint32(len(body))) | |
| 199 | 204 | binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body)) | |
| 200 | 205 | copy(out[16:], body) | |
| 201 | 206 | return out, nil | |
| 202 | 207 | } | |
| 203 | 208 | ||
| 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 { | |
| 206 | 211 | return nil, ErrShortPayload | |
| 207 | 212 | } | |
| 208 | 213 | if string(encoded[:4]) != expectedMagic { | |
| 209 | 214 | return nil, ErrBadMagic | |
| 210 | 215 | } | |
| 211 | - | if binary.BigEndian.Uint16(encoded[4:6]) != codecVersion { | |
| 216 | + | if binary.BigEndian.Uint16(encoded[4:6]) != ManifestVersion { | |
| 212 | 217 | return nil, ErrBadVersion | |
| 213 | 218 | } | |
| 214 | 219 | if binary.BigEndian.Uint16(encoded[6:8]) != 0 { | |
| 215 | 220 | return nil, fmt.Errorf("%w: flags", ErrInvalidData) | |
| 216 | 221 | } | |
| 217 | - | bodyLen := binary.BigEndian.Uint32(encoded[8:12]) | |
| 218 | - | if bodyLen > maxDecodedBodySize { | |
| 222 | + | n := binary.BigEndian.Uint32(encoded[8:12]) | |
| 223 | + | if n > maxDecodedBodySize { | |
| 219 | 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 | 227 | return nil, ErrShortPayload | |
| 223 | 228 | } | |
| 224 | - | if uint64(len(encoded)) != 16+uint64(bodyLen) { | |
| 229 | + | if uint64(len(encoded)) != uint64(manifestHeaderSize)+uint64(n) { | |
| 225 | 230 | return nil, ErrTrailingBytes | |
| 226 | 231 | } | |
| 227 | - | body := encoded[16:] | |
| 232 | + | body := encoded[manifestHeaderSize:] | |
| 228 | 233 | if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(encoded[12:16]) { | |
| 229 | 234 | return nil, ErrBadCRC | |
| 230 | 235 | } | |
| 231 | 236 | return body, nil | |
| 232 | 237 | } | |
| 233 | 238 | ||
| 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 { | |
| 324 | 240 | if err := w.putString(tab.StableID); err != nil { | |
| 325 | 241 | return err | |
| 326 | 242 | } | |
| @@ -348,8 +264,7 @@ func writeManifestTab(w *payloadWriter, tab Tab) error { | |||
| 348 | 264 | } | |
| 349 | 265 | return nil | |
| 350 | 266 | } | |
| 351 | - | ||
| 352 | - | func writeManifestPane(w *payloadWriter, pane Pane) error { | |
| 267 | + | func writeManifestPane(w *payloadWriter, pane ManifestPane) error { | |
| 353 | 268 | if err := w.putString(string(pane.ID)); err != nil { | |
| 354 | 269 | return err | |
| 355 | 270 | } | |
| @@ -359,458 +274,166 @@ func writeManifestPane(w *payloadWriter, pane Pane) error { | |||
| 359 | 274 | if err := w.putString(pane.Cwd); err != nil { | |
| 360 | 275 | return err | |
| 361 | 276 | } | |
| 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) | |
| 364 | 279 | } | |
| 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) | |
| 369 | 283 | } | |
| 284 | + | writeObjectRef(w, pane.Tail) | |
| 285 | + | writeObjectRef(w, pane.Visible) | |
| 370 | 286 | return writeProcess(w, pane.Process) | |
| 371 | 287 | } | |
| 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) | |
| 397 | 292 | } | |
| 398 | 293 | ||
| 399 | - | func readManifestTab(r *payloadReader) (Tab, error) { | |
| 294 | + | func readManifestTab(r *payloadReader) (ManifestTab, error) { | |
| 400 | 295 | stableID, err := r.getString() | |
| 401 | 296 | if err != nil { | |
| 402 | - | return Tab{}, err | |
| 297 | + | return ManifestTab{}, err | |
| 403 | 298 | } | |
| 404 | 299 | cols, err := r.getUint16() | |
| 405 | 300 | if err != nil { | |
| 406 | - | return Tab{}, err | |
| 301 | + | return ManifestTab{}, err | |
| 407 | 302 | } | |
| 408 | 303 | rows, err := r.getUint16() | |
| 409 | 304 | if err != nil { | |
| 410 | - | return Tab{}, err | |
| 305 | + | return ManifestTab{}, err | |
| 411 | 306 | } | |
| 412 | 307 | next, err := r.getUint64() | |
| 413 | 308 | if err != nil { | |
| 414 | - | return Tab{}, err | |
| 309 | + | return ManifestTab{}, err | |
| 415 | 310 | } | |
| 416 | 311 | focus, err := r.getString() | |
| 417 | 312 | if err != nil { | |
| 418 | - | return Tab{}, err | |
| 313 | + | return ManifestTab{}, err | |
| 419 | 314 | } | |
| 420 | 315 | root, err := readNode(r) | |
| 421 | 316 | if err != nil { | |
| 422 | - | return Tab{}, err | |
| 317 | + | return ManifestTab{}, err | |
| 423 | 318 | } | |
| 424 | 319 | n, err := r.getUint16() | |
| 425 | 320 | if err != nil { | |
| 426 | - | return Tab{}, err | |
| 321 | + | 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 | 324 | for i := range tab.Panes { | |
| 430 | 325 | if tab.Panes[i], err = readManifestPane(r); err != nil { | |
| 431 | - | return Tab{}, err | |
| 326 | + | return ManifestTab{}, err | |
| 432 | 327 | } | |
| 433 | 328 | } | |
| 434 | 329 | return tab, nil | |
| 435 | 330 | } | |
| 436 | - | ||
| 437 | - | func readManifestPane(r *payloadReader) (Pane, error) { | |
| 331 | + | func readManifestPane(r *payloadReader) (ManifestPane, error) { | |
| 438 | 332 | id, err := r.getString() | |
| 439 | 333 | if err != nil { | |
| 440 | - | return Pane{}, err | |
| 334 | + | return ManifestPane{}, err | |
| 441 | 335 | } | |
| 442 | 336 | stableID, err := r.getString() | |
| 443 | 337 | if err != nil { | |
| 444 | - | return Pane{}, err | |
| 338 | + | return ManifestPane{}, err | |
| 445 | 339 | } | |
| 446 | 340 | cwd, err := r.getString() | |
| 447 | 341 | if err != nil { | |
| 448 | - | return Pane{}, err | |
| 342 | + | return ManifestPane{}, err | |
| 449 | 343 | } | |
| 450 | 344 | n, err := r.getUint16() | |
| 451 | 345 | 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 | |
| 528 | 347 | } | |
| 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 | |
| 563 | 352 | } | |
| 564 | 353 | } | |
| 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 | |
| 571 | 356 | } | |
| 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 | |
| 587 | 359 | } | |
| 588 | - | return preflightManifestProcess(r, budget) | |
| 360 | + | pane.Process, err = readProcess(r) | |
| 361 | + | return pane, err | |
| 589 | 362 | } | |
| 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() | |
| 593 | 365 | if err != nil { | |
| 594 | - | return err | |
| 595 | - | } | |
| 596 | - | if uint64(n) > uint64(len(r.b)) { | |
| 597 | - | return ErrShortPayload | |
| 366 | + | return ObjectRef{}, err | |
| 598 | 367 | } | |
| 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 | |
| 601 | 370 | } | |
| 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 | |
| 604 | 377 | } | |
| 605 | 378 | ||
| 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) | |
| 609 | 382 | } | |
| 610 | - | kind, err := r.getUint8() | |
| 611 | - | if err != nil { | |
| 383 | + | if err := validateActive(m.Active, len(m.Tabs)); err != nil { | |
| 612 | 384 | return err | |
| 613 | 385 | } | |
| 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) | |
| 624 | 389 | } | |
| 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{}{} | |
| 628 | 402 | } | |
| 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 | |
| 632 | 405 | } | |
| 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 { | |
| 646 | 407 | return err | |
| 647 | 408 | } | |
| 648 | 409 | } | |
| 649 | 410 | return nil | |
| 650 | 411 | } | |
| 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 | |
| 679 | 414 | } | |
| 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 { | |
| 698 | 418 | return err | |
| 699 | 419 | } | |
| 700 | 420 | } | |
| 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 { | |
| 715 | 422 | return err | |
| 716 | 423 | } | |
| 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) | |
| 769 | 425 | } | |
| 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) | |
| 803 | 429 | } | |
| 804 | - | return fmt.Errorf("%w: missing visible object", ErrInvalidData) | |
| 430 | + | return nil | |
| 805 | 431 | } | |
| 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 | |
| 809 | 434 | } | |
| 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 { | |
| 814 | 437 | if b != 0 { | |
| 815 | 438 | return false | |
| 816 | 439 | } | |
| @@ -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.SnapshotDigest | 23 | 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 + 1 | 30 | + | HistoryChunk ObjectKind = iota + 1 |
| 32 | - | ObjectRoleTail | 31 | + | HistoryTail |
| 33 | - | ObjectRoleVisible | 32 | + | 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 ObjectRole | 36 | + | Kind ObjectKind |
| 39 | Digest SnapshotDigest | 37 | Digest SnapshotDigest | ||
| 38 | + | Size uint32 | |||
| 40 | } | 39 | } | ||
| 41 | 40 | ||||
| 42 | - | // Manifest is the incremental durable representation of a named session. Its | 41 | + | // Head is the repository-owned generation pointer. It is deliberately not a |
| 43 | - | // panes contain references only; terminal bytes belong exclusively in VEVO | 42 | + | // 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 string | 45 | + | 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 uint64 | 50 | Generation uint64 | ||
| 56 | - | Manifest SnapshotDigest | 51 | + | 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 ObjectRole | 59 | + | Cols uint16 |
| 62 | - | Data []byte | 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 | |||
| 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, err | 79 | return nil, err | ||
| 69 | } | 80 | } | ||
| 70 | var w payloadWriter | 81 | 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, err | 84 | 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{}, err | 103 | return Manifest{}, err | ||
| 84 | } | 104 | } | ||
| @@ -86,241 +106,137 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) { | |||||
| 86 | return Manifest{}, err | 106 | 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{}, err | 111 | return Manifest{}, err | ||
| 92 | } | 112 | } | ||
| 93 | - | if err := r.done(); err != nil { | 113 | + | name, err := r.getString() |
| 114 | + | if err != nil { | |||
| 94 | return Manifest{}, err | 115 | return Manifest{}, err | ||
| 95 | } | 116 | } | ||
| 96 | - | return m, nil | 117 | + | 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{}, err | 119 | + | 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{}, err | 123 | + | 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{}, err | 127 | + | 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{}, ErrShortPayload | 130 | + | 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{}, err | 136 | + | 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}, nil | 141 | + | 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 payloadWriter | 150 | 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 object | 161 | + | 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 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 | |||
| 161 | } | 175 | } | ||
| 162 | if len(body) < 5 { | 176 | if len(body) < 5 { | ||
| 163 | - | return ErrShortPayload | 177 | + | 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 ErrShortPayload | 188 | + | return 0, nil, ErrShortPayload |
| 174 | } | 189 | } | ||
| 175 | if uint64(n) != uint64(len(body)-5) { | 190 | if uint64(n) != uint64(len(body)-5) { | ||
| 176 | - | return ErrTrailingBytes | 191 | + | 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, nil | 206 | 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, ErrShortPayload | 211 | return nil, ErrShortPayload | ||
| 207 | } | 212 | } | ||
| 208 | if string(encoded[:4]) != expectedMagic { | 213 | if string(encoded[:4]) != expectedMagic { | ||
| 209 | return nil, ErrBadMagic | 214 | 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, ErrBadVersion | 217 | 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, ErrShortPayload | 227 | 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, ErrTrailingBytes | 230 | 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, ErrBadCRC | 234 | return nil, ErrBadCRC | ||
| 230 | } | 235 | } | ||
| 231 | return body, nil | 236 | 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 err | 241 | return err | ||
| 326 | } | 242 | } | ||
| @@ -348,8 +264,7 @@ func writeManifestTab(w *payloadWriter, tab Tab) error { | |||||
| 348 | } | 264 | } | ||
| 349 | return nil | 265 | 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 err | 269 | 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 err | 275 | 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{}, err | 297 | + | 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{}, err | 301 | + | 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{}, err | 305 | + | 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{}, err | 309 | + | 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{}, err | 313 | + | 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{}, err | 317 | + | 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{}, err | 321 | + | 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{}, err | 326 | + | return ManifestTab{}, err |
| 432 | } | 327 | } | ||
| 433 | } | 328 | } | ||
| 434 | return tab, nil | 329 | 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{}, err | 334 | + | 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{}, err | 338 | + | 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{}, err | 342 | + | 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{}, err | 346 | + | 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 err | 366 | + | 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 nil | 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 | |||
| 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 err | 384 | 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 err | 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{}{} | |||
| 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 nil | 406 | + | 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 err | 407 | return err | ||
| 647 | } | 408 | } | ||
| 648 | } | 409 | } | ||
| 649 | return nil | 410 | 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 err | 418 | 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 err | 422 | 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 false | 438 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | + | "crypto/sha256" | |
| 5 | 6 | "errors" | |
| 7 | + | "math" | |
| 6 | 8 | "testing" | |
| 7 | 9 | ||
| 10 | + | "github.com/bnema/vev/internal/ports" | |
| 8 | 11 | "github.com/bnema/vev/internal/usecase/layout" | |
| 9 | 12 | ) | |
| 10 | 13 | ||
| 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) | |
| 26 | 18 | } | |
| 27 | - | encoded, err := MarshalManifest(manifest) | |
| 19 | + | tail, err := MarshalObject(HistoryTail, []byte("tail")) | |
| 28 | 20 | if err != nil { | |
| 29 | 21 | t.Fatal(err) | |
| 30 | 22 | } | |
| 31 | - | encodedAgain, err := MarshalManifest(manifest) | |
| 23 | + | visible, err := MarshalObject(Visible, []byte("visible")) | |
| 32 | 24 | if err != nil { | |
| 33 | 25 | t.Fatal(err) | |
| 34 | 26 | } | |
| 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) | |
| 37 | 38 | } | |
| 38 | 39 | got, err := UnmarshalManifest(encoded) | |
| 39 | 40 | if err != nil { | |
| 40 | 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 | - | 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) | |
| 44 | 45 | } | |
| 45 | 46 | for n := range len(encoded) { | |
| 46 | 47 | if _, err := UnmarshalManifest(encoded[:n]); err == nil { | |
| @@ -51,68 +52,67 @@ func TestManifestAndHeadCodecsAreDeterministicAndStrict(t *testing.T) { | |||
| 51 | 52 | t.Fatalf("trailing data error = %v", err) | |
| 52 | 53 | } | |
| 53 | 54 | ||
| 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) | |
| 69 | 58 | } | |
| 70 | 59 | } | |
| 71 | 60 | ||
| 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")) | |
| 75 | 63 | if err != nil { | |
| 76 | 64 | t.Fatal(err) | |
| 77 | 65 | } | |
| 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") | |
| 81 | 68 | } | |
| 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) | |
| 84 | 72 | } | |
| 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) | |
| 88 | 80 | } | |
| 89 | 81 | } | |
| 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) | |
| 92 | 84 | } | |
| 93 | 85 | } | |
| 94 | 86 | ||
| 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} | |
| 97 | 90 | for _, tc := range []struct { | |
| 98 | - | name string | |
| 99 | - | mutate func(*Manifest) | |
| 91 | + | name string | |
| 92 | + | cols, rows uint16 | |
| 93 | + | wantErr bool | |
| 100 | 94 | }{ | |
| 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}, | |
| 106 | 98 | } { | |
| 107 | 99 | 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) | |
| 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 | + | } | |
| @@ -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 string | 91 | + | 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 := valid | 100 | + | 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 { | |||
| 31 | 31 | Tail []byte // mandatory nonempty canonical VT blob | |
| 32 | 32 | Visible []byte // mandatory nonempty canonical VT blob | |
| 33 | 33 | 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 | 36 | // Process captures restorable foreground process metadata. | |
| @@ -31,11 +31,6 @@ type Pane struct { | |||||
| 31 | Tail []byte // mandatory nonempty canonical VT blob | 31 | Tail []byte // mandatory nonempty canonical VT blob | ||
| 32 | Visible []byte // mandatory nonempty canonical VT blob | 32 | Visible []byte // mandatory nonempty canonical VT blob | ||
| 33 | Process *Process | 33 | 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. | ||