main ece7682

protoconv.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close