detached 42155b1

refactor(wire): share preview and target converters in protoconv

Extract the duplicated lifecycle/tab-selector/remote-target/RGB/cell-style/
preview-cell and RemotePreviewRequest/RemotePreview converters out of
sessionwire and brokerwire into a new internal/adapters/protoconv package,
the single canonical implementation. sessionwire and brokerwire keep thin
wrappers that preserve their own local error sentinels and nil-tolerance
policy, so wire bytes and validation/error semantics are unchanged.
commit
42155b13912b7812492c41d57d4aa010b390920d
author
brice · · 2026-09-22 20:29 UTC
parent
ef46af7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +976 -583
M internal/adapters/brokerwire/preview.go +28 -322
@@ -17,367 +17,73 @@ package brokerwire
1717 // identity response; route/request dimensions are retained by client
1818 // subscription state and re-checked here through the ports preview
1919 // contract.
20+//
21+// The field-by-field RemotePreviewRequest/RemotePreview conversion logic
22+// lives in internal/adapters/protoconv, the canonical implementation shared
23+// with sessionwire. The wrappers below adapt protoconv's shared
24+// ErrOutOfRange to this package's errConvertRange, keeping range failures
25+// distinct from a semantic protocol validation failure (which maps to
26+// ErrInvalidMessage, or ErrTooLarge for an oversized preview) exactly as
27+// before extraction.
2028
2129 import (
2230 "errors"
23- "math"
2431
25- renderer "github.com/bnema/vev-vt"
32+ "github.com/bnema/vev/internal/adapters/protoconv"
2633 "github.com/bnema/vev/internal/domain"
2734 "github.com/bnema/vev/internal/ports"
2835 "github.com/bnema/vev/internal/protocol"
2936 "github.com/bnema/vev/internal/protocol/wire"
3037 )
3138
32-func previewLifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
33- value := append([]byte(nil), id[:]...)
34- return &wire.LifecycleID{Value: value}
35-}
36-
37-func previewLifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
38- var id domain.SessionLifecycleID
39- if message == nil || len(message.GetValue()) != len(id) {
40- return id, errConvertRange
41- }
42- copy(id[:], message.GetValue())
43- return id, nil
44-}
45-
46-func previewTabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
47- return &wire.TabSelector{
48- Kind: uint32(selector.Kind),
49- StableId: string(selector.StableID),
50- Ordinal: uint32(selector.Ordinal),
51- RawName: selector.RawName,
52- ExpectedCount: uint32(selector.ExpectedCount),
53- }
54-}
55-
56-func previewTabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
57- var selector domain.TabSelector
58- if message == nil {
59- return selector, nil
60- }
61- kind, err := brokerEnum8[domain.TabSelectorKind](message.GetKind())
62- if err != nil {
63- return domain.TabSelector{}, err
64- }
65- selector.Kind = kind
66- selector.StableID = domain.TabStableID(message.GetStableId())
67- ordinal, err := brokerEnum16[uint16](message.GetOrdinal())
68- if err != nil {
69- return domain.TabSelector{}, err
70- }
71- selector.Ordinal = ordinal
72- selector.RawName = message.GetRawName()
73- expected, err := brokerEnum16[uint16](message.GetExpectedCount())
74- if err != nil {
75- return domain.TabSelector{}, err
76- }
77- selector.ExpectedCount = expected
78- return selector, nil
79-}
80-
81-func previewRemoteTargetToWire(target domain.RemoteSessionTarget) (*wire.RemoteTarget, error) {
82- lifecycle := previewLifecycleToWire(target.LifecycleID)
83- return &wire.RemoteTarget{
84- Endpoint: target.Endpoint,
85- DisplayOrigin: target.DisplayOrigin,
86- LifecycleId: lifecycle,
87- SessionName: target.SessionName,
88- LiveTabId: string(target.LiveTabID),
89- StoppedTab: previewTabSelectorToWire(target.StoppedTab),
90- Stopped: target.Stopped,
91- }, nil
92-}
93-
94-func previewRemoteTargetFromWire(message *wire.RemoteTarget) (domain.RemoteSessionTarget, error) {
95- var target domain.RemoteSessionTarget
96- if message == nil {
97- return target, errConvertRange
98- }
99- lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
100- if err != nil {
101- return domain.RemoteSessionTarget{}, err
102- }
103- stopped, err := previewTabSelectorFromWire(message.GetStoppedTab())
104- if err != nil {
105- return domain.RemoteSessionTarget{}, err
106- }
107- return domain.RemoteSessionTarget{
108- Endpoint: message.GetEndpoint(),
109- DisplayOrigin: message.GetDisplayOrigin(),
110- LifecycleID: lifecycle,
111- SessionName: message.GetSessionName(),
112- LiveTabID: domain.TabStableID(message.GetLiveTabId()),
113- StoppedTab: stopped,
114- Stopped: message.GetStopped(),
115- }, nil
116-}
117-
118-func previewRGBToWire(color renderer.RGB) *wire.RGB {
119- return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}
120-}
121-
122-func previewRGBFromWire(message *wire.RGB) (renderer.RGB, error) {
123- var color renderer.RGB
124- if message == nil {
125- return color, nil
126- }
127- r, err := brokerEnum8[uint8](message.GetR())
128- if err != nil {
129- return renderer.RGB{}, err
130- }
131- g, err := brokerEnum8[uint8](message.GetG())
132- if err != nil {
133- return renderer.RGB{}, err
134- }
135- b, err := brokerEnum8[uint8](message.GetB())
136- if err != nil {
137- return renderer.RGB{}, err
138- }
139- return renderer.RGB{R: r, G: g, B: b}, nil
140-}
141-
142-func previewCellStyleToWire(style renderer.Style) *wire.CellStyle {
143- return &wire.CellStyle{
144- Bold: style.Bold,
145- Italic: style.Italic,
146- Inverse: style.Inverse,
147- Attrs: uint32(style.Attrs),
148- Foreground: int32(style.Foreground),
149- Background: int32(style.Background),
150- HasForegroundRgb: style.HasForegroundRGB,
151- HasBackgroundRgb: style.HasBackgroundRGB,
152- HasUnderlineColor: style.HasUnderlineColor,
153- HasUnderlineColorRgb: style.HasUnderlineColorRGB,
154- UnderlineStyle: uint32(style.UnderlineStyle),
155- UnderlineColor: int32(style.UnderlineColor),
156- ForegroundRgb: previewRGBToWire(style.ForegroundRGB),
157- BackgroundRgb: previewRGBToWire(style.BackgroundRGB),
158- UnderlineColorRgb: previewRGBToWire(style.UnderlineColorRGB),
159- }
160-}
161-
162-func previewCellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
163- var style renderer.Style
164- if message == nil {
165- return style, nil
166- }
167- foreground := message.GetForeground()
168- if foreground < math.MinInt16 || foreground > math.MaxInt16 {
169- return renderer.Style{}, errConvertRange
170- }
171- background := message.GetBackground()
172- if background < math.MinInt16 || background > math.MaxInt16 {
173- return renderer.Style{}, errConvertRange
174- }
175- underline := message.GetUnderlineColor()
176- if underline < math.MinInt16 || underline > math.MaxInt16 {
177- return renderer.Style{}, errConvertRange
178- }
179- underlineStyle, err := brokerEnum8[uint8](message.GetUnderlineStyle())
180- if err != nil {
181- return renderer.Style{}, err
182- }
183- attrs := message.GetAttrs()
184- if attrs > math.MaxUint16 {
185- return renderer.Style{}, errConvertRange
186- }
187- foregroundRGB, err := previewRGBFromWire(message.GetForegroundRgb())
188- if err != nil {
189- return renderer.Style{}, err
190- }
191- backgroundRGB, err := previewRGBFromWire(message.GetBackgroundRgb())
192- if err != nil {
193- return renderer.Style{}, err
194- }
195- underlineRGB, err := previewRGBFromWire(message.GetUnderlineColorRgb())
196- if err != nil {
197- return renderer.Style{}, err
198- }
199- return renderer.Style{
200- Bold: message.GetBold(),
201- Italic: message.GetItalic(),
202- Inverse: message.GetInverse(),
203- Attrs: renderer.StyleAttrs(attrs),
204- Foreground: int(foreground),
205- Background: int(background),
206- HasForegroundRGB: message.GetHasForegroundRgb(),
207- HasBackgroundRGB: message.GetHasBackgroundRgb(),
208- HasUnderlineColor: message.GetHasUnderlineColor(),
209- HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
210- UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
211- UnderlineColor: int(underline),
212- ForegroundRGB: foregroundRGB,
213- BackgroundRGB: backgroundRGB,
214- UnderlineColorRGB: underlineRGB,
215- }, nil
216-}
217-
218-func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
219- if cell.Rune < 0 || cell.Rune > 0x10FFFF {
220- return nil, errConvertRange
221- }
222- return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: previewCellStyleToWire(cell.Style)}, nil
223-}
224-
225-func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
226- var cell renderer.Cell
227- if message == nil {
228- return cell, errConvertRange
229- }
230- style, err := previewCellStyleFromWire(message.GetStyle())
231- if err != nil {
232- return renderer.Cell{}, err
233- }
234- runeValue := message.GetRuneValue()
235- if runeValue > 0x10FFFF {
236- return renderer.Cell{}, errConvertRange
237- }
238- return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil
239-}
240-
241-func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
242- if len(cells) == 0 {
243- return nil, nil
244- }
245- out := make([]*wire.PreviewCell, 0, len(cells))
246- for _, cell := range cells {
247- converted, err := previewCellToWire(cell)
248- if err != nil {
249- return nil, err
250- }
251- out = append(out, converted)
252- }
253- return out, nil
254-}
255-
256-func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
257- if len(cells) == 0 {
258- return nil, nil
259- }
260- out := make([]renderer.Cell, 0, len(cells))
261- for _, cell := range cells {
262- converted, err := previewCellFromWire(cell)
263- if err != nil {
264- return nil, err
265- }
266- out = append(out, converted)
267- }
268- return out, nil
269-}
270-
27139 func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
272- if err := protocol.ValidateRemotePreviewRequest(message); err != nil {
273- return nil, ErrInvalidMessage
274- }
275- target, err := previewRemoteTargetToWire(message.Target)
40+ out, err := protoconv.RemotePreviewRequestToWire(message)
27641 if err != nil {
277- return nil, err
42+ return nil, ErrInvalidMessage
27843 }
279- return &wire.RemotePreviewRequest{
280- Version: uint32(message.Version),
281- Target: target,
282- Width: uint32(message.Width),
283- Height: uint32(message.Height),
284- }, nil
44+ return out, nil
28545 }
28646
28747 func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
288- var request protocol.RemotePreviewRequest
28948 if message == nil {
290- return request, ErrInvalidMessage
291- }
292- version, err := brokerEnum16[uint16](message.GetVersion())
293- if err != nil {
294- return protocol.RemotePreviewRequest{}, errConvertRange
295- }
296- target, err := previewRemoteTargetFromWire(message.GetTarget())
297- if err != nil {
298- return protocol.RemotePreviewRequest{}, err
299- }
300- width, err := brokerEnum16[uint16](message.GetWidth())
301- if err != nil {
302- return protocol.RemotePreviewRequest{}, errConvertRange
49+ return protocol.RemotePreviewRequest{}, ErrInvalidMessage
30350 }
304- height, err := brokerEnum16[uint16](message.GetHeight())
51+ request, err := protoconv.RemotePreviewRequestFromWire(message)
30552 if err != nil {
306- return protocol.RemotePreviewRequest{}, errConvertRange
307- }
308- request = protocol.RemotePreviewRequest{Version: version, Target: target, Width: width, Height: height}
309- if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
53+ if errors.Is(err, protoconv.ErrOutOfRange) {
54+ return protocol.RemotePreviewRequest{}, errConvertRange
55+ }
31056 return protocol.RemotePreviewRequest{}, ErrInvalidMessage
31157 }
31258 return request, nil
31359 }
31460
31561 func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
316- if err := protocol.ValidateRemotePreview(message); err != nil {
62+ out, err := protoconv.RemotePreviewToWire(message)
63+ if err != nil {
31764 if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
31865 return nil, ErrTooLarge
31966 }
67+ if errors.Is(err, protoconv.ErrOutOfRange) {
68+ return nil, errConvertRange
69+ }
32070 return nil, ErrInvalidMessage
32171 }
322- cells, err := previewCellsToWire(message.Cells)
323- if err != nil {
324- return nil, err
325- }
326- return &wire.RemotePreview{
327- Version: uint32(message.Version),
328- Status: uint32(message.Status),
329- LifecycleId: previewLifecycleToWire(message.LifecycleID),
330- TabId: string(message.TabID),
331- Revision: message.Revision,
332- Width: uint32(message.Width),
333- Height: uint32(message.Height),
334- Cells: cells,
335- }, nil
72+ return out, nil
33673 }
33774
33875 func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
339- var preview protocol.RemotePreview
34076 if message == nil {
341- return preview, ErrInvalidMessage
342- }
343- version, err := brokerEnum16[uint16](message.GetVersion())
344- if err != nil {
345- return protocol.RemotePreview{}, errConvertRange
346- }
347- status, err := brokerEnum8[protocol.RemotePreviewStatus](message.GetStatus())
348- if err != nil {
349- return protocol.RemotePreview{}, errConvertRange
350- }
351- lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
352- if err != nil {
353- return protocol.RemotePreview{}, err
354- }
355- width, err := brokerEnum16[uint16](message.GetWidth())
356- if err != nil {
357- return protocol.RemotePreview{}, errConvertRange
358- }
359- height, err := brokerEnum16[uint16](message.GetHeight())
360- if err != nil {
361- return protocol.RemotePreview{}, errConvertRange
77+ return protocol.RemotePreview{}, ErrInvalidMessage
36278 }
363- cells, err := previewCellsFromWire(message.GetCells())
79+ preview, err := protoconv.RemotePreviewFromWire(message)
36480 if err != nil {
365- return protocol.RemotePreview{}, err
366- }
367- preview = protocol.RemotePreview{
368- Version: version,
369- Status: status,
370- LifecycleID: lifecycle,
371- TabID: domain.TabStableID(message.GetTabId()),
372- Revision: message.GetRevision(),
373- Width: width,
374- Height: height,
375- Cells: cells,
376- }
377- if err := protocol.ValidateRemotePreview(preview); err != nil {
37881 if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
37982 return protocol.RemotePreview{}, ErrTooLarge
38083 }
84+ if errors.Is(err, protoconv.ErrOutOfRange) {
85+ return protocol.RemotePreview{}, errConvertRange
86+ }
38187 return protocol.RemotePreview{}, ErrInvalidMessage
38288 }
38389 return preview, nil
@@ -17,367 +17,73 @@ package brokerwire
17 // identity response; route/request dimensions are retained by client17 // identity response; route/request dimensions are retained by client
18 // subscription state and re-checked here through the ports preview18 // subscription state and re-checked here through the ports preview
19 // contract.19 // contract.
20+//
21+// The field-by-field RemotePreviewRequest/RemotePreview conversion logic
22+// lives in internal/adapters/protoconv, the canonical implementation shared
23+// with sessionwire. The wrappers below adapt protoconv's shared
24+// ErrOutOfRange to this package's errConvertRange, keeping range failures
25+// distinct from a semantic protocol validation failure (which maps to
26+// ErrInvalidMessage, or ErrTooLarge for an oversized preview) exactly as
27+// before extraction.
20 28
21 import (29 import (
22 "errors"30 "errors"
23- "math"
24 31
25- renderer "github.com/bnema/vev-vt"32+ "github.com/bnema/vev/internal/adapters/protoconv"
26 "github.com/bnema/vev/internal/domain"33 "github.com/bnema/vev/internal/domain"
27 "github.com/bnema/vev/internal/ports"34 "github.com/bnema/vev/internal/ports"
28 "github.com/bnema/vev/internal/protocol"35 "github.com/bnema/vev/internal/protocol"
29 "github.com/bnema/vev/internal/protocol/wire"36 "github.com/bnema/vev/internal/protocol/wire"
30 )37 )
31 38
32-func previewLifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
33- value := append([]byte(nil), id[:]...)
34- return &wire.LifecycleID{Value: value}
35-}
36-
37-func previewLifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
38- var id domain.SessionLifecycleID
39- if message == nil || len(message.GetValue()) != len(id) {
40- return id, errConvertRange
41- }
42- copy(id[:], message.GetValue())
43- return id, nil
44-}
45-
46-func previewTabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
47- return &wire.TabSelector{
48- Kind: uint32(selector.Kind),
49- StableId: string(selector.StableID),
50- Ordinal: uint32(selector.Ordinal),
51- RawName: selector.RawName,
52- ExpectedCount: uint32(selector.ExpectedCount),
53- }
54-}
55-
56-func previewTabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
57- var selector domain.TabSelector
58- if message == nil {
59- return selector, nil
60- }
61- kind, err := brokerEnum8[domain.TabSelectorKind](message.GetKind())
62- if err != nil {
63- return domain.TabSelector{}, err
64- }
65- selector.Kind = kind
66- selector.StableID = domain.TabStableID(message.GetStableId())
67- ordinal, err := brokerEnum16[uint16](message.GetOrdinal())
68- if err != nil {
69- return domain.TabSelector{}, err
70- }
71- selector.Ordinal = ordinal
72- selector.RawName = message.GetRawName()
73- expected, err := brokerEnum16[uint16](message.GetExpectedCount())
74- if err != nil {
75- return domain.TabSelector{}, err
76- }
77- selector.ExpectedCount = expected
78- return selector, nil
79-}
80-
81-func previewRemoteTargetToWire(target domain.RemoteSessionTarget) (*wire.RemoteTarget, error) {
82- lifecycle := previewLifecycleToWire(target.LifecycleID)
83- return &wire.RemoteTarget{
84- Endpoint: target.Endpoint,
85- DisplayOrigin: target.DisplayOrigin,
86- LifecycleId: lifecycle,
87- SessionName: target.SessionName,
88- LiveTabId: string(target.LiveTabID),
89- StoppedTab: previewTabSelectorToWire(target.StoppedTab),
90- Stopped: target.Stopped,
91- }, nil
92-}
93-
94-func previewRemoteTargetFromWire(message *wire.RemoteTarget) (domain.RemoteSessionTarget, error) {
95- var target domain.RemoteSessionTarget
96- if message == nil {
97- return target, errConvertRange
98- }
99- lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
100- if err != nil {
101- return domain.RemoteSessionTarget{}, err
102- }
103- stopped, err := previewTabSelectorFromWire(message.GetStoppedTab())
104- if err != nil {
105- return domain.RemoteSessionTarget{}, err
106- }
107- return domain.RemoteSessionTarget{
108- Endpoint: message.GetEndpoint(),
109- DisplayOrigin: message.GetDisplayOrigin(),
110- LifecycleID: lifecycle,
111- SessionName: message.GetSessionName(),
112- LiveTabID: domain.TabStableID(message.GetLiveTabId()),
113- StoppedTab: stopped,
114- Stopped: message.GetStopped(),
115- }, nil
116-}
117-
118-func previewRGBToWire(color renderer.RGB) *wire.RGB {
119- return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}
120-}
121-
122-func previewRGBFromWire(message *wire.RGB) (renderer.RGB, error) {
123- var color renderer.RGB
124- if message == nil {
125- return color, nil
126- }
127- r, err := brokerEnum8[uint8](message.GetR())
128- if err != nil {
129- return renderer.RGB{}, err
130- }
131- g, err := brokerEnum8[uint8](message.GetG())
132- if err != nil {
133- return renderer.RGB{}, err
134- }
135- b, err := brokerEnum8[uint8](message.GetB())
136- if err != nil {
137- return renderer.RGB{}, err
138- }
139- return renderer.RGB{R: r, G: g, B: b}, nil
140-}
141-
142-func previewCellStyleToWire(style renderer.Style) *wire.CellStyle {
143- return &wire.CellStyle{
144- Bold: style.Bold,
145- Italic: style.Italic,
146- Inverse: style.Inverse,
147- Attrs: uint32(style.Attrs),
148- Foreground: int32(style.Foreground),
149- Background: int32(style.Background),
150- HasForegroundRgb: style.HasForegroundRGB,
151- HasBackgroundRgb: style.HasBackgroundRGB,
152- HasUnderlineColor: style.HasUnderlineColor,
153- HasUnderlineColorRgb: style.HasUnderlineColorRGB,
154- UnderlineStyle: uint32(style.UnderlineStyle),
155- UnderlineColor: int32(style.UnderlineColor),
156- ForegroundRgb: previewRGBToWire(style.ForegroundRGB),
157- BackgroundRgb: previewRGBToWire(style.BackgroundRGB),
158- UnderlineColorRgb: previewRGBToWire(style.UnderlineColorRGB),
159- }
160-}
161-
162-func previewCellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
163- var style renderer.Style
164- if message == nil {
165- return style, nil
166- }
167- foreground := message.GetForeground()
168- if foreground < math.MinInt16 || foreground > math.MaxInt16 {
169- return renderer.Style{}, errConvertRange
170- }
171- background := message.GetBackground()
172- if background < math.MinInt16 || background > math.MaxInt16 {
173- return renderer.Style{}, errConvertRange
174- }
175- underline := message.GetUnderlineColor()
176- if underline < math.MinInt16 || underline > math.MaxInt16 {
177- return renderer.Style{}, errConvertRange
178- }
179- underlineStyle, err := brokerEnum8[uint8](message.GetUnderlineStyle())
180- if err != nil {
181- return renderer.Style{}, err
182- }
183- attrs := message.GetAttrs()
184- if attrs > math.MaxUint16 {
185- return renderer.Style{}, errConvertRange
186- }
187- foregroundRGB, err := previewRGBFromWire(message.GetForegroundRgb())
188- if err != nil {
189- return renderer.Style{}, err
190- }
191- backgroundRGB, err := previewRGBFromWire(message.GetBackgroundRgb())
192- if err != nil {
193- return renderer.Style{}, err
194- }
195- underlineRGB, err := previewRGBFromWire(message.GetUnderlineColorRgb())
196- if err != nil {
197- return renderer.Style{}, err
198- }
199- return renderer.Style{
200- Bold: message.GetBold(),
201- Italic: message.GetItalic(),
202- Inverse: message.GetInverse(),
203- Attrs: renderer.StyleAttrs(attrs),
204- Foreground: int(foreground),
205- Background: int(background),
206- HasForegroundRGB: message.GetHasForegroundRgb(),
207- HasBackgroundRGB: message.GetHasBackgroundRgb(),
208- HasUnderlineColor: message.GetHasUnderlineColor(),
209- HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
210- UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
211- UnderlineColor: int(underline),
212- ForegroundRGB: foregroundRGB,
213- BackgroundRGB: backgroundRGB,
214- UnderlineColorRGB: underlineRGB,
215- }, nil
216-}
217-
218-func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
219- if cell.Rune < 0 || cell.Rune > 0x10FFFF {
220- return nil, errConvertRange
221- }
222- return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: previewCellStyleToWire(cell.Style)}, nil
223-}
224-
225-func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
226- var cell renderer.Cell
227- if message == nil {
228- return cell, errConvertRange
229- }
230- style, err := previewCellStyleFromWire(message.GetStyle())
231- if err != nil {
232- return renderer.Cell{}, err
233- }
234- runeValue := message.GetRuneValue()
235- if runeValue > 0x10FFFF {
236- return renderer.Cell{}, errConvertRange
237- }
238- return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil
239-}
240-
241-func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
242- if len(cells) == 0 {
243- return nil, nil
244- }
245- out := make([]*wire.PreviewCell, 0, len(cells))
246- for _, cell := range cells {
247- converted, err := previewCellToWire(cell)
248- if err != nil {
249- return nil, err
250- }
251- out = append(out, converted)
252- }
253- return out, nil
254-}
255-
256-func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
257- if len(cells) == 0 {
258- return nil, nil
259- }
260- out := make([]renderer.Cell, 0, len(cells))
261- for _, cell := range cells {
262- converted, err := previewCellFromWire(cell)
263- if err != nil {
264- return nil, err
265- }
266- out = append(out, converted)
267- }
268- return out, nil
269-}
270-
271 func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {39 func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
272- if err := protocol.ValidateRemotePreviewRequest(message); err != nil {40+ out, err := protoconv.RemotePreviewRequestToWire(message)
273- return nil, ErrInvalidMessage
274- }
275- target, err := previewRemoteTargetToWire(message.Target)
276 if err != nil {41 if err != nil {
277- return nil, err42+ return nil, ErrInvalidMessage
278 }43 }
279- return &wire.RemotePreviewRequest{44+ return out, nil
280- Version: uint32(message.Version),
281- Target: target,
282- Width: uint32(message.Width),
283- Height: uint32(message.Height),
284- }, nil
285 }45 }
286 46
287 func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {47 func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
288- var request protocol.RemotePreviewRequest
289 if message == nil {48 if message == nil {
290- return request, ErrInvalidMessage49+ return protocol.RemotePreviewRequest{}, ErrInvalidMessage
291- }
292- version, err := brokerEnum16[uint16](message.GetVersion())
293- if err != nil {
294- return protocol.RemotePreviewRequest{}, errConvertRange
295- }
296- target, err := previewRemoteTargetFromWire(message.GetTarget())
297- if err != nil {
298- return protocol.RemotePreviewRequest{}, err
299- }
300- width, err := brokerEnum16[uint16](message.GetWidth())
301- if err != nil {
302- return protocol.RemotePreviewRequest{}, errConvertRange
303 }50 }
304- height, err := brokerEnum16[uint16](message.GetHeight())51+ request, err := protoconv.RemotePreviewRequestFromWire(message)
305 if err != nil {52 if err != nil {
306- return protocol.RemotePreviewRequest{}, errConvertRange53+ if errors.Is(err, protoconv.ErrOutOfRange) {
307- }54+ return protocol.RemotePreviewRequest{}, errConvertRange
308- request = protocol.RemotePreviewRequest{Version: version, Target: target, Width: width, Height: height}55+ }
309- if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
310 return protocol.RemotePreviewRequest{}, ErrInvalidMessage56 return protocol.RemotePreviewRequest{}, ErrInvalidMessage
311 }57 }
312 return request, nil58 return request, nil
313 }59 }
314 60
315 func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {61 func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
316- if err := protocol.ValidateRemotePreview(message); err != nil {62+ out, err := protoconv.RemotePreviewToWire(message)
63+ if err != nil {
317 if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {64 if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
318 return nil, ErrTooLarge65 return nil, ErrTooLarge
319 }66 }
67+ if errors.Is(err, protoconv.ErrOutOfRange) {
68+ return nil, errConvertRange
69+ }
320 return nil, ErrInvalidMessage70 return nil, ErrInvalidMessage
321 }71 }
322- cells, err := previewCellsToWire(message.Cells)72+ return out, nil
323- if err != nil {
324- return nil, err
325- }
326- return &wire.RemotePreview{
327- Version: uint32(message.Version),
328- Status: uint32(message.Status),
329- LifecycleId: previewLifecycleToWire(message.LifecycleID),
330- TabId: string(message.TabID),
331- Revision: message.Revision,
332- Width: uint32(message.Width),
333- Height: uint32(message.Height),
334- Cells: cells,
335- }, nil
336 }73 }
337 74
338 func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {75 func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
339- var preview protocol.RemotePreview
340 if message == nil {76 if message == nil {
341- return preview, ErrInvalidMessage77+ return protocol.RemotePreview{}, ErrInvalidMessage
342- }
343- version, err := brokerEnum16[uint16](message.GetVersion())
344- if err != nil {
345- return protocol.RemotePreview{}, errConvertRange
346- }
347- status, err := brokerEnum8[protocol.RemotePreviewStatus](message.GetStatus())
348- if err != nil {
349- return protocol.RemotePreview{}, errConvertRange
350- }
351- lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
352- if err != nil {
353- return protocol.RemotePreview{}, err
354- }
355- width, err := brokerEnum16[uint16](message.GetWidth())
356- if err != nil {
357- return protocol.RemotePreview{}, errConvertRange
358- }
359- height, err := brokerEnum16[uint16](message.GetHeight())
360- if err != nil {
361- return protocol.RemotePreview{}, errConvertRange
362 }78 }
363- cells, err := previewCellsFromWire(message.GetCells())79+ preview, err := protoconv.RemotePreviewFromWire(message)
364 if err != nil {80 if err != nil {
365- return protocol.RemotePreview{}, err
366- }
367- preview = protocol.RemotePreview{
368- Version: version,
369- Status: status,
370- LifecycleID: lifecycle,
371- TabID: domain.TabStableID(message.GetTabId()),
372- Revision: message.GetRevision(),
373- Width: width,
374- Height: height,
375- Cells: cells,
376- }
377- if err := protocol.ValidateRemotePreview(preview); err != nil {
378 if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {81 if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
379 return protocol.RemotePreview{}, ErrTooLarge82 return protocol.RemotePreview{}, ErrTooLarge
380 }83 }
84+ if errors.Is(err, protoconv.ErrOutOfRange) {
85+ return protocol.RemotePreview{}, errConvertRange
86+ }
381 return protocol.RemotePreview{}, ErrInvalidMessage87 return protocol.RemotePreview{}, ErrInvalidMessage
382 }88 }
383 return preview, nil89 return preview, nil
A internal/adapters/protoconv/preview.go +135 -0
@@ -0,0 +1,135 @@
1+package protoconv
2+
3+// RemotePreviewRequest/RemotePreview converters (shared by sessionwire and
4+// brokerwire).
5+//
6+// On failure these return either ErrOutOfRange (from a narrowing numeric or
7+// identity conversion) or one of protocol's own RemotePreview* validation
8+// sentinels, unwrapped. The two callers diverge on how they collapse these
9+// into their own local sentinel errors, so this package never wraps or
10+// renames them: sessionwire always folds every failure into a single fixed
11+// request/preview sentinel, while brokerwire keeps range failures distinct
12+// from semantic validation failures (and maps ErrRemotePreviewTooLarge to
13+// its own bounded-size sentinel). See the callers' wrapper functions for the
14+// exact mapping.
15+
16+import (
17+ "github.com/bnema/vev/internal/domain"
18+ "github.com/bnema/vev/internal/protocol"
19+ "github.com/bnema/vev/internal/protocol/wire"
20+)
21+
22+// RemotePreviewRequestToWire validates before encoding: an invalid request
23+// is refused with protocol's own sentinel rather than emitted.
24+func RemotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
25+ if err := protocol.ValidateRemotePreviewRequest(message); err != nil {
26+ return nil, err
27+ }
28+ return &wire.RemotePreviewRequest{
29+ Version: uint32(message.Version),
30+ Target: RemoteTargetToWire(message.Target),
31+ Width: uint32(message.Width),
32+ Height: uint32(message.Height),
33+ }, nil
34+}
35+
36+// RemotePreviewRequestFromWire requires a non-nil message and re-validates
37+// the fully decoded request, so decode refuses exactly what encode refuses.
38+func RemotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
39+ var request protocol.RemotePreviewRequest
40+ if message == nil {
41+ return request, ErrOutOfRange
42+ }
43+ version, err := Uint16[uint16](message.GetVersion())
44+ if err != nil {
45+ return protocol.RemotePreviewRequest{}, err
46+ }
47+ request.Version = version
48+ target, err := RemoteTargetFromWire(message.GetTarget())
49+ if err != nil {
50+ return protocol.RemotePreviewRequest{}, err
51+ }
52+ request.Target = target
53+ width, err := Uint16[uint16](message.GetWidth())
54+ if err != nil {
55+ return protocol.RemotePreviewRequest{}, err
56+ }
57+ request.Width = width
58+ height, err := Uint16[uint16](message.GetHeight())
59+ if err != nil {
60+ return protocol.RemotePreviewRequest{}, err
61+ }
62+ request.Height = height
63+ if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
64+ return protocol.RemotePreviewRequest{}, err
65+ }
66+ return request, nil
67+}
68+
69+// RemotePreviewToWire validates before encoding. A failure can be either
70+// protocol.ErrInvalidRemotePreview or protocol.ErrRemotePreviewTooLarge;
71+// callers distinguish the two.
72+func RemotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
73+ if err := protocol.ValidateRemotePreview(message); err != nil {
74+ return nil, err
75+ }
76+ cells, err := PreviewCellsToWire(message.Cells)
77+ if err != nil {
78+ return nil, err
79+ }
80+ return &wire.RemotePreview{
81+ Version: uint32(message.Version),
82+ Status: uint32(message.Status),
83+ LifecycleId: LifecycleToWire(message.LifecycleID),
84+ TabId: string(message.TabID),
85+ Revision: message.Revision,
86+ Width: uint32(message.Width),
87+ Height: uint32(message.Height),
88+ Cells: cells,
89+ }, nil
90+}
91+
92+// RemotePreviewFromWire requires a non-nil message and re-validates the
93+// fully decoded preview.
94+func RemotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
95+ var preview protocol.RemotePreview
96+ if message == nil {
97+ return preview, ErrOutOfRange
98+ }
99+ version, err := Uint16[uint16](message.GetVersion())
100+ if err != nil {
101+ return protocol.RemotePreview{}, err
102+ }
103+ preview.Version = version
104+ status, err := Uint8[protocol.RemotePreviewStatus](message.GetStatus())
105+ if err != nil {
106+ return protocol.RemotePreview{}, err
107+ }
108+ preview.Status = status
109+ lifecycle, err := LifecycleFromWire(message.GetLifecycleId())
110+ if err != nil {
111+ return protocol.RemotePreview{}, err
112+ }
113+ preview.LifecycleID = lifecycle
114+ preview.TabID = domain.TabStableID(message.GetTabId())
115+ preview.Revision = message.GetRevision()
116+ width, err := Uint16[uint16](message.GetWidth())
117+ if err != nil {
118+ return protocol.RemotePreview{}, err
119+ }
120+ preview.Width = width
121+ height, err := Uint16[uint16](message.GetHeight())
122+ if err != nil {
123+ return protocol.RemotePreview{}, err
124+ }
125+ preview.Height = height
126+ cells, err := PreviewCellsFromWire(message.GetCells())
127+ if err != nil {
128+ return protocol.RemotePreview{}, err
129+ }
130+ preview.Cells = cells
131+ if err := protocol.ValidateRemotePreview(preview); err != nil {
132+ return protocol.RemotePreview{}, err
133+ }
134+ return preview, nil
135+}
@@ -0,0 +1,135 @@
1+package protoconv
2+
3+// RemotePreviewRequest/RemotePreview converters (shared by sessionwire and
4+// brokerwire).
5+//
6+// On failure these return either ErrOutOfRange (from a narrowing numeric or
7+// identity conversion) or one of protocol's own RemotePreview* validation
8+// sentinels, unwrapped. The two callers diverge on how they collapse these
9+// into their own local sentinel errors, so this package never wraps or
10+// renames them: sessionwire always folds every failure into a single fixed
11+// request/preview sentinel, while brokerwire keeps range failures distinct
12+// from semantic validation failures (and maps ErrRemotePreviewTooLarge to
13+// its own bounded-size sentinel). See the callers' wrapper functions for the
14+// exact mapping.
15+
16+import (
17+ "github.com/bnema/vev/internal/domain"
18+ "github.com/bnema/vev/internal/protocol"
19+ "github.com/bnema/vev/internal/protocol/wire"
20+)
21+
22+// RemotePreviewRequestToWire validates before encoding: an invalid request
23+// is refused with protocol's own sentinel rather than emitted.
24+func RemotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
25+ if err := protocol.ValidateRemotePreviewRequest(message); err != nil {
26+ return nil, err
27+ }
28+ return &wire.RemotePreviewRequest{
29+ Version: uint32(message.Version),
30+ Target: RemoteTargetToWire(message.Target),
31+ Width: uint32(message.Width),
32+ Height: uint32(message.Height),
33+ }, nil
34+}
35+
36+// RemotePreviewRequestFromWire requires a non-nil message and re-validates
37+// the fully decoded request, so decode refuses exactly what encode refuses.
38+func RemotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
39+ var request protocol.RemotePreviewRequest
40+ if message == nil {
41+ return request, ErrOutOfRange
42+ }
43+ version, err := Uint16[uint16](message.GetVersion())
44+ if err != nil {
45+ return protocol.RemotePreviewRequest{}, err
46+ }
47+ request.Version = version
48+ target, err := RemoteTargetFromWire(message.GetTarget())
49+ if err != nil {
50+ return protocol.RemotePreviewRequest{}, err
51+ }
52+ request.Target = target
53+ width, err := Uint16[uint16](message.GetWidth())
54+ if err != nil {
55+ return protocol.RemotePreviewRequest{}, err
56+ }
57+ request.Width = width
58+ height, err := Uint16[uint16](message.GetHeight())
59+ if err != nil {
60+ return protocol.RemotePreviewRequest{}, err
61+ }
62+ request.Height = height
63+ if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
64+ return protocol.RemotePreviewRequest{}, err
65+ }
66+ return request, nil
67+}
68+
69+// RemotePreviewToWire validates before encoding. A failure can be either
70+// protocol.ErrInvalidRemotePreview or protocol.ErrRemotePreviewTooLarge;
71+// callers distinguish the two.
72+func RemotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
73+ if err := protocol.ValidateRemotePreview(message); err != nil {
74+ return nil, err
75+ }
76+ cells, err := PreviewCellsToWire(message.Cells)
77+ if err != nil {
78+ return nil, err
79+ }
80+ return &wire.RemotePreview{
81+ Version: uint32(message.Version),
82+ Status: uint32(message.Status),
83+ LifecycleId: LifecycleToWire(message.LifecycleID),
84+ TabId: string(message.TabID),
85+ Revision: message.Revision,
86+ Width: uint32(message.Width),
87+ Height: uint32(message.Height),
88+ Cells: cells,
89+ }, nil
90+}
91+
92+// RemotePreviewFromWire requires a non-nil message and re-validates the
93+// fully decoded preview.
94+func RemotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
95+ var preview protocol.RemotePreview
96+ if message == nil {
97+ return preview, ErrOutOfRange
98+ }
99+ version, err := Uint16[uint16](message.GetVersion())
100+ if err != nil {
101+ return protocol.RemotePreview{}, err
102+ }
103+ preview.Version = version
104+ status, err := Uint8[protocol.RemotePreviewStatus](message.GetStatus())
105+ if err != nil {
106+ return protocol.RemotePreview{}, err
107+ }
108+ preview.Status = status
109+ lifecycle, err := LifecycleFromWire(message.GetLifecycleId())
110+ if err != nil {
111+ return protocol.RemotePreview{}, err
112+ }
113+ preview.LifecycleID = lifecycle
114+ preview.TabID = domain.TabStableID(message.GetTabId())
115+ preview.Revision = message.GetRevision()
116+ width, err := Uint16[uint16](message.GetWidth())
117+ if err != nil {
118+ return protocol.RemotePreview{}, err
119+ }
120+ preview.Width = width
121+ height, err := Uint16[uint16](message.GetHeight())
122+ if err != nil {
123+ return protocol.RemotePreview{}, err
124+ }
125+ preview.Height = height
126+ cells, err := PreviewCellsFromWire(message.GetCells())
127+ if err != nil {
128+ return protocol.RemotePreview{}, err
129+ }
130+ preview.Cells = cells
131+ if err := protocol.ValidateRemotePreview(preview); err != nil {
132+ return protocol.RemotePreview{}, err
133+ }
134+ return preview, nil
135+}
A internal/adapters/protoconv/protoconv.go +316 -0
@@ -0,0 +1,316 @@
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.
19+package protoconv
20+
21+import (
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.
33+var 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.
38+func 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.
46+func 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.
55+func 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.
63+func 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.
73+func 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.
86+func 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.
112+func 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.
128+func 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.
153+func 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.
159+func 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.
180+func 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.
202+func 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.
260+func 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.
269+func 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.
287+func 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.
303+func 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+}
@@ -0,0 +1,316 @@
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.
19+package protoconv
20+
21+import (
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.
33+var 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.
38+func 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.
46+func 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.
55+func 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.
63+func 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.
73+func 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.
86+func 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.
112+func 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.
128+func 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.
153+func 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.
159+func 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.
180+func 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.
202+func 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.
260+func 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.
269+func 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.
287+func 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.
303+func 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+}
A internal/adapters/protoconv/protoconv_test.go +429 -0
@@ -0,0 +1,429 @@
1+package protoconv
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ renderer "github.com/bnema/vev-vt"
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/wire"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+// TestRangeHelpers proves Uint8/Uint16 accept every in-range value and
15+// refuse every value whose high bits would otherwise truncate into an
16+// aliased in-range value.
17+func TestRangeHelpers(t *testing.T) {
18+ t.Run("Uint8", func(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ in uint32
22+ want uint8
23+ wantErr bool
24+ }{
25+ {"zero", 0, 0, false},
26+ {"max", 255, 255, false},
27+ {"overflow by one", 256, 0, true},
28+ {"far overflow", 1 << 20, 0, true},
29+ }
30+ for _, tt := range tests {
31+ t.Run(tt.name, func(t *testing.T) {
32+ got, err := Uint8[uint8](tt.in)
33+ if tt.wantErr {
34+ require.ErrorIs(t, err, ErrOutOfRange)
35+ return
36+ }
37+ require.NoError(t, err)
38+ require.Equal(t, tt.want, got)
39+ })
40+ }
41+ })
42+
43+ t.Run("Uint16", func(t *testing.T) {
44+ tests := []struct {
45+ name string
46+ in uint32
47+ want uint16
48+ wantErr bool
49+ }{
50+ {"zero", 0, 0, false},
51+ {"max", 65535, 65535, false},
52+ {"overflow by one", 65536, 0, true},
53+ {"aliasing overflow", 1<<16 + 1, 0, true},
54+ }
55+ for _, tt := range tests {
56+ t.Run(tt.name, func(t *testing.T) {
57+ got, err := Uint16[uint16](tt.in)
58+ if tt.wantErr {
59+ require.ErrorIs(t, err, ErrOutOfRange)
60+ return
61+ }
62+ require.NoError(t, err)
63+ require.Equal(t, tt.want, got)
64+ })
65+ }
66+ })
67+
68+ t.Run("Uint8 instantiated over a named enum", func(t *testing.T) {
69+ got, err := Uint8[domain.TabSelectorKind](uint32(domain.TabSelectorByStableID))
70+ require.NoError(t, err)
71+ require.Equal(t, domain.TabSelectorByStableID, got)
72+ _, err = Uint8[domain.TabSelectorKind](256)
73+ require.ErrorIs(t, err, ErrOutOfRange)
74+ })
75+}
76+
77+// TestLifecycleRoundTrip proves lifecycle IDs survive an encode/decode
78+// cycle byte for byte and that decode refuses a nil message or a
79+// wrong-length value.
80+func TestLifecycleRoundTrip(t *testing.T) {
81+ id := domain.SessionLifecycleID{1, 2, 3, 4}
82+ wireID := LifecycleToWire(id)
83+ require.Equal(t, id[:], wireID.GetValue())
84+
85+ decoded, err := LifecycleFromWire(wireID)
86+ require.NoError(t, err)
87+ require.Equal(t, id, decoded)
88+
89+ t.Run("nil message refused", func(t *testing.T) {
90+ _, err := LifecycleFromWire(nil)
91+ require.ErrorIs(t, err, ErrOutOfRange)
92+ })
93+
94+ t.Run("wrong length refused", func(t *testing.T) {
95+ _, err := LifecycleFromWire(&wire.LifecycleID{Value: []byte{1, 2, 3}})
96+ require.ErrorIs(t, err, ErrOutOfRange)
97+ })
98+}
99+
100+// TestTabSelectorRoundTrip proves a tab selector survives an encode/decode
101+// cycle, that a nil message decodes as the zero selector (it composes into
102+// an optional stopped-tab field), and that every narrowing field is
103+// range-checked.
104+func TestTabSelectorRoundTrip(t *testing.T) {
105+ selector := domain.TabSelector{
106+ Kind: domain.TabSelectorByStableID, StableID: "tab-1",
107+ Ordinal: 3, RawName: "work", ExpectedCount: 4,
108+ }
109+ wireSelector := TabSelectorToWire(selector)
110+ decoded, err := TabSelectorFromWire(wireSelector)
111+ require.NoError(t, err)
112+ require.Equal(t, selector, decoded)
113+
114+ t.Run("nil message tolerated", func(t *testing.T) {
115+ decoded, err := TabSelectorFromWire(nil)
116+ require.NoError(t, err)
117+ require.Equal(t, domain.TabSelector{}, decoded)
118+ })
119+
120+ tests := []struct {
121+ name string
122+ msg *wire.TabSelector
123+ }{
124+ {"kind out of range", &wire.TabSelector{Kind: 256}},
125+ {"ordinal out of range", &wire.TabSelector{Ordinal: 1 << 16}},
126+ {"expected count out of range", &wire.TabSelector{ExpectedCount: 1 << 16}},
127+ }
128+ for _, tt := range tests {
129+ t.Run(tt.name, func(t *testing.T) {
130+ _, err := TabSelectorFromWire(tt.msg)
131+ require.ErrorIs(t, err, ErrOutOfRange)
132+ })
133+ }
134+}
135+
136+func testRemoteTarget() domain.RemoteSessionTarget {
137+ return domain.RemoteSessionTarget{
138+ Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
139+ LifecycleID: domain.SessionLifecycleID{5, 6, 7},
140+ SessionName: "work", LiveTabID: "tab-1",
141+ }
142+}
143+
144+// TestRemoteTargetRoundTrip proves a remote target survives an
145+// encode/decode cycle and that decode refuses a nil message and every
146+// nested narrowing failure.
147+func TestRemoteTargetRoundTrip(t *testing.T) {
148+ target := testRemoteTarget()
149+ wireTarget := RemoteTargetToWire(target)
150+ decoded, err := RemoteTargetFromWire(wireTarget)
151+ require.NoError(t, err)
152+ require.Equal(t, target, decoded)
153+
154+ t.Run("nil message refused", func(t *testing.T) {
155+ _, err := RemoteTargetFromWire(nil)
156+ require.ErrorIs(t, err, ErrOutOfRange)
157+ })
158+
159+ t.Run("invalid lifecycle refused", func(t *testing.T) {
160+ broken := RemoteTargetToWire(target)
161+ broken.LifecycleId = &wire.LifecycleID{Value: []byte{1}}
162+ _, err := RemoteTargetFromWire(broken)
163+ require.ErrorIs(t, err, ErrOutOfRange)
164+ })
165+
166+ t.Run("invalid stopped tab refused", func(t *testing.T) {
167+ broken := RemoteTargetToWire(target)
168+ broken.StoppedTab.Kind = 256
169+ _, err := RemoteTargetFromWire(broken)
170+ require.ErrorIs(t, err, ErrOutOfRange)
171+ })
172+}
173+
174+// TestRGBRoundTrip proves an RGB color survives an encode/decode cycle,
175+// that a nil message decodes as the zero color, and every component is
176+// range-checked.
177+func TestRGBRoundTrip(t *testing.T) {
178+ color := renderer.RGB{R: 10, G: 20, B: 30}
179+ wireColor := RGBToWire(color)
180+ decoded, err := RGBFromWire(wireColor)
181+ require.NoError(t, err)
182+ require.Equal(t, color, decoded)
183+
184+ t.Run("nil message tolerated", func(t *testing.T) {
185+ decoded, err := RGBFromWire(nil)
186+ require.NoError(t, err)
187+ require.Equal(t, renderer.RGB{}, decoded)
188+ })
189+
190+ tests := []struct {
191+ name string
192+ msg *wire.RGB
193+ }{
194+ {"red out of range", &wire.RGB{R: 256}},
195+ {"green out of range", &wire.RGB{G: 256}},
196+ {"blue out of range", &wire.RGB{B: 256}},
197+ }
198+ for _, tt := range tests {
199+ t.Run(tt.name, func(t *testing.T) {
200+ _, err := RGBFromWire(tt.msg)
201+ require.ErrorIs(t, err, ErrOutOfRange)
202+ })
203+ }
204+}
205+
206+func testStyle() renderer.Style {
207+ return renderer.Style{
208+ Bold: true, Italic: true, Attrs: renderer.AttrDim,
209+ Foreground: 1, Background: 2,
210+ UnderlineStyle: renderer.UnderlineSingle, UnderlineColor: 3, HasUnderlineColor: true,
211+ ForegroundRGB: renderer.RGB{R: 1}, BackgroundRGB: renderer.RGB{G: 1}, UnderlineColorRGB: renderer.RGB{B: 1},
212+ }
213+}
214+
215+// TestCellStyleRoundTrip proves a cell style survives an encode/decode
216+// cycle, that a nil message decodes as the zero style, and every narrowing
217+// field is range-checked.
218+func TestCellStyleRoundTrip(t *testing.T) {
219+ style := testStyle()
220+ wireStyle := CellStyleToWire(style)
221+ decoded, err := CellStyleFromWire(wireStyle)
222+ require.NoError(t, err)
223+ require.Equal(t, style, decoded)
224+
225+ t.Run("nil message tolerated", func(t *testing.T) {
226+ decoded, err := CellStyleFromWire(nil)
227+ require.NoError(t, err)
228+ require.Equal(t, renderer.Style{}, decoded)
229+ })
230+
231+ tests := []struct {
232+ name string
233+ msg *wire.CellStyle
234+ }{
235+ {"foreground out of range", &wire.CellStyle{Foreground: 1 << 20}},
236+ {"background out of range", &wire.CellStyle{Background: -(1 << 20)}},
237+ {"underline color out of range", &wire.CellStyle{UnderlineColor: 1 << 20}},
238+ {"underline style out of range", &wire.CellStyle{UnderlineStyle: 256}},
239+ {"attrs out of range", &wire.CellStyle{Attrs: 1 << 16}},
240+ {"foreground rgb out of range", &wire.CellStyle{ForegroundRgb: &wire.RGB{R: 256}}},
241+ }
242+ for _, tt := range tests {
243+ t.Run(tt.name, func(t *testing.T) {
244+ _, err := CellStyleFromWire(tt.msg)
245+ require.ErrorIs(t, err, ErrOutOfRange)
246+ })
247+ }
248+}
249+
250+// TestPreviewCellRoundTrip proves a cell survives an encode/decode cycle,
251+// that decode refuses a nil message (unlike RGB/CellStyle, a single grid
252+// cell is never optional), and that an out-of-range rune is refused in
253+// both directions.
254+func TestPreviewCellRoundTrip(t *testing.T) {
255+ cell := renderer.Cell{Rune: 'A', Style: testStyle()}
256+ wireCell, err := PreviewCellToWire(cell)
257+ require.NoError(t, err)
258+ decoded, err := PreviewCellFromWire(wireCell)
259+ require.NoError(t, err)
260+ require.Equal(t, cell, decoded)
261+
262+ t.Run("nil message refused", func(t *testing.T) {
263+ _, err := PreviewCellFromWire(nil)
264+ require.ErrorIs(t, err, ErrOutOfRange)
265+ })
266+
267+ t.Run("encode rejects rune above max code point", func(t *testing.T) {
268+ _, err := PreviewCellToWire(renderer.Cell{Rune: 0x110000})
269+ require.ErrorIs(t, err, ErrOutOfRange)
270+ })
271+
272+ t.Run("encode rejects negative rune", func(t *testing.T) {
273+ _, err := PreviewCellToWire(renderer.Cell{Rune: -1})
274+ require.ErrorIs(t, err, ErrOutOfRange)
275+ })
276+
277+ t.Run("decode rejects rune above max code point", func(t *testing.T) {
278+ _, err := PreviewCellFromWire(&wire.PreviewCell{RuneValue: 0x110000})
279+ require.ErrorIs(t, err, ErrOutOfRange)
280+ })
281+}
282+
283+// TestPreviewCellsRoundTrip proves a slice of cells survives an
284+// encode/decode cycle, that an empty/nil slice stays nil rather than an
285+// empty allocation, and that one bad cell fails the whole slice.
286+func TestPreviewCellsRoundTrip(t *testing.T) {
287+ cells := []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}}
288+ wireCells, err := PreviewCellsToWire(cells)
289+ require.NoError(t, err)
290+ decoded, err := PreviewCellsFromWire(wireCells)
291+ require.NoError(t, err)
292+ require.Equal(t, cells, decoded)
293+
294+ t.Run("nil in, nil out", func(t *testing.T) {
295+ out, err := PreviewCellsToWire(nil)
296+ require.NoError(t, err)
297+ require.Nil(t, out)
298+ decoded, err := PreviewCellsFromWire(nil)
299+ require.NoError(t, err)
300+ require.Nil(t, decoded)
301+ })
302+
303+ t.Run("bad cell fails the slice on encode", func(t *testing.T) {
304+ _, err := PreviewCellsToWire([]renderer.Cell{{Rune: 'a'}, {Rune: 0x110000}})
305+ require.ErrorIs(t, err, ErrOutOfRange)
306+ })
307+
308+ t.Run("nil member fails the slice on decode", func(t *testing.T) {
309+ _, err := PreviewCellsFromWire([]*wire.PreviewCell{{RuneValue: 'a'}, nil})
310+ require.ErrorIs(t, err, ErrOutOfRange)
311+ })
312+}
313+
314+func testPreviewRequest() protocol.RemotePreviewRequest {
315+ return protocol.RemotePreviewRequest{
316+ Version: protocol.RemotePreviewSchemaVersion,
317+ Target: testRemoteTarget(),
318+ Width: 2, Height: 1,
319+ }
320+}
321+
322+// TestRemotePreviewRequestRoundTrip proves a remote preview request
323+// survives an encode/decode cycle byte-for-byte (via re-marshal), that
324+// decode refuses a nil message, and that both an invalid semantic value and
325+// an out-of-range wire field are refused with the right error shape.
326+func TestRemotePreviewRequestRoundTrip(t *testing.T) {
327+ request := testPreviewRequest()
328+ wireRequest, err := RemotePreviewRequestToWire(request)
329+ require.NoError(t, err)
330+ decoded, err := RemotePreviewRequestFromWire(wireRequest)
331+ require.NoError(t, err)
332+ require.Equal(t, request, decoded)
333+
334+ t.Run("nil message refused", func(t *testing.T) {
335+ _, err := RemotePreviewRequestFromWire(nil)
336+ require.ErrorIs(t, err, ErrOutOfRange)
337+ })
338+
339+ t.Run("encode refuses invalid semantic value", func(t *testing.T) {
340+ invalid := request
341+ invalid.Width = 0
342+ _, err := RemotePreviewRequestToWire(invalid)
343+ require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
344+ })
345+
346+ t.Run("decode refuses out-of-range wire field", func(t *testing.T) {
347+ broken, err := RemotePreviewRequestToWire(request)
348+ require.NoError(t, err)
349+ broken.Width = 1 << 16
350+ _, err = RemotePreviewRequestFromWire(broken)
351+ require.ErrorIs(t, err, ErrOutOfRange)
352+ })
353+
354+ t.Run("decode re-validates the fully decoded request", func(t *testing.T) {
355+ broken, err := RemotePreviewRequestToWire(request)
356+ require.NoError(t, err)
357+ broken.Width = 0
358+ _, err = RemotePreviewRequestFromWire(broken)
359+ require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
360+ })
361+}
362+
363+func testRemotePreview() protocol.RemotePreview {
364+ target := testRemoteTarget()
365+ return protocol.RemotePreview{
366+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
367+ LifecycleID: target.LifecycleID, TabID: target.LiveTabID,
368+ Revision: 11, Width: 2, Height: 1,
369+ Cells: []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}},
370+ }
371+}
372+
373+// TestRemotePreviewRoundTrip proves a remote preview survives an
374+// encode/decode cycle, that decode refuses a nil message, and that a
375+// too-large preview is refused with protocol.ErrRemotePreviewTooLarge on
376+// both directions distinctly from an out-of-range wire field.
377+func TestRemotePreviewRoundTrip(t *testing.T) {
378+ preview := testRemotePreview()
379+ wirePreview, err := RemotePreviewToWire(preview)
380+ require.NoError(t, err)
381+ decoded, err := RemotePreviewFromWire(wirePreview)
382+ require.NoError(t, err)
383+ require.Equal(t, preview, decoded)
384+
385+ t.Run("nil message refused", func(t *testing.T) {
386+ _, err := RemotePreviewFromWire(nil)
387+ require.ErrorIs(t, err, ErrOutOfRange)
388+ })
389+
390+ t.Run("encode refuses a too-large preview", func(t *testing.T) {
391+ invalid := preview
392+ invalid.Cells = invalid.Cells[:1]
393+ _, err := RemotePreviewToWire(invalid)
394+ require.ErrorIs(t, err, protocol.ErrRemotePreviewTooLarge)
395+ })
396+
397+ t.Run("decode refuses out-of-range wire field", func(t *testing.T) {
398+ broken, err := RemotePreviewToWire(preview)
399+ require.NoError(t, err)
400+ broken.Status = 1 << 16
401+ _, err = RemotePreviewFromWire(broken)
402+ require.ErrorIs(t, err, ErrOutOfRange)
403+ })
404+
405+ t.Run("decode refuses a too-large preview", func(t *testing.T) {
406+ broken, err := RemotePreviewToWire(preview)
407+ require.NoError(t, err)
408+ broken.Cells = broken.Cells[:1]
409+ _, err = RemotePreviewFromWire(broken)
410+ require.ErrorIs(t, err, protocol.ErrRemotePreviewTooLarge)
411+ })
412+
413+ t.Run("decode refuses cell encoding out of range", func(t *testing.T) {
414+ broken, err := RemotePreviewToWire(preview)
415+ require.NoError(t, err)
416+ broken.Cells[0].RuneValue = 0x110000
417+ _, err = RemotePreviewFromWire(broken)
418+ require.ErrorIs(t, err, ErrOutOfRange)
419+ })
420+}
421+
422+// TestErrOutOfRangeIsDistinctFromProtocolSentinels proves protoconv never
423+// aliases its own range sentinel with a protocol validation sentinel, since
424+// callers (sessionwire, brokerwire) switch behavior on which one they see.
425+func TestErrOutOfRangeIsDistinctFromProtocolSentinels(t *testing.T) {
426+ require.False(t, errors.Is(ErrOutOfRange, protocol.ErrInvalidRemotePreview))
427+ require.False(t, errors.Is(ErrOutOfRange, protocol.ErrInvalidRemotePreviewRequest))
428+ require.False(t, errors.Is(ErrOutOfRange, protocol.ErrRemotePreviewTooLarge))
429+}
@@ -0,0 +1,429 @@
1+package protoconv
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ renderer "github.com/bnema/vev-vt"
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/wire"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+// TestRangeHelpers proves Uint8/Uint16 accept every in-range value and
15+// refuse every value whose high bits would otherwise truncate into an
16+// aliased in-range value.
17+func TestRangeHelpers(t *testing.T) {
18+ t.Run("Uint8", func(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ in uint32
22+ want uint8
23+ wantErr bool
24+ }{
25+ {"zero", 0, 0, false},
26+ {"max", 255, 255, false},
27+ {"overflow by one", 256, 0, true},
28+ {"far overflow", 1 << 20, 0, true},
29+ }
30+ for _, tt := range tests {
31+ t.Run(tt.name, func(t *testing.T) {
32+ got, err := Uint8[uint8](tt.in)
33+ if tt.wantErr {
34+ require.ErrorIs(t, err, ErrOutOfRange)
35+ return
36+ }
37+ require.NoError(t, err)
38+ require.Equal(t, tt.want, got)
39+ })
40+ }
41+ })
42+
43+ t.Run("Uint16", func(t *testing.T) {
44+ tests := []struct {
45+ name string
46+ in uint32
47+ want uint16
48+ wantErr bool
49+ }{
50+ {"zero", 0, 0, false},
51+ {"max", 65535, 65535, false},
52+ {"overflow by one", 65536, 0, true},
53+ {"aliasing overflow", 1<<16 + 1, 0, true},
54+ }
55+ for _, tt := range tests {
56+ t.Run(tt.name, func(t *testing.T) {
57+ got, err := Uint16[uint16](tt.in)
58+ if tt.wantErr {
59+ require.ErrorIs(t, err, ErrOutOfRange)
60+ return
61+ }
62+ require.NoError(t, err)
63+ require.Equal(t, tt.want, got)
64+ })
65+ }
66+ })
67+
68+ t.Run("Uint8 instantiated over a named enum", func(t *testing.T) {
69+ got, err := Uint8[domain.TabSelectorKind](uint32(domain.TabSelectorByStableID))
70+ require.NoError(t, err)
71+ require.Equal(t, domain.TabSelectorByStableID, got)
72+ _, err = Uint8[domain.TabSelectorKind](256)
73+ require.ErrorIs(t, err, ErrOutOfRange)
74+ })
75+}
76+
77+// TestLifecycleRoundTrip proves lifecycle IDs survive an encode/decode
78+// cycle byte for byte and that decode refuses a nil message or a
79+// wrong-length value.
80+func TestLifecycleRoundTrip(t *testing.T) {
81+ id := domain.SessionLifecycleID{1, 2, 3, 4}
82+ wireID := LifecycleToWire(id)
83+ require.Equal(t, id[:], wireID.GetValue())
84+
85+ decoded, err := LifecycleFromWire(wireID)
86+ require.NoError(t, err)
87+ require.Equal(t, id, decoded)
88+
89+ t.Run("nil message refused", func(t *testing.T) {
90+ _, err := LifecycleFromWire(nil)
91+ require.ErrorIs(t, err, ErrOutOfRange)
92+ })
93+
94+ t.Run("wrong length refused", func(t *testing.T) {
95+ _, err := LifecycleFromWire(&wire.LifecycleID{Value: []byte{1, 2, 3}})
96+ require.ErrorIs(t, err, ErrOutOfRange)
97+ })
98+}
99+
100+// TestTabSelectorRoundTrip proves a tab selector survives an encode/decode
101+// cycle, that a nil message decodes as the zero selector (it composes into
102+// an optional stopped-tab field), and that every narrowing field is
103+// range-checked.
104+func TestTabSelectorRoundTrip(t *testing.T) {
105+ selector := domain.TabSelector{
106+ Kind: domain.TabSelectorByStableID, StableID: "tab-1",
107+ Ordinal: 3, RawName: "work", ExpectedCount: 4,
108+ }
109+ wireSelector := TabSelectorToWire(selector)
110+ decoded, err := TabSelectorFromWire(wireSelector)
111+ require.NoError(t, err)
112+ require.Equal(t, selector, decoded)
113+
114+ t.Run("nil message tolerated", func(t *testing.T) {
115+ decoded, err := TabSelectorFromWire(nil)
116+ require.NoError(t, err)
117+ require.Equal(t, domain.TabSelector{}, decoded)
118+ })
119+
120+ tests := []struct {
121+ name string
122+ msg *wire.TabSelector
123+ }{
124+ {"kind out of range", &wire.TabSelector{Kind: 256}},
125+ {"ordinal out of range", &wire.TabSelector{Ordinal: 1 << 16}},
126+ {"expected count out of range", &wire.TabSelector{ExpectedCount: 1 << 16}},
127+ }
128+ for _, tt := range tests {
129+ t.Run(tt.name, func(t *testing.T) {
130+ _, err := TabSelectorFromWire(tt.msg)
131+ require.ErrorIs(t, err, ErrOutOfRange)
132+ })
133+ }
134+}
135+
136+func testRemoteTarget() domain.RemoteSessionTarget {
137+ return domain.RemoteSessionTarget{
138+ Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
139+ LifecycleID: domain.SessionLifecycleID{5, 6, 7},
140+ SessionName: "work", LiveTabID: "tab-1",
141+ }
142+}
143+
144+// TestRemoteTargetRoundTrip proves a remote target survives an
145+// encode/decode cycle and that decode refuses a nil message and every
146+// nested narrowing failure.
147+func TestRemoteTargetRoundTrip(t *testing.T) {
148+ target := testRemoteTarget()
149+ wireTarget := RemoteTargetToWire(target)
150+ decoded, err := RemoteTargetFromWire(wireTarget)
151+ require.NoError(t, err)
152+ require.Equal(t, target, decoded)
153+
154+ t.Run("nil message refused", func(t *testing.T) {
155+ _, err := RemoteTargetFromWire(nil)
156+ require.ErrorIs(t, err, ErrOutOfRange)
157+ })
158+
159+ t.Run("invalid lifecycle refused", func(t *testing.T) {
160+ broken := RemoteTargetToWire(target)
161+ broken.LifecycleId = &wire.LifecycleID{Value: []byte{1}}
162+ _, err := RemoteTargetFromWire(broken)
163+ require.ErrorIs(t, err, ErrOutOfRange)
164+ })
165+
166+ t.Run("invalid stopped tab refused", func(t *testing.T) {
167+ broken := RemoteTargetToWire(target)
168+ broken.StoppedTab.Kind = 256
169+ _, err := RemoteTargetFromWire(broken)
170+ require.ErrorIs(t, err, ErrOutOfRange)
171+ })
172+}
173+
174+// TestRGBRoundTrip proves an RGB color survives an encode/decode cycle,
175+// that a nil message decodes as the zero color, and every component is
176+// range-checked.
177+func TestRGBRoundTrip(t *testing.T) {
178+ color := renderer.RGB{R: 10, G: 20, B: 30}
179+ wireColor := RGBToWire(color)
180+ decoded, err := RGBFromWire(wireColor)
181+ require.NoError(t, err)
182+ require.Equal(t, color, decoded)
183+
184+ t.Run("nil message tolerated", func(t *testing.T) {
185+ decoded, err := RGBFromWire(nil)
186+ require.NoError(t, err)
187+ require.Equal(t, renderer.RGB{}, decoded)
188+ })
189+
190+ tests := []struct {
191+ name string
192+ msg *wire.RGB
193+ }{
194+ {"red out of range", &wire.RGB{R: 256}},
195+ {"green out of range", &wire.RGB{G: 256}},
196+ {"blue out of range", &wire.RGB{B: 256}},
197+ }
198+ for _, tt := range tests {
199+ t.Run(tt.name, func(t *testing.T) {
200+ _, err := RGBFromWire(tt.msg)
201+ require.ErrorIs(t, err, ErrOutOfRange)
202+ })
203+ }
204+}
205+
206+func testStyle() renderer.Style {
207+ return renderer.Style{
208+ Bold: true, Italic: true, Attrs: renderer.AttrDim,
209+ Foreground: 1, Background: 2,
210+ UnderlineStyle: renderer.UnderlineSingle, UnderlineColor: 3, HasUnderlineColor: true,
211+ ForegroundRGB: renderer.RGB{R: 1}, BackgroundRGB: renderer.RGB{G: 1}, UnderlineColorRGB: renderer.RGB{B: 1},
212+ }
213+}
214+
215+// TestCellStyleRoundTrip proves a cell style survives an encode/decode
216+// cycle, that a nil message decodes as the zero style, and every narrowing
217+// field is range-checked.
218+func TestCellStyleRoundTrip(t *testing.T) {
219+ style := testStyle()
220+ wireStyle := CellStyleToWire(style)
221+ decoded, err := CellStyleFromWire(wireStyle)
222+ require.NoError(t, err)
223+ require.Equal(t, style, decoded)
224+
225+ t.Run("nil message tolerated", func(t *testing.T) {
226+ decoded, err := CellStyleFromWire(nil)
227+ require.NoError(t, err)
228+ require.Equal(t, renderer.Style{}, decoded)
229+ })
230+
231+ tests := []struct {
232+ name string
233+ msg *wire.CellStyle
234+ }{
235+ {"foreground out of range", &wire.CellStyle{Foreground: 1 << 20}},
236+ {"background out of range", &wire.CellStyle{Background: -(1 << 20)}},
237+ {"underline color out of range", &wire.CellStyle{UnderlineColor: 1 << 20}},
238+ {"underline style out of range", &wire.CellStyle{UnderlineStyle: 256}},
239+ {"attrs out of range", &wire.CellStyle{Attrs: 1 << 16}},
240+ {"foreground rgb out of range", &wire.CellStyle{ForegroundRgb: &wire.RGB{R: 256}}},
241+ }
242+ for _, tt := range tests {
243+ t.Run(tt.name, func(t *testing.T) {
244+ _, err := CellStyleFromWire(tt.msg)
245+ require.ErrorIs(t, err, ErrOutOfRange)
246+ })
247+ }
248+}
249+
250+// TestPreviewCellRoundTrip proves a cell survives an encode/decode cycle,
251+// that decode refuses a nil message (unlike RGB/CellStyle, a single grid
252+// cell is never optional), and that an out-of-range rune is refused in
253+// both directions.
254+func TestPreviewCellRoundTrip(t *testing.T) {
255+ cell := renderer.Cell{Rune: 'A', Style: testStyle()}
256+ wireCell, err := PreviewCellToWire(cell)
257+ require.NoError(t, err)
258+ decoded, err := PreviewCellFromWire(wireCell)
259+ require.NoError(t, err)
260+ require.Equal(t, cell, decoded)
261+
262+ t.Run("nil message refused", func(t *testing.T) {
263+ _, err := PreviewCellFromWire(nil)
264+ require.ErrorIs(t, err, ErrOutOfRange)
265+ })
266+
267+ t.Run("encode rejects rune above max code point", func(t *testing.T) {
268+ _, err := PreviewCellToWire(renderer.Cell{Rune: 0x110000})
269+ require.ErrorIs(t, err, ErrOutOfRange)
270+ })
271+
272+ t.Run("encode rejects negative rune", func(t *testing.T) {
273+ _, err := PreviewCellToWire(renderer.Cell{Rune: -1})
274+ require.ErrorIs(t, err, ErrOutOfRange)
275+ })
276+
277+ t.Run("decode rejects rune above max code point", func(t *testing.T) {
278+ _, err := PreviewCellFromWire(&wire.PreviewCell{RuneValue: 0x110000})
279+ require.ErrorIs(t, err, ErrOutOfRange)
280+ })
281+}
282+
283+// TestPreviewCellsRoundTrip proves a slice of cells survives an
284+// encode/decode cycle, that an empty/nil slice stays nil rather than an
285+// empty allocation, and that one bad cell fails the whole slice.
286+func TestPreviewCellsRoundTrip(t *testing.T) {
287+ cells := []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}}
288+ wireCells, err := PreviewCellsToWire(cells)
289+ require.NoError(t, err)
290+ decoded, err := PreviewCellsFromWire(wireCells)
291+ require.NoError(t, err)
292+ require.Equal(t, cells, decoded)
293+
294+ t.Run("nil in, nil out", func(t *testing.T) {
295+ out, err := PreviewCellsToWire(nil)
296+ require.NoError(t, err)
297+ require.Nil(t, out)
298+ decoded, err := PreviewCellsFromWire(nil)
299+ require.NoError(t, err)
300+ require.Nil(t, decoded)
301+ })
302+
303+ t.Run("bad cell fails the slice on encode", func(t *testing.T) {
304+ _, err := PreviewCellsToWire([]renderer.Cell{{Rune: 'a'}, {Rune: 0x110000}})
305+ require.ErrorIs(t, err, ErrOutOfRange)
306+ })
307+
308+ t.Run("nil member fails the slice on decode", func(t *testing.T) {
309+ _, err := PreviewCellsFromWire([]*wire.PreviewCell{{RuneValue: 'a'}, nil})
310+ require.ErrorIs(t, err, ErrOutOfRange)
311+ })
312+}
313+
314+func testPreviewRequest() protocol.RemotePreviewRequest {
315+ return protocol.RemotePreviewRequest{
316+ Version: protocol.RemotePreviewSchemaVersion,
317+ Target: testRemoteTarget(),
318+ Width: 2, Height: 1,
319+ }
320+}
321+
322+// TestRemotePreviewRequestRoundTrip proves a remote preview request
323+// survives an encode/decode cycle byte-for-byte (via re-marshal), that
324+// decode refuses a nil message, and that both an invalid semantic value and
325+// an out-of-range wire field are refused with the right error shape.
326+func TestRemotePreviewRequestRoundTrip(t *testing.T) {
327+ request := testPreviewRequest()
328+ wireRequest, err := RemotePreviewRequestToWire(request)
329+ require.NoError(t, err)
330+ decoded, err := RemotePreviewRequestFromWire(wireRequest)
331+ require.NoError(t, err)
332+ require.Equal(t, request, decoded)
333+
334+ t.Run("nil message refused", func(t *testing.T) {
335+ _, err := RemotePreviewRequestFromWire(nil)
336+ require.ErrorIs(t, err, ErrOutOfRange)
337+ })
338+
339+ t.Run("encode refuses invalid semantic value", func(t *testing.T) {
340+ invalid := request
341+ invalid.Width = 0
342+ _, err := RemotePreviewRequestToWire(invalid)
343+ require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
344+ })
345+
346+ t.Run("decode refuses out-of-range wire field", func(t *testing.T) {
347+ broken, err := RemotePreviewRequestToWire(request)
348+ require.NoError(t, err)
349+ broken.Width = 1 << 16
350+ _, err = RemotePreviewRequestFromWire(broken)
351+ require.ErrorIs(t, err, ErrOutOfRange)
352+ })
353+
354+ t.Run("decode re-validates the fully decoded request", func(t *testing.T) {
355+ broken, err := RemotePreviewRequestToWire(request)
356+ require.NoError(t, err)
357+ broken.Width = 0
358+ _, err = RemotePreviewRequestFromWire(broken)
359+ require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
360+ })
361+}
362+
363+func testRemotePreview() protocol.RemotePreview {
364+ target := testRemoteTarget()
365+ return protocol.RemotePreview{
366+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
367+ LifecycleID: target.LifecycleID, TabID: target.LiveTabID,
368+ Revision: 11, Width: 2, Height: 1,
369+ Cells: []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}},
370+ }
371+}
372+
373+// TestRemotePreviewRoundTrip proves a remote preview survives an
374+// encode/decode cycle, that decode refuses a nil message, and that a
375+// too-large preview is refused with protocol.ErrRemotePreviewTooLarge on
376+// both directions distinctly from an out-of-range wire field.
377+func TestRemotePreviewRoundTrip(t *testing.T) {
378+ preview := testRemotePreview()
379+ wirePreview, err := RemotePreviewToWire(preview)
380+ require.NoError(t, err)
381+ decoded, err := RemotePreviewFromWire(wirePreview)
382+ require.NoError(t, err)
383+ require.Equal(t, preview, decoded)
384+
385+ t.Run("nil message refused", func(t *testing.T) {
386+ _, err := RemotePreviewFromWire(nil)
387+ require.ErrorIs(t, err, ErrOutOfRange)
388+ })
389+
390+ t.Run("encode refuses a too-large preview", func(t *testing.T) {
391+ invalid := preview
392+ invalid.Cells = invalid.Cells[:1]
393+ _, err := RemotePreviewToWire(invalid)
394+ require.ErrorIs(t, err, protocol.ErrRemotePreviewTooLarge)
395+ })
396+
397+ t.Run("decode refuses out-of-range wire field", func(t *testing.T) {
398+ broken, err := RemotePreviewToWire(preview)
399+ require.NoError(t, err)
400+ broken.Status = 1 << 16
401+ _, err = RemotePreviewFromWire(broken)
402+ require.ErrorIs(t, err, ErrOutOfRange)
403+ })
404+
405+ t.Run("decode refuses a too-large preview", func(t *testing.T) {
406+ broken, err := RemotePreviewToWire(preview)
407+ require.NoError(t, err)
408+ broken.Cells = broken.Cells[:1]
409+ _, err = RemotePreviewFromWire(broken)
410+ require.ErrorIs(t, err, protocol.ErrRemotePreviewTooLarge)
411+ })
412+
413+ t.Run("decode refuses cell encoding out of range", func(t *testing.T) {
414+ broken, err := RemotePreviewToWire(preview)
415+ require.NoError(t, err)
416+ broken.Cells[0].RuneValue = 0x110000
417+ _, err = RemotePreviewFromWire(broken)
418+ require.ErrorIs(t, err, ErrOutOfRange)
419+ })
420+}
421+
422+// TestErrOutOfRangeIsDistinctFromProtocolSentinels proves protoconv never
423+// aliases its own range sentinel with a protocol validation sentinel, since
424+// callers (sessionwire, brokerwire) switch behavior on which one they see.
425+func TestErrOutOfRangeIsDistinctFromProtocolSentinels(t *testing.T) {
426+ require.False(t, errors.Is(ErrOutOfRange, protocol.ErrInvalidRemotePreview))
427+ require.False(t, errors.Is(ErrOutOfRange, protocol.ErrInvalidRemotePreviewRequest))
428+ require.False(t, errors.Is(ErrOutOfRange, protocol.ErrRemotePreviewTooLarge))
429+}
M internal/adapters/sessionwire/proto_convert_preview.go +22 -77
@@ -1,116 +1,61 @@
11 package sessionwire
22
3+// RemotePreviewRequest/RemotePreview converters delegate their field-by-field
4+// logic to internal/adapters/protoconv, the canonical implementation shared
5+// with brokerwire. sessionwire's policy is to collapse every failure -
6+// whether a narrowing range failure or a semantic validation failure - into
7+// one fixed sentinel per direction (protocol.ErrInvalidRemotePreviewRequest
8+// or protocol.ErrInvalidRemotePreview), except that remotePreviewToWire
9+// still distinguishes protoconv's own ErrOutOfRange (encoded as
10+// errProtoConvertRange) from a protocol validation failure (passed through
11+// unchanged), matching this file's pre-extraction behavior exactly.
12+
313 import (
4- "github.com/bnema/vev/internal/domain"
14+ "errors"
15+
16+ "github.com/bnema/vev/internal/adapters/protoconv"
517 "github.com/bnema/vev/internal/protocol"
618 "github.com/bnema/vev/internal/protocol/wire"
719 )
820
921 func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
10- if err := protocol.ValidateRemotePreviewRequest(message); err != nil {
11- return nil, err
12- }
13- width := uint32(message.Width)
14- height := uint32(message.Height)
15- version := uint32(message.Version)
16- remote, err := remoteTargetToWire(&message.Target)
22+ out, err := protoconv.RemotePreviewRequestToWire(message)
1723 if err != nil {
1824 return nil, err
1925 }
20- return &wire.RemotePreviewRequest{Version: uint32(version), Target: remote, Width: width, Height: height}, nil
26+ return out, nil
2127 }
2228
2329 func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
24- var request protocol.RemotePreviewRequest
2530 if message == nil {
26- return request, protocol.ErrInvalidRemotePreviewRequest
27- }
28- version, err := mustUint16(message.GetVersion())
29- if err != nil {
3031 return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
3132 }
32- request.Version = version
33- remote, err := remoteTargetFromWire(message.GetTarget())
34- if err != nil || remote == nil {
35- return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
36- }
37- request.Target = *remote
38- width, err := mustUint16(message.GetWidth())
33+ request, err := protoconv.RemotePreviewRequestFromWire(message)
3934 if err != nil {
4035 return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
4136 }
42- height, err := mustUint16(message.GetHeight())
43- if err != nil {
44- return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
45- }
46- request.Width = width
47- request.Height = height
48- if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
49- return protocol.RemotePreviewRequest{}, err
50- }
5137 return request, nil
5238 }
5339
5440 func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
55- if err := protocol.ValidateRemotePreview(message); err != nil {
56- return nil, err
57- }
58- lifecycle := lifecycleToWire(message.LifecycleID)
59- cells, err := previewCellsToWire(message.Cells)
41+ out, err := protoconv.RemotePreviewToWire(message)
6042 if err != nil {
43+ if errors.Is(err, protoconv.ErrOutOfRange) {
44+ return nil, errProtoConvertRange
45+ }
6146 return nil, err
6247 }
63- return &wire.RemotePreview{
64- Version: uint32(message.Version),
65- Status: uint32(message.Status),
66- LifecycleId: lifecycle,
67- TabId: string(message.TabID),
68- Revision: message.Revision,
69- Width: uint32(message.Width),
70- Height: uint32(message.Height),
71- Cells: cells,
72- }, nil
48+ return out, nil
7349 }
7450
7551 func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
76- var preview protocol.RemotePreview
7752 if message == nil {
78- return preview, protocol.ErrInvalidRemotePreview
79- }
80- version, err := mustUint16(message.GetVersion())
81- if err != nil {
82- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
83- }
84- preview.Version = version
85- preview.Status, err = enum8[protocol.RemotePreviewStatus](message.GetStatus())
86- if err != nil {
87- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
88- }
89- lifecycle, err := lifecycleFromWire(message.GetLifecycleId())
90- if err != nil {
9153 return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
9254 }
93- preview.LifecycleID = lifecycle
94- preview.TabID = domain.TabStableID(message.GetTabId())
95- preview.Revision = message.GetRevision()
96- previewWidth, err := mustUint16(message.GetWidth())
55+ preview, err := protoconv.RemotePreviewFromWire(message)
9756 if err != nil {
9857 return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
9958 }
100- previewHeight, err := mustUint16(message.GetHeight())
101- if err != nil {
102- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
103- }
104- preview.Width = previewWidth
105- preview.Height = previewHeight
106- cells, err := previewCellsFromWire(message.GetCells())
107- if err != nil {
108- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
109- }
110- preview.Cells = cells
111- if err := protocol.ValidateRemotePreview(preview); err != nil {
112- return protocol.RemotePreview{}, err
113- }
11459 return preview, nil
11560 }
11661
@@ -1,116 +1,61 @@
1 package sessionwire1 package sessionwire
2 2
3+// RemotePreviewRequest/RemotePreview converters delegate their field-by-field
4+// logic to internal/adapters/protoconv, the canonical implementation shared
5+// with brokerwire. sessionwire's policy is to collapse every failure -
6+// whether a narrowing range failure or a semantic validation failure - into
7+// one fixed sentinel per direction (protocol.ErrInvalidRemotePreviewRequest
8+// or protocol.ErrInvalidRemotePreview), except that remotePreviewToWire
9+// still distinguishes protoconv's own ErrOutOfRange (encoded as
10+// errProtoConvertRange) from a protocol validation failure (passed through
11+// unchanged), matching this file's pre-extraction behavior exactly.
12+
3 import (13 import (
4- "github.com/bnema/vev/internal/domain"14+ "errors"
15+
16+ "github.com/bnema/vev/internal/adapters/protoconv"
5 "github.com/bnema/vev/internal/protocol"17 "github.com/bnema/vev/internal/protocol"
6 "github.com/bnema/vev/internal/protocol/wire"18 "github.com/bnema/vev/internal/protocol/wire"
7 )19 )
8 20
9 func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {21 func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
10- if err := protocol.ValidateRemotePreviewRequest(message); err != nil {22+ out, err := protoconv.RemotePreviewRequestToWire(message)
11- return nil, err
12- }
13- width := uint32(message.Width)
14- height := uint32(message.Height)
15- version := uint32(message.Version)
16- remote, err := remoteTargetToWire(&message.Target)
17 if err != nil {23 if err != nil {
18 return nil, err24 return nil, err
19 }25 }
20- return &wire.RemotePreviewRequest{Version: uint32(version), Target: remote, Width: width, Height: height}, nil26+ return out, nil
21 }27 }
22 28
23 func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {29 func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
24- var request protocol.RemotePreviewRequest
25 if message == nil {30 if message == nil {
26- return request, protocol.ErrInvalidRemotePreviewRequest
27- }
28- version, err := mustUint16(message.GetVersion())
29- if err != nil {
30 return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest31 return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
31 }32 }
32- request.Version = version33+ request, err := protoconv.RemotePreviewRequestFromWire(message)
33- remote, err := remoteTargetFromWire(message.GetTarget())
34- if err != nil || remote == nil {
35- return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
36- }
37- request.Target = *remote
38- width, err := mustUint16(message.GetWidth())
39 if err != nil {34 if err != nil {
40 return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest35 return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
41 }36 }
42- height, err := mustUint16(message.GetHeight())
43- if err != nil {
44- return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
45- }
46- request.Width = width
47- request.Height = height
48- if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
49- return protocol.RemotePreviewRequest{}, err
50- }
51 return request, nil37 return request, nil
52 }38 }
53 39
54 func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {40 func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
55- if err := protocol.ValidateRemotePreview(message); err != nil {41+ out, err := protoconv.RemotePreviewToWire(message)
56- return nil, err
57- }
58- lifecycle := lifecycleToWire(message.LifecycleID)
59- cells, err := previewCellsToWire(message.Cells)
60 if err != nil {42 if err != nil {
43+ if errors.Is(err, protoconv.ErrOutOfRange) {
44+ return nil, errProtoConvertRange
45+ }
61 return nil, err46 return nil, err
62 }47 }
63- return &wire.RemotePreview{48+ return out, nil
64- Version: uint32(message.Version),
65- Status: uint32(message.Status),
66- LifecycleId: lifecycle,
67- TabId: string(message.TabID),
68- Revision: message.Revision,
69- Width: uint32(message.Width),
70- Height: uint32(message.Height),
71- Cells: cells,
72- }, nil
73 }49 }
74 50
75 func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {51 func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
76- var preview protocol.RemotePreview
77 if message == nil {52 if message == nil {
78- return preview, protocol.ErrInvalidRemotePreview
79- }
80- version, err := mustUint16(message.GetVersion())
81- if err != nil {
82- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
83- }
84- preview.Version = version
85- preview.Status, err = enum8[protocol.RemotePreviewStatus](message.GetStatus())
86- if err != nil {
87- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
88- }
89- lifecycle, err := lifecycleFromWire(message.GetLifecycleId())
90- if err != nil {
91 return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview53 return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
92 }54 }
93- preview.LifecycleID = lifecycle55+ preview, err := protoconv.RemotePreviewFromWire(message)
94- preview.TabID = domain.TabStableID(message.GetTabId())
95- preview.Revision = message.GetRevision()
96- previewWidth, err := mustUint16(message.GetWidth())
97 if err != nil {56 if err != nil {
98 return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview57 return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
99 }58 }
100- previewHeight, err := mustUint16(message.GetHeight())
101- if err != nil {
102- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
103- }
104- preview.Width = previewWidth
105- preview.Height = previewHeight
106- cells, err := previewCellsFromWire(message.GetCells())
107- if err != nil {
108- return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview
109- }
110- preview.Cells = cells
111- if err := protocol.ValidateRemotePreview(preview); err != nil {
112- return protocol.RemotePreview{}, err
113- }
114 return preview, nil59 return preview, nil
115 }60 }
116 61
M internal/adapters/sessionwire/proto_convert_shared.go +46 -184
@@ -1,17 +1,20 @@
11 // Protobuf semantic converters (P2.2, wired at the P3.3 cutover).
22 //
33 // This file owns every semantic protocol value <-> generated wire envelope
4-// conversion used by client.go/server.go dispatch. Converters preserve
5-// domain-owned identifiers/geometry field-by-field and validate every
6-// narrowing numeric conversion before casting. Use cases always see
7-// uncompressed terminal data.
4+// conversion used by client.go/server.go dispatch. The actual field-by-field
5+// conversion logic lives in internal/adapters/protoconv, the single
6+// canonical implementation shared with brokerwire; the wrappers below adapt
7+// protoconv's shared ErrOutOfRange (and, for the RemotePreview family,
8+// protocol's own validation sentinels) to this package's local
9+// errProtoConvertRange, and add the pointer/nil-tolerance policy specific to
10+// sessionwire's call sites. Use cases always see uncompressed terminal data.
811 package sessionwire
912
1013 import (
1114 "errors"
12- "math"
1315
1416 renderer "github.com/bnema/vev-vt"
17+ "github.com/bnema/vev/internal/adapters/protoconv"
1518 "github.com/bnema/vev/internal/domain"
1619 "github.com/bnema/vev/internal/protocol"
1720 "github.com/bnema/vev/internal/protocol/wire"
@@ -20,77 +23,55 @@ import (
2023 var errProtoConvertRange = errors.New("sessionwire: wire value out of semantic range")
2124
2225 func mustUint16(v uint32) (uint16, error) {
23- if v > math.MaxUint16 {
26+ out, err := protoconv.Uint16[uint16](v)
27+ if err != nil {
2428 return 0, errProtoConvertRange
2529 }
26- return uint16(v), nil
30+ return out, nil
2731 }
2832
2933 func mustUint8(v uint32) (uint8, error) {
30- if v > math.MaxUint8 {
34+ out, err := protoconv.Uint8[uint8](v)
35+ if err != nil {
3136 return 0, errProtoConvertRange
3237 }
33- return uint8(v), nil
38+ return out, nil
3439 }
3540
3641 // enum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose
3742 // high bits would otherwise truncate into a valid enum range are refused
3843 // before any cast, so hostile wire input cannot alias a legitimate value.
3944 func enum8[T ~uint8](value uint32) (T, error) {
40- if value > math.MaxUint8 {
45+ out, err := protoconv.Uint8[T](value)
46+ if err != nil {
4147 return 0, errProtoConvertRange
4248 }
43- return T(value), nil
49+ return out, nil
4450 }
4551
4652 func mustTabIDString(value string) domain.TabStableID { return domain.TabStableID(value) }
4753
4854 func lifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
49- value := append([]byte(nil), id[:]...)
50- return &wire.LifecycleID{Value: value}
55+ return protoconv.LifecycleToWire(id)
5156 }
5257
5358 func lifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
54- var id domain.SessionLifecycleID
55- if message == nil || len(message.GetValue()) != len(id) {
56- return id, errProtoConvertRange
59+ id, err := protoconv.LifecycleFromWire(message)
60+ if err != nil {
61+ return domain.SessionLifecycleID{}, errProtoConvertRange
5762 }
58- copy(id[:], message.GetValue())
5963 return id, nil
6064 }
6165
6266 func tabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
63- return &wire.TabSelector{
64- Kind: uint32(selector.Kind),
65- StableId: string(selector.StableID),
66- Ordinal: uint32(selector.Ordinal),
67- RawName: selector.RawName,
68- ExpectedCount: uint32(selector.ExpectedCount),
69- }
67+ return protoconv.TabSelectorToWire(selector)
7068 }
7169
7270 func tabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
73- var selector domain.TabSelector
74- if message == nil {
75- return selector, nil
76- }
77- kind, err := enum8[domain.TabSelectorKind](message.GetKind())
78- if err != nil {
79- return domain.TabSelector{}, err
80- }
81- selector.Kind = kind
82- selector.StableID = domain.TabStableID(message.GetStableId())
83- ordinal, err := mustUint16(message.GetOrdinal())
84- if err != nil {
85- return domain.TabSelector{}, err
86- }
87- selector.Ordinal = ordinal
88- selector.RawName = message.GetRawName()
89- expected, err := mustUint16(message.GetExpectedCount())
71+ selector, err := protoconv.TabSelectorFromWire(message)
9072 if err != nil {
91- return domain.TabSelector{}, err
73+ return domain.TabSelector{}, errProtoConvertRange
9274 }
93- selector.ExpectedCount = expected
9475 return selector, nil
9576 }
9677
@@ -98,40 +79,18 @@ func remoteTargetToWire(target *domain.RemoteSessionTarget) (*wire.RemoteTarget,
9879 if target == nil {
9980 return nil, nil
10081 }
101- lifecycle := lifecycleToWire(target.LifecycleID)
102- return &wire.RemoteTarget{
103- Endpoint: target.Endpoint,
104- DisplayOrigin: target.DisplayOrigin,
105- LifecycleId: lifecycle,
106- SessionName: target.SessionName,
107- LiveTabId: string(target.LiveTabID),
108- StoppedTab: tabSelectorToWire(target.StoppedTab),
109- Stopped: target.Stopped,
110- }, nil
82+ return protoconv.RemoteTargetToWire(*target), nil
11183 }
11284
11385 func remoteTargetFromWire(message *wire.RemoteTarget) (*domain.RemoteSessionTarget, error) {
11486 if message == nil {
11587 return nil, nil
11688 }
117- target := &domain.RemoteSessionTarget{
118- Endpoint: message.GetEndpoint(),
119- DisplayOrigin: message.GetDisplayOrigin(),
120- SessionName: message.GetSessionName(),
121- LiveTabID: domain.TabStableID(message.GetLiveTabId()),
122- Stopped: message.GetStopped(),
123- }
124- lifecycle, err := lifecycleFromWire(message.GetLifecycleId())
89+ target, err := protoconv.RemoteTargetFromWire(message)
12590 if err != nil {
126- return nil, err
127- }
128- target.LifecycleID = lifecycle
129- stopped, err := tabSelectorFromWire(message.GetStoppedTab())
130- if err != nil {
131- return nil, err
91+ return nil, errProtoConvertRange
13292 }
133- target.StoppedTab = stopped
134- return target, nil
93+ return &target, nil
13594 }
13695
13796 func exactTargetToWire(target *protocol.ExactSessionTarget) *wire.ExactTarget {
@@ -153,154 +112,57 @@ func exactTargetFromWire(message *wire.ExactTarget) (*protocol.ExactSessionTarge
153112 }
154113
155114 func rgbToWire(color renderer.RGB) *wire.RGB {
156- return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}
115+ return protoconv.RGBToWire(color)
157116 }
158117
159118 func rgbFromWire(message *wire.RGB) (renderer.RGB, error) {
160- var color renderer.RGB
161- if message == nil {
162- return color, nil
163- }
164- r, err := mustUint8(message.GetR())
165- if err != nil {
166- return renderer.RGB{}, err
167- }
168- g, err := mustUint8(message.GetG())
119+ color, err := protoconv.RGBFromWire(message)
169120 if err != nil {
170- return renderer.RGB{}, err
121+ return renderer.RGB{}, errProtoConvertRange
171122 }
172- b, err := mustUint8(message.GetB())
173- if err != nil {
174- return renderer.RGB{}, err
175- }
176- return renderer.RGB{R: r, G: g, B: b}, nil
123+ return color, nil
177124 }
178125
179126 func cellStyleToWire(style renderer.Style) *wire.CellStyle {
180- return &wire.CellStyle{
181- Bold: style.Bold,
182- Italic: style.Italic,
183- Inverse: style.Inverse,
184- Attrs: uint32(style.Attrs),
185- Foreground: int32(style.Foreground),
186- Background: int32(style.Background),
187- HasForegroundRgb: style.HasForegroundRGB,
188- HasBackgroundRgb: style.HasBackgroundRGB,
189- HasUnderlineColor: style.HasUnderlineColor,
190- HasUnderlineColorRgb: style.HasUnderlineColorRGB,
191- UnderlineStyle: uint32(style.UnderlineStyle),
192- UnderlineColor: int32(style.UnderlineColor),
193- ForegroundRgb: rgbToWire(style.ForegroundRGB),
194- BackgroundRgb: rgbToWire(style.BackgroundRGB),
195- UnderlineColorRgb: rgbToWire(style.UnderlineColorRGB),
196- }
127+ return protoconv.CellStyleToWire(style)
197128 }
198129
199130 func cellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
200- var style renderer.Style
201- if message == nil {
202- return style, nil
203- }
204- foreground := message.GetForeground()
205- if foreground < math.MinInt16 || foreground > math.MaxInt16 {
206- return renderer.Style{}, errProtoConvertRange
207- }
208- background := message.GetBackground()
209- if background < math.MinInt16 || background > math.MaxInt16 {
210- return renderer.Style{}, errProtoConvertRange
211- }
212- underline := message.GetUnderlineColor()
213- if underline < math.MinInt16 || underline > math.MaxInt16 {
214- return renderer.Style{}, errProtoConvertRange
215- }
216- underlineStyle, err := mustUint8(message.GetUnderlineStyle())
131+ style, err := protoconv.CellStyleFromWire(message)
217132 if err != nil {
218- return renderer.Style{}, err
219- }
220- attrs := message.GetAttrs()
221- if attrs > math.MaxUint16 {
222133 return renderer.Style{}, errProtoConvertRange
223134 }
224- foregroundRGB, err := rgbFromWire(message.GetForegroundRgb())
225- if err != nil {
226- return renderer.Style{}, err
227- }
228- backgroundRGB, err := rgbFromWire(message.GetBackgroundRgb())
229- if err != nil {
230- return renderer.Style{}, err
231- }
232- underlineRGB, err := rgbFromWire(message.GetUnderlineColorRgb())
233- if err != nil {
234- return renderer.Style{}, err
235- }
236- return renderer.Style{
237- Bold: message.GetBold(),
238- Italic: message.GetItalic(),
239- Inverse: message.GetInverse(),
240- Attrs: renderer.StyleAttrs(attrs),
241- Foreground: int(foreground),
242- Background: int(background),
243- HasForegroundRGB: message.GetHasForegroundRgb(),
244- HasBackgroundRGB: message.GetHasBackgroundRgb(),
245- HasUnderlineColor: message.GetHasUnderlineColor(),
246- HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
247- UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
248- UnderlineColor: int(underline),
249- ForegroundRGB: foregroundRGB,
250- BackgroundRGB: backgroundRGB,
251- UnderlineColorRGB: underlineRGB,
252- }, nil
135+ return style, nil
253136 }
254137
255138 func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
256- if cell.Rune < 0 || cell.Rune > 0x10FFFF {
139+ out, err := protoconv.PreviewCellToWire(cell)
140+ if err != nil {
257141 return nil, errProtoConvertRange
258142 }
259- return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: cellStyleToWire(cell.Style)}, nil
143+ return out, nil
260144 }
261145
262146 func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
263- var cell renderer.Cell
264- if message == nil {
265- return cell, errProtoConvertRange
266- }
267- style, err := cellStyleFromWire(message.GetStyle())
147+ cell, err := protoconv.PreviewCellFromWire(message)
268148 if err != nil {
269- return renderer.Cell{}, err
270- }
271- runeValue := message.GetRuneValue()
272- if runeValue > 0x10FFFF {
273149 return renderer.Cell{}, errProtoConvertRange
274150 }
275- return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil
151+ return cell, nil
276152 }
277153
278154 func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
279- if len(cells) == 0 {
280- return nil, nil
281- }
282- out := make([]*wire.PreviewCell, 0, len(cells))
283- for _, cell := range cells {
284- converted, err := previewCellToWire(cell)
285- if err != nil {
286- return nil, err
287- }
288- out = append(out, converted)
155+ out, err := protoconv.PreviewCellsToWire(cells)
156+ if err != nil {
157+ return nil, errProtoConvertRange
289158 }
290159 return out, nil
291160 }
292161
293162 func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
294- if len(cells) == 0 {
295- return nil, nil
296- }
297- out := make([]renderer.Cell, 0, len(cells))
298- for _, cell := range cells {
299- converted, err := previewCellFromWire(cell)
300- if err != nil {
301- return nil, err
302- }
303- out = append(out, converted)
163+ out, err := protoconv.PreviewCellsFromWire(cells)
164+ if err != nil {
165+ return nil, errProtoConvertRange
304166 }
305167 return out, nil
306168 }
@@ -1,17 +1,20 @@
1 // Protobuf semantic converters (P2.2, wired at the P3.3 cutover).1 // Protobuf semantic converters (P2.2, wired at the P3.3 cutover).
2 //2 //
3 // This file owns every semantic protocol value <-> generated wire envelope3 // This file owns every semantic protocol value <-> generated wire envelope
4-// conversion used by client.go/server.go dispatch. Converters preserve4+// conversion used by client.go/server.go dispatch. The actual field-by-field
5-// domain-owned identifiers/geometry field-by-field and validate every5+// conversion logic lives in internal/adapters/protoconv, the single
6-// narrowing numeric conversion before casting. Use cases always see6+// canonical implementation shared with brokerwire; the wrappers below adapt
7-// uncompressed terminal data.7+// protoconv's shared ErrOutOfRange (and, for the RemotePreview family,
8+// protocol's own validation sentinels) to this package's local
9+// errProtoConvertRange, and add the pointer/nil-tolerance policy specific to
10+// sessionwire's call sites. Use cases always see uncompressed terminal data.
8 package sessionwire11 package sessionwire
9 12
10 import (13 import (
11 "errors"14 "errors"
12- "math"
13 15
14 renderer "github.com/bnema/vev-vt"16 renderer "github.com/bnema/vev-vt"
17+ "github.com/bnema/vev/internal/adapters/protoconv"
15 "github.com/bnema/vev/internal/domain"18 "github.com/bnema/vev/internal/domain"
16 "github.com/bnema/vev/internal/protocol"19 "github.com/bnema/vev/internal/protocol"
17 "github.com/bnema/vev/internal/protocol/wire"20 "github.com/bnema/vev/internal/protocol/wire"
@@ -20,77 +23,55 @@ import (
20 var errProtoConvertRange = errors.New("sessionwire: wire value out of semantic range")23 var errProtoConvertRange = errors.New("sessionwire: wire value out of semantic range")
21 24
22 func mustUint16(v uint32) (uint16, error) {25 func mustUint16(v uint32) (uint16, error) {
23- if v > math.MaxUint16 {26+ out, err := protoconv.Uint16[uint16](v)
27+ if err != nil {
24 return 0, errProtoConvertRange28 return 0, errProtoConvertRange
25 }29 }
26- return uint16(v), nil30+ return out, nil
27 }31 }
28 32
29 func mustUint8(v uint32) (uint8, error) {33 func mustUint8(v uint32) (uint8, error) {
30- if v > math.MaxUint8 {34+ out, err := protoconv.Uint8[uint8](v)
35+ if err != nil {
31 return 0, errProtoConvertRange36 return 0, errProtoConvertRange
32 }37 }
33- return uint8(v), nil38+ return out, nil
34 }39 }
35 40
36 // enum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose41 // enum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose
37 // high bits would otherwise truncate into a valid enum range are refused42 // high bits would otherwise truncate into a valid enum range are refused
38 // before any cast, so hostile wire input cannot alias a legitimate value.43 // before any cast, so hostile wire input cannot alias a legitimate value.
39 func enum8[T ~uint8](value uint32) (T, error) {44 func enum8[T ~uint8](value uint32) (T, error) {
40- if value > math.MaxUint8 {45+ out, err := protoconv.Uint8[T](value)
46+ if err != nil {
41 return 0, errProtoConvertRange47 return 0, errProtoConvertRange
42 }48 }
43- return T(value), nil49+ return out, nil
44 }50 }
45 51
46 func mustTabIDString(value string) domain.TabStableID { return domain.TabStableID(value) }52 func mustTabIDString(value string) domain.TabStableID { return domain.TabStableID(value) }
47 53
48 func lifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {54 func lifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
49- value := append([]byte(nil), id[:]...)55+ return protoconv.LifecycleToWire(id)
50- return &wire.LifecycleID{Value: value}
51 }56 }
52 57
53 func lifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {58 func lifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
54- var id domain.SessionLifecycleID59+ id, err := protoconv.LifecycleFromWire(message)
55- if message == nil || len(message.GetValue()) != len(id) {60+ if err != nil {
56- return id, errProtoConvertRange61+ return domain.SessionLifecycleID{}, errProtoConvertRange
57 }62 }
58- copy(id[:], message.GetValue())
59 return id, nil63 return id, nil
60 }64 }
61 65
62 func tabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {66 func tabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
63- return &wire.TabSelector{67+ return protoconv.TabSelectorToWire(selector)
64- Kind: uint32(selector.Kind),
65- StableId: string(selector.StableID),
66- Ordinal: uint32(selector.Ordinal),
67- RawName: selector.RawName,
68- ExpectedCount: uint32(selector.ExpectedCount),
69- }
70 }68 }
71 69
72 func tabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {70 func tabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
73- var selector domain.TabSelector71+ selector, err := protoconv.TabSelectorFromWire(message)
74- if message == nil {
75- return selector, nil
76- }
77- kind, err := enum8[domain.TabSelectorKind](message.GetKind())
78- if err != nil {
79- return domain.TabSelector{}, err
80- }
81- selector.Kind = kind
82- selector.StableID = domain.TabStableID(message.GetStableId())
83- ordinal, err := mustUint16(message.GetOrdinal())
84- if err != nil {
85- return domain.TabSelector{}, err
86- }
87- selector.Ordinal = ordinal
88- selector.RawName = message.GetRawName()
89- expected, err := mustUint16(message.GetExpectedCount())
90 if err != nil {72 if err != nil {
91- return domain.TabSelector{}, err73+ return domain.TabSelector{}, errProtoConvertRange
92 }74 }
93- selector.ExpectedCount = expected
94 return selector, nil75 return selector, nil
95 }76 }
96 77
@@ -98,40 +79,18 @@ func remoteTargetToWire(target *domain.RemoteSessionTarget) (*wire.RemoteTarget,
98 if target == nil {79 if target == nil {
99 return nil, nil80 return nil, nil
100 }81 }
101- lifecycle := lifecycleToWire(target.LifecycleID)82+ return protoconv.RemoteTargetToWire(*target), nil
102- return &wire.RemoteTarget{
103- Endpoint: target.Endpoint,
104- DisplayOrigin: target.DisplayOrigin,
105- LifecycleId: lifecycle,
106- SessionName: target.SessionName,
107- LiveTabId: string(target.LiveTabID),
108- StoppedTab: tabSelectorToWire(target.StoppedTab),
109- Stopped: target.Stopped,
110- }, nil
111 }83 }
112 84
113 func remoteTargetFromWire(message *wire.RemoteTarget) (*domain.RemoteSessionTarget, error) {85 func remoteTargetFromWire(message *wire.RemoteTarget) (*domain.RemoteSessionTarget, error) {
114 if message == nil {86 if message == nil {
115 return nil, nil87 return nil, nil
116 }88 }
117- target := &domain.RemoteSessionTarget{89+ target, err := protoconv.RemoteTargetFromWire(message)
118- Endpoint: message.GetEndpoint(),
119- DisplayOrigin: message.GetDisplayOrigin(),
120- SessionName: message.GetSessionName(),
121- LiveTabID: domain.TabStableID(message.GetLiveTabId()),
122- Stopped: message.GetStopped(),
123- }
124- lifecycle, err := lifecycleFromWire(message.GetLifecycleId())
125 if err != nil {90 if err != nil {
126- return nil, err91+ return nil, errProtoConvertRange
127- }
128- target.LifecycleID = lifecycle
129- stopped, err := tabSelectorFromWire(message.GetStoppedTab())
130- if err != nil {
131- return nil, err
132 }92 }
133- target.StoppedTab = stopped93+ return &target, nil
134- return target, nil
135 }94 }
136 95
137 func exactTargetToWire(target *protocol.ExactSessionTarget) *wire.ExactTarget {96 func exactTargetToWire(target *protocol.ExactSessionTarget) *wire.ExactTarget {
@@ -153,154 +112,57 @@ func exactTargetFromWire(message *wire.ExactTarget) (*protocol.ExactSessionTarge
153 }112 }
154 113
155 func rgbToWire(color renderer.RGB) *wire.RGB {114 func rgbToWire(color renderer.RGB) *wire.RGB {
156- return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}115+ return protoconv.RGBToWire(color)
157 }116 }
158 117
159 func rgbFromWire(message *wire.RGB) (renderer.RGB, error) {118 func rgbFromWire(message *wire.RGB) (renderer.RGB, error) {
160- var color renderer.RGB119+ color, err := protoconv.RGBFromWire(message)
161- if message == nil {
162- return color, nil
163- }
164- r, err := mustUint8(message.GetR())
165- if err != nil {
166- return renderer.RGB{}, err
167- }
168- g, err := mustUint8(message.GetG())
169 if err != nil {120 if err != nil {
170- return renderer.RGB{}, err121+ return renderer.RGB{}, errProtoConvertRange
171 }122 }
172- b, err := mustUint8(message.GetB())123+ return color, nil
173- if err != nil {
174- return renderer.RGB{}, err
175- }
176- return renderer.RGB{R: r, G: g, B: b}, nil
177 }124 }
178 125
179 func cellStyleToWire(style renderer.Style) *wire.CellStyle {126 func cellStyleToWire(style renderer.Style) *wire.CellStyle {
180- return &wire.CellStyle{127+ return protoconv.CellStyleToWire(style)
181- Bold: style.Bold,
182- Italic: style.Italic,
183- Inverse: style.Inverse,
184- Attrs: uint32(style.Attrs),
185- Foreground: int32(style.Foreground),
186- Background: int32(style.Background),
187- HasForegroundRgb: style.HasForegroundRGB,
188- HasBackgroundRgb: style.HasBackgroundRGB,
189- HasUnderlineColor: style.HasUnderlineColor,
190- HasUnderlineColorRgb: style.HasUnderlineColorRGB,
191- UnderlineStyle: uint32(style.UnderlineStyle),
192- UnderlineColor: int32(style.UnderlineColor),
193- ForegroundRgb: rgbToWire(style.ForegroundRGB),
194- BackgroundRgb: rgbToWire(style.BackgroundRGB),
195- UnderlineColorRgb: rgbToWire(style.UnderlineColorRGB),
196- }
197 }128 }
198 129
199 func cellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {130 func cellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
200- var style renderer.Style131+ style, err := protoconv.CellStyleFromWire(message)
201- if message == nil {
202- return style, nil
203- }
204- foreground := message.GetForeground()
205- if foreground < math.MinInt16 || foreground > math.MaxInt16 {
206- return renderer.Style{}, errProtoConvertRange
207- }
208- background := message.GetBackground()
209- if background < math.MinInt16 || background > math.MaxInt16 {
210- return renderer.Style{}, errProtoConvertRange
211- }
212- underline := message.GetUnderlineColor()
213- if underline < math.MinInt16 || underline > math.MaxInt16 {
214- return renderer.Style{}, errProtoConvertRange
215- }
216- underlineStyle, err := mustUint8(message.GetUnderlineStyle())
217 if err != nil {132 if err != nil {
218- return renderer.Style{}, err
219- }
220- attrs := message.GetAttrs()
221- if attrs > math.MaxUint16 {
222 return renderer.Style{}, errProtoConvertRange133 return renderer.Style{}, errProtoConvertRange
223 }134 }
224- foregroundRGB, err := rgbFromWire(message.GetForegroundRgb())135+ return style, nil
225- if err != nil {
226- return renderer.Style{}, err
227- }
228- backgroundRGB, err := rgbFromWire(message.GetBackgroundRgb())
229- if err != nil {
230- return renderer.Style{}, err
231- }
232- underlineRGB, err := rgbFromWire(message.GetUnderlineColorRgb())
233- if err != nil {
234- return renderer.Style{}, err
235- }
236- return renderer.Style{
237- Bold: message.GetBold(),
238- Italic: message.GetItalic(),
239- Inverse: message.GetInverse(),
240- Attrs: renderer.StyleAttrs(attrs),
241- Foreground: int(foreground),
242- Background: int(background),
243- HasForegroundRGB: message.GetHasForegroundRgb(),
244- HasBackgroundRGB: message.GetHasBackgroundRgb(),
245- HasUnderlineColor: message.GetHasUnderlineColor(),
246- HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
247- UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
248- UnderlineColor: int(underline),
249- ForegroundRGB: foregroundRGB,
250- BackgroundRGB: backgroundRGB,
251- UnderlineColorRGB: underlineRGB,
252- }, nil
253 }136 }
254 137
255 func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {138 func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
256- if cell.Rune < 0 || cell.Rune > 0x10FFFF {139+ out, err := protoconv.PreviewCellToWire(cell)
140+ if err != nil {
257 return nil, errProtoConvertRange141 return nil, errProtoConvertRange
258 }142 }
259- return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: cellStyleToWire(cell.Style)}, nil143+ return out, nil
260 }144 }
261 145
262 func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {146 func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
263- var cell renderer.Cell147+ cell, err := protoconv.PreviewCellFromWire(message)
264- if message == nil {
265- return cell, errProtoConvertRange
266- }
267- style, err := cellStyleFromWire(message.GetStyle())
268 if err != nil {148 if err != nil {
269- return renderer.Cell{}, err
270- }
271- runeValue := message.GetRuneValue()
272- if runeValue > 0x10FFFF {
273 return renderer.Cell{}, errProtoConvertRange149 return renderer.Cell{}, errProtoConvertRange
274 }150 }
275- return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil151+ return cell, nil
276 }152 }
277 153
278 func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {154 func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
279- if len(cells) == 0 {155+ out, err := protoconv.PreviewCellsToWire(cells)
280- return nil, nil156+ if err != nil {
281- }157+ return nil, errProtoConvertRange
282- out := make([]*wire.PreviewCell, 0, len(cells))
283- for _, cell := range cells {
284- converted, err := previewCellToWire(cell)
285- if err != nil {
286- return nil, err
287- }
288- out = append(out, converted)
289 }158 }
290 return out, nil159 return out, nil
291 }160 }
292 161
293 func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {162 func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
294- if len(cells) == 0 {163+ out, err := protoconv.PreviewCellsFromWire(cells)
295- return nil, nil164+ if err != nil {
296- }165+ return nil, errProtoConvertRange
297- out := make([]renderer.Cell, 0, len(cells))
298- for _, cell := range cells {
299- converted, err := previewCellFromWire(cell)
300- if err != nil {
301- return nil, err
302- }
303- out = append(out, converted)
304 }166 }
305 return out, nil167 return out, nil
306 }168 }

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