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