1package snapshotcodec
2
3import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "hash/crc32"
8 "math"
9
10 "github.com/bnema/vev/internal/domain/layout"
11)
12
13var (
14 ErrBadMagic = errors.New("snapshot: bad magic")
15 ErrBadVersion = errors.New("snapshot: bad version")
16 ErrBadCRC = errors.New("snapshot: bad crc")
17 ErrShortPayload = errors.New("snapshot: payload too short")
18 ErrTrailingBytes = errors.New("snapshot: trailing bytes")
19 ErrUnknownPane = errors.New("snapshot: tree references unknown pane")
20 ErrInvalidData = errors.New("snapshot: invalid data")
21)
22
23const (
24 magic = "VEVS"
25 version = uint16(5) // Compact VTC1 payloads; dense checkpoints are incompatible.
26 maxDecodedBodySize = 256 << 20
27 maxSnapshotObjects = 1 << 16
28 maxSnapshotBytes = 256 << 20
29 maxSnapshotRows = 1 << 20
30 maxSnapshotCells = 16 << 20
31 maxSnapshotTreeNodes = 1 << 15
32 // 256 MiB accommodates a full 10k-row history at practical terminal widths
33 // while bounding hostile manifests before decoded slices are allocated.
34 maxSnapshotDecodedAllocation = 256 << 20
35)
36
37// MaxGenerationPayloadBytes bounds the inflated object payloads of one
38// incremental generation, the same budget an uncompressed checkpoint had.
39const MaxGenerationPayloadBytes = 256 << 20
40
41// Unmarshal first measures the complete manifest and its VT blobs without
42// allocating. After accepting aggregate budgets, it validates references and
43// blob semantics before decoding the session.
44func Unmarshal(b []byte) (Session, error) {
45 if len(b) < 16 {
46 return Session{}, ErrShortPayload
47 }
48 if string(b[:4]) != magic {
49 return Session{}, ErrBadMagic
50 }
51 if binary.BigEndian.Uint16(b[4:6]) != version {
52 return Session{}, ErrBadVersion
53 }
54 if binary.BigEndian.Uint16(b[6:8]) != 0 {
55 return Session{}, fmt.Errorf("%w: flags", ErrInvalidData)
56 }
57 bodyLen := binary.BigEndian.Uint32(b[8:12])
58 if bodyLen > maxDecodedBodySize {
59 return Session{}, fmt.Errorf("%w: body too large", ErrInvalidData)
60 }
61 if uint64(len(b)) < 16+uint64(bodyLen) {
62 return Session{}, ErrShortPayload
63 }
64 if uint64(len(b)) != 16+uint64(bodyLen) {
65 return Session{}, ErrTrailingBytes
66 }
67 body := b[16:]
68 if crc32.ChecksumIEEE(body) != binary.BigEndian.Uint32(b[12:16]) {
69 return Session{}, ErrBadCRC
70 }
71 if err := preflightSession(body); err != nil {
72 return Session{}, err
73 }
74 r := payloadReader{b: body}
75 s, err := readSession(&r)
76 if err != nil {
77 return Session{}, err
78 }
79 if err := r.done(); err != nil {
80 return Session{}, err
81 }
82 for _, tab := range s.Tabs {
83 ids := make(map[layout.PaneID]struct{}, len(tab.Panes))
84 for _, p := range tab.Panes {
85 ids[p.ID] = struct{}{}
86 }
87 if _, ok := ids[tab.Focus]; tab.Focus != "" && !ok {
88 return Session{}, ErrUnknownPane
89 }
90 if err := validateTree(tab.Tree, ids); err != nil {
91 return Session{}, err
92 }
93 }
94 return s, nil
95}
96
97type payloadWriter struct{ b []byte }
98
99func (w *payloadWriter) putUint8(v uint8) { w.b = append(w.b, v) }
100func (w *payloadWriter) putUint16(v uint16) {
101 var t [2]byte
102 binary.BigEndian.PutUint16(t[:], v)
103 w.b = append(w.b, t[:]...)
104}
105func (w *payloadWriter) putUint32(v uint32) {
106 var t [4]byte
107 binary.BigEndian.PutUint32(t[:], v)
108 w.b = append(w.b, t[:]...)
109}
110func (w *payloadWriter) putUint64(v uint64) {
111 var t [8]byte
112 binary.BigEndian.PutUint64(t[:], v)
113 w.b = append(w.b, t[:]...)
114}
115func (w *payloadWriter) putString(s string) error {
116 if len(s) > math.MaxUint16 {
117 return fmt.Errorf("%w: string too long", ErrInvalidData)
118 }
119 w.putUint16(uint16(len(s)))
120 w.b = append(w.b, s...)
121 return nil
122}
123func (w *payloadWriter) putStrings(ss []string) error {
124 if len(ss) > math.MaxUint16 {
125 return fmt.Errorf("%w: too many strings", ErrInvalidData)
126 }
127 w.putUint16(uint16(len(ss)))
128 for _, s := range ss {
129 if err := w.putString(s); err != nil {
130 return err
131 }
132 }
133 return nil
134}
135
136func writeNode(w *payloadWriter, n *layout.Node) error {
137 if n == nil {
138 w.putUint8(manifestNodeNil)
139 return nil
140 }
141 switch n.Kind {
142 case layout.Leaf:
143 w.putUint8(manifestNodeLeaf)
144 return w.putString(string(n.Leaf))
145 case layout.Split:
146 w.putUint8(manifestNodeSplit)
147 w.putUint8(uint8(n.Dir))
148 return writeChildren(w, n.Children)
149 case layout.Stack:
150 w.putUint8(manifestNodeStack)
151 if err := w.putString(string(n.Expanded)); err != nil {
152 return err
153 }
154 return writeChildren(w, n.Children)
155 default:
156 return fmt.Errorf("%w: unknown node kind", ErrInvalidData)
157 }
158}
159
160func writeChildren(w *payloadWriter, children []*layout.Node) error {
161 if len(children) > math.MaxUint16 {
162 return fmt.Errorf("%w: too many children", ErrInvalidData)
163 }
164 w.putUint16(uint16(len(children)))
165 for _, c := range children {
166 if err := writeNode(w, c); err != nil {
167 return err
168 }
169 }
170 return nil
171}
172
173func writeProcess(w *payloadWriter, p *Process) error {
174 if p == nil {
175 w.putUint8(processAbsent)
176 return nil
177 }
178 if len(p.Argv) == 0 {
179 return fmt.Errorf("%w: process argv empty", ErrInvalidData)
180 }
181 w.putUint8(processPresent)
182 if err := w.putStrings(p.Argv); err != nil {
183 return err
184 }
185 if err := w.putString(p.Strategy); err != nil {
186 return err
187 }
188 return w.putString(p.Opts.AgentSessionID)
189}
190
191type payloadReader struct{ b []byte }
192
193func (r *payloadReader) getBytes(n int) ([]byte, error) {
194 if n < 0 || len(r.b) < n {
195 return nil, ErrShortPayload
196 }
197 b := r.b[:n]
198 r.b = r.b[n:]
199 return b, nil
200}
201
202func (r *payloadReader) getUint8() (uint8, error) {
203 if len(r.b) < 1 {
204 return 0, ErrShortPayload
205 }
206 v := r.b[0]
207 r.b = r.b[1:]
208 return v, nil
209}
210func (r *payloadReader) getUint16() (uint16, error) {
211 if len(r.b) < 2 {
212 return 0, ErrShortPayload
213 }
214 v := binary.BigEndian.Uint16(r.b)
215 r.b = r.b[2:]
216 return v, nil
217}
218func (r *payloadReader) getUint32() (uint32, error) {
219 if len(r.b) < 4 {
220 return 0, ErrShortPayload
221 }
222 v := binary.BigEndian.Uint32(r.b)
223 r.b = r.b[4:]
224 return v, nil
225}
226func (r *payloadReader) getUint64() (uint64, error) {
227 if len(r.b) < 8 {
228 return 0, ErrShortPayload
229 }
230 v := binary.BigEndian.Uint64(r.b)
231 r.b = r.b[8:]
232 return v, nil
233}
234func (r *payloadReader) getString() (string, error) {
235 n, err := r.getUint16()
236 if err != nil {
237 return "", err
238 }
239 if len(r.b) < int(n) {
240 return "", ErrShortPayload
241 }
242 s := string(r.b[:n])
243 r.b = r.b[n:]
244 return s, nil
245}
246func (r *payloadReader) getStrings() ([]string, error) {
247 n, err := r.getUint16()
248 if err != nil {
249 return nil, err
250 }
251 out := make([]string, n)
252 for i := range out {
253 s, err := r.getString()
254 if err != nil {
255 return nil, err
256 }
257 out[i] = s
258 }
259 return out, nil
260}
261func (r *payloadReader) getBlob() ([]byte, error) {
262 n, err := r.getUint32()
263 if err != nil {
264 return nil, err
265 }
266 if uint64(n) > uint64(len(r.b)) {
267 return nil, ErrShortPayload
268 }
269 blob := append([]byte(nil), r.b[:n]...)
270 r.b = r.b[n:]
271 return blob, nil
272}
273func (r *payloadReader) done() error {
274 if len(r.b) != 0 {
275 return ErrTrailingBytes
276 }
277 return nil
278}
279
280func readSession(r *payloadReader) (Session, error) {
281 name, err := r.getString()
282 if err != nil {
283 return Session{}, err
284 }
285 created, err := r.getUint64()
286 if err != nil {
287 return Session{}, err
288 }
289 active, err := r.getUint16()
290 if err != nil {
291 return Session{}, err
292 }
293 nt, err := r.getUint16()
294 if err != nil {
295 return Session{}, err
296 }
297 if err := validateActive(active, int(nt)); err != nil {
298 return Session{}, err
299 }
300 s := Session{Name: name, CreatedAt: created, Active: active, Tabs: make([]Tab, nt)}
301 for i := range s.Tabs {
302 t, err := readTab(r)
303 if err != nil {
304 return Session{}, err
305 }
306 s.Tabs[i] = t
307 }
308 return s, nil
309}
310
311func readTab(r *payloadReader) (Tab, error) {
312 stableID, err := r.getString()
313 if err != nil {
314 return Tab{}, err
315 }
316 cols, err := r.getUint16()
317 if err != nil {
318 return Tab{}, err
319 }
320 rows, err := r.getUint16()
321 if err != nil {
322 return Tab{}, err
323 }
324 next, err := r.getUint64()
325 if err != nil {
326 return Tab{}, err
327 }
328 focus, err := r.getString()
329 if err != nil {
330 return Tab{}, err
331 }
332 root, err := readNode(r)
333 if err != nil {
334 return Tab{}, err
335 }
336 np, err := r.getUint16()
337 if err != nil {
338 return Tab{}, err
339 }
340 t := 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, np)}
341 for i := range t.Panes {
342 p, err := readPane(r)
343 if err != nil {
344 return Tab{}, err
345 }
346 t.Panes[i] = p
347 }
348 return t, nil
349}
350
351func readNode(r *payloadReader) (*layout.Node, error) {
352 kind, err := r.getUint8()
353 if err != nil {
354 return nil, err
355 }
356 switch kind {
357 case manifestNodeLeaf:
358 leaf, err := r.getString()
359 if err != nil {
360 return nil, err
361 }
362 return layout.NewLeaf(layout.PaneID(leaf)), nil
363 case manifestNodeSplit:
364 dir, err := r.getUint8()
365 if err != nil {
366 return nil, err
367 }
368 children, err := readChildren(r)
369 if err != nil {
370 return nil, err
371 }
372 return &layout.Node{Kind: layout.Split, Dir: layout.SplitDir(dir), Children: children}, nil
373 case manifestNodeStack:
374 exp, err := r.getString()
375 if err != nil {
376 return nil, err
377 }
378 children, err := readChildren(r)
379 if err != nil {
380 return nil, err
381 }
382 return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil
383 case manifestNodeNil:
384 return nil, nil
385 default:
386 return nil, fmt.Errorf("%w: unknown node kind", ErrInvalidData)
387 }
388}
389
390func readChildren(r *payloadReader) ([]*layout.Node, error) {
391 n, err := r.getUint16()
392 if err != nil {
393 return nil, err
394 }
395 out := make([]*layout.Node, n)
396 for i := range out {
397 c, err := readNode(r)
398 if err != nil {
399 return nil, err
400 }
401 out[i] = c
402 }
403 return out, nil
404}
405
406func readPane(r *payloadReader) (Pane, error) {
407 id, err := r.getString()
408 if err != nil {
409 return Pane{}, err
410 }
411 stableID, err := r.getString()
412 if err != nil {
413 return Pane{}, err
414 }
415 cwd, err := r.getString()
416 if err != nil {
417 return Pane{}, err
418 }
419 n, err := r.getUint32()
420 if err != nil {
421 return Pane{}, err
422 }
423 if n > maxSnapshotObjects || uint64(n) > uint64(len(r.b))/4 {
424 return Pane{}, fmt.Errorf("%w: sealed chunks", ErrInvalidData)
425 }
426 sealed := make([][]byte, n)
427 for i := range sealed {
428 if sealed[i], err = r.getBlob(); err != nil {
429 return Pane{}, err
430 }
431 }
432 tail, err := r.getBlob()
433 if err != nil {
434 return Pane{}, err
435 }
436 transcript, err := r.getBlob()
437 if err != nil {
438 return Pane{}, err
439 }
440 proc, err := readProcess(r)
441 if err != nil {
442 return Pane{}, err
443 }
444 return Pane{ID: layout.PaneID(id), StableID: stableID, Cwd: cwd, SealedChunks: sealed, Tail: tail, Transcript: transcript, Process: proc}, nil
445}
446
447func readProcess(r *payloadReader) (*Process, error) {
448 present, err := r.getUint8()
449 if err != nil {
450 return nil, err
451 }
452 if present == processAbsent {
453 return nil, nil
454 }
455 if present != processPresent {
456 return nil, fmt.Errorf("%w: process presence", ErrInvalidData)
457 }
458 argv, err := r.getStrings()
459 if err != nil {
460 return nil, err
461 }
462 if len(argv) == 0 {
463 return nil, fmt.Errorf("%w: process argv empty", ErrInvalidData)
464 }
465 strategy, err := r.getString()
466 if err != nil {
467 return nil, err
468 }
469 agentSessionID, err := r.getString()
470 if err != nil {
471 return nil, err
472 }
473 return &Process{Argv: argv, Strategy: strategy, Opts: ProcessOpts{AgentSessionID: agentSessionID}}, nil
474}
475
476func validateActive(active uint16, tabCount int) error {
477 if tabCount == 0 {
478 if active == 0 {
479 return nil
480 }
481 return fmt.Errorf("%w: active tab", ErrInvalidData)
482 }
483 if int(active) >= tabCount {
484 return fmt.Errorf("%w: active tab", ErrInvalidData)
485 }
486 return nil
487}
488
489// validateTree checks that every pane reference in the layout tree resolves
490// to a pane declared by the enclosing tab.
491func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {
492 if t == nil || t.Root == nil {
493 return nil
494 }
495 return validateNode(t.Root, ids)
496}
497
498func validateNode(n *layout.Node, ids map[layout.PaneID]struct{}) error {
499 if n == nil {
500 return fmt.Errorf("%w: nil child node", ErrInvalidData)
501 }
502 switch n.Kind {
503 case layout.Leaf:
504 if _, ok := ids[n.Leaf]; !ok {
505 return ErrUnknownPane
506 }
507 case layout.Split:
508 if n.Dir != layout.Horizontal && n.Dir != layout.Vertical {
509 return fmt.Errorf("%w: split dir", ErrInvalidData)
510 }
511 for _, c := range n.Children {
512 if err := validateNode(c, ids); err != nil {
513 return err
514 }
515 }
516 case layout.Stack:
517 if _, ok := ids[n.Expanded]; n.Expanded != "" && !ok {
518 return ErrUnknownPane
519 }
520 for _, c := range n.Children {
521 if err := validateNode(c, ids); err != nil {
522 return err
523 }
524 }
525 default:
526 return fmt.Errorf("%w: node kind", ErrInvalidData)
527 }
528 return nil
529}