1// Wire codecs for vev's IPC message payloads. They live in ports (alongside
2// Frame and the MsgType constants) because wire encoding is protocol
3// surface, not I/O: everything here is pure, stdlib-only byte marshalling.
4// Adapters keep what actually performs I/O (framing over a connection,
5// listening on a socket) and import these types for the payloads they carry.
6
7package ports
8
9import (
10 "encoding/binary"
11 "errors"
12 "fmt"
13 "math"
14
15 renderer "github.com/bnema/vev-vt"
16 "github.com/bnema/vev/internal/domain"
17)
18
19// errShortPayload is returned when a payload ends before a required field
20// has been fully read.
21var errShortPayload = errors.New("ports: payload too short")
22
23// errTrailingBytes is returned when a fixed-shape payload has bytes left
24// over after every field has been consumed. Protocol strictness here
25// catches version drift early instead of silently ignoring extra data.
26var errTrailingBytes = errors.New("ports: unexpected trailing bytes in payload")
27
28var errInvalidBoolean = errors.New("ports: invalid boolean flag")
29var errInvalidEnum = errors.New("ports: invalid enum value")
30
31const (
32 outputHeaderLen = 5*8 + 2*2 + 1
33 outputPayloadOverhead = outputHeaderLen + 4
34)
35
36// ErrInvalidSize reports a terminal size that cannot be represented by the
37// protocol or would exceed the bounded screen area.
38var ErrInvalidSize = errors.New("ports: invalid terminal size")
39
40// ErrInvalidHello reports a malformed Hello semantic value.
41var ErrInvalidHello = errors.New("ports: invalid hello")
42
43// ErrInvalidOutput reports a malformed Output semantic value.
44var ErrInvalidOutput = errors.New("ports: invalid output")
45
46// ErrInvalidAck reports a malformed Ack semantic value.
47var ErrInvalidAck = errors.New("ports: invalid ack")
48
49// ErrInvalidAttachTarget reports a malformed attach target.
50var ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
51
52// ErrInvalidNavigation reports an unknown or impossible navigation value.
53var ErrInvalidNavigation = errors.New("ports: invalid navigation")
54
55// Intent values carried by Hello, describing what the client wants to do.
56const (
57 IntentEphemeral uint8 = 0
58 IntentNew uint8 = 1
59 IntentAttach uint8 = 2
60 IntentResume uint8 = 3
61)
62
63// EnvironmentPolicy controls which side owns the environment used for future
64// PTY children. Direct CLI attaches keep the historical client-owned policy;
65// picker handoffs use daemon ownership so a local environment cannot leak into
66// a remote daemon.
67type EnvironmentPolicy uint8
68
69const (
70 EnvironmentPolicyClientOwned EnvironmentPolicy = iota
71 EnvironmentPolicyDaemonOwned
72)
73
74func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
75 return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
76}
77
78func validNavigationCapabilities(capabilities NavigationCapabilities) bool {
79 return capabilities&^(NavigationCapabilityHomePicker|NavigationCapabilityBack) == 0
80}
81
82func validStartupOverlay(overlay StartupOverlay) bool {
83 return overlay == StartupOverlayNone || overlay == StartupOverlaySessionPicker
84}
85
86// ValidateNavigation validates route capabilities independently of the rest of
87// the Hello payload. homePickerRoute indicates that the route may open its
88// home/session picker (for example, a remote target or daemon-owned route).
89func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOverlay, homePickerRoute bool) error {
90 if !validNavigationCapabilities(capabilities) || !validStartupOverlay(overlay) {
91 return ErrInvalidNavigation
92 }
93 home := capabilities&NavigationCapabilityHomePicker != 0
94 back := capabilities&NavigationCapabilityBack != 0
95 if (home && !homePickerRoute) || back != (overlay == StartupOverlaySessionPicker) {
96 return ErrInvalidNavigation
97 }
98 if homePickerRoute && back {
99 return ErrInvalidNavigation
100 }
101 return nil
102}
103
104func validateHelloNavigation(h Hello) error {
105 if h.Intent != IntentAttach && h.Intent != IntentResume {
106 if h.NavigationCapabilities != 0 || h.StartupOverlay != StartupOverlayNone {
107 return ErrInvalidNavigation
108 }
109 return nil
110 }
111 return ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
112}
113
114// Capability bits advertised in Welcome.
115const (
116 CapabilityResume uint32 = 1 << 0
117 CapabilityUDP uint32 = 1 << 1
118 CapabilityPredict uint32 = 1 << 2
119)
120
121// ErrorMsg codes.
122const (
123 ErrVersionMismatch uint16 = 1
124 ErrNoSuchSession uint16 = 2
125 ErrNameTaken uint16 = 3
126 ErrServerShutdown uint16 = 4
127 ErrInvalidSessionName uint16 = 5
128 ErrUnknownCommand uint16 = 6
129 ErrNotScriptable uint16 = 7
130 ErrInvalidCommandArgs uint16 = 8
131 ErrNoSuchTarget uint16 = 9
132 ErrAmbiguousTarget uint16 = 10
133 ErrInternal uint16 = 255
134)
135
136// Detached reasons.
137const (
138 ReasonDetach uint8 = 0
139 ReasonSessionKilled uint8 = 1
140 ReasonServerShutdown uint8 = 2
141 ReasonReplaced uint8 = 3
142)
143
144// Hello is sent by the client immediately after connecting.
145type Hello struct {
146 Version uint16
147 Intent uint8
148 ClientID [16]byte
149 ResumeToken uint64
150 Name string
151 Size domain.Size
152 TermEnv string
153 Cwd string
154 TrueColor bool
155 // MaxOutputInFlight is the requested maximum number of unacknowledged
156 // state-bearing output frames.
157 MaxOutputInFlight uint8
158 // Env is the complete exported client environment for future PTY children.
159 // Entries are opaque strings so their ordering and contents are preserved.
160 Env []string
161 // RemoteTarget is present only for a picker-selected remote attach. It is
162 // carried through reconnects and is validated by the owning daemon before
163 // it creates or publishes any session resources.
164 RemoteTarget *domain.RemoteSessionTarget
165 // ExactTarget is an optional lifecycle/name identity selected by the client
166 // ledger. It is transport-neutral and never replaces the daemon's authority
167 // to validate or commit the target.
168 ExactTarget *ExactSessionTarget
169 // PreferredTabID is a client-owned route cursor. The daemon treats it as an
170 // attachment-local hint and falls back when the stable tab no longer exists.
171 PreferredTabID domain.TabStableID
172 RemoteOrigin string
173 EnvironmentPolicy EnvironmentPolicy
174 NavigationCapabilities NavigationCapabilities
175 StartupOverlay StartupOverlay
176}
177
178// Input carries raw bytes typed/pasted by the client, destined for the PTY.
179type Input struct {
180 InputSeq uint64
181 Data []byte
182}
183
184// Resize notifies the daemon of a client-side terminal size change.
185type Resize struct {
186 Size domain.Size
187}
188
189// Theme reports the client's terminal foreground/background colors, ANSI
190// palette entries, and whether the client terminal supports truecolor.
191type Theme struct {
192 HasForeground bool
193 Foreground renderer.RGB
194 HasBackground bool
195 Background renderer.RGB
196 TrueColor bool
197 SchemeKnown bool
198 Light bool
199 PaletteKnown uint16
200 Palette [16]renderer.RGB
201}
202
203// ImagePush carries a clipboard image from a remote client, to be written to
204// a temp file and injected into the focused pane's PTY as a path (see
205// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
206type ImagePush struct {
207 InputSeq uint64
208 Mime string
209 Data []byte
210}
211
212// ClientNotice reports a fixed client-side event for daemon-rendered user
213// feedback. Action values are deliberately closed so a client cannot inject
214// arbitrary display text into a shared session.
215type ClientNotice struct {
216 Action uint8
217}
218
219const (
220 ClientNoticeClipboardFallback uint8 = 1
221 ClientNoticeClipboardTooLarge uint8 = 2
222 ClientNoticeLinkDegraded uint8 = 3
223 ClientNoticeLinkConnected uint8 = 4
224)
225
226func validClientNoticeAction(action uint8) bool {
227 switch action {
228 case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
229 return true
230 default:
231 return false
232 }
233}
234
235func validDetachedReason(reason uint8) bool {
236 switch reason {
237 case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
238 return true
239 default:
240 return false
241 }
242}
243
244// Detach asks the daemon to detach the current client without killing the
245// session.
246type Detach struct{}
247
248// Ping is a keepalive/liveness probe sent by either side.
249type Ping struct{}
250
251// Pong answers a Ping.
252type Pong struct{}
253
254// Ack acknowledges receipt/application of an output state in one output epoch.
255type Ack struct {
256 Epoch uint64
257 State uint64
258}
259
260// Welcome is the daemon's reply to a successful Hello.
261type Welcome struct {
262 SessionID string
263 SessionName string
264 Ephemeral bool
265 ResumeToken uint64
266 Capabilities uint32
267 CommittedIdentity *CommittedRouteIdentity
268}
269
270// ErrorMsg reports a protocol- or session-level failure to the client.
271type ErrorMsg struct {
272 Code uint16
273 Text string
274}
275
276// Output carries one ordered output publication for an attachment.
277type Output struct {
278 Epoch uint64
279 Base uint64
280 New uint64
281 Echo uint64
282 ViewRevision uint64
283 Size domain.Size
284 Full bool
285 Data []byte
286}
287
288// AttachTarget identifies the endpoint and exact session/tab selected for a
289// new attachment. Endpoint and Session remain the compatibility route fields;
290// RemoteTarget is the authoritative picker identity when non-nil.
291type AttachTarget struct {
292 Endpoint string
293 Session string
294 Intent uint8
295 RemoteTarget *domain.RemoteSessionTarget
296 EnvironmentPolicy EnvironmentPolicy
297}
298
299// Detached tells a client it has been disconnected from its session and why.
300type Detached struct {
301 Reason uint8
302}
303
304// List asks the daemon to enumerate its live sessions. It carries no
305// fields; the request is fully described by its message type.
306type List struct{}
307
308// Kill asks the daemon to terminate one named session or all sessions.
309type Kill struct {
310 Name string
311 All bool
312}
313
314// SessionState is the client-visible lifecycle state of a session.
315type SessionState uint8
316
317const (
318 SessionUp SessionState = iota
319 SessionDown
320 SessionBroken
321)
322
323// SessionInfo describes one session in a Sessions listing.
324type SessionInfo struct {
325 SessionID string
326 Name string
327 State SessionState
328 Ephemeral bool
329 Tabs uint16
330 Attached bool
331}
332
333// Sessions is the daemon's reply to a List, enumerating live sessions.
334type Sessions struct {
335 Sessions []SessionInfo
336}
337
338// OutputResetRequest asks a remote output stream to rebase.
339type OutputResetRequest struct{}
340
341// SessionTabMeta describes one tab in a remote session's metadata snapshot.
342// ID is stable for the lifetime of its remote tab and ordered only for display.
343// payloadWriter builds a message payload by appending fields in wire order.
344type payloadWriter struct {
345 b []byte
346}
347
348func (w *payloadWriter) putUint8(v uint8) {
349 w.b = append(w.b, v)
350}
351
352// putBool writes the single byte payloadReader.getBool accepts: 1 for true and
353// 0 for false.
354func (w *payloadWriter) putBool(v bool) {
355 if v {
356 w.putUint8(1)
357 return
358 }
359 w.putUint8(0)
360}
361
362func (w *payloadWriter) putUint16(v uint16) {
363 var tmp [2]byte
364 binary.BigEndian.PutUint16(tmp[:], v)
365 w.b = append(w.b, tmp[:]...)
366}
367
368func (w *payloadWriter) putUint32(v uint32) {
369 var tmp [4]byte
370 binary.BigEndian.PutUint32(tmp[:], v)
371 w.b = append(w.b, tmp[:]...)
372}
373
374func (w *payloadWriter) putUint64(v uint64) {
375 var tmp [8]byte
376 binary.BigEndian.PutUint64(tmp[:], v)
377 w.b = append(w.b, tmp[:]...)
378}
379
380func (w *payloadWriter) putBytes(b []byte) {
381 w.b = append(w.b, b...)
382}
383
384func (w *payloadWriter) putString(s string) {
385 w.putUint16(uint16(len(s)))
386 w.b = append(w.b, s...)
387}
388
389func (w *payloadWriter) putLongString(s string) {
390 w.putUint32(uint32(len(s)))
391 w.b = append(w.b, s...)
392}
393
394func (w *payloadWriter) putLongBytes(b []byte) {
395 w.putUint32(uint32(len(b)))
396 w.b = append(w.b, b...)
397}
398
399// marshalRemoteTargetSection writes the required v27 target/policy section.
400// A target may be absent for direct local attaches, but its presence marker and
401// environment policy are never optional: v27 has no extension or legacy tail
402// inside this section; the exact-target section follows it.
403func marshalRemoteTargetSection(w *payloadWriter, target *domain.RemoteSessionTarget, policy EnvironmentPolicy) {
404 if target == nil {
405 w.putBool(false)
406 } else {
407 w.putBool(true)
408 w.putString(target.Endpoint)
409 w.putString(target.DisplayOrigin)
410 w.putBytes(target.LifecycleID[:])
411 w.putString(target.SessionName)
412 w.putBool(target.Stopped)
413 w.putString(string(target.LiveTabID))
414 w.putUint8(uint8(target.StoppedTab.Kind))
415 switch target.StoppedTab.Kind {
416 case domain.TabSelectorByStableID:
417 w.putString(string(target.StoppedTab.StableID))
418 case domain.TabSelectorByOrdinal:
419 w.putUint16(target.StoppedTab.Ordinal)
420 w.putString(target.StoppedTab.RawName)
421 w.putUint16(target.StoppedTab.ExpectedCount)
422 }
423 }
424 w.putUint8(uint8(policy))
425}
426
427func skipRemoteTargetSection(r *payloadReader) error {
428 present, err := r.getBool()
429 if err != nil {
430 return err
431 }
432 if present {
433 if err := r.skipString(); err != nil {
434 return err
435 }
436 if err := r.skipString(); err != nil {
437 return err
438 }
439 if _, err := r.getBytes(16); err != nil {
440 return err
441 }
442 if err := r.skipString(); err != nil {
443 return err
444 }
445 if _, err := r.getBool(); err != nil {
446 return err
447 }
448 if err := r.skipString(); err != nil {
449 return err
450 }
451 kind, err := r.getUint8()
452 if err != nil {
453 return err
454 }
455 switch domain.TabSelectorKind(kind) {
456 case domain.TabSelectorByStableID:
457 if err := r.skipString(); err != nil {
458 return err
459 }
460 case domain.TabSelectorByOrdinal:
461 if _, err := r.getUint16(); err != nil {
462 return err
463 }
464 if err := r.skipString(); err != nil {
465 return err
466 }
467 if _, err := r.getUint16(); err != nil {
468 return err
469 }
470 case 0:
471 default:
472 return errInvalidEnum
473 }
474 }
475 policy, err := r.getUint8()
476 if err != nil {
477 return err
478 }
479 if !validEnvironmentPolicy(EnvironmentPolicy(policy)) {
480 return errInvalidEnum
481 }
482 return nil
483}
484
485func unmarshalRemoteTarget(r *payloadReader) (*domain.RemoteSessionTarget, error) {
486 present, err := r.getBool()
487 if err != nil {
488 return nil, err
489 }
490 if !present {
491 return nil, nil
492 }
493 target := &domain.RemoteSessionTarget{}
494 if target.Endpoint, err = r.getString(); err != nil {
495 return nil, err
496 }
497 if target.DisplayOrigin, err = r.getString(); err != nil {
498 return nil, err
499 }
500 id, err := r.getBytes(16)
501 if err != nil {
502 return nil, err
503 }
504 copy(target.LifecycleID[:], id)
505 if target.SessionName, err = r.getString(); err != nil {
506 return nil, err
507 }
508 if target.Stopped, err = r.getBool(); err != nil {
509 return nil, err
510 }
511 liveID, err := r.getString()
512 if err != nil {
513 return nil, err
514 }
515 target.LiveTabID = domain.TabStableID(liveID)
516 kind, err := r.getUint8()
517 if err != nil {
518 return nil, err
519 }
520 target.StoppedTab.Kind = domain.TabSelectorKind(kind)
521 switch target.StoppedTab.Kind {
522 case domain.TabSelectorByStableID:
523 stableID, err := r.getString()
524 if err != nil {
525 return nil, err
526 }
527 target.StoppedTab.StableID = domain.TabStableID(stableID)
528 case domain.TabSelectorByOrdinal:
529 if target.StoppedTab.Ordinal, err = r.getUint16(); err != nil {
530 return nil, err
531 }
532 if target.StoppedTab.RawName, err = r.getString(); err != nil {
533 return nil, err
534 }
535 if target.StoppedTab.ExpectedCount, err = r.getUint16(); err != nil {
536 return nil, err
537 }
538 case 0:
539 default:
540 return nil, errInvalidEnum
541 }
542 return target, nil
543}
544
545func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget, EnvironmentPolicy, error) {
546 target, err := unmarshalRemoteTarget(r)
547 if err != nil {
548 return nil, 0, err
549 }
550 policy, err := r.getUint8()
551 if err != nil {
552 return nil, 0, err
553 }
554 if !validEnvironmentPolicy(EnvironmentPolicy(policy)) {
555 return nil, 0, errInvalidEnum
556 }
557 return target, EnvironmentPolicy(policy), nil
558}
559
560// payloadReader consumes a message payload field by field in wire order,
561// erroring instead of panicking on any short read.
562type payloadReader struct {
563 b []byte
564}
565
566func (r *payloadReader) getUint8() (uint8, error) {
567 if len(r.b) < 1 {
568 return 0, errShortPayload
569 }
570 v := r.b[0]
571 r.b = r.b[1:]
572 return v, nil
573}
574
575func (r *payloadReader) getBool() (bool, error) {
576 v, err := r.getUint8()
577 if err != nil {
578 return false, err
579 }
580 if v > 1 {
581 return false, errInvalidBoolean
582 }
583 return v == 1, nil
584}
585
586func (r *payloadReader) getUint16() (uint16, error) {
587 if len(r.b) < 2 {
588 return 0, errShortPayload
589 }
590 v := binary.BigEndian.Uint16(r.b)
591 r.b = r.b[2:]
592 return v, nil
593}
594
595func (r *payloadReader) getUint32() (uint32, error) {
596 if len(r.b) < 4 {
597 return 0, errShortPayload
598 }
599 v := binary.BigEndian.Uint32(r.b)
600 r.b = r.b[4:]
601 return v, nil
602}
603
604func (r *payloadReader) getUint64() (uint64, error) {
605 if len(r.b) < 8 {
606 return 0, errShortPayload
607 }
608 v := binary.BigEndian.Uint64(r.b)
609 r.b = r.b[8:]
610 return v, nil
611}
612
613func (r *payloadReader) getBytes(n int) ([]byte, error) {
614 if len(r.b) < n {
615 return nil, errShortPayload
616 }
617 b := append([]byte(nil), r.b[:n]...)
618 r.b = r.b[n:]
619 return b, nil
620}
621
622func (r *payloadReader) getString() (string, error) {
623 n, err := r.getUint16()
624 if err != nil {
625 return "", err
626 }
627 if len(r.b) < int(n) {
628 return "", errShortPayload
629 }
630 s := string(r.b[:n])
631 r.b = r.b[n:]
632 return s, nil
633}
634
635func (r *payloadReader) getLongString() (string, error) {
636 n, err := r.getUint32()
637 if err != nil {
638 return "", err
639 }
640 if uint64(n) > uint64(len(r.b)) {
641 return "", errShortPayload
642 }
643 s := string(r.b[:int(n)])
644 r.b = r.b[int(n):]
645 return s, nil
646}
647
648func (r *payloadReader) getLongBytes() ([]byte, error) {
649 n, err := r.getUint32()
650 if err != nil {
651 return nil, err
652 }
653 if uint64(n) > uint64(len(r.b)) {
654 return nil, errShortPayload
655 }
656 b := append([]byte(nil), r.b[:int(n)]...)
657 r.b = r.b[int(n):]
658 return b, nil
659}
660
661func (r *payloadReader) skipString() error {
662 n, err := r.getUint16()
663 if err != nil {
664 return err
665 }
666 if int(n) > len(r.b) {
667 return errShortPayload
668 }
669 r.b = r.b[n:]
670 return nil
671}
672
673func (r *payloadReader) skipLongString() error {
674 n, err := r.getUint32()
675 if err != nil {
676 return err
677 }
678 if uint64(n) > uint64(len(r.b)) {
679 return errShortPayload
680 }
681 r.b = r.b[n:]
682 return nil
683}
684
685// rest consumes and returns all remaining bytes, copied so the result is
686// independent of the reader's backing array.
687func (r *payloadReader) rest() []byte {
688 b := append([]byte(nil), r.b...)
689 r.b = nil
690 return b
691}
692
693// done reports an error if any bytes remain unconsumed.
694func (r *payloadReader) done() error {
695 if len(r.b) != 0 {
696 return errTrailingBytes
697 }
698 return nil
699}
700
701// PeekHelloVersion returns the leading protocol version from a Hello payload.
702func PeekHelloVersion(b []byte) (uint16, bool) {
703 if len(b) < 2 {
704 return 0, false
705 }
706 return binary.BigEndian.Uint16(b), true
707}
708
709// ValidateHello validates Hello semantics without retaining or allocating any
710// payload data.
711func ValidateHello(h Hello) error {
712 if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
713 return ErrInvalidHello
714 }
715 if err := ValidateSize(h.Size); err != nil {
716 return fmt.Errorf("%w: size", ErrInvalidHello)
717 }
718 if !validEnvironmentPolicy(h.EnvironmentPolicy) {
719 return ErrInvalidHello
720 }
721 if err := validateHelloNavigation(h); err != nil {
722 return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
723 }
724 if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
725 return ErrInvalidHello
726 }
727 for _, entry := range h.Env {
728 if uint64(len(entry)) > math.MaxUint32 {
729 return ErrInvalidHello
730 }
731 }
732 if h.RemoteOrigin != "" {
733 if err := domain.ValidateRemoteDisplayOrigin(h.RemoteOrigin); err != nil {
734 return fmt.Errorf("%w: remote origin: %v", ErrInvalidHello, err)
735 }
736 }
737 if h.PreferredTabID != "" {
738 if h.Intent != IntentAttach && h.Intent != IntentResume {
739 return ErrInvalidHello
740 }
741 if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
742 return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
743 }
744 }
745 if h.ExactTarget != nil {
746 if h.Intent != IntentAttach && h.Intent != IntentResume {
747 return ErrInvalidHello
748 }
749 if err := h.ExactTarget.Validate(); err != nil {
750 return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
751 }
752 if h.Name != h.ExactTarget.SessionName {
753 return ErrInvalidHello
754 }
755 if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
756 return ErrInvalidHello
757 }
758 }
759 if h.RemoteTarget == nil {
760 if h.EnvironmentPolicy != EnvironmentPolicyClientOwned && h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
761 return ErrInvalidHello
762 }
763 return nil
764 }
765 if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
766 return ErrInvalidHello
767 }
768 if h.Intent != IntentAttach && h.Intent != IntentResume {
769 return ErrInvalidHello
770 }
771 if err := validateWireRemoteTarget(*h.RemoteTarget); err != nil {
772 return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
773 }
774 if h.Name != h.RemoteTarget.SessionName {
775 return ErrInvalidHello
776 }
777 return nil
778}
779
780// ValidateOutput validates the final output state before data allocation.
781func ValidateOutput(m Output) error {
782 if m.Epoch == 0 {
783 return ErrInvalidOutput
784 }
785 if m.New == 0 {
786 if m.Base != 0 || m.Full {
787 return ErrInvalidOutput
788 }
789 } else if (m.Base == 0 && !m.Full) || (m.Base != 0 && (m.Full || m.New != m.Base+1)) {
790 return ErrInvalidOutput
791 }
792 if err := ValidateSize(m.Size); err != nil {
793 return fmt.Errorf("%w: size", ErrInvalidOutput)
794 }
795 if uint64(len(m.Data)) > math.MaxUint32 || len(m.Data) > MaxFrameLen-1-outputPayloadOverhead {
796 return ErrInvalidOutput
797 }
798 return nil
799}
800
801// ValidateAck validates an epoch-scoped output acknowledgement.
802func ValidateAck(m Ack) error {
803 if m.Epoch == 0 {
804 return ErrInvalidAck
805 }
806 return nil
807}
808
809// ValidateAttachTarget validates route fields and, when present, the exact
810// picker identity carried by the handoff.
811func ValidateAttachTarget(m AttachTarget) error {
812 if m.Endpoint == "" || m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
813 return ErrInvalidAttachTarget
814 }
815 if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
816 return ErrInvalidAttachTarget
817 }
818 if m.Intent == IntentResume {
819 return ErrInvalidAttachTarget
820 }
821 if !validEnvironmentPolicy(m.EnvironmentPolicy) {
822 return ErrInvalidAttachTarget
823 }
824 if m.RemoteTarget == nil {
825 return nil
826 }
827 if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
828 return ErrInvalidAttachTarget
829 }
830 if m.Intent != IntentAttach {
831 return ErrInvalidAttachTarget
832 }
833 if err := validateWireRemoteTarget(*m.RemoteTarget); err != nil {
834 return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
835 }
836 if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
837 return ErrInvalidAttachTarget
838 }
839 return nil
840}
841
842func validateWireRemoteTarget(target domain.RemoteSessionTarget) error {
843 if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {
844 return errors.New("remote target string too long")
845 }
846 return target.Validate()
847}
848
849// MarshalHello encodes h into a Hello message payload.
850func MarshalHello(h Hello) []byte {
851 if err := ValidateHello(h); err != nil {
852 return nil
853 }
854 w := payloadWriter{}
855 w.putUint16(h.Version)
856 w.putUint8(h.Intent)
857 w.putBytes(h.ClientID[:])
858 w.putUint64(h.ResumeToken)
859 w.putString(h.Name)
860 w.putUint16(uint16(h.Size.Cols))
861 w.putUint16(uint16(h.Size.Rows))
862 w.putString(h.TermEnv)
863 w.putString(h.Cwd)
864 w.putBool(h.TrueColor)
865 w.putUint8(h.MaxOutputInFlight)
866 w.putUint32(uint32(len(h.Env)))
867 for _, entry := range h.Env {
868 w.putLongString(entry)
869 }
870 marshalRemoteTargetSection(&w, h.RemoteTarget, h.EnvironmentPolicy)
871 marshalExactTargetSection(&w, h.ExactTarget)
872 w.putString(string(h.PreferredTabID))
873 w.putString(h.RemoteOrigin)
874 w.putUint8(uint8(h.NavigationCapabilities))
875 w.putUint8(uint8(h.StartupOverlay))
876 return w.b
877}
878
879func preflightHello(b []byte) error {
880 r := payloadReader{b: b}
881 version, err := r.getUint16()
882 if err != nil {
883 return err
884 }
885 if version == 21 {
886 return ErrInvalidHello
887 }
888 if _, err := r.getUint8(); err != nil {
889 return err
890 }
891 if len(r.b) < 16 {
892 return errShortPayload
893 }
894 r.b = r.b[16:]
895 if _, err := r.getUint64(); err != nil {
896 return err
897 }
898 if err := r.skipString(); err != nil {
899 return err
900 }
901 if _, err := r.getUint16(); err != nil {
902 return err
903 }
904 if _, err := r.getUint16(); err != nil {
905 return err
906 }
907 if err := r.skipString(); err != nil {
908 return err
909 }
910 if err := r.skipString(); err != nil {
911 return err
912 }
913 if _, err := r.getBool(); err != nil {
914 return err
915 }
916 if _, err := r.getUint8(); err != nil {
917 return err
918 }
919 envCount, err := r.getUint32()
920 if err != nil {
921 return err
922 }
923 if uint64(envCount) > uint64(len(r.b)/4) {
924 return errShortPayload
925 }
926 for range int(envCount) {
927 if err := r.skipLongString(); err != nil {
928 return err
929 }
930 }
931 if err := skipRemoteTargetSection(&r); err != nil {
932 return err
933 }
934 if err := skipExactTargetSection(&r); err != nil {
935 return err
936 }
937 if err := r.skipString(); err != nil {
938 return err
939 }
940 if err := r.skipString(); err != nil {
941 return err
942 }
943 if _, err := r.getUint8(); err != nil {
944 return err
945 }
946 if _, err := r.getUint8(); err != nil {
947 return err
948 }
949 return r.done()
950}
951
952// UnmarshalHello decodes a Hello message payload.
953func UnmarshalHello(b []byte) (Hello, error) {
954 if len(b) > MaxFrameLen-1 {
955 return Hello{}, ErrInvalidHello
956 }
957 if err := preflightHello(b); err != nil {
958 return Hello{}, err
959 }
960 r := payloadReader{b: b}
961 var h Hello
962 var err error
963
964 if h.Version, err = r.getUint16(); err != nil {
965 return Hello{}, err
966 }
967 if h.Intent, err = r.getUint8(); err != nil {
968 return Hello{}, err
969 }
970 clientID, err := r.getBytes(len(h.ClientID))
971 if err != nil {
972 return Hello{}, err
973 }
974 copy(h.ClientID[:], clientID)
975 if h.ResumeToken, err = r.getUint64(); err != nil {
976 return Hello{}, err
977 }
978 if h.Name, err = r.getString(); err != nil {
979 return Hello{}, err
980 }
981 cols, err := r.getUint16()
982 if err != nil {
983 return Hello{}, err
984 }
985 rows, err := r.getUint16()
986 if err != nil {
987 return Hello{}, err
988 }
989 h.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
990 if h.TermEnv, err = r.getString(); err != nil {
991 return Hello{}, err
992 }
993 if h.Cwd, err = r.getString(); err != nil {
994 return Hello{}, err
995 }
996 if h.TrueColor, err = r.getBool(); err != nil {
997 return Hello{}, err
998 }
999 if h.MaxOutputInFlight, err = r.getUint8(); err != nil {
1000 return Hello{}, err
1001 }
1002 envCount, err := r.getUint32()
1003 if err != nil {
1004 return Hello{}, err
1005 }
1006 // Each entry has at least its uint32 byte length. Check that before
1007 // allocating so a malformed count cannot force an excessive allocation.
1008 if uint64(envCount) > uint64(len(r.b)/4) {
1009 return Hello{}, errShortPayload
1010 }
1011 if envCount != 0 {
1012 h.Env = make([]string, 0, int(envCount))
1013 for range int(envCount) {
1014 entry, err := r.getLongString()
1015 if err != nil {
1016 return Hello{}, err
1017 }
1018 h.Env = append(h.Env, entry)
1019 }
1020 }
1021 if h.RemoteTarget, h.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {
1022 return Hello{}, err
1023 }
1024 if h.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {
1025 return Hello{}, err
1026 }
1027 preferredTabID, err := r.getString()
1028 if err != nil {
1029 return Hello{}, err
1030 }
1031 h.PreferredTabID = domain.TabStableID(preferredTabID)
1032 if h.RemoteOrigin, err = r.getString(); err != nil {
1033 return Hello{}, err
1034 }
1035 capabilities, err := r.getUint8()
1036 if err != nil {
1037 return Hello{}, err
1038 }
1039 h.NavigationCapabilities = NavigationCapabilities(capabilities)
1040 overlay, err := r.getUint8()
1041 if err != nil {
1042 return Hello{}, err
1043 }
1044 h.StartupOverlay = StartupOverlay(overlay)
1045 if err := r.done(); err != nil {
1046 return Hello{}, err
1047 }
1048 if err := ValidateHello(h); err != nil {
1049 return Hello{}, err
1050 }
1051 return h, nil
1052}
1053
1054// MarshalInput encodes m into an Input message payload.
1055func MarshalInput(m Input) []byte {
1056 w := payloadWriter{}
1057 w.putUint64(m.InputSeq)
1058 w.putBytes(m.Data)
1059 return w.b
1060}
1061
1062// UnmarshalInput decodes an Input message payload. After the fixed input
1063// sequence, the rest of the payload is data; there is no length prefix.
1064func UnmarshalInput(b []byte) (Input, error) {
1065 r := payloadReader{b: b}
1066 seq, err := r.getUint64()
1067 if err != nil {
1068 return Input{}, err
1069 }
1070 return Input{InputSeq: seq, Data: r.rest()}, nil
1071}
1072
1073// MarshalImagePush encodes m into an ImagePush message payload.
1074func MarshalImagePush(m ImagePush) []byte {
1075 w := payloadWriter{}
1076 w.putUint64(m.InputSeq)
1077 w.putString(m.Mime)
1078 w.putBytes(m.Data)
1079 return w.b
1080}
1081
1082// UnmarshalImagePush decodes an ImagePush message payload. After the fixed
1083// input sequence and length-prefixed mime string, the rest of the payload is
1084// data; there is no length prefix for it.
1085func UnmarshalImagePush(b []byte) (ImagePush, error) {
1086 r := payloadReader{b: b}
1087 seq, err := r.getUint64()
1088 if err != nil {
1089 return ImagePush{}, err
1090 }
1091 mime, err := r.getString()
1092 if err != nil {
1093 return ImagePush{}, err
1094 }
1095 return ImagePush{InputSeq: seq, Mime: mime, Data: r.rest()}, nil
1096}
1097
1098// MarshalClientNotice encodes a fixed one-byte client notice action.
1099func MarshalClientNotice(m ClientNotice) []byte {
1100 return []byte{m.Action}
1101}
1102
1103// UnmarshalClientNotice decodes a fixed client notice action and rejects both
1104// unknown values and any trailing bytes.
1105func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
1106 r := payloadReader{b: b}
1107 action, err := r.getUint8()
1108 if err != nil {
1109 return ClientNotice{}, err
1110 }
1111 if !validClientNoticeAction(action) {
1112 return ClientNotice{}, errors.New("ports: unknown client notice action")
1113 }
1114 if err := r.done(); err != nil {
1115 return ClientNotice{}, err
1116 }
1117 return ClientNotice{Action: action}, nil
1118}
1119
1120// MarshalResize encodes m into a Resize message payload.
1121func MarshalResize(m Resize) ([]byte, error) {
1122 if err := ValidateSize(m.Size); err != nil {
1123 return nil, err
1124 }
1125 w := payloadWriter{}
1126 w.putUint16(uint16(m.Size.Cols))
1127 w.putUint16(uint16(m.Size.Rows))
1128 return w.b, nil
1129}
1130
1131// MarshalTheme encodes m into a 57-byte fixed-width Theme message payload.
1132func MarshalTheme(m Theme) []byte {
1133 var flags uint8
1134 if m.HasForeground {
1135 flags |= 0x01
1136 }
1137 if m.HasBackground {
1138 flags |= 0x02
1139 }
1140 if m.TrueColor {
1141 flags |= 0x04
1142 }
1143 if m.SchemeKnown {
1144 flags |= 0x08
1145 }
1146 if m.Light {
1147 flags |= 0x10
1148 }
1149
1150 w := payloadWriter{b: make([]byte, 0, 57)}
1151 w.putUint8(flags)
1152 w.putUint8(m.Foreground.R)
1153 w.putUint8(m.Foreground.G)
1154 w.putUint8(m.Foreground.B)
1155 w.putUint8(m.Background.R)
1156 w.putUint8(m.Background.G)
1157 w.putUint8(m.Background.B)
1158 w.putUint16(m.PaletteKnown)
1159 for _, color := range m.Palette {
1160 w.putUint8(color.R)
1161 w.putUint8(color.G)
1162 w.putUint8(color.B)
1163 }
1164 return w.b
1165}
1166
1167// UnmarshalTheme decodes a 57-byte fixed-width Theme message payload.
1168func UnmarshalTheme(b []byte) (Theme, error) {
1169 r := payloadReader{b: b}
1170 flags, err := r.getUint8()
1171 if err != nil {
1172 return Theme{}, err
1173 }
1174 if flags&^uint8(0x1f) != 0 {
1175 return Theme{}, errInvalidEnum
1176 }
1177 fgR, err := r.getUint8()
1178 if err != nil {
1179 return Theme{}, err
1180 }
1181 fgG, err := r.getUint8()
1182 if err != nil {
1183 return Theme{}, err
1184 }
1185 fgB, err := r.getUint8()
1186 if err != nil {
1187 return Theme{}, err
1188 }
1189 bgR, err := r.getUint8()
1190 if err != nil {
1191 return Theme{}, err
1192 }
1193 bgG, err := r.getUint8()
1194 if err != nil {
1195 return Theme{}, err
1196 }
1197 bgB, err := r.getUint8()
1198 if err != nil {
1199 return Theme{}, err
1200 }
1201 paletteKnown, err := r.getUint16()
1202 if err != nil {
1203 return Theme{}, err
1204 }
1205
1206 m := Theme{
1207 HasForeground: flags&0x01 != 0,
1208 Foreground: renderer.RGB{R: fgR, G: fgG, B: fgB},
1209 HasBackground: flags&0x02 != 0,
1210 Background: renderer.RGB{R: bgR, G: bgG, B: bgB},
1211 TrueColor: flags&0x04 != 0,
1212 SchemeKnown: flags&0x08 != 0,
1213 Light: flags&0x10 != 0,
1214 PaletteKnown: paletteKnown,
1215 }
1216 for i := range m.Palette {
1217 if m.Palette[i].R, err = r.getUint8(); err != nil {
1218 return Theme{}, err
1219 }
1220 if m.Palette[i].G, err = r.getUint8(); err != nil {
1221 return Theme{}, err
1222 }
1223 if m.Palette[i].B, err = r.getUint8(); err != nil {
1224 return Theme{}, err
1225 }
1226 }
1227 if err := r.done(); err != nil {
1228 return Theme{}, err
1229 }
1230 return m, nil
1231}
1232
1233// UnmarshalResize decodes a Resize message payload.
1234func UnmarshalResize(b []byte) (Resize, error) {
1235 r := payloadReader{b: b}
1236 cols, err := r.getUint16()
1237 if err != nil {
1238 return Resize{}, err
1239 }
1240 rows, err := r.getUint16()
1241 if err != nil {
1242 return Resize{}, err
1243 }
1244 if err := r.done(); err != nil {
1245 return Resize{}, err
1246 }
1247 m := Resize{Size: domain.Size{Cols: int(cols), Rows: int(rows)}}
1248 if err := ValidateSize(m.Size); err != nil {
1249 return Resize{}, err
1250 }
1251 return m, nil
1252}
1253
1254// MarshalDetach encodes a Detach message payload (always empty).
1255func MarshalDetach(Detach) []byte {
1256 return nil
1257}
1258
1259// UnmarshalDetach decodes a Detach message payload.
1260func UnmarshalDetach(b []byte) (Detach, error) {
1261 r := payloadReader{b: b}
1262 if err := r.done(); err != nil {
1263 return Detach{}, err
1264 }
1265 return Detach{}, nil
1266}
1267
1268// MarshalPing encodes a Ping message payload (always empty).
1269func MarshalPing(Ping) []byte {
1270 return nil
1271}
1272
1273// UnmarshalPing decodes a Ping message payload.
1274func UnmarshalPing(b []byte) (Ping, error) {
1275 r := payloadReader{b: b}
1276 if err := r.done(); err != nil {
1277 return Ping{}, err
1278 }
1279 return Ping{}, nil
1280}
1281
1282// MarshalPong encodes a Pong message payload (always empty).
1283func MarshalPong(Pong) []byte {
1284 return nil
1285}
1286
1287// UnmarshalPong decodes a Pong message payload.
1288func UnmarshalPong(b []byte) (Pong, error) {
1289 r := payloadReader{b: b}
1290 if err := r.done(); err != nil {
1291 return Pong{}, err
1292 }
1293 return Pong{}, nil
1294}
1295
1296// MarshalAck encodes m into an epoch/state Ack payload.
1297func MarshalAck(m Ack) ([]byte, error) {
1298 if err := ValidateAck(m); err != nil {
1299 return nil, err
1300 }
1301 w := payloadWriter{}
1302 w.putUint64(m.Epoch)
1303 w.putUint64(m.State)
1304 return w.b, nil
1305}
1306
1307// UnmarshalAck decodes an epoch/state Ack payload.
1308func UnmarshalAck(b []byte) (Ack, error) {
1309 r := payloadReader{b: b}
1310 var m Ack
1311 var err error
1312 if m.Epoch, err = r.getUint64(); err != nil {
1313 return Ack{}, err
1314 }
1315 if m.State, err = r.getUint64(); err != nil {
1316 return Ack{}, err
1317 }
1318 if err := r.done(); err != nil {
1319 return Ack{}, err
1320 }
1321 if err := ValidateAck(m); err != nil {
1322 return Ack{}, err
1323 }
1324 return m, nil
1325}
1326
1327// MarshalWelcome encodes m into a Welcome message payload.
1328func MarshalWelcome(m Welcome) []byte {
1329 if m.CommittedIdentity != nil && (m.SessionName != m.CommittedIdentity.Target.SessionName || m.Ephemeral != m.CommittedIdentity.Ephemeral) {
1330 return nil
1331 }
1332 w := payloadWriter{}
1333 w.putString(m.SessionID)
1334 w.putString(m.SessionName)
1335 if m.Ephemeral {
1336 w.putUint8(1)
1337 } else {
1338 w.putUint8(0)
1339 }
1340 w.putUint64(m.ResumeToken)
1341 w.putUint32(m.Capabilities)
1342 if !marshalCommittedIdentitySection(&w, m.CommittedIdentity) {
1343 return nil
1344 }
1345 return w.b
1346}
1347
1348// UnmarshalWelcome decodes a Welcome message payload.
1349func UnmarshalWelcome(b []byte) (Welcome, error) {
1350 r := payloadReader{b: b}
1351 var m Welcome
1352 var err error
1353
1354 if m.SessionID, err = r.getString(); err != nil {
1355 return Welcome{}, err
1356 }
1357 if m.SessionName, err = r.getString(); err != nil {
1358 return Welcome{}, err
1359 }
1360 var ephemeral bool
1361 if ephemeral, err = r.getBool(); err != nil {
1362 return Welcome{}, err
1363 }
1364 m.Ephemeral = ephemeral
1365 if m.ResumeToken, err = r.getUint64(); err != nil {
1366 return Welcome{}, err
1367 }
1368 if m.Capabilities, err = r.getUint32(); err != nil {
1369 return Welcome{}, err
1370 }
1371 if m.CommittedIdentity, err = unmarshalCommittedIdentitySection(&r); err != nil {
1372 return Welcome{}, err
1373 }
1374 if m.CommittedIdentity != nil && (m.CommittedIdentity.Target.SessionName != m.SessionName || m.CommittedIdentity.Ephemeral != m.Ephemeral) {
1375 return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", ErrInvalidRouteWire)
1376 }
1377 if err := r.done(); err != nil {
1378 return Welcome{}, err
1379 }
1380 return m, nil
1381}
1382
1383// MarshalErrorMsg encodes m into an ErrorMsg message payload.
1384func MarshalErrorMsg(m ErrorMsg) []byte {
1385 w := payloadWriter{}
1386 w.putUint16(m.Code)
1387 w.putString(m.Text)
1388 return w.b
1389}
1390
1391// UnmarshalErrorMsg decodes an ErrorMsg message payload.
1392func UnmarshalErrorMsg(b []byte) (ErrorMsg, error) {
1393 r := payloadReader{b: b}
1394 var m ErrorMsg
1395 var err error
1396
1397 if m.Code, err = r.getUint16(); err != nil {
1398 return ErrorMsg{}, err
1399 }
1400 if m.Text, err = r.getString(); err != nil {
1401 return ErrorMsg{}, err
1402 }
1403 if err := r.done(); err != nil {
1404 return ErrorMsg{}, err
1405 }
1406 return m, nil
1407}
1408
1409// MarshalOutput encodes m into the epoch/base/new/echo/viewRevision/
1410// size/full/data message layout.
1411func MarshalOutput(m Output) ([]byte, error) {
1412 if err := ValidateOutput(m); err != nil {
1413 return nil, err
1414 }
1415 w := payloadWriter{}
1416 w.putUint64(m.Epoch)
1417 w.putUint64(m.Base)
1418 w.putUint64(m.New)
1419 w.putUint64(m.Echo)
1420 w.putUint64(m.ViewRevision)
1421 w.putUint16(uint16(m.Size.Cols))
1422 w.putUint16(uint16(m.Size.Rows))
1423 w.putBool(m.Full)
1424 w.putLongBytes(m.Data)
1425 return w.b, nil
1426}
1427
1428// UnmarshalOutput strictly decodes one final Output payload. Header semantics
1429// are validated before the length-prefixed data is copied.
1430func UnmarshalOutput(b []byte) (Output, error) {
1431 if len(b) > MaxFrameLen-1 {
1432 return Output{}, ErrInvalidOutput
1433 }
1434 if len(b) < outputPayloadOverhead {
1435 return Output{}, errShortPayload
1436 }
1437 r := payloadReader{b: b}
1438 var m Output
1439 var err error
1440 if m.Epoch, err = r.getUint64(); err != nil {
1441 return Output{}, err
1442 }
1443 if m.Base, err = r.getUint64(); err != nil {
1444 return Output{}, err
1445 }
1446 if m.New, err = r.getUint64(); err != nil {
1447 return Output{}, err
1448 }
1449 if m.Echo, err = r.getUint64(); err != nil {
1450 return Output{}, err
1451 }
1452 if m.ViewRevision, err = r.getUint64(); err != nil {
1453 return Output{}, err
1454 }
1455 cols, err := r.getUint16()
1456 if err != nil {
1457 return Output{}, err
1458 }
1459 rows, err := r.getUint16()
1460 if err != nil {
1461 return Output{}, err
1462 }
1463 m.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
1464 if m.Full, err = r.getBool(); err != nil {
1465 return Output{}, err
1466 }
1467 if err := ValidateOutput(m); err != nil {
1468 return Output{}, err
1469 }
1470 if len(r.b) < 4 {
1471 return Output{}, errShortPayload
1472 }
1473 dataLen := binary.BigEndian.Uint32(r.b)
1474 if uint64(dataLen) > uint64(len(r.b)-4) {
1475 return Output{}, errShortPayload
1476 }
1477 if uint64(dataLen) < uint64(len(r.b)-4) {
1478 return Output{}, errTrailingBytes
1479 }
1480 data, err := r.getLongBytes()
1481 if err != nil {
1482 return Output{}, err
1483 }
1484 if err := r.done(); err != nil {
1485 return Output{}, err
1486 }
1487 m.Data = data
1488 return m, nil
1489}
1490
1491// MarshalAttachTarget encodes a strict server attach-target payload.
1492func MarshalAttachTarget(m AttachTarget) []byte {
1493 if err := ValidateAttachTarget(m); err != nil {
1494 return nil
1495 }
1496 w := payloadWriter{}
1497 w.putString(m.Endpoint)
1498 w.putString(m.Session)
1499 w.putUint8(m.Intent)
1500 marshalRemoteTargetSection(&w, m.RemoteTarget, m.EnvironmentPolicy)
1501 return w.b
1502}
1503
1504// UnmarshalAttachTarget decodes a strict server attach-target payload.
1505func UnmarshalAttachTarget(b []byte) (AttachTarget, error) {
1506 // Preflight lengths and intent before getString can allocate either value.
1507 probe := payloadReader{b: b}
1508 endpointLen, err := probe.getUint16()
1509 if err != nil || endpointLen == 0 || int(endpointLen) > len(probe.b) {
1510 return AttachTarget{}, ErrInvalidAttachTarget
1511 }
1512 probe.b = probe.b[endpointLen:]
1513 sessionLen, err := probe.getUint16()
1514 if err != nil || sessionLen == 0 || int(sessionLen) > len(probe.b) {
1515 return AttachTarget{}, ErrInvalidAttachTarget
1516 }
1517 probe.b = probe.b[sessionLen:]
1518 intent, err := probe.getUint8()
1519 if err != nil || (intent != IntentEphemeral && intent != IntentNew && intent != IntentAttach && intent != IntentResume) {
1520 return AttachTarget{}, ErrInvalidAttachTarget
1521 }
1522 if err := skipRemoteTargetSection(&probe); err != nil {
1523 return AttachTarget{}, ErrInvalidAttachTarget
1524 }
1525 if err := probe.done(); err != nil {
1526 return AttachTarget{}, ErrInvalidAttachTarget
1527 }
1528
1529 r := payloadReader{b: b}
1530 var m AttachTarget
1531 if m.Endpoint, err = r.getString(); err != nil {
1532 return AttachTarget{}, err
1533 }
1534 if m.Session, err = r.getString(); err != nil {
1535 return AttachTarget{}, err
1536 }
1537 if m.Intent, err = r.getUint8(); err != nil {
1538 return AttachTarget{}, err
1539 }
1540 if m.RemoteTarget, m.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {
1541 return AttachTarget{}, err
1542 }
1543 if err := r.done(); err != nil {
1544 return AttachTarget{}, err
1545 }
1546 if err := ValidateAttachTarget(m); err != nil {
1547 return AttachTarget{}, err
1548 }
1549 return m, nil
1550}
1551
1552// MarshalDetached encodes m into a Detached message payload.
1553func MarshalDetached(m Detached) []byte {
1554 return []byte{m.Reason}
1555}
1556
1557// UnmarshalDetached decodes a Detached message payload.
1558func UnmarshalDetached(b []byte) (Detached, error) {
1559 r := payloadReader{b: b}
1560 reason, err := r.getUint8()
1561 if err != nil {
1562 return Detached{}, err
1563 }
1564 if !validDetachedReason(reason) {
1565 return Detached{}, errInvalidEnum
1566 }
1567 if err := r.done(); err != nil {
1568 return Detached{}, err
1569 }
1570 return Detached{Reason: reason}, nil
1571}
1572
1573// MarshalList encodes a List message payload (always empty).
1574func MarshalList(List) []byte {
1575 return nil
1576}
1577
1578// UnmarshalList decodes a List message payload.
1579func UnmarshalList(b []byte) (List, error) {
1580 r := payloadReader{b: b}
1581 if err := r.done(); err != nil {
1582 return List{}, err
1583 }
1584 return List{}, nil
1585}
1586
1587// MarshalOutputResetRequest encodes an OutputResetRequest payload (always
1588// empty).
1589func MarshalOutputResetRequest(OutputResetRequest) []byte {
1590 return nil
1591}
1592
1593// UnmarshalOutputResetRequest decodes a strict empty OutputResetRequest
1594// payload.
1595func UnmarshalOutputResetRequest(b []byte) (OutputResetRequest, error) {
1596 r := payloadReader{b: b}
1597 if err := r.done(); err != nil {
1598 return OutputResetRequest{}, err
1599 }
1600 return OutputResetRequest{}, nil
1601}
1602
1603// MarshalKill encodes m into a Kill message payload.
1604func MarshalKill(m Kill) []byte {
1605 w := payloadWriter{}
1606 w.putString(m.Name)
1607 if m.All {
1608 w.putUint8(1)
1609 }
1610 return w.b
1611}
1612
1613// UnmarshalKill decodes a Kill message payload.
1614func UnmarshalKill(b []byte) (Kill, error) {
1615 r := payloadReader{b: b}
1616 name, err := r.getString()
1617 if err != nil {
1618 return Kill{}, err
1619 }
1620 var all bool
1621 if len(r.b) > 0 {
1622 all, err = r.getBool()
1623 if err != nil {
1624 return Kill{}, err
1625 }
1626 }
1627 if err := r.done(); err != nil {
1628 return Kill{}, err
1629 }
1630 return Kill{Name: name, All: all}, nil
1631}
1632
1633// MarshalSessions encodes m into a Sessions message payload: a uint16 count
1634// followed by that many session records.
1635func MarshalSessions(m Sessions) []byte {
1636 w := payloadWriter{}
1637 w.putUint16(uint16(len(m.Sessions)))
1638 for _, s := range m.Sessions {
1639 w.putString(s.SessionID)
1640 w.putString(s.Name)
1641 if s.Ephemeral {
1642 w.putUint8(1)
1643 } else {
1644 w.putUint8(0)
1645 }
1646 w.putUint16(s.Tabs)
1647 if s.Attached {
1648 w.putUint8(1)
1649 } else {
1650 w.putUint8(0)
1651 }
1652 w.putUint8(uint8(s.State))
1653 }
1654 return w.b
1655}
1656
1657// UnmarshalSessions decodes a Sessions message payload.
1658func UnmarshalSessions(b []byte) (Sessions, error) {
1659 r := payloadReader{b: b}
1660 count, err := r.getUint16()
1661 if err != nil {
1662 return Sessions{}, err
1663 }
1664 sessions := make([]SessionInfo, 0, count)
1665 for range int(count) {
1666 var s SessionInfo
1667 if s.SessionID, err = r.getString(); err != nil {
1668 return Sessions{}, err
1669 }
1670 if s.Name, err = r.getString(); err != nil {
1671 return Sessions{}, err
1672 }
1673 eph, err := r.getBool()
1674 if err != nil {
1675 return Sessions{}, err
1676 }
1677 s.Ephemeral = eph
1678 if s.Tabs, err = r.getUint16(); err != nil {
1679 return Sessions{}, err
1680 }
1681 att, err := r.getBool()
1682 if err != nil {
1683 return Sessions{}, err
1684 }
1685 s.Attached = att
1686 state, err := r.getUint8()
1687 if err != nil {
1688 return Sessions{}, err
1689 }
1690 s.State = SessionState(state)
1691 if s.State > SessionBroken {
1692 return Sessions{}, errors.New("ports: invalid session state")
1693 }
1694 sessions = append(sessions, s)
1695 }
1696 if err := r.done(); err != nil {
1697 return Sessions{}, err
1698 }
1699 return Sessions{Sessions: sessions}, nil
1700}
1701func putInt16(w *payloadWriter, n int) { w.putUint16(uint16(int16(n))) }
1702
1703func getInt16(r *payloadReader) (int, error) {
1704 n, err := r.getUint16()
1705 return int(int16(n)), err
1706}
1707
1708func putRGB(w *payloadWriter, c renderer.RGB) {
1709 w.putUint8(c.R)
1710 w.putUint8(c.G)
1711 w.putUint8(c.B)
1712}
1713
1714func getRGB(r *payloadReader) (renderer.RGB, error) {
1715 red, err := r.getUint8()
1716 if err != nil {
1717 return renderer.RGB{}, err
1718 }
1719 green, err := r.getUint8()
1720 if err != nil {
1721 return renderer.RGB{}, err
1722 }
1723 blue, err := r.getUint8()
1724 if err != nil {
1725 return renderer.RGB{}, err
1726 }
1727 return renderer.RGB{R: red, G: green, B: blue}, nil
1728}
1729
1730func putPreviewStyle(w *payloadWriter, s renderer.Style) {
1731 var flags uint8
1732 if s.Bold {
1733 flags |= 1 << 0
1734 }
1735 if s.Italic {
1736 flags |= 1 << 1
1737 }
1738 if s.Inverse {
1739 flags |= 1 << 2
1740 }
1741 if s.HasForegroundRGB {
1742 flags |= 1 << 3
1743 }
1744 if s.HasBackgroundRGB {
1745 flags |= 1 << 4
1746 }
1747 if s.HasUnderlineColor {
1748 flags |= 1 << 5
1749 }
1750 if s.HasUnderlineColorRGB {
1751 flags |= 1 << 6
1752 }
1753 w.putUint8(flags)
1754 w.putUint16(uint16(s.Attrs))
1755 putInt16(w, s.Foreground)
1756 putInt16(w, s.Background)
1757 w.putUint8(uint8(s.UnderlineStyle))
1758 putInt16(w, s.UnderlineColor)
1759 putRGB(w, s.ForegroundRGB)
1760 putRGB(w, s.BackgroundRGB)
1761 putRGB(w, s.UnderlineColorRGB)
1762}
1763
1764func getPreviewStyle(r *payloadReader) (renderer.Style, error) {
1765 flags, err := r.getUint8()
1766 if err != nil {
1767 return renderer.Style{}, err
1768 }
1769 if flags&0x80 != 0 {
1770 return renderer.Style{}, ErrRemotePreviewUnsupportedStyle
1771 }
1772 attrs, err := r.getUint16()
1773 if err != nil {
1774 return renderer.Style{}, err
1775 }
1776 fg, err := getInt16(r)
1777 if err != nil {
1778 return renderer.Style{}, err
1779 }
1780 bg, err := getInt16(r)
1781 if err != nil {
1782 return renderer.Style{}, err
1783 }
1784 ul, err := r.getUint8()
1785 if err != nil {
1786 return renderer.Style{}, err
1787 }
1788 ulc, err := getInt16(r)
1789 if err != nil {
1790 return renderer.Style{}, err
1791 }
1792 fgrgb, err := getRGB(r)
1793 if err != nil {
1794 return renderer.Style{}, err
1795 }
1796 bgrgb, err := getRGB(r)
1797 if err != nil {
1798 return renderer.Style{}, err
1799 }
1800 ulrgb, err := getRGB(r)
1801 if err != nil {
1802 return renderer.Style{}, err
1803 }
1804 return renderer.Style{Bold: flags&1 != 0, Italic: flags&(1<<1) != 0, Inverse: flags&(1<<2) != 0,
1805 HasForegroundRGB: flags&(1<<3) != 0, HasBackgroundRGB: flags&(1<<4) != 0,
1806 HasUnderlineColor: flags&(1<<5) != 0, HasUnderlineColorRGB: flags&(1<<6) != 0,
1807 Attrs: renderer.StyleAttrs(attrs), Foreground: fg, Background: bg, UnderlineStyle: renderer.UnderlineStyle(ul), UnderlineColor: ulc,
1808 ForegroundRGB: fgrgb, BackgroundRGB: bgrgb, UnderlineColorRGB: ulrgb}, nil
1809}
1810
1811func MarshalRemotePreviewRequest(request RemotePreviewRequest) []byte {
1812 if ValidateRemotePreviewRequest(request) != nil {
1813 return nil
1814 }
1815 w := payloadWriter{}
1816 w.putUint16(request.Version)
1817 w.putBytes(request.Target.LifecycleID[:])
1818 w.putString(request.Target.Endpoint)
1819 w.putString(request.Target.DisplayOrigin)
1820 w.putString(request.Target.SessionName)
1821 w.putString(string(request.Target.LiveTabID))
1822 w.putUint16(request.Width)
1823 w.putUint16(request.Height)
1824 return w.b
1825}
1826
1827func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {
1828 if len(data) > RemotePreviewMaxBytes {
1829 return RemotePreviewRequest{}, ErrInvalidRemotePreviewRequest
1830 }
1831 r := payloadReader{b: data}
1832 var q RemotePreviewRequest
1833 var err error
1834 if q.Version, err = r.getUint16(); err != nil {
1835 return q, err
1836 }
1837 id, err := r.getBytes(16)
1838 if err != nil {
1839 return q, err
1840 }
1841 copy(q.Target.LifecycleID[:], id)
1842 if q.Target.Endpoint, err = r.getString(); err != nil {
1843 return q, err
1844 }
1845 if q.Target.DisplayOrigin, err = r.getString(); err != nil {
1846 return q, err
1847 }
1848 if q.Target.SessionName, err = r.getString(); err != nil {
1849 return q, err
1850 }
1851 tab, err := r.getString()
1852 if err != nil {
1853 return q, err
1854 }
1855 q.Target.LiveTabID = domain.TabStableID(tab)
1856 if q.Width, err = r.getUint16(); err != nil {
1857 return q, err
1858 }
1859 if q.Height, err = r.getUint16(); err != nil {
1860 return q, err
1861 }
1862 if err := r.done(); err != nil {
1863 return q, err
1864 }
1865 if err := ValidateRemotePreviewRequest(q); err != nil {
1866 return q, err
1867 }
1868 return q, nil
1869}
1870
1871const previewCellWireSize = 4 + 1 + 1 + 2 + 2 + 2 + 1 + 2 + 3 + 3 + 3
1872
1873func MarshalRemotePreview(preview RemotePreview) []byte {
1874 if ValidateRemotePreview(preview) != nil {
1875 return nil
1876 }
1877 w := payloadWriter{}
1878 w.putUint16(preview.Version)
1879 w.putUint8(uint8(preview.Status))
1880 w.putBytes(preview.LifecycleID[:])
1881 w.putString(string(preview.TabID))
1882 w.putUint64(preview.Revision)
1883 w.putUint16(preview.Width)
1884 w.putUint16(preview.Height)
1885 w.putUint32(uint32(len(preview.Cells)))
1886 for _, cell := range preview.Cells {
1887 var flags uint8
1888 if cell.Continuation {
1889 flags = 1
1890 }
1891 w.putUint32(uint32(cell.Rune))
1892 w.putUint8(flags)
1893 putPreviewStyle(&w, cell.Style)
1894 }
1895 return w.b
1896}
1897
1898func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
1899 if len(data) > RemotePreviewMaxBytes {
1900 return RemotePreview{}, ErrRemotePreviewTooLarge
1901 }
1902 r := payloadReader{b: data}
1903 var p RemotePreview
1904 var err error
1905 if p.Version, err = r.getUint16(); err != nil {
1906 return p, err
1907 }
1908 status, err := r.getUint8()
1909 if err != nil {
1910 return p, err
1911 }
1912 p.Status = RemotePreviewStatus(status)
1913 id, err := r.getBytes(16)
1914 if err != nil {
1915 return p, err
1916 }
1917 copy(p.LifecycleID[:], id)
1918 tab, err := r.getString()
1919 if err != nil {
1920 return p, err
1921 }
1922 p.TabID = domain.TabStableID(tab)
1923 if p.Revision, err = r.getUint64(); err != nil {
1924 return p, err
1925 }
1926 if p.Width, err = r.getUint16(); err != nil {
1927 return p, err
1928 }
1929 if p.Height, err = r.getUint16(); err != nil {
1930 return p, err
1931 }
1932 count, err := r.getUint32()
1933 if err != nil {
1934 return p, err
1935 }
1936 if count > RemotePreviewMaxCells || uint64(count) > uint64(len(r.b)/previewCellWireSize) {
1937 return p, ErrRemotePreviewTooLarge
1938 }
1939 if count != 0 {
1940 p.Cells = make([]renderer.Cell, 0, int(count))
1941 for range int(count) {
1942 runeValue, e := r.getUint32()
1943 if e != nil {
1944 return p, e
1945 }
1946 flags, e := r.getUint8()
1947 if e != nil {
1948 return p, e
1949 }
1950 if flags&^uint8(1) != 0 {
1951 return p, ErrInvalidRemotePreview
1952 }
1953 style, e := getPreviewStyle(&r)
1954 if e != nil {
1955 return p, e
1956 }
1957 p.Cells = append(p.Cells, renderer.Cell{Rune: rune(runeValue), Continuation: flags&1 != 0, Style: style})
1958 }
1959 }
1960 if err := r.done(); err != nil {
1961 return p, err
1962 }
1963 if err := ValidateRemotePreview(p); err != nil {
1964 return p, err
1965 }
1966 return p, nil
1967}
1968
1969// MarshalNavigationAction encodes one bounded navigation action.
1970func MarshalNavigationAction(action NavigationAction) []byte {
1971 if action != NavigationOpenHomePicker && action != NavigationBack {
1972 return nil
1973 }
1974 return []byte{byte(action)}
1975}
1976
1977// UnmarshalNavigationAction decodes one strict navigation action payload.
1978func UnmarshalNavigationAction(b []byte) (NavigationAction, error) {
1979 r := payloadReader{b: b}
1980 value, err := r.getUint8()
1981 if err != nil {
1982 return 0, ErrInvalidNavigation
1983 }
1984 if err := r.done(); err != nil {
1985 return 0, ErrInvalidNavigation
1986 }
1987 action := NavigationAction(value)
1988 if action != NavigationOpenHomePicker && action != NavigationBack {
1989 return 0, ErrInvalidNavigation
1990 }
1991 return action, nil
1992}