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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +374 -312
M internal/adapters/brokerwire/codec.go +8 -16
| @@ -21,6 +21,7 @@ import ( | |||
| 21 | 21 | ||
| 22 | 22 | "google.golang.org/protobuf/proto" | |
| 23 | 23 | ||
| 24 | + | "github.com/bnema/vev/internal/adapters/protoconv" | |
| 24 | 25 | "github.com/bnema/vev/internal/domain" | |
| 25 | 26 | "github.com/bnema/vev/internal/ports" | |
| 26 | 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 | - | // 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. | |
| 607 | 606 | 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 { | |
| 614 | 609 | 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 | 614 | 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 { | |
| 626 | 617 | return 0, errConvertRange | |
| 627 | 618 | } | |
| 619 | + | return mode, nil | |
| 628 | 620 | } | |
| 629 | 621 | ||
| 630 | 622 | 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, errConvertRange | 609 | 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, errConvertRange | 617 | 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 | |||
| 12 | 12 | ||
| 13 | 13 | import ( | |
| 14 | 14 | "math" | |
| 15 | - | "strings" | |
| 16 | - | "unicode" | |
| 17 | - | "unicode/utf8" | |
| 18 | 15 | ||
| 16 | + | "github.com/bnema/vev/internal/adapters/protoconv" | |
| 19 | 17 | "github.com/bnema/vev/internal/domain" | |
| 20 | 18 | "github.com/bnema/vev/internal/ports" | |
| 21 | 19 | "github.com/bnema/vev/internal/protocol" | |
| @@ -23,22 +21,22 @@ import ( | |||
| 23 | 21 | "github.com/bnema/vev/internal/protocol/wire" | |
| 24 | 22 | ) | |
| 25 | 23 | ||
| 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. | |
| 29 | 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 | 28 | return 0, errConvertRange | |
| 32 | 29 | } | |
| 33 | - | return T(value), nil | |
| 30 | + | 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 | 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 | 37 | return 0, errConvertRange | |
| 40 | 38 | } | |
| 41 | - | return T(value), nil | |
| 39 | + | return v, nil | |
| 42 | 40 | } | |
| 43 | 41 | ||
| 44 | 42 | func scopeToWire(epoch ports.BrokerEpoch, connection ports.BrokerConnectionID) *wire.BrokerScope { | |
| @@ -101,138 +99,65 @@ func operationFromWire(raw []byte) (ports.BrokerOperationID, error) { | |||
| 101 | 99 | return id, nil | |
| 102 | 100 | } | |
| 103 | 101 | ||
| 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) | |
| 111 | 104 | } | |
| 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 { | |
| 129 | 108 | return domain.RemoteRegistration{}, errConvertRange | |
| 130 | 109 | } | |
| 131 | - | return registration, nil | |
| 110 | + | return out, nil | |
| 132 | 111 | } | |
| 133 | 112 | ||
| 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) | |
| 140 | 115 | } | |
| 141 | 116 | ||
| 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 { | |
| 149 | 120 | return protocol.ExactSessionTarget{}, errConvertRange | |
| 150 | 121 | } | |
| 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) | |
| 155 | 133 | } | |
| 156 | - | return target, nil | |
| 134 | + | return out, nil | |
| 157 | 135 | } | |
| 158 | 136 | ||
| 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: | |
| 164 | 142 | return ErrTooLarge | |
| 165 | - | } | |
| 166 | - | if !utf8.ValidString(entry) { | |
| 143 | + | default: | |
| 167 | 144 | 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 { | |
| 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)) | |
| 187 | 150 | } | |
| 188 | 151 | ||
| 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)) | |
| 219 | 154 | } | |
| 220 | 155 | ||
| 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 | |
| 234 | 159 | } | |
| 235 | - | return nil | |
| 160 | + | return mapBrokerConversionError(err) | |
| 236 | 161 | } | |
| 237 | 162 | ||
| 238 | 163 | // validateBrokerEndpoint enforces the SSH target contract shared by | |
| @@ -465,19 +390,7 @@ func validateSnapshotDaemonFields(daemon ports.BrokerDaemonObservation) error { | |||
| 465 | 390 | return nil | |
| 466 | 391 | } | |
| 467 | 392 | ||
| 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. | |
| 470 | 394 | 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)) | |
| 483 | 396 | } | |
| @@ -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. Values | 24 | + | // 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, errConvertRange | 28 | return 0, errConvertRange | ||
| 32 | } | 29 | } | ||
| 33 | - | return T(value), nil | 30 | + | 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, errConvertRange | 37 | return 0, errConvertRange | ||
| 40 | } | 38 | } | ||
| 41 | - | return T(value), nil | 39 | + | 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, nil | 99 | 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.RemoteRegistration | 107 | + | 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{}, errConvertRange | 108 | return domain.RemoteRegistration{}, errConvertRange | ||
| 130 | } | 109 | } | ||
| 131 | - | return registration, nil | 110 | + | 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.ExactSessionTarget | 118 | + | 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{}, errConvertRange | 120 | 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{}, errConvertRange | 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) | |||
| 155 | } | 133 | } | ||
| 156 | - | return target, nil | 134 | + | 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 never | 138 | + | 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 ErrTooLarge | 142 | return ErrTooLarge | ||
| 165 | - | } | 143 | + | default: |
| 166 | - | if !utf8.ValidString(entry) { | |||
| 167 | return ErrInvalidMessage | 144 | 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.BrokerPolicy | 153 | + | 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 nil | 160 | + | return mapBrokerConversionError(err) |
| 236 | } | 161 | } | ||
| 237 | 162 | ||||
| 238 | // validateBrokerEndpoint enforces the SSH target contract shared by | 163 | // validateBrokerEndpoint enforces the SSH target contract shared by | ||
| @@ -465,19 +390,7 @@ func validateSnapshotDaemonFields(daemon ports.BrokerDaemonObservation) error { | |||||
| 465 | return nil | 390 | return nil | ||
| 466 | } | 391 | } | ||
| 467 | 392 | ||||
| 468 | - | // checkEnvEntries enforces the per-request environment bound: at most | 393 | + | // 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 | |||
| 15 | 15 | // directly, so the two conversations stay independently evolvable. | |
| 16 | 16 | ||
| 17 | 17 | import ( | |
| 18 | - | "math" | |
| 19 | - | "strings" | |
| 20 | - | "unicode" | |
| 21 | - | "unicode/utf8" | |
| 22 | - | ||
| 18 | + | "github.com/bnema/vev/internal/adapters/protoconv" | |
| 23 | 19 | "github.com/bnema/vev/internal/domain" | |
| 24 | 20 | "github.com/bnema/vev/internal/ports" | |
| 25 | 21 | "github.com/bnema/vev/internal/protocol" | |
| 26 | 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 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. | |
| 32 | 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 | 29 | return 0, errConvertRange | |
| 35 | 30 | } | |
| 36 | - | return T(value), nil | |
| 31 | + | 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 | 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 | 38 | return 0, errConvertRange | |
| 43 | 39 | } | |
| 44 | - | return T(value), nil | |
| 40 | + | 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 | 43 | 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 { | |
| 57 | 46 | 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 | 51 | 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 { | |
| 69 | 54 | return 0, errConvertRange | |
| 70 | 55 | } | |
| 56 | + | return v, nil | |
| 71 | 57 | } | |
| 72 | 58 | ||
| 73 | 59 | func physicalToWire(id PhysicalStreamID) uint64 { return uint64(id) } | |
| @@ -121,105 +107,63 @@ func refFromWire(message *wire.MuxStreamRef) (StreamRef, error) { | |||
| 121 | 107 | }, nil | |
| 122 | 108 | } | |
| 123 | 109 | ||
| 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) | |
| 133 | 112 | } | |
| 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 { | |
| 151 | 116 | return domain.RemoteRegistration{}, errConvertRange | |
| 152 | 117 | } | |
| 153 | - | return registration, nil | |
| 118 | + | return out, nil | |
| 154 | 119 | } | |
| 155 | 120 | ||
| 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) | |
| 162 | 123 | } | |
| 163 | 124 | ||
| 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 { | |
| 176 | 128 | return protocol.ExactSessionTarget{}, errConvertRange | |
| 177 | 129 | } | |
| 178 | - | return target, nil | |
| 130 | + | return out, nil | |
| 179 | 131 | } | |
| 180 | 132 | ||
| 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) | |
| 191 | 135 | } | |
| 192 | 136 | ||
| 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) | |
| 207 | 139 | if err != nil { | |
| 208 | - | return ports.BrokerPolicy{}, err | |
| 140 | + | return ports.BrokerPolicy{}, mapBrokerConversionError(err) | |
| 209 | 141 | } | |
| 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 | |
| 218 | 153 | } | |
| 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 | |
| 221 | 165 | } | |
| 222 | - | return policy, nil | |
| 166 | + | return mapBrokerConversionError(err) | |
| 223 | 167 | } | |
| 224 | 168 | ||
| 225 | 169 | func errorDetailToWire(detail ErrorDetail) *wire.BrokerErrorDetail { | |
| @@ -268,55 +212,7 @@ func errorDetailFromWire(message *wire.BrokerErrorDetail) (ErrorDetail, error) { | |||
| 268 | 212 | return detail, nil | |
| 269 | 213 | } | |
| 270 | 214 | ||
| 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. | |
| 309 | 216 | 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)) | |
| 322 | 218 | } | |
| @@ -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 whose | 25 | + | // 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, errConvertRange | 29 | return 0, errConvertRange | ||
| 35 | } | 30 | } | ||
| 36 | - | return T(value), nil | 31 | + | 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, errConvertRange | 38 | return 0, errConvertRange | ||
| 43 | } | 39 | } | ||
| 44 | - | return T(value), nil | 40 | + | 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, errConvertRange | 46 | 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, errConvertRange | 54 | 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 | }, nil | 107 | }, nil | ||
| 122 | } | 108 | } | ||
| 123 | 109 | ||||
| 124 | - | // registrationToWire mirrors domain.RemoteRegistration: an endpoint, an exact | 110 | + | 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.RemoteRegistration | 115 | + | 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{}, errConvertRange | 116 | return domain.RemoteRegistration{}, errConvertRange | ||
| 152 | } | 117 | } | ||
| 153 | - | return registration, nil | 118 | + | 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.ExactSessionTarget | 126 | + | 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{}, errConvertRange | 128 | return protocol.ExactSessionTarget{}, errConvertRange | ||
| 177 | } | 129 | } | ||
| 178 | - | return target, nil | 130 | + | 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.BrokerPolicy | 138 | + | 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{}, err | 140 | + | 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{}, ErrInvalidMessage | 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 | |||
| 221 | } | 165 | } | ||
| 222 | - | return policy, nil | 166 | + | 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, nil | 212 | 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 | + | } | |||