preview.go RawHistoryFolder
1package protocol
2
3import (
4 "errors"
5 "math"
6 "time"
7 "unicode"
8 "unicode/utf8"
9
10 renderer "github.com/bnema/vev-vt"
11 "github.com/bnema/vev/internal/domain"
12)
13
14// RemotePreviewSchemaVersion is independent from the attachment protocol version.
15const RemotePreviewSchemaVersion uint16 = 1
16
17type RemotePreviewStatus uint8
18
19const (
20 RemotePreviewOK RemotePreviewStatus = iota
21 RemotePreviewUnavailable
22 RemotePreviewNoSuchTarget
23 RemotePreviewStale
24 RemotePreviewMalformed
25 RemotePreviewTooLarge
26)
27
28// RemotePreviewRequest is the bounded exact-target viewport a preview watch
29// observes. It is never a session message on its own.
30type RemotePreviewRequest struct {
31 Version uint16
32 Target domain.RemoteSessionTarget
33 Width uint16
34 Height uint16
35}
36
37// RemotePreviewWatch is a client first message: the daemon answers with a
38// stream of RemotePreview frames for Request, at most one per MinInterval,
39// until the client closes the connection. A dead target ends the stream with
40// one RemotePreviewNoSuchTarget frame.
41type RemotePreviewWatch struct {
42 Request RemotePreviewRequest
43 MinInterval time.Duration
44}
45
46// Watch interval bounds. The wire carries whole milliseconds.
47const (
48 RemotePreviewWatchMinInterval = 16 * time.Millisecond
49 RemotePreviewWatchMaxInterval = time.Second
50)
51
52var ErrInvalidRemotePreviewWatch = errors.New("invalid remote preview watch")
53
54// ValidateRemotePreviewWatch accepts only a valid request and a whole
55// millisecond interval inside the watch bounds.
56func ValidateRemotePreviewWatch(watch RemotePreviewWatch) error {
57 if ValidateRemotePreviewRequest(watch.Request) != nil ||
58 watch.MinInterval < RemotePreviewWatchMinInterval || watch.MinInterval > RemotePreviewWatchMaxInterval ||
59 watch.MinInterval%time.Millisecond != 0 {
60 return ErrInvalidRemotePreviewWatch
61 }
62 return nil
63}
64
65// RemotePreview is a bounded row-major styled-cell viewport. It is never
66// persisted, logged, or traced.
67type RemotePreview struct {
68 Version uint16
69 Status RemotePreviewStatus
70 LifecycleID domain.SessionLifecycleID
71 TabID domain.TabStableID
72 Revision uint64
73 Width uint16
74 Height uint16
75 Cells []renderer.Cell
76}
77
78const (
79 RemotePreviewMaxWidth = 256
80 RemotePreviewMaxHeight = 128
81 RemotePreviewMaxCells = RemotePreviewMaxWidth * RemotePreviewMaxHeight
82 RemotePreviewMaxBytes = 1 << 20
83)
84
85var (
86 ErrInvalidRemotePreviewRequest = errors.New("ports: invalid remote preview request")
87 ErrInvalidRemotePreview = errors.New("ports: invalid remote preview")
88 ErrRemotePreviewTooLarge = errors.New("ports: remote preview exceeds size limit")
89 ErrRemotePreviewTimeout = errors.New("ports: remote preview command timed out")
90 ErrRemotePreviewUnsupportedStyle = errors.New("ports: remote preview has unsupported style")
91)
92
93func ValidateRemotePreviewRequest(request RemotePreviewRequest) error {
94 if request.Version != RemotePreviewSchemaVersion || request.Target.Stopped || request.Target.Validate() != nil {
95 return ErrInvalidRemotePreviewRequest
96 }
97 // These fields are encoded with uint16 byte lengths. Reject an oversized
98 // nested route before putString truncates it on the wire.
99 if len(request.Target.Endpoint) > math.MaxUint16 ||
100 len(request.Target.DisplayOrigin) > math.MaxUint16 ||
101 len(request.Target.SessionName) > math.MaxUint16 ||
102 len(request.Target.LiveTabID) > math.MaxUint16 {
103 return ErrInvalidRemotePreviewRequest
104 }
105 if request.Width == 0 || request.Height == 0 || int(request.Width) > RemotePreviewMaxWidth || int(request.Height) > RemotePreviewMaxHeight {
106 return ErrInvalidRemotePreviewRequest
107 }
108 return nil
109}
110
111func ValidateRemotePreview(preview RemotePreview) error {
112 if preview.Version != RemotePreviewSchemaVersion || preview.Status > RemotePreviewTooLarge {
113 return ErrInvalidRemotePreview
114 }
115 if preview.Status != RemotePreviewOK {
116 if preview.Width != 0 || preview.Height != 0 || len(preview.Cells) != 0 {
117 return ErrInvalidRemotePreview
118 }
119 return nil
120 }
121 if preview.LifecycleID == (domain.SessionLifecycleID{}) || domain.ValidateTabStableID(preview.TabID) != nil || preview.Revision == 0 {
122 return ErrInvalidRemotePreview
123 }
124 if preview.Width == 0 || preview.Height == 0 || int(preview.Width) > RemotePreviewMaxWidth || int(preview.Height) > RemotePreviewMaxHeight {
125 return ErrInvalidRemotePreview
126 }
127 want := int(preview.Width) * int(preview.Height)
128 if want > RemotePreviewMaxCells || len(preview.Cells) != want {
129 return ErrRemotePreviewTooLarge
130 }
131 if !previewCellRunValid(int(preview.Width), preview.Cells) {
132 return ErrInvalidRemotePreview
133 }
134 return nil
135}
136
137func validPreviewCellStyle(style renderer.Style) bool {
138 return style.Attrs&^(renderer.AttrDim|renderer.AttrUnderline|renderer.AttrBlink|renderer.AttrStrikethrough) == 0 &&
139 style.UnderlineStyle <= renderer.UnderlineDashed &&
140 style.Foreground >= math.MinInt16 && style.Foreground <= math.MaxInt16 &&
141 style.Background >= math.MinInt16 && style.Background <= math.MaxInt16 &&
142 style.UnderlineColor >= math.MinInt16 && style.UnderlineColor <= math.MaxInt16
143}
144
145func (p RemotePreview) FrameRows() [][]renderer.Cell {
146 if p.Width == 0 || p.Height == 0 || len(p.Cells) != int(p.Width)*int(p.Height) {
147 return nil
148 }
149 rows := make([][]renderer.Cell, p.Height)
150 width := int(p.Width)
151 for y := range rows {
152 rows[y] = append([]renderer.Cell(nil), p.Cells[y*width:(y+1)*width]...)
153 }
154 return rows
155}
156
157// previewCellRunValid reports whether cells form the row-major run of a
158// width-wide viewport: every rune and style is displayable, and each
159// continuation column follows a double-width cell on the same row.
160func previewCellRunValid(width int, cells []renderer.Cell) bool {
161 if width <= 0 {
162 return false
163 }
164 for i, cell := range cells {
165 if !utf8.ValidRune(cell.Rune) || (cell.Rune != 0 && unicode.IsControl(cell.Rune)) || !validPreviewCellStyle(cell.Style) {
166 return false
167 }
168 rowStart := (i / width) * width
169 rowEnd := rowStart + width
170 if cell.Continuation {
171 if cell.Rune != 0 || i == rowStart || cells[i-1].Continuation || renderer.RuneWidth(cells[i-1].Rune) != 2 {
172 return false
173 }
174 } else if renderer.RuneWidth(cell.Rune) == 2 && (i+1 >= rowEnd || !cells[i+1].Continuation) {
175 return false
176 }
177 }
178 return true
179}