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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 17 | 17 | // identity response; route/request dimensions are retained by client | |
| 18 | 18 | // subscription state and re-checked here through the ports preview | |
| 19 | 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 | 29 | import ( | |
| 22 | 30 | "errors" | |
| 23 | - | "math" | |
| 24 | 31 | ||
| 25 | - | renderer "github.com/bnema/vev-vt" | |
| 32 | + | "github.com/bnema/vev/internal/adapters/protoconv" | |
| 26 | 33 | "github.com/bnema/vev/internal/domain" | |
| 27 | 34 | "github.com/bnema/vev/internal/ports" | |
| 28 | 35 | "github.com/bnema/vev/internal/protocol" | |
| 29 | 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 | 39 | 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) | |
| 276 | 41 | if err != nil { | |
| 277 | - | return nil, err | |
| 42 | + | return nil, ErrInvalidMessage | |
| 278 | 43 | } | |
| 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 | |
| 285 | 45 | } | |
| 286 | 46 | ||
| 287 | 47 | func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) { | |
| 288 | - | var request protocol.RemotePreviewRequest | |
| 289 | 48 | 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 | |
| 303 | 50 | } | |
| 304 | - | height, err := brokerEnum16[uint16](message.GetHeight()) | |
| 51 | + | request, err := protoconv.RemotePreviewRequestFromWire(message) | |
| 305 | 52 | 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 | + | } | |
| 310 | 56 | return protocol.RemotePreviewRequest{}, ErrInvalidMessage | |
| 311 | 57 | } | |
| 312 | 58 | return request, nil | |
| 313 | 59 | } | |
| 314 | 60 | ||
| 315 | 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 | 64 | if errors.Is(err, protocol.ErrRemotePreviewTooLarge) { | |
| 318 | 65 | return nil, ErrTooLarge | |
| 319 | 66 | } | |
| 67 | + | if errors.Is(err, protoconv.ErrOutOfRange) { | |
| 68 | + | return nil, errConvertRange | |
| 69 | + | } | |
| 320 | 70 | return nil, ErrInvalidMessage | |
| 321 | 71 | } | |
| 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 | |
| 336 | 73 | } | |
| 337 | 74 | ||
| 338 | 75 | func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) { | |
| 339 | - | var preview protocol.RemotePreview | |
| 340 | 76 | 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 | |
| 362 | 78 | } | |
| 363 | - | cells, err := previewCellsFromWire(message.GetCells()) | |
| 79 | + | preview, err := protoconv.RemotePreviewFromWire(message) | |
| 364 | 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 | 81 | if errors.Is(err, protocol.ErrRemotePreviewTooLarge) { | |
| 379 | 82 | return protocol.RemotePreview{}, ErrTooLarge | |
| 380 | 83 | } | |
| 84 | + | if errors.Is(err, protoconv.ErrOutOfRange) { | |
| 85 | + | return protocol.RemotePreview{}, errConvertRange | |
| 86 | + | } | |
| 381 | 87 | return protocol.RemotePreview{}, ErrInvalidMessage | |
| 382 | 88 | } | |
| 383 | 89 | return preview, nil | |
| @@ -17,367 +17,73 @@ package brokerwire | |||||
| 17 | // identity response; route/request dimensions are retained by client | 17 | // identity response; route/request dimensions are retained by client | ||
| 18 | // subscription state and re-checked here through the ports preview | 18 | // 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, err | 42 | + | 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, ErrInvalidMessage | 49 | + | 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{}, errConvertRange | 53 | + | 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{}, ErrInvalidMessage | 56 | return protocol.RemotePreviewRequest{}, ErrInvalidMessage | ||
| 311 | } | 57 | } | ||
| 312 | return request, nil | 58 | 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, ErrTooLarge | 65 | return nil, ErrTooLarge | ||
| 319 | } | 66 | } | ||
| 67 | + | if errors.Is(err, protoconv.ErrOutOfRange) { | |||
| 68 | + | return nil, errConvertRange | |||
| 69 | + | } | |||
| 320 | return nil, ErrInvalidMessage | 70 | 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, ErrInvalidMessage | 77 | + | 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{}, ErrTooLarge | 82 | return protocol.RemotePreview{}, ErrTooLarge | ||
| 380 | } | 83 | } | ||
| 84 | + | if errors.Is(err, protoconv.ErrOutOfRange) { | |||
| 85 | + | return protocol.RemotePreview{}, errConvertRange | |||
| 86 | + | } | |||
| 381 | return protocol.RemotePreview{}, ErrInvalidMessage | 87 | return protocol.RemotePreview{}, ErrInvalidMessage | ||
| 382 | } | 88 | } | ||
| 383 | return preview, nil | 89 | 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 @@ | |||
| 1 | 1 | 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 | 13 | import ( | |
| 4 | - | "github.com/bnema/vev/internal/domain" | |
| 14 | + | "errors" | |
| 15 | + | ||
| 16 | + | "github.com/bnema/vev/internal/adapters/protoconv" | |
| 5 | 17 | "github.com/bnema/vev/internal/protocol" | |
| 6 | 18 | "github.com/bnema/vev/internal/protocol/wire" | |
| 7 | 19 | ) | |
| 8 | 20 | ||
| 9 | 21 | 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) | |
| 17 | 23 | if err != nil { | |
| 18 | 24 | return nil, err | |
| 19 | 25 | } | |
| 20 | - | return &wire.RemotePreviewRequest{Version: uint32(version), Target: remote, Width: width, Height: height}, nil | |
| 26 | + | return out, nil | |
| 21 | 27 | } | |
| 22 | 28 | ||
| 23 | 29 | func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) { | |
| 24 | - | var request protocol.RemotePreviewRequest | |
| 25 | 30 | if message == nil { | |
| 26 | - | return request, protocol.ErrInvalidRemotePreviewRequest | |
| 27 | - | } | |
| 28 | - | version, err := mustUint16(message.GetVersion()) | |
| 29 | - | if err != nil { | |
| 30 | 31 | return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest | |
| 31 | 32 | } | |
| 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) | |
| 39 | 34 | if err != nil { | |
| 40 | 35 | 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 | 37 | return request, nil | |
| 52 | 38 | } | |
| 53 | 39 | ||
| 54 | 40 | 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) | |
| 60 | 42 | if err != nil { | |
| 43 | + | if errors.Is(err, protoconv.ErrOutOfRange) { | |
| 44 | + | return nil, errProtoConvertRange | |
| 45 | + | } | |
| 61 | 46 | return nil, err | |
| 62 | 47 | } | |
| 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 | |
| 73 | 49 | } | |
| 74 | 50 | ||
| 75 | 51 | func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) { | |
| 76 | - | var preview protocol.RemotePreview | |
| 77 | 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 | 53 | return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview | |
| 92 | 54 | } | |
| 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) | |
| 97 | 56 | if err != nil { | |
| 98 | 57 | 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 | 59 | return preview, nil | |
| 115 | 60 | } | |
| 116 | 61 | ||
| @@ -1,116 +1,61 @@ | |||||
| 1 | package sessionwire | 1 | 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, err | 24 | return nil, err | ||
| 19 | } | 25 | } | ||
| 20 | - | return &wire.RemotePreviewRequest{Version: uint32(version), Target: remote, Width: width, Height: height}, nil | 26 | + | 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.ErrInvalidRemotePreviewRequest | 31 | return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest | ||
| 31 | } | 32 | } | ||
| 32 | - | request.Version = version | 33 | + | 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.ErrInvalidRemotePreviewRequest | 35 | 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, nil | 37 | 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, err | 46 | 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.ErrInvalidRemotePreview | 53 | return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreview | ||
| 92 | } | 54 | } | ||
| 93 | - | preview.LifecycleID = lifecycle | 55 | + | 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.ErrInvalidRemotePreview | 57 | 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, nil | 59 | return preview, nil | ||
| 115 | } | 60 | } | ||
| 116 | 61 | ||||
M internal/adapters/sessionwire/proto_convert_shared.go +46 -184
| @@ -1,17 +1,20 @@ | |||
| 1 | 1 | // Protobuf semantic converters (P2.2, wired at the P3.3 cutover). | |
| 2 | 2 | // | |
| 3 | 3 | // 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. | |
| 8 | 11 | package sessionwire | |
| 9 | 12 | ||
| 10 | 13 | import ( | |
| 11 | 14 | "errors" | |
| 12 | - | "math" | |
| 13 | 15 | ||
| 14 | 16 | renderer "github.com/bnema/vev-vt" | |
| 17 | + | "github.com/bnema/vev/internal/adapters/protoconv" | |
| 15 | 18 | "github.com/bnema/vev/internal/domain" | |
| 16 | 19 | "github.com/bnema/vev/internal/protocol" | |
| 17 | 20 | "github.com/bnema/vev/internal/protocol/wire" | |
| @@ -20,77 +23,55 @@ import ( | |||
| 20 | 23 | var errProtoConvertRange = errors.New("sessionwire: wire value out of semantic range") | |
| 21 | 24 | ||
| 22 | 25 | func mustUint16(v uint32) (uint16, error) { | |
| 23 | - | if v > math.MaxUint16 { | |
| 26 | + | out, err := protoconv.Uint16[uint16](v) | |
| 27 | + | if err != nil { | |
| 24 | 28 | return 0, errProtoConvertRange | |
| 25 | 29 | } | |
| 26 | - | return uint16(v), nil | |
| 30 | + | return out, nil | |
| 27 | 31 | } | |
| 28 | 32 | ||
| 29 | 33 | func mustUint8(v uint32) (uint8, error) { | |
| 30 | - | if v > math.MaxUint8 { | |
| 34 | + | out, err := protoconv.Uint8[uint8](v) | |
| 35 | + | if err != nil { | |
| 31 | 36 | return 0, errProtoConvertRange | |
| 32 | 37 | } | |
| 33 | - | return uint8(v), nil | |
| 38 | + | return out, nil | |
| 34 | 39 | } | |
| 35 | 40 | ||
| 36 | 41 | // enum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose | |
| 37 | 42 | // high bits would otherwise truncate into a valid enum range are refused | |
| 38 | 43 | // before any cast, so hostile wire input cannot alias a legitimate value. | |
| 39 | 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 | 47 | return 0, errProtoConvertRange | |
| 42 | 48 | } | |
| 43 | - | return T(value), nil | |
| 49 | + | return out, nil | |
| 44 | 50 | } | |
| 45 | 51 | ||
| 46 | 52 | func mustTabIDString(value string) domain.TabStableID { return domain.TabStableID(value) } | |
| 47 | 53 | ||
| 48 | 54 | func lifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID { | |
| 49 | - | value := append([]byte(nil), id[:]...) | |
| 50 | - | return &wire.LifecycleID{Value: value} | |
| 55 | + | return protoconv.LifecycleToWire(id) | |
| 51 | 56 | } | |
| 52 | 57 | ||
| 53 | 58 | 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 | |
| 57 | 62 | } | |
| 58 | - | copy(id[:], message.GetValue()) | |
| 59 | 63 | return id, nil | |
| 60 | 64 | } | |
| 61 | 65 | ||
| 62 | 66 | 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) | |
| 70 | 68 | } | |
| 71 | 69 | ||
| 72 | 70 | 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) | |
| 90 | 72 | if err != nil { | |
| 91 | - | return domain.TabSelector{}, err | |
| 73 | + | return domain.TabSelector{}, errProtoConvertRange | |
| 92 | 74 | } | |
| 93 | - | selector.ExpectedCount = expected | |
| 94 | 75 | return selector, nil | |
| 95 | 76 | } | |
| 96 | 77 | ||
| @@ -98,40 +79,18 @@ func remoteTargetToWire(target *domain.RemoteSessionTarget) (*wire.RemoteTarget, | |||
| 98 | 79 | if target == nil { | |
| 99 | 80 | return nil, nil | |
| 100 | 81 | } | |
| 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 | |
| 111 | 83 | } | |
| 112 | 84 | ||
| 113 | 85 | func remoteTargetFromWire(message *wire.RemoteTarget) (*domain.RemoteSessionTarget, error) { | |
| 114 | 86 | if message == nil { | |
| 115 | 87 | return nil, nil | |
| 116 | 88 | } | |
| 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) | |
| 125 | 90 | 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 | |
| 132 | 92 | } | |
| 133 | - | target.StoppedTab = stopped | |
| 134 | - | return target, nil | |
| 93 | + | return &target, nil | |
| 135 | 94 | } | |
| 136 | 95 | ||
| 137 | 96 | func exactTargetToWire(target *protocol.ExactSessionTarget) *wire.ExactTarget { | |
| @@ -153,154 +112,57 @@ func exactTargetFromWire(message *wire.ExactTarget) (*protocol.ExactSessionTarge | |||
| 153 | 112 | } | |
| 154 | 113 | ||
| 155 | 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 | 118 | 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) | |
| 169 | 120 | if err != nil { | |
| 170 | - | return renderer.RGB{}, err | |
| 121 | + | return renderer.RGB{}, errProtoConvertRange | |
| 171 | 122 | } | |
| 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 | |
| 177 | 124 | } | |
| 178 | 125 | ||
| 179 | 126 | 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) | |
| 197 | 128 | } | |
| 198 | 129 | ||
| 199 | 130 | 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) | |
| 217 | 132 | if err != nil { | |
| 218 | - | return renderer.Style{}, err | |
| 219 | - | } | |
| 220 | - | attrs := message.GetAttrs() | |
| 221 | - | if attrs > math.MaxUint16 { | |
| 222 | 133 | return renderer.Style{}, errProtoConvertRange | |
| 223 | 134 | } | |
| 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 | |
| 253 | 136 | } | |
| 254 | 137 | ||
| 255 | 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 | 141 | return nil, errProtoConvertRange | |
| 258 | 142 | } | |
| 259 | - | return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: cellStyleToWire(cell.Style)}, nil | |
| 143 | + | return out, nil | |
| 260 | 144 | } | |
| 261 | 145 | ||
| 262 | 146 | 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) | |
| 268 | 148 | if err != nil { | |
| 269 | - | return renderer.Cell{}, err | |
| 270 | - | } | |
| 271 | - | runeValue := message.GetRuneValue() | |
| 272 | - | if runeValue > 0x10FFFF { | |
| 273 | 149 | return renderer.Cell{}, errProtoConvertRange | |
| 274 | 150 | } | |
| 275 | - | return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil | |
| 151 | + | return cell, nil | |
| 276 | 152 | } | |
| 277 | 153 | ||
| 278 | 154 | 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 | |
| 289 | 158 | } | |
| 290 | 159 | return out, nil | |
| 291 | 160 | } | |
| 292 | 161 | ||
| 293 | 162 | 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 | |
| 304 | 166 | } | |
| 305 | 167 | return out, nil | |
| 306 | 168 | } | |
| @@ -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 envelope | 3 | // This file owns every semantic protocol value <-> generated wire envelope | ||
| 4 | - | // conversion used by client.go/server.go dispatch. Converters preserve | 4 | + | // conversion used by client.go/server.go dispatch. The actual field-by-field |
| 5 | - | // domain-owned identifiers/geometry field-by-field and validate every | 5 | + | // conversion logic lives in internal/adapters/protoconv, the single |
| 6 | - | // narrowing numeric conversion before casting. Use cases always see | 6 | + | // 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 sessionwire | 11 | 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, errProtoConvertRange | 28 | return 0, errProtoConvertRange | ||
| 25 | } | 29 | } | ||
| 26 | - | return uint16(v), nil | 30 | + | 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, errProtoConvertRange | 36 | return 0, errProtoConvertRange | ||
| 32 | } | 37 | } | ||
| 33 | - | return uint8(v), nil | 38 | + | return out, nil |
| 34 | } | 39 | } | ||
| 35 | 40 | ||||
| 36 | // enum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose | 41 | // 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 refused | 42 | // 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, errProtoConvertRange | 47 | return 0, errProtoConvertRange | ||
| 42 | } | 48 | } | ||
| 43 | - | return T(value), nil | 49 | + | 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.SessionLifecycleID | 59 | + | id, err := protoconv.LifecycleFromWire(message) |
| 55 | - | if message == nil || len(message.GetValue()) != len(id) { | 60 | + | if err != nil { |
| 56 | - | return id, errProtoConvertRange | 61 | + | return domain.SessionLifecycleID{}, errProtoConvertRange |
| 57 | } | 62 | } | ||
| 58 | - | copy(id[:], message.GetValue()) | |||
| 59 | return id, nil | 63 | 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.TabSelector | 71 | + | 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{}, err | 73 | + | return domain.TabSelector{}, errProtoConvertRange |
| 92 | } | 74 | } | ||
| 93 | - | selector.ExpectedCount = expected | |||
| 94 | return selector, nil | 75 | 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, nil | 80 | 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, nil | 87 | 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, err | 91 | + | 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 = stopped | 93 | + | 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.RGB | 119 | + | 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{}, err | 121 | + | 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.Style | 131 | + | 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{}, errProtoConvertRange | 133 | 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, errProtoConvertRange | 141 | return nil, errProtoConvertRange | ||
| 258 | } | 142 | } | ||
| 259 | - | return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: cellStyleToWire(cell.Style)}, nil | 143 | + | 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.Cell | 147 | + | 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{}, errProtoConvertRange | 149 | return renderer.Cell{}, errProtoConvertRange | ||
| 274 | } | 150 | } | ||
| 275 | - | return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil | 151 | + | 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, nil | 156 | + | 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, nil | 159 | 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, nil | 164 | + | 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, nil | 167 | return out, nil | ||
| 306 | } | 168 | } | ||