codec.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close