1package domain
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "strings"
8 "unicode"
9 "unicode/utf8"
10)
11
12// SessionLifecycleID is the opaque identity of one session lifecycle.
13//
14// IncarnationID predates the remote picker and is the durable implementation
15// used by snapshots and the catalogue. This alias deliberately keeps one
16// identity type rather than introducing a second, independently comparable
17// UUID. A lifecycle ID changes only when a session is destroyed and recreated.
18type SessionLifecycleID = IncarnationID
19
20// NewSessionLifecycleID allocates a non-zero lifecycle identity.
21func NewSessionLifecycleID(r io.Reader) (SessionLifecycleID, error) {
22 return NewIncarnationID(r)
23}
24
25// TabSelectorKind identifies the exact selector form used for a stopped tab.
26type TabSelectorKind uint8
27
28const (
29 TabSelectorByStableID TabSelectorKind = iota + 1
30 TabSelectorByOrdinal
31)
32
33// TabSelector is an exact tab identity handoff. Stable IDs are preferred for
34// persisted tabs. The ordinal form is a compatibility selector for catalogue
35// records that predate stable stopped-tab IDs; raw name and expected count are
36// required so a reordered or replaced tab cannot be selected accidentally.
37type TabSelector struct {
38 Kind TabSelectorKind
39 StableID TabStableID
40 Ordinal uint16
41 RawName string
42 ExpectedCount uint16
43}
44
45// NewStableTabSelector creates an exact stable-ID selector.
46func NewStableTabSelector(id TabStableID) TabSelector {
47 return TabSelector{Kind: TabSelectorByStableID, StableID: id}
48}
49
50// NewOrdinalTabSelector creates an exact legacy-compatible selector.
51func NewOrdinalTabSelector(ordinal uint16, rawName string, expectedCount uint16) TabSelector {
52 return TabSelector{Kind: TabSelectorByOrdinal, Ordinal: ordinal, RawName: rawName, ExpectedCount: expectedCount}
53}
54
55// Validate rejects selectors that could silently resolve a different tab.
56func (s TabSelector) Validate() error {
57 switch s.Kind {
58 case TabSelectorByStableID:
59 if s.Ordinal != 0 || s.RawName != "" || s.ExpectedCount != 0 || ValidateTabStableID(s.StableID) != nil {
60 return errors.New("invalid stable tab selector")
61 }
62 case TabSelectorByOrdinal:
63 if s.ExpectedCount == 0 || s.Ordinal >= s.ExpectedCount || s.StableID != "" {
64 return errors.New("invalid ordinal tab selector")
65 }
66 if err := validateTabSelectorName(s.RawName); err != nil {
67 return err
68 }
69 default:
70 return errors.New("unknown tab selector kind")
71 }
72 return nil
73}
74
75// TabSelectorTab is the immutable metadata needed to resolve a selector.
76type TabSelectorTab struct {
77 ID TabStableID
78 Name string
79}
80
81// Resolve returns the selected tab index only when the complete selector still
82// describes the supplied ordered tab metadata.
83func (s TabSelector) Resolve(tabs []TabSelectorTab) (int, bool) {
84 if s.Validate() != nil || (len(tabs) != int(s.ExpectedCount) && s.Kind == TabSelectorByOrdinal) {
85 return 0, false
86 }
87 switch s.Kind {
88 case TabSelectorByStableID:
89 if len(tabs) == 0 {
90 return 0, false
91 }
92 found := -1
93 for i, tab := range tabs {
94 if tab.ID != s.StableID {
95 continue
96 }
97 if found != -1 {
98 return 0, false
99 }
100 found = i
101 }
102 return found, found >= 0
103 case TabSelectorByOrdinal:
104 index := int(s.Ordinal)
105 return index, index < len(tabs) && tabs[index].Name == s.RawName
106 default:
107 return 0, false
108 }
109}
110
111// RemoteSessionTarget is the structured identity used by picker-based remote
112// access. Endpoint and DisplayOrigin are intentionally separate values: the
113// latter is presentation-only and is never parsed back into a route.
114type RemoteSessionTarget struct {
115 Endpoint string
116 DisplayOrigin string
117 LifecycleID SessionLifecycleID
118 SessionName string
119 LiveTabID TabStableID
120 StoppedTab TabSelector
121 Stopped bool
122}
123
124// Validate validates route, presentation, lifecycle, and selector identity as
125// independent contract fields.
126func (t RemoteSessionTarget) Validate() error {
127 if err := ValidateRemoteHostTarget(t.Endpoint); err != nil {
128 return fmt.Errorf("remote route endpoint: %w", err)
129 }
130 if err := ValidateRemoteDisplayOrigin(t.DisplayOrigin); err != nil {
131 return fmt.Errorf("remote display origin: %w", err)
132 }
133 if t.LifecycleID == (SessionLifecycleID{}) {
134 return errors.New("remote lifecycle ID is zero")
135 }
136 if err := ValidateSessionName(t.SessionName); err != nil {
137 return err
138 }
139 if t.Stopped {
140 if t.LiveTabID != "" || t.StoppedTab.Validate() != nil {
141 return errors.New("down remote target requires an ordinal or stable tab selector")
142 }
143 return nil
144 }
145 if err := ValidateTabStableID(t.LiveTabID); err != nil {
146 return errors.New("up remote target requires a stable tab ID")
147 }
148 if t.StoppedTab != (TabSelector{}) {
149 return errors.New("up remote target cannot carry a down selector")
150 }
151 return nil
152}
153
154// ValidateRemoteDisplayOrigin validates presentation data without imposing
155// SSH route grammar. Aliases, users, ports, and bracketed IPv6 remain opaque.
156func ValidateRemoteDisplayOrigin(origin string) error {
157 return validateRemoteText(origin, 256, "remote display origin")
158}
159
160// ValidateTabStableID validates the opaque stable tab identity used by
161// picker and attachment handoffs.
162func ValidateTabStableID(value TabStableID) error {
163 if value == "" || len(value) > 128 || !utf8.ValidString(string(value)) {
164 return errors.New("invalid stable tab ID")
165 }
166 for _, r := range value {
167 if unicode.IsControl(r) || unicode.IsSpace(r) {
168 return errors.New("invalid stable tab ID")
169 }
170 }
171 return nil
172}
173
174func validateTabSelectorName(value string) error {
175 if len(value) > 256 {
176 return errors.New("tab name exceeds 256 bytes")
177 }
178 if !utf8.ValidString(value) {
179 return errors.New("tab name is not valid UTF-8")
180 }
181 for _, r := range value {
182 if unicode.IsControl(r) || r == '\n' || r == '\r' || r == '\x1b' {
183 return errors.New("tab name contains control characters")
184 }
185 }
186 return nil
187}
188
189func validateRemoteText(value string, maxBytes int, label string) error {
190 if value == "" {
191 return fmt.Errorf("%s is empty", label)
192 }
193 if len(value) > maxBytes {
194 return fmt.Errorf("%s exceeds %d bytes", label, maxBytes)
195 }
196 if !utf8.ValidString(value) {
197 return fmt.Errorf("%s is not valid UTF-8", label)
198 }
199 if strings.TrimSpace(value) != value {
200 return fmt.Errorf("%s has surrounding whitespace", label)
201 }
202 for _, r := range value {
203 if unicode.IsControl(r) || r == '\n' || r == '\r' || r == '\x1b' {
204 return fmt.Errorf("%s contains control characters", label)
205 }
206 }
207 return nil
208}