Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
408d4f9cc3 | ||
|
|
346bf5fe4d | ||
|
|
7c89ef5bf5 | ||
|
|
c554cac00d | ||
|
|
d1d00d6472 | ||
|
|
03e14a9ae3 | ||
|
|
995f63a27f |
@@ -20,8 +20,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||||
with:
|
with:
|
||||||
go-version: "1.26.5"
|
go-version: "1.26.5"
|
||||||
cache: true
|
cache: true
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
- name: Assert pinned toolchain
|
- name: Assert pinned toolchain
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated
|
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify ci-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated
|
||||||
|
|
||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
PROTOC ?= protoc
|
PROTOC ?= protoc
|
||||||
@@ -23,6 +23,9 @@ scope-verify:
|
|||||||
$(PYTHON) -B tools/test_check_scope.py
|
$(PYTHON) -B tools/test_check_scope.py
|
||||||
$(PYTHON) -B tools/check_scope.py
|
$(PYTHON) -B tools/check_scope.py
|
||||||
|
|
||||||
|
ci-verify:
|
||||||
|
$(PYTHON) -B tools/check_ci_actions.py
|
||||||
|
|
||||||
go-test:
|
go-test:
|
||||||
go test ./gen/go/... ./tests/go
|
go test ./gen/go/... ./tests/go
|
||||||
|
|
||||||
@@ -45,4 +48,4 @@ frame-verify:
|
|||||||
clean-generated:
|
clean-generated:
|
||||||
$(PYTHON) tools/generate.py --check
|
$(PYTHON) tools/generate.py --check
|
||||||
|
|
||||||
verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated
|
verify: generate proto-lint proto-breaking source-verify scope-verify ci-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id version kind input expected
|
id version kind input expected
|
||||||
valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
|
valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
|
||||||
invalid-short 1 datagram hex=564401 invalid:truncated
|
invalid-short 1 datagram hex=564401 invalid:truncated
|
||||||
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
invalid-version 1 datagram hex=564403010000000000000000000000000000010000 invalid:unsupported_version
|
||||||
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
||||||
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
||||||
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
|
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
|
||||||
|
|||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
id version kind input expected
|
||||||
|
v2-valid-video-single 2 datagram hex=5644020a00000000010000000000000002000000010003010203 valid
|
||||||
|
v2-valid-video-last-fragment 2 datagram hex=5644020a00000000010000000000000002037a037b0000 valid
|
||||||
|
v2-invalid-short 2 datagram hex=564402 invalid:truncated
|
||||||
|
v2-invalid-version 2 datagram hex=5644030a00000000010000000000000002000000010000 invalid:unsupported_version
|
||||||
|
v2-invalid-channel 2 datagram hex=5644020d00000000010000000000000002000000010000 invalid:unknown_channel
|
||||||
|
v2-invalid-fragment-zero 2 datagram hex=5644020a00000000010000000000000002000000000000 invalid:fragment
|
||||||
|
v2-invalid-fragment-index 2 datagram hex=5644020a00000000010000000000000002000100010000 invalid:fragment
|
||||||
|
v2-invalid-fragment-count-limit 2 datagram hex=5644020a000000000100000000000000020000037c0000 invalid:fragment_limit
|
||||||
|
v2-invalid-length 2 datagram hex=5644020a00000000010000000000000002000000010001 invalid:length_mismatch
|
||||||
|
@@ -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
|
||||||
|
|||||||
|
@@ -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
|
||||||
|
|||||||
|
@@ -3,11 +3,12 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"fixtures/conformance/control-v1.tsv",
|
"fixtures/conformance/control-v1.tsv",
|
||||||
"fixtures/conformance/datagram-v1.tsv",
|
"fixtures/conformance/datagram-v1.tsv",
|
||||||
|
"fixtures/conformance/datagram-v2.tsv",
|
||||||
"fixtures/conformance/events-v1.tsv",
|
"fixtures/conformance/events-v1.tsv",
|
||||||
"fixtures/conformance/gateway-clipboard-audit-v1.tsv",
|
"fixtures/conformance/gateway-clipboard-audit-v1.tsv",
|
||||||
"fixtures/conformance/gateway-clipboard-v1.tsv",
|
"fixtures/conformance/gateway-clipboard-v1.tsv",
|
||||||
"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": "0eb9e905e77069c0bd67182b40e7f9b1b41041d5290b0e3b2152abf83ce32eae"
|
"corpus_sha256": "4f8bf520d4f61588235b00776b21202ff701b20d6d11b461037b911c2117852b"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# VerseVDI complete media datagram v2
|
||||||
|
|
||||||
|
Datagram v2 carries one complete encoded video or audio unit under one sequence.
|
||||||
|
It is negotiated explicitly as `datagram-v2`; v1 bytes are never reinterpreted.
|
||||||
|
The gateway relays encoded bytes without codec processing or provider exposure.
|
||||||
|
|
||||||
|
All multi-byte integers are unsigned big-endian. The fixed header is 23 bytes:
|
||||||
|
|
||||||
|
| Offset | Size | Field | Rule |
|
||||||
|
|---:|---:|---|---|
|
||||||
|
| 0 | 2 | magic | ASCII `VD` (`0x56 0x44`) |
|
||||||
|
| 2 | 1 | version | `2` only |
|
||||||
|
| 3 | 1 | channel | `media.video.v1` (10) or `media.audio.v1` (11) |
|
||||||
|
| 4 | 1 | flags | zero; unknown bits reject |
|
||||||
|
| 5 | 4 | sequence | session-local wrapping complete-unit sequence |
|
||||||
|
| 9 | 8 | timestamp_ms | sender timestamp, bounded by transport skew policy |
|
||||||
|
| 17 | 2 | fragment_index | zero-based |
|
||||||
|
| 19 | 2 | fragment_count | 1 through 891; index less than count |
|
||||||
|
| 21 | 2 | payload_length | exact payload byte count, at most 1,177 |
|
||||||
|
|
||||||
|
Each QUIC datagram is at most 1,200 bytes. One complete unit is at most
|
||||||
|
1,048,576 encoded bytes and 891 fragments. A sender rejects a larger unit
|
||||||
|
before fragmentation.
|
||||||
|
|
||||||
|
A receiver retains at most four incomplete media units and only received
|
||||||
|
fragment bytes. Fragments for one unit must agree on channel, sequence,
|
||||||
|
timestamp, flags, and count. Exact duplicates are ignored; conflicting
|
||||||
|
duplicates reject that unit. Bounded reorder is accepted. An incomplete unit
|
||||||
|
expires after 250 milliseconds, and accepting a fifth incomplete unit evicts
|
||||||
|
the oldest. Reassembly checks the 1,048,576-byte ceiling before appending and
|
||||||
|
emits only after every fragment is present.
|
||||||
@@ -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`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
|
"framing_profiles": ["datagram-v1", "datagram-v2"],
|
||||||
"header_bytes": 21,
|
"header_bytes": 21,
|
||||||
"maximum_frame_bytes": 65536,
|
"maximum_frame_bytes": 65536,
|
||||||
"channels": [
|
"channels": [
|
||||||
|
|||||||
+123
-6
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
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"
|
||||||
@@ -65,6 +65,8 @@ type BrokerSession struct {
|
|||||||
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"`
|
||||||
@@ -265,6 +273,7 @@ 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 {
|
||||||
@@ -388,6 +397,7 @@ type SessionRequest struct {
|
|||||||
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 {
|
||||||
@@ -851,11 +877,8 @@ func (v CapabilityProfile) Validate() error {
|
|||||||
if v.Framing == "" {
|
if v.Framing == "" {
|
||||||
violations = append(violations, FieldViolation{Field: "framing", Code: "required"})
|
violations = append(violations, FieldViolation{Field: "framing", Code: "required"})
|
||||||
}
|
}
|
||||||
if len(v.Framing) < 1 && v.Framing != "" {
|
if v.Framing != "" && !(v.Framing == "datagram-v1" || v.Framing == "datagram-v2") {
|
||||||
violations = append(violations, FieldViolation{Field: "framing", Code: "min_length"})
|
violations = append(violations, FieldViolation{Field: "framing", Code: "invalid_value"})
|
||||||
}
|
|
||||||
if len(v.Framing) > 64 {
|
|
||||||
violations = append(violations, FieldViolation{Field: "framing", Code: "max_length"})
|
|
||||||
}
|
}
|
||||||
if v.Media == "" {
|
if v.Media == "" {
|
||||||
violations = append(violations, FieldViolation{Field: "media", Code: "required"})
|
violations = append(violations, FieldViolation{Field: "media", Code: "required"})
|
||||||
@@ -1628,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 == "" {
|
||||||
@@ -3205,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}
|
||||||
}
|
}
|
||||||
@@ -3226,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 {
|
||||||
@@ -4658,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}
|
||||||
}
|
}
|
||||||
@@ -4688,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
@@ -12,7 +12,7 @@
|
|||||||
"2"
|
"2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
|
"generator_sha256": "992235a56d3467313148f86e47931f247591e4c8de737b55ac9c9eee35725fc5",
|
||||||
"protocol_version": "1.0.0",
|
"protocol_version": "1.0.0",
|
||||||
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
"schema_sha256": "b2bb0a8ac8ef56dbc0e1443eeb5b3028be9e71ec2f5fd8e73928d71b7cd9340c"
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-10
@@ -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 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716";
|
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)]
|
||||||
@@ -223,9 +233,7 @@ impl CapabilityProfile {
|
|||||||
if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); }
|
if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); }
|
||||||
if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); }
|
if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); }
|
||||||
if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); }
|
if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); }
|
||||||
if self.framing.is_empty() { return Err(ValidationError::new("framing", "required")); }
|
if self.framing != "datagram-v1" && self.framing != "datagram-v2" { return Err(ValidationError::new("framing", "invalid_value")); }
|
||||||
if !self.framing.is_empty() && self.framing.len() < 1 { return Err(ValidationError::new("framing", "min_length")); }
|
|
||||||
if self.framing.len() > 64 { return Err(ValidationError::new("framing", "max_length")); }
|
|
||||||
if self.media.is_empty() { return Err(ValidationError::new("media", "required")); }
|
if self.media.is_empty() { return Err(ValidationError::new("media", "required")); }
|
||||||
if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); }
|
if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); }
|
||||||
if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); }
|
if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); }
|
||||||
@@ -496,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,
|
||||||
@@ -1082,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)
|
||||||
}
|
}
|
||||||
@@ -1095,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)]
|
||||||
@@ -1615,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)
|
||||||
}
|
}
|
||||||
@@ -1637,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 }
|
||||||
@@ -1644,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)]
|
||||||
|
|||||||
+69
-10
@@ -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 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
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) }
|
||||||
@@ -278,9 +290,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
|||||||
if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") }
|
if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") }
|
||||||
if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") }
|
if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") }
|
||||||
if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") }
|
if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") }
|
||||||
if self.framing.isEmpty { throw ContractValidationError(field: "framing", code: "required") }
|
if !["datagram-v1", "datagram-v2"].contains(self.framing) { throw ContractValidationError(field: "framing", code: "invalid_value") }
|
||||||
if !self.framing.isEmpty && self.framing.utf8.count < 1 { throw ContractValidationError(field: "framing", code: "min_length") }
|
|
||||||
if self.framing.utf8.count > 64 { throw ContractValidationError(field: "framing", code: "max_length") }
|
|
||||||
if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") }
|
if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") }
|
||||||
if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") }
|
if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") }
|
||||||
if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") }
|
if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") }
|
||||||
@@ -634,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
|
||||||
@@ -1416,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,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 {
|
||||||
@@ -1439,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) }
|
||||||
@@ -2134,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2155,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 {
|
||||||
@@ -2172,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) }
|
||||||
|
|||||||
+2
-2
@@ -11,5 +11,5 @@
|
|||||||
|
|
||||||
## 3. Immutable Release
|
## 3. Immutable Release
|
||||||
|
|
||||||
- [ ] 3.1 Verify the next version is unused locally and remotely, publish one immutable annotated tag, and verify its object and peeled commit
|
- [x] 3.1 Verify the next version is unused locally and remotely, publish one immutable annotated tag, and verify its object and peeled commit
|
||||||
- [ ] 3.2 Resolve the version from separate empty caches and pin exact fetched checksums in Data Plane and Connection Server
|
- [x] 3.2 Resolve the version from separate empty caches and pin exact fetched checksums in Data Plane and Connection Server
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-07-30
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
RC9 datagram-v1 uses one-byte fragment fields and caps a media unit at 16 path-MTU fragments. The reviewed Apollo assembler can recover an encoded frame up to 1,028,152 bytes, so the replacement contract must carry at least that size while rejecting proportional allocation from untrusted header claims.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Preserve datagram-v1 byte-for-byte and add explicitly negotiated datagram-v2.
|
||||||
|
- Keep each QUIC datagram at 1,200 bytes or less.
|
||||||
|
- Bound one complete encoded unit to 1 MiB, at most 891 fragments, four incomplete units, and a 250 ms reassembly lifetime.
|
||||||
|
- Define duplicate, reorder, timeout, and malformed behavior for every validator.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Provider packet exposure, codec parsing, decode/transcode, retransmission, or a generic transport framework.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Datagram-v2 widens fragment index and count to unsigned 16-bit network-order fields, making a 23-byte header and 1,177-byte media payload. This is the smallest header change that covers the reviewed provider maximum without unrelated sequences.
|
||||||
|
- Sequence identifies one complete unit. All fragments must agree on channel, sequence, timestamp, count, and flags.
|
||||||
|
- Reassembly stores only received fragment bytes, rejects conflicting duplicates, permits exact duplicates and bounded reorder, evicts oldest state above four units, and expires state after 250 ms.
|
||||||
|
- Capability validation registers `datagram-v1` and `datagram-v2`; exact profile equality keeps downgrade behavior fail closed.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [More fragments increase per-frame overhead] → retain the 1,200-byte PMTU envelope and the 1 MiB frame ceiling.
|
||||||
|
- [Four concurrent 1 MiB units can consume bounded memory] → allocate only received bytes and evict/expire incomplete state.
|
||||||
|
- [RC9 peers cannot validate datagram-v2] → require a new immutable Protocol release and exact consumer pins; never mutate RC9.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The RC9 datagram contract limits one encoded media unit to 18,864 bytes, below the bounded complete frames required by the reviewed Apollo adapter and the fixed Phase 3C profiles. A new immutable Protocol release must define complete-frame transport and reassembly bounds before consumers can relay realistic encoded frames without mutation.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a versioned PMTU-safe media datagram header with wider fragment fields.
|
||||||
|
- Bound complete encoded frames, fragments, reassembly state, duplicates, reorder, timeout, and allocation.
|
||||||
|
- Preserve RC9 datagram-v1 unchanged and require explicit negotiation of the new framing profile.
|
||||||
|
- Add fixed Python, Go, Rust, and Swift conformance vectors for valid and malformed framing.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `gateway-media-framing`: Versioned complete encoded-frame fragmentation and bounded reassembly contract.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Protocol framing documentation, registries, capability schema, fixed conformance fixtures, validators, generated Go/Rust/Swift bindings, and immutable consumer versioning. Requirements: SYS-002, SYS-003, P3C-006, P3C-007, P3C-008, P3C-030, P3C-038, VER-001, VER-002, VER-028.
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Versioned complete encoded-frame datagrams
|
||||||
|
The Protocol SHALL register `datagram-v2` with a 23-byte network-order header containing magic, version, channel, flags, sequence, timestamp, unsigned 16-bit fragment index, unsigned 16-bit fragment count, and unsigned 16-bit payload length. Media datagrams MUST remain at most 1,200 bytes with at most 1,177 payload bytes, and one sequence MUST represent exactly one complete encoded unit of at most 1,048,576 bytes and 891 fragments.
|
||||||
|
|
||||||
|
#### Scenario: Bounded large encoded frame
|
||||||
|
- **WHEN** an encoded media frame is larger than the datagram-v1 18,864-byte limit but no larger than 1,048,576 bytes
|
||||||
|
- **THEN** datagram-v2 carries it under one sequence in at most 891 independently bounded fragments
|
||||||
|
|
||||||
|
#### Scenario: Oversized encoded frame
|
||||||
|
- **WHEN** a sender attempts more than 1,048,576 complete bytes or 891 fragments
|
||||||
|
- **THEN** framing rejects the unit before sending or allocating proportional state
|
||||||
|
|
||||||
|
### Requirement: Bounded complete-frame reassembly
|
||||||
|
Datagram-v2 reassembly SHALL retain at most four incomplete media units and only the bytes actually received, permit bounded fragment reorder and exact duplicate fragments, reject conflicting duplicates or inconsistent metadata, expire incomplete state after 250 milliseconds, and emit a payload only after every fragment is present exactly once.
|
||||||
|
|
||||||
|
#### Scenario: Reordered frame completes
|
||||||
|
- **WHEN** every valid fragment for one unit arrives out of order within the reassembly bounds
|
||||||
|
- **THEN** the receiver emits exactly one byte-identical complete encoded frame with its original boundary
|
||||||
|
|
||||||
|
#### Scenario: Malformed or stale reassembly
|
||||||
|
- **WHEN** fragments conflict, metadata changes, an index or count is invalid, state exceeds four incomplete units, or a unit exceeds its timeout
|
||||||
|
- **THEN** the receiver rejects or evicts that incomplete unit without proportional allocation or payload emission
|
||||||
|
|
||||||
|
### Requirement: Explicit framing negotiation
|
||||||
|
`CapabilityProfile.framing` SHALL accept only registered `datagram-v1` or `datagram-v2` values. A peer MUST use datagram-v2 only after exact capability intersection and MUST NOT reinterpret datagram-v1 bytes as datagram-v2.
|
||||||
|
|
||||||
|
#### Scenario: Independent datagram-v2 peers overlap
|
||||||
|
- **WHEN** both peers advertise the registered datagram-v2 framing profile
|
||||||
|
- **THEN** generated Go, Rust, and Swift validation and fixed conformance accept the profile
|
||||||
|
|
||||||
|
#### Scenario: Unknown or downgraded framing
|
||||||
|
- **WHEN** a peer advertises an unknown framing value or the peers advertise different versions
|
||||||
|
- **THEN** capability validation or intersection rejects the session before media forwarding
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
## 1. Red conformance
|
||||||
|
|
||||||
|
- [x] 1.1 Add fixed datagram-v2 and capability vectors that fail current Python, Go, Rust, and Swift validators
|
||||||
|
- [x] 1.2 Prove unknown version, malformed length, invalid fragment, and oversized complete-unit cases remain rejected
|
||||||
|
|
||||||
|
## 2. Contract implementation
|
||||||
|
|
||||||
|
- [x] 2.1 Add the versioned datagram-v2 frame grammar and registered framing values
|
||||||
|
- [x] 2.2 Update generator/native validators and regenerate Go, Rust, and Swift bindings normally
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 Run focused cross-language conformance and strict OpenSpec validation
|
||||||
|
- [x] 3.2 Run complete `make verify` and prove a second generation has no drift
|
||||||
|
|
||||||
|
## 4. Immutable boundary
|
||||||
|
|
||||||
|
- [x] 4.1 Publish one new never-reused immutable Protocol version, resolve it
|
||||||
|
from separate empty consumer caches, and pin the fetched checksums in both
|
||||||
|
consumers
|
||||||
@@ -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.
|
||||||
@@ -40,11 +40,15 @@ an implementation-specific release-all provider command.
|
|||||||
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
|
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
|
||||||
with a direction byte, type byte, big-endian payload length, and exact payload
|
with a direction byte, type byte, big-endian payload length, and exact payload
|
||||||
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
|
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
|
||||||
gateway to the client. Only IDR, FEC/loss feedback, and an empty terminal receipt
|
gateway to the client. Only IDR, FEC/loss feedback, and client-direction type
|
||||||
SHALL be valid from the client to the gateway. The terminal receipt SHALL be
|
`0x03` with an empty payload as terminal receipt SHALL be valid from the client
|
||||||
valid only while the same session awaits receipt of its one terminal event and
|
to the gateway. The fixed conformance corpus and every generated or native
|
||||||
MUST NOT be forwarded to the provider. The envelope SHALL contain no provider
|
Protocol validator SHALL accept that exact receipt and reject unknown types,
|
||||||
address, certificate, credential, or opaque provider packet.
|
wrong direction, nonempty receipt bodies, truncation, and length mismatch. The
|
||||||
|
terminal receipt SHALL be valid only while the same session awaits receipt of
|
||||||
|
its one terminal event and MUST NOT be forwarded to the provider. The envelope
|
||||||
|
SHALL contain no provider address, certificate, credential, or opaque provider
|
||||||
|
packet.
|
||||||
|
|
||||||
#### Scenario: Host termination forwarding
|
#### Scenario: Host termination forwarding
|
||||||
- **WHEN** the Apollo adapter receives an authenticated host termination packet
|
- **WHEN** the Apollo adapter receives an authenticated host termination packet
|
||||||
@@ -52,7 +56,7 @@ address, certificate, credential, or opaque provider packet.
|
|||||||
|
|
||||||
#### Scenario: Terminal event receipt
|
#### Scenario: Terminal event receipt
|
||||||
- **WHEN** a client receives the reliable typed terminal event
|
- **WHEN** a client receives the reliable typed terminal event
|
||||||
- **THEN** it sends the empty terminal receipt and the gateway owns bounded tunnel closure without forwarding the receipt to the provider
|
- **THEN** it sends the fixed empty client-direction type `0x03` receipt and the gateway owns bounded tunnel closure without forwarding the receipt to the provider
|
||||||
|
|
||||||
#### Scenario: Unauthorized or malformed feedback
|
#### Scenario: Unauthorized or malformed feedback
|
||||||
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length, contains a forbidden provider field, or sends a terminal receipt outside the awaiting-terminal state
|
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length, contains a forbidden provider field, or sends a terminal receipt outside the awaiting-terminal state
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# gateway-media-framing Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Define the versioned datagram framing, bounded reassembly, and explicit negotiation required to carry complete encoded media units.
|
||||||
|
## Requirements
|
||||||
|
### Requirement: Versioned complete encoded-frame datagrams
|
||||||
|
The Protocol SHALL register `datagram-v2` with a 23-byte network-order header containing magic, version, channel, flags, sequence, timestamp, unsigned 16-bit fragment index, unsigned 16-bit fragment count, and unsigned 16-bit payload length. Media datagrams MUST remain at most 1,200 bytes with at most 1,177 payload bytes, and one sequence MUST represent exactly one complete encoded unit of at most 1,048,576 bytes and 891 fragments.
|
||||||
|
|
||||||
|
#### Scenario: Bounded large encoded frame
|
||||||
|
- **WHEN** an encoded media frame is larger than the datagram-v1 18,864-byte limit but no larger than 1,048,576 bytes
|
||||||
|
- **THEN** datagram-v2 carries it under one sequence in at most 891 independently bounded fragments
|
||||||
|
|
||||||
|
#### Scenario: Oversized encoded frame
|
||||||
|
- **WHEN** a sender attempts more than 1,048,576 complete bytes or 891 fragments
|
||||||
|
- **THEN** framing rejects the unit before sending or allocating proportional state
|
||||||
|
|
||||||
|
### Requirement: Bounded complete-frame reassembly
|
||||||
|
Datagram-v2 reassembly SHALL retain at most four incomplete media units and only the bytes actually received, permit bounded fragment reorder and exact duplicate fragments, reject conflicting duplicates or inconsistent metadata, expire incomplete state after 250 milliseconds, and emit a payload only after every fragment is present exactly once.
|
||||||
|
|
||||||
|
#### Scenario: Reordered frame completes
|
||||||
|
- **WHEN** every valid fragment for one unit arrives out of order within the reassembly bounds
|
||||||
|
- **THEN** the receiver emits exactly one byte-identical complete encoded frame with its original boundary
|
||||||
|
|
||||||
|
#### Scenario: Malformed or stale reassembly
|
||||||
|
- **WHEN** fragments conflict, metadata changes, an index or count is invalid, state exceeds four incomplete units, or a unit exceeds its timeout
|
||||||
|
- **THEN** the receiver rejects or evicts that incomplete unit without proportional allocation or payload emission
|
||||||
|
|
||||||
|
### Requirement: Explicit framing negotiation
|
||||||
|
`CapabilityProfile.framing` SHALL accept only registered `datagram-v1` or `datagram-v2` values. A peer MUST use datagram-v2 only after exact capability intersection and MUST NOT reinterpret datagram-v1 bytes as datagram-v2.
|
||||||
|
|
||||||
|
#### Scenario: Independent datagram-v2 peers overlap
|
||||||
|
- **WHEN** both peers advertise the registered datagram-v2 framing profile
|
||||||
|
- **THEN** generated Go, Rust, and Swift validation and fixed conformance accept the profile
|
||||||
|
|
||||||
|
#### Scenario: Unknown or downgraded framing
|
||||||
|
- **WHEN** a peer advertises an unknown framing value or the peers advertise different versions
|
||||||
|
- **THEN** capability validation or intersection rejects the session before media forwarding
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"max_frame_bytes": 65536,
|
"max_frame_bytes": 65536,
|
||||||
|
"framing_profiles": [
|
||||||
|
{"id": "datagram-v1", "header_bytes": 21, "media_payload_bytes": 1179, "max_fragments": 16, "max_complete_bytes": 18864},
|
||||||
|
{"id": "datagram-v2", "header_bytes": 23, "media_payload_bytes": 1177, "max_fragments": 891, "max_complete_bytes": 1048576}
|
||||||
|
],
|
||||||
"datagrams": [
|
"datagrams": [
|
||||||
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"},
|
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"},
|
||||||
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||||
|
|||||||
@@ -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}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -372,7 +386,7 @@
|
|||||||
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
"framing": {"type": "string", "minLength": 1, "maxLength": 64},
|
"framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]},
|
||||||
"media": {"type": "string", "minLength": 1, "maxLength": 64},
|
"media": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
|
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
|
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -122,6 +200,21 @@ func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCapabilityProfileRequiresRegisteredFraming(t *testing.T) {
|
||||||
|
valid := protocol.CapabilityProfile{
|
||||||
|
Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded",
|
||||||
|
Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
|
||||||
|
}
|
||||||
|
if err := valid.Validate(); err != nil {
|
||||||
|
t.Fatalf("registered datagram-v2 rejected: %v", err)
|
||||||
|
}
|
||||||
|
invalid := valid
|
||||||
|
invalid.Framing = "datagram-v3"
|
||||||
|
if err := invalid.Validate(); err == nil {
|
||||||
|
t.Fatal("unregistered framing accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
||||||
request := protocol.TunnelAdmissionRequest{
|
request := protocol.TunnelAdmissionRequest{
|
||||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Require immutable commits for third-party Gitea workflow actions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
USE = re.compile(r"^\s*-\s+uses:\s+([^@\s]+)@([^\s#]+)", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
for workflow in sorted((ROOT / ".gitea/workflows").glob("*.y*ml")):
|
||||||
|
for action, revision in USE.findall(workflow.read_text(encoding="utf-8")):
|
||||||
|
if not action.startswith("./") and not re.fullmatch(r"[0-9a-f]{40}", revision):
|
||||||
|
raise SystemExit(f"{workflow.relative_to(ROOT)}: mutable action {action}@{revision}")
|
||||||
|
|
||||||
|
print("Protocol CI action references are immutable")
|
||||||
@@ -87,6 +87,7 @@ def check_text_boundaries() -> None:
|
|||||||
ROOT / "openapi/control-v1.yaml",
|
ROOT / "openapi/control-v1.yaml",
|
||||||
ROOT / "proto/versevdi/control/v1/control.proto",
|
ROOT / "proto/versevdi/control/v1/control.proto",
|
||||||
ROOT / "frames/datagram-v1.md",
|
ROOT / "frames/datagram-v1.md",
|
||||||
|
ROOT / "frames/datagram-v2.md",
|
||||||
ROOT / "frames/registry.json",
|
ROOT / "frames/registry.json",
|
||||||
ROOT / "registries/features.json",
|
ROOT / "registries/features.json",
|
||||||
ROOT / "registries/datagrams.json",
|
ROOT / "registries/datagrams.json",
|
||||||
|
|||||||
+16
-2
@@ -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)})")
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ import (
|
|||||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
datagramHeaderBytes = 21
|
|
||||||
maximumFrameBytes = 65536
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
entries, err := os.ReadDir("fixtures/conformance")
|
entries, err := os.ReadDir("fixtures/conformance")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -130,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"
|
||||||
@@ -231,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"
|
||||||
}
|
}
|
||||||
@@ -320,16 +330,24 @@ func classifyDatagram(encoded string) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "invalid:hex"
|
return "invalid:hex"
|
||||||
}
|
}
|
||||||
if len(raw) < datagramHeaderBytes {
|
if len(raw) < 3 {
|
||||||
return "invalid:truncated"
|
return "invalid:truncated"
|
||||||
}
|
}
|
||||||
if string(raw[:2]) != "VD" {
|
if string(raw[:2]) != "VD" {
|
||||||
return "invalid:magic"
|
return "invalid:magic"
|
||||||
}
|
}
|
||||||
if raw[2] != 1 {
|
if raw[2] != 1 && raw[2] != 2 {
|
||||||
return "invalid:unsupported_version"
|
return "invalid:unsupported_version"
|
||||||
}
|
}
|
||||||
|
headerBytes := 21
|
||||||
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
|
if raw[2] == 2 {
|
||||||
|
headerBytes = 23
|
||||||
|
limits = map[byte]int{10: 1177, 11: 1177}
|
||||||
|
}
|
||||||
|
if len(raw) < headerBytes {
|
||||||
|
return "invalid:truncated"
|
||||||
|
}
|
||||||
limit, ok := limits[raw[3]]
|
limit, ok := limits[raw[3]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "invalid:unknown_channel"
|
return "invalid:unknown_channel"
|
||||||
@@ -337,17 +355,27 @@ func classifyDatagram(encoded string) string {
|
|||||||
if raw[4] != 0 {
|
if raw[4] != 0 {
|
||||||
return "invalid:flags"
|
return "invalid:flags"
|
||||||
}
|
}
|
||||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
fragmentIndex, fragmentCount := int(raw[17]), int(raw[18])
|
||||||
|
payloadOffset := 19
|
||||||
|
if raw[2] == 2 {
|
||||||
|
fragmentIndex = int(raw[17])<<8 | int(raw[18])
|
||||||
|
fragmentCount = int(raw[19])<<8 | int(raw[20])
|
||||||
|
payloadOffset = 21
|
||||||
|
if fragmentCount > 891 {
|
||||||
|
return "invalid:fragment_limit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fragmentCount == 0 || fragmentIndex >= fragmentCount {
|
||||||
return "invalid:fragment"
|
return "invalid:fragment"
|
||||||
}
|
}
|
||||||
payloadLength := int(raw[19])<<8 | int(raw[20])
|
payloadLength := int(raw[payloadOffset])<<8 | int(raw[payloadOffset+1])
|
||||||
if payloadLength > limit {
|
if payloadLength > limit {
|
||||||
return "invalid:payload_limit"
|
return "invalid:payload_limit"
|
||||||
}
|
}
|
||||||
if len(raw) != datagramHeaderBytes+payloadLength {
|
if len(raw) != headerBytes+payloadLength {
|
||||||
return "invalid:length_mismatch"
|
return "invalid:length_mismatch"
|
||||||
}
|
}
|
||||||
if len(raw) > maximumFrameBytes {
|
if raw[2] == 1 && len(raw) > 65536 || raw[2] == 2 && len(raw) > 1200 {
|
||||||
return "invalid:frame_limit"
|
return "invalid:frame_limit"
|
||||||
}
|
}
|
||||||
return "valid"
|
return "valid"
|
||||||
|
|||||||
+37
-13
@@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,36 +199,50 @@ fn classify_datagram(encoded: &str) -> &'static str {
|
|||||||
Some(raw) => raw,
|
Some(raw) => raw,
|
||||||
None => return "invalid:hex",
|
None => return "invalid:hex",
|
||||||
};
|
};
|
||||||
if raw.len() < 21 {
|
if raw.len() < 3 {
|
||||||
return "invalid:truncated";
|
return "invalid:truncated";
|
||||||
}
|
}
|
||||||
if raw[0..2] != *b"VD" {
|
if raw[0..2] != *b"VD" {
|
||||||
return "invalid:magic";
|
return "invalid:magic";
|
||||||
}
|
}
|
||||||
if raw[2] != 1 {
|
if raw[2] != 1 && raw[2] != 2 {
|
||||||
return "invalid:unsupported_version";
|
return "invalid:unsupported_version";
|
||||||
}
|
}
|
||||||
let limit = match raw[3] {
|
let header_bytes = if raw[2] == 1 { 21 } else { 23 };
|
||||||
1 => 1024,
|
if raw.len() < header_bytes {
|
||||||
2 => 2048,
|
return "invalid:truncated";
|
||||||
3 => 65515,
|
}
|
||||||
10 | 11 | 12 => 1179,
|
let limit = match (raw[2], raw[3]) {
|
||||||
|
(1, 1) => 1024,
|
||||||
|
(1, 2) => 2048,
|
||||||
|
(1, 3) => 65515,
|
||||||
|
(1, 10 | 11 | 12) => 1179,
|
||||||
|
(2, 10 | 11) => 1177,
|
||||||
_ => return "invalid:unknown_channel",
|
_ => return "invalid:unknown_channel",
|
||||||
};
|
};
|
||||||
if raw[4] != 0 {
|
if raw[4] != 0 {
|
||||||
return "invalid:flags";
|
return "invalid:flags";
|
||||||
}
|
}
|
||||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
let (fragment_index, fragment_count, payload_offset) = if raw[2] == 1 {
|
||||||
|
(raw[17] as usize, raw[18] as usize, 19)
|
||||||
|
} else {
|
||||||
|
let count = ((raw[19] as usize) << 8) | raw[20] as usize;
|
||||||
|
if count > 891 {
|
||||||
|
return "invalid:fragment_limit";
|
||||||
|
}
|
||||||
|
((((raw[17] as usize) << 8) | raw[18] as usize), count, 21)
|
||||||
|
};
|
||||||
|
if fragment_count == 0 || fragment_index >= fragment_count {
|
||||||
return "invalid:fragment";
|
return "invalid:fragment";
|
||||||
}
|
}
|
||||||
let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize;
|
let payload_length = ((raw[payload_offset] as usize) << 8) | raw[payload_offset + 1] as usize;
|
||||||
if payload_length > limit {
|
if payload_length > limit {
|
||||||
return "invalid:payload_limit";
|
return "invalid:payload_limit";
|
||||||
}
|
}
|
||||||
if raw.len() != 21 + payload_length {
|
if raw.len() != header_bytes + payload_length {
|
||||||
return "invalid:length_mismatch";
|
return "invalid:length_mismatch";
|
||||||
}
|
}
|
||||||
if raw.len() > 65536 {
|
if raw[2] == 1 && raw.len() > 65536 || raw[2] == 2 && raw.len() > 1200 {
|
||||||
return "invalid:frame_limit";
|
return "invalid:frame_limit";
|
||||||
}
|
}
|
||||||
"valid"
|
"valid"
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,23 +138,30 @@ func validFECStatus(_ body: [UInt8]) -> Bool {
|
|||||||
|
|
||||||
func classifyDatagram(_ encoded: String) -> String {
|
func classifyDatagram(_ encoded: String) -> String {
|
||||||
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
||||||
guard raw.count >= 21 else { return "invalid:truncated" }
|
guard raw.count >= 3 else { return "invalid:truncated" }
|
||||||
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
||||||
guard raw[2] == 1 else { return "invalid:unsupported_version" }
|
guard raw[2] == 1 || raw[2] == 2 else { return "invalid:unsupported_version" }
|
||||||
|
let headerBytes = raw[2] == 1 ? 21 : 23
|
||||||
|
guard raw.count >= headerBytes else { return "invalid:truncated" }
|
||||||
let limit: Int
|
let limit: Int
|
||||||
switch raw[3] {
|
switch (raw[2], raw[3]) {
|
||||||
case 1: limit = 1024
|
case (1, 1): limit = 1024
|
||||||
case 2: limit = 2048
|
case (1, 2): limit = 2048
|
||||||
case 3: limit = 65515
|
case (1, 3): limit = 65515
|
||||||
case 10, 11, 12: limit = 1179
|
case (1, 10), (1, 11), (1, 12): limit = 1179
|
||||||
|
case (2, 10), (2, 11): limit = 1177
|
||||||
default: return "invalid:unknown_channel"
|
default: return "invalid:unknown_channel"
|
||||||
}
|
}
|
||||||
guard raw[4] == 0 else { return "invalid:flags" }
|
guard raw[4] == 0 else { return "invalid:flags" }
|
||||||
guard raw[18] > 0 && raw[17] < raw[18] else { return "invalid:fragment" }
|
let fragmentIndex = raw[2] == 1 ? Int(raw[17]) : Int(raw[17]) * 256 + Int(raw[18])
|
||||||
let payloadLength = Int(raw[19]) * 256 + Int(raw[20])
|
let fragmentCount = raw[2] == 1 ? Int(raw[18]) : Int(raw[19]) * 256 + Int(raw[20])
|
||||||
|
if raw[2] == 2 && fragmentCount > 891 { return "invalid:fragment_limit" }
|
||||||
|
guard fragmentCount > 0 && fragmentIndex < fragmentCount else { return "invalid:fragment" }
|
||||||
|
let payloadOffset = raw[2] == 1 ? 19 : 21
|
||||||
|
let payloadLength = Int(raw[payloadOffset]) * 256 + Int(raw[payloadOffset + 1])
|
||||||
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
||||||
guard raw.count == 21 + payloadLength else { return "invalid:length_mismatch" }
|
guard raw.count == headerBytes + payloadLength else { return "invalid:length_mismatch" }
|
||||||
guard raw.count <= 65536 else { return "invalid:frame_limit" }
|
guard raw[2] == 1 ? raw.count <= 65536 : raw.count <= 1200 else { return "invalid:frame_limit" }
|
||||||
return "valid"
|
return "valid"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ TEXT_PATHS = (
|
|||||||
"proto/versevdi/control/v1/control.proto",
|
"proto/versevdi/control/v1/control.proto",
|
||||||
"proto/versevdi/tunnel/v1/tunnel.proto",
|
"proto/versevdi/tunnel/v1/tunnel.proto",
|
||||||
"frames/datagram-v1.md",
|
"frames/datagram-v1.md",
|
||||||
|
"frames/datagram-v2.md",
|
||||||
"frames/registry.json",
|
"frames/registry.json",
|
||||||
"registries/features.json",
|
"registries/features.json",
|
||||||
"registries/datagrams.json",
|
"registries/datagrams.json",
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ let capability = try CapabilityProfile(
|
|||||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
)
|
)
|
||||||
|
_ = try CapabilityProfile(
|
||||||
|
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||||
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
_ = try CapabilityProfile(
|
||||||
|
transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
|
||||||
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
|
)
|
||||||
|
fatalError("unregistered framing was accepted")
|
||||||
|
} catch { }
|
||||||
let request = try TunnelAdmissionRequest(
|
let request = try TunnelAdmissionRequest(
|
||||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||||
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
||||||
@@ -92,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
|
||||||
@@ -162,6 +211,14 @@ fn main() {
|
|||||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
assert!(CapabilityProfile::new(
|
||||||
|
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
|
||||||
|
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||||
|
).is_ok());
|
||||||
|
assert!(CapabilityProfile::new(
|
||||||
|
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
|
||||||
|
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||||
|
).is_err());
|
||||||
let request = TunnelAdmissionRequest::new(
|
let request = TunnelAdmissionRequest::new(
|
||||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
@@ -199,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());
|
||||||
|
|||||||
+20
-1
@@ -31,9 +31,28 @@ def main() -> int:
|
|||||||
value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
|
value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
|
||||||
entries = value.get("features", value.get("datagrams"))
|
entries = value.get("features", value.get("datagrams"))
|
||||||
assert entries and len({entry["id"] for entry in entries}) == len(entries)
|
assert entries and len({entry["id"] for entry in entries}) == len(entries)
|
||||||
|
maximum_bound = 1_048_576 if registry.endswith("datagrams.json") else 65_536
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
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 <= 65536
|
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"]))
|
||||||
|
|||||||
+24
-13
@@ -7,47 +7,58 @@ import binascii
|
|||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
HEADER_BYTES = 21
|
V1_CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
MAX_FRAME_BYTES = 65536
|
V2_CHANNEL_LIMITS = {10: 1177, 11: 1177}
|
||||||
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
|
||||||
|
|
||||||
|
|
||||||
def classify(raw: bytes) -> str:
|
def classify(raw: bytes) -> str:
|
||||||
if len(raw) < HEADER_BYTES:
|
if len(raw) < 3:
|
||||||
return "invalid:truncated"
|
return "invalid:truncated"
|
||||||
if raw[:2] != b"VD":
|
if raw[:2] != b"VD":
|
||||||
return "invalid:magic"
|
return "invalid:magic"
|
||||||
if raw[2] != 1:
|
if raw[2] not in (1, 2):
|
||||||
return "invalid:unsupported_version"
|
return "invalid:unsupported_version"
|
||||||
if raw[3] not in CHANNEL_LIMITS:
|
header_bytes = 21 if raw[2] == 1 else 23
|
||||||
|
if len(raw) < header_bytes:
|
||||||
|
return "invalid:truncated"
|
||||||
|
limits = V1_CHANNEL_LIMITS if raw[2] == 1 else V2_CHANNEL_LIMITS
|
||||||
|
if raw[3] not in limits:
|
||||||
return "invalid:unknown_channel"
|
return "invalid:unknown_channel"
|
||||||
if raw[4] != 0:
|
if raw[4] != 0:
|
||||||
return "invalid:flags"
|
return "invalid:flags"
|
||||||
|
if raw[2] == 1:
|
||||||
fragment_index, fragment_count = raw[17], raw[18]
|
fragment_index, fragment_count = raw[17], raw[18]
|
||||||
|
payload_length = int.from_bytes(raw[19:21], "big")
|
||||||
|
else:
|
||||||
|
fragment_index = int.from_bytes(raw[17:19], "big")
|
||||||
|
fragment_count = int.from_bytes(raw[19:21], "big")
|
||||||
|
payload_length = int.from_bytes(raw[21:23], "big")
|
||||||
|
if fragment_count > 891:
|
||||||
|
return "invalid:fragment_limit"
|
||||||
if fragment_count == 0 or fragment_index >= fragment_count:
|
if fragment_count == 0 or fragment_index >= fragment_count:
|
||||||
return "invalid:fragment"
|
return "invalid:fragment"
|
||||||
payload_length = int.from_bytes(raw[19:21], "big")
|
if payload_length > limits[raw[3]]:
|
||||||
if payload_length > CHANNEL_LIMITS[raw[3]]:
|
|
||||||
return "invalid:payload_limit"
|
return "invalid:payload_limit"
|
||||||
if len(raw) != HEADER_BYTES + payload_length:
|
if len(raw) != header_bytes + payload_length:
|
||||||
return "invalid:length_mismatch"
|
return "invalid:length_mismatch"
|
||||||
if len(raw) > MAX_FRAME_BYTES:
|
if raw[2] == 1 and len(raw) > 65536 or raw[2] == 2 and len(raw) > 1200:
|
||||||
return "invalid:frame_limit"
|
return "invalid:frame_limit"
|
||||||
return "valid"
|
return "valid"
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
lines = (ROOT / "fixtures/conformance/datagram-v1.tsv").read_text(encoding="utf-8").splitlines()
|
for fixture in ("datagram-v1.tsv", "datagram-v2.tsv"):
|
||||||
|
lines = (ROOT / "fixtures/conformance" / fixture).read_text(encoding="utf-8").splitlines()
|
||||||
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
identifier, version, kind, input_value, expected = line.split("\t")
|
identifier, version, kind, input_value, expected = line.split("\t")
|
||||||
assert kind == "datagram" and version == "1"
|
assert kind == "datagram" and version in ("1", "2")
|
||||||
encoded = input_value.removeprefix("hex=")
|
encoded = input_value.removeprefix("hex=")
|
||||||
try:
|
try:
|
||||||
actual = classify(binascii.unhexlify(encoded))
|
actual = classify(binascii.unhexlify(encoded))
|
||||||
except binascii.Error:
|
except binascii.Error:
|
||||||
actual = "invalid:hex"
|
actual = "invalid:hex"
|
||||||
assert actual == expected, f"{identifier}: {actual} != {expected}"
|
assert actual == expected, f"{fixture}:{identifier}: {actual} != {expected}"
|
||||||
print("Datagram frame validation passed")
|
print("Datagram frame validation passed")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user