1package snapshotcodec
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "hash/crc32"
8 "math"
9 "strconv"
10 "testing"
11
12 vt "github.com/bnema/vev-vt"
13 renderer "github.com/bnema/vev-vt/ansi"
14 "github.com/bnema/vev/internal/domain/layout"
15)
16
17func TestPayloadReaderGetBytes(t *testing.T) {
18 for _, tc := range []struct {
19 name string
20 input []byte
21 n int
22 want []byte
23 remaining []byte
24 wantErr error
25 }{
26 {name: "reads and advances", input: []byte{1, 2, 3}, n: 2, want: []byte{1, 2}, remaining: []byte{3}},
27 {name: "reads empty", input: []byte{1}, n: 0, want: []byte{}, remaining: []byte{1}},
28 {name: "short payload does not advance", input: []byte{1}, n: 2, remaining: []byte{1}, wantErr: ErrShortPayload},
29 } {
30 t.Run(tc.name, func(t *testing.T) {
31 r := payloadReader{b: append([]byte(nil), tc.input...)}
32 got, err := r.getBytes(tc.n)
33 if !errors.Is(err, tc.wantErr) {
34 t.Fatalf("getBytes(%d) error = %v, want %v", tc.n, err, tc.wantErr)
35 }
36 if !bytes.Equal(got, tc.want) {
37 t.Fatalf("getBytes(%d) = %v, want %v", tc.n, got, tc.want)
38 }
39 if !bytes.Equal(r.b, tc.remaining) {
40 t.Fatalf("remaining bytes = %v, want %v", r.b, tc.remaining)
41 }
42 })
43 }
44}
45
46func TestV4SnapshotRoundTripPreservesCanonicalTerminalData(t *testing.T) {
47 indexed := renderer.DefaultStyle()
48 indexed.Bold, indexed.Inverse = true, true
49 indexed.Attrs = renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough
50 indexed.Foreground, indexed.Background = 196, 17
51 indexed.UnderlineStyle, indexed.HasUnderlineColor, indexed.UnderlineColor = renderer.UnderlineCurly, true, 203
52 rgb := renderer.DefaultStyle()
53 rgb.Italic, rgb.HasForegroundRGB, rgb.ForegroundRGB = true, true, renderer.RGB{R: 1, G: 2, B: 3}
54 rgb.HasBackgroundRGB, rgb.BackgroundRGB = true, renderer.RGB{R: 4, G: 5, B: 6}
55 rgb.UnderlineStyle, rgb.HasUnderlineColorRGB, rgb.UnderlineColorRGB = renderer.UnderlineDashed, true, renderer.RGB{R: 7, G: 8, B: 9}
56 sealed, tail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'I', Style: indexed}, {Rune: '好', Style: rgb}, {Continuation: true, Style: rgb}}})
57 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'V', Style: rgb}, {Rune: '界', Style: indexed}, {Continuation: true, Style: indexed}}})
58 sealed2, tail2 := historyBlobs(t, [][]renderer.Cell{{{Rune: 'S', Style: rgb}}, {{Rune: '2', Style: indexed}}})
59 transcript2 := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'P', Style: indexed}, {Rune: '2', Style: rgb}}})
60 want := Session{Name: "v4 exact", Tabs: []Tab{{Focus: "p", Tree: &layout.Tree{Focus: "p", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("p"), layout.NewLeaf("p2")}}}, Panes: []Pane{
61 {ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript},
62 {ID: "p2", SealedChunks: sealed2, Tail: tail2, Transcript: transcript2},
63 }}}}
64 encoded, err := marshalTest(want)
65 if err != nil {
66 t.Fatal(err)
67 }
68 requireV4Envelope(t, encoded)
69 got, err := Unmarshal(encoded)
70 if err != nil {
71 t.Fatal(err)
72 }
73 if len(got.Tabs) != 1 || len(got.Tabs[0].Panes) != 2 || got.Tabs[0].Panes[0].ID != "p" || got.Tabs[0].Panes[1].ID != "p2" {
74 t.Fatalf("manifest round trip = %#v", got)
75 }
76 for i, wantPane := range want.Tabs[0].Panes {
77 gotPane := got.Tabs[0].Panes[i]
78 if len(gotPane.SealedChunks) != len(wantPane.SealedChunks) {
79 t.Fatalf("pane %q sealed chunks = %d, want %d", wantPane.ID, len(gotPane.SealedChunks), len(wantPane.SealedChunks))
80 }
81 for j := range wantPane.SealedChunks {
82 if !bytes.Equal(gotPane.SealedChunks[j], wantPane.SealedChunks[j]) {
83 t.Fatalf("pane %q sealed chunk %d changed during round trip", wantPane.ID, j)
84 }
85 }
86 if !bytes.Equal(gotPane.Tail, wantPane.Tail) {
87 t.Fatalf("pane %q tail changed during round trip", wantPane.ID)
88 }
89 if !bytes.Equal(gotPane.Transcript, wantPane.Transcript) {
90 t.Fatalf("pane %q transcript blob changed during round trip", wantPane.ID)
91 }
92 }
93 if _, err := vt.HistoryFromBlobs(vt.HistoryConfig{MaxRows: 8, ChunkRows: 2}, got.Tabs[0].Panes[0].SealedChunks, got.Tabs[0].Panes[0].Tail); err != nil {
94 t.Fatalf("history decode: %v", err)
95 }
96 view, err := vt.UnmarshalHistory(got.Tabs[0].Panes[0].Transcript)
97 if err != nil {
98 t.Fatal(err)
99 }
100 if !view.Row(0)[0].Equal(renderer.Cell{Rune: 'V', Style: rgb}) || !view.Row(0)[2].Continuation {
101 t.Fatalf("transcript terminal data lost: %#v", view.Row(0))
102 }
103}
104
105func TestActiveTabReferenceMustBeExact(t *testing.T) {
106 validTab := Tab{}
107 for _, tc := range []struct {
108 name string
109 s Session
110 valid bool
111 }{
112 {name: "zero tabs active zero", s: Session{Active: 0}, valid: true},
113 {name: "zero tabs active nonzero", s: Session{Active: 1}},
114 {name: "nonempty tabs active in range", s: Session{Active: 0, Tabs: []Tab{validTab}}, valid: true},
115 {name: "nonempty tabs active past end", s: Session{Active: 1, Tabs: []Tab{validTab}}},
116 } {
117 t.Run("marshal "+tc.name, func(t *testing.T) {
118 _, err := marshalTest(tc.s)
119 if tc.valid {
120 requireNoError(t, err)
121 return
122 }
123 if !errors.Is(err, ErrInvalidData) {
124 t.Fatalf("marshalTest() error = %v, want %v", err, ErrInvalidData)
125 }
126 })
127 }
128
129 for _, tc := range []struct {
130 name string
131 data []byte
132 }{
133 {name: "zero tabs active nonzero", data: v4ActiveManifest(1, 0)},
134 {name: "nonempty tabs active past end", data: v4ActiveManifest(1, 1)},
135 } {
136 t.Run("unmarshal "+tc.name, func(t *testing.T) {
137 reject(t, v4Envelope(version, 0, tc.data), ErrInvalidData)
138 })
139 }
140}
141
142func TestUnmarshalRejectsV4LegacyVersions(t *testing.T) {
143 for _, version := range []uint16{1, 2, 3, 4} {
144 if _, err := Unmarshal(v4Envelope(version, 0, nil)); !errors.Is(err, ErrBadVersion) {
145 t.Fatalf("version %d: %v", version, err)
146 }
147 }
148}
149
150func TestUnmarshalRejectsMalformedV4EnvelopeBeforeAllocation(t *testing.T) {
151 body := v4Manifest(0, nil, nil, 0, nil, 0, nil)
152 valid := v4Envelope(version, 0, body)
153 badCRC := append([]byte(nil), valid...)
154 badCRC[15] ^= 1
155 for _, tc := range []struct {
156 name string
157 data []byte
158 want error
159 }{
160 {"short", valid[:15], ErrShortPayload}, {"magic", append([]byte("NOPE"), valid[4:]...), ErrBadMagic},
161 {"future", v4Envelope(version+1, 0, body), ErrBadVersion}, {"flags", v4Envelope(version, 1, body), ErrInvalidData},
162 {"crc", badCRC, ErrBadCRC}, {"body", v4EnvelopeWithLength(version, 0, nil, math.MaxUint32), ErrInvalidData},
163 } {
164 t.Run(tc.name, func(t *testing.T) { reject(t, tc.data, tc.want) })
165 }
166}
167
168func TestUnmarshalRejectsHostileV4ManifestDeclarationsBeforeAllocation(t *testing.T) {
169 for _, tc := range []struct {
170 name string
171 body []byte
172 }{
173 {"count", v4Manifest(math.MaxUint32, nil, nil, 0, nil, 0, nil)},
174 {"sealed", v4Manifest(1, []uint32{math.MaxUint32}, nil, 0, nil, 0, nil)},
175 {"tail", v4Manifest(0, nil, nil, math.MaxUint32, nil, 0, nil)},
176 {"transcript", v4Manifest(0, nil, nil, 0, nil, math.MaxUint32, nil)},
177 } {
178 t.Run(tc.name, func(t *testing.T) { reject(t, v4Envelope(version, 0, tc.body), ErrInvalidData) })
179 }
180}
181
182func TestUnmarshalRejectsEveryV4PrefixAndTrailingGarbage(t *testing.T) {
183 sealed, tail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'h'}}})
184 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
185 encoded, err := marshalTest(Session{Name: "p", Tabs: []Tab{{Focus: "p", Tree: layout.NewTree("p"), Panes: []Pane{{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript}}}}})
186 if err != nil {
187 t.Fatal(err)
188 }
189 for n := range len(encoded) {
190 reject(t, encoded[:n], nil)
191 }
192 reject(t, append(encoded, 0), ErrTrailingBytes)
193}
194
195func TestUnmarshalRejectsEmptyAndDuplicatePaneIDsDuringPreflight(t *testing.T) {
196 sealed, tail := historyBlobs(t, nil)
197 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
198 for _, tc := range []struct {
199 name string
200 panes []Pane
201 }{
202 {name: "empty", panes: []Pane{{ID: "", Tail: tail, Transcript: transcript}}},
203 {name: "duplicate", panes: []Pane{{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript}, {ID: "p", Tail: tail, Transcript: transcript}}},
204 } {
205 t.Run(tc.name, func(t *testing.T) {
206 data, err := marshalTest(Session{Name: "s", Tabs: []Tab{{Panes: tc.panes}}})
207 requireNoError(t, err)
208 reject(t, data, ErrInvalidData)
209 })
210 }
211}
212
213func TestUnmarshalRejectsInvalidTreeReference(t *testing.T) {
214 sealed, tail := historyBlobs(t, nil)
215 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
216 encoded, err := marshalTest(Session{Name: "p", Tabs: []Tab{{Focus: "missing", Tree: layout.NewTree("missing"), Panes: []Pane{{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript}}}}})
217 if err != nil {
218 t.Fatal(err)
219 }
220 if _, err := Unmarshal(encoded); !errors.Is(err, ErrUnknownPane) {
221 t.Fatalf("error = %v", err)
222 }
223}
224
225func TestUnmarshalEnforcesCanonicalHistoryBlobRoles(t *testing.T) {
226 sealed, emptyTail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'a'}}, {{Rune: 'b'}}, {{Rune: 'c'}}})
227 if len(sealed) < 2 {
228 t.Fatalf("expected at least two sealed chunks, got %d", len(sealed))
229 }
230 multiChunk := append([]byte(nil), sealed[0][:16]...)
231 binary.BigEndian.PutUint32(multiChunk[4:8], 2)
232 binary.BigEndian.PutUint64(multiChunk[8:16], 4)
233 multiChunk = append(multiChunk, sealed[0][16:]...)
234 multiChunk = append(multiChunk, sealed[1][16:]...)
235 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
236 emptyTranscript := transcriptBlob(t, nil)
237
238 // Incremental captures retain one copied mutable tail chunk instead of
239 // rotating it into the sealed history set. That one chunk is canonical and
240 // must remain a valid v4 import payload.
241 mutableHistory := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, ChunkRows: 2})
242 for _, row := range [][]renderer.Cell{{{Rune: 'a'}}, {{Rune: 'b'}}, {{Rune: 'c'}}} {
243 requireNoError(t, mutableHistory.Append(row, vt.LineBound{End: len(row)}))
244 }
245 mutableTail, err := vt.MarshalHistoryTail(mutableHistory.SnapshotView())
246 requireNoError(t, err)
247 fullHistory, err := vt.MarshalHistory(mutableHistory.SealAndView())
248 requireNoError(t, err)
249 for _, tc := range []struct {
250 name string
251 transcript []byte
252 }{
253 {name: "single chunk", transcript: transcript},
254 {name: "empty", transcript: emptyTranscript},
255 {name: "multiple chunks", transcript: multiChunk},
256 } {
257 t.Run("valid "+tc.name, func(t *testing.T) {
258 data, err := marshalTest(Session{Name: "s", Tabs: []Tab{{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", SealedChunks: sealed[:1], Tail: mutableTail, Transcript: tc.transcript}}}}})
259 requireNoError(t, err)
260 roundTrip, err := Unmarshal(data)
261 requireNoError(t, err)
262 if got := roundTrip.Tabs[0].Panes[0].Tail; !bytes.Equal(got, mutableTail) {
263 t.Fatal("mutable tail did not round trip")
264 }
265 })
266 }
267
268 for _, tc := range []struct {
269 name string
270 tab Tab
271 }{
272 {name: "sealed multi-chunk", tab: Tab{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", SealedChunks: [][]byte{multiChunk}, Tail: emptyTail, Transcript: transcript}}}},
273 {name: "tail has multiple chunks", tab: Tab{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", Tail: multiChunk, Transcript: transcript}}}},
274 {name: "noncanonical full history tail", tab: Tab{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", Tail: fullHistory, Transcript: transcript}}}},
275 } {
276 t.Run(tc.name, func(t *testing.T) {
277 data, err := marshalTest(Session{Name: "s", Tabs: []Tab{tc.tab}})
278 requireNoError(t, err)
279 reject(t, data, ErrInvalidData)
280 })
281 }
282}
283
284func TestUnmarshalRejectsMalformedCanonicalVTBlobsWithoutPanic(t *testing.T) {
285 tail := mustHistoryTail(t)
286 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
287 for _, tc := range []struct {
288 name string
289 blob []byte
290 }{
291 {name: "malformed", blob: append([]byte("NOPE"), transcript[4:]...)},
292 {name: "truncated", blob: transcript[:len(transcript)-1]},
293 {name: "trailing", blob: append(append([]byte(nil), transcript...), 0)},
294 } {
295 t.Run(tc.name, func(t *testing.T) {
296 body := v4Manifest(0, nil, nil, uint32(len(tail)), tail, uint32(len(tc.blob)), tc.blob)
297 reject(t, v4Envelope(version, 0, body), ErrInvalidData)
298 })
299 }
300}
301
302func TestRoundTripNilTreeAndRootDoNotMaterializeLeaf(t *testing.T) {
303 for _, tc := range []struct {
304 name string
305 tab Tab
306 }{
307 {name: "nil tree", tab: Tab{Tree: nil}},
308 {name: "nil root", tab: Tab{Tree: &layout.Tree{Root: nil}}},
309 } {
310 t.Run(tc.name, func(t *testing.T) {
311 data, err := marshalTest(Session{Name: "s", Tabs: []Tab{tc.tab}})
312 requireNoError(t, err)
313 got, err := Unmarshal(data)
314 requireNoError(t, err)
315 if got.Tabs[0].Tree == nil || got.Tabs[0].Tree.Root != nil {
316 t.Fatalf("tree = %#v, want nonnil tree with nil root", got.Tabs[0].Tree)
317 }
318 })
319 }
320}
321
322func TestRoundTripSessionMetadataAndProcess(t *testing.T) {
323 sealed, tail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'h'}}, {{Rune: 'i'}}})
324 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
325 want := Session{Name: "named", CreatedAt: 42, Active: 1, Tabs: []Tab{
326 {
327 StableID: "t1", Cols: 100, Rows: 40, NextPaneID: 9, Focus: "2",
328 Tree: &layout.Tree{Focus: "2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{
329 layout.NewLeaf("1"), layout.NewLeaf("2"),
330 }}},
331 Panes: []Pane{
332 {ID: "1", StableID: "p1", Cwd: "/one", SealedChunks: sealed, Tail: tail, Transcript: transcript, Process: &Process{Argv: []string{"pi", "--resume"}, Strategy: "pi", Opts: ProcessOpts{AgentSessionID: "agent-123"}}},
333 {ID: "2", StableID: "p2", Cwd: "/two", SealedChunks: sealed, Tail: tail, Transcript: transcript},
334 },
335 },
336 {StableID: "t2", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "a", Tree: layout.NewTree("a"), Panes: []Pane{{ID: "a", Cwd: "/tmp", SealedChunks: sealed, Tail: tail, Transcript: transcript}}},
337 }}
338 data, err := marshalTest(want)
339 requireNoError(t, err)
340 got, err := Unmarshal(data)
341 requireNoError(t, err)
342 if got.Name != want.Name || got.CreatedAt != want.CreatedAt || got.Active != want.Active || len(got.Tabs) != 2 {
343 t.Fatalf("session metadata round trip = %#v", got)
344 }
345 pane := got.Tabs[0].Panes[0]
346 if pane.ID != "1" || pane.StableID != "p1" || pane.Cwd != "/one" || pane.Process == nil || pane.Process.Opts.AgentSessionID != "agent-123" || got.Tabs[0].Tree.Root.Kind != layout.Split {
347 t.Fatalf("pane metadata round trip = %#v", pane)
348 }
349}
350
351func TestMarshalRejectsProcessWithoutArgv(t *testing.T) {
352 _, err := marshalTest(Session{Name: "s", Tabs: []Tab{
353 {Tree: layout.NewTree("p"), Focus: "p", Panes: []Pane{{ID: "p", Process: &Process{Strategy: "generic"}}}},
354 }})
355 if !errors.Is(err, ErrInvalidData) {
356 t.Fatalf("marshalTest() error = %v, want %v", err, ErrInvalidData)
357 }
358}
359
360func TestUnmarshalRejectsProcessWithoutArgv(t *testing.T) {
361 tail := mustHistoryTail(t)
362 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
363 var w payloadWriter
364 _ = w.putString("s")
365 w.putUint64(0)
366 w.putUint16(0)
367 w.putUint16(1)
368 _ = w.putString("t")
369 w.putUint16(80)
370 w.putUint16(24)
371 w.putUint64(1)
372 _ = w.putString("p")
373 _ = writeNode(&w, layout.NewLeaf("p"))
374 w.putUint16(1)
375 _ = w.putString("p")
376 _ = w.putString("")
377 _ = w.putString("")
378 w.putUint32(0)
379 w.putUint32(uint32(len(tail)))
380 w.b = append(w.b, tail...)
381 w.putUint32(uint32(len(transcript)))
382 w.b = append(w.b, transcript...)
383 w.putUint8(1)
384 w.putUint16(0) // argv count
385 _ = w.putString("generic")
386 _ = w.putString("")
387 if _, err := Unmarshal(v4Envelope(version, 0, w.b)); !errors.Is(err, ErrInvalidData) {
388 t.Fatalf("Unmarshal() error = %v, want %v", err, ErrInvalidData)
389 }
390}
391
392func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) {
393 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256})
394 for range 256 {
395 requireNoError(t, history.Append([]renderer.Cell{{Rune: 'x'}}, vt.LineBound{End: 1}))
396 }
397 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
398 requireNoError(t, err)
399 transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
400
401 const paneCount = maxSnapshotRows/256 + 1
402 tabs := make([]Tab, paneCount)
403 for i := range tabs {
404 tabs[i] = Tab{Panes: []Pane{{ID: layout.PaneID(strconv.Itoa(i)), SealedChunks: sealed, Tail: tail, Transcript: transcript}}}
405 }
406 data, err := marshalTest(Session{Name: "over-budget", Tabs: tabs})
407 requireNoError(t, err)
408
409 allocs := testing.AllocsPerRun(3, func() {
410 _, err := Unmarshal(data)
411 if !errors.Is(err, ErrInvalidData) {
412 panic(err)
413 }
414 })
415 // The error and testing harness may allocate a small fixed amount. Rejecting
416 // before semantic maps/slices are built must not scale with paneCount.
417 if allocs > 8 {
418 t.Fatalf("Unmarshal() allocations = %f, want fixed overhead only", allocs)
419 }
420}
421
422func TestPreflightBudgetRejectsAggregateDecodedAllocation(t *testing.T) {
423 budget := preflightBudget{alloc: maxSnapshotDecodedAllocation - 1}
424 if budget.addProduct(1, 2) {
425 t.Fatal("allocation budget accepted an over-limit aggregate")
426 }
427}
428
429func TestPreflightRejectsBroadTreeBeforeSecondPassAllocation(t *testing.T) {
430 body := v4WideTreeManifest(32_769)
431 if err := preflightSession(body); !errors.Is(err, ErrInvalidData) {
432 t.Fatalf("preflightSession() error = %v, want %v", err, ErrInvalidData)
433 }
434}
435
436func TestUnmarshalRejectsAggregateOpaqueBlobDeclarations(t *testing.T) {
437 body := v4Manifest(math.MaxUint32, nil, nil, math.MaxUint32, nil, math.MaxUint32, nil)
438 reject(t, v4Envelope(version, 0, body), ErrInvalidData)
439}
440
441func mustHistoryTail(t *testing.T) []byte {
442 t.Helper()
443 _, tail := historyBlobs(t, nil)
444 return tail
445}
446
447func requireNoError(t *testing.T, err error) {
448 t.Helper()
449 if err != nil {
450 t.Fatal(err)
451 }
452}
453
454func historyBlobs(t *testing.T, rows [][]renderer.Cell) ([][]byte, []byte) {
455 t.Helper()
456 h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2})
457 for _, row := range rows {
458 requireNoError(t, h.Append(row, vt.LineBound{End: len(row)}))
459 }
460 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())
461 if err != nil {
462 t.Fatal(err)
463 }
464 return sealed, tail
465}
466func transcriptBlob(t *testing.T, rows [][]renderer.Cell) []byte {
467 t.Helper()
468 history := vt.NewHistory(vt.HistoryConfig{MaxRows: max(1, len(rows)), ChunkRows: 2})
469 for _, row := range rows {
470 requireNoError(t, history.Append(row, vt.LineBound{End: len(row)}))
471 }
472 blob, err := vt.MarshalHistory(history.SealAndView())
473 if err != nil {
474 t.Fatal(err)
475 }
476 return blob
477}
478func requireV4Envelope(t *testing.T, data []byte) {
479 t.Helper()
480 if version != 5 || len(data) < 16 || binary.BigEndian.Uint16(data[4:6]) != 5 || binary.BigEndian.Uint16(data[6:8]) != 0 {
481 t.Fatalf("not v4 envelope: % x", data)
482 }
483}
484func reject(t *testing.T, data []byte, want error) {
485 t.Helper()
486 defer func() {
487 if r := recover(); r != nil {
488 t.Fatalf("panic: %v", r)
489 }
490 }()
491 _, err := Unmarshal(data)
492 if err == nil {
493 t.Fatal("accepted malformed input")
494 }
495 if want != nil && !errors.Is(err, want) {
496 t.Fatalf("error = %v, want %v", err, want)
497 }
498}
499func v4Envelope(v, flags uint16, body []byte) []byte {
500 return v4EnvelopeWithLength(v, flags, body, uint32(len(body)))
501}
502func v4EnvelopeWithLength(v, flags uint16, body []byte, length uint32) []byte {
503 out := make([]byte, 16+len(body))
504 copy(out, magic)
505 binary.BigEndian.PutUint16(out[4:6], v)
506 binary.BigEndian.PutUint16(out[6:8], flags)
507 binary.BigEndian.PutUint32(out[8:12], length)
508 binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
509 copy(out[16:], body)
510 return out
511}
512
513func v4ActiveManifest(active, tabs uint16) []byte {
514 var b []byte
515 putS := func(s string) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))); b = append(b, s...) }
516 putS("s")
517 b = append(b, make([]byte, 8)...)
518 b = binary.BigEndian.AppendUint16(b, active)
519 b = binary.BigEndian.AppendUint16(b, tabs)
520 for range tabs {
521 putS("")
522 b = append(b, make([]byte, 2+2+8)...)
523 putS("")
524 b = append(b, 3) // nil root
525 b = binary.BigEndian.AppendUint16(b, 0)
526 }
527 return b
528}
529
530// v4Manifest creates just enough session/tab/pane framing for hostile declarations.
531// v4WideTreeManifest declares a broad but compact tree without allocating a
532// matching layout.Node graph. It exercises preflight's manifest budget.
533func v4WideTreeManifest(children uint16) []byte {
534 var b []byte
535 putS := func(s string) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))); b = append(b, s...) }
536 putS("s")
537 b = append(b, make([]byte, 8+2)...)
538 b = binary.BigEndian.AppendUint16(b, 1)
539 putS("")
540 b = append(b, make([]byte, 2+2+8)...)
541 putS("")
542 b = append(b, 1, byte(layout.Horizontal)) // split node
543 b = binary.BigEndian.AppendUint16(b, children)
544 for range children {
545 b = append(b, 0) // leaf
546 putS("")
547 }
548 b = binary.BigEndian.AppendUint16(b, 0) // panes
549 return b
550}
551
552func v4Manifest(sealed uint32, sealedLens []uint32, sealedData []byte, tailLen uint32, tailData []byte, transcriptLen uint32, transcriptData []byte) []byte {
553 var b []byte
554 putS := func(s string) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))); b = append(b, s...) }
555 putS("s")
556 b = append(b, make([]byte, 8+2)...)
557 b = binary.BigEndian.AppendUint16(b, 1)
558 putS("")
559 b = append(b, make([]byte, 2+2+8)...)
560 putS("p")
561 b = append(b, 0)
562 putS("p")
563 b = binary.BigEndian.AppendUint16(b, 1)
564 putS("p")
565 putS("")
566 putS("")
567 b = binary.BigEndian.AppendUint32(b, sealed)
568 for _, l := range sealedLens {
569 b = binary.BigEndian.AppendUint32(b, l)
570 }
571 b = append(b, sealedData...)
572 b = binary.BigEndian.AppendUint32(b, tailLen)
573 b = append(b, tailData...)
574 b = binary.BigEndian.AppendUint32(b, transcriptLen)
575 b = append(b, transcriptData...)
576 return b
577}