preflight.go RawHistoryFolder

1package snapshotcodec
2
3import (
4	"fmt"
5	"math"
6
7	vt "github.com/bnema/vev-vt"
8	"github.com/bnema/vev/internal/domain/layout"
9)
10
11func preflightSession(body []byte) error {
12	if err := preflightSessionStructure(body); err != nil {
13		return err
14	}
15	return preflightSessionReferences(body)
16}
17
18func preflightSessionStructure(body []byte) error {
19	r := payloadReader{b: body}
20	if err := skipString(&r); err != nil {
21		return err
22	}
23	if _, err := r.getUint64(); err != nil {
24		return err
25	}
26	active, err := r.getUint16()
27	if err != nil {
28		return err
29	}
30	tabs, err := r.getUint16()
31	if err != nil {
32		return err
33	}
34	if err := validateActive(active, int(tabs)); err != nil {
35		return err
36	}
37	if uint64(tabs) > uint64(len(r.b))/2 {
38		return ErrShortPayload
39	}
40
41	var totals vt.DecodeStats
42	blobs := uint64(0)
43	budget := preflightBudget{}
44	for range tabs {
45		if err := preflightTabStructure(&r, &totals, &blobs, &budget); err != nil {
46			return fmt.Errorf("preflight tab: %w", err)
47		}
48	}
49	if err := r.done(); err != nil {
50		return err
51	}
52	if totals.Rows > maxSnapshotRows || totals.Cells > maxSnapshotCells || totals.Bytes > maxSnapshotBytes {
53		return fmt.Errorf("%w: global VT budget", ErrInvalidData)
54	}
55	return nil
56}
57
58func skipString(r *payloadReader) error {
59	n, err := r.getUint16()
60	if err != nil {
61		return err
62	}
63	if uint64(n) > uint64(len(r.b)) {
64		return ErrShortPayload
65	}
66	r.b = r.b[n:]
67	return nil
68}
69
70type preflightBudget struct {
71	nodes uint64
72	alloc uint64
73}
74
75func (b *preflightBudget) addAllocation(amount uint64) bool {
76	if amount > maxSnapshotDecodedAllocation-b.alloc {
77		return false
78	}
79	b.alloc += amount
80	return true
81}
82
83func (b *preflightBudget) addProduct(count, size uint64) bool {
84	if count != 0 && size > math.MaxUint64/count {
85		return false
86	}
87	return b.addAllocation(count * size)
88}
89
90func (b *preflightBudget) addNode() bool {
91	if b.nodes >= maxSnapshotTreeNodes || !b.addAllocation(128) {
92		return false
93	}
94	b.nodes++
95	return true
96}
97
98func preflightTabStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
99	if !budget.addAllocation(256) {
100		return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
101	}
102	if err := skipString(r); err != nil {
103		return err
104	}
105	if _, err := r.getUint16(); err != nil {
106		return err
107	}
108	if _, err := r.getUint16(); err != nil {
109		return err
110	}
111	if _, err := r.getUint64(); err != nil {
112		return err
113	}
114	if err := skipString(r); err != nil {
115		return err
116	}
117	if err := preflightNodeStructure(r, 0, true, budget); err != nil {
118		return err
119	}
120	panes, err := r.getUint16()
121	if err != nil {
122		return err
123	}
124	if uint64(panes) > uint64(len(r.b))/2 {
125		return ErrShortPayload
126	}
127	for range panes {
128		if err := preflightPaneStructure(r, totals, blobs, budget); err != nil {
129			return err
130		}
131	}
132	return nil
133}
134
135func preflightNodeStructure(r *payloadReader, depth int, root bool, budget *preflightBudget) error {
136	if depth > 64 {
137		return fmt.Errorf("%w: tree depth", ErrInvalidData)
138	}
139	if !budget.addNode() {
140		return fmt.Errorf("%w: tree node or decoded allocation budget", ErrInvalidData)
141	}
142	kind, err := r.getUint8()
143	if err != nil {
144		return err
145	}
146	switch kind {
147	case 0:
148		return skipString(r)
149	case 1:
150		dir, err := r.getUint8()
151		if err != nil {
152			return err
153		}
154		if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
155			return fmt.Errorf("%w: split dir", ErrInvalidData)
156		}
157	case 2:
158		if err := skipString(r); err != nil {
159			return err
160		}
161	case 3:
162		if !root {
163			return fmt.Errorf("%w: nil child node", ErrInvalidData)
164		}
165		return nil
166	default:
167		return fmt.Errorf("%w: node kind", ErrInvalidData)
168	}
169	children, err := r.getUint16()
170	if err != nil {
171		return err
172	}
173	if uint64(children) > uint64(len(r.b)) {
174		return ErrShortPayload
175	}
176	for range children {
177		if err := preflightNodeStructure(r, depth+1, false, budget); err != nil {
178			return err
179		}
180	}
181	return nil
182}
183
184func preflightPaneStructure(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) error {
185	if !budget.addAllocation(256) {
186		return fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
187	}
188	if err := skipString(r); err != nil {
189		return err
190	}
191	if err := skipString(r); err != nil {
192		return err
193	}
194	if err := skipString(r); err != nil {
195		return err
196	}
197	n, err := r.getUint32()
198	if err != nil {
199		return err
200	}
201	if n > maxSnapshotObjects || uint64(n) > uint64(len(r.b))/4 {
202		return fmt.Errorf("%w: sealed chunks", ErrInvalidData)
203	}
204	for range n {
205		stats, err := preflightBlob(r, totals, blobs, budget)
206		if err != nil {
207			return err
208		}
209		if stats.Chunks != 1 || stats.Rows == 0 {
210			return fmt.Errorf("%w: sealed blob role", ErrInvalidData)
211		}
212	}
213	tail, err := preflightBlob(r, totals, blobs, budget)
214	if err != nil {
215		return err
216	}
217	// A capture keeps the live mutable tail separate from immutable sealed
218	// chunks. It may therefore contain one partial chunk (or the canonical
219	// empty tail), but never a sequence of sealed chunks.
220	if tail.Chunks > 1 {
221		return fmt.Errorf("%w: tail blob role", ErrInvalidData)
222	}
223	transcript, err := preflightBlob(r, totals, blobs, budget)
224	if err != nil {
225		return err
226	}
227	// Transcripts are canonical history; they may contain multiple chunks or be
228	// empty, so no blob-role assertion applies.
229	_ = transcript
230	return preflightProcess(r)
231}
232
233func preflightBlob(r *payloadReader, totals *vt.DecodeStats, blobs *uint64, budget *preflightBudget) (vt.DecodeStats, error) {
234	n, err := r.getUint32()
235	if err != nil {
236		return vt.DecodeStats{}, err
237	}
238	if n == 0 || uint64(n) > uint64(len(r.b)) || uint64(n) > maxSnapshotBytes {
239		return vt.DecodeStats{}, fmt.Errorf("%w: VT blob length", ErrInvalidData)
240	}
241	if *blobs >= maxSnapshotObjects {
242		return vt.DecodeStats{}, fmt.Errorf("%w: too many VT blobs", ErrInvalidData)
243	}
244	blob := r.b[:n]
245	r.b = r.b[n:]
246	stats, err := vt.MeasureHistoryBlob(blob)
247	if err != nil {
248		return vt.DecodeStats{}, fmt.Errorf("%w: VT blob", ErrInvalidData)
249	}
250	if !totals.Add(stats) {
251		return vt.DecodeStats{}, fmt.Errorf("%w: VT budget overflow", ErrInvalidData)
252	}
253	if !budget.addAllocation(uint64(n)) || !budget.addProduct(stats.Chunks, 64) || !budget.addProduct(stats.Rows, 32) || !budget.addProduct(stats.Cells, 64) {
254		return vt.DecodeStats{}, fmt.Errorf("%w: decoded allocation budget", ErrInvalidData)
255	}
256	*blobs++
257	return stats, nil
258}
259
260func preflightProcess(r *payloadReader) error {
261	present, err := r.getUint8()
262	if err != nil {
263		return err
264	}
265	if present == 0 {
266		return nil
267	}
268	if present != 1 {
269		return fmt.Errorf("%w: process presence", ErrInvalidData)
270	}
271	n, err := r.getUint16()
272	if err != nil {
273		return err
274	}
275	if n == 0 {
276		return fmt.Errorf("%w: process argv empty", ErrInvalidData)
277	}
278	for range n {
279		if err := skipString(r); err != nil {
280			return err
281		}
282	}
283	if err := skipString(r); err != nil {
284		return err
285	}
286	return skipString(r)
287}
288
289// preflightSessionReferences validates duplicate, empty, and dangling pane
290// references only after the structural pass has admitted the complete payload.
291func preflightSessionReferences(body []byte) error {
292	r := payloadReader{b: body}
293	if err := skipString(&r); err != nil {
294		return err
295	}
296	if _, err := r.getUint64(); err != nil {
297		return err
298	}
299	if _, err := r.getUint16(); err != nil {
300		return err
301	}
302	tabs, err := r.getUint16()
303	if err != nil {
304		return err
305	}
306	for range tabs {
307		if err := preflightTabReferences(&r); err != nil {
308			return fmt.Errorf("preflight tab references: %w", err)
309		}
310	}
311	return r.done()
312}
313
314func preflightTabReferences(r *payloadReader) error {
315	if err := skipString(r); err != nil {
316		return err
317	}
318	if _, err := r.getUint16(); err != nil {
319		return err
320	}
321	if _, err := r.getUint16(); err != nil {
322		return err
323	}
324	if _, err := r.getUint64(); err != nil {
325		return err
326	}
327	focus, err := r.getString()
328	if err != nil {
329		return err
330	}
331	references := preflightTreeReferences{}
332	if err := preflightNodeReferences(r, &references); err != nil {
333		return err
334	}
335	panes, err := r.getUint16()
336	if err != nil {
337		return err
338	}
339	ids := make(map[layout.PaneID]struct{}, panes)
340	for range panes {
341		id, err := r.getString()
342		if err != nil {
343			return err
344		}
345		if id == "" {
346			return fmt.Errorf("%w: empty pane ID", ErrInvalidData)
347		}
348		paneID := layout.PaneID(id)
349		if _, exists := ids[paneID]; exists {
350			return fmt.Errorf("%w: duplicate pane ID", ErrInvalidData)
351		}
352		ids[paneID] = struct{}{}
353		if err := skipString(r); err != nil {
354			return err
355		}
356		if err := skipString(r); err != nil {
357			return err
358		}
359		n, err := r.getUint32()
360		if err != nil {
361			return err
362		}
363		for range n {
364			if err := validateBlob(r); err != nil {
365				return err
366			}
367		}
368		if err := validateBlob(r); err != nil {
369			return err
370		}
371		if err := validateBlob(r); err != nil {
372			return err
373		}
374		if err := preflightProcess(r); err != nil {
375			return err
376		}
377	}
378	if focus != "" {
379		references.ids = append(references.ids, layout.PaneID(focus))
380	}
381	for _, id := range references.ids {
382		if _, ok := ids[id]; !ok {
383			return ErrUnknownPane
384		}
385	}
386	return nil
387}
388
389// validateBlob runs only after the complete collection passes resource sizing.
390// Dictionary/identity validation may allocate, but cannot run once per pane in
391// an over-budget collection. Sizing alone never authorizes session construction.
392func validateBlob(r *payloadReader) error {
393	n, err := r.getUint32()
394	if err != nil {
395		return err
396	}
397	if uint64(n) > uint64(len(r.b)) {
398		return ErrShortPayload
399	}
400	if _, err := vt.PreflightHistoryBlob(r.b[:n]); err != nil {
401		return fmt.Errorf("%w: VT blob", ErrInvalidData)
402	}
403	r.b = r.b[n:]
404	return nil
405}
406
407type preflightTreeReferences struct{ ids []layout.PaneID }
408
409func (r *preflightTreeReferences) add(id layout.PaneID) {
410	r.ids = append(r.ids, id)
411}
412
413func preflightNodeReferences(r *payloadReader, references *preflightTreeReferences) error {
414	kind, err := r.getUint8()
415	if err != nil {
416		return err
417	}
418	switch kind {
419	case 0:
420		leaf, err := r.getString()
421		if err != nil {
422			return err
423		}
424		references.add(layout.PaneID(leaf))
425		return nil
426	case 1:
427		if _, err := r.getUint8(); err != nil {
428			return err
429		}
430	case 2:
431		expanded, err := r.getString()
432		if err != nil {
433			return err
434		}
435		if expanded != "" {
436			references.add(layout.PaneID(expanded))
437		}
438	case 3:
439		return nil
440	default:
441		return fmt.Errorf("%w: node kind", ErrInvalidData)
442	}
443	children, err := r.getUint16()
444	if err != nil {
445		return err
446	}
447	for range children {
448		if err := preflightNodeReferences(r, references); err != nil {
449			return err
450		}
451	}
452	return nil
453}

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