1// Package protoconv holds the single canonical implementation of the
2// semantic protocol value <-> generated wire envelope converters shared by
3// internal/adapters/sessionwire and internal/adapters/brokerwire.
4//
5// Every narrowing numeric conversion is validated before the cast: values
6// whose high bits would otherwise alias a legitimate value are refused with
7// ErrOutOfRange before any truncation, so hostile wire input cannot alias a
8// valid identifier, enum, or geometry value. A nil wire message that carries
9// no optional-vs-absent distinction for its caller is refused the same way,
10// except where a field is documented below as tolerant of a nil submessage.
11//
12// protoconv returns exactly two error shapes: ErrOutOfRange for every
13// numeric/identity range or required-nil failure, and (for the
14// RemotePreviewRequest/RemotePreview converters) the protocol package's own
15// validation sentinels, returned unwrapped. Callers in sessionwire and
16// brokerwire map these to their own local sentinel errors, since the two
17// packages diverge on how strictly they distinguish range failures from
18// structural/semantic ones.
19package protoconv
20
21import (
22 "errors"
23 "math"
24
25 renderer "github.com/bnema/vev-vt"
26 "github.com/bnema/vev/internal/domain"
27 "github.com/bnema/vev/internal/protocol/wire"
28)
29
30// ErrOutOfRange is returned by every converter in this package when a wire
31// value cannot losslessly represent its semantic counterpart, or a wire
32// submessage required for a semantic value is nil.
33var ErrOutOfRange = errors.New("protoconv: wire value out of semantic range")
34
35// Uint8 narrows one wire uint32 into an 8-bit semantic value or enum. Values
36// whose high bits would otherwise truncate into a valid range are refused
37// before any cast.
38func Uint8[T ~uint8](v uint32) (T, error) {
39 if v > math.MaxUint8 {
40 return 0, ErrOutOfRange
41 }
42 return T(v), nil
43}
44
45// Uint16 narrows one wire uint32 into a 16-bit semantic value or enum.
46func Uint16[T ~uint16](v uint32) (T, error) {
47 if v > math.MaxUint16 {
48 return 0, ErrOutOfRange
49 }
50 return T(v), nil
51}
52
53// LifecycleToWire never fails: a semantic lifecycle ID is always a fixed
54// byte array.
55func LifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
56 value := append([]byte(nil), id[:]...)
57 return &wire.LifecycleID{Value: value}
58}
59
60// LifecycleFromWire requires an exact-length value: a nil message or a
61// wrong-length byte slice is refused rather than silently zero-padded or
62// truncated.
63func LifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
64 var id domain.SessionLifecycleID
65 if message == nil || len(message.GetValue()) != len(id) {
66 return id, ErrOutOfRange
67 }
68 copy(id[:], message.GetValue())
69 return id, nil
70}
71
72// TabSelectorToWire never fails.
73func TabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
74 return &wire.TabSelector{
75 Kind: uint32(selector.Kind),
76 StableId: string(selector.StableID),
77 Ordinal: uint32(selector.Ordinal),
78 RawName: selector.RawName,
79 ExpectedCount: uint32(selector.ExpectedCount),
80 }
81}
82
83// TabSelectorFromWire tolerates a nil message: an absent selector decodes
84// as the zero selector rather than an error, matching the optional stopped
85// tab it composes into a RemoteTarget.
86func TabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
87 var selector domain.TabSelector
88 if message == nil {
89 return selector, nil
90 }
91 kind, err := Uint8[domain.TabSelectorKind](message.GetKind())
92 if err != nil {
93 return domain.TabSelector{}, err
94 }
95 selector.Kind = kind
96 selector.StableID = domain.TabStableID(message.GetStableId())
97 ordinal, err := Uint16[uint16](message.GetOrdinal())
98 if err != nil {
99 return domain.TabSelector{}, err
100 }
101 selector.Ordinal = ordinal
102 selector.RawName = message.GetRawName()
103 expected, err := Uint16[uint16](message.GetExpectedCount())
104 if err != nil {
105 return domain.TabSelector{}, err
106 }
107 selector.ExpectedCount = expected
108 return selector, nil
109}
110
111// RemoteTargetToWire never fails.
112func RemoteTargetToWire(target domain.RemoteSessionTarget) *wire.RemoteTarget {
113 return &wire.RemoteTarget{
114 Endpoint: target.Endpoint,
115 DisplayOrigin: target.DisplayOrigin,
116 LifecycleId: LifecycleToWire(target.LifecycleID),
117 SessionName: target.SessionName,
118 LiveTabId: string(target.LiveTabID),
119 StoppedTab: TabSelectorToWire(target.StoppedTab),
120 Stopped: target.Stopped,
121 }
122}
123
124// RemoteTargetFromWire requires a non-nil message: unlike TabSelectorFromWire,
125// a remote target is never optional at this layer, so a nil submessage is a
126// range failure. Callers that carry an optional target (a pointer field)
127// check nil themselves before calling in.
128func RemoteTargetFromWire(message *wire.RemoteTarget) (domain.RemoteSessionTarget, error) {
129 var target domain.RemoteSessionTarget
130 if message == nil {
131 return target, ErrOutOfRange
132 }
133 lifecycle, err := LifecycleFromWire(message.GetLifecycleId())
134 if err != nil {
135 return domain.RemoteSessionTarget{}, err
136 }
137 stopped, err := TabSelectorFromWire(message.GetStoppedTab())
138 if err != nil {
139 return domain.RemoteSessionTarget{}, err
140 }
141 return domain.RemoteSessionTarget{
142 Endpoint: message.GetEndpoint(),
143 DisplayOrigin: message.GetDisplayOrigin(),
144 LifecycleID: lifecycle,
145 SessionName: message.GetSessionName(),
146 LiveTabID: domain.TabStableID(message.GetLiveTabId()),
147 StoppedTab: stopped,
148 Stopped: message.GetStopped(),
149 }, nil
150}
151
152// RGBToWire never fails.
153func RGBToWire(color renderer.RGB) *wire.RGB {
154 return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}
155}
156
157// RGBFromWire tolerates a nil message: an absent color decodes as the zero
158// RGB, matching an unset foreground/background/underline color.
159func RGBFromWire(message *wire.RGB) (renderer.RGB, error) {
160 var color renderer.RGB
161 if message == nil {
162 return color, nil
163 }
164 r, err := Uint8[uint8](message.GetR())
165 if err != nil {
166 return renderer.RGB{}, err
167 }
168 g, err := Uint8[uint8](message.GetG())
169 if err != nil {
170 return renderer.RGB{}, err
171 }
172 b, err := Uint8[uint8](message.GetB())
173 if err != nil {
174 return renderer.RGB{}, err
175 }
176 return renderer.RGB{R: r, G: g, B: b}, nil
177}
178
179// CellStyleToWire never fails.
180func CellStyleToWire(style renderer.Style) *wire.CellStyle {
181 return &wire.CellStyle{
182 Bold: style.Bold,
183 Italic: style.Italic,
184 Inverse: style.Inverse,
185 Attrs: uint32(style.Attrs),
186 Foreground: int32(style.Foreground),
187 Background: int32(style.Background),
188 HasForegroundRgb: style.HasForegroundRGB,
189 HasBackgroundRgb: style.HasBackgroundRGB,
190 HasUnderlineColor: style.HasUnderlineColor,
191 HasUnderlineColorRgb: style.HasUnderlineColorRGB,
192 UnderlineStyle: uint32(style.UnderlineStyle),
193 UnderlineColor: int32(style.UnderlineColor),
194 ForegroundRgb: RGBToWire(style.ForegroundRGB),
195 BackgroundRgb: RGBToWire(style.BackgroundRGB),
196 UnderlineColorRgb: RGBToWire(style.UnderlineColorRGB),
197 }
198}
199
200// CellStyleFromWire tolerates a nil message: an absent style decodes as the
201// zero style.
202func CellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
203 var style renderer.Style
204 if message == nil {
205 return style, nil
206 }
207 foreground := message.GetForeground()
208 if foreground < math.MinInt16 || foreground > math.MaxInt16 {
209 return renderer.Style{}, ErrOutOfRange
210 }
211 background := message.GetBackground()
212 if background < math.MinInt16 || background > math.MaxInt16 {
213 return renderer.Style{}, ErrOutOfRange
214 }
215 underline := message.GetUnderlineColor()
216 if underline < math.MinInt16 || underline > math.MaxInt16 {
217 return renderer.Style{}, ErrOutOfRange
218 }
219 underlineStyle, err := Uint8[uint8](message.GetUnderlineStyle())
220 if err != nil {
221 return renderer.Style{}, err
222 }
223 attrs := message.GetAttrs()
224 if attrs > math.MaxUint16 {
225 return renderer.Style{}, ErrOutOfRange
226 }
227 foregroundRGB, err := RGBFromWire(message.GetForegroundRgb())
228 if err != nil {
229 return renderer.Style{}, err
230 }
231 backgroundRGB, err := RGBFromWire(message.GetBackgroundRgb())
232 if err != nil {
233 return renderer.Style{}, err
234 }
235 underlineRGB, err := RGBFromWire(message.GetUnderlineColorRgb())
236 if err != nil {
237 return renderer.Style{}, err
238 }
239 return renderer.Style{
240 Bold: message.GetBold(),
241 Italic: message.GetItalic(),
242 Inverse: message.GetInverse(),
243 Attrs: renderer.StyleAttrs(attrs),
244 Foreground: int(foreground),
245 Background: int(background),
246 HasForegroundRGB: message.GetHasForegroundRgb(),
247 HasBackgroundRGB: message.GetHasBackgroundRgb(),
248 HasUnderlineColor: message.GetHasUnderlineColor(),
249 HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
250 UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
251 UnderlineColor: int(underline),
252 ForegroundRGB: foregroundRGB,
253 BackgroundRGB: backgroundRGB,
254 UnderlineColorRGB: underlineRGB,
255 }, nil
256}
257
258// PreviewCellToWire refuses a rune outside the valid Unicode code point
259// range before it can be emitted.
260func PreviewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
261 if cell.Rune < 0 || cell.Rune > 0x10FFFF {
262 return nil, ErrOutOfRange
263 }
264 return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: CellStyleToWire(cell.Style)}, nil
265}
266
267// PreviewCellFromWire requires a non-nil message: unlike CellStyleFromWire
268// and RGBFromWire, a single grid cell is never optional.
269func PreviewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
270 var cell renderer.Cell
271 if message == nil {
272 return cell, ErrOutOfRange
273 }
274 style, err := CellStyleFromWire(message.GetStyle())
275 if err != nil {
276 return renderer.Cell{}, err
277 }
278 runeValue := message.GetRuneValue()
279 if runeValue > 0x10FFFF {
280 return renderer.Cell{}, ErrOutOfRange
281 }
282 return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil
283}
284
285// PreviewCellsToWire preserves nil for an empty/absent slice rather than
286// allocating an empty one.
287func PreviewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
288 if len(cells) == 0 {
289 return nil, nil
290 }
291 out := make([]*wire.PreviewCell, 0, len(cells))
292 for _, cell := range cells {
293 converted, err := PreviewCellToWire(cell)
294 if err != nil {
295 return nil, err
296 }
297 out = append(out, converted)
298 }
299 return out, nil
300}
301
302// PreviewCellsFromWire preserves nil for an empty/absent slice.
303func PreviewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
304 if len(cells) == 0 {
305 return nil, nil
306 }
307 out := make([]renderer.Cell, 0, len(cells))
308 for _, cell := range cells {
309 converted, err := PreviewCellFromWire(cell)
310 if err != nil {
311 return nil, err
312 }
313 out = append(out, converted)
314 }
315 return out, nil
316}