codec.go RawHistoryFolder

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}

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