v0.2.0 80c61e3

wire.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close