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}