1package snapshotcodec
2
3import (
4 "encoding/binary"
5 "errors"
6 "hash/crc32"
7 "testing"
8
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/domain/layout"
11)
12
13func TestManifestGrammarRoundTrip(t *testing.T) {
14 for _, tc := range []struct {
15 name string
16 tree *layout.Tree
17 proc *Process
18 }{
19 {"split without process", &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, nil},
20 {"stack with process argv", &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Expanded: "one", Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, &Process{Argv: []string{"sh", "-c", "echo"}, Strategy: "restore", Opts: ProcessOpts{AgentSessionID: "agent"}}},
21 } {
22 t.Run(tc.name, func(t *testing.T) {
23 encoded, err := MarshalManifest(testManifest(tc.tree, tc.proc))
24 if err != nil {
25 t.Fatal(err)
26 }
27 got, err := UnmarshalManifest(encoded)
28 if err != nil {
29 t.Fatal(err)
30 }
31 if got.Tabs[0].Tree.Root.Kind != tc.tree.Root.Kind || (got.Tabs[0].Panes[0].Process == nil) != (tc.proc == nil) {
32 t.Fatalf("decoded tree/process = %#v/%#v", got.Tabs[0].Tree, got.Tabs[0].Panes[0].Process)
33 }
34 })
35 }
36}
37
38func TestManifestEnvelopePreflightParity(t *testing.T) {
39 encoded, err := MarshalManifest(testManifest(layout.NewTree("one"), nil))
40 if err != nil {
41 t.Fatal(err)
42 }
43 nameOffset, nodeKindOffset := manifestFieldOffsets(t, encoded)
44 mutations := []struct {
45 name string
46 edit func([]byte) []byte
47 }{
48 {"valid", func(b []byte) []byte { return b }},
49 {"magic", func(b []byte) []byte { b[0] ^= 1; return b }},
50 {"version", func(b []byte) []byte { binary.BigEndian.PutUint16(b[4:6], ManifestVersion+1); return b }},
51 {"flags", func(b []byte) []byte { b[7] = 1; return b }},
52 {"body length", func(b []byte) []byte { binary.BigEndian.PutUint32(b[8:12], uint32(len(b))); return b }},
53 {"crc", func(b []byte) []byte { b[12] ^= 1; return b }},
54 {"trailing", func(b []byte) []byte { return append(b, 0) }},
55 {"body name", func(b []byte) []byte {
56 b[nameOffset] = 0
57 binary.BigEndian.PutUint32(b[12:16], checksum(b[16:]))
58 return b
59 }},
60 {"body generation", func(b []byte) []byte {
61 clear(b[manifestHeaderSize : manifestHeaderSize+8])
62 binary.BigEndian.PutUint32(b[12:16], checksum(b[16:]))
63 return b
64 }},
65 {"body node kind", func(b []byte) []byte {
66 b[nodeKindOffset] = 99
67 binary.BigEndian.PutUint32(b[12:16], checksum(b[16:]))
68 return b
69 }},
70 }
71 for _, tc := range mutations {
72 t.Run(tc.name, func(t *testing.T) {
73 candidate := tc.edit(append([]byte(nil), encoded...))
74 _, decodeErr := UnmarshalManifest(candidate)
75 preflightErr := preflightManifestEnvelope(candidate)
76 uncheckedErr := decodeManifestEnvelope(candidate)
77 if (decodeErr == nil) != (preflightErr == nil) || (preflightErr == nil) != (uncheckedErr == nil) {
78 t.Fatalf("preflight error = %v, unchecked decode error = %v, decode error = %v", preflightErr, uncheckedErr, decodeErr)
79 }
80 })
81 }
82}
83
84func manifestFieldOffsets(t *testing.T, encoded []byte) (nameOffset, nodeKindOffset int) {
85 t.Helper()
86 r := payloadReader{b: encoded[manifestHeaderSize:]}
87 must := func(err error) {
88 if err != nil {
89 t.Fatal(err)
90 }
91 }
92 _, err := r.getUint64()
93 must(err)
94 if len(r.b) < len(domain.IncarnationID{}) {
95 t.Fatalf("manifest body has %d incarnation bytes, want at least %d", len(r.b), len(domain.IncarnationID{}))
96 }
97 r.b = r.b[len(domain.IncarnationID{}):]
98 _, err = r.getUint8()
99 must(err)
100 nameLengthOffset := len(encoded) - len(r.b)
101 name, err := r.getString()
102 must(err)
103 nameOffset = nameLengthOffset + 2
104 if len(name) == 0 {
105 t.Fatal("test manifest has empty name")
106 }
107 _, err = r.getUint64()
108 must(err)
109 _, err = r.getUint16()
110 must(err)
111 _, err = r.getUint16()
112 must(err)
113 _, err = r.getString()
114 must(err)
115 _, err = r.getUint16()
116 must(err)
117 _, err = r.getUint16()
118 must(err)
119 _, err = r.getUint64()
120 must(err)
121 _, err = r.getString()
122 must(err)
123 nodeKindOffset = len(encoded) - len(r.b)
124 const wantName = "named"
125 if nameOffset < 0 || nameOffset+len(wantName) > len(encoded) || string(encoded[nameOffset:nameOffset+len(wantName)]) != wantName {
126 t.Fatalf("name offset %d does not point to %q", nameOffset, wantName)
127 }
128 if nodeKindOffset < 0 || nodeKindOffset >= len(encoded) || encoded[nodeKindOffset] != manifestNodeLeaf {
129 t.Fatalf("node kind offset %d does not point to leaf kind", nodeKindOffset)
130 }
131 return nameOffset, nodeKindOffset
132}
133
134func TestManifestObjectRefKinds(t *testing.T) {
135 for _, tc := range []struct {
136 name string
137 got ObjectKind
138 want ObjectKind
139 valid bool
140 }{
141 {"chunk", HistoryChunk, HistoryChunk, true},
142 {"tail", HistoryTail, HistoryTail, true},
143 {"transcript", RecoveryTranscript, RecoveryTranscript, true},
144 {"invalid", ObjectKind(99), RecoveryTranscript, false},
145 {"wrong role", HistoryTail, RecoveryTranscript, false},
146 } {
147 t.Run(tc.name, func(t *testing.T) {
148 var w payloadWriter
149 writeObjectRef(&w, ObjectRef{Kind: tc.got, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize})
150 err := preflightObjectRef(&payloadReader{b: w.b}, tc.want)
151 if (err == nil) != tc.valid {
152 t.Fatalf("preflightObjectRef() error = %v, valid=%v", err, tc.valid)
153 }
154 })
155 }
156}
157
158func TestManifestPreflightHostileDeclarations(t *testing.T) {
159 t.Run("tab count", func(t *testing.T) {
160 var w payloadWriter
161 w.putUint64(1)
162 if err := w.putString("name"); err != nil {
163 t.Fatalf("putString(name): %v", err)
164 }
165 w.putUint64(1)
166 w.putUint16(0)
167 w.putUint16(^uint16(0))
168 if err := preflightManifest(w.b); !errors.Is(err, ErrInvalidData) {
169 t.Fatalf("preflightManifest() error = %v", err)
170 }
171 })
172 for _, tc := range []struct {
173 name string
174 fn func(*payloadReader, *manifestPreflightBudget) error
175 body []byte
176 }{
177 {"string length", func(r *payloadReader, b *manifestPreflightBudget) error { return skipManifestString(r, b) }, []byte{0, 1}},
178 {"tree children", func(r *payloadReader, b *manifestPreflightBudget) error {
179 return preflightManifestNode(r, 0, true, b, nil)
180 }, []byte{manifestNodeSplit, byte(layout.Horizontal), 0xff, 0xff}},
181 {"nil child", func(r *payloadReader, b *manifestPreflightBudget) error {
182 return preflightManifestNode(r, 1, false, b, nil)
183 }, []byte{manifestNodeNil}},
184 {"process declaration", func(r *payloadReader, b *manifestPreflightBudget) error { return preflightManifestProcess(r, b) }, []byte{processPresent, 0, 1, 0, 2, 'x'}},
185 } {
186 t.Run(tc.name, func(t *testing.T) {
187 err := tc.fn(&payloadReader{b: tc.body}, &manifestPreflightBudget{})
188 if err == nil {
189 t.Fatal("hostile declaration accepted")
190 }
191 })
192 }
193}
194
195func TestSkipManifestStringDoesNotAllocate(t *testing.T) {
196 raw := make([]byte, 2+1024)
197 binary.BigEndian.PutUint16(raw, 1024)
198 if allocations := testing.AllocsPerRun(100, func() {
199 r := payloadReader{b: raw}
200 if err := skipManifestString(&r, &manifestPreflightBudget{}); err != nil {
201 t.Fatal(err)
202 }
203 }); allocations != 0 {
204 t.Fatalf("skipManifestString allocations = %v, want 0", allocations)
205 }
206}
207
208func preflightManifestEnvelope(encoded []byte) error {
209 body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
210 if err != nil {
211 return err
212 }
213 return preflightManifest(body)
214}
215
216func decodeManifestEnvelope(encoded []byte) error {
217 body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
218 if err != nil {
219 return err
220 }
221 _, err = decodeManifest(body)
222 return err
223}
224
225func checksum(body []byte) uint32 {
226 return crc32.ChecksumIEEE(body)
227}
228
229func testManifest(tree *layout.Tree, process *Process) Manifest {
230 return Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "named", Tabs: []ManifestTab{{
231 StableID: "tab", Cols: 80, Rows: 24, Focus: "one", Tree: tree,
232 Panes: []ManifestPane{
233 {ID: "one", StableID: "one", Cwd: "/one", Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize}}, Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize}, Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{3}, Size: minObjectEnvelopeSize}, Process: process},
234 {ID: "two", StableID: "two", Cwd: "/two", Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{4}, Size: minObjectEnvelopeSize}, Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{5}, Size: minObjectEnvelopeSize}},
235 },
236 }}}
237}