Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f6edffca | ||
|
|
357e5e0dbc | ||
|
|
4a2772c053 | ||
|
|
be709724ba | ||
|
|
7c145001c0 | ||
|
|
28a9aace24 | ||
|
|
b5558133e3 |
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile clean-generated
|
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated
|
||||||
|
|
||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
PROTOC ?= protoc
|
PROTOC ?= protoc
|
||||||
@@ -29,6 +29,9 @@ binding-compile:
|
|||||||
rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib
|
rustc --crate-type lib gen/rust/protocol.rs -o /tmp/versevdi-protocol-generated.rlib
|
||||||
swiftc -typecheck gen/swift/Protocol.swift
|
swiftc -typecheck gen/swift/Protocol.swift
|
||||||
|
|
||||||
|
strict-contracts:
|
||||||
|
$(PYTHON) -B tools/test_generated_contracts.py
|
||||||
|
|
||||||
conformance:
|
conformance:
|
||||||
$(PYTHON) -B tools/fixture_digest.py
|
$(PYTHON) -B tools/fixture_digest.py
|
||||||
go run ./tools/go-conformance
|
go run ./tools/go-conformance
|
||||||
@@ -40,4 +43,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 conformance frame-verify clean-generated
|
verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ invalid-short 1 datagram hex=564401 invalid:truncated
|
|||||||
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 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
|
||||||
|
invalid-media-channel 1 datagram hex=5644010d0000000000000000000000000000010000 invalid:unknown_channel
|
||||||
|
|||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"fixtures/conformance/events-v1.tsv",
|
"fixtures/conformance/events-v1.tsv",
|
||||||
"fixtures/conformance/tunnel-v1.tsv"
|
"fixtures/conformance/tunnel-v1.tsv"
|
||||||
],
|
],
|
||||||
"corpus_sha256": "c91a512dc67aa9912b31b21144be2adfeacf0dc80dd8515bd3b4a8f52977e761"
|
"corpus_sha256": "0874d39dd14c0107e602ea8909f8d53673f964c67dc9fd5fcadb8641e6ebf592"
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -1,7 +1,9 @@
|
|||||||
# VerseVDI control datagram v1
|
# VerseVDI control datagram v1
|
||||||
|
|
||||||
Phase 3A reserves a bounded control-datagram envelope. It does not forward video,
|
Phase 3C adds bounded encoded video/audio and sequenced-input channels to the same
|
||||||
audio, provider traffic, VM traffic, or arbitrary binary clipboard data.
|
transport envelope. The gateway forwards encoded bytes; it does not decode, encode,
|
||||||
|
transcode, render, or expose provider traffic. Arbitrary binary clipboard data remains
|
||||||
|
rejected.
|
||||||
|
|
||||||
All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes:
|
All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes:
|
||||||
|
|
||||||
@@ -22,6 +24,9 @@ Truncated, oversized, unknown-version, unknown-channel, invalid-fragment, and
|
|||||||
length-mismatch frames are rejected before allocation proportional to the claimed
|
length-mismatch frames are rejected before allocation proportional to the claimed
|
||||||
payload. Media/provider identifiers are not registered channels.
|
payload. Media/provider identifiers are not registered channels.
|
||||||
|
|
||||||
Registered Phase 3A channels are `control.ack.v1`, `control.cancel.v1`, and
|
Registered channels are `control.ack.v1`, `control.cancel.v1`, `clipboard.text.v1`,
|
||||||
`clipboard.text.v1`. Clipboard payloads are UTF-8 JSON text contracts and remain
|
`media.video.v1`, `media.audio.v1`, and `input.sequenced.v1`. Media/input frames use
|
||||||
subject to the 65,536-byte text limit and explicit authorization.
|
application flow IDs 10, 11, and 12 and a path-MTU-safe payload limit of 1,179 bytes;
|
||||||
|
larger encoded units use at most 16 validated fragments. Clipboard payloads are UTF-8
|
||||||
|
JSON text contracts and remain subject to the 65,536-byte text limit and explicit
|
||||||
|
authorization.
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
"channels": [
|
"channels": [
|
||||||
{"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
{"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||||
{"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
{"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||||
{"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515}
|
{"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515},
|
||||||
|
{"id": 10, "name": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1179},
|
||||||
|
{"id": 11, "name": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1179},
|
||||||
|
{"id": 12, "name": "input.sequenced.v1", "direction": "client-to-server", "max_payload_bytes": 1179}
|
||||||
],
|
],
|
||||||
"reserved_rejected": ["video", "audio", "provider", "vm", "file-transfer", "clipboard.binary"]
|
"reserved_rejected": ["provider", "vm", "file-transfer", "clipboard.binary"]
|
||||||
}
|
}
|
||||||
|
|||||||
+1459
-1
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -12,7 +12,7 @@
|
|||||||
"2"
|
"2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c",
|
"generator_sha256": "e9c6ee1541585fcb00dcc5e94a5a6d93dbe3a719a5c545f31e5eda268f2638ab",
|
||||||
"protocol_version": "1.0.0",
|
"protocol_version": "1.0.0",
|
||||||
"schema_sha256": "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
"schema_sha256": "792abfb9cfe70e79911d499d76c009ab848713278bc240b88576df520580e480"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+1394
-130
File diff suppressed because it is too large
Load Diff
+1551
-188
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Use the existing generated JSON binding pipeline and add only object definitions that
|
||||||
|
consumers need now.
|
||||||
|
- Keep registration/authority messages distinct from `ConnectionManifest`; the latter is
|
||||||
|
client-facing and contains no provider route, certificate, identity, or credential.
|
||||||
|
- Encode capability domains as bounded registered strings (`transport`, `framing`, `media`,
|
||||||
|
`audio`, `source_rate_control`, `client_decode`) so unknown or empty required domains
|
||||||
|
cannot silently fall back.
|
||||||
|
- Use a fixed 21-byte big-endian datagram header with application flow IDs and a payload
|
||||||
|
limit below the path MTU; control channels 1-3 remain compatible with Phase 3A.
|
||||||
|
|
||||||
|
## Bounds and failure behavior
|
||||||
|
|
||||||
|
All arrays, strings, payloads, fragments, and timestamps are bounded by the JSON schema or
|
||||||
|
frame registry. Generated decoders reject unknown fields, trailing values, invalid versions,
|
||||||
|
and missing required fields. Provider address, RTSP, credential, and private-key names are
|
||||||
|
not added to client-facing definitions.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The current wire version remains `1`; current, N-1, and N-2 declarations remain unchanged.
|
||||||
|
New identifiers are additive. Consumers must reject an unknown major or no-overlap selection
|
||||||
|
before provider launch.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Phase 3A defines gateway-only manifests and control datagrams but has no versioned
|
||||||
|
contracts for the separately deployed gateway's registration, admission authority,
|
||||||
|
capability intersection, lifecycle, or encoded relay framing. Phase 3C-G needs those
|
||||||
|
contracts frozen before Data Plane or Connection Server consumers change.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add bounded JSON bindings for gateway registration, heartbeat/drain, tunnel admission,
|
||||||
|
session-scoped authority, capability selection, channel framing, provider state, and
|
||||||
|
stable errors.
|
||||||
|
- Extend the tunnel protobuf descriptor with the same versioned control messages.
|
||||||
|
- Register only gateway-owned media/input flow identifiers; keep provider endpoints and
|
||||||
|
credentials out of client-facing manifests.
|
||||||
|
|
||||||
|
## Ownership and provenance
|
||||||
|
|
||||||
|
Protocol owns the wire contract. The contract is original VerseVDI work derived from the
|
||||||
|
Phase 3A schemas and public Apollo/Moonlight behavior recorded in the Planning Hub. No
|
||||||
|
GPL source is copied into this repository.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Provider implementation, database policy, QUIC implementation, media decoding, or client
|
||||||
|
rendering.
|
||||||
|
- Direct client-to-Apollo routing, provider endpoint exposure, or a speculative plugin ABI.
|
||||||
|
|
||||||
|
## Stop conditions
|
||||||
|
|
||||||
|
Unknown versions, malformed bounds, no capability overlap, downgrade without explicit
|
||||||
|
acknowledgement, forbidden provider fields, and oversized frames fail closed.
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Versioned gateway authority contracts
|
||||||
|
Gateway registration, heartbeat, drain, admission, capability selection, channel framing,
|
||||||
|
provider state, and stable errors SHALL use bounded versioned objects with strict decoding.
|
||||||
|
|
||||||
|
#### Scenario: Unknown or malformed gateway message
|
||||||
|
- **WHEN** a consumer receives an unknown version, missing required field, unknown field,
|
||||||
|
invalid bound, or trailing JSON value
|
||||||
|
- **THEN** it rejects the message before allocating provider or media state with a stable
|
||||||
|
validation error.
|
||||||
|
|
||||||
|
### Requirement: Gateway-only client manifest
|
||||||
|
Client-facing manifests SHALL expose only the selected gateway, tunnel/profile identifiers,
|
||||||
|
policy bounds, opaque grant, audience, expiry, session, and correlation data.
|
||||||
|
|
||||||
|
#### Scenario: Provider route injection
|
||||||
|
- **WHEN** a manifest or client-facing authority contains a provider address, RTSP URL,
|
||||||
|
certificate, pairing identity, credential, or private key field
|
||||||
|
- **THEN** schema validation rejects it and no generated client binding accepts it.
|
||||||
|
|
||||||
|
### Requirement: Explicit capability intersection
|
||||||
|
Transport, framing, media, audio, source-rate-control, and client-decode capabilities SHALL
|
||||||
|
remain separate and no-overlap or unknown-required-profile results SHALL fail closed.
|
||||||
|
|
||||||
|
#### Scenario: No capability overlap
|
||||||
|
- **WHEN** policy, gateway, provider, and client capabilities have no allowed intersection
|
||||||
|
- **THEN** negotiation returns a stable no-overlap error before provider launch or media flow.
|
||||||
|
|
||||||
|
### Requirement: Bounded encoded datagrams
|
||||||
|
Encoded media and approved sequenced input SHALL use registered application flow identifiers,
|
||||||
|
validated fixed headers, bounded fragments, and payload bytes that are not codec-transformed.
|
||||||
|
|
||||||
|
#### Scenario: Malformed or oversized datagram
|
||||||
|
- **WHEN** a datagram is truncated, has an unknown flow, invalid fragment, length mismatch,
|
||||||
|
or exceeds its registered payload limit
|
||||||
|
- **THEN** the datagram is rejected without allocation proportional to the claimed payload.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- [x] Add versioned gateway and authority definitions to the schema and tunnel descriptor.
|
||||||
|
- [x] Add registered media/audio/sequenced-input flow identifiers and positive/negative fixtures.
|
||||||
|
- [x] Regenerate Go/Rust/Swift bindings and descriptor outputs.
|
||||||
|
- [x] Run strict validation, cross-language conformance, and deterministic fixture hashing.
|
||||||
|
- [x] Freeze the local Protocol candidate commit and record its hash for consumers.
|
||||||
@@ -49,3 +49,116 @@ message ChannelOpen {
|
|||||||
string direction = 2;
|
string direction = 2;
|
||||||
uint32 maximum_frame_bytes = 3;
|
uint32 maximum_frame_bytes = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CapabilityProfile {
|
||||||
|
string transport = 1;
|
||||||
|
string framing = 2;
|
||||||
|
string media = 3;
|
||||||
|
string audio = 4;
|
||||||
|
string source_rate_control = 5;
|
||||||
|
string client_decode = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayRegistration {
|
||||||
|
string version = 1;
|
||||||
|
string gateway_id = 2;
|
||||||
|
string instance_identity = 3;
|
||||||
|
string certificate_identity = 4;
|
||||||
|
string public_identity = 5;
|
||||||
|
string address = 6;
|
||||||
|
uint32 protocol_min_version = 7;
|
||||||
|
uint32 protocol_max_version = 8;
|
||||||
|
uint32 connection_capacity = 9;
|
||||||
|
uint64 bandwidth_capacity_kbps = 10;
|
||||||
|
repeated string features = 11;
|
||||||
|
CapabilityProfile capabilities = 12;
|
||||||
|
string provider_identity = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayHeartbeat {
|
||||||
|
string version = 1;
|
||||||
|
string gateway_id = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
google.protobuf.Timestamp observed_at = 4;
|
||||||
|
uint32 active_connections = 5;
|
||||||
|
uint64 egress_kbps = 6;
|
||||||
|
string state = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GatewayDrain {
|
||||||
|
string version = 1;
|
||||||
|
string gateway_id = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
string reason = 4;
|
||||||
|
google.protobuf.Timestamp deadline = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TunnelAdmissionRequest {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string gateway_id = 3;
|
||||||
|
string audience = 4;
|
||||||
|
string grant = 5;
|
||||||
|
uint64 reconnect_sequence = 6;
|
||||||
|
string client_nonce = 7;
|
||||||
|
CapabilityProfile capabilities = 8;
|
||||||
|
string device_signature = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SessionAuthority {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string gateway_id = 3;
|
||||||
|
string audience = 4;
|
||||||
|
uint64 reconnect_sequence = 5;
|
||||||
|
google.protobuf.Timestamp expires_at = 6;
|
||||||
|
CapabilityProfile capabilities = 7;
|
||||||
|
string provider_profile = 8;
|
||||||
|
string provider_identity = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProviderSessionWork {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string gateway_id = 3;
|
||||||
|
uint64 reconnect_sequence = 4;
|
||||||
|
google.protobuf.Timestamp expires_at = 5;
|
||||||
|
string provider_profile = 6;
|
||||||
|
string provider_identity = 7;
|
||||||
|
string policy_version_id = 8;
|
||||||
|
string application_id = 9;
|
||||||
|
string management_host = 10;
|
||||||
|
uint32 management_port = 11;
|
||||||
|
string stream_host = 12;
|
||||||
|
uint32 stream_port = 13;
|
||||||
|
string client_certificate_pem = 14;
|
||||||
|
string client_private_key_pem = 15;
|
||||||
|
string server_certificate_pem = 16;
|
||||||
|
string client_id = 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChannelFrame {
|
||||||
|
string version = 1;
|
||||||
|
string flow_id = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
uint32 flags = 4;
|
||||||
|
uint32 fragment_index = 5;
|
||||||
|
uint32 fragment_count = 6;
|
||||||
|
uint64 timestamp_ms = 7;
|
||||||
|
bytes payload = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProviderState {
|
||||||
|
string version = 1;
|
||||||
|
string session_id = 2;
|
||||||
|
string state = 3;
|
||||||
|
bool cleanup_pending = 4;
|
||||||
|
repeated string channels = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StableError {
|
||||||
|
string version = 1;
|
||||||
|
string code = 2;
|
||||||
|
string message = 3;
|
||||||
|
bool retryable = 4;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"datagrams": [
|
"datagrams": [
|
||||||
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||||
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||||
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536}
|
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536},
|
||||||
|
{"id": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
|
||||||
|
{"id": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
|
||||||
|
{"id": "input.sequenced.v1", "direction": "client-to-server", "max_payload_bytes": 1200}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,6 +343,159 @@
|
|||||||
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
|
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
|
||||||
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}}
|
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CapabilityProfile": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
||||||
|
"properties": {
|
||||||
|
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"framing": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"media": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"client_decode": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GatewayRegistration": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "gateway_id", "instance_identity", "certificate_identity", "public_identity", "address", "provider_identity", "protocol_min_version", "protocol_max_version", "connection_capacity", "bandwidth_capacity_kbps", "features", "capabilities"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"instance_identity": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||||
|
"certificate_identity": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||||
|
"public_identity": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"address": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"protocol_min_version": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||||
|
"protocol_max_version": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||||
|
"connection_capacity": {"type": "integer", "minimum": 1, "maximum": 1000000},
|
||||||
|
"bandwidth_capacity_kbps": {"type": "integer", "minimum": 1, "maximum": 1000000000},
|
||||||
|
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "minLength": 1, "maxLength": 64}},
|
||||||
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GatewayHeartbeat": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "gateway_id", "sequence", "observed_at", "active_connections", "egress_kbps", "state"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"sequence": {"type": "integer", "minimum": 1},
|
||||||
|
"observed_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||||
|
"active_connections": {"type": "integer", "minimum": 0, "maximum": 1000000},
|
||||||
|
"egress_kbps": {"type": "integer", "minimum": 0, "maximum": 1000000000},
|
||||||
|
"state": {"type": "string", "enum": ["ready", "draining", "offline"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GatewayDrain": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "gateway_id", "sequence", "reason", "deadline"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"sequence": {"type": "integer", "minimum": 1},
|
||||||
|
"reason": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"deadline": {"type": "string", "format": "date-time", "maxLength": 64}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TunnelAdmissionRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "device_signature", "capabilities"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"grant": {"type": "string", "minLength": 43, "maxLength": 256},
|
||||||
|
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"client_nonce": {"type": "string", "minLength": 16, "maxLength": 128},
|
||||||
|
"device_signature": {"type": "string", "minLength": 86, "maxLength": 86},
|
||||||
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SessionAuthority": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||||
|
"capabilities": {"$ref": "#/$defs/CapabilityProfile"},
|
||||||
|
"provider_profile": {"type": "string", "enum": ["apollo"]},
|
||||||
|
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ProviderSessionWork": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "expires_at", "provider_profile", "provider_identity", "policy_version_id", "application_id", "client_id", "management_host", "management_port", "stream_host", "stream_port", "client_certificate_pem", "client_private_key_pem", "server_certificate_pem"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||||
|
"provider_profile": {"type": "string", "const": "apollo"},
|
||||||
|
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"policy_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"application_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"client_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"management_host": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"management_port": {"type": "integer", "minimum": 1, "maximum": 65535},
|
||||||
|
"stream_host": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||||
|
"stream_port": {"type": "integer", "minimum": 1, "maximum": 65535},
|
||||||
|
"client_certificate_pem": {"type": "string", "minLength": 1, "maxLength": 32768},
|
||||||
|
"client_private_key_pem": {"type": "string", "minLength": 1, "maxLength": 32768},
|
||||||
|
"server_certificate_pem": {"type": "string", "minLength": 1, "maxLength": 32768}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ChannelFrame": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "flow_id", "sequence", "flags", "fragment_index", "fragment_count", "timestamp_ms", "payload"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"flow_id": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
"sequence": {"type": "integer", "minimum": 0},
|
||||||
|
"flags": {"type": "integer", "minimum": 0, "maximum": 255},
|
||||||
|
"fragment_index": {"type": "integer", "minimum": 0, "maximum": 15},
|
||||||
|
"fragment_count": {"type": "integer", "minimum": 1, "maximum": 16},
|
||||||
|
"timestamp_ms": {"type": "integer", "minimum": 0},
|
||||||
|
"payload": {"type": "string", "maxLength": 87384, "x-max-bytes": 65536}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ProviderState": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "session_id", "state", "cleanup_pending", "channels"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"state": {"type": "string", "enum": ["starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"]},
|
||||||
|
"cleanup_pending": {"type": "boolean"},
|
||||||
|
"channels": {"type": "array", "maxItems": 8, "items": {"type": "string", "minLength": 1, "maxLength": 64}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StableError": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version", "code", "message", "retryable"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"type": "string", "const": "1"},
|
||||||
|
"code": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"message": {"type": "string", "minLength": 1, "maxLength": 512},
|
||||||
|
"retryable": {"type": "boolean"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,90 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
|
|||||||
t.Fatal("DecodeErrorEnvelope accepted a missing required boolean")
|
t.Fatal("DecodeErrorEnvelope accepted a missing required boolean")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
|
||||||
|
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}`
|
||||||
|
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil {
|
||||||
|
t.Fatalf("valid gateway registration rejected: %v", err)
|
||||||
|
}
|
||||||
|
for _, invalid := range []string{
|
||||||
|
strings.Replace(registration, `"version":"1"`, `"version":"2"`, 1),
|
||||||
|
strings.Replace(registration, `"version":"1"`, `"version":"0"`, 1),
|
||||||
|
strings.Replace(registration, `"features":["datagram.media"]`, `"features":["datagram.media"],"provider_url":"https://provider.invalid"`, 1),
|
||||||
|
} {
|
||||||
|
if _, err := protocol.DecodeGatewayRegistration([]byte(invalid)); err == nil {
|
||||||
|
t.Fatalf("invalid gateway registration accepted: %s", invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := protocol.DecodeGatewayRegistration([]byte("{")); err == nil {
|
||||||
|
t.Fatal("DecodeGatewayRegistration accepted malformed JSON")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayRegistrationRejectsInvertedProtocolBounds(t *testing.T) {
|
||||||
|
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":2,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}`
|
||||||
|
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err == nil {
|
||||||
|
t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
|
||||||
|
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: "h264-opus"}
|
||||||
|
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || got != first {
|
||||||
|
t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err)
|
||||||
|
}
|
||||||
|
second := first
|
||||||
|
second.ClientDecode = "hevc-opus"
|
||||||
|
if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil {
|
||||||
|
t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
||||||
|
request := protocol.TunnelAdmissionRequest{
|
||||||
|
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||||
|
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
|
||||||
|
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
|
||||||
|
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||||
|
SourceRateControl: "server", ClientDecode: "h264-opus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
|
||||||
|
t.Fatal("EncodeTunnelAdmissionRequest accepted an unsigned device admission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing.T) {
|
||||||
|
request := protocol.TunnelAdmissionRequest{
|
||||||
|
Version: "1", SessionID: "session", GatewayID: "gateway", Audience: "audience",
|
||||||
|
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
|
||||||
|
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
|
||||||
|
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||||
|
SourceRateControl: "server", ClientDecode: "h264-opus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||||
|
if got := string(request.DeviceAdmissionTranscript()); got != want {
|
||||||
|
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
|
||||||
|
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
|
||||||
|
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
|
||||||
|
t.Fatalf("valid session authority rejected: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := protocol.DecodeSessionAuthority([]byte(strings.Replace(valid, `"provider_identity":"provider-1"`, `"provider_identity":"provider-1","rtsp_url":"rtsp://provider.invalid"`, 1))); err == nil {
|
||||||
|
t.Fatal("session authority accepted a provider route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
|
||||||
|
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","application_id":"42","client_id":"paired-client-1","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate"}`
|
||||||
|
if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil {
|
||||||
|
t.Fatalf("valid provider work rejected: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := protocol.DecodeProviderSessionWork([]byte(strings.Replace(valid, `"application_id":"42"`, `"application_id":"42","management_password":"forbidden"`, 1))); err == nil {
|
||||||
|
t.Fatal("provider work accepted a management credential")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+235
-5
@@ -151,6 +151,12 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
|||||||
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\"}}) }}")
|
lines.append(f"\tif err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}")
|
||||||
|
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\"}) }")
|
||||||
|
if name == "GatewayRegistration":
|
||||||
|
lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }")
|
||||||
|
if name == "ChannelFrame":
|
||||||
|
lines.append("\tif v.FragmentIndex >= v.FragmentCount { violations = append(violations, FieldViolation{Field: \"fragment_index\", Code: \"invalid_order\"}) }")
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -165,6 +171,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
|||||||
"\"errors\"",
|
"\"errors\"",
|
||||||
"\"fmt\"",
|
"\"fmt\"",
|
||||||
"\"reflect\"",
|
"\"reflect\"",
|
||||||
|
"\"strings\"",
|
||||||
"\"time\"",
|
"\"time\"",
|
||||||
")",
|
")",
|
||||||
"",
|
"",
|
||||||
@@ -235,6 +242,30 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
|||||||
out.append("\treturn json.Marshal(value)")
|
out.append("\treturn json.Marshal(value)")
|
||||||
out.append("}")
|
out.append("}")
|
||||||
out.append("")
|
out.append("")
|
||||||
|
out.extend([
|
||||||
|
"var ErrNoCapabilityOverlap = errors.New(\"no capability overlap\")",
|
||||||
|
"",
|
||||||
|
"func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfile, error) {",
|
||||||
|
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||||
|
"\tselected := profiles[0]",
|
||||||
|
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||||
|
"\tfor _, profile := range profiles[1:] {",
|
||||||
|
"\t\tif err := profile.Validate(); err != nil || profile != selected { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||||
|
"\t}",
|
||||||
|
"\treturn selected, nil",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
out.extend([
|
||||||
|
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
|
||||||
|
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}",
|
||||||
|
"\tvar transcript strings.Builder",
|
||||||
|
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
||||||
|
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
||||||
|
"\treturn []byte(transcript.String())",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
# Use io.EOF in generated code without making every generated decoder depend on
|
# Use io.EOF in generated code without making every generated decoder depend on
|
||||||
# error-string comparison; replace the deliberately compact placeholder.
|
# error-string comparison; replace the deliberately compact placeholder.
|
||||||
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
|
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
|
||||||
@@ -272,6 +303,58 @@ def swift_type(prop: dict[str, Any]) -> str:
|
|||||||
return "String"
|
return "String"
|
||||||
|
|
||||||
|
|
||||||
|
def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||||
|
lines: list[str] = []
|
||||||
|
required = set(definition.get("required", []))
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = rust_field(prop_name)
|
||||||
|
value = f"self.{field}"
|
||||||
|
if prop_name not in required:
|
||||||
|
value = f"value"
|
||||||
|
lines.append(f" if let Some(value) = &self.{field} {{")
|
||||||
|
prefix, suffix = " ", " }"
|
||||||
|
else:
|
||||||
|
prefix, suffix = "", ""
|
||||||
|
if prop.get("type") == "string":
|
||||||
|
if prop_name in required and prop.get("minLength", 0) > 0:
|
||||||
|
lines.append(f" {prefix}if {value}.is_empty() {{ return Err(ValidationError::new(\"{prop_name}\", \"required\")); }}")
|
||||||
|
if "minLength" in prop:
|
||||||
|
lines.append(f" {prefix}if !{value}.is_empty() && {value}.len() < {prop['minLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_length\")); }}")
|
||||||
|
if "maxLength" in prop:
|
||||||
|
lines.append(f" {prefix}if {value}.len() > {prop['maxLength']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_length\")); }}")
|
||||||
|
if "const" in prop:
|
||||||
|
lines.append(f" {prefix}if {value} != \"{prop['const']}\" {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}")
|
||||||
|
if "enum" in prop:
|
||||||
|
allowed = " && ".join(f'{value} != \"{item}\"' for item in prop["enum"])
|
||||||
|
lines.append(f" {prefix}if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_value\")); }}")
|
||||||
|
if prop.get("type") == "integer":
|
||||||
|
if "minimum" in prop:
|
||||||
|
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}")
|
||||||
|
if "maximum" in prop:
|
||||||
|
lines.append(f" {prefix}if {value} > {prop['maximum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"maximum\")); }}")
|
||||||
|
if prop.get("type") == "array":
|
||||||
|
if "minItems" in prop:
|
||||||
|
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
|
||||||
|
if "maxItems" in prop:
|
||||||
|
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
|
||||||
|
item_ref = ref_name(prop.get("items", {}))
|
||||||
|
if item_ref:
|
||||||
|
lines.append(f" {prefix}for item in {value}.iter() {{ item.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_item\"))?; }}")
|
||||||
|
reference = ref_name(prop)
|
||||||
|
if reference:
|
||||||
|
lines.append(f" {prefix}{value}.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_object\"))?;")
|
||||||
|
if suffix:
|
||||||
|
lines.append(suffix)
|
||||||
|
name = definition["name"]
|
||||||
|
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||||
|
lines.append(" if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new(\"bounds\", \"invalid_order\")); }")
|
||||||
|
if name == "GatewayRegistration":
|
||||||
|
lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }")
|
||||||
|
if name == "ChannelFrame":
|
||||||
|
lines.append(" if self.fragmentIndex >= self.fragmentCount { return Err(ValidationError::new(\"fragment_index\", \"invalid_order\")); }")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
||||||
out = [
|
out = [
|
||||||
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
||||||
@@ -282,6 +365,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
|||||||
f'pub const N_MINUS_2_WIRE_VERSION: &str = "{compatibility["n_minus_2"]}";',
|
f'pub const N_MINUS_2_WIRE_VERSION: &str = "{compatibility["n_minus_2"]}";',
|
||||||
"pub type JsonObject = std::collections::BTreeMap<String, String>;",
|
"pub type JsonObject = std::collections::BTreeMap<String, String>;",
|
||||||
"",
|
"",
|
||||||
|
"#[derive(Debug, Clone, PartialEq, Eq)]",
|
||||||
|
"pub struct ValidationError { pub field: &'static str, pub code: &'static str }",
|
||||||
|
"impl ValidationError { pub const fn new(field: &'static str, code: &'static str) -> Self { Self { field, code } } }",
|
||||||
|
"",
|
||||||
]
|
]
|
||||||
for name in sorted(defs):
|
for name in sorted(defs):
|
||||||
definition = defs[name]
|
definition = defs[name]
|
||||||
@@ -292,11 +379,113 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
|||||||
typ = rust_type(prop)
|
typ = rust_type(prop)
|
||||||
if prop_name not in required:
|
if prop_name not in required:
|
||||||
typ = f"Option<{typ}>"
|
typ = f"Option<{typ}>"
|
||||||
out.append(f" pub {field}: {typ},")
|
out.append(f" {field}: {typ},")
|
||||||
out.extend(["}", ""])
|
out.extend(["}", ""])
|
||||||
|
parameters: list[str] = []
|
||||||
|
assignments: list[str] = []
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = rust_field(prop_name)
|
||||||
|
typ = rust_type(prop)
|
||||||
|
if prop_name not in required:
|
||||||
|
typ = f"Option<{typ}>"
|
||||||
|
parameters.append(f"{field}: {typ}")
|
||||||
|
assignments.append(field)
|
||||||
|
out.append(f"impl {name} {{")
|
||||||
|
out.append(f" pub fn new({', '.join(parameters)}) -> Result<Self, ValidationError> {{")
|
||||||
|
out.append(f" let value = Self {{ {', '.join(assignments)} }};")
|
||||||
|
out.append(" value.validate()?;")
|
||||||
|
out.append(" Ok(value)")
|
||||||
|
out.append(" }")
|
||||||
|
out.append(" pub fn validate(&self) -> Result<(), ValidationError> {")
|
||||||
|
out.extend(rust_validation(definition))
|
||||||
|
out.append(" Ok(())")
|
||||||
|
out.append(" }")
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = rust_field(prop_name)
|
||||||
|
typ = rust_type(prop)
|
||||||
|
if prop_name not in required:
|
||||||
|
typ = f"Option<{typ}>"
|
||||||
|
out.append(f" pub fn {field}(&self) -> &{typ} {{ &self.{field} }}")
|
||||||
|
if name == "TunnelAdmissionRequest":
|
||||||
|
out.extend([
|
||||||
|
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
|
||||||
|
" let reconnect_sequence = self.reconnectSequence.to_string();",
|
||||||
|
" let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];",
|
||||||
|
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
|
||||||
|
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
|
||||||
|
" transcript.into_bytes()",
|
||||||
|
" }",
|
||||||
|
])
|
||||||
|
out.extend(["}", ""])
|
||||||
|
out.extend([
|
||||||
|
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
|
||||||
|
" let selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
|
||||||
|
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||||
|
" for profile in &profiles[1..] {",
|
||||||
|
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||||
|
" if profile != &selected { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||||
|
" }",
|
||||||
|
" Ok(selected)",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||||
|
lines: list[str] = []
|
||||||
|
required = set(definition.get("required", []))
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = swift_field(prop_name)
|
||||||
|
value = f"self.{field}"
|
||||||
|
if prop_name not in required:
|
||||||
|
value = "value"
|
||||||
|
lines.append(f" if let value = self.{field} {{")
|
||||||
|
prefix, suffix = " ", " }"
|
||||||
|
else:
|
||||||
|
prefix, suffix = "", ""
|
||||||
|
if prop.get("type") == "string":
|
||||||
|
if prop_name in required and prop.get("minLength", 0) > 0:
|
||||||
|
lines.append(f" {prefix}if {value}.isEmpty {{ throw ContractValidationError(field: \"{prop_name}\", code: \"required\") }}")
|
||||||
|
if "minLength" in prop:
|
||||||
|
lines.append(f" {prefix}if !{value}.isEmpty && {value}.utf8.count < {prop['minLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_length\") }}")
|
||||||
|
if "maxLength" in prop:
|
||||||
|
lines.append(f" {prefix}if {value}.utf8.count > {prop['maxLength']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_length\") }}")
|
||||||
|
if "const" in prop:
|
||||||
|
lines.append(f" {prefix}if {value} != \"{prop['const']}\" {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
|
||||||
|
if "enum" in prop:
|
||||||
|
allowed = ", ".join(f'\"{item}\"' for item in prop["enum"])
|
||||||
|
lines.append(f" {prefix}if ![{allowed}].contains({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_value\") }}")
|
||||||
|
if prop.get("format") == "date-time":
|
||||||
|
lines.append(f" {prefix}if ISO8601DateFormatter().date(from: {value}) == nil {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
|
||||||
|
if prop.get("type") == "integer":
|
||||||
|
if "minimum" in prop:
|
||||||
|
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}")
|
||||||
|
if "maximum" in prop:
|
||||||
|
lines.append(f" {prefix}if {value} > {prop['maximum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"maximum\") }}")
|
||||||
|
if prop.get("type") == "array":
|
||||||
|
if "minItems" in prop:
|
||||||
|
lines.append(f" {prefix}if {value}.count < {prop['minItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_items\") }}")
|
||||||
|
if "maxItems" in prop:
|
||||||
|
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
|
||||||
|
item_ref = ref_name(prop.get("items", {}))
|
||||||
|
if item_ref:
|
||||||
|
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
|
||||||
|
reference = ref_name(prop)
|
||||||
|
if reference:
|
||||||
|
lines.append(f" {prefix}try {value}.validate()")
|
||||||
|
if suffix:
|
||||||
|
lines.append(suffix)
|
||||||
|
name = definition["name"]
|
||||||
|
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||||
|
lines.append(" if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: \"bounds\", code: \"invalid_order\") }")
|
||||||
|
if name == "GatewayRegistration":
|
||||||
|
lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }")
|
||||||
|
if name == "ChannelFrame":
|
||||||
|
lines.append(" if fragmentIndex >= fragmentCount { throw ContractValidationError(field: \"fragment_index\", code: \"invalid_order\") }")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
||||||
out = [
|
out = [
|
||||||
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
||||||
@@ -306,6 +495,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
|||||||
f'public let currentWireVersion = "{compatibility["current"]}"',
|
f'public let currentWireVersion = "{compatibility["current"]}"',
|
||||||
f'public let nMinus1WireVersion = "{compatibility["n_minus_1"]}"',
|
f'public let nMinus1WireVersion = "{compatibility["n_minus_1"]}"',
|
||||||
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
||||||
|
"public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String }",
|
||||||
|
"private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } }",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
for name in sorted(defs):
|
for name in sorted(defs):
|
||||||
@@ -320,16 +511,55 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
|||||||
out.append(" enum CodingKeys: String, CodingKey {")
|
out.append(" enum CodingKeys: String, CodingKey {")
|
||||||
for prop_name in definition.get("properties", {}):
|
for prop_name in definition.get("properties", {}):
|
||||||
out.append(f" case {swift_field(prop_name)} = \"{prop_name}\"")
|
out.append(f" case {swift_field(prop_name)} = \"{prop_name}\"")
|
||||||
out.extend([" }", "", " public init(from decoder: Decoder) throws {",])
|
parameters: list[str] = []
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
typ = swift_type(prop)
|
||||||
|
if prop_name not in required:
|
||||||
|
typ += "?"
|
||||||
|
parameters.append(f"{swift_field(prop_name)}: {typ}")
|
||||||
|
out.extend([" }", "", f" public init({', '.join(parameters)}) throws {{"])
|
||||||
|
for prop_name in definition.get("properties", {}):
|
||||||
|
field = swift_field(prop_name)
|
||||||
|
out.append(f" self.{field} = {field}")
|
||||||
|
out.extend([" try validate()", " }", "", " public init(from decoder: Decoder) throws {"])
|
||||||
|
out.append(" let all = try decoder.container(keyedBy: AnyCodingKey.self)")
|
||||||
|
out.append(" for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: \"unknown_field\") }")
|
||||||
out.append(" let c = try decoder.container(keyedBy: CodingKeys.self)")
|
out.append(" let c = try decoder.container(keyedBy: CodingKeys.self)")
|
||||||
|
decoded: list[str] = []
|
||||||
for prop_name, prop in definition.get("properties", {}).items():
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
field = swift_field(prop_name)
|
field = swift_field(prop_name)
|
||||||
typ = swift_type(prop)
|
typ = swift_type(prop)
|
||||||
if prop_name in required:
|
if prop_name in required:
|
||||||
out.append(f" {field} = try c.decode({typ}.self, forKey: .{field})")
|
decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})")
|
||||||
else:
|
else:
|
||||||
out.append(f" {field} = try c.decodeIfPresent({typ}.self, forKey: .{field})")
|
decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})")
|
||||||
out.extend([" }", "}", ""])
|
out.append(f" try self.init({', '.join(decoded)})")
|
||||||
|
out.extend([" }", "", " public func validate() throws {"])
|
||||||
|
out.extend(swift_validation(definition))
|
||||||
|
out.extend([" }", "", " 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) }", "}", ""])
|
||||||
|
out.extend([
|
||||||
|
"public extension TunnelAdmissionRequest {",
|
||||||
|
" func deviceAdmissionTranscript() -> Data {",
|
||||||
|
" let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]",
|
||||||
|
" var transcript = \"versevdi/tunnel-admission/v1\"",
|
||||||
|
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
||||||
|
" return Data(transcript.utf8)",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"public extension CapabilityProfile {",
|
||||||
|
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
||||||
|
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||||
|
" try selected.validate()",
|
||||||
|
" for profile in profiles.dropFirst() {",
|
||||||
|
" try profile.validate()",
|
||||||
|
" if profile != selected { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||||
|
" }",
|
||||||
|
" return selected",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
])
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func classifyDatagram(encoded string) string {
|
|||||||
if raw[2] != 1 {
|
if raw[2] != 1 {
|
||||||
return "invalid:unsupported_version"
|
return "invalid:unsupported_version"
|
||||||
}
|
}
|
||||||
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515}
|
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
limit, ok := limits[raw[3]]
|
limit, ok := limits[raw[3]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "invalid:unknown_channel"
|
return "invalid:unknown_channel"
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ fn classify_datagram(encoded: &str) -> &'static str {
|
|||||||
1 => 1024,
|
1 => 1024,
|
||||||
2 => 2048,
|
2 => 2048,
|
||||||
3 => 65515,
|
3 => 65515,
|
||||||
|
10 | 11 | 12 => 1179,
|
||||||
_ => return "invalid:unknown_channel",
|
_ => return "invalid:unknown_channel",
|
||||||
};
|
};
|
||||||
if raw[4] != 0 {
|
if raw[4] != 0 {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func classifyDatagram(_ encoded: String) -> String {
|
|||||||
case 1: limit = 1024
|
case 1: limit = 1024
|
||||||
case 2: limit = 2048
|
case 2: limit = 2048
|
||||||
case 3: limit = 65515
|
case 3: limit = 65515
|
||||||
|
case 10, 11, 12: limit = 1179
|
||||||
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" }
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compile and exercise strict generated Swift and Rust gateway contracts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: list[str], directory: pathlib.Path) -> None:
|
||||||
|
result = subprocess.run(command, cwd=directory, text=True, capture_output=True, check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
|
||||||
|
|
||||||
|
|
||||||
|
def run_failure(command: list[str], directory: pathlib.Path, expected: str) -> None:
|
||||||
|
result = subprocess.run(command, cwd=directory, text=True, capture_output=True, check=False)
|
||||||
|
if result.returncode == 0 or expected not in result.stdout + result.stderr:
|
||||||
|
raise RuntimeError("expected failure: %s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="versevdi-generated-contracts-") as temporary:
|
||||||
|
workspace = pathlib.Path(temporary)
|
||||||
|
swift = workspace / "main.swift"
|
||||||
|
swift.write_text(
|
||||||
|
"""import Foundation
|
||||||
|
|
||||||
|
let capability = try CapabilityProfile(
|
||||||
|
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||||
|
audio: "encoded", sourceRateControl: "server", clientDecode: "h264-opus"
|
||||||
|
)
|
||||||
|
let request = try TunnelAdmissionRequest(
|
||||||
|
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||||
|
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
||||||
|
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
|
||||||
|
)
|
||||||
|
_ = request
|
||||||
|
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||||
|
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
|
||||||
|
fatalError("unexpected device admission transcript")
|
||||||
|
}
|
||||||
|
let incompatible = try CapabilityProfile(
|
||||||
|
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||||
|
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
|
||||||
|
fatalError("matching capability profiles did not intersect")
|
||||||
|
}
|
||||||
|
} catch { fatalError("matching capability profiles did not intersect") }
|
||||||
|
do {
|
||||||
|
_ = try CapabilityProfile.intersection([capability, incompatible])
|
||||||
|
fatalError("profiles without overlap were accepted")
|
||||||
|
} catch { }
|
||||||
|
let valid = try request.encodeJSON()
|
||||||
|
var unsupported = try JSONSerialization.jsonObject(with: valid) as! [String: Any]
|
||||||
|
unsupported["version"] = "2"
|
||||||
|
var downgrade = try JSONSerialization.jsonObject(with: valid) as! [String: Any]
|
||||||
|
downgrade["version"] = "0"
|
||||||
|
var unknown = try JSONSerialization.jsonObject(with: valid) as! [String: Any]
|
||||||
|
unknown["unknown"] = true
|
||||||
|
for invalid in [
|
||||||
|
try JSONSerialization.data(withJSONObject: unsupported),
|
||||||
|
try JSONSerialization.data(withJSONObject: downgrade),
|
||||||
|
try JSONSerialization.data(withJSONObject: unknown),
|
||||||
|
Data("{".utf8),
|
||||||
|
valid + Data(" {}".utf8),
|
||||||
|
] {
|
||||||
|
do {
|
||||||
|
_ = try TunnelAdmissionRequest.decodeJSON(invalid)
|
||||||
|
fatalError("invalid tunnel admission request was accepted")
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
_ = try AllocationPolicy(
|
||||||
|
minimumKbps: 100, targetKbps: 50, maximumKbps: 25, tier: "standard",
|
||||||
|
audience: "audience", protocolValue: "verse", protocolVersion: 1,
|
||||||
|
grantTtlSeconds: 60, reservationLeaseSeconds: 300
|
||||||
|
)
|
||||||
|
fatalError("invalid allocation bounds were accepted")
|
||||||
|
} catch { }
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
run(["swiftc", str(ROOT / "gen/swift/Protocol.swift"), str(swift), "-o", str(workspace / "swift-contracts")], ROOT)
|
||||||
|
run([str(workspace / "swift-contracts")], ROOT)
|
||||||
|
|
||||||
|
rust = workspace / "protocol.rs"
|
||||||
|
shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust)
|
||||||
|
with rust.open("a", encoding="utf-8") as output:
|
||||||
|
output.write(
|
||||||
|
"""
|
||||||
|
fn main() {
|
||||||
|
let capabilities = CapabilityProfile::new(
|
||||||
|
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||||
|
"encoded".into(), "server".into(), "h264-opus".into(),
|
||||||
|
).unwrap();
|
||||||
|
let request = TunnelAdmissionRequest::new(
|
||||||
|
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
|
).unwrap();
|
||||||
|
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
|
||||||
|
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
|
||||||
|
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus";
|
||||||
|
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
|
||||||
|
assert!(TunnelAdmissionRequest::new(
|
||||||
|
"2".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
|
).is_err());
|
||||||
|
assert!(TunnelAdmissionRequest::new(
|
||||||
|
"0".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
|
).is_err());
|
||||||
|
assert!(TunnelAdmissionRequest::new(
|
||||||
|
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
|
"g".repeat(43), 0, "short".into(), "s".repeat(86), capabilities.clone(),
|
||||||
|
).is_err());
|
||||||
|
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
|
||||||
|
let incompatible = CapabilityProfile::new(
|
||||||
|
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||||
|
"encoded".into(), "server".into(), "hevc-opus".into(),
|
||||||
|
).unwrap();
|
||||||
|
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
|
||||||
|
assert!(AllocationPolicy::new(
|
||||||
|
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
|
||||||
|
).is_err());
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
run(["rustc", str(rust), "-o", str(workspace / "rust-contracts")], ROOT)
|
||||||
|
run([str(workspace / "rust-contracts")], ROOT)
|
||||||
|
rust_unknown = workspace / "unknown.rs"
|
||||||
|
shutil.copyfile(ROOT / "gen/rust/protocol.rs", rust_unknown)
|
||||||
|
with rust_unknown.open("a", encoding="utf-8") as output:
|
||||||
|
output.write("\nfn main() { let _ = CapabilityProfile { unknown: String::new() }; }\n")
|
||||||
|
run_failure(["rustc", str(rust_unknown), "-o", str(workspace / "rust-unknown")], ROOT, "no field named `unknown`")
|
||||||
|
print("Generated strict contract checks passed")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -9,7 +9,7 @@ import pathlib
|
|||||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
HEADER_BYTES = 21
|
HEADER_BYTES = 21
|
||||||
MAX_FRAME_BYTES = 65536
|
MAX_FRAME_BYTES = 65536
|
||||||
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515}
|
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
|
|
||||||
|
|
||||||
def classify(raw: bytes) -> str:
|
def classify(raw: bytes) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user