v0.3.0 99dc266

routes_wire.go RawHistoryFolder

1package ports
2
3import (
4	"errors"
5	"fmt"
6
7	"github.com/bnema/vev/internal/domain"
8)
9
10var ErrInvalidRouteWire = errors.New("invalid route wire message")
11
12func validateRouteString(value, field string, allowEmpty bool) error {
13	if err := ValidateRouteLabel(value, allowEmpty); err != nil {
14		return fmt.Errorf("%s: %w", field, err)
15	}
16	return nil
17}
18
19func marshalExactSessionTarget(w *payloadWriter, target ExactSessionTarget) {
20	w.putBytes(target.LifecycleID[:])
21	w.putString(target.SessionName)
22}
23
24func marshalExactTargetSection(w *payloadWriter, target *ExactSessionTarget) {
25	w.putBool(target != nil)
26	if target != nil {
27		marshalExactSessionTarget(w, *target)
28	}
29}
30
31func unmarshalExactTargetSection(r *payloadReader) (*ExactSessionTarget, error) {
32	present, err := r.getBool()
33	if err != nil {
34		return nil, err
35	}
36	if !present {
37		return nil, nil
38	}
39	target, err := unmarshalExactSessionTarget(r)
40	if err != nil {
41		return nil, err
42	}
43	return &target, nil
44}
45
46func skipExactTargetSection(r *payloadReader) error {
47	present, err := r.getBool()
48	if err != nil {
49		return err
50	}
51	if !present {
52		return nil
53	}
54	if _, err := unmarshalExactSessionTarget(r); err != nil {
55		return err
56	}
57	return nil
58}
59
60func unmarshalExactSessionTarget(r *payloadReader) (ExactSessionTarget, error) {
61	var target ExactSessionTarget
62	lifecycle, err := r.getBytes(len(target.LifecycleID))
63	if err != nil {
64		return ExactSessionTarget{}, err
65	}
66	copy(target.LifecycleID[:], lifecycle)
67	if target.SessionName, err = r.getString(); err != nil {
68		return ExactSessionTarget{}, err
69	}
70	if err := target.Validate(); err != nil {
71		return ExactSessionTarget{}, fmt.Errorf("%w: exact target: %v", ErrInvalidRouteWire, err)
72	}
73	return target, nil
74}
75
76// MarshalExactSessionTarget encodes the exact lifecycle/name pair used by
77// Hello and route-transition tests.
78func MarshalExactSessionTarget(target ExactSessionTarget) ([]byte, error) {
79	if err := target.Validate(); err != nil {
80		return nil, fmt.Errorf("%w: exact target: %v", ErrInvalidRouteWire, err)
81	}
82	w := payloadWriter{}
83	marshalExactSessionTarget(&w, target)
84	return w.b, nil
85}
86
87// UnmarshalExactSessionTarget decodes one exact lifecycle/name pair and rejects
88// truncation, invalid identity, and trailing bytes.
89func UnmarshalExactSessionTarget(b []byte) (ExactSessionTarget, error) {
90	r := payloadReader{b: b}
91	target, err := unmarshalExactSessionTarget(&r)
92	if err != nil {
93		return ExactSessionTarget{}, err
94	}
95	if err := r.done(); err != nil {
96		return ExactSessionTarget{}, err
97	}
98	return target, nil
99}
100
101func validateCommittedRouteIdentity(identity CommittedRouteIdentity) error {
102	if err := identity.Validate(); err != nil {
103		return fmt.Errorf("%w: committed identity: %v", ErrInvalidRouteWire, err)
104	}
105	return nil
106}
107
108// MarshalCommittedRouteIdentity encodes the daemon's committed exact session
109// identity without exposing transport or client-ledger details.
110func MarshalCommittedRouteIdentity(identity CommittedRouteIdentity) ([]byte, error) {
111	if err := validateCommittedRouteIdentity(identity); err != nil {
112		return nil, err
113	}
114	w := payloadWriter{}
115	marshalCommittedRouteIdentityFields(&w, identity)
116	return w.b, nil
117}
118
119func marshalCommittedRouteIdentityFields(w *payloadWriter, identity CommittedRouteIdentity) {
120	marshalExactSessionTarget(w, identity.Target)
121	w.putBool(identity.Ephemeral)
122}
123
124// marshalCommittedIdentitySection shares the standalone identity body and
125// validates embedded identities before Welcome serialization.
126func marshalCommittedIdentitySection(w *payloadWriter, identity *CommittedRouteIdentity) bool {
127	w.putBool(identity != nil)
128	if identity == nil {
129		return true
130	}
131	if err := validateCommittedRouteIdentity(*identity); err != nil {
132		return false
133	}
134	marshalCommittedRouteIdentityFields(w, *identity)
135	return true
136}
137
138func unmarshalCommittedIdentitySection(r *payloadReader) (*CommittedRouteIdentity, error) {
139	present, err := r.getBool()
140	if err != nil {
141		return nil, err
142	}
143	if !present {
144		return nil, nil
145	}
146	target, err := unmarshalExactSessionTarget(r)
147	if err != nil {
148		return nil, err
149	}
150	ephemeral, err := r.getBool()
151	if err != nil {
152		return nil, err
153	}
154	identity := &CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
155	if err := validateCommittedRouteIdentity(*identity); err != nil {
156		return nil, err
157	}
158	return identity, nil
159}
160
161func UnmarshalCommittedRouteIdentity(b []byte) (CommittedRouteIdentity, error) {
162	r := payloadReader{b: b}
163	target, err := unmarshalExactSessionTarget(&r)
164	if err != nil {
165		return CommittedRouteIdentity{}, err
166	}
167	ephemeral, err := r.getBool()
168	if err != nil {
169		return CommittedRouteIdentity{}, err
170	}
171	if err := r.done(); err != nil {
172		return CommittedRouteIdentity{}, err
173	}
174	identity := CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
175	if err := validateCommittedRouteIdentity(identity); err != nil {
176		return CommittedRouteIdentity{}, err
177	}
178	return identity, nil
179}
180
181// MarshalRoutePosition encodes one exact session and its attachment-local tab
182// cursor. Route positions are mutable client memory, not attach authority.
183func MarshalRoutePosition(position RoutePosition) ([]byte, error) {
184	if err := position.Validate(); err != nil {
185		return nil, fmt.Errorf("%w: route position: %v", ErrInvalidRouteWire, err)
186	}
187	w := payloadWriter{}
188	marshalExactSessionTarget(&w, position.Target)
189	w.putString(string(position.ActiveTabID))
190	return w.b, nil
191}
192
193// UnmarshalRoutePosition decodes one strict route-position payload.
194func UnmarshalRoutePosition(b []byte) (RoutePosition, error) {
195	r := payloadReader{b: b}
196	target, err := unmarshalExactSessionTarget(&r)
197	if err != nil {
198		return RoutePosition{}, err
199	}
200	activeTabID, err := r.getString()
201	if err != nil {
202		return RoutePosition{}, err
203	}
204	if err := r.done(); err != nil {
205		return RoutePosition{}, err
206	}
207	position := RoutePosition{Target: target, ActiveTabID: domain.TabStableID(activeTabID)}
208	if err := position.Validate(); err != nil {
209		return RoutePosition{}, fmt.Errorf("%w: route position: %v", ErrInvalidRouteWire, err)
210	}
211	return position, nil
212}
213
214func validateRouteRef(ref RouteRef) error {
215	if err := ref.Validate(); err != nil {
216		return fmt.Errorf("%w: route reference: %v", ErrInvalidRouteWire, err)
217	}
218	return nil
219}
220
221func marshalRouteRef(w *payloadWriter, ref RouteRef) {
222	w.putUint64(ref.Key)
223	w.putUint64(ref.Generation)
224}
225
226func unmarshalRouteRef(r *payloadReader) (RouteRef, error) {
227	key, err := r.getUint64()
228	if err != nil {
229		return RouteRef{}, err
230	}
231	generation, err := r.getUint64()
232	if err != nil {
233		return RouteRef{}, err
234	}
235	ref := RouteRef{Key: key, Generation: generation}
236	if err := validateRouteRef(ref); err != nil {
237		return RouteRef{}, err
238	}
239	return ref, nil
240}
241
242func validateRecentRouteEntry(entry RecentRouteEntry) error {
243	if entry.Key == 0 || entry.Generation == 0 {
244		return fmt.Errorf("%w: route entry identity is zero", ErrInvalidRouteWire)
245	}
246	if err := validateRouteString(entry.Name, "route name", false); err != nil {
247		return fmt.Errorf("%w: %v", ErrInvalidRouteWire, err)
248	}
249	if err := validateRouteString(entry.HostLabel, "route host label", true); err != nil {
250		return fmt.Errorf("%w: %v", ErrInvalidRouteWire, err)
251	}
252	if !entry.Kind.valid() {
253		return fmt.Errorf("%w: invalid route kind", ErrInvalidRouteWire)
254	}
255	if !entry.Reachability.valid() {
256		return fmt.Errorf("%w: invalid route reachability", ErrInvalidRouteWire)
257	}
258	return nil
259}
260
261func validateRecentRouteSnapshot(snapshot RecentRouteSnapshot) error {
262	if len(snapshot.Entries) > RouteSnapshotMaxEntries {
263		return fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)
264	}
265	if snapshot.Generation == 0 && len(snapshot.Entries) != 0 {
266		return fmt.Errorf("%w: non-empty snapshot has zero generation", ErrInvalidRouteWire)
267	}
268	if err := validateRouteRef(snapshot.Active); err != nil {
269		return err
270	}
271	if err := validateRouteRef(snapshot.Previous); err != nil {
272		return err
273	}
274	if err := validateRouteRef(snapshot.Home); err != nil {
275		return err
276	}
277	if !snapshot.Active.empty() && snapshot.Active == snapshot.Previous {
278		return fmt.Errorf("%w: active and previous routes are identical", ErrInvalidRouteWire)
279	}
280	refs := make(map[RouteRef]struct{}, len(snapshot.Entries))
281	for _, entry := range snapshot.Entries {
282		if snapshot.Active == (RouteRef{Key: entry.Key, Generation: entry.Generation}) {
283			return fmt.Errorf("%w: active route must be metadata-only", ErrInvalidRouteWire)
284		}
285		if err := validateRecentRouteEntry(entry); err != nil {
286			return err
287		}
288		ref := RouteRef{Key: entry.Key, Generation: entry.Generation}
289		if _, exists := refs[ref]; exists {
290			return fmt.Errorf("%w: duplicate route entry", ErrInvalidRouteWire)
291		}
292		refs[ref] = struct{}{}
293	}
294	for _, item := range []struct {
295		name string
296		ref  RouteRef
297	}{
298		{name: "previous", ref: snapshot.Previous},
299		{name: "home", ref: snapshot.Home},
300	} {
301		if item.ref.empty() || item.ref == snapshot.Active {
302			// Active is metadata-only and intentionally excluded from the
303			// recent display entries. Home may point at active as well.
304			continue
305		}
306		if _, exists := refs[item.ref]; !exists {
307			return fmt.Errorf("%w: %s route reference is absent from snapshot", ErrInvalidRouteWire, item.name)
308		}
309	}
310	return nil
311}
312
313// MarshalRecentRouteSnapshot encodes a complete bounded client route view.
314func MarshalRecentRouteSnapshot(snapshot RecentRouteSnapshot) ([]byte, error) {
315	if err := validateRecentRouteSnapshot(snapshot); err != nil {
316		return nil, err
317	}
318	w := payloadWriter{}
319	w.putUint64(snapshot.Generation)
320	marshalRouteRef(&w, snapshot.Active)
321	marshalRouteRef(&w, snapshot.Previous)
322	marshalRouteRef(&w, snapshot.Home)
323	w.putUint8(uint8(len(snapshot.Entries)))
324	for _, entry := range snapshot.Entries {
325		w.putUint64(entry.Key)
326		w.putUint64(entry.Generation)
327		w.putString(entry.Name)
328		w.putString(entry.HostLabel)
329		w.putUint8(uint8(entry.Kind))
330		w.putBool(entry.Ephemeral)
331		w.putBool(entry.Attention)
332		w.putUint8(uint8(entry.Reachability))
333	}
334	// Keep the frame bound defensive even though current entry/label bounds
335	// make this unreachable; it protects the wire contract if those bounds
336	// change independently in a future protocol revision.
337	if len(w.b) > MaxFrameLen-1 {
338		return nil, fmt.Errorf("%w: route snapshot is too large", ErrInvalidRouteWire)
339	}
340	return w.b, nil
341}
342
343func validateRouteAttentionSubscription(subscription RouteAttentionSubscription) error {
344	if len(subscription.Targets) > RouteSnapshotMaxEntries {
345		return fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)
346	}
347	refs := make(map[RouteRef]struct{}, len(subscription.Targets))
348	for _, target := range subscription.Targets {
349		if err := validateRouteRef(target.Ref); err != nil || target.Ref.empty() {
350			return fmt.Errorf("%w: invalid attention route reference", ErrInvalidRouteWire)
351		}
352		if err := target.Target.Validate(); err != nil {
353			return fmt.Errorf("%w: invalid attention target: %v", ErrInvalidRouteWire, err)
354		}
355		if _, exists := refs[target.Ref]; exists {
356			return fmt.Errorf("%w: duplicate attention route reference", ErrInvalidRouteWire)
357		}
358		refs[target.Ref] = struct{}{}
359	}
360	return nil
361}
362
363// MarshalRouteAttentionSubscription encodes the bounded client mapping used
364// by the connected daemon to resolve live route-attention indicators.
365func MarshalRouteAttentionSubscription(subscription RouteAttentionSubscription) ([]byte, error) {
366	if err := validateRouteAttentionSubscription(subscription); err != nil {
367		return nil, err
368	}
369	w := payloadWriter{}
370	w.putUint8(uint8(len(subscription.Targets)))
371	for _, target := range subscription.Targets {
372		marshalRouteRef(&w, target.Ref)
373		marshalExactSessionTarget(&w, target.Target)
374	}
375	return w.b, nil
376}
377
378// UnmarshalRouteAttentionSubscription decodes one strict bounded attention
379// mapping and rejects truncated, malformed, and trailing payloads.
380func UnmarshalRouteAttentionSubscription(b []byte) (RouteAttentionSubscription, error) {
381	r := payloadReader{b: b}
382	count, err := r.getUint8()
383	if err != nil {
384		return RouteAttentionSubscription{}, err
385	}
386	if int(count) > RouteSnapshotMaxEntries {
387		return RouteAttentionSubscription{}, fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)
388	}
389	subscription := RouteAttentionSubscription{Targets: make([]RouteAttentionTarget, 0, int(count))}
390	for range int(count) {
391		ref, err := unmarshalRouteRef(&r)
392		if err != nil {
393			return RouteAttentionSubscription{}, err
394		}
395		target, err := unmarshalExactSessionTarget(&r)
396		if err != nil {
397			return RouteAttentionSubscription{}, err
398		}
399		subscription.Targets = append(subscription.Targets, RouteAttentionTarget{Ref: ref, Target: target})
400	}
401	if err := r.done(); err != nil {
402		return RouteAttentionSubscription{}, err
403	}
404	if err := validateRouteAttentionSubscription(subscription); err != nil {
405		return RouteAttentionSubscription{}, err
406	}
407	return subscription, nil
408}
409
410// UnmarshalRecentRouteSnapshot decodes and strictly validates one complete
411// immutable snapshot, including every referenced active/previous/home entry.
412func UnmarshalRecentRouteSnapshot(b []byte) (RecentRouteSnapshot, error) {
413	if len(b) > MaxFrameLen-1 {
414		return RecentRouteSnapshot{}, ErrInvalidRouteWire
415	}
416	r := payloadReader{b: b}
417	var snapshot RecentRouteSnapshot
418	var err error
419	if snapshot.Generation, err = r.getUint64(); err != nil {
420		return RecentRouteSnapshot{}, err
421	}
422	if snapshot.Active, err = unmarshalRouteRef(&r); err != nil {
423		return RecentRouteSnapshot{}, err
424	}
425	if snapshot.Previous, err = unmarshalRouteRef(&r); err != nil {
426		return RecentRouteSnapshot{}, err
427	}
428	if snapshot.Home, err = unmarshalRouteRef(&r); err != nil {
429		return RecentRouteSnapshot{}, err
430	}
431	count, err := r.getUint8()
432	if err != nil {
433		return RecentRouteSnapshot{}, err
434	}
435	if int(count) > RouteSnapshotMaxEntries {
436		return RecentRouteSnapshot{}, fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)
437	}
438	if count != 0 {
439		snapshot.Entries = make([]RecentRouteEntry, 0, int(count))
440	}
441	for range int(count) {
442		var entry RecentRouteEntry
443		if entry.Key, err = r.getUint64(); err != nil {
444			return RecentRouteSnapshot{}, err
445		}
446		if entry.Generation, err = r.getUint64(); err != nil {
447			return RecentRouteSnapshot{}, err
448		}
449		if entry.Name, err = r.getString(); err != nil {
450			return RecentRouteSnapshot{}, err
451		}
452		if entry.HostLabel, err = r.getString(); err != nil {
453			return RecentRouteSnapshot{}, err
454		}
455		kind, err := r.getUint8()
456		if err != nil {
457			return RecentRouteSnapshot{}, err
458		}
459		entry.Kind = RouteKind(kind)
460		if entry.Ephemeral, err = r.getBool(); err != nil {
461			return RecentRouteSnapshot{}, err
462		}
463		if entry.Attention, err = r.getBool(); err != nil {
464			return RecentRouteSnapshot{}, err
465		}
466		reachability, err := r.getUint8()
467		if err != nil {
468			return RecentRouteSnapshot{}, err
469		}
470		entry.Reachability = RouteReachability(reachability)
471		snapshot.Entries = append(snapshot.Entries, entry)
472	}
473	if err := r.done(); err != nil {
474		return RecentRouteSnapshot{}, err
475	}
476	if err := validateRecentRouteSnapshot(snapshot); err != nil {
477		return RecentRouteSnapshot{}, err
478	}
479	return snapshot, nil
480}
481
482func validateRouteNavigationAction(action RouteNavigationAction) error {
483	if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
484		return fmt.Errorf("%w: navigation action identity is zero", ErrInvalidRouteWire)
485	}
486	return nil
487}
488
489func MarshalRouteNavigationAction(action RouteNavigationAction) ([]byte, error) {
490	if err := validateRouteNavigationAction(action); err != nil {
491		return nil, err
492	}
493	w := payloadWriter{}
494	w.putUint64(action.SnapshotGeneration)
495	w.putUint64(action.Key)
496	w.putUint64(action.Generation)
497	return w.b, nil
498}
499
500func UnmarshalRouteNavigationAction(b []byte) (RouteNavigationAction, error) {
501	r := payloadReader{b: b}
502	var action RouteNavigationAction
503	var err error
504	if action.SnapshotGeneration, err = r.getUint64(); err != nil {
505		return RouteNavigationAction{}, err
506	}
507	if action.Key, err = r.getUint64(); err != nil {
508		return RouteNavigationAction{}, err
509	}
510	if action.Generation, err = r.getUint64(); err != nil {
511		return RouteNavigationAction{}, err
512	}
513	if err := r.done(); err != nil {
514		return RouteNavigationAction{}, err
515	}
516	if err := validateRouteNavigationAction(action); err != nil {
517		return RouteNavigationAction{}, err
518	}
519	return action, nil
520}
521
522func validateRouteNavigationFailure(failure RouteNavigationFailure) error {
523	if failure.Key == 0 || failure.Generation == 0 {
524		return fmt.Errorf("%w: failure identity is zero", ErrInvalidRouteWire)
525	}
526	if !failure.Code.valid() {
527		return fmt.Errorf("%w: invalid failure code", ErrInvalidRouteWire)
528	}
529	return nil
530}
531
532func MarshalRouteNavigationFailure(failure RouteNavigationFailure) ([]byte, error) {
533	if err := validateRouteNavigationFailure(failure); err != nil {
534		return nil, err
535	}
536	w := payloadWriter{}
537	w.putUint64(failure.Key)
538	w.putUint64(failure.Generation)
539	w.putUint8(uint8(failure.Code))
540	return w.b, nil
541}
542
543func UnmarshalRouteNavigationFailure(b []byte) (RouteNavigationFailure, error) {
544	r := payloadReader{b: b}
545	var failure RouteNavigationFailure
546	var err error
547	if failure.Key, err = r.getUint64(); err != nil {
548		return RouteNavigationFailure{}, err
549	}
550	if failure.Generation, err = r.getUint64(); err != nil {
551		return RouteNavigationFailure{}, err
552	}
553	code, err := r.getUint8()
554	if err != nil {
555		return RouteNavigationFailure{}, err
556	}
557	failure.Code = RouteFailureCode(code)
558	if err := r.done(); err != nil {
559		return RouteNavigationFailure{}, err
560	}
561	if err := validateRouteNavigationFailure(failure); err != nil {
562		return RouteNavigationFailure{}, err
563	}
564	return failure, nil
565}

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