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}