detached ece7682

navigation_inventory.go RawHistoryFolder

1package protocol
2
3import (
4	"unicode/utf8"
5
6	"github.com/bnema/vev/internal/domain"
7)
8
9// Navigation inventory bounds. These are export limits for UI listings, not
10// limits on stored or running sessions.
11const (
12	NavigationInventoryMaxSourceGroups           = 65
13	NavigationInventoryMaxLocalEntries           = 4096
14	NavigationInventoryMaxRemoteEntriesPerSource = 256
15	NavigationInventoryMaxDisplayBytes           = 256
16	NavigationInventoryMaxKeyBytes               = 128
17)
18
19// NavigationInventoryLocalSourceKey identifies the control daemon's own
20// sessions inside inventory groups.
21const NavigationInventoryLocalSourceKey = "local"
22
23// NavigationInventoryRemoteSourcePrefix namespaces remote source keys so
24// relayed identifiers never expose raw configured endpoints and never
25// collide with the reserved local source.
26const NavigationInventoryRemoteSourcePrefix = "remote/"
27
28// NavigationInventoryOperation selects the control-source query.
29type NavigationInventoryOperation uint8
30
31const (
32	NavigationInventorySnapshot NavigationInventoryOperation = 1
33	NavigationInventoryResolve  NavigationInventoryOperation = 2
34)
35
36// NavigationInventoryStatus is the overall source-control outcome.
37type NavigationInventoryStatus uint8
38
39const (
40	NavigationInventoryOK              NavigationInventoryStatus = 1
41	NavigationInventoryUnavailable     NavigationInventoryStatus = 2
42	NavigationInventoryInvalid         NavigationInventoryStatus = 3
43	NavigationInventoryVersionMismatch NavigationInventoryStatus = 4
44)
45
46// NavigationInventorySourceStatus is the per-source outcome. Healthy sources
47// survive another source's size or error failure.
48type NavigationInventorySourceStatus uint8
49
50const (
51	NavigationInventorySourceOK          NavigationInventorySourceStatus = 1
52	NavigationInventorySourceUnavailable NavigationInventorySourceStatus = 2
53	NavigationInventorySourceTooLarge    NavigationInventorySourceStatus = 3
54)
55
56// NavigationInventoryFailureCode classifies resolve/selection failures.
57type NavigationInventoryFailureCode uint8
58
59const (
60	NavigationInventoryStaleIdentity    NavigationInventoryFailureCode = 1
61	NavigationInventorySourceGone       NavigationInventoryFailureCode = 2
62	NavigationInventoryIncompatible     NavigationInventoryFailureCode = 3
63	NavigationInventoryInvalidTarget    NavigationInventoryFailureCode = 4
64	NavigationInventoryNavigationFailed NavigationInventoryFailureCode = 5
65	NavigationInventoryRestoreFailed    NavigationInventoryFailureCode = 6
66)
67
68// NavigationInventoryEntry is one sanitized public row. It carries display
69// facts only, never exact endpoints, connection parameters, CWD,
70// environment, preview, or pane content.
71type NavigationInventoryEntry struct {
72	SourceKey     string
73	EntryKey      string
74	Name          string
75	DisplayOrigin string
76	State         string
77	Reason        string
78}
79
80// NavigationInventorySourceGroup is one packed source with its own status.
81type NavigationInventorySourceGroup struct {
82	SourceKey string
83	Status    NavigationInventorySourceStatus
84	Entries   []NavigationInventoryEntry
85}
86
87// NavigationInventoryRequest queries a source control connection without
88// creating an attachment or Hello session.
89type NavigationInventoryRequest struct {
90	Version      uint16
91	RequestID    uint64
92	Operation    NavigationInventoryOperation
93	SourceKey    string
94	EntryKey     string
95	Registration domain.RemoteRegistration
96}
97
98// NavigationInventoryResponse answers a control query. Snapshot carries
99// source groups; resolve carries a non-mutating AttachTarget; never both.
100// Version-mismatch and malformed responses never carry a target.
101type NavigationInventoryResponse struct {
102	RequestID uint64
103	Operation NavigationInventoryOperation
104	Status    NavigationInventoryStatus
105	Groups    []NavigationInventorySourceGroup
106	Resolved  *AttachTarget
107}
108
109// NavigationInventoryDemand is serving daemon → client.
110type NavigationInventoryDemand struct {
111	InteractionGeneration uint64
112	Open                  bool
113}
114
115// NavigationInventoryPublication is client → serving daemon.
116type NavigationInventoryPublication struct {
117	InteractionGeneration uint64
118	PublicationGeneration uint64
119	Groups                []NavigationInventorySourceGroup
120}
121
122// NavigationInventorySelection is serving daemon → client. No endpoint or
123// name is supplied as authority; the key resolves through the source.
124type NavigationInventorySelection struct {
125	CauseActionID         uint64
126	InteractionGeneration uint64
127	PublicationGeneration uint64
128	SourceKey             string
129	EntryKey              string
130}
131
132// NavigationInventoryFailure is client → serving daemon.
133type NavigationInventoryFailure struct {
134	CauseActionID         uint64
135	InteractionGeneration uint64
136	SourceKey             string
137	EntryKey              string
138	Code                  NavigationInventoryFailureCode
139}
140
141func validNavigationInventoryOperation(op NavigationInventoryOperation) bool {
142	return op == NavigationInventorySnapshot || op == NavigationInventoryResolve
143}
144
145func validNavigationInventoryStatus(status NavigationInventoryStatus) bool {
146	switch status {
147	case NavigationInventoryOK,
148		NavigationInventoryUnavailable,
149		NavigationInventoryInvalid,
150		NavigationInventoryVersionMismatch:
151		return true
152	default:
153		return false
154	}
155}
156
157func validNavigationInventorySourceStatus(status NavigationInventorySourceStatus) bool {
158	switch status {
159	case NavigationInventorySourceOK,
160		NavigationInventorySourceUnavailable,
161		NavigationInventorySourceTooLarge:
162		return true
163	default:
164		return false
165	}
166}
167
168func validNavigationInventoryFailureCode(code NavigationInventoryFailureCode) bool {
169	switch code {
170	case NavigationInventoryStaleIdentity,
171		NavigationInventorySourceGone,
172		NavigationInventoryIncompatible,
173		NavigationInventoryInvalidTarget,
174		NavigationInventoryNavigationFailed,
175		NavigationInventoryRestoreFailed:
176		return true
177	default:
178		return false
179	}
180}
181
182func validInventoryKey(value string) bool {
183	if value == "" || len(value) > NavigationInventoryMaxKeyBytes {
184		return false
185	}
186	if !utf8.ValidString(value) {
187		return false
188	}
189	for _, r := range value {
190		if r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f) {
191			return false
192		}
193	}
194	return true
195}
196
197func validInventoryDisplay(value string) bool {
198	if len(value) > NavigationInventoryMaxDisplayBytes {
199		return false
200	}
201	if !utf8.ValidString(value) {
202		return false
203	}
204	for _, r := range value {
205		if r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f) {
206			return false
207		}
208	}
209	return true
210}
211
212// ValidateNavigationInventoryRequest enforces nonzero IDs, operation-dependent
213// unions, key shape, registration identity for resolve, and display safety.
214// Local resolve carries a zero registration (the source is the control daemon
215// itself); remote resolve carries the configured registration to fence ABA
216// re-registration.
217func ValidateNavigationInventoryRequest(request NavigationInventoryRequest) error {
218	if request.RequestID == 0 || !validNavigationInventoryOperation(request.Operation) {
219		return ErrInvalidNavigation
220	}
221	if request.Operation == NavigationInventorySnapshot {
222		if request.SourceKey != "" || request.EntryKey != "" || !request.Registration.IsZero() {
223			return ErrInvalidNavigation
224		}
225		return nil
226	}
227	if !validInventoryKey(request.SourceKey) || !validInventoryKey(request.EntryKey) {
228		return ErrInvalidNavigation
229	}
230	if request.SourceKey == NavigationInventoryLocalSourceKey {
231		if !request.Registration.IsZero() {
232			return ErrInvalidNavigation
233		}
234		return nil
235	}
236	if request.Registration.Validate() != nil {
237		return ErrInvalidNavigation
238	}
239	return nil
240}
241
242// ValidateNavigationInventoryResponse enforces ID/operation echo, status
243// codes, union shape, unique keys, and display safety. Targets are rejected
244// on version-mismatch or malformed outcomes.
245func ValidateNavigationInventoryResponse(response NavigationInventoryResponse) error {
246	if response.RequestID == 0 || !validNavigationInventoryOperation(response.Operation) {
247		return ErrInvalidNavigation
248	}
249	if !validNavigationInventoryStatus(response.Status) {
250		return ErrInvalidNavigation
251	}
252	if response.Status == NavigationInventoryVersionMismatch {
253		if response.Resolved != nil || len(response.Groups) != 0 {
254			return ErrInvalidNavigation
255		}
256		return nil
257	}
258	if response.Operation == NavigationInventorySnapshot {
259		if response.Resolved != nil {
260			return ErrInvalidNavigation
261		}
262		return validateNavigationInventoryGroups(response.Groups, true)
263	}
264	if len(response.Groups) != 0 {
265		return ErrInvalidNavigation
266	}
267	if response.Status != NavigationInventoryOK {
268		if response.Resolved != nil {
269			return ErrInvalidNavigation
270		}
271		return nil
272	}
273	if response.Resolved == nil {
274		return ErrInvalidNavigation
275	}
276	return ValidateAttachTarget(*response.Resolved)
277}
278
279func validateNavigationInventoryGroups(groups []NavigationInventorySourceGroup, snapshot bool) error {
280	if len(groups) > NavigationInventoryMaxSourceGroups {
281		return ErrInvalidNavigation
282	}
283	sources := make(map[string]struct{}, len(groups))
284	for _, group := range groups {
285		if !validInventoryKey(group.SourceKey) || !validNavigationInventorySourceStatus(group.Status) {
286			return ErrInvalidNavigation
287		}
288		// Row limits are source-specific: the local source streams live
289		// state, remote sources mirror bounded directory caches.
290		limit := NavigationInventoryMaxRemoteEntriesPerSource
291		if group.SourceKey == NavigationInventoryLocalSourceKey {
292			limit = NavigationInventoryMaxLocalEntries
293		}
294		if len(group.Entries) > limit {
295			return ErrInvalidNavigation
296		}
297		if _, dup := sources[group.SourceKey]; dup {
298			return ErrInvalidNavigation
299		}
300		sources[group.SourceKey] = struct{}{}
301		if group.Status != NavigationInventorySourceOK && len(group.Entries) != 0 {
302			return ErrInvalidNavigation
303		}
304		entries := make(map[string]struct{}, len(group.Entries))
305		for _, entry := range group.Entries {
306			if entry.SourceKey != group.SourceKey || !validInventoryKey(entry.EntryKey) {
307				return ErrInvalidNavigation
308			}
309			if _, dup := entries[entry.EntryKey]; dup {
310				return ErrInvalidNavigation
311			}
312			entries[entry.EntryKey] = struct{}{}
313			if !validInventoryDisplay(entry.Name) || !validInventoryDisplay(entry.DisplayOrigin) ||
314				!validInventoryDisplay(entry.State) || !validInventoryDisplay(entry.Reason) {
315				return ErrInvalidNavigation
316			}
317		}
318		if !snapshot && len(group.Entries) == 0 {
319			return ErrInvalidNavigation
320		}
321	}
322	return nil
323}
324
325// ValidateNavigationInventoryDemand requires a nonzero interaction.
326func ValidateNavigationInventoryDemand(demand NavigationInventoryDemand) error {
327	if demand.InteractionGeneration == 0 {
328		return ErrInvalidNavigation
329	}
330	return nil
331}
332
333// ValidateNavigationInventoryPublication requires nonzero generations and
334// sanitized groups.
335func ValidateNavigationInventoryPublication(publication NavigationInventoryPublication) error {
336	if publication.InteractionGeneration == 0 || publication.PublicationGeneration == 0 {
337		return ErrInvalidNavigation
338	}
339	return validateNavigationInventoryGroups(publication.Groups, true)
340}
341
342// ValidateNavigationInventorySelection rejects unknown keys only at resolve
343// time; wire validation enforces shape, nonzero generations, and key
344// safety. The cause correlates input when present and stays zero for
345// ordinary keyboard input, matching the optional UI-cause contract of
346// other navigation actions.
347func ValidateNavigationInventorySelection(selection NavigationInventorySelection) error {
348	if selection.InteractionGeneration == 0 || selection.PublicationGeneration == 0 {
349		return ErrInvalidNavigation
350	}
351	if !validInventoryKey(selection.SourceKey) || !validInventoryKey(selection.EntryKey) {
352		return ErrInvalidNavigation
353	}
354	return nil
355}
356
357// ValidateNavigationInventoryFailure enforces matching IDs and bounded codes.
358func ValidateNavigationInventoryFailure(failure NavigationInventoryFailure) error {
359	if failure.InteractionGeneration == 0 {
360		return ErrInvalidNavigation
361	}
362	if !validInventoryKey(failure.SourceKey) || !validInventoryKey(failure.EntryKey) {
363		return ErrInvalidNavigation
364	}
365	if !validNavigationInventoryFailureCode(failure.Code) {
366		return ErrInvalidNavigation
367	}
368	return nil
369}

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