detached 986c5fc

refactor(adapters): share broker conversion helpers

commit
986c5fcc566c7ea0394f56918595b5f2f0d8a039
author
brice · · 2026-09-23 14:40 UTC
committer
brice · · 2026-09-23 14:49 UTC
parent
f97fa3a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +374 -312
M internal/adapters/brokerwire/codec.go +8 -16
@@ -21,6 +21,7 @@ import (
2121
2222 "google.golang.org/protobuf/proto"
2323
24+ "github.com/bnema/vev/internal/adapters/protoconv"
2425 "github.com/bnema/vev/internal/domain"
2526 "github.com/bnema/vev/internal/ports"
2627 "github.com/bnema/vev/internal/protocol"
@@ -601,30 +602,21 @@ func admissionFromWire(value uint32) (ports.BrokerStreamAdmission, error) {
601602 }
602603 }
603604
604-// startModeToWire maps the closed daemon-start taxonomy onto its wire code.
605-// The zero value is refused rather than encoded, so a peer never has to guess
606-// whether an absent mode authorized a spawn.
605+// Start-mode conversion retains the brokerwire range sentinel at the boundary.
607606 func startModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {
608- switch mode {
609- case ports.BrokerDaemonExistingOnly:
610- return 1, nil
611- case ports.BrokerDaemonStartIfNeeded:
612- return 2, nil
613- default:
607+ value, err := protoconv.BrokerStartModeToWire(mode)
608+ if err != nil {
614609 return 0, errConvertRange
615610 }
611+ return value, nil
616612 }
617613
618-// startModeFromWire maps a wire daemon-start code onto the closed taxonomy.
619614 func startModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {
620- switch value {
621- case 1:
622- return ports.BrokerDaemonExistingOnly, nil
623- case 2:
624- return ports.BrokerDaemonStartIfNeeded, nil
625- default:
615+ mode, err := protoconv.BrokerStartModeFromWire(value)
616+ if err != nil {
626617 return 0, errConvertRange
627618 }
619+ return mode, nil
628620 }
629621
630622 func openStreamFromWire(message *wire.OpenStream) (OpenStream, error) {
@@ -21,6 +21,7 @@ import (
21 21
22 "google.golang.org/protobuf/proto"22 "google.golang.org/protobuf/proto"
23 23
24+ "github.com/bnema/vev/internal/adapters/protoconv"
24 "github.com/bnema/vev/internal/domain"25 "github.com/bnema/vev/internal/domain"
25 "github.com/bnema/vev/internal/ports"26 "github.com/bnema/vev/internal/ports"
26 "github.com/bnema/vev/internal/protocol"27 "github.com/bnema/vev/internal/protocol"
@@ -601,30 +602,21 @@ func admissionFromWire(value uint32) (ports.BrokerStreamAdmission, error) {
601 }602 }
602 }603 }
603 604
604-// startModeToWire maps the closed daemon-start taxonomy onto its wire code.605+// Start-mode conversion retains the brokerwire range sentinel at the boundary.
605-// The zero value is refused rather than encoded, so a peer never has to guess
606-// whether an absent mode authorized a spawn.
607 func startModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {606 func startModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {
608- switch mode {607+ value, err := protoconv.BrokerStartModeToWire(mode)
609- case ports.BrokerDaemonExistingOnly:608+ if err != nil {
610- return 1, nil
611- case ports.BrokerDaemonStartIfNeeded:
612- return 2, nil
613- default:
614 return 0, errConvertRange609 return 0, errConvertRange
615 }610 }
611+ return value, nil
616 }612 }
617 613
618-// startModeFromWire maps a wire daemon-start code onto the closed taxonomy.
619 func startModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {614 func startModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {
620- switch value {615+ mode, err := protoconv.BrokerStartModeFromWire(value)
621- case 1:616+ if err != nil {
622- return ports.BrokerDaemonExistingOnly, nil
623- case 2:
624- return ports.BrokerDaemonStartIfNeeded, nil
625- default:
626 return 0, errConvertRange617 return 0, errConvertRange
627 }618 }
619+ return mode, nil
628 }620 }
629 621
630 func openStreamFromWire(message *wire.OpenStream) (OpenStream, error) {622 func openStreamFromWire(message *wire.OpenStream) (OpenStream, error) {
M internal/adapters/brokerwire/convert.go +48 -135
@@ -12,10 +12,8 @@ package brokerwire
1212
1313 import (
1414 "math"
15- "strings"
16- "unicode"
17- "unicode/utf8"
1815
16+ "github.com/bnema/vev/internal/adapters/protoconv"
1917 "github.com/bnema/vev/internal/domain"
2018 "github.com/bnema/vev/internal/ports"
2119 "github.com/bnema/vev/internal/protocol"
@@ -23,22 +21,22 @@ import (
2321 "github.com/bnema/vev/internal/protocol/wire"
2422 )
2523
26-// brokerEnum8 narrows one wire uint32 into an 8-bit semantic enum. Values
27-// whose high bits would otherwise truncate into a valid enum range are
28-// refused before any cast.
24+// brokerEnum8 maps shared narrowing failures to the local range sentinel.
2925 func brokerEnum8[T ~uint8](value uint32) (T, error) {
30- if value > math.MaxUint8 {
26+ v, err := protoconv.Uint8[T](value)
27+ if err != nil {
3128 return 0, errConvertRange
3229 }
33- return T(value), nil
30+ return v, nil
3431 }
3532
36-// brokerEnum16 narrows one wire uint32 into a 16-bit semantic value.
33+// brokerEnum16 maps shared narrowing failures to the local range sentinel.
3734 func brokerEnum16[T ~uint16](value uint32) (T, error) {
38- if value > math.MaxUint16 {
35+ v, err := protoconv.Uint16[T](value)
36+ if err != nil {
3937 return 0, errConvertRange
4038 }
41- return T(value), nil
39+ return v, nil
4240 }
4341
4442 func scopeToWire(epoch ports.BrokerEpoch, connection ports.BrokerConnectionID) *wire.BrokerScope {
@@ -101,138 +99,65 @@ func operationFromWire(raw []byte) (ports.BrokerOperationID, error) {
10199 return id, nil
102100 }
103101
104-func registrationToWire(registration domain.RemoteRegistration) *wire.RemoteRegistration {
105- incarnation := append([]byte(nil), registration.Incarnation[:]...)
106- return &wire.RemoteRegistration{
107- Endpoint: registration.Endpoint,
108- Incarnation: incarnation,
109- Generation: uint64(registration.Generation),
110- }
102+func registrationToWire(v domain.RemoteRegistration) *wire.RemoteRegistration {
103+ return protoconv.BrokerRegistrationToWire(v)
111104 }
112-
113-func registrationFromWire(message *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
114- var registration domain.RemoteRegistration
115- if message == nil {
116- return registration, errConvertRange
117- }
118- registration.Endpoint = message.GetEndpoint()
119- if err := domain.ValidateRemoteHostTarget(registration.Endpoint); err != nil {
120- return domain.RemoteRegistration{}, errConvertRange
121- }
122- raw := message.GetIncarnation()
123- if len(raw) != len(registration.Incarnation) {
124- return domain.RemoteRegistration{}, errConvertRange
125- }
126- copy(registration.Incarnation[:], raw)
127- registration.Generation = domain.RemoteGeneration(message.GetGeneration())
128- if err := registration.Validate(); err != nil {
105+func registrationFromWire(v *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
106+ out, err := protoconv.BrokerRegistrationFromWire(v)
107+ if err != nil {
129108 return domain.RemoteRegistration{}, errConvertRange
130109 }
131- return registration, nil
110+ return out, nil
132111 }
133112
134-func exactTargetToWire(target protocol.ExactSessionTarget) *wire.ExactTarget {
135- lifecycle := append([]byte(nil), target.LifecycleID[:]...)
136- return &wire.ExactTarget{
137- LifecycleId: &wire.LifecycleID{Value: lifecycle},
138- SessionName: target.SessionName,
139- }
113+func exactTargetToWire(v protocol.ExactSessionTarget) *wire.ExactTarget {
114+ return protoconv.BrokerExactTargetToWire(v)
140115 }
141116
142-func exactTargetFromWire(message *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
143- var target protocol.ExactSessionTarget
144- if message == nil {
145- return target, errConvertRange
146- }
147- lifecycle := message.GetLifecycleId()
148- if lifecycle == nil || len(lifecycle.GetValue()) != len(target.LifecycleID) {
117+func exactTargetFromWire(v *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
118+ out, err := protoconv.BrokerExactTargetFromWire(v)
119+ if err != nil {
149120 return protocol.ExactSessionTarget{}, errConvertRange
150121 }
151- copy(target.LifecycleID[:], lifecycle.GetValue())
152- target.SessionName = message.GetSessionName()
153- if err := target.Validate(); err != nil {
154- return protocol.ExactSessionTarget{}, errConvertRange
122+ return out, nil
123+}
124+
125+func policyToWire(v ports.BrokerPolicy) *wire.BrokerWirePolicy {
126+ return protoconv.BrokerPolicyToWire(v)
127+}
128+
129+func policyFromWire(v *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
130+ out, err := protoconv.BrokerPolicyFromWire(v)
131+ if err != nil {
132+ return ports.BrokerPolicy{}, mapBrokerConversionError(err)
155133 }
156- return target, nil
134+ return out, nil
157135 }
158136
159-// validateBrokerEnvEntry enforces the per-request environment contract:
160-// valid UTF-8, no control/bidi characters, bounded bytes. The broker never
161-// merges these into its own process environment.
162-func validateBrokerEnvEntry(entry string) error {
163- if len(entry) > ports.BrokerMaxEnvEntryBytes {
137+func mapBrokerConversionError(err error) error {
138+ switch err {
139+ case protoconv.ErrOutOfRange:
140+ return errConvertRange
141+ case protoconv.ErrTooLarge:
164142 return ErrTooLarge
165- }
166- if !utf8.ValidString(entry) {
143+ default:
167144 return ErrInvalidMessage
168145 }
169- for _, r := range entry {
170- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
171- return ErrInvalidMessage
172- }
173- }
174- return nil
175146 }
176147
177-func policyToWire(policy ports.BrokerPolicy) *wire.BrokerWirePolicy {
178- return &wire.BrokerWirePolicy{
179- ProtocolVersion: uint32(policy.ProtocolVersion),
180- CatalogueVersion: uint32(policy.CatalogSchemaVersion),
181- EnvironmentPolicy: uint32(policy.EnvironmentPolicy),
182- Transport: policy.Transport,
183- Trust: policy.Trust,
184- Launch: policy.Launch,
185- Isolation: policy.Isolation,
186- }
148+func validateBrokerDisplayText(v string, max int) error {
149+ return mapOptionalBrokerError(protoconv.BrokerDisplayText(v, max))
187150 }
188151
189-func policyFromWire(message *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
190- var policy ports.BrokerPolicy
191- if message == nil {
192- return policy, errConvertRange
193- }
194- protocolVersion, err := brokerEnum16[uint16](message.GetProtocolVersion())
195- if err != nil {
196- return ports.BrokerPolicy{}, err
197- }
198- catalogueVersion, err := brokerEnum16[uint16](message.GetCatalogueVersion())
199- if err != nil {
200- return ports.BrokerPolicy{}, err
201- }
202- env, err := brokerEnum8[protocol.EnvironmentPolicy](message.GetEnvironmentPolicy())
203- if err != nil {
204- return ports.BrokerPolicy{}, err
205- }
206- policy = ports.BrokerPolicy{
207- ProtocolVersion: protocolVersion,
208- CatalogSchemaVersion: catalogueVersion,
209- EnvironmentPolicy: env,
210- Transport: message.GetTransport(),
211- Trust: message.GetTrust(),
212- Launch: message.GetLaunch(),
213- Isolation: message.GetIsolation(),
214- }
215- if err := policy.Validate(); err != nil {
216- return ports.BrokerPolicy{}, ErrInvalidMessage
217- }
218- return policy, nil
152+func validateBrokerEnvEntry(v string) error {
153+ return mapOptionalBrokerError(protoconv.BrokerEnvEntry(v))
219154 }
220155
221-// validateBrokerDisplayText enforces bounded, presentation-safe text:
222-// valid UTF-8, no control/bidi/line-separator characters, within maxBytes.
223-func validateBrokerDisplayText(value string, maxBytes int) error {
224- if len(value) > maxBytes {
225- return ErrTooLarge
226- }
227- if !utf8.ValidString(value) {
228- return ErrInvalidMessage
229- }
230- for _, r := range value {
231- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
232- return ErrInvalidMessage
233- }
156+func mapOptionalBrokerError(err error) error {
157+ if err == nil {
158+ return nil
234159 }
235- return nil
160+ return mapBrokerConversionError(err)
236161 }
237162
238163 // validateBrokerEndpoint enforces the SSH target contract shared by
@@ -465,19 +390,7 @@ func validateSnapshotDaemonFields(daemon ports.BrokerDaemonObservation) error {
465390 return nil
466391 }
467392
468-// checkEnvEntries enforces the per-request environment bound: at most
469-// BrokerMaxEnvEntries entries, each valid and bounded.
393+// checkEnvEntries keeps the local error taxonomy at the adapter boundary.
470394 func checkEnvEntries(env []string) error {
471- if len(env) > ports.BrokerMaxEnvEntries {
472- return ErrTooLarge
473- }
474- for _, entry := range env {
475- if err := validateBrokerEnvEntry(entry); err != nil {
476- return err
477- }
478- if !strings.Contains(entry, "=") {
479- return ErrInvalidMessage
480- }
481- }
482- return nil
395+ return mapOptionalBrokerError(protoconv.BrokerEnvEntries(env))
483396 }
@@ -12,10 +12,8 @@ package brokerwire
12 12
13 import (13 import (
14 "math"14 "math"
15- "strings"
16- "unicode"
17- "unicode/utf8"
18 15
16+ "github.com/bnema/vev/internal/adapters/protoconv"
19 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
20 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
21 "github.com/bnema/vev/internal/protocol"19 "github.com/bnema/vev/internal/protocol"
@@ -23,22 +21,22 @@ import (
23 "github.com/bnema/vev/internal/protocol/wire"21 "github.com/bnema/vev/internal/protocol/wire"
24 )22 )
25 23
26-// brokerEnum8 narrows one wire uint32 into an 8-bit semantic enum. Values24+// brokerEnum8 maps shared narrowing failures to the local range sentinel.
27-// whose high bits would otherwise truncate into a valid enum range are
28-// refused before any cast.
29 func brokerEnum8[T ~uint8](value uint32) (T, error) {25 func brokerEnum8[T ~uint8](value uint32) (T, error) {
30- if value > math.MaxUint8 {26+ v, err := protoconv.Uint8[T](value)
27+ if err != nil {
31 return 0, errConvertRange28 return 0, errConvertRange
32 }29 }
33- return T(value), nil30+ return v, nil
34 }31 }
35 32
36-// brokerEnum16 narrows one wire uint32 into a 16-bit semantic value.33+// brokerEnum16 maps shared narrowing failures to the local range sentinel.
37 func brokerEnum16[T ~uint16](value uint32) (T, error) {34 func brokerEnum16[T ~uint16](value uint32) (T, error) {
38- if value > math.MaxUint16 {35+ v, err := protoconv.Uint16[T](value)
36+ if err != nil {
39 return 0, errConvertRange37 return 0, errConvertRange
40 }38 }
41- return T(value), nil39+ return v, nil
42 }40 }
43 41
44 func scopeToWire(epoch ports.BrokerEpoch, connection ports.BrokerConnectionID) *wire.BrokerScope {42 func scopeToWire(epoch ports.BrokerEpoch, connection ports.BrokerConnectionID) *wire.BrokerScope {
@@ -101,138 +99,65 @@ func operationFromWire(raw []byte) (ports.BrokerOperationID, error) {
101 return id, nil99 return id, nil
102 }100 }
103 101
104-func registrationToWire(registration domain.RemoteRegistration) *wire.RemoteRegistration {102+func registrationToWire(v domain.RemoteRegistration) *wire.RemoteRegistration {
105- incarnation := append([]byte(nil), registration.Incarnation[:]...)103+ return protoconv.BrokerRegistrationToWire(v)
106- return &wire.RemoteRegistration{
107- Endpoint: registration.Endpoint,
108- Incarnation: incarnation,
109- Generation: uint64(registration.Generation),
110- }
111 }104 }
112-105+func registrationFromWire(v *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
113-func registrationFromWire(message *wire.RemoteRegistration) (domain.RemoteRegistration, error) {106+ out, err := protoconv.BrokerRegistrationFromWire(v)
114- var registration domain.RemoteRegistration107+ if err != nil {
115- if message == nil {
116- return registration, errConvertRange
117- }
118- registration.Endpoint = message.GetEndpoint()
119- if err := domain.ValidateRemoteHostTarget(registration.Endpoint); err != nil {
120- return domain.RemoteRegistration{}, errConvertRange
121- }
122- raw := message.GetIncarnation()
123- if len(raw) != len(registration.Incarnation) {
124- return domain.RemoteRegistration{}, errConvertRange
125- }
126- copy(registration.Incarnation[:], raw)
127- registration.Generation = domain.RemoteGeneration(message.GetGeneration())
128- if err := registration.Validate(); err != nil {
129 return domain.RemoteRegistration{}, errConvertRange108 return domain.RemoteRegistration{}, errConvertRange
130 }109 }
131- return registration, nil110+ return out, nil
132 }111 }
133 112
134-func exactTargetToWire(target protocol.ExactSessionTarget) *wire.ExactTarget {113+func exactTargetToWire(v protocol.ExactSessionTarget) *wire.ExactTarget {
135- lifecycle := append([]byte(nil), target.LifecycleID[:]...)114+ return protoconv.BrokerExactTargetToWire(v)
136- return &wire.ExactTarget{
137- LifecycleId: &wire.LifecycleID{Value: lifecycle},
138- SessionName: target.SessionName,
139- }
140 }115 }
141 116
142-func exactTargetFromWire(message *wire.ExactTarget) (protocol.ExactSessionTarget, error) {117+func exactTargetFromWire(v *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
143- var target protocol.ExactSessionTarget118+ out, err := protoconv.BrokerExactTargetFromWire(v)
144- if message == nil {119+ if err != nil {
145- return target, errConvertRange
146- }
147- lifecycle := message.GetLifecycleId()
148- if lifecycle == nil || len(lifecycle.GetValue()) != len(target.LifecycleID) {
149 return protocol.ExactSessionTarget{}, errConvertRange120 return protocol.ExactSessionTarget{}, errConvertRange
150 }121 }
151- copy(target.LifecycleID[:], lifecycle.GetValue())122+ return out, nil
152- target.SessionName = message.GetSessionName()123+}
153- if err := target.Validate(); err != nil {124+
154- return protocol.ExactSessionTarget{}, errConvertRange125+func policyToWire(v ports.BrokerPolicy) *wire.BrokerWirePolicy {
126+ return protoconv.BrokerPolicyToWire(v)
127+}
128+
129+func policyFromWire(v *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
130+ out, err := protoconv.BrokerPolicyFromWire(v)
131+ if err != nil {
132+ return ports.BrokerPolicy{}, mapBrokerConversionError(err)
155 }133 }
156- return target, nil134+ return out, nil
157 }135 }
158 136
159-// validateBrokerEnvEntry enforces the per-request environment contract:137+func mapBrokerConversionError(err error) error {
160-// valid UTF-8, no control/bidi characters, bounded bytes. The broker never138+ switch err {
161-// merges these into its own process environment.139+ case protoconv.ErrOutOfRange:
162-func validateBrokerEnvEntry(entry string) error {140+ return errConvertRange
163- if len(entry) > ports.BrokerMaxEnvEntryBytes {141+ case protoconv.ErrTooLarge:
164 return ErrTooLarge142 return ErrTooLarge
165- }143+ default:
166- if !utf8.ValidString(entry) {
167 return ErrInvalidMessage144 return ErrInvalidMessage
168 }145 }
169- for _, r := range entry {
170- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
171- return ErrInvalidMessage
172- }
173- }
174- return nil
175 }146 }
176 147
177-func policyToWire(policy ports.BrokerPolicy) *wire.BrokerWirePolicy {148+func validateBrokerDisplayText(v string, max int) error {
178- return &wire.BrokerWirePolicy{149+ return mapOptionalBrokerError(protoconv.BrokerDisplayText(v, max))
179- ProtocolVersion: uint32(policy.ProtocolVersion),
180- CatalogueVersion: uint32(policy.CatalogSchemaVersion),
181- EnvironmentPolicy: uint32(policy.EnvironmentPolicy),
182- Transport: policy.Transport,
183- Trust: policy.Trust,
184- Launch: policy.Launch,
185- Isolation: policy.Isolation,
186- }
187 }150 }
188 151
189-func policyFromWire(message *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {152+func validateBrokerEnvEntry(v string) error {
190- var policy ports.BrokerPolicy153+ return mapOptionalBrokerError(protoconv.BrokerEnvEntry(v))
191- if message == nil {
192- return policy, errConvertRange
193- }
194- protocolVersion, err := brokerEnum16[uint16](message.GetProtocolVersion())
195- if err != nil {
196- return ports.BrokerPolicy{}, err
197- }
198- catalogueVersion, err := brokerEnum16[uint16](message.GetCatalogueVersion())
199- if err != nil {
200- return ports.BrokerPolicy{}, err
201- }
202- env, err := brokerEnum8[protocol.EnvironmentPolicy](message.GetEnvironmentPolicy())
203- if err != nil {
204- return ports.BrokerPolicy{}, err
205- }
206- policy = ports.BrokerPolicy{
207- ProtocolVersion: protocolVersion,
208- CatalogSchemaVersion: catalogueVersion,
209- EnvironmentPolicy: env,
210- Transport: message.GetTransport(),
211- Trust: message.GetTrust(),
212- Launch: message.GetLaunch(),
213- Isolation: message.GetIsolation(),
214- }
215- if err := policy.Validate(); err != nil {
216- return ports.BrokerPolicy{}, ErrInvalidMessage
217- }
218- return policy, nil
219 }154 }
220 155
221-// validateBrokerDisplayText enforces bounded, presentation-safe text:156+func mapOptionalBrokerError(err error) error {
222-// valid UTF-8, no control/bidi/line-separator characters, within maxBytes.157+ if err == nil {
223-func validateBrokerDisplayText(value string, maxBytes int) error {158+ return nil
224- if len(value) > maxBytes {
225- return ErrTooLarge
226- }
227- if !utf8.ValidString(value) {
228- return ErrInvalidMessage
229- }
230- for _, r := range value {
231- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
232- return ErrInvalidMessage
233- }
234 }159 }
235- return nil160+ return mapBrokerConversionError(err)
236 }161 }
237 162
238 // validateBrokerEndpoint enforces the SSH target contract shared by163 // validateBrokerEndpoint enforces the SSH target contract shared by
@@ -465,19 +390,7 @@ func validateSnapshotDaemonFields(daemon ports.BrokerDaemonObservation) error {
465 return nil390 return nil
466 }391 }
467 392
468-// checkEnvEntries enforces the per-request environment bound: at most393+// checkEnvEntries keeps the local error taxonomy at the adapter boundary.
469-// BrokerMaxEnvEntries entries, each valid and bounded.
470 func checkEnvEntries(env []string) error {394 func checkEnvEntries(env []string) error {
471- if len(env) > ports.BrokerMaxEnvEntries {395+ return mapOptionalBrokerError(protoconv.BrokerEnvEntries(env))
472- return ErrTooLarge
473- }
474- for _, entry := range env {
475- if err := validateBrokerEnvEntry(entry); err != nil {
476- return err
477- }
478- if !strings.Contains(entry, "=") {
479- return ErrInvalidMessage
480- }
481- }
482- return nil
483 }396 }
M internal/adapters/daemonmux/convert.go +57 -161
@@ -15,59 +15,45 @@ package daemonmux
1515 // directly, so the two conversations stay independently evolvable.
1616
1717 import (
18- "math"
19- "strings"
20- "unicode"
21- "unicode/utf8"
22-
18+ "github.com/bnema/vev/internal/adapters/protoconv"
2319 "github.com/bnema/vev/internal/domain"
2420 "github.com/bnema/vev/internal/ports"
2521 "github.com/bnema/vev/internal/protocol"
2622 "github.com/bnema/vev/internal/protocol/wire"
2723 )
2824
29-// muxEnum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose
30-// high bits would otherwise truncate into a valid enum range are refused
31-// before any cast.
25+// muxEnum8 maps shared narrowing failures to the local range sentinel.
3226 func muxEnum8[T ~uint8](value uint32) (T, error) {
33- if value > math.MaxUint8 {
27+ v, err := protoconv.Uint8[T](value)
28+ if err != nil {
3429 return 0, errConvertRange
3530 }
36- return T(value), nil
31+ return v, nil
3732 }
3833
39-// muxEnum16 narrows one wire uint32 into a 16-bit semantic value.
34+// muxEnum16 maps shared narrowing failures to the local range sentinel.
4035 func muxEnum16[T ~uint16](value uint32) (T, error) {
41- if value > math.MaxUint16 {
36+ v, err := protoconv.Uint16[T](value)
37+ if err != nil {
4238 return 0, errConvertRange
4339 }
44- return T(value), nil
40+ return v, nil
4541 }
4642
47-// startModeToWire maps the closed daemon-start taxonomy onto its wire code.
48-// The zero value is refused rather than encoded: a peer must never have to
49-// guess whether an absent mode authorized a spawn.
5043 func startModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {
51- switch mode {
52- case ports.BrokerDaemonExistingOnly:
53- return 1, nil
54- case ports.BrokerDaemonStartIfNeeded:
55- return 2, nil
56- default:
44+ v, err := protoconv.BrokerStartModeToWire(mode)
45+ if err != nil {
5746 return 0, errConvertRange
5847 }
48+ return v, nil
5949 }
6050
61-// startModeFromWire maps a wire daemon-start code onto the closed taxonomy.
6251 func startModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {
63- switch value {
64- case 1:
65- return ports.BrokerDaemonExistingOnly, nil
66- case 2:
67- return ports.BrokerDaemonStartIfNeeded, nil
68- default:
52+ v, err := protoconv.BrokerStartModeFromWire(value)
53+ if err != nil {
6954 return 0, errConvertRange
7055 }
56+ return v, nil
7157 }
7258
7359 func physicalToWire(id PhysicalStreamID) uint64 { return uint64(id) }
@@ -121,105 +107,63 @@ func refFromWire(message *wire.MuxStreamRef) (StreamRef, error) {
121107 }, nil
122108 }
123109
124-// registrationToWire mirrors domain.RemoteRegistration: an endpoint, an exact
125-// 16-byte incarnation, and a generation.
126-func registrationToWire(registration domain.RemoteRegistration) *wire.RemoteRegistration {
127- incarnation := append([]byte(nil), registration.Incarnation[:]...)
128- return &wire.RemoteRegistration{
129- Endpoint: registration.Endpoint,
130- Incarnation: incarnation,
131- Generation: uint64(registration.Generation),
132- }
110+func registrationToWire(v domain.RemoteRegistration) *wire.RemoteRegistration {
111+ return protoconv.BrokerRegistrationToWire(v)
133112 }
134-
135-func registrationFromWire(message *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
136- var registration domain.RemoteRegistration
137- if message == nil {
138- return registration, errConvertRange
139- }
140- registration.Endpoint = message.GetEndpoint()
141- if err := domain.ValidateRemoteHostTarget(registration.Endpoint); err != nil {
142- return domain.RemoteRegistration{}, errConvertRange
143- }
144- raw := message.GetIncarnation()
145- if len(raw) != len(registration.Incarnation) {
146- return domain.RemoteRegistration{}, errConvertRange
147- }
148- copy(registration.Incarnation[:], raw)
149- registration.Generation = domain.RemoteGeneration(message.GetGeneration())
150- if err := registration.Validate(); err != nil {
113+func registrationFromWire(v *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
114+ out, err := protoconv.BrokerRegistrationFromWire(v)
115+ if err != nil {
151116 return domain.RemoteRegistration{}, errConvertRange
152117 }
153- return registration, nil
118+ return out, nil
154119 }
155120
156-func exactTargetToWire(target protocol.ExactSessionTarget) *wire.ExactTarget {
157- lifecycle := append([]byte(nil), target.LifecycleID[:]...)
158- return &wire.ExactTarget{
159- LifecycleId: &wire.LifecycleID{Value: lifecycle},
160- SessionName: target.SessionName,
161- }
121+func exactTargetToWire(v protocol.ExactSessionTarget) *wire.ExactTarget {
122+ return protoconv.BrokerExactTargetToWire(v)
162123 }
163124
164-func exactTargetFromWire(message *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
165- var target protocol.ExactSessionTarget
166- if message == nil {
167- return target, errConvertRange
168- }
169- lifecycle := message.GetLifecycleId()
170- if lifecycle == nil || len(lifecycle.GetValue()) != len(target.LifecycleID) {
171- return protocol.ExactSessionTarget{}, errConvertRange
172- }
173- copy(target.LifecycleID[:], lifecycle.GetValue())
174- target.SessionName = message.GetSessionName()
175- if err := target.Validate(); err != nil {
125+func exactTargetFromWire(v *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
126+ out, err := protoconv.BrokerExactTargetFromWire(v)
127+ if err != nil {
176128 return protocol.ExactSessionTarget{}, errConvertRange
177129 }
178- return target, nil
130+ return out, nil
179131 }
180132
181-func policyToWire(policy ports.BrokerPolicy) *wire.BrokerWirePolicy {
182- return &wire.BrokerWirePolicy{
183- ProtocolVersion: uint32(policy.ProtocolVersion),
184- CatalogueVersion: uint32(policy.CatalogSchemaVersion),
185- EnvironmentPolicy: uint32(policy.EnvironmentPolicy),
186- Transport: policy.Transport,
187- Trust: policy.Trust,
188- Launch: policy.Launch,
189- Isolation: policy.Isolation,
190- }
133+func policyToWire(v ports.BrokerPolicy) *wire.BrokerWirePolicy {
134+ return protoconv.BrokerPolicyToWire(v)
191135 }
192136
193-func policyFromWire(message *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
194- var policy ports.BrokerPolicy
195- if message == nil {
196- return policy, errConvertRange
197- }
198- protocolVersion, err := muxEnum16[uint16](message.GetProtocolVersion())
199- if err != nil {
200- return ports.BrokerPolicy{}, err
201- }
202- catalogueVersion, err := muxEnum16[uint16](message.GetCatalogueVersion())
203- if err != nil {
204- return ports.BrokerPolicy{}, err
205- }
206- env, err := muxEnum8[protocol.EnvironmentPolicy](message.GetEnvironmentPolicy())
137+func policyFromWire(v *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
138+ out, err := protoconv.BrokerPolicyFromWire(v)
207139 if err != nil {
208- return ports.BrokerPolicy{}, err
140+ return ports.BrokerPolicy{}, mapBrokerConversionError(err)
209141 }
210- policy = ports.BrokerPolicy{
211- ProtocolVersion: protocolVersion,
212- CatalogSchemaVersion: catalogueVersion,
213- EnvironmentPolicy: env,
214- Transport: message.GetTransport(),
215- Trust: message.GetTrust(),
216- Launch: message.GetLaunch(),
217- Isolation: message.GetIsolation(),
142+ return out, nil
143+}
144+
145+func mapBrokerConversionError(err error) error {
146+ switch err {
147+ case protoconv.ErrOutOfRange:
148+ return errConvertRange
149+ case protoconv.ErrTooLarge:
150+ return ErrTooLarge
151+ default:
152+ return ErrInvalidMessage
218153 }
219- if err := policy.Validate(); err != nil {
220- return ports.BrokerPolicy{}, ErrInvalidMessage
154+}
155+
156+func validateDisplayText(v string, max int) error {
157+ return mapOptionalBrokerError(protoconv.BrokerDisplayText(v, max))
158+}
159+
160+func validateEnvEntry(v string) error { return mapOptionalBrokerError(protoconv.BrokerEnvEntry(v)) }
161+
162+func mapOptionalBrokerError(err error) error {
163+ if err == nil {
164+ return nil
221165 }
222- return policy, nil
166+ return mapBrokerConversionError(err)
223167 }
224168
225169 func errorDetailToWire(detail ErrorDetail) *wire.BrokerErrorDetail {
@@ -268,55 +212,7 @@ func errorDetailFromWire(message *wire.BrokerErrorDetail) (ErrorDetail, error) {
268212 return detail, nil
269213 }
270214
271-// validateDisplayText enforces bounded, presentation-safe text: valid UTF-8,
272-// no control/bidi/line-separator characters, within maxBytes.
273-func validateDisplayText(value string, maxBytes int) error {
274- if len(value) > maxBytes {
275- return ErrTooLarge
276- }
277- if !utf8.ValidString(value) {
278- return ErrInvalidMessage
279- }
280- for _, r := range value {
281- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
282- return ErrInvalidMessage
283- }
284- }
285- return nil
286-}
287-
288-// validateEnvEntry enforces the per-request environment contract: valid
289-// UTF-8, no control/bidi characters, bounded bytes. The daemonmux client
290-// never merges these into the broker or daemon process environment.
291-func validateEnvEntry(entry string) error {
292- if len(entry) > ports.BrokerMaxEnvEntryBytes {
293- return ErrTooLarge
294- }
295- if !utf8.ValidString(entry) {
296- return ErrInvalidMessage
297- }
298- for _, r := range entry {
299- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
300- return ErrInvalidMessage
301- }
302- }
303- return nil
304-}
305-
306-// checkEnvEntries enforces the per-request environment bound: at most
307-// BrokerMaxEnvEntries entries, each valid, bounded, and an explicit
308-// KEY=VALUE assignment.
215+// checkEnvEntries keeps the local error taxonomy at the adapter boundary.
309216 func checkEnvEntries(env []string) error {
310- if len(env) > ports.BrokerMaxEnvEntries {
311- return ErrTooLarge
312- }
313- for _, entry := range env {
314- if err := validateEnvEntry(entry); err != nil {
315- return err
316- }
317- if !strings.Contains(entry, "=") {
318- return ErrInvalidMessage
319- }
320- }
321- return nil
217+ return mapOptionalBrokerError(protoconv.BrokerEnvEntries(env))
322218 }
@@ -15,59 +15,45 @@ package daemonmux
15 // directly, so the two conversations stay independently evolvable.15 // directly, so the two conversations stay independently evolvable.
16 16
17 import (17 import (
18- "math"18+ "github.com/bnema/vev/internal/adapters/protoconv"
19- "strings"
20- "unicode"
21- "unicode/utf8"
22-
23 "github.com/bnema/vev/internal/domain"19 "github.com/bnema/vev/internal/domain"
24 "github.com/bnema/vev/internal/ports"20 "github.com/bnema/vev/internal/ports"
25 "github.com/bnema/vev/internal/protocol"21 "github.com/bnema/vev/internal/protocol"
26 "github.com/bnema/vev/internal/protocol/wire"22 "github.com/bnema/vev/internal/protocol/wire"
27 )23 )
28 24
29-// muxEnum8 narrows one wire uint32 into an 8-bit semantic enum. Values whose25+// muxEnum8 maps shared narrowing failures to the local range sentinel.
30-// high bits would otherwise truncate into a valid enum range are refused
31-// before any cast.
32 func muxEnum8[T ~uint8](value uint32) (T, error) {26 func muxEnum8[T ~uint8](value uint32) (T, error) {
33- if value > math.MaxUint8 {27+ v, err := protoconv.Uint8[T](value)
28+ if err != nil {
34 return 0, errConvertRange29 return 0, errConvertRange
35 }30 }
36- return T(value), nil31+ return v, nil
37 }32 }
38 33
39-// muxEnum16 narrows one wire uint32 into a 16-bit semantic value.34+// muxEnum16 maps shared narrowing failures to the local range sentinel.
40 func muxEnum16[T ~uint16](value uint32) (T, error) {35 func muxEnum16[T ~uint16](value uint32) (T, error) {
41- if value > math.MaxUint16 {36+ v, err := protoconv.Uint16[T](value)
37+ if err != nil {
42 return 0, errConvertRange38 return 0, errConvertRange
43 }39 }
44- return T(value), nil40+ return v, nil
45 }41 }
46 42
47-// startModeToWire maps the closed daemon-start taxonomy onto its wire code.
48-// The zero value is refused rather than encoded: a peer must never have to
49-// guess whether an absent mode authorized a spawn.
50 func startModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {43 func startModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {
51- switch mode {44+ v, err := protoconv.BrokerStartModeToWire(mode)
52- case ports.BrokerDaemonExistingOnly:45+ if err != nil {
53- return 1, nil
54- case ports.BrokerDaemonStartIfNeeded:
55- return 2, nil
56- default:
57 return 0, errConvertRange46 return 0, errConvertRange
58 }47 }
48+ return v, nil
59 }49 }
60 50
61-// startModeFromWire maps a wire daemon-start code onto the closed taxonomy.
62 func startModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {51 func startModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {
63- switch value {52+ v, err := protoconv.BrokerStartModeFromWire(value)
64- case 1:53+ if err != nil {
65- return ports.BrokerDaemonExistingOnly, nil
66- case 2:
67- return ports.BrokerDaemonStartIfNeeded, nil
68- default:
69 return 0, errConvertRange54 return 0, errConvertRange
70 }55 }
56+ return v, nil
71 }57 }
72 58
73 func physicalToWire(id PhysicalStreamID) uint64 { return uint64(id) }59 func physicalToWire(id PhysicalStreamID) uint64 { return uint64(id) }
@@ -121,105 +107,63 @@ func refFromWire(message *wire.MuxStreamRef) (StreamRef, error) {
121 }, nil107 }, nil
122 }108 }
123 109
124-// registrationToWire mirrors domain.RemoteRegistration: an endpoint, an exact110+func registrationToWire(v domain.RemoteRegistration) *wire.RemoteRegistration {
125-// 16-byte incarnation, and a generation.111+ return protoconv.BrokerRegistrationToWire(v)
126-func registrationToWire(registration domain.RemoteRegistration) *wire.RemoteRegistration {
127- incarnation := append([]byte(nil), registration.Incarnation[:]...)
128- return &wire.RemoteRegistration{
129- Endpoint: registration.Endpoint,
130- Incarnation: incarnation,
131- Generation: uint64(registration.Generation),
132- }
133 }112 }
134-113+func registrationFromWire(v *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
135-func registrationFromWire(message *wire.RemoteRegistration) (domain.RemoteRegistration, error) {114+ out, err := protoconv.BrokerRegistrationFromWire(v)
136- var registration domain.RemoteRegistration115+ if err != nil {
137- if message == nil {
138- return registration, errConvertRange
139- }
140- registration.Endpoint = message.GetEndpoint()
141- if err := domain.ValidateRemoteHostTarget(registration.Endpoint); err != nil {
142- return domain.RemoteRegistration{}, errConvertRange
143- }
144- raw := message.GetIncarnation()
145- if len(raw) != len(registration.Incarnation) {
146- return domain.RemoteRegistration{}, errConvertRange
147- }
148- copy(registration.Incarnation[:], raw)
149- registration.Generation = domain.RemoteGeneration(message.GetGeneration())
150- if err := registration.Validate(); err != nil {
151 return domain.RemoteRegistration{}, errConvertRange116 return domain.RemoteRegistration{}, errConvertRange
152 }117 }
153- return registration, nil118+ return out, nil
154 }119 }
155 120
156-func exactTargetToWire(target protocol.ExactSessionTarget) *wire.ExactTarget {121+func exactTargetToWire(v protocol.ExactSessionTarget) *wire.ExactTarget {
157- lifecycle := append([]byte(nil), target.LifecycleID[:]...)122+ return protoconv.BrokerExactTargetToWire(v)
158- return &wire.ExactTarget{
159- LifecycleId: &wire.LifecycleID{Value: lifecycle},
160- SessionName: target.SessionName,
161- }
162 }123 }
163 124
164-func exactTargetFromWire(message *wire.ExactTarget) (protocol.ExactSessionTarget, error) {125+func exactTargetFromWire(v *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
165- var target protocol.ExactSessionTarget126+ out, err := protoconv.BrokerExactTargetFromWire(v)
166- if message == nil {127+ if err != nil {
167- return target, errConvertRange
168- }
169- lifecycle := message.GetLifecycleId()
170- if lifecycle == nil || len(lifecycle.GetValue()) != len(target.LifecycleID) {
171- return protocol.ExactSessionTarget{}, errConvertRange
172- }
173- copy(target.LifecycleID[:], lifecycle.GetValue())
174- target.SessionName = message.GetSessionName()
175- if err := target.Validate(); err != nil {
176 return protocol.ExactSessionTarget{}, errConvertRange128 return protocol.ExactSessionTarget{}, errConvertRange
177 }129 }
178- return target, nil130+ return out, nil
179 }131 }
180 132
181-func policyToWire(policy ports.BrokerPolicy) *wire.BrokerWirePolicy {133+func policyToWire(v ports.BrokerPolicy) *wire.BrokerWirePolicy {
182- return &wire.BrokerWirePolicy{134+ return protoconv.BrokerPolicyToWire(v)
183- ProtocolVersion: uint32(policy.ProtocolVersion),
184- CatalogueVersion: uint32(policy.CatalogSchemaVersion),
185- EnvironmentPolicy: uint32(policy.EnvironmentPolicy),
186- Transport: policy.Transport,
187- Trust: policy.Trust,
188- Launch: policy.Launch,
189- Isolation: policy.Isolation,
190- }
191 }135 }
192 136
193-func policyFromWire(message *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {137+func policyFromWire(v *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
194- var policy ports.BrokerPolicy138+ out, err := protoconv.BrokerPolicyFromWire(v)
195- if message == nil {
196- return policy, errConvertRange
197- }
198- protocolVersion, err := muxEnum16[uint16](message.GetProtocolVersion())
199- if err != nil {
200- return ports.BrokerPolicy{}, err
201- }
202- catalogueVersion, err := muxEnum16[uint16](message.GetCatalogueVersion())
203- if err != nil {
204- return ports.BrokerPolicy{}, err
205- }
206- env, err := muxEnum8[protocol.EnvironmentPolicy](message.GetEnvironmentPolicy())
207 if err != nil {139 if err != nil {
208- return ports.BrokerPolicy{}, err140+ return ports.BrokerPolicy{}, mapBrokerConversionError(err)
209 }141 }
210- policy = ports.BrokerPolicy{142+ return out, nil
211- ProtocolVersion: protocolVersion,143+}
212- CatalogSchemaVersion: catalogueVersion,144+
213- EnvironmentPolicy: env,145+func mapBrokerConversionError(err error) error {
214- Transport: message.GetTransport(),146+ switch err {
215- Trust: message.GetTrust(),147+ case protoconv.ErrOutOfRange:
216- Launch: message.GetLaunch(),148+ return errConvertRange
217- Isolation: message.GetIsolation(),149+ case protoconv.ErrTooLarge:
150+ return ErrTooLarge
151+ default:
152+ return ErrInvalidMessage
218 }153 }
219- if err := policy.Validate(); err != nil {154+}
220- return ports.BrokerPolicy{}, ErrInvalidMessage155+
156+func validateDisplayText(v string, max int) error {
157+ return mapOptionalBrokerError(protoconv.BrokerDisplayText(v, max))
158+}
159+
160+func validateEnvEntry(v string) error { return mapOptionalBrokerError(protoconv.BrokerEnvEntry(v)) }
161+
162+func mapOptionalBrokerError(err error) error {
163+ if err == nil {
164+ return nil
221 }165 }
222- return policy, nil166+ return mapBrokerConversionError(err)
223 }167 }
224 168
225 func errorDetailToWire(detail ErrorDetail) *wire.BrokerErrorDetail {169 func errorDetailToWire(detail ErrorDetail) *wire.BrokerErrorDetail {
@@ -268,55 +212,7 @@ func errorDetailFromWire(message *wire.BrokerErrorDetail) (ErrorDetail, error) {
268 return detail, nil212 return detail, nil
269 }213 }
270 214
271-// validateDisplayText enforces bounded, presentation-safe text: valid UTF-8,215+// checkEnvEntries keeps the local error taxonomy at the adapter boundary.
272-// no control/bidi/line-separator characters, within maxBytes.
273-func validateDisplayText(value string, maxBytes int) error {
274- if len(value) > maxBytes {
275- return ErrTooLarge
276- }
277- if !utf8.ValidString(value) {
278- return ErrInvalidMessage
279- }
280- for _, r := range value {
281- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
282- return ErrInvalidMessage
283- }
284- }
285- return nil
286-}
287-
288-// validateEnvEntry enforces the per-request environment contract: valid
289-// UTF-8, no control/bidi characters, bounded bytes. The daemonmux client
290-// never merges these into the broker or daemon process environment.
291-func validateEnvEntry(entry string) error {
292- if len(entry) > ports.BrokerMaxEnvEntryBytes {
293- return ErrTooLarge
294- }
295- if !utf8.ValidString(entry) {
296- return ErrInvalidMessage
297- }
298- for _, r := range entry {
299- if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
300- return ErrInvalidMessage
301- }
302- }
303- return nil
304-}
305-
306-// checkEnvEntries enforces the per-request environment bound: at most
307-// BrokerMaxEnvEntries entries, each valid, bounded, and an explicit
308-// KEY=VALUE assignment.
309 func checkEnvEntries(env []string) error {216 func checkEnvEntries(env []string) error {
310- if len(env) > ports.BrokerMaxEnvEntries {217+ return mapOptionalBrokerError(protoconv.BrokerEnvEntries(env))
311- return ErrTooLarge
312- }
313- for _, entry := range env {
314- if err := validateEnvEntry(entry); err != nil {
315- return err
316- }
317- if !strings.Contains(entry, "=") {
318- return ErrInvalidMessage
319- }
320- }
321- return nil
322 }218 }
A internal/adapters/protoconv/broker_convert.go +149 -0
@@ -0,0 +1,149 @@
1+package protoconv
2+
3+import (
4+ "errors"
5+ "strings"
6+ "unicode"
7+ "unicode/utf8"
8+
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
12+ "github.com/bnema/vev/internal/protocol/wire"
13+)
14+
15+// ErrInvalid and ErrTooLarge distinguish semantic and byte-bound failures
16+// from ErrOutOfRange; adapters translate them to their local sentinels.
17+var (
18+ ErrInvalid = errors.New("protoconv: invalid broker value")
19+ ErrTooLarge = errors.New("protoconv: broker value exceeds bound")
20+)
21+
22+func BrokerStartModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {
23+ switch mode {
24+ case ports.BrokerDaemonExistingOnly:
25+ return 1, nil
26+ case ports.BrokerDaemonStartIfNeeded:
27+ return 2, nil
28+ default:
29+ return 0, ErrOutOfRange
30+ }
31+}
32+
33+func BrokerStartModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {
34+ switch value {
35+ case 1:
36+ return ports.BrokerDaemonExistingOnly, nil
37+ case 2:
38+ return ports.BrokerDaemonStartIfNeeded, nil
39+ default:
40+ return 0, ErrOutOfRange
41+ }
42+}
43+
44+func BrokerRegistrationToWire(registration domain.RemoteRegistration) *wire.RemoteRegistration {
45+ return &wire.RemoteRegistration{Endpoint: registration.Endpoint, Incarnation: append([]byte(nil), registration.Incarnation[:]...), Generation: uint64(registration.Generation)}
46+}
47+
48+func BrokerRegistrationFromWire(message *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
49+ var registration domain.RemoteRegistration
50+ if message == nil {
51+ return registration, ErrOutOfRange
52+ }
53+ registration.Endpoint = message.GetEndpoint()
54+ if err := domain.ValidateRemoteHostTarget(registration.Endpoint); err != nil {
55+ return domain.RemoteRegistration{}, ErrOutOfRange
56+ }
57+ raw := message.GetIncarnation()
58+ if len(raw) != len(registration.Incarnation) {
59+ return domain.RemoteRegistration{}, ErrOutOfRange
60+ }
61+ copy(registration.Incarnation[:], raw)
62+ registration.Generation = domain.RemoteGeneration(message.GetGeneration())
63+ if err := registration.Validate(); err != nil {
64+ return domain.RemoteRegistration{}, ErrOutOfRange
65+ }
66+ return registration, nil
67+}
68+
69+func BrokerExactTargetToWire(target protocol.ExactSessionTarget) *wire.ExactTarget {
70+ return &wire.ExactTarget{LifecycleId: &wire.LifecycleID{Value: append([]byte(nil), target.LifecycleID[:]...)}, SessionName: target.SessionName}
71+}
72+
73+func BrokerExactTargetFromWire(message *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
74+ var target protocol.ExactSessionTarget
75+ if message == nil {
76+ return target, ErrOutOfRange
77+ }
78+ lifecycle := message.GetLifecycleId()
79+ if lifecycle == nil || len(lifecycle.GetValue()) != len(target.LifecycleID) {
80+ return protocol.ExactSessionTarget{}, ErrOutOfRange
81+ }
82+ copy(target.LifecycleID[:], lifecycle.GetValue())
83+ target.SessionName = message.GetSessionName()
84+ if err := target.Validate(); err != nil {
85+ return protocol.ExactSessionTarget{}, ErrOutOfRange
86+ }
87+ return target, nil
88+}
89+
90+func BrokerPolicyToWire(policy ports.BrokerPolicy) *wire.BrokerWirePolicy {
91+ return &wire.BrokerWirePolicy{ProtocolVersion: uint32(policy.ProtocolVersion), CatalogueVersion: uint32(policy.CatalogSchemaVersion), EnvironmentPolicy: uint32(policy.EnvironmentPolicy), Transport: policy.Transport, Trust: policy.Trust, Launch: policy.Launch, Isolation: policy.Isolation}
92+}
93+
94+func BrokerPolicyFromWire(message *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
95+ if message == nil {
96+ return ports.BrokerPolicy{}, ErrOutOfRange
97+ }
98+ version, err := Uint16[uint16](message.GetProtocolVersion())
99+ if err != nil {
100+ return ports.BrokerPolicy{}, err
101+ }
102+ catalogue, err := Uint16[uint16](message.GetCatalogueVersion())
103+ if err != nil {
104+ return ports.BrokerPolicy{}, err
105+ }
106+ env, err := Uint8[protocol.EnvironmentPolicy](message.GetEnvironmentPolicy())
107+ if err != nil {
108+ return ports.BrokerPolicy{}, err
109+ }
110+ policy := ports.BrokerPolicy{ProtocolVersion: version, CatalogSchemaVersion: catalogue, EnvironmentPolicy: env, Transport: message.GetTransport(), Trust: message.GetTrust(), Launch: message.GetLaunch(), Isolation: message.GetIsolation()}
111+ if err := policy.Validate(); err != nil {
112+ return ports.BrokerPolicy{}, ErrInvalid
113+ }
114+ return policy, nil
115+}
116+
117+func BrokerDisplayText(value string, maxBytes int) error {
118+ if len(value) > maxBytes {
119+ return ErrTooLarge
120+ }
121+ if !utf8.ValidString(value) {
122+ return ErrInvalid
123+ }
124+ for _, r := range value {
125+ if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
126+ return ErrInvalid
127+ }
128+ }
129+ return nil
130+}
131+
132+func BrokerEnvEntry(entry string) error {
133+ return BrokerDisplayText(entry, ports.BrokerMaxEnvEntryBytes)
134+}
135+
136+func BrokerEnvEntries(env []string) error {
137+ if len(env) > ports.BrokerMaxEnvEntries {
138+ return ErrTooLarge
139+ }
140+ for _, entry := range env {
141+ if err := BrokerEnvEntry(entry); err != nil {
142+ return err
143+ }
144+ if !strings.Contains(entry, "=") {
145+ return ErrInvalid
146+ }
147+ }
148+ return nil
149+}
@@ -0,0 +1,149 @@
1+package protoconv
2+
3+import (
4+ "errors"
5+ "strings"
6+ "unicode"
7+ "unicode/utf8"
8+
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
12+ "github.com/bnema/vev/internal/protocol/wire"
13+)
14+
15+// ErrInvalid and ErrTooLarge distinguish semantic and byte-bound failures
16+// from ErrOutOfRange; adapters translate them to their local sentinels.
17+var (
18+ ErrInvalid = errors.New("protoconv: invalid broker value")
19+ ErrTooLarge = errors.New("protoconv: broker value exceeds bound")
20+)
21+
22+func BrokerStartModeToWire(mode ports.BrokerDaemonStartMode) (uint32, error) {
23+ switch mode {
24+ case ports.BrokerDaemonExistingOnly:
25+ return 1, nil
26+ case ports.BrokerDaemonStartIfNeeded:
27+ return 2, nil
28+ default:
29+ return 0, ErrOutOfRange
30+ }
31+}
32+
33+func BrokerStartModeFromWire(value uint32) (ports.BrokerDaemonStartMode, error) {
34+ switch value {
35+ case 1:
36+ return ports.BrokerDaemonExistingOnly, nil
37+ case 2:
38+ return ports.BrokerDaemonStartIfNeeded, nil
39+ default:
40+ return 0, ErrOutOfRange
41+ }
42+}
43+
44+func BrokerRegistrationToWire(registration domain.RemoteRegistration) *wire.RemoteRegistration {
45+ return &wire.RemoteRegistration{Endpoint: registration.Endpoint, Incarnation: append([]byte(nil), registration.Incarnation[:]...), Generation: uint64(registration.Generation)}
46+}
47+
48+func BrokerRegistrationFromWire(message *wire.RemoteRegistration) (domain.RemoteRegistration, error) {
49+ var registration domain.RemoteRegistration
50+ if message == nil {
51+ return registration, ErrOutOfRange
52+ }
53+ registration.Endpoint = message.GetEndpoint()
54+ if err := domain.ValidateRemoteHostTarget(registration.Endpoint); err != nil {
55+ return domain.RemoteRegistration{}, ErrOutOfRange
56+ }
57+ raw := message.GetIncarnation()
58+ if len(raw) != len(registration.Incarnation) {
59+ return domain.RemoteRegistration{}, ErrOutOfRange
60+ }
61+ copy(registration.Incarnation[:], raw)
62+ registration.Generation = domain.RemoteGeneration(message.GetGeneration())
63+ if err := registration.Validate(); err != nil {
64+ return domain.RemoteRegistration{}, ErrOutOfRange
65+ }
66+ return registration, nil
67+}
68+
69+func BrokerExactTargetToWire(target protocol.ExactSessionTarget) *wire.ExactTarget {
70+ return &wire.ExactTarget{LifecycleId: &wire.LifecycleID{Value: append([]byte(nil), target.LifecycleID[:]...)}, SessionName: target.SessionName}
71+}
72+
73+func BrokerExactTargetFromWire(message *wire.ExactTarget) (protocol.ExactSessionTarget, error) {
74+ var target protocol.ExactSessionTarget
75+ if message == nil {
76+ return target, ErrOutOfRange
77+ }
78+ lifecycle := message.GetLifecycleId()
79+ if lifecycle == nil || len(lifecycle.GetValue()) != len(target.LifecycleID) {
80+ return protocol.ExactSessionTarget{}, ErrOutOfRange
81+ }
82+ copy(target.LifecycleID[:], lifecycle.GetValue())
83+ target.SessionName = message.GetSessionName()
84+ if err := target.Validate(); err != nil {
85+ return protocol.ExactSessionTarget{}, ErrOutOfRange
86+ }
87+ return target, nil
88+}
89+
90+func BrokerPolicyToWire(policy ports.BrokerPolicy) *wire.BrokerWirePolicy {
91+ return &wire.BrokerWirePolicy{ProtocolVersion: uint32(policy.ProtocolVersion), CatalogueVersion: uint32(policy.CatalogSchemaVersion), EnvironmentPolicy: uint32(policy.EnvironmentPolicy), Transport: policy.Transport, Trust: policy.Trust, Launch: policy.Launch, Isolation: policy.Isolation}
92+}
93+
94+func BrokerPolicyFromWire(message *wire.BrokerWirePolicy) (ports.BrokerPolicy, error) {
95+ if message == nil {
96+ return ports.BrokerPolicy{}, ErrOutOfRange
97+ }
98+ version, err := Uint16[uint16](message.GetProtocolVersion())
99+ if err != nil {
100+ return ports.BrokerPolicy{}, err
101+ }
102+ catalogue, err := Uint16[uint16](message.GetCatalogueVersion())
103+ if err != nil {
104+ return ports.BrokerPolicy{}, err
105+ }
106+ env, err := Uint8[protocol.EnvironmentPolicy](message.GetEnvironmentPolicy())
107+ if err != nil {
108+ return ports.BrokerPolicy{}, err
109+ }
110+ policy := ports.BrokerPolicy{ProtocolVersion: version, CatalogSchemaVersion: catalogue, EnvironmentPolicy: env, Transport: message.GetTransport(), Trust: message.GetTrust(), Launch: message.GetLaunch(), Isolation: message.GetIsolation()}
111+ if err := policy.Validate(); err != nil {
112+ return ports.BrokerPolicy{}, ErrInvalid
113+ }
114+ return policy, nil
115+}
116+
117+func BrokerDisplayText(value string, maxBytes int) error {
118+ if len(value) > maxBytes {
119+ return ErrTooLarge
120+ }
121+ if !utf8.ValidString(value) {
122+ return ErrInvalid
123+ }
124+ for _, r := range value {
125+ if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
126+ return ErrInvalid
127+ }
128+ }
129+ return nil
130+}
131+
132+func BrokerEnvEntry(entry string) error {
133+ return BrokerDisplayText(entry, ports.BrokerMaxEnvEntryBytes)
134+}
135+
136+func BrokerEnvEntries(env []string) error {
137+ if len(env) > ports.BrokerMaxEnvEntries {
138+ return ErrTooLarge
139+ }
140+ for _, entry := range env {
141+ if err := BrokerEnvEntry(entry); err != nil {
142+ return err
143+ }
144+ if !strings.Contains(entry, "=") {
145+ return ErrInvalid
146+ }
147+ }
148+ return nil
149+}
A internal/adapters/protoconv/broker_convert_test.go +112 -0
@@ -0,0 +1,112 @@
1+package protoconv
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/catalogue"
11+ "github.com/bnema/vev/internal/protocol/wire"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestBrokerConversionRoundTrips(t *testing.T) {
16+ registration := domain.RemoteRegistration{Endpoint: "dev@host:22", Incarnation: [16]byte{1, 2, 3}, Generation: 9}
17+ target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1, 2, 3}, SessionName: "work"}
18+ policy := ports.BrokerPolicy{ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "pinned", Launch: "agent", Isolation: "user"}
19+ for _, tc := range []struct {
20+ name string
21+ run func(*testing.T)
22+ }{
23+ {"registration", func(t *testing.T) {
24+ encoded := BrokerRegistrationToWire(registration)
25+ got, err := BrokerRegistrationFromWire(encoded)
26+ require.NoError(t, err)
27+ require.Equal(t, registration, got)
28+ encoded.Incarnation[0] = 0
29+ require.Equal(t, byte(1), registration.Incarnation[0])
30+ }},
31+ {"target", func(t *testing.T) {
32+ encoded := BrokerExactTargetToWire(target)
33+ got, err := BrokerExactTargetFromWire(encoded)
34+ require.NoError(t, err)
35+ require.Equal(t, target, got)
36+ encoded.LifecycleId.Value[0] = 0
37+ require.Equal(t, byte(1), target.LifecycleID[0])
38+ }},
39+ {"policy", func(t *testing.T) {
40+ got, err := BrokerPolicyFromWire(BrokerPolicyToWire(policy))
41+ require.NoError(t, err)
42+ require.Equal(t, policy, got)
43+ }},
44+ } {
45+ t.Run(tc.name, tc.run)
46+ }
47+}
48+
49+func TestBrokerStartModeCodes(t *testing.T) {
50+ for _, tc := range []struct {
51+ name string
52+ mode ports.BrokerDaemonStartMode
53+ code uint32
54+ valid bool
55+ }{
56+ {"existing", ports.BrokerDaemonExistingOnly, 1, true},
57+ {"spawn", ports.BrokerDaemonStartIfNeeded, 2, true},
58+ {"absent", 0, 0, false},
59+ {"unknown", 3, 3, false},
60+ {"aliased", 0, 257, false},
61+ } {
62+ t.Run(tc.name, func(t *testing.T) {
63+ code, encodeErr := BrokerStartModeToWire(tc.mode)
64+ mode, decodeErr := BrokerStartModeFromWire(tc.code)
65+ if !tc.valid {
66+ require.ErrorIs(t, encodeErr, ErrOutOfRange)
67+ require.ErrorIs(t, decodeErr, ErrOutOfRange)
68+ return
69+ }
70+ require.NoError(t, encodeErr)
71+ require.NoError(t, decodeErr)
72+ require.Equal(t, tc.code, code)
73+ require.Equal(t, tc.mode, mode)
74+ })
75+ }
76+}
77+
78+func TestBrokerConversionFailures(t *testing.T) {
79+ for _, tc := range []struct {
80+ name string
81+ run func() error
82+ want error
83+ }{
84+ {"nil registration", func() error { _, err := BrokerRegistrationFromWire(nil); return err }, ErrOutOfRange},
85+ {"short incarnation", func() error {
86+ _, err := BrokerRegistrationFromWire(&wire.RemoteRegistration{Endpoint: "dev@host:22", Incarnation: []byte{1}, Generation: 1})
87+ return err
88+ }, ErrOutOfRange},
89+ {"nil target", func() error { _, err := BrokerExactTargetFromWire(nil); return err }, ErrOutOfRange},
90+ {"nil policy", func() error { _, err := BrokerPolicyFromWire(nil); return err }, ErrOutOfRange},
91+ {"policy narrowing", func() error {
92+ _, err := BrokerPolicyFromWire(&wire.BrokerWirePolicy{ProtocolVersion: 1<<16 + uint32(protocol.Version)})
93+ return err
94+ }, ErrOutOfRange},
95+ {"policy validation", func() error { _, err := BrokerPolicyFromWire(&wire.BrokerWirePolicy{}); return err }, ErrInvalid},
96+ {"oversize text", func() error { return BrokerDisplayText("abc", 2) }, ErrTooLarge},
97+ {"bidi text", func() error { return BrokerDisplayText("a\u202eb", 20) }, ErrInvalid},
98+ {"invalid utf8", func() error { return BrokerDisplayText("\xff", 20) }, ErrInvalid},
99+ {"oversize env", func() error { return BrokerEnvEntries([]string{strings.Repeat("a", ports.BrokerMaxEnvEntryBytes+1)}) }, ErrTooLarge},
100+ {"missing equals", func() error { return BrokerEnvEntries([]string{"TERM"}) }, ErrInvalid},
101+ {"valid env", func() error { return BrokerEnvEntries([]string{"TERM=x"}) }, nil},
102+ } {
103+ t.Run(tc.name, func(t *testing.T) {
104+ err := tc.run()
105+ if tc.want == nil {
106+ require.NoError(t, err)
107+ } else {
108+ require.ErrorIs(t, err, tc.want)
109+ }
110+ })
111+ }
112+}
@@ -0,0 +1,112 @@
1+package protoconv
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/catalogue"
11+ "github.com/bnema/vev/internal/protocol/wire"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestBrokerConversionRoundTrips(t *testing.T) {
16+ registration := domain.RemoteRegistration{Endpoint: "dev@host:22", Incarnation: [16]byte{1, 2, 3}, Generation: 9}
17+ target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1, 2, 3}, SessionName: "work"}
18+ policy := ports.BrokerPolicy{ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "pinned", Launch: "agent", Isolation: "user"}
19+ for _, tc := range []struct {
20+ name string
21+ run func(*testing.T)
22+ }{
23+ {"registration", func(t *testing.T) {
24+ encoded := BrokerRegistrationToWire(registration)
25+ got, err := BrokerRegistrationFromWire(encoded)
26+ require.NoError(t, err)
27+ require.Equal(t, registration, got)
28+ encoded.Incarnation[0] = 0
29+ require.Equal(t, byte(1), registration.Incarnation[0])
30+ }},
31+ {"target", func(t *testing.T) {
32+ encoded := BrokerExactTargetToWire(target)
33+ got, err := BrokerExactTargetFromWire(encoded)
34+ require.NoError(t, err)
35+ require.Equal(t, target, got)
36+ encoded.LifecycleId.Value[0] = 0
37+ require.Equal(t, byte(1), target.LifecycleID[0])
38+ }},
39+ {"policy", func(t *testing.T) {
40+ got, err := BrokerPolicyFromWire(BrokerPolicyToWire(policy))
41+ require.NoError(t, err)
42+ require.Equal(t, policy, got)
43+ }},
44+ } {
45+ t.Run(tc.name, tc.run)
46+ }
47+}
48+
49+func TestBrokerStartModeCodes(t *testing.T) {
50+ for _, tc := range []struct {
51+ name string
52+ mode ports.BrokerDaemonStartMode
53+ code uint32
54+ valid bool
55+ }{
56+ {"existing", ports.BrokerDaemonExistingOnly, 1, true},
57+ {"spawn", ports.BrokerDaemonStartIfNeeded, 2, true},
58+ {"absent", 0, 0, false},
59+ {"unknown", 3, 3, false},
60+ {"aliased", 0, 257, false},
61+ } {
62+ t.Run(tc.name, func(t *testing.T) {
63+ code, encodeErr := BrokerStartModeToWire(tc.mode)
64+ mode, decodeErr := BrokerStartModeFromWire(tc.code)
65+ if !tc.valid {
66+ require.ErrorIs(t, encodeErr, ErrOutOfRange)
67+ require.ErrorIs(t, decodeErr, ErrOutOfRange)
68+ return
69+ }
70+ require.NoError(t, encodeErr)
71+ require.NoError(t, decodeErr)
72+ require.Equal(t, tc.code, code)
73+ require.Equal(t, tc.mode, mode)
74+ })
75+ }
76+}
77+
78+func TestBrokerConversionFailures(t *testing.T) {
79+ for _, tc := range []struct {
80+ name string
81+ run func() error
82+ want error
83+ }{
84+ {"nil registration", func() error { _, err := BrokerRegistrationFromWire(nil); return err }, ErrOutOfRange},
85+ {"short incarnation", func() error {
86+ _, err := BrokerRegistrationFromWire(&wire.RemoteRegistration{Endpoint: "dev@host:22", Incarnation: []byte{1}, Generation: 1})
87+ return err
88+ }, ErrOutOfRange},
89+ {"nil target", func() error { _, err := BrokerExactTargetFromWire(nil); return err }, ErrOutOfRange},
90+ {"nil policy", func() error { _, err := BrokerPolicyFromWire(nil); return err }, ErrOutOfRange},
91+ {"policy narrowing", func() error {
92+ _, err := BrokerPolicyFromWire(&wire.BrokerWirePolicy{ProtocolVersion: 1<<16 + uint32(protocol.Version)})
93+ return err
94+ }, ErrOutOfRange},
95+ {"policy validation", func() error { _, err := BrokerPolicyFromWire(&wire.BrokerWirePolicy{}); return err }, ErrInvalid},
96+ {"oversize text", func() error { return BrokerDisplayText("abc", 2) }, ErrTooLarge},
97+ {"bidi text", func() error { return BrokerDisplayText("a\u202eb", 20) }, ErrInvalid},
98+ {"invalid utf8", func() error { return BrokerDisplayText("\xff", 20) }, ErrInvalid},
99+ {"oversize env", func() error { return BrokerEnvEntries([]string{strings.Repeat("a", ports.BrokerMaxEnvEntryBytes+1)}) }, ErrTooLarge},
100+ {"missing equals", func() error { return BrokerEnvEntries([]string{"TERM"}) }, ErrInvalid},
101+ {"valid env", func() error { return BrokerEnvEntries([]string{"TERM=x"}) }, nil},
102+ } {
103+ t.Run(tc.name, func(t *testing.T) {
104+ err := tc.run()
105+ if tc.want == nil {
106+ require.NoError(t, err)
107+ } else {
108+ require.ErrorIs(t, err, tc.want)
109+ }
110+ })
111+ }
112+}

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