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