feat(protocol): negotiate display and native input
Verify Protocol / module (push) Successful in 1m17s
Verify Protocol / verify (push) Successful in 55s

This commit is contained in:
sechmachine
2026-08-10 23:07:04 +07:00
parent 346bf5fe4d
commit 408d4f9cc3
24 changed files with 707 additions and 60 deletions
@@ -7,6 +7,8 @@ valid-relative-mouse 1 gateway_input hex=564749310304fffe0003 valid
valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid
valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid
valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 valid
valid-scroll 1 gateway_input hex=564749310704ff880078 valid
valid-idr 1 gateway_feedback hex=5647463100010000 valid valid-idr 1 gateway_feedback hex=5647463100010000 valid
valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
@@ -18,6 +20,11 @@ invalid-input-kind 1 gateway_input hex=564749317f00 invalid:kind
invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8 invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
invalid-absolute-zero-viewport 1 gateway_input hex=56474931060800000000000005a0 invalid:field
invalid-absolute-x-out-of-range 1 gateway_input hex=5647493106080a0000000a0005a0 invalid:field
invalid-absolute-y-out-of-range 1 gateway_input hex=564749310608000005a00a0005a0 invalid:field
invalid-absolute-length 1 gateway_input hex=56474931060700000000010001 invalid:length
invalid-scroll-length 1 gateway_input hex=5647493107020000 invalid:length
invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 invalid:direction invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 invalid:direction
invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
1 id version kind input expected
7 valid-utf8-scalar 1 gateway_input hex=564749310403e29883 valid
8 valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040005 valid
9 valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
10 valid-absolute-mouse 1 gateway_input hex=56474931060804d202370a0005a0 valid
11 valid-scroll 1 gateway_input hex=564749310704ff880078 valid
12 valid-idr 1 gateway_feedback hex=5647463100010000 valid
13 valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
14 valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
20 invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
21 invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
22 invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
23 invalid-absolute-zero-viewport 1 gateway_input hex=56474931060800000000000005a0 invalid:field
24 invalid-absolute-x-out-of-range 1 gateway_input hex=5647493106080a0000000a0005a0 invalid:field
25 invalid-absolute-y-out-of-range 1 gateway_input hex=564749310608000005a00a0005a0 invalid:field
26 invalid-absolute-length 1 gateway_input hex=56474931060700000000010001 invalid:length
27 invalid-scroll-length 1 gateway_input hex=5647493107020000 invalid:length
28 invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
29 invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 invalid:direction
30 invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
+3
View File
@@ -2,5 +2,8 @@ id version kind input expected
tunnel-current 1 tunnel offered=1;feature=control.v1 valid tunnel-current 1 tunnel offered=1;feature=control.v1 valid
tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid
tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid
tunnel-display-request 1 tunnel offered=1;feature=display.request.v1 valid
tunnel-absolute-input 1 tunnel offered=1;feature=input.absolute.v1 valid
tunnel-scroll-input 1 tunnel offered=1;feature=input.scroll.v1 valid
tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version
tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature
1 id version kind input expected
2 tunnel-current 1 tunnel offered=1;feature=control.v1 valid
3 tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid
4 tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid
5 tunnel-display-request 1 tunnel offered=1;feature=display.request.v1 valid
6 tunnel-absolute-input 1 tunnel offered=1;feature=input.absolute.v1 valid
7 tunnel-scroll-input 1 tunnel offered=1;feature=input.scroll.v1 valid
8 tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version
9 tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature
+1 -1
View File
@@ -10,5 +10,5 @@
"fixtures/conformance/gateway-input-feedback-v1.tsv", "fixtures/conformance/gateway-input-feedback-v1.tsv",
"fixtures/conformance/tunnel-v1.tsv" "fixtures/conformance/tunnel-v1.tsv"
], ],
"corpus_sha256": "544ba8765b4ddcfbe807d1d6e6788fe03d06b8e50d1424b82e4e512312e51248" "corpus_sha256": "4f8bf520d4f61588235b00776b21202ff701b20d6d11b461037b911c2117852b"
} }
+14 -8
View File
@@ -4,12 +4,14 @@ This grammar is carried only in an authenticated Phase 3C gateway session. It
is deliberately provider-neutral: it never carries provider routes, is deliberately provider-neutral: it never carries provider routes,
certificates, credentials, opaque provider packets, clipboard bytes, files, or certificates, credentials, opaque provider packets, clipboard bytes, files, or
client-folder data. It does not change the v1 datagram header or any existing client-folder data. It does not change the v1 datagram header or any existing
release candidate. kind encoding; the new kinds require explicit feature negotiation and a new
immutable Protocol release candidate.
## `input.sequenced.v1` payload (`VGI1`) ## `input.sequenced.v1` payload (`VGI1`)
All multibyte fields are unsigned big-endian. The payload has exactly six bytes All multibyte fields are big-endian and unsigned unless a field is explicitly
of header followed by the declared body: marked signed. The payload has exactly six bytes of header followed by the
declared body:
| Offset | Size | Field | Rule | | Offset | Size | Field | Rule |
|---:|---:|---|---| |---:|---:|---|---|
@@ -32,12 +34,16 @@ during cleanup.
| `0x03` | relative mouse | `delta_x` (i16), `delta_y` (i16). | | `0x03` | relative mouse | `delta_x` (i16), `delta_y` (i16). |
| `0x04` | UTF-8 scalar | exactly one valid UTF-8 Unicode scalar, one through four bytes. | | `0x04` | UTF-8 scalar | exactly one valid UTF-8 Unicode scalar, one through four bytes. |
| `0x05` | controller state | `controller` (0 through 15), `active_mask` (u16), `button_flags` (u16), `left_trigger` (u8), `right_trigger` (u8), `left_x` (i16), `left_y` (i16), `right_x` (i16), `right_y` (i16), `extra_button_flags` (u16). A zero `active_mask` and zero state is release. | | `0x05` | controller state | `controller` (0 through 15), `active_mask` (u16), `button_flags` (u16), `left_trigger` (u8), `right_trigger` (u8), `left_x` (i16), `left_y` (i16), `right_x` (i16), `right_y` (i16), `extra_button_flags` (u16). A zero `active_mask` and zero state is release. |
| `0x06` | absolute mouse | `x` (u16), `y` (u16), `viewport_width` (u16), `viewport_height` (u16). Viewport dimensions must be nonzero and each coordinate must be strictly smaller than its corresponding dimension. |
| `0x07` | high-resolution scroll | `vertical_delta` (i16), `horizontal_delta` (i16). Positive and negative values preserve the client scroll direction without provider-specific scaling. |
Keyboard, mouse button, UTF-8, and controller messages are delivered over the Keyboard, mouse button, UTF-8, controller, absolute mouse, and scroll messages
gateway's reliable ordered input flow. Relative mouse is a state change, not a are delivered over the gateway's reliable ordered input flow. Relative and
pressed-state entry. The gateway maps the validated values to the provider's absolute mouse movement and scroll are state changes, not pressed-state
separate keyboard, mouse, UTF-8, and controller control messages; it does not entries. Kinds `0x06` and `0x07` are accepted only when the session advertises
forward this envelope to the provider. `input.absolute.v1` and `input.scroll.v1`, respectively. The gateway maps the
validated values to the provider's separate input messages; it does not forward
this envelope to the provider.
## Reliable control payload (`VGF1`) ## Reliable control payload (`VGF1`)
+143 -23
View File
@@ -13,7 +13,7 @@ import (
"time" "time"
) )
const SchemaSHA256 = "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058" const SchemaSHA256 = "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
const ProtocolVersion = "1.0.0" const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "1" const CurrentWireVersion = "1"
const NMinus1WireVersion = "0" const NMinus1WireVersion = "0"
@@ -50,21 +50,23 @@ type AssignedDesktop struct {
} }
type BrokerSession struct { type BrokerSession struct {
ID string `json:"id"` ID string `json:"id"`
PrincipalID string `json:"principal_id"` PrincipalID string `json:"principal_id"`
PoolID string `json:"pool_id"` PoolID string `json:"pool_id"`
AssignmentID string `json:"assignment_id,omitempty"` AssignmentID string `json:"assignment_id,omitempty"`
State string `json:"state"` State string `json:"state"`
PolicySnapshot AllocationPolicy `json:"policy_snapshot"` PolicySnapshot AllocationPolicy `json:"policy_snapshot"`
ReconnectDeadline string `json:"reconnect_deadline,omitempty"` ReconnectDeadline string `json:"reconnect_deadline,omitempty"`
Outcome string `json:"outcome,omitempty"` Outcome string `json:"outcome,omitempty"`
FailureCode string `json:"failure_code,omitempty"` FailureCode string `json:"failure_code,omitempty"`
CleanupState string `json:"cleanup_state"` CleanupState string `json:"cleanup_state"`
IdempotencyKey string `json:"idempotency_key"` IdempotencyKey string `json:"idempotency_key"`
CorrelationID string `json:"correlation_id"` CorrelationID string `json:"correlation_id"`
RequestedAt string `json:"requested_at"` RequestedAt string `json:"requested_at"`
EndedAt string `json:"ended_at,omitempty"` EndedAt string `json:"ended_at,omitempty"`
Version int64 `json:"version"` Version int64 `json:"version"`
RequestedDisplayMode *DisplayMode `json:"requested_display_mode,omitempty"`
EffectiveDisplayMode *DisplayMode `json:"effective_display_mode,omitempty"`
} }
type CapabilityProfile struct { type CapabilityProfile struct {
@@ -134,6 +136,12 @@ type DeviceRegistrationRequest struct {
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
} }
type DisplayMode struct {
ResolutionWidth int64 `json:"resolution_width"`
ResolutionHeight int64 `json:"resolution_height"`
Fps int64 `json:"fps"`
}
type EntitledPool struct { type EntitledPool struct {
PoolID string `json:"pool_id"` PoolID string `json:"pool_id"`
Name string `json:"name"` Name string `json:"name"`
@@ -263,8 +271,9 @@ type ManifestGateway struct {
} }
type ManifestProfile struct { type ManifestProfile struct {
ID string `json:"id"` ID string `json:"id"`
Bounds ManifestBounds `json:"bounds"` Bounds ManifestBounds `json:"bounds"`
DisplayMode *DisplayMode `json:"display_mode,omitempty"`
} }
type ManifestTunnel struct { type ManifestTunnel struct {
@@ -383,11 +392,12 @@ type SessionAuthority struct {
} }
type SessionRequest struct { type SessionRequest struct {
ClientDeviceID string `json:"client_device_id"` ClientDeviceID string `json:"client_device_id"`
DeviceKeyID string `json:"device_key_id"` DeviceKeyID string `json:"device_key_id"`
PoolID string `json:"pool_id"` PoolID string `json:"pool_id"`
IdempotencyKey string `json:"idempotency_key"` IdempotencyKey string `json:"idempotency_key"`
PolicySnapshot AllocationPolicy `json:"policy_snapshot"` PolicySnapshot AllocationPolicy `json:"policy_snapshot"`
RequestedDisplayMode *DisplayMode `json:"requested_display_mode,omitempty"`
} }
type StableError struct { type StableError struct {
@@ -767,6 +777,16 @@ func (v BrokerSession) Validate() error {
if v.Version != 0 && v.Version < 1 { if v.Version != 0 && v.Version < 1 {
violations = append(violations, FieldViolation{Field: "version", Code: "minimum"}) violations = append(violations, FieldViolation{Field: "version", Code: "minimum"})
} }
if v.RequestedDisplayMode != nil {
if err := v.RequestedDisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "requested_display_mode", Code: "invalid_object"})
}
}
if v.EffectiveDisplayMode != nil {
if err := v.EffectiveDisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "effective_display_mode", Code: "invalid_object"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -812,6 +832,12 @@ func DecodeBrokerSession(data []byte) (BrokerSession, error) {
if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}}
} }
if raw, ok := fields["requested_display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "requested_display_mode", Code: "invalid_object"}}}
}
if raw, ok := fields["effective_display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "effective_display_mode", Code: "invalid_object"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil { if err := decoder.Decode(&value); err != nil {
@@ -1625,6 +1651,84 @@ func EncodeDeviceRegistrationRequest(value DeviceRegistrationRequest) ([]byte, e
return json.Marshal(value) return json.Marshal(value)
} }
func (v DisplayMode) Validate() error {
var violations []FieldViolation
if v.ResolutionWidth == 0 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "required"})
}
if v.ResolutionWidth != 0 && v.ResolutionWidth < 320 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "minimum"})
}
if v.ResolutionWidth > 16384 {
violations = append(violations, FieldViolation{Field: "resolution_width", Code: "maximum"})
}
if v.ResolutionHeight == 0 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "required"})
}
if v.ResolutionHeight != 0 && v.ResolutionHeight < 200 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "minimum"})
}
if v.ResolutionHeight > 8640 {
violations = append(violations, FieldViolation{Field: "resolution_height", Code: "maximum"})
}
if v.Fps == 0 {
violations = append(violations, FieldViolation{Field: "fps", Code: "required"})
}
if v.Fps != 0 && v.Fps < 1 {
violations = append(violations, FieldViolation{Field: "fps", Code: "minimum"})
}
if v.Fps > 240 {
violations = append(violations, FieldViolation{Field: "fps", Code: "maximum"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeDisplayMode(data []byte) (DisplayMode, error) {
var value DisplayMode
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["fps"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "fps", Code: "required"}}}
}
if raw, ok := fields["resolution_height"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "resolution_height", Code: "required"}}}
}
if raw, ok := fields["resolution_width"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "resolution_width", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeDisplayMode(value DisplayMode) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v EntitledPool) Validate() error { func (v EntitledPool) Validate() error {
var violations []FieldViolation var violations []FieldViolation
if v.PoolID == "" { if v.PoolID == "" {
@@ -3202,6 +3306,11 @@ func (v ManifestProfile) Validate() error {
if err := v.Bounds.Validate(); err != nil { if err := v.Bounds.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_object"}) violations = append(violations, FieldViolation{Field: "bounds", Code: "invalid_object"})
} }
if v.DisplayMode != nil {
if err := v.DisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "display_mode", Code: "invalid_object"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -3223,6 +3332,9 @@ func DecodeManifestProfile(data []byte) (ManifestProfile, error) {
if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "id", Code: "required"}}}
} }
if raw, ok := fields["display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "display_mode", Code: "invalid_object"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil { if err := decoder.Decode(&value); err != nil {
@@ -4655,6 +4767,11 @@ func (v SessionRequest) Validate() error {
if err := v.PolicySnapshot.Validate(); err != nil { if err := v.PolicySnapshot.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "invalid_object"}) violations = append(violations, FieldViolation{Field: "policy_snapshot", Code: "invalid_object"})
} }
if v.RequestedDisplayMode != nil {
if err := v.RequestedDisplayMode.Validate(); err != nil {
violations = append(violations, FieldViolation{Field: "requested_display_mode", Code: "invalid_object"})
}
}
if len(violations) > 0 { if len(violations) > 0 {
return ValidationError{Violations: violations} return ValidationError{Violations: violations}
} }
@@ -4685,6 +4802,9 @@ func DecodeSessionRequest(data []byte) (SessionRequest, error) {
if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { if raw, ok := fields["pool_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", Code: "required"}}} return value, ValidationError{Violations: []FieldViolation{{Field: "pool_id", Code: "required"}}}
} }
if raw, ok := fields["requested_display_mode"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "requested_display_mode", Code: "invalid_object"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil { if err := decoder.Decode(&value); err != nil {
+2 -2
View File
@@ -12,7 +12,7 @@
"2" "2"
] ]
}, },
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6", "generator_sha256": "992235a56d3467313148f86e47931f247591e4c8de737b55ac9c9eee35725fc5",
"protocol_version": "1.0.0", "protocol_version": "1.0.0",
"schema_sha256": "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058" "schema_sha256": "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
} }
+54 -7
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)] #![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058"; pub const SCHEMA_SHA256: &str = "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c";
pub const CURRENT_WIRE_VERSION: &str = "1"; pub const CURRENT_WIRE_VERSION: &str = "1";
pub const N_MINUS_1_WIRE_VERSION: &str = "0"; pub const N_MINUS_1_WIRE_VERSION: &str = "0";
pub const N_MINUS_2_WIRE_VERSION: &str = "-1"; pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
@@ -136,11 +136,13 @@ pub struct BrokerSession {
requestedAt: String, requestedAt: String,
endedAt: Option<String>, endedAt: Option<String>,
version: i64, version: i64,
requestedDisplayMode: Option<DisplayMode>,
effectiveDisplayMode: Option<DisplayMode>,
} }
impl BrokerSession { impl BrokerSession {
pub fn new(id: String, principalId: String, poolId: String, assignmentId: Option<String>, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: Option<String>, outcome: Option<String>, failureCode: Option<String>, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: Option<String>, version: i64) -> Result<Self, ValidationError> { pub fn new(id: String, principalId: String, poolId: String, assignmentId: Option<String>, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: Option<String>, outcome: Option<String>, failureCode: Option<String>, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: Option<String>, version: i64, requestedDisplayMode: Option<DisplayMode>, effectiveDisplayMode: Option<DisplayMode>) -> Result<Self, ValidationError> {
let value = Self { id, principalId, poolId, assignmentId, state, policySnapshot, reconnectDeadline, outcome, failureCode, cleanupState, idempotencyKey, correlationId, requestedAt, endedAt, version }; let value = Self { id, principalId, poolId, assignmentId, state, policySnapshot, reconnectDeadline, outcome, failureCode, cleanupState, idempotencyKey, correlationId, requestedAt, endedAt, version, requestedDisplayMode, effectiveDisplayMode };
value.validate()?; value.validate()?;
Ok(value) Ok(value)
} }
@@ -184,6 +186,12 @@ impl BrokerSession {
if value.len() > 64 { return Err(ValidationError::new("ended_at", "max_length")); } if value.len() > 64 { return Err(ValidationError::new("ended_at", "max_length")); }
} }
if self.version < 1 { return Err(ValidationError::new("version", "minimum")); } if self.version < 1 { return Err(ValidationError::new("version", "minimum")); }
if let Some(value) = &self.requestedDisplayMode {
value.validate().map_err(|_| ValidationError::new("requested_display_mode", "invalid_object"))?;
}
if let Some(value) = &self.effectiveDisplayMode {
value.validate().map_err(|_| ValidationError::new("effective_display_mode", "invalid_object"))?;
}
Ok(()) Ok(())
} }
pub fn id(&self) -> &String { &self.id } pub fn id(&self) -> &String { &self.id }
@@ -201,6 +209,8 @@ impl BrokerSession {
pub fn requestedAt(&self) -> &String { &self.requestedAt } pub fn requestedAt(&self) -> &String { &self.requestedAt }
pub fn endedAt(&self) -> &Option<String> { &self.endedAt } pub fn endedAt(&self) -> &Option<String> { &self.endedAt }
pub fn version(&self) -> &i64 { &self.version } pub fn version(&self) -> &i64 { &self.version }
pub fn requestedDisplayMode(&self) -> &Option<DisplayMode> { &self.requestedDisplayMode }
pub fn effectiveDisplayMode(&self) -> &Option<DisplayMode> { &self.effectiveDisplayMode }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -494,6 +504,33 @@ impl DeviceRegistrationRequest {
pub fn publicKey(&self) -> &String { &self.publicKey } pub fn publicKey(&self) -> &String { &self.publicKey }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisplayMode {
resolutionWidth: i64,
resolutionHeight: i64,
fps: i64,
}
impl DisplayMode {
pub fn new(resolutionWidth: i64, resolutionHeight: i64, fps: i64) -> Result<Self, ValidationError> {
let value = Self { resolutionWidth, resolutionHeight, fps };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.resolutionWidth < 320 { return Err(ValidationError::new("resolution_width", "minimum")); }
if self.resolutionWidth > 16384 { return Err(ValidationError::new("resolution_width", "maximum")); }
if self.resolutionHeight < 200 { return Err(ValidationError::new("resolution_height", "minimum")); }
if self.resolutionHeight > 8640 { return Err(ValidationError::new("resolution_height", "maximum")); }
if self.fps < 1 { return Err(ValidationError::new("fps", "minimum")); }
if self.fps > 240 { return Err(ValidationError::new("fps", "maximum")); }
Ok(())
}
pub fn resolutionWidth(&self) -> &i64 { &self.resolutionWidth }
pub fn resolutionHeight(&self) -> &i64 { &self.resolutionHeight }
pub fn fps(&self) -> &i64 { &self.fps }
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntitledPool { pub struct EntitledPool {
poolId: String, poolId: String,
@@ -1080,11 +1117,12 @@ impl ManifestGateway {
pub struct ManifestProfile { pub struct ManifestProfile {
id: String, id: String,
bounds: ManifestBounds, bounds: ManifestBounds,
displayMode: Option<DisplayMode>,
} }
impl ManifestProfile { impl ManifestProfile {
pub fn new(id: String, bounds: ManifestBounds) -> Result<Self, ValidationError> { pub fn new(id: String, bounds: ManifestBounds, displayMode: Option<DisplayMode>) -> Result<Self, ValidationError> {
let value = Self { id, bounds }; let value = Self { id, bounds, displayMode };
value.validate()?; value.validate()?;
Ok(value) Ok(value)
} }
@@ -1093,10 +1131,14 @@ impl ManifestProfile {
if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); } if !self.id.is_empty() && self.id.len() < 1 { return Err(ValidationError::new("id", "min_length")); }
if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); } if self.id.len() > 128 { return Err(ValidationError::new("id", "max_length")); }
self.bounds.validate().map_err(|_| ValidationError::new("bounds", "invalid_object"))?; self.bounds.validate().map_err(|_| ValidationError::new("bounds", "invalid_object"))?;
if let Some(value) = &self.displayMode {
value.validate().map_err(|_| ValidationError::new("display_mode", "invalid_object"))?;
}
Ok(()) Ok(())
} }
pub fn id(&self) -> &String { &self.id } pub fn id(&self) -> &String { &self.id }
pub fn bounds(&self) -> &ManifestBounds { &self.bounds } pub fn bounds(&self) -> &ManifestBounds { &self.bounds }
pub fn displayMode(&self) -> &Option<DisplayMode> { &self.displayMode }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -1613,11 +1655,12 @@ pub struct SessionRequest {
poolId: String, poolId: String,
idempotencyKey: String, idempotencyKey: String,
policySnapshot: AllocationPolicy, policySnapshot: AllocationPolicy,
requestedDisplayMode: Option<DisplayMode>,
} }
impl SessionRequest { impl SessionRequest {
pub fn new(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy) -> Result<Self, ValidationError> { pub fn new(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy, requestedDisplayMode: Option<DisplayMode>) -> Result<Self, ValidationError> {
let value = Self { clientDeviceId, deviceKeyId, poolId, idempotencyKey, policySnapshot }; let value = Self { clientDeviceId, deviceKeyId, poolId, idempotencyKey, policySnapshot, requestedDisplayMode };
value.validate()?; value.validate()?;
Ok(value) Ok(value)
} }
@@ -1635,6 +1678,9 @@ impl SessionRequest {
if !self.idempotencyKey.is_empty() && self.idempotencyKey.len() < 1 { return Err(ValidationError::new("idempotency_key", "min_length")); } if !self.idempotencyKey.is_empty() && self.idempotencyKey.len() < 1 { return Err(ValidationError::new("idempotency_key", "min_length")); }
if self.idempotencyKey.len() > 256 { return Err(ValidationError::new("idempotency_key", "max_length")); } if self.idempotencyKey.len() > 256 { return Err(ValidationError::new("idempotency_key", "max_length")); }
self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?; self.policySnapshot.validate().map_err(|_| ValidationError::new("policy_snapshot", "invalid_object"))?;
if let Some(value) = &self.requestedDisplayMode {
value.validate().map_err(|_| ValidationError::new("requested_display_mode", "invalid_object"))?;
}
Ok(()) Ok(())
} }
pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId } pub fn clientDeviceId(&self) -> &String { &self.clientDeviceId }
@@ -1642,6 +1688,7 @@ impl SessionRequest {
pub fn poolId(&self) -> &String { &self.poolId } pub fn poolId(&self) -> &String { &self.poolId }
pub fn idempotencyKey(&self) -> &String { &self.idempotencyKey } pub fn idempotencyKey(&self) -> &String { &self.idempotencyKey }
pub fn policySnapshot(&self) -> &AllocationPolicy { &self.policySnapshot } pub fn policySnapshot(&self) -> &AllocationPolicy { &self.policySnapshot }
pub fn requestedDisplayMode(&self) -> &Option<DisplayMode> { &self.requestedDisplayMode }
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
+68 -7
View File
@@ -1,7 +1,7 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
import Foundation import Foundation
public typealias JSONObject = [String: String] public typealias JSONObject = [String: String]
public let schemaSHA256 = "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058" public let schemaSHA256 = "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
public let currentWireVersion = "1" public let currentWireVersion = "1"
public let nMinus1WireVersion = "0" public let nMinus1WireVersion = "0"
public let nMinus2WireVersion = "-1" public let nMinus2WireVersion = "-1"
@@ -148,6 +148,8 @@ public struct BrokerSession: Codable, Equatable {
public let requestedAt: String public let requestedAt: String
public let endedAt: String? public let endedAt: String?
public let version: Int64 public let version: Int64
public let requestedDisplayMode: DisplayMode?
public let effectiveDisplayMode: DisplayMode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id = "id" case id = "id"
case principalId = "principal_id" case principalId = "principal_id"
@@ -164,9 +166,11 @@ public struct BrokerSession: Codable, Equatable {
case requestedAt = "requested_at" case requestedAt = "requested_at"
case endedAt = "ended_at" case endedAt = "ended_at"
case version = "version" case version = "version"
case requestedDisplayMode = "requested_display_mode"
case effectiveDisplayMode = "effective_display_mode"
} }
public init(id: String, principalId: String, poolId: String, assignmentId: String?, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: String?, outcome: String?, failureCode: String?, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: String?, version: Int64) throws { public init(id: String, principalId: String, poolId: String, assignmentId: String?, state: String, policySnapshot: AllocationPolicy, reconnectDeadline: String?, outcome: String?, failureCode: String?, cleanupState: String, idempotencyKey: String, correlationId: String, requestedAt: String, endedAt: String?, version: Int64, requestedDisplayMode: DisplayMode?, effectiveDisplayMode: DisplayMode?) throws {
self.id = id self.id = id
self.principalId = principalId self.principalId = principalId
self.poolId = poolId self.poolId = poolId
@@ -182,6 +186,8 @@ public struct BrokerSession: Codable, Equatable {
self.requestedAt = requestedAt self.requestedAt = requestedAt
self.endedAt = endedAt self.endedAt = endedAt
self.version = version self.version = version
self.requestedDisplayMode = requestedDisplayMode
self.effectiveDisplayMode = effectiveDisplayMode
try validate() try validate()
} }
@@ -189,7 +195,7 @@ public struct BrokerSession: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self) let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(id: try c.decode(String.self, forKey: .id), principalId: try c.decode(String.self, forKey: .principalId), poolId: try c.decode(String.self, forKey: .poolId), assignmentId: try c.decodeIfPresent(String.self, forKey: .assignmentId), state: try c.decode(String.self, forKey: .state), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot), reconnectDeadline: try c.decodeIfPresent(String.self, forKey: .reconnectDeadline), outcome: try c.decodeIfPresent(String.self, forKey: .outcome), failureCode: try c.decodeIfPresent(String.self, forKey: .failureCode), cleanupState: try c.decode(String.self, forKey: .cleanupState), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), correlationId: try c.decode(String.self, forKey: .correlationId), requestedAt: try c.decode(String.self, forKey: .requestedAt), endedAt: try c.decodeIfPresent(String.self, forKey: .endedAt), version: try c.decode(Int64.self, forKey: .version)) try self.init(id: try c.decode(String.self, forKey: .id), principalId: try c.decode(String.self, forKey: .principalId), poolId: try c.decode(String.self, forKey: .poolId), assignmentId: try c.decodeIfPresent(String.self, forKey: .assignmentId), state: try c.decode(String.self, forKey: .state), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot), reconnectDeadline: try c.decodeIfPresent(String.self, forKey: .reconnectDeadline), outcome: try c.decodeIfPresent(String.self, forKey: .outcome), failureCode: try c.decodeIfPresent(String.self, forKey: .failureCode), cleanupState: try c.decode(String.self, forKey: .cleanupState), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), correlationId: try c.decode(String.self, forKey: .correlationId), requestedAt: try c.decode(String.self, forKey: .requestedAt), endedAt: try c.decodeIfPresent(String.self, forKey: .endedAt), version: try c.decode(Int64.self, forKey: .version), requestedDisplayMode: try c.contains(.requestedDisplayMode) ? c.decode(DisplayMode.self, forKey: .requestedDisplayMode) : nil, effectiveDisplayMode: try c.contains(.effectiveDisplayMode) ? c.decode(DisplayMode.self, forKey: .effectiveDisplayMode) : nil)
} }
public func validate() throws { public func validate() throws {
@@ -235,6 +241,12 @@ public struct BrokerSession: Codable, Equatable {
if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "ended_at", code: "invalid_time") } if ISO8601DateFormatter().date(from: value) == nil { throw ContractValidationError(field: "ended_at", code: "invalid_time") }
} }
if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") } if self.version < 1 { throw ContractValidationError(field: "version", code: "minimum") }
if let value = self.requestedDisplayMode {
try value.validate()
}
if let value = self.effectiveDisplayMode {
try value.validate()
}
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -632,6 +644,43 @@ public struct DeviceRegistrationRequest: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) } public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
} }
public struct DisplayMode: Codable, Equatable {
public let resolutionWidth: Int64
public let resolutionHeight: Int64
public let fps: Int64
enum CodingKeys: String, CodingKey {
case resolutionWidth = "resolution_width"
case resolutionHeight = "resolution_height"
case fps = "fps"
}
public init(resolutionWidth: Int64, resolutionHeight: Int64, fps: Int64) throws {
self.resolutionWidth = resolutionWidth
self.resolutionHeight = resolutionHeight
self.fps = fps
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(resolutionWidth: try c.decode(Int64.self, forKey: .resolutionWidth), resolutionHeight: try c.decode(Int64.self, forKey: .resolutionHeight), fps: try c.decode(Int64.self, forKey: .fps))
}
public func validate() throws {
if self.resolutionWidth < 320 { throw ContractValidationError(field: "resolution_width", code: "minimum") }
if self.resolutionWidth > 16384 { throw ContractValidationError(field: "resolution_width", code: "maximum") }
if self.resolutionHeight < 200 { throw ContractValidationError(field: "resolution_height", code: "minimum") }
if self.resolutionHeight > 8640 { throw ContractValidationError(field: "resolution_height", code: "maximum") }
if self.fps < 1 { throw ContractValidationError(field: "fps", code: "minimum") }
if self.fps > 240 { throw ContractValidationError(field: "fps", code: "maximum") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct EntitledPool: Codable, Equatable { public struct EntitledPool: Codable, Equatable {
public let poolId: String public let poolId: String
public let name: String public let name: String
@@ -1414,14 +1463,17 @@ public struct ManifestGateway: Codable, Equatable {
public struct ManifestProfile: Codable, Equatable { public struct ManifestProfile: Codable, Equatable {
public let id: String public let id: String
public let bounds: ManifestBounds public let bounds: ManifestBounds
public let displayMode: DisplayMode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id = "id" case id = "id"
case bounds = "bounds" case bounds = "bounds"
case displayMode = "display_mode"
} }
public init(id: String, bounds: ManifestBounds) throws { public init(id: String, bounds: ManifestBounds, displayMode: DisplayMode?) throws {
self.id = id self.id = id
self.bounds = bounds self.bounds = bounds
self.displayMode = displayMode
try validate() try validate()
} }
@@ -1429,7 +1481,7 @@ public struct ManifestProfile: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self) let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(id: try c.decode(String.self, forKey: .id), bounds: try c.decode(ManifestBounds.self, forKey: .bounds)) try self.init(id: try c.decode(String.self, forKey: .id), bounds: try c.decode(ManifestBounds.self, forKey: .bounds), displayMode: try c.contains(.displayMode) ? c.decode(DisplayMode.self, forKey: .displayMode) : nil)
} }
public func validate() throws { public func validate() throws {
@@ -1437,6 +1489,9 @@ public struct ManifestProfile: Codable, Equatable {
if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") } if !self.id.isEmpty && self.id.utf8.count < 1 { throw ContractValidationError(field: "id", code: "min_length") }
if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") } if self.id.utf8.count > 128 { throw ContractValidationError(field: "id", code: "max_length") }
try self.bounds.validate() try self.bounds.validate()
if let value = self.displayMode {
try value.validate()
}
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -2132,20 +2187,23 @@ public struct SessionRequest: Codable, Equatable {
public let poolId: String public let poolId: String
public let idempotencyKey: String public let idempotencyKey: String
public let policySnapshot: AllocationPolicy public let policySnapshot: AllocationPolicy
public let requestedDisplayMode: DisplayMode?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case clientDeviceId = "client_device_id" case clientDeviceId = "client_device_id"
case deviceKeyId = "device_key_id" case deviceKeyId = "device_key_id"
case poolId = "pool_id" case poolId = "pool_id"
case idempotencyKey = "idempotency_key" case idempotencyKey = "idempotency_key"
case policySnapshot = "policy_snapshot" case policySnapshot = "policy_snapshot"
case requestedDisplayMode = "requested_display_mode"
} }
public init(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy) throws { public init(clientDeviceId: String, deviceKeyId: String, poolId: String, idempotencyKey: String, policySnapshot: AllocationPolicy, requestedDisplayMode: DisplayMode?) throws {
self.clientDeviceId = clientDeviceId self.clientDeviceId = clientDeviceId
self.deviceKeyId = deviceKeyId self.deviceKeyId = deviceKeyId
self.poolId = poolId self.poolId = poolId
self.idempotencyKey = idempotencyKey self.idempotencyKey = idempotencyKey
self.policySnapshot = policySnapshot self.policySnapshot = policySnapshot
self.requestedDisplayMode = requestedDisplayMode
try validate() try validate()
} }
@@ -2153,7 +2211,7 @@ public struct SessionRequest: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self) let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") } for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), poolId: try c.decode(String.self, forKey: .poolId), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot)) try self.init(clientDeviceId: try c.decode(String.self, forKey: .clientDeviceId), deviceKeyId: try c.decode(String.self, forKey: .deviceKeyId), poolId: try c.decode(String.self, forKey: .poolId), idempotencyKey: try c.decode(String.self, forKey: .idempotencyKey), policySnapshot: try c.decode(AllocationPolicy.self, forKey: .policySnapshot), requestedDisplayMode: try c.contains(.requestedDisplayMode) ? c.decode(DisplayMode.self, forKey: .requestedDisplayMode) : nil)
} }
public func validate() throws { public func validate() throws {
@@ -2170,6 +2228,9 @@ public struct SessionRequest: Codable, Equatable {
if !self.idempotencyKey.isEmpty && self.idempotencyKey.utf8.count < 1 { throw ContractValidationError(field: "idempotency_key", code: "min_length") } if !self.idempotencyKey.isEmpty && self.idempotencyKey.utf8.count < 1 { throw ContractValidationError(field: "idempotency_key", code: "min_length") }
if self.idempotencyKey.utf8.count > 256 { throw ContractValidationError(field: "idempotency_key", code: "max_length") } if self.idempotencyKey.utf8.count > 256 { throw ContractValidationError(field: "idempotency_key", code: "max_length") }
try self.policySnapshot.validate() try self.policySnapshot.validate()
if let value = self.requestedDisplayMode {
try value.validate()
}
} }
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) } public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-10
@@ -0,0 +1,54 @@
## Context
The JSON control schema is the generated-binding authority for broker requests
and manifests, while VGI1 is the provider-neutral input payload. Both contracts
are strict: old decoders reject unknown response fields and old gateways reject
unknown VGI kinds. Phase 3D therefore needs optional fields plus explicit
feature negotiation rather than a wire-version or protobuf change.
## Goals / Non-Goals
**Goals:**
- Preserve legacy JSON shapes when display negotiation is absent.
- Generate optional referenced objects correctly in Go, Rust, and Swift.
- Define exact, bounded, cross-language absolute-pointer and scroll bytes.
**Non-Goals:**
- Server clamp policy, database persistence, or provider translation.
- Live display renegotiation, provider packet exposure, or protobuf changes.
## Decisions
- Keep control wire version 1 and gate additions with exact feature IDs. This
avoids changing every legacy request while allowing strict clients to demand
the accepted display field.
- Use one reusable `DisplayMode` with the existing provider-policy dimension
bounds. Optional referenced objects become Go pointers so `omitempty` is real;
Rust and Swift retain their existing optional generation.
- Encode absolute pointer as four big-endian u16 values and scroll as two
big-endian i16 values in VGI1. Viewport coordinates are self-contained and
provider-neutral; provider scaling stays outside Protocol.
- Leave protobuf unchanged because the observed broker and VGI consumers use
JSON and fixed byte frames, not generated protobuf messages.
## Risks / Trade-offs
- [Old strict consumers reject new response fields] → emit them only after
`display.request.v1` negotiation.
- [New VGI kinds reach an old or unadvertised gateway] → reject before provider
translation unless the matching input feature is active.
- [Generated Go optional values serialize as zero objects] → require pointers,
omission tests, and clean second generation.
## Migration Plan
Freeze a new never-reused Protocol RC after full verification. Server and Data
Plane then pin that exact tag together. Legacy sessions omit all new fields and
continue using existing input kinds.
## Open Questions
None at the Protocol boundary; policy clamp and Apollo translation remain
consumer-owned work.
@@ -0,0 +1,38 @@
## Why
The Phase 3D native client must request a bounded display mode and send
provider-neutral absolute-pointer and high-resolution scroll input. The current
wire contract exposes only policy-selected stream dimensions and rejects both
new input kinds.
## What Changes
- Add one bounded `DisplayMode` object and optional display fields to session
request/session/manifest contracts while keeping control wire version 1.
- Register `display.request.v1`, `input.absolute.v1`, and `input.scroll.v1` so
every new behavior is explicitly negotiated.
- Extend VGI1 with exact absolute-pointer and signed scroll bodies while
preserving all existing kind encodings.
- Require a new immutable Protocol release candidate after deterministic
Go/Rust/Swift generation and conformance pass; existing tags remain unchanged.
## Capabilities
### New Capabilities
- `session-display-mode`: Optional feature-gated requested and effective display
mode disclosure for native session creation and manifests.
### Modified Capabilities
- `gateway-input-feedback`: Add negotiated provider-neutral absolute-pointer and
high-resolution scroll kinds to the existing sequenced input grammar.
## Impact
- JSON Schema, feature registry, VGI frame documentation, fixed conformance
fixtures, generated Go/Rust/Swift bindings, and strict validators.
- Connection Server session persistence/allocation and Data Plane input
translation after consumers pin the immutable Protocol candidate.
- Protobuf remains unchanged because no observed consumer uses it for these
JSON control or VGI payload paths.
@@ -0,0 +1,33 @@
## MODIFIED Requirements
### Requirement: Typed sequenced input envelope
The `input.sequenced.v1` payload SHALL begin with ASCII `VGI1`, a one-byte
event kind, and one-byte payload length. It SHALL contain exactly one bounded
keyboard, mouse-button, relative-mouse, UTF-8 scalar, controller-state,
absolute-mouse, or high-resolution-scroll event. Absolute mouse kind `0x06`
SHALL contain big-endian u16 x, y, viewport width, and viewport height, reject
zero viewports and coordinates outside the viewport, and require negotiated
`input.absolute.v1`. Scroll kind `0x07` SHALL contain big-endian signed i16
vertical and horizontal deltas and require negotiated `input.scroll.v1`. False
keyboard/mouse state and zeroed controller state are explicit releases.
Unknown or unadvertised kinds, length mismatches, malformed UTF-8, unsupported
controller indices, and reserved fields SHALL be rejected before provider
translation.
#### Scenario: Keyboard state change
- **WHEN** a client sends a valid keyboard press or release envelope
- **THEN** the gateway forwards the corresponding typed provider input on its
reliable keyboard channel and records the pressed state for cleanup.
#### Scenario: Absolute pointer and scroll
- **WHEN** an advertised client sends an in-viewport absolute coordinate or
signed scroll delta with the exact body length
- **THEN** the gateway accepts the provider-neutral event without adding it to
pressed-state cleanup.
#### Scenario: Invalid input envelope
- **WHEN** a client sends an unknown or unadvertised kind, invalid length,
malformed UTF-8 scalar, zero viewport, out-of-range coordinate, or nonzero
reserved field
- **THEN** the gateway rejects it without sending provider input or changing
pressed state.
@@ -0,0 +1,35 @@
## ADDED Requirements
### Requirement: Bounded display mode contract
The Protocol SHALL define `DisplayMode` with required integer
`resolution_width` from 320 through 16384, `resolution_height` from 200 through
8640, and `fps` from 1 through 240, rejecting missing, unknown, or out-of-range
fields.
#### Scenario: Valid display mode
- **WHEN** a client encodes a 2560 by 1440 display mode at 120 FPS
- **THEN** generated Go, Rust, and Swift bindings accept the same exact values.
#### Scenario: Invalid display mode
- **WHEN** any dimension or FPS is outside its bound or an unknown field exists
- **THEN** strict Protocol validation rejects the object.
### Requirement: Feature-gated optional display disclosure
`SessionRequest.requested_display_mode`,
`BrokerSession.requested_display_mode`,
`BrokerSession.effective_display_mode`, and `ManifestProfile.display_mode`
SHALL be optional references to `DisplayMode`. Go bindings SHALL use pointers
with JSON omission and Rust/Swift bindings SHALL use optionals. A client MUST
send the request only after `display.request.v1` negotiation, and a
display-aware manifest MUST contain the accepted mode. Legacy requests SHALL
produce legacy response shapes without these fields.
#### Scenario: Legacy request remains unchanged
- **WHEN** a client omits `requested_display_mode`
- **THEN** encoding omits the field and compatible Server responses omit all
display-mode fields.
#### Scenario: Display-aware request discloses acceptance
- **WHEN** a negotiated client sends a valid requested mode
- **THEN** session responses preserve the requested mode and the allocated
manifest includes the Server-accepted mode.
@@ -0,0 +1,17 @@
## 1. Contract sources
- [x] 1.1 Add failing Go omission/bounds tests and fixed absolute/scroll vectors.
- [x] 1.2 Add the bounded display schema, optional generated references, and feature registrations.
- [x] 1.3 Extend the VGI frame grammar and Python/Go/Rust/Swift classifiers without changing existing kinds.
## 2. Deterministic verification
- [x] 2.1 Regenerate Go/Rust/Swift bindings and pass focused Go, frame, source, binding, and strict-contract checks.
- [x] 2.2 Run the complete Protocol verifier with isolated compiler caches.
- [x] 2.3 Regenerate a second time, confirm zero drift, and inspect the exact source/generated diff.
## 3. Freeze
- [ ] 3.1 Commit the verified source and generated bindings as one unsigned capability-sized change.
- [ ] 3.2 Verify the next Phase 3D Protocol RC name is unused locally and remotely, then create one local immutable candidate tag without publishing it.
- [ ] 3.3 Reconcile and archive this OpenSpec change after the immutable candidate is frozen.
+4 -1
View File
@@ -3,6 +3,9 @@
"features": [ "features": [
{"id": "control.v1", "direction": "bidirectional", "max_frame_bytes": 65536}, {"id": "control.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
{"id": "events.resume.v1", "direction": "server-to-client", "max_frame_bytes": 65536}, {"id": "events.resume.v1", "direction": "server-to-client", "max_frame_bytes": 65536},
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_frame_bytes": 65536} {"id": "clipboard.text.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
{"id": "display.request.v1", "direction": "client-to-server", "max_frame_bytes": 65536},
{"id": "input.absolute.v1", "direction": "client-to-server", "max_frame_bytes": 14},
{"id": "input.scroll.v1", "direction": "client-to-server", "max_frame_bytes": 10}
] ]
} }
+17 -3
View File
@@ -178,6 +178,16 @@
"refresh_expires_at": {"type": "string", "format": "date-time", "maxLength": 64} "refresh_expires_at": {"type": "string", "format": "date-time", "maxLength": 64}
} }
}, },
"DisplayMode": {
"type": "object",
"additionalProperties": false,
"required": ["resolution_width", "resolution_height", "fps"],
"properties": {
"resolution_width": {"type": "integer", "minimum": 320, "maximum": 16384},
"resolution_height": {"type": "integer", "minimum": 200, "maximum": 8640},
"fps": {"type": "integer", "minimum": 1, "maximum": 240}
}
},
"SessionRequest": { "SessionRequest": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -187,7 +197,8 @@
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128}, "device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, "pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256}, "idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
"policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"} "policy_snapshot": {"$ref": "#/$defs/AllocationPolicy"},
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"ReconnectRequest": { "ReconnectRequest": {
@@ -219,7 +230,9 @@
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128}, "correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
"requested_at": {"type": "string", "format": "date-time", "maxLength": 64}, "requested_at": {"type": "string", "format": "date-time", "maxLength": 64},
"ended_at": {"type": "string", "format": "date-time", "maxLength": 64}, "ended_at": {"type": "string", "format": "date-time", "maxLength": 64},
"version": {"type": "integer", "minimum": 1} "version": {"type": "integer", "minimum": 1},
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"},
"effective_display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"AllocationPolicy": { "AllocationPolicy": {
@@ -279,7 +292,8 @@
"required": ["id", "bounds"], "required": ["id", "bounds"],
"properties": { "properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 128}, "id": {"type": "string", "minLength": 1, "maxLength": 128},
"bounds": {"$ref": "#/$defs/ManifestBounds"} "bounds": {"$ref": "#/$defs/ManifestBounds"},
"display_mode": {"$ref": "#/$defs/DisplayMode"}
} }
}, },
"ManifestBounds": { "ManifestBounds": {
+78
View File
@@ -1,6 +1,7 @@
package protocol_test package protocol_test
import ( import (
"bytes"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -22,6 +23,83 @@ func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) {
} }
} }
func TestDisplayModeIsOptionalStrictAndBounded(t *testing.T) {
displayType := reflect.TypeOf(protocol.DisplayMode{})
for _, field := range []struct {
owner reflect.Type
name string
}{
{reflect.TypeOf(protocol.SessionRequest{}), "RequestedDisplayMode"},
{reflect.TypeOf(protocol.BrokerSession{}), "RequestedDisplayMode"},
{reflect.TypeOf(protocol.BrokerSession{}), "EffectiveDisplayMode"},
{reflect.TypeOf(protocol.ManifestProfile{}), "DisplayMode"},
} {
actual, ok := field.owner.FieldByName(field.name)
if !ok || actual.Type.Kind() != reflect.Pointer || actual.Type.Elem() != displayType {
t.Fatalf("%s.%s must be *DisplayMode, got %v", field.owner, field.name, actual.Type)
}
}
request := protocol.SessionRequest{
ClientDeviceID: "device-1", DeviceKeyID: "key-1", PoolID: "pool-1", IdempotencyKey: "request-1",
PolicySnapshot: protocol.AllocationPolicy{
MinimumKbps: 1000, TargetKbps: 2000, MaximumKbps: 3000, Tier: "standard",
Audience: "versevdi-gateway", Protocol: "verse", ProtocolVersion: 1,
GrantTTLSeconds: 60, ReservationLeaseSeconds: 300,
},
}
legacy, err := protocol.EncodeSessionRequest(request)
if err != nil {
t.Fatalf("EncodeSessionRequest legacy request: %v", err)
}
if bytes.Contains(legacy, []byte("requested_display_mode")) {
t.Fatalf("legacy request disclosed an absent display mode: %s", legacy)
}
mode := protocol.DisplayMode{ResolutionWidth: 2560, ResolutionHeight: 1440, Fps: 120}
request.RequestedDisplayMode = &mode
aware, err := protocol.EncodeSessionRequest(request)
if err != nil || !bytes.Contains(aware, []byte(`"requested_display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120}`)) {
t.Fatalf("display-aware request = %s, err = %v", aware, err)
}
decoded, err := protocol.DecodeSessionRequest(aware)
if err != nil || decoded.RequestedDisplayMode == nil || *decoded.RequestedDisplayMode != mode {
t.Fatalf("DecodeSessionRequest display mode = %+v, err = %v", decoded.RequestedDisplayMode, err)
}
nullMode := bytes.Replace(aware, []byte(`{"resolution_width":2560,"resolution_height":1440,"fps":120}`), []byte("null"), 1)
if _, err := protocol.DecodeSessionRequest(nullMode); err == nil {
t.Fatalf("DecodeSessionRequest accepted explicit null display mode: %s", nullMode)
}
profile := protocol.ManifestProfile{
ID: "standard",
Bounds: protocol.ManifestBounds{MinimumKbps: 1000, TargetKbps: 2000, MaximumKbps: 3000},
}
legacyProfile, err := protocol.EncodeManifestProfile(profile)
if err != nil || bytes.Contains(legacyProfile, []byte("display_mode")) {
t.Fatalf("legacy profile = %s, err = %v", legacyProfile, err)
}
profile.DisplayMode = &mode
awareProfile, err := protocol.EncodeManifestProfile(profile)
if err != nil || !bytes.Contains(awareProfile, []byte(`"display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120}`)) {
t.Fatalf("display-aware profile = %s, err = %v", awareProfile, err)
}
for _, invalid := range []string{
`{"resolution_width":319,"resolution_height":1440,"fps":120}`,
`{"resolution_width":16385,"resolution_height":1440,"fps":120}`,
`{"resolution_width":2560,"resolution_height":199,"fps":120}`,
`{"resolution_width":2560,"resolution_height":8641,"fps":120}`,
`{"resolution_width":2560,"resolution_height":1440,"fps":0}`,
`{"resolution_width":2560,"resolution_height":1440,"fps":241}`,
`{"resolution_width":2560,"resolution_height":1440,"fps":120,"unknown":true}`,
} {
if _, err := protocol.DecodeDisplayMode([]byte(invalid)); err == nil {
t.Fatalf("DecodeDisplayMode accepted %s", invalid)
}
}
}
func TestPageInfoRejectsOutOfBoundsLimit(t *testing.T) { func TestPageInfoRejectsOutOfBoundsLimit(t *testing.T) {
if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil { if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil {
t.Fatal("DecodePageInfo accepted limit above the contract maximum") t.Fatal("DecodePageInfo accepted limit above the contract maximum")
+16 -2
View File
@@ -160,7 +160,11 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}") lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
reference = ref_name(prop) reference = ref_name(prop)
if reference: if reference:
lines.append(f"\tif err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}") validation = f"if err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}"
if prop_name not in required:
lines.append(f"\tif v.{field} != nil {{ {validation} }}")
else:
lines.append(f"\t{validation}")
if name in {"AllocationPolicy", "ManifestBounds"}: if name in {"AllocationPolicy", "ManifestBounds"}:
lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }") lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }")
if name == "GatewayRegistration": if name == "GatewayRegistration":
@@ -212,7 +216,10 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
required = set(definition.get("required", [])) required = set(definition.get("required", []))
for prop_name, prop in definition.get("properties", {}).items(): for prop_name, prop in definition.get("properties", {}).items():
tag = prop_name + (",omitempty" if prop_name not in required else "") tag = prop_name + (",omitempty" if prop_name not in required else "")
out.append(f"\t{go_field(prop_name)} {prop_type(prop, 'go')} `json:\"{tag}\"`") typ = prop_type(prop, "go")
if prop_name not in required and ref_name(prop):
typ = "*" + typ
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
out.extend(["}", ""]) out.extend(["}", ""])
for name in sorted(defs): for name in sorted(defs):
out.append(f"func (v {name}) Validate() error {{") out.append(f"func (v {name}) Validate() error {{")
@@ -234,6 +241,11 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
% (prop_name, prop_name) % (prop_name, prop_name)
) )
for prop_name, prop in defs[name].get("properties", {}).items(): for prop_name, prop in defs[name].get("properties", {}).items():
if prop_name not in required_fields and ref_name(prop):
out.append(
'\tif raw, ok := fields["%s"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "invalid_object"}}} }'
% (prop_name, prop_name)
)
if "x-max-bytes" in prop and prop.get("type") != "string": if "x-max-bytes" in prop and prop.get("type") != "string":
out.append( out.append(
'\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }' '\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }'
@@ -603,6 +615,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
typ = swift_type(prop) typ = swift_type(prop)
if prop_name in required: if prop_name in required:
decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})") decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})")
elif ref_name(prop):
decoded.append(f"{field}: try c.contains(.{field}) ? c.decode({typ}.self, forKey: .{field}) : nil")
else: else:
decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})") decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})")
out.append(f" try self.init({', '.join(decoded)})") out.append(f" try self.init({', '.join(decoded)})")
+17 -2
View File
@@ -125,10 +125,12 @@ func evaluate(kind, input string) string {
} }
return "valid" return "valid"
case "tunnel": case "tunnel":
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && parts["feature"] == "control.v1" { feature := parts["feature"]
registered := feature == "control.v1" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && registered {
return "valid" return "valid"
} }
if parts["feature"] != "control.v1" { if !registered {
return "invalid:unsupported_feature" return "invalid:unsupported_feature"
} }
return "invalid:unsupported_version" return "invalid:unsupported_version"
@@ -226,6 +228,19 @@ func classifyGatewayInput(encoded string) string {
} }
} }
} }
case 6:
if len(body) != 8 {
return "invalid:length"
}
x, y := uint16(body[0])<<8|uint16(body[1]), uint16(body[2])<<8|uint16(body[3])
width, height := uint16(body[4])<<8|uint16(body[5]), uint16(body[6])<<8|uint16(body[7])
if width == 0 || height == 0 || x >= width || y >= height {
return "invalid:field"
}
case 7:
if len(body) != 4 {
return "invalid:length"
}
default: default:
return "invalid:kind" return "invalid:kind"
} }
+12 -2
View File
@@ -47,8 +47,8 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
|| !values.contains_key("correlation_id") => "invalid:required", || !values.contains_key("correlation_id") => "invalid:required",
"event" => "valid", "event" => "valid",
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1")) "tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1"))
&& values.get("feature").map(String::as_str) == Some("control.v1") => "valid", && matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => "valid",
"tunnel" if values.get("feature").map(String::as_str) != Some("control.v1") => { "tunnel" if !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => {
"invalid:unsupported_feature" "invalid:unsupported_feature"
} }
"tunnel" => "invalid:unsupported_version", "tunnel" => "invalid:unsupported_version",
@@ -110,6 +110,16 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
5 if body[0] > 15 => "invalid:field", 5 if body[0] > 15 => "invalid:field",
5 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field", 5 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field",
5 => "valid", 5 => "valid",
6 if body.len() != 8 => "invalid:length",
6 => {
let x = u16::from_be_bytes([body[0], body[1]]);
let y = u16::from_be_bytes([body[2], body[3]]);
let width = u16::from_be_bytes([body[4], body[5]]);
let height = u16::from_be_bytes([body[6], body[7]]);
if width != 0 && height != 0 && x < width && y < height { "valid" } else { "invalid:field" }
}
7 if body.len() == 4 => "valid",
7 => "invalid:length",
_ => "invalid:kind", _ => "invalid:kind",
} }
} }
+11 -2
View File
@@ -27,8 +27,9 @@ func evaluate(_ kind: String, _ input: String) -> String {
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" } guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
return "valid" return "valid"
case "tunnel": case "tunnel":
if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" } let registered = ["control.v1", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_feature" if ["1", "0", "-1"].contains(values["offered"] ?? "") && registered { return "valid" }
return registered ? "invalid:unsupported_version" : "invalid:unsupported_feature"
case "datagram": return classifyDatagram(values["hex"] ?? "") case "datagram": return classifyDatagram(values["hex"] ?? "")
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "") case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "") case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "")
@@ -82,6 +83,14 @@ func classifyGatewayInput(_ encoded: String) -> String {
guard body[0] <= 15 else { return "invalid:field" } guard body[0] <= 15 else { return "invalid:field" }
guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" } guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" }
return "valid" return "valid"
case 6:
guard body.count == 8 else { return "invalid:length" }
let x = Int(body[0]) * 256 + Int(body[1])
let y = Int(body[2]) * 256 + Int(body[3])
let width = Int(body[4]) * 256 + Int(body[5])
let height = Int(body[6]) * 256 + Int(body[7])
return width > 0 && height > 0 && x < width && y < height ? "valid" : "invalid:field"
case 7: return body.count == 4 ? "valid" : "invalid:length"
default: return "invalid:kind" default: return "invalid:kind"
} }
} }
+56
View File
@@ -103,6 +103,44 @@ do {
) )
fatalError("invalid allocation bounds were accepted") fatalError("invalid allocation bounds were accepted")
} catch { } } catch { }
let displayMode = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
for invalid in [
{ try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) },
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) },
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241) },
] {
do {
_ = try invalid()
fatalError("invalid display mode was accepted")
} catch { }
}
let allocationPolicy = try AllocationPolicy(
minimumKbps: 1000, targetKbps: 2000, maximumKbps: 3000, tier: "standard",
audience: "versevdi-gateway", protocolValue: "verse", protocolVersion: 1,
grantTtlSeconds: 60, reservationLeaseSeconds: 300
)
let legacyDisplayRequest = try SessionRequest(
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
requestedDisplayMode: nil
).encodeJSON()
guard !String(data: legacyDisplayRequest, encoding: .utf8)!.contains("requested_display_mode") else {
fatalError("legacy request encoded an absent display mode")
}
let displayRequest = try SessionRequest(
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
requestedDisplayMode: displayMode
)
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
fatalError("display mode did not round-trip")
}
var nullDisplayRequest = try JSONSerialization.jsonObject(with: displayRequest.encodeJSON()) as! [String: Any]
nullDisplayRequest["requested_display_mode"] = NSNull()
do {
_ = try SessionRequest.decodeJSON(try JSONSerialization.data(withJSONObject: nullDisplayRequest))
fatalError("explicit null display mode was accepted")
} catch { }
let streamPolicy = try ProviderStreamPolicy( let streamPolicy = try ProviderStreamPolicy(
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
@@ -218,6 +256,24 @@ fn main() {
assert!(AllocationPolicy::new( assert!(AllocationPolicy::new(
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300, 100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
).is_err()); ).is_err());
let display_mode = DisplayMode::new(2560, 1440, 120).unwrap();
assert!(DisplayMode::new(319, 1440, 120).is_err());
assert!(DisplayMode::new(2560, 199, 120).is_err());
assert!(DisplayMode::new(2560, 1440, 241).is_err());
let allocation_policy = AllocationPolicy::new(
1000, 2000, 3000, "standard".into(), "versevdi-gateway".into(),
"verse".into(), 1, 60, 300,
).unwrap();
let legacy_display_request = SessionRequest::new(
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
allocation_policy.clone(), None,
).unwrap();
assert!(legacy_display_request.requestedDisplayMode().is_none());
let display_request = SessionRequest::new(
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
allocation_policy, Some(display_mode.clone()),
).unwrap();
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
assert!(ProviderStreamPolicy::new( assert!(ProviderStreamPolicy::new(
2560, 1440, 120, "HEVC".into(), 40000, true, 2560, 1440, 120, "HEVC".into(), 40000, true,
).is_ok()); ).is_ok());
+18
View File
@@ -36,6 +36,24 @@ def main() -> int:
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes")) maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
assert isinstance(maximum, int) and 1 <= maximum <= maximum_bound assert isinstance(maximum, int) and 1 <= maximum <= maximum_bound
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
registered_features = {entry["id"] for entry in feature_registry["features"]}
assert {"display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
display_mode = defs["DisplayMode"]
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
assert display_mode["properties"]["resolution_width"] == {"type": "integer", "minimum": 320, "maximum": 16384}
assert display_mode["properties"]["resolution_height"] == {"type": "integer", "minimum": 200, "maximum": 8640}
assert display_mode["properties"]["fps"] == {"type": "integer", "minimum": 1, "maximum": 240}
for owner, field in (
("SessionRequest", "requested_display_mode"),
("BrokerSession", "requested_display_mode"),
("BrokerSession", "effective_display_mode"),
("ManifestProfile", "display_mode"),
):
assert field not in defs[owner]["required"]
assert defs[owner]["properties"][field] == {"$ref": "#/$defs/DisplayMode"}
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8")) manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"])) assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8")) forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8"))
+7
View File
@@ -37,6 +37,13 @@ def classify_input(raw: bytes) -> str:
return "invalid:field" return "invalid:field"
active_mask = int.from_bytes(body[1:3], "big") active_mask = int.from_bytes(body[1:3], "big")
return "valid" if active_mask or not any(body[3:]) else "invalid:field" return "valid" if active_mask or not any(body[3:]) else "invalid:field"
if kind == 6:
if len(body) != 8:
return "invalid:length"
x, y, width, height = (int.from_bytes(body[index:index + 2], "big") for index in range(0, 8, 2))
return "valid" if width and height and x < width and y < height else "invalid:field"
if kind == 7:
return "valid" if len(body) == 4 else "invalid:length"
return "invalid:kind" return "invalid:kind"