v0.3.0 99dc266

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	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}

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