Compare commits
3
Commits
4693102b3c
...
c3ea2f35c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3ea2f35c6 | ||
|
|
6e18bc9ee6 | ||
|
|
8eacc4fda9 |
@@ -9,9 +9,12 @@ valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040
|
||||
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-controller-arrival 1 gateway_input hex=564749310808020003010000001f valid
|
||||
valid-idr 1 gateway_feedback hex=5647463100010000 valid
|
||||
valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
|
||||
valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
|
||||
valid-quality-prompt 1 gateway_feedback hex=564746310004001812345678123456781234567812345678000000000000002a valid
|
||||
valid-stop-prompt 1 gateway_feedback hex=564746310005001012345678123456781234567812345678 valid
|
||||
valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
|
||||
valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
|
||||
valid-hdr 1 gateway_feedback hex=564746310112000101 valid
|
||||
@@ -30,5 +33,9 @@ invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 inval
|
||||
invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
|
||||
invalid-terminal-receipt-truncated 1 gateway_feedback hex=56474631000300 invalid:truncated
|
||||
invalid-terminal-receipt-length 1 gateway_feedback hex=5647463100030001 invalid:length
|
||||
invalid-feedback-type 1 gateway_feedback hex=5647463100040000 invalid:type
|
||||
invalid-feedback-type 1 gateway_feedback hex=5647463100060000 invalid:type
|
||||
invalid-quality-prompt-trailing 1 gateway_feedback hex=564746310004001912345678123456781234567812345678000000000000002a00 invalid:length
|
||||
invalid-quality-prompt-zero-uuid 1 gateway_feedback hex=564746310004001800000000000000000000000000000000000000000000002a invalid:field
|
||||
invalid-stop-prompt-zero-uuid 1 gateway_feedback hex=564746310005001000000000000000000000000000000000 invalid:field
|
||||
invalid-controller-arrival-family 1 gateway_input hex=564749310808020003040000001f invalid:field
|
||||
invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length
|
||||
|
||||
|
@@ -0,0 +1 @@
|
||||
{"mode":"auto","target_kbps":1000}
|
||||
@@ -0,0 +1 @@
|
||||
{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"12345678-1234-1234-1234-123456789abc","revision":7,"lease_generation":3,"outcome":"applied","current_applied_revision":6}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"12345678-1234-1234-1234-123456789ABC","revision":7,"lease_generation":3,"outcome":"unknown"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"00000000-0000-0000-0000-000000000000","revision":7,"lease_generation":3,"outcome":"unknown"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"acquisition":"poll","operation_id":"12345678-1234-1234-1234-123456789abc","revision":7}
|
||||
@@ -26,5 +26,14 @@
|
||||
"audience": "versevdi-gateway"
|
||||
},
|
||||
"correlation_id": "correlation-1",
|
||||
"provider_url": "https://provider.invalid"
|
||||
"provider_url": "https://provider.invalid",
|
||||
"selected_descriptor": {
|
||||
"video_profile": {"codec": "h264", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"},
|
||||
"audio_profile": {"codec": "opus", "sample_rate_hz": 48000, "channels": 2, "channel_layout": "stereo", "packet_duration_ms": 5},
|
||||
"display_mode": {"resolution_width": 1920, "resolution_height": 1080, "fps": 60},
|
||||
"bitrate_target_kbps": 20000,
|
||||
"bitrate_maximum_kbps": 40000,
|
||||
"adjustment": {"display_reason": "none", "bitrate_reason": "none"},
|
||||
"media_timestamp_basis": "gateway-send-wall-clock-ms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"resolution_width":1920,"resolution_height":1080,"fps":60,"codec":"H264","bitrate_kbps":20000,"audio_enabled":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":7,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":2,"created_at":"2099-01-01T00:00:00.100Z","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}
|
||||
@@ -0,0 +1 @@
|
||||
{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":7,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":2,"created_at":"2099-01-01T00:00:00+00:00","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}
|
||||
@@ -0,0 +1 @@
|
||||
{"video_profile":{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60},"bitrate_target_kbps":20000,"bitrate_maximum_kbps":40000,"adjustment":{"display_reason":"none","bitrate_reason":"none"},"media_timestamp_basis":"gateway-send-wall-clock-ms","provider_identity":"forbidden"}
|
||||
@@ -0,0 +1 @@
|
||||
{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"codec":"vp9","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}
|
||||
+30
-1
@@ -11,5 +11,34 @@
|
||||
"fixtures/conformance/gateway-input-feedback-v1.tsv",
|
||||
"fixtures/conformance/tunnel-v1.tsv"
|
||||
],
|
||||
"corpus_sha256": "6d2ce3a855b2fa45733a5f7b5b4c2e68448cceed5dfbca535ec81fe8cf230b30"
|
||||
"corpus_sha256": "10751a9192961aeb8ddcf3bbf720b026b07f92c871c813c301660dd3de930186",
|
||||
"json_files": [
|
||||
"fixtures/invalid/authenticated-session-partial-native-identity.json",
|
||||
"fixtures/invalid/bitrate-preference-auto-target.json",
|
||||
"fixtures/invalid/browser-session-native-identity.json",
|
||||
"fixtures/invalid/capability-rc5-opaque.json",
|
||||
"fixtures/invalid/gateway-quality-ack-contradictory.json",
|
||||
"fixtures/invalid/gateway-quality-ack-uppercase-uuid.json",
|
||||
"fixtures/invalid/gateway-quality-ack-zero-uuid.json",
|
||||
"fixtures/invalid/gateway-quality-poll-coordinates.json",
|
||||
"fixtures/invalid/manifest-provider-field.json",
|
||||
"fixtures/invalid/native-session-missing-identity.json",
|
||||
"fixtures/invalid/provider-stream-policy-rc5.json",
|
||||
"fixtures/invalid/quality-operation-noncanonical-fraction.json",
|
||||
"fixtures/invalid/quality-operation-offset-time.json",
|
||||
"fixtures/invalid/selected-session-descriptor-provider-field.json",
|
||||
"fixtures/invalid/session-request-policy-snapshot.json",
|
||||
"fixtures/invalid/session-request-rc5.json",
|
||||
"fixtures/invalid/video-profile-unknown.json",
|
||||
"fixtures/valid/authenticated-browser-session.json",
|
||||
"fixtures/valid/authenticated-native-session.json",
|
||||
"fixtures/valid/gateway-quality-ack-applied.json",
|
||||
"fixtures/valid/gateway-quality-poll.json",
|
||||
"fixtures/valid/manifest.json",
|
||||
"fixtures/valid/native-tunnel-credential.json",
|
||||
"fixtures/valid/selected-session-descriptor.json",
|
||||
"fixtures/valid/session-quality-limits.json",
|
||||
"fixtures/valid/session-request.json"
|
||||
],
|
||||
"json_corpus_sha256": "08dea278526a523d21ea4ca6270ba1e33d44dc0d701681917d7ffc6fb13053d7"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"operation_id":"12345678-1234-1234-1234-123456789abc","revision":7,"lease_generation":3,"outcome":"applied","current_applied_revision":7}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":2,"acquisition":"poll"}
|
||||
@@ -25,5 +25,14 @@
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"audience": "versevdi-gateway"
|
||||
},
|
||||
"correlation_id": "correlation-1"
|
||||
"correlation_id": "correlation-1",
|
||||
"selected_descriptor": {
|
||||
"video_profile": {"codec": "h264", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"},
|
||||
"audio_profile": {"codec": "opus", "sample_rate_hz": 48000, "channels": 2, "channel_layout": "stereo", "packet_duration_ms": 5},
|
||||
"display_mode": {"resolution_width": 1920, "resolution_height": 1080, "fps": 60},
|
||||
"bitrate_target_kbps": 20000,
|
||||
"bitrate_maximum_kbps": 40000,
|
||||
"adjustment": {"display_reason": "none", "bitrate_reason": "none"},
|
||||
"media_timestamp_basis": "gateway-send-wall-clock-ms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"video_profile": {"codec": "hevc", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"},
|
||||
"audio_profile": {"codec": "opus", "sample_rate_hz": 48000, "channels": 2, "channel_layout": "stereo", "packet_duration_ms": 5},
|
||||
"display_mode": {"resolution_width": 2560, "resolution_height": 1440, "fps": 120},
|
||||
"bitrate_target_kbps": 40000,
|
||||
"bitrate_maximum_kbps": 50000,
|
||||
"adjustment": {"display_reason": "none", "bitrate_reason": "policy_default"},
|
||||
"media_timestamp_basis": "gateway-send-wall-clock-ms"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"policy_version_id": "policy-1",
|
||||
"policy_display_limit": {"resolution_width": 3840, "resolution_height": 2160, "fps": 120},
|
||||
"selectable_display_maximum": {"resolution_width": 3840, "resolution_height": 2160, "fps": 120},
|
||||
"display_limit_override": false,
|
||||
"bitrate_minimum_kbps": 1000,
|
||||
"bitrate_target_kbps": 20000,
|
||||
"bitrate_maximum_kbps": 50000
|
||||
}
|
||||
@@ -2,5 +2,9 @@
|
||||
"client_device_id": "device-1",
|
||||
"device_key_id": "key-1",
|
||||
"pool_id": "pool-1",
|
||||
"idempotency_key": "request-1"
|
||||
"idempotency_key": "request-1",
|
||||
"video_profiles": [
|
||||
{"codec": "h264", "bit_depth": 8, "chroma_subsampling": "4:2:0", "color_space": "bt709-limited", "transfer_function": "sdr"}
|
||||
],
|
||||
"bitrate_preference": {"mode": "auto"}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ during cleanup.
|
||||
| `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. |
|
||||
| `0x08` | controller arrival | `controller` (u8, 0 through 15), `active_mask` (u16), `family` (u8: `0` unknown, `1` xbox, `2` playstation, `3` nintendo), `supported_button_flags` (u32). It must precede controller state for that controller. |
|
||||
|
||||
Keyboard, mouse button, UTF-8, controller, absolute mouse, and scroll messages
|
||||
are delivered over the gateway's reliable ordered input flow. Relative and
|
||||
@@ -60,7 +61,12 @@ is the following exact envelope:
|
||||
| 8 | N | payload | exact type-specific body |
|
||||
|
||||
The client-to-gateway types are `0x01` IDR request (empty), `0x02` FEC
|
||||
status, and `0x03` terminal receipt (empty). FEC status contains
|
||||
status, `0x03` terminal receipt (empty), `0x04` quality-work prompt, and
|
||||
`0x05` Stop-work prompt. The quality prompt is exactly `operation_id` (16
|
||||
canonical UUID bytes) followed by `revision` (nonzero u64). The Stop prompt is
|
||||
exactly `operation_id` (16 canonical UUID bytes). Prompts never authorize or
|
||||
complete work; the gateway acquires the matching durable mTLS work item.
|
||||
FEC status contains
|
||||
`frame_index` (u32), `highest_received_sequence` (u16),
|
||||
`next_contiguous_sequence` (u16), `missing_before_highest` (u16),
|
||||
`total_data_packets` (u16), `total_parity_packets` (u16),
|
||||
|
||||
+2357
-90
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -12,7 +12,7 @@
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"generator_sha256": "00c1905fc611ca9e226cd90da761b48b8e203734b10542befea397a30082d360",
|
||||
"generator_sha256": "c5756780f59a82d8f56762e52f58e22a4d6aca6648157f45e5bad6559b9deeec",
|
||||
"protocol_version": "1.0.0",
|
||||
"schema_sha256": "b2353c12269304289b4e872f27cc370ae61b958dea90d9fb7b6ab8afd7d37248"
|
||||
"schema_sha256": "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297"
|
||||
}
|
||||
|
||||
Binary file not shown.
+759
-59
File diff suppressed because it is too large
Load Diff
+1103
-119
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ tags:
|
||||
- name: resources
|
||||
- name: broker
|
||||
- name: events
|
||||
- name: session-quality
|
||||
- name: gateway
|
||||
paths:
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
@@ -326,6 +328,151 @@ paths:
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/session-quality-limits:
|
||||
get:
|
||||
tags: [session-quality]
|
||||
operationId: getSessionQualityLimits
|
||||
security: [{nativeBearer: []}]
|
||||
responses:
|
||||
'200': {description: 'Current global native session quality limits. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/SessionQualityLimits}}}}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
/api/v1/session-quality-limits/assignments/{assignment_id}:
|
||||
get:
|
||||
tags: [session-quality]
|
||||
operationId: getAssignmentSessionQualityLimits
|
||||
security: [{nativeBearer: []}]
|
||||
parameters: [{$ref: '#/components/parameters/AssignmentID'}]
|
||||
responses:
|
||||
'200': {description: 'Current assignment-specific native session quality limits. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/SessionQualityLimits}}}}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/v1/session-quality-limits/pools/{pool_id}:
|
||||
get:
|
||||
tags: [session-quality]
|
||||
operationId: getPoolSessionQualityLimits
|
||||
security: [{nativeBearer: []}]
|
||||
parameters: [{$ref: '#/components/parameters/PoolID'}]
|
||||
responses:
|
||||
'200': {description: 'Current pool-specific native session quality limits. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/SessionQualityLimits}}}}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/v1/admin/entitlements/{entitlement_id}/display-limit-override:
|
||||
patch:
|
||||
tags: [session-quality]
|
||||
operationId: updateEntitlementDisplayLimitOverride
|
||||
security:
|
||||
- browserSession: []
|
||||
browserCsrfCookie: []
|
||||
browserCsrfHeader: []
|
||||
parameters: [{$ref: '#/components/parameters/EntitlementID'}]
|
||||
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/DisplayLimitOverrideRequest}}}}
|
||||
responses:
|
||||
'204': {description: Direct entitlement display override updated.}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/broker/sessions/{session_id}/quality-changes:
|
||||
post:
|
||||
tags: [session-quality]
|
||||
operationId: createSessionQualityChange
|
||||
description: The owning principal and active device/key MUST match the subject-scoped broker session. Maximum JSON body: 16384 bytes.
|
||||
security: [{nativeBearer: []}]
|
||||
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeRequest}}}}
|
||||
responses:
|
||||
'201': {description: 'Durable quality change created. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeOperation}}}}
|
||||
'200': {description: 'Existing idempotent quality change returned. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeOperation}}}}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/broker/sessions/{session_id}/quality-changes/{operation_id}:
|
||||
get:
|
||||
tags: [session-quality]
|
||||
operationId: getSessionQualityChange
|
||||
description: The owning principal and active device/key MUST match the subject-scoped broker session and operation. Maximum JSON body: 16384 bytes.
|
||||
security: [{nativeBearer: []}]
|
||||
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/OperationID'}]
|
||||
responses:
|
||||
'200': {description: 'Durable quality change state. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/QualityChangeOperation}}}}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/v1/broker/sessions/{session_id}/stop-operations:
|
||||
post:
|
||||
tags: [session-quality]
|
||||
operationId: createSessionStopOperation
|
||||
description: The owning principal and active device/key MUST match the subject-scoped broker session. Maximum JSON body: 16384 bytes.
|
||||
security: [{nativeBearer: []}]
|
||||
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
responses:
|
||||
'201': {description: 'Durable Stop operation created. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation}}}}
|
||||
'200': {description: 'Existing idempotent Stop operation returned. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation}}}}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/broker/sessions/{session_id}/stop-operations/{operation_id}:
|
||||
get:
|
||||
tags: [session-quality]
|
||||
operationId: getSessionStopOperation
|
||||
description: The owning principal and active device/key MUST match the subject-scoped broker session and operation. Maximum JSON body: 16384 bytes.
|
||||
security: [{nativeBearer: []}]
|
||||
parameters: [{$ref: '#/components/parameters/SessionID'}, {$ref: '#/components/parameters/OperationID'}]
|
||||
responses:
|
||||
'200': {description: 'Durable Stop operation state. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/StopOperation}}}}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/v1/gateway/quality-work:
|
||||
post:
|
||||
tags: [gateway]
|
||||
operationId: acquireGatewayQualityWork
|
||||
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST match the session, gateway, reconnect sequence, and supplied operation/revision/lease generation coordinates. A `poll` acquisition omits unknown operation coordinates and discovers pending session work after a lost prompt; `prompt` and `observation` coordinates MUST match exactly. Maximum JSON body: 16384 bytes.
|
||||
security: [{gatewayMutualTLS: []}]
|
||||
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityWorkRequest}}}}
|
||||
responses:
|
||||
'200': {description: 'Leased quality work or reconciliation observation. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityWork}}}}
|
||||
'204': {description: No matching work is available.}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/gateway/quality-ack:
|
||||
post:
|
||||
tags: [gateway]
|
||||
operationId: acknowledgeGatewayQualityWork
|
||||
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST exactly match the session, gateway, reconnect sequence, operation, revision, and monotonic lease generation tuple. Stale lease generations MUST be rejected. `applied` requires `current_applied_revision == revision`; `proven_prior` requires `current_applied_revision < revision`; `unknown` forbids `current_applied_revision` and makes no applied-revision assertion. Maximum JSON body: 16384 bytes.
|
||||
security: [{gatewayMutualTLS: []}]
|
||||
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityAck}}}}
|
||||
responses:
|
||||
'204': {description: Quality acknowledgement committed idempotently.}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/gateway/stop-work:
|
||||
post:
|
||||
tags: [gateway]
|
||||
operationId: acquireGatewayStopWork
|
||||
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST match the session, gateway, reconnect sequence, and any supplied operation coordinate. A `poll` acquisition omits the unknown operation ID and discovers pending session work after a lost prompt; a `prompt` coordinate MUST match exactly. Maximum JSON body: 16384 bytes.
|
||||
security: [{gatewayMutualTLS: []}]
|
||||
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayStopWorkRequest}}}}
|
||||
responses:
|
||||
'200': {description: 'One-way non-reissued Stop work claim. Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayStopWork}}}}
|
||||
'204': {description: No matching Stop work is available.}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/gateway/stop-ack:
|
||||
post:
|
||||
tags: [gateway]
|
||||
operationId: acknowledgeGatewayStopWork
|
||||
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST exactly match the session, gateway, reconnect sequence, and operation tuple. Maximum JSON body: 16384 bytes.
|
||||
security: [{gatewayMutualTLS: []}]
|
||||
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayStopAck}}}}
|
||||
responses:
|
||||
'204': {description: Stop acknowledgement committed idempotently.}
|
||||
'400': {$ref: '#/components/responses/InvalidRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/v1/events:
|
||||
get:
|
||||
tags: [events]
|
||||
@@ -385,6 +532,8 @@ components:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque
|
||||
gatewayMutualTLS:
|
||||
type: mutualTLS
|
||||
parameters:
|
||||
DeviceID:
|
||||
name: device_id
|
||||
@@ -396,6 +545,26 @@ components:
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
AssignmentID:
|
||||
name: assignment_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
PoolID:
|
||||
name: pool_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
EntitlementID:
|
||||
name: entitlement_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
OperationID:
|
||||
name: operation_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, format: uuid, pattern: '^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'}
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
|
||||
@@ -54,9 +54,56 @@ message CapabilityProfile {
|
||||
string transport = 1;
|
||||
string framing = 2;
|
||||
string media = 3;
|
||||
string audio = 4;
|
||||
reserved 4;
|
||||
string source_rate_control = 5;
|
||||
repeated string client_decode = 6;
|
||||
reserved 6;
|
||||
repeated VideoProfile video_profiles = 7;
|
||||
repeated AudioProfile audio_profiles = 8;
|
||||
}
|
||||
|
||||
message VideoProfile {
|
||||
string codec = 1;
|
||||
uint32 bit_depth = 2;
|
||||
string chroma_subsampling = 3;
|
||||
string color_space = 4;
|
||||
string transfer_function = 5;
|
||||
}
|
||||
|
||||
message AudioProfile {
|
||||
string codec = 1;
|
||||
uint32 sample_rate_hz = 2;
|
||||
uint32 channels = 3;
|
||||
string channel_layout = 4;
|
||||
uint32 packet_duration_ms = 5;
|
||||
}
|
||||
|
||||
message DisplayMode {
|
||||
uint32 resolution_width = 1;
|
||||
uint32 resolution_height = 2;
|
||||
uint32 fps = 3;
|
||||
}
|
||||
|
||||
message SessionAdjustment {
|
||||
string display_reason = 1;
|
||||
string bitrate_reason = 2;
|
||||
}
|
||||
|
||||
message SelectedSessionDescriptor {
|
||||
VideoProfile video_profile = 1;
|
||||
AudioProfile audio_profile = 2;
|
||||
DisplayMode display_mode = 3;
|
||||
uint64 bitrate_target_kbps = 4;
|
||||
uint64 bitrate_maximum_kbps = 5;
|
||||
SessionAdjustment adjustment = 6;
|
||||
string media_timestamp_basis = 7;
|
||||
}
|
||||
|
||||
message ProviderStreamPolicy {
|
||||
VideoProfile video_profile = 1;
|
||||
AudioProfile audio_profile = 2;
|
||||
DisplayMode display_mode = 3;
|
||||
uint64 bitrate_target_kbps = 4;
|
||||
uint64 bitrate_maximum_kbps = 5;
|
||||
}
|
||||
|
||||
message GatewayRegistration {
|
||||
@@ -115,6 +162,7 @@ message SessionAuthority {
|
||||
CapabilityProfile capabilities = 7;
|
||||
string provider_profile = 8;
|
||||
string provider_identity = 9;
|
||||
SelectedSessionDescriptor selected_descriptor = 10;
|
||||
}
|
||||
|
||||
message ClientSessionAuthority {
|
||||
@@ -125,6 +173,7 @@ message ClientSessionAuthority {
|
||||
uint64 reconnect_sequence = 5;
|
||||
google.protobuf.Timestamp expires_at = 6;
|
||||
CapabilityProfile capabilities = 7;
|
||||
SelectedSessionDescriptor selected_descriptor = 8;
|
||||
}
|
||||
|
||||
message ProviderSessionWork {
|
||||
@@ -147,6 +196,73 @@ message ProviderSessionWork {
|
||||
string client_id = 17;
|
||||
ClipboardPolicy clipboard_policy = 18;
|
||||
bool provider_application_termination_allowed = 19;
|
||||
ProviderStreamPolicy stream_policy = 20;
|
||||
}
|
||||
|
||||
message GatewayQualityWorkRequest {
|
||||
string version = 1;
|
||||
string session_id = 2;
|
||||
string gateway_id = 3;
|
||||
uint64 reconnect_sequence = 4;
|
||||
string operation_id = 5;
|
||||
optional uint64 revision = 6;
|
||||
optional uint64 current_applied_revision = 7;
|
||||
string acquisition = 8;
|
||||
optional uint64 lease_generation = 9;
|
||||
}
|
||||
|
||||
message GatewayQualityWork {
|
||||
string version = 1;
|
||||
string session_id = 2;
|
||||
string gateway_id = 3;
|
||||
uint64 reconnect_sequence = 4;
|
||||
string operation_id = 5;
|
||||
uint64 revision = 6;
|
||||
google.protobuf.Timestamp lease_expires_at = 7;
|
||||
SelectedSessionDescriptor selected_descriptor = 8;
|
||||
optional uint64 current_applied_revision = 9;
|
||||
uint64 lease_generation = 10;
|
||||
}
|
||||
|
||||
message GatewayQualityAck {
|
||||
string version = 1;
|
||||
string session_id = 2;
|
||||
string gateway_id = 3;
|
||||
uint64 reconnect_sequence = 4;
|
||||
string operation_id = 5;
|
||||
uint64 revision = 6;
|
||||
string outcome = 7;
|
||||
optional uint64 current_applied_revision = 8;
|
||||
string failure_code = 9;
|
||||
uint64 lease_generation = 10;
|
||||
}
|
||||
|
||||
message GatewayStopWork {
|
||||
string version = 1;
|
||||
string session_id = 2;
|
||||
string gateway_id = 3;
|
||||
uint64 reconnect_sequence = 4;
|
||||
string operation_id = 5;
|
||||
uint32 attempt = 6;
|
||||
}
|
||||
|
||||
message GatewayStopWorkRequest {
|
||||
string version = 1;
|
||||
string session_id = 2;
|
||||
string gateway_id = 3;
|
||||
uint64 reconnect_sequence = 4;
|
||||
string operation_id = 5;
|
||||
string acquisition = 6;
|
||||
}
|
||||
|
||||
message GatewayStopAck {
|
||||
string version = 1;
|
||||
string session_id = 2;
|
||||
string gateway_id = 3;
|
||||
uint64 reconnect_sequence = 4;
|
||||
string operation_id = 5;
|
||||
string outcome = 6;
|
||||
string failure_code = 7;
|
||||
}
|
||||
|
||||
message ClipboardPolicy {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"datagrams": [
|
||||
{"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": "session.quality.prompt.v1", "direction": "client-to-server", "max_payload_bytes": 32, "payload_profile": "gateway-feedback-v1"},
|
||||
{"id": "session.stop.prompt.v1", "direction": "client-to-server", "max_payload_bytes": 24, "payload_profile": "gateway-feedback-v1"},
|
||||
{"id": "controller.arrival.v1", "direction": "client-to-server", "max_payload_bytes": 14, "payload_profile": "gateway-input-v1"},
|
||||
{"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},
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
{"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}
|
||||
{"id": "input.scroll.v1", "direction": "client-to-server", "max_frame_bytes": 10},
|
||||
{"id": "video.profile.v1", "direction": "bidirectional", "max_frame_bytes": 65536},
|
||||
{"id": "session.quality.v1", "direction": "bidirectional", "max_frame_bytes": 32},
|
||||
{"id": "session.stop.v1", "direction": "bidirectional", "max_frame_bytes": 24},
|
||||
{"id": "controller.arrival.v1", "direction": "client-to-server", "max_frame_bytes": 14}
|
||||
]
|
||||
}
|
||||
|
||||
+241
-31
@@ -64,22 +64,24 @@
|
||||
"AssignedDesktop": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["assignment_id", "pool_id", "name", "availability"],
|
||||
"required": ["assignment_id", "pool_id", "name", "availability", "quality_limits"],
|
||||
"properties": {
|
||||
"assignment_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"availability": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||
"availability": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"quality_limits": {"$ref": "#/$defs/SessionQualityLimits"}
|
||||
}
|
||||
},
|
||||
"EntitledPool": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["pool_id", "name", "assignment_state"],
|
||||
"required": ["pool_id", "name", "assignment_state", "quality_limits"],
|
||||
"properties": {
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"assignment_state": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||
"assignment_state": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"quality_limits": {"$ref": "#/$defs/SessionQualityLimits"}
|
||||
}
|
||||
},
|
||||
"ResourceList": {
|
||||
@@ -232,32 +234,106 @@
|
||||
"fps": {"type": "integer", "minimum": 1, "maximum": 240}
|
||||
}
|
||||
},
|
||||
"VideoProfile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["codec", "bit_depth", "chroma_subsampling", "color_space", "transfer_function"],
|
||||
"properties": {
|
||||
"codec": {"type": "string", "enum": ["h264", "hevc", "av1"]},
|
||||
"bit_depth": {"type": "integer", "minimum": 8, "maximum": 8},
|
||||
"chroma_subsampling": {"type": "string", "const": "4:2:0"},
|
||||
"color_space": {"type": "string", "const": "bt709-limited"},
|
||||
"transfer_function": {"type": "string", "const": "sdr"}
|
||||
}
|
||||
},
|
||||
"AudioProfile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["codec", "sample_rate_hz", "channels", "channel_layout", "packet_duration_ms"],
|
||||
"properties": {
|
||||
"codec": {"type": "string", "const": "opus"},
|
||||
"sample_rate_hz": {"type": "integer", "minimum": 48000, "maximum": 48000},
|
||||
"channels": {"type": "integer", "minimum": 2, "maximum": 2},
|
||||
"channel_layout": {"type": "string", "const": "stereo"},
|
||||
"packet_duration_ms": {"type": "integer", "minimum": 5, "maximum": 5}
|
||||
}
|
||||
},
|
||||
"BitratePreference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode"],
|
||||
"properties": {
|
||||
"mode": {"type": "string", "enum": ["auto", "explicit"]},
|
||||
"target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000, "x-optional-pointer": true}
|
||||
}
|
||||
},
|
||||
"SessionQualityLimits": {
|
||||
"type": "object",
|
||||
"x-max-bytes": 16384,
|
||||
"additionalProperties": false,
|
||||
"required": ["policy_version_id", "policy_display_limit", "selectable_display_maximum", "display_limit_override", "bitrate_minimum_kbps", "bitrate_target_kbps", "bitrate_maximum_kbps"],
|
||||
"properties": {
|
||||
"policy_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"policy_display_limit": {"$ref": "#/$defs/DisplayMode"},
|
||||
"selectable_display_maximum": {"$ref": "#/$defs/DisplayMode"},
|
||||
"display_limit_override": {"type": "boolean"},
|
||||
"bitrate_minimum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"bitrate_target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"bitrate_maximum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000}
|
||||
}
|
||||
},
|
||||
"SessionAdjustment": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["display_reason", "bitrate_reason"],
|
||||
"properties": {
|
||||
"display_reason": {"type": "string", "enum": ["none", "policy_clamp", "direct_entitlement_override"]},
|
||||
"bitrate_reason": {"type": "string", "enum": ["none", "policy_default", "session_ceiling_clamp", "aggregate_capacity_clamp"]}
|
||||
}
|
||||
},
|
||||
"SelectedSessionDescriptor": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["video_profile", "audio_profile", "display_mode", "bitrate_target_kbps", "bitrate_maximum_kbps", "adjustment", "media_timestamp_basis"],
|
||||
"properties": {
|
||||
"video_profile": {"$ref": "#/$defs/VideoProfile"},
|
||||
"audio_profile": {"$ref": "#/$defs/AudioProfile"},
|
||||
"display_mode": {"$ref": "#/$defs/DisplayMode"},
|
||||
"bitrate_target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"bitrate_maximum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"adjustment": {"$ref": "#/$defs/SessionAdjustment"},
|
||||
"media_timestamp_basis": {"type": "string", "const": "gateway-send-wall-clock-ms"}
|
||||
}
|
||||
},
|
||||
"SessionRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key"],
|
||||
"required": ["client_device_id", "device_key_id", "pool_id", "idempotency_key", "video_profiles", "bitrate_preference"],
|
||||
"properties": {
|
||||
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"pool_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"idempotency_key": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
|
||||
"bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
|
||||
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"}
|
||||
}
|
||||
},
|
||||
"ReconnectRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["client_device_id", "device_key_id", "expected_version"],
|
||||
"required": ["client_device_id", "device_key_id", "expected_version", "display_relaunch_confirmed"],
|
||||
"properties": {
|
||||
"client_device_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"device_key_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"expected_version": {"type": "integer", "minimum": 1}
|
||||
"expected_version": {"type": "integer", "minimum": 1},
|
||||
"display_relaunch_confirmed": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"BrokerSession": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "principal_id", "pool_id", "state", "policy_snapshot", "cleanup_state", "idempotency_key", "correlation_id", "requested_at", "version"],
|
||||
"required": ["id", "principal_id", "pool_id", "state", "policy_snapshot", "cleanup_state", "idempotency_key", "correlation_id", "requested_at", "version", "requested_video_profiles", "requested_bitrate_preference"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"principal_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
@@ -275,7 +351,10 @@
|
||||
"ended_at": {"type": "string", "format": "date-time", "maxLength": 64},
|
||||
"version": {"type": "integer", "minimum": 1},
|
||||
"requested_display_mode": {"$ref": "#/$defs/DisplayMode"},
|
||||
"effective_display_mode": {"$ref": "#/$defs/DisplayMode"}
|
||||
"effective_display_mode": {"$ref": "#/$defs/DisplayMode"},
|
||||
"requested_video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
|
||||
"requested_bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
|
||||
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
|
||||
}
|
||||
},
|
||||
"AllocationPolicy": {
|
||||
@@ -297,7 +376,7 @@
|
||||
"ConnectionManifest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "purpose", "session_id", "reconnect_sequence", "gateway", "tunnel", "profile", "grant", "correlation_id"],
|
||||
"required": ["version", "purpose", "session_id", "reconnect_sequence", "gateway", "tunnel", "profile", "grant", "correlation_id", "selected_descriptor"],
|
||||
"properties": {
|
||||
"version": {"type": "string", "const": "1"},
|
||||
"purpose": {"type": "string", "enum": ["launch", "reconnect"]},
|
||||
@@ -307,7 +386,8 @@
|
||||
"tunnel": {"$ref": "#/$defs/ManifestTunnel"},
|
||||
"profile": {"$ref": "#/$defs/ManifestProfile"},
|
||||
"grant": {"$ref": "#/$defs/GrantReference"},
|
||||
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
"correlation_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
|
||||
}
|
||||
},
|
||||
"ManifestGateway": {
|
||||
@@ -431,20 +511,14 @@
|
||||
"CapabilityProfile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
||||
"required": ["transport", "framing", "media", "source_rate_control", "video_profiles", "audio_profiles"],
|
||||
"properties": {
|
||||
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||
"framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]},
|
||||
"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": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 2,
|
||||
"uniqueItems": true,
|
||||
"items": {"type": "string", "enum": ["h264-opus", "hevc-opus"]}
|
||||
}
|
||||
"video_profiles": {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": {"$ref": "#/$defs/VideoProfile"}},
|
||||
"audio_profiles": {"type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/AudioProfile"}}
|
||||
}
|
||||
},
|
||||
"GatewayRegistration": {
|
||||
@@ -538,7 +612,7 @@
|
||||
"SessionAuthority": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity"],
|
||||
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity", "selected_descriptor"],
|
||||
"properties": {
|
||||
"version": {"type": "string", "const": "1"},
|
||||
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
@@ -548,13 +622,14 @@
|
||||
"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}
|
||||
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
|
||||
}
|
||||
},
|
||||
"ClientSessionAuthority": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"],
|
||||
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"],
|
||||
"properties": {
|
||||
"version": {"type": "string", "const": "1"},
|
||||
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
@@ -562,20 +637,155 @@
|
||||
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"reconnect_sequence": {"type": "integer", "minimum": 0},
|
||||
"expires_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$", "maxLength": 64},
|
||||
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
|
||||
"capabilities": {"$ref": "#/$defs/CapabilityProfile"},
|
||||
"selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"}
|
||||
}
|
||||
},
|
||||
"ProviderStreamPolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["resolution_width", "resolution_height", "fps", "codec", "bitrate_kbps", "audio_enabled"],
|
||||
"required": ["video_profile", "audio_profile", "display_mode", "bitrate_target_kbps", "bitrate_maximum_kbps"],
|
||||
"properties": {
|
||||
"resolution_width": {"type": "integer", "minimum": 320, "maximum": 16384},
|
||||
"resolution_height": {"type": "integer", "minimum": 200, "maximum": 8640},
|
||||
"fps": {"type": "integer", "minimum": 1, "maximum": 240},
|
||||
"codec": {"type": "string", "enum": ["H264", "HEVC", "AV1"]},
|
||||
"bitrate_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"audio_enabled": {"type": "boolean"}
|
||||
"video_profile": {"$ref": "#/$defs/VideoProfile"},
|
||||
"audio_profile": {"$ref": "#/$defs/AudioProfile"},
|
||||
"display_mode": {"$ref": "#/$defs/DisplayMode"},
|
||||
"bitrate_target_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"bitrate_maximum_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000}
|
||||
}
|
||||
},
|
||||
"DisplayLimitOverrideRequest": {
|
||||
"type": "object",
|
||||
"x-max-bytes": 16384,
|
||||
"additionalProperties": false,
|
||||
"required": ["allow_display_limit_override", "expected_version"],
|
||||
"properties": {
|
||||
"allow_display_limit_override": {"type": "boolean"},
|
||||
"expected_version": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"QualityChangeRequest": {
|
||||
"type": "object",
|
||||
"x-max-bytes": 16384,
|
||||
"additionalProperties": false,
|
||||
"required": ["bitrate_preference", "expected_session_version", "expected_policy_version"],
|
||||
"properties": {
|
||||
"bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
|
||||
"expected_session_version": {"type": "integer", "minimum": 1},
|
||||
"expected_policy_version": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"QualityChangeOperation": {
|
||||
"type": "object",
|
||||
"x-max-bytes": 16384,
|
||||
"additionalProperties": false,
|
||||
"required": ["operation_id", "session_id", "revision", "state", "requested_bitrate_preference", "effective_bitrate_kbps", "governing_policy_version", "session_version", "created_at", "deadline_at", "updated_at"],
|
||||
"properties": {
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36},
|
||||
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"revision": {"type": "integer", "minimum": 1},
|
||||
"state": {"type": "string", "enum": ["pending", "applying", "applied", "failed", "timed_out"]},
|
||||
"requested_bitrate_preference": {"$ref": "#/$defs/BitratePreference"},
|
||||
"effective_bitrate_kbps": {"type": "integer", "minimum": 100, "maximum": 1000000},
|
||||
"governing_policy_version": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"session_version": {"type": "integer", "minimum": 1},
|
||||
"created_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
|
||||
"deadline_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
|
||||
"updated_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
|
||||
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"StopOperation": {
|
||||
"type": "object",
|
||||
"x-max-bytes": 16384,
|
||||
"additionalProperties": false,
|
||||
"required": ["operation_id", "session_id", "state", "session_version", "created_at", "deadline_at", "updated_at"],
|
||||
"properties": {
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36},
|
||||
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"state": {"type": "string", "enum": ["pending", "attempting", "applied", "failed", "termination_unconfirmed"]},
|
||||
"session_version": {"type": "integer", "minimum": 1},
|
||||
"created_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
|
||||
"deadline_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
|
||||
"updated_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64},
|
||||
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"GatewayQualityWorkRequest": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"x-max-bytes": 16384,
|
||||
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"],
|
||||
"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},
|
||||
"acquisition": {"type": "string", "enum": ["poll", "prompt", "observation"]},
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36},
|
||||
"revision": {"type": "integer", "minimum": 1, "x-optional-pointer": true},
|
||||
"lease_generation": {"type": "integer", "minimum": 1, "x-optional-pointer": true},
|
||||
"current_applied_revision": {"type": "integer", "minimum": 0, "x-optional-pointer": true}
|
||||
}
|
||||
},
|
||||
"GatewayQualityWork": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"x-max-bytes": 16384,
|
||||
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "revision", "lease_generation", "lease_expires_at", "selected_descriptor"],
|
||||
"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},
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "revision": {"type": "integer", "minimum": 1},
|
||||
"lease_generation": {"type": "integer", "minimum": 1},
|
||||
"lease_expires_at": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{0,8}[1-9])?Z$", "maxLength": 64}, "selected_descriptor": {"$ref": "#/$defs/SelectedSessionDescriptor"},
|
||||
"current_applied_revision": {"type": "integer", "minimum": 0, "x-optional-pointer": true}
|
||||
}
|
||||
},
|
||||
"GatewayQualityAck": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"x-max-bytes": 16384,
|
||||
"description": "Outcome invariants: applied requires current_applied_revision equal to revision; proven_prior requires current_applied_revision strictly less than revision; unknown forbids current_applied_revision and makes no applied-revision assertion.",
|
||||
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "revision", "lease_generation", "outcome"],
|
||||
"oneOf": [
|
||||
{"properties": {"outcome": {"const": "applied"}}, "required": ["current_applied_revision"]},
|
||||
{"properties": {"outcome": {"const": "proven_prior"}}, "required": ["current_applied_revision"]},
|
||||
{"properties": {"outcome": {"const": "unknown"}}, "not": {"required": ["current_applied_revision"]}}
|
||||
],
|
||||
"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},
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "revision": {"type": "integer", "minimum": 1},
|
||||
"lease_generation": {"type": "integer", "minimum": 1},
|
||||
"outcome": {"type": "string", "enum": ["applied", "proven_prior", "unknown"]}, "current_applied_revision": {"type": "integer", "minimum": 0, "x-optional-pointer": true},
|
||||
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"GatewayStopWorkRequest": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"x-max-bytes": 16384,
|
||||
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"],
|
||||
"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},
|
||||
"acquisition": {"type": "string", "enum": ["poll", "prompt"]},
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}
|
||||
}
|
||||
},
|
||||
"GatewayStopWork": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"x-max-bytes": 16384,
|
||||
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "attempt"],
|
||||
"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},
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "attempt": {"type": "integer", "minimum": 1, "maximum": 1}
|
||||
}
|
||||
},
|
||||
"GatewayStopAck": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"x-max-bytes": 16384,
|
||||
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "outcome"],
|
||||
"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},
|
||||
"operation_id": {"type": "string", "format": "uuid", "pattern": "^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36}, "outcome": {"type": "string", "enum": ["applied", "failed", "termination_unconfirmed"]},
|
||||
"failure_code": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
},
|
||||
"ProviderSessionWork": {
|
||||
|
||||
+51
-31
@@ -11,6 +11,24 @@ import (
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
func rc6Video(codec string) protocol.VideoProfile {
|
||||
return protocol.VideoProfile{Codec: codec, BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}
|
||||
}
|
||||
|
||||
func rc6Audio() protocol.AudioProfile {
|
||||
return protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5}
|
||||
}
|
||||
|
||||
func rc6Capability(codecs ...string) protocol.CapabilityProfile {
|
||||
videos := make([]protocol.VideoProfile, len(codecs))
|
||||
for index, codec := range codecs { videos[index] = rc6Video(codec) }
|
||||
return protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", SourceRateControl: "server", VideoProfiles: videos, AudioProfiles: []protocol.AudioProfile{rc6Audio()}}
|
||||
}
|
||||
|
||||
func rc6Descriptor(codec string) protocol.SelectedSessionDescriptor {
|
||||
return protocol.SelectedSessionDescriptor{VideoProfile: rc6Video(codec), AudioProfile: rc6Audio(), DisplayMode: protocol.DisplayMode{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60}, BitrateTargetKbps: 20000, BitrateMaximumKbps: 40000, Adjustment: protocol.SessionAdjustment{DisplayReason: "none", BitrateReason: "none"}, MediaTimestampBasis: "gateway-send-wall-clock-ms"}
|
||||
}
|
||||
|
||||
func TestDeviceRegistrationProofTranscriptIsCanonicalAndStrict(t *testing.T) {
|
||||
serverID, _ := hex.DecodeString("00112233445566778899aabbccddeeff")
|
||||
principalID, _ := hex.DecodeString("102132435465768798a9bacbdcedfe0f")
|
||||
@@ -51,7 +69,7 @@ func TestDeviceRegistrationProofTranscriptIsCanonicalAndStrict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManifestRejectsForbiddenAndUnknownFields(t *testing.T) {
|
||||
valid := `{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":{"id":"gateway-1","addresses":["gateway.control.test:443"],"public_identity":"gateway.control.test"},"tunnel":{"versions":["verse-gateway-v1/1"],"features":["control.v1"]},"profile":{"id":"standard","bounds":{"minimum_kbps":1,"target_kbps":2,"maximum_kbps":3}},"grant":{"opaque_value":"opaque-one-time-grant-value-with-at-least-43-bytes","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"correlation-1"}`
|
||||
valid := `{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":{"id":"gateway-1","addresses":["gateway.control.test:443"],"public_identity":"gateway.control.test"},"tunnel":{"versions":["verse-gateway-v1/1"],"features":["control.v1"]},"profile":{"id":"standard","bounds":{"minimum_kbps":1,"target_kbps":2,"maximum_kbps":3}},"grant":{"opaque_value":"opaque-one-time-grant-value-with-at-least-43-bytes","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"correlation-1","selected_descriptor":{"video_profile":{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60},"bitrate_target_kbps":20000,"bitrate_maximum_kbps":40000,"adjustment":{"display_reason":"none","bitrate_reason":"none"},"media_timestamp_basis":"gateway-send-wall-clock-ms"}}`
|
||||
manifest, err := protocol.DecodeConnectionManifest([]byte(valid))
|
||||
if err != nil || manifest.Gateway.ID != "gateway-1" {
|
||||
t.Fatalf("valid manifest = %+v, err = %v", manifest, err)
|
||||
@@ -83,6 +101,7 @@ func TestDisplayModeIsOptionalStrictAndBounded(t *testing.T) {
|
||||
|
||||
request := protocol.SessionRequest{
|
||||
ClientDeviceID: "device-1", DeviceKeyID: "key-1", PoolID: "pool-1", IdempotencyKey: "request-1",
|
||||
VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, BitratePreference: protocol.BitratePreference{Mode: "auto"},
|
||||
}
|
||||
legacy, err := protocol.EncodeSessionRequest(request)
|
||||
if err != nil {
|
||||
@@ -140,7 +159,7 @@ func TestSessionRequestRejectsClientPolicySnapshot(t *testing.T) {
|
||||
if protocol.CurrentWireVersion != "2" || protocol.NMinus1WireVersion != "1" || protocol.NMinus2WireVersion != "0" {
|
||||
t.Fatalf("wire compatibility = current %q, n-1 %q, n-2 %q", protocol.CurrentWireVersion, protocol.NMinus1WireVersion, protocol.NMinus2WireVersion)
|
||||
}
|
||||
request := `{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}`
|
||||
request := `{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"bitrate_preference":{"mode":"auto"}}`
|
||||
if _, err := protocol.DecodeSessionRequest([]byte(request)); err != nil {
|
||||
t.Fatalf("policy-free SessionRequest rejected: %v", err)
|
||||
}
|
||||
@@ -225,7 +244,7 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
|
||||
}
|
||||
|
||||
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"]}}`
|
||||
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","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}}`
|
||||
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil {
|
||||
t.Fatalf("valid gateway registration rejected: %v", err)
|
||||
}
|
||||
@@ -244,7 +263,7 @@ func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
|
||||
}
|
||||
|
||||
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"]}}`
|
||||
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","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}}`
|
||||
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err == nil {
|
||||
t.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds")
|
||||
}
|
||||
@@ -267,12 +286,12 @@ func TestGatewayHeartbeatCarriesBoundedObservedTelemetry(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
|
||||
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"}}
|
||||
first := rc6Capability("h264")
|
||||
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || !reflect.DeepEqual(got, first) {
|
||||
t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err)
|
||||
}
|
||||
second := first
|
||||
second.ClientDecode = []string{"hevc-opus"}
|
||||
second.VideoProfiles = []protocol.VideoProfile{rc6Video("hevc")}
|
||||
if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil {
|
||||
t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile")
|
||||
}
|
||||
@@ -280,31 +299,31 @@ func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
|
||||
|
||||
func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
|
||||
gateway := protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||
SourceRateControl: "server", ClientDecode: []string{"hevc-opus", "h264-opus"},
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
|
||||
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("hevc"), rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
|
||||
}
|
||||
h264Client := gateway
|
||||
h264Client.ClientDecode = []string{"h264-opus"}
|
||||
h264Client.VideoProfiles = []protocol.VideoProfile{rc6Video("h264")}
|
||||
selected, err := protocol.IntersectCapabilityProfiles(gateway, h264Client)
|
||||
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"h264-opus"}) {
|
||||
if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{rc6Video("h264")}) {
|
||||
t.Fatalf("H.264 profile intersection = %+v, %v", selected, err)
|
||||
}
|
||||
hevcClient := gateway
|
||||
hevcClient.ClientDecode = []string{"hevc-opus"}
|
||||
hevcClient.VideoProfiles = []protocol.VideoProfile{rc6Video("hevc")}
|
||||
selected, err = protocol.IntersectCapabilityProfiles(gateway, hevcClient)
|
||||
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"hevc-opus"}) {
|
||||
if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{rc6Video("hevc")}) {
|
||||
t.Fatalf("HEVC profile intersection = %+v, %v", selected, err)
|
||||
}
|
||||
noOverlap := gateway
|
||||
noOverlap.ClientDecode = []string{"h264-opus"}
|
||||
noOverlap.VideoProfiles = []protocol.VideoProfile{rc6Video("h264")}
|
||||
if _, err := protocol.IntersectCapabilityProfiles(noOverlap, hevcClient); err == nil {
|
||||
t.Fatal("intersection accepted registered profiles without overlap")
|
||||
}
|
||||
for _, invalid := range [][]string{{"h264-hevc-opus"}, {"h264-opus", "h264-opus"}} {
|
||||
for _, invalid := range [][]protocol.VideoProfile{{{Codec: "vp9", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}}, {rc6Video("h264"), rc6Video("h264")}} {
|
||||
profile := gateway
|
||||
profile.ClientDecode = invalid
|
||||
profile.VideoProfiles = invalid
|
||||
if err := profile.Validate(); err == nil {
|
||||
t.Fatalf("CapabilityProfile accepted invalid registered profile set %q", invalid)
|
||||
t.Fatalf("CapabilityProfile accepted invalid registered profile set %+v", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,7 +331,7 @@ 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"},
|
||||
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
|
||||
}
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("registered datagram-v2 rejected: %v", err)
|
||||
@@ -329,8 +348,8 @@ func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
||||
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: []string{"h264-opus"},
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
|
||||
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
|
||||
},
|
||||
}
|
||||
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
|
||||
@@ -343,18 +362,18 @@ func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing
|
||||
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: []string{"h264-opus"},
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
|
||||
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
|
||||
},
|
||||
}
|
||||
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:server1:19: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:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5"
|
||||
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"}`
|
||||
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","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]},"provider_profile":"apollo","provider_identity":"provider-1","selected_descriptor":{"video_profile":{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":1920,"resolution_height":1080,"fps":60},"bitrate_target_kbps":20000,"bitrate_maximum_kbps":40000,"adjustment":{"display_reason":"none","bitrate_reason":"none"},"media_timestamp_basis":"gateway-send-wall-clock-ms"}}`
|
||||
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
|
||||
t.Fatalf("valid session authority rejected: %v", err)
|
||||
}
|
||||
@@ -367,9 +386,10 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
|
||||
authority := protocol.ClientSessionAuthority{
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||
ReconnectSequence: 2, ExpiresAt: "2099-01-01T00:00:00Z", Capabilities: protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded",
|
||||
SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{rc6Video("h264")}, AudioProfiles: []protocol.AudioProfile{rc6Audio()},
|
||||
},
|
||||
SelectedDescriptor: rc6Descriptor("h264"),
|
||||
}
|
||||
encoded, err := protocol.EncodeClientSessionAuthority(authority)
|
||||
if err != nil {
|
||||
@@ -381,7 +401,7 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
|
||||
}
|
||||
wantFields := map[string]bool{
|
||||
"version": true, "session_id": true, "gateway_id": true, "audience": true,
|
||||
"reconnect_sequence": true, "expires_at": true, "capabilities": true,
|
||||
"reconnect_sequence": true, "expires_at": true, "capabilities": true, "selected_descriptor": true,
|
||||
}
|
||||
if len(fields) != len(wantFields) {
|
||||
t.Fatalf("encoded client authority fields = %v; want exactly %v", fields, wantFields)
|
||||
@@ -399,7 +419,7 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
|
||||
t.Fatalf("DecodeClientSessionAuthority() = %+v, %v; want %+v", decoded, err, authority)
|
||||
}
|
||||
|
||||
for _, field := range []string{"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"} {
|
||||
for _, field := range []string{"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"} {
|
||||
missing := make(map[string]json.RawMessage, len(fields)-1)
|
||||
for key, value := range fields {
|
||||
if key != field {
|
||||
@@ -439,7 +459,7 @@ func TestClientSessionAuthorityIsStrictAndProviderFree(t *testing.T) {
|
||||
}
|
||||
|
||||
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","stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true},"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","clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30},"provider_application_termination_allowed":false}`
|
||||
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","stream_policy":{"video_profile":{"codec":"hevc","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120},"bitrate_target_kbps":40000,"bitrate_maximum_kbps":50000},"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","clipboard_policy":{"client_to_provider_enabled":false,"provider_to_client_enabled":false,"max_text_bytes":65536,"max_updates_per_minute":30},"provider_application_termination_allowed":false}`
|
||||
if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil {
|
||||
t.Fatalf("valid provider work rejected: %v", err)
|
||||
}
|
||||
@@ -450,10 +470,10 @@ func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
|
||||
t.Fatal("provider work accepted missing clipboard policy")
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
strings.Replace(valid, `,"stream_policy":{"resolution_width":2560,"resolution_height":1440,"fps":120,"codec":"HEVC","bitrate_kbps":40000,"audio_enabled":true}`, "", 1),
|
||||
strings.Replace(valid, `,"stream_policy":{"video_profile":{"codec":"hevc","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"},"audio_profile":{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5},"display_mode":{"resolution_width":2560,"resolution_height":1440,"fps":120},"bitrate_target_kbps":40000,"bitrate_maximum_kbps":50000}`, "", 1),
|
||||
strings.Replace(valid, `"fps":120`, `"fps":241`, 1),
|
||||
strings.Replace(valid, `"codec":"HEVC"`, `"codec":"VP9"`, 1),
|
||||
strings.Replace(valid, `"audio_enabled":true`, `"audio_enabled":true,"unknown":false`, 1),
|
||||
strings.Replace(valid, `"codec":"hevc"`, `"codec":"vp9"`, 1),
|
||||
strings.Replace(valid, `"packet_duration_ms":5`, `"packet_duration_ms":5,"unknown":false`, 1),
|
||||
} {
|
||||
if _, err := protocol.DecodeProviderSessionWork([]byte(invalid)); err == nil {
|
||||
t.Fatalf("provider work accepted invalid stream policy: %s", invalid)
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package protocol_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
func TestRC6GeneratedContractsHaveFrozenExactShapes(t *testing.T) {
|
||||
tests := []struct {
|
||||
value any
|
||||
fields []string
|
||||
}{
|
||||
{protocol.VideoProfile{}, []string{"Codec", "BitDepth", "ChromaSubsampling", "ColorSpace", "TransferFunction"}},
|
||||
{protocol.AudioProfile{}, []string{"Codec", "SampleRateHz", "Channels", "ChannelLayout", "PacketDurationMs"}},
|
||||
{protocol.BitratePreference{}, []string{"Mode", "TargetKbps"}},
|
||||
{protocol.SessionQualityLimits{}, []string{"PolicyVersionID", "PolicyDisplayLimit", "SelectableDisplayMaximum", "DisplayLimitOverride", "BitrateMinimumKbps", "BitrateTargetKbps", "BitrateMaximumKbps"}},
|
||||
{protocol.SessionAdjustment{}, []string{"DisplayReason", "BitrateReason"}},
|
||||
{protocol.SelectedSessionDescriptor{}, []string{"VideoProfile", "AudioProfile", "DisplayMode", "BitrateTargetKbps", "BitrateMaximumKbps", "Adjustment", "MediaTimestampBasis"}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
typeOf := reflect.TypeOf(test.value)
|
||||
if typeOf.NumField() != len(test.fields) {
|
||||
t.Fatalf("%s field count = %d, want %d", typeOf.Name(), typeOf.NumField(), len(test.fields))
|
||||
}
|
||||
for index, field := range test.fields {
|
||||
if typeOf.Field(index).Name != field {
|
||||
t.Fatalf("%s field %d = %s, want %s", typeOf.Name(), index, typeOf.Field(index).Name, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRC6ProfileValidationAndOrderedIntersection(t *testing.T) {
|
||||
h264 := protocol.VideoProfile{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}
|
||||
hevc := h264
|
||||
hevc.Codec = "hevc"
|
||||
audio := protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5}
|
||||
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded", SourceRateControl: "server", VideoProfiles: []protocol.VideoProfile{h264, hevc}, AudioProfiles: []protocol.AudioProfile{audio}}
|
||||
second := first
|
||||
second.VideoProfiles = []protocol.VideoProfile{hevc, h264}
|
||||
selected, err := protocol.IntersectCapabilityProfiles(first, second)
|
||||
if err != nil || !reflect.DeepEqual(selected.VideoProfiles, []protocol.VideoProfile{h264, hevc}) {
|
||||
t.Fatalf("ordered intersection = %+v, %v", selected.VideoProfiles, err)
|
||||
}
|
||||
duplicate := first
|
||||
duplicate.VideoProfiles = []protocol.VideoProfile{h264, h264}
|
||||
if duplicate.Validate() == nil {
|
||||
t.Fatal("CapabilityProfile accepted duplicate video profiles")
|
||||
}
|
||||
invalid := h264
|
||||
invalid.BitDepth = 10
|
||||
if invalid.Validate() == nil {
|
||||
t.Fatal("VideoProfile accepted a non-RC6 bit depth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRC6StrictDecodeRejectsLegacyAndMalformedBodies(t *testing.T) {
|
||||
for name, test := range map[string]struct {
|
||||
payload []byte
|
||||
decode func([]byte) error
|
||||
}{
|
||||
"opaque capability": {[]byte(`{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]}`), func(raw []byte) error { _, err := protocol.DecodeCapabilityProfile(raw); return err }},
|
||||
"old session request": {[]byte(`{"client_device_id":"device-1","device_key_id":"key-1","pool_id":"pool-1","idempotency_key":"request-1"}`), func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err }},
|
||||
"old provider policy": {[]byte(`{"resolution_width":1920,"resolution_height":1080,"fps":60,"codec":"H264","bitrate_kbps":20000,"audio_enabled":true}`), func(raw []byte) error { _, err := protocol.DecodeProviderStreamPolicy(raw); return err }},
|
||||
"unknown field": {[]byte(`{"mode":"explicit","target_kbps":1000,"unknown":true}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
|
||||
"duplicate key": {[]byte(`{"mode":"explicit","mode":"auto","target_kbps":1000}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
|
||||
"trailing JSON": {[]byte(`{"mode":"explicit","target_kbps":1000} {}`), func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err }},
|
||||
} {
|
||||
if err := test.decode(test.payload); err == nil {
|
||||
t.Fatalf("RC6 accepted %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRC6BitratePreferenceTaggedBounds(t *testing.T) {
|
||||
for _, valid := range []protocol.BitratePreference{{Mode: "auto"}, {Mode: "explicit", TargetKbps: int64Pointer(100)}, {Mode: "explicit", TargetKbps: int64Pointer(1_000_000)}} {
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("valid preference %+v rejected: %v", valid, err)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []protocol.BitratePreference{{Mode: "auto", TargetKbps: int64Pointer(100)}, {Mode: "explicit"}, {Mode: "explicit", TargetKbps: int64Pointer(99)}, {Mode: "explicit", TargetKbps: int64Pointer(1_000_001)}} {
|
||||
if invalid.Validate() == nil {
|
||||
t.Fatalf("invalid preference %+v accepted", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRC6GatewayQualityDiscoveryAndLeaseFencing(t *testing.T) {
|
||||
operationID := "12345678-1234-1234-1234-123456789abc"
|
||||
revision := int64(7)
|
||||
leaseGeneration := int64(3)
|
||||
currentRevision := int64(6)
|
||||
|
||||
poll := protocol.GatewayQualityWorkRequest{Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2, Acquisition: "poll"}
|
||||
if err := poll.Validate(); err != nil {
|
||||
t.Fatalf("session-bound lost-prompt poll rejected: %v", err)
|
||||
}
|
||||
prompt := poll
|
||||
prompt.Acquisition = "prompt"
|
||||
prompt.OperationID = operationID
|
||||
prompt.Revision = &revision
|
||||
if err := prompt.Validate(); err != nil {
|
||||
t.Fatalf("prompt-bound acquisition rejected: %v", err)
|
||||
}
|
||||
observation := prompt
|
||||
observation.Acquisition = "observation"
|
||||
observation.LeaseGeneration = &leaseGeneration
|
||||
observation.CurrentAppliedRevision = ¤tRevision
|
||||
if err := observation.Validate(); err != nil {
|
||||
t.Fatalf("lease-bound applied-revision observation rejected: %v", err)
|
||||
}
|
||||
|
||||
for name, invalid := range map[string]protocol.GatewayQualityWorkRequest{
|
||||
"poll with unknown coordinates": prompt,
|
||||
"prompt missing revision": {Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Acquisition: "prompt", OperationID: operationID},
|
||||
"observation missing lease": {Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Acquisition: "observation", OperationID: operationID, Revision: &revision, CurrentAppliedRevision: ¤tRevision},
|
||||
} {
|
||||
if name == "poll with unknown coordinates" {
|
||||
invalid.Acquisition = "poll"
|
||||
}
|
||||
if invalid.Validate() == nil {
|
||||
t.Fatalf("accepted invalid %s: %+v", name, invalid)
|
||||
}
|
||||
}
|
||||
|
||||
stopPoll := protocol.GatewayStopWorkRequest{Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2, Acquisition: "poll"}
|
||||
if err := stopPoll.Validate(); err != nil {
|
||||
t.Fatalf("session-bound Stop poll rejected: %v", err)
|
||||
}
|
||||
stopPrompt := stopPoll
|
||||
stopPrompt.Acquisition = "prompt"
|
||||
stopPrompt.OperationID = operationID
|
||||
if err := stopPrompt.Validate(); err != nil {
|
||||
t.Fatalf("prompt-bound Stop acquisition rejected: %v", err)
|
||||
}
|
||||
stopPoll.OperationID = operationID
|
||||
if stopPoll.Validate() == nil {
|
||||
t.Fatal("Stop poll accepted unknown operation coordinates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRC6GatewayQualityAckHasUnambiguousObservation(t *testing.T) {
|
||||
revision := int64(7)
|
||||
prior := int64(6)
|
||||
base := protocol.GatewayQualityAck{
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", ReconnectSequence: 2,
|
||||
OperationID: "12345678-1234-1234-1234-123456789abc", Revision: revision, LeaseGeneration: 3,
|
||||
}
|
||||
for _, valid := range []protocol.GatewayQualityAck{
|
||||
func() protocol.GatewayQualityAck {
|
||||
value := base
|
||||
value.Outcome = "applied"
|
||||
value.CurrentAppliedRevision = &revision
|
||||
return value
|
||||
}(),
|
||||
func() protocol.GatewayQualityAck {
|
||||
value := base
|
||||
value.Outcome = "proven_prior"
|
||||
value.CurrentAppliedRevision = &prior
|
||||
return value
|
||||
}(),
|
||||
func() protocol.GatewayQualityAck { value := base; value.Outcome = "unknown"; return value }(),
|
||||
} {
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("valid quality acknowledgement rejected: %+v: %v", valid, err)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []protocol.GatewayQualityAck{
|
||||
func() protocol.GatewayQualityAck {
|
||||
value := base
|
||||
value.Outcome = "applied"
|
||||
value.CurrentAppliedRevision = &prior
|
||||
return value
|
||||
}(),
|
||||
func() protocol.GatewayQualityAck {
|
||||
value := base
|
||||
value.Outcome = "proven_prior"
|
||||
value.CurrentAppliedRevision = &revision
|
||||
return value
|
||||
}(),
|
||||
func() protocol.GatewayQualityAck {
|
||||
value := base
|
||||
value.Outcome = "unknown"
|
||||
value.CurrentAppliedRevision = &prior
|
||||
return value
|
||||
}(),
|
||||
func() protocol.GatewayQualityAck {
|
||||
value := base
|
||||
value.Outcome = "unknown"
|
||||
value.LeaseGeneration = 0
|
||||
return value
|
||||
}(),
|
||||
} {
|
||||
if invalid.Validate() == nil {
|
||||
t.Fatalf("contradictory or unfenced quality acknowledgement accepted: %+v", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRC6JSONFixturesAndCanonicalOperationFields(t *testing.T) {
|
||||
validFixtures := map[string]func([]byte) error{
|
||||
"../../fixtures/valid/session-request.json": func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err },
|
||||
"../../fixtures/valid/selected-session-descriptor.json": func(raw []byte) error { _, err := protocol.DecodeSelectedSessionDescriptor(raw); return err },
|
||||
"../../fixtures/valid/session-quality-limits.json": func(raw []byte) error { _, err := protocol.DecodeSessionQualityLimits(raw); return err },
|
||||
"../../fixtures/valid/gateway-quality-poll.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityWorkRequest(raw); return err },
|
||||
"../../fixtures/valid/gateway-quality-ack-applied.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
||||
}
|
||||
for path, decode := range validFixtures {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read valid fixture %s: %v", path, err)
|
||||
}
|
||||
if err := decode(raw); err != nil {
|
||||
t.Fatalf("valid fixture %s rejected: %v", path, err)
|
||||
}
|
||||
}
|
||||
invalidFixtures := map[string]func([]byte) error{
|
||||
"../../fixtures/invalid/capability-rc5-opaque.json": func(raw []byte) error { _, err := protocol.DecodeCapabilityProfile(raw); return err },
|
||||
"../../fixtures/invalid/session-request-rc5.json": func(raw []byte) error { _, err := protocol.DecodeSessionRequest(raw); return err },
|
||||
"../../fixtures/invalid/provider-stream-policy-rc5.json": func(raw []byte) error { _, err := protocol.DecodeProviderStreamPolicy(raw); return err },
|
||||
"../../fixtures/invalid/video-profile-unknown.json": func(raw []byte) error { _, err := protocol.DecodeVideoProfile(raw); return err },
|
||||
"../../fixtures/invalid/bitrate-preference-auto-target.json": func(raw []byte) error { _, err := protocol.DecodeBitratePreference(raw); return err },
|
||||
"../../fixtures/invalid/selected-session-descriptor-provider-field.json": func(raw []byte) error { _, err := protocol.DecodeSelectedSessionDescriptor(raw); return err },
|
||||
"../../fixtures/invalid/gateway-quality-poll-coordinates.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityWorkRequest(raw); return err },
|
||||
"../../fixtures/invalid/gateway-quality-ack-contradictory.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
||||
"../../fixtures/invalid/gateway-quality-ack-uppercase-uuid.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
||||
"../../fixtures/invalid/gateway-quality-ack-zero-uuid.json": func(raw []byte) error { _, err := protocol.DecodeGatewayQualityAck(raw); return err },
|
||||
"../../fixtures/invalid/quality-operation-offset-time.json": func(raw []byte) error { _, err := protocol.DecodeQualityChangeOperation(raw); return err },
|
||||
"../../fixtures/invalid/quality-operation-noncanonical-fraction.json": func(raw []byte) error { _, err := protocol.DecodeQualityChangeOperation(raw); return err },
|
||||
}
|
||||
for path, decode := range invalidFixtures {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decode(raw) == nil {
|
||||
t.Fatalf("invalid fixture %s accepted", path)
|
||||
}
|
||||
}
|
||||
validOperation := `{"operation_id":"12345678-1234-1234-1234-123456789abc","session_id":"session-1","revision":1,"state":"pending","requested_bitrate_preference":{"mode":"auto"},"effective_bitrate_kbps":20000,"governing_policy_version":"policy-1","session_version":1,"created_at":"2099-01-01T00:00:00Z","deadline_at":"2099-01-01T00:00:30Z","updated_at":"2099-01-01T00:00:00Z"}`
|
||||
if _, err := protocol.DecodeQualityChangeOperation([]byte(validOperation)); err != nil {
|
||||
t.Fatalf("valid quality operation rejected: %v", err)
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
strings.Replace(validOperation, "123456789abc", "123456789ABC", 1),
|
||||
strings.Replace(validOperation, "12345678-1234-1234-1234-123456789abc", "00000000-0000-0000-0000-000000000000", 1),
|
||||
strings.Replace(validOperation, "2099-01-01T00:00:00Z", "2099-01-01T00:00:00+00:00", 1),
|
||||
strings.Replace(validOperation, `,"revision":1`, "", 1),
|
||||
} {
|
||||
if _, err := protocol.DecodeQualityChangeOperation([]byte(invalid)); err == nil {
|
||||
t.Fatalf("invalid operation accepted: %s", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func int64Pointer(value int64) *int64 { return &value }
|
||||
+11
-1
@@ -32,7 +32,17 @@ def main() -> int:
|
||||
expected = manifest.get("corpus_sha256")
|
||||
if not expected or actual != expected:
|
||||
raise ValueError(f"fixture corpus hash mismatch: {actual}")
|
||||
print(f"Fixture corpus SHA256 {actual}")
|
||||
json_paths = sorted(
|
||||
path.relative_to(ROOT).as_posix()
|
||||
for directory in (ROOT / "fixtures/valid", ROOT / "fixtures/invalid")
|
||||
for path in directory.glob("*.json")
|
||||
)
|
||||
if json_paths != manifest.get("json_files"):
|
||||
raise ValueError("JSON fixture manifest file list is stale")
|
||||
json_actual = digest(json_paths)
|
||||
if json_actual != manifest.get("json_corpus_sha256"):
|
||||
raise ValueError(f"JSON fixture corpus hash mismatch: {json_actual}")
|
||||
print(f"Fixture corpus SHA256 {actual}; JSON SHA256 {json_actual}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+200
-33
@@ -139,11 +139,15 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
)
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f"\tif v.{field} != \"\" {{ if _, err := base64.RawURLEncoding.Strict().DecodeString(v.{field}); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_format\"}}) }} }}")
|
||||
if prop.get("format") == "uuid":
|
||||
lines.append(f"\tif v.{field} != \"\" && !validCanonicalUUID(v.{field}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_uuid\"}}) }}")
|
||||
if prop.get("type") == "integer":
|
||||
value = f"*v.{field}" if prop.get("x-optional-pointer") else f"v.{field}"
|
||||
guard = f"v.{field} != nil && " if prop.get("x-optional-pointer") else ""
|
||||
if "minimum" in prop:
|
||||
lines.append(f"\tif v.{field} != 0 && v.{field} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}")
|
||||
lines.append(f"\tif {guard}{value} != 0 && {value} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}")
|
||||
if "maximum" in prop:
|
||||
lines.append(f"\tif v.{field} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}")
|
||||
lines.append(f"\tif {guard}{value} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}")
|
||||
if prop.get("type") == "array":
|
||||
if "minItems" in prop:
|
||||
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
|
||||
@@ -159,8 +163,8 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in items:
|
||||
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
|
||||
if prop.get("uniqueItems"):
|
||||
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if reflect.DeepEqual(item, v.{field}[prior]) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
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\"}}) }} }}")
|
||||
@@ -173,6 +177,22 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f"\t{validation}")
|
||||
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 == "SessionQualityLimits":
|
||||
lines.append("\tif v.BitrateMinimumKbps > v.BitrateTargetKbps || v.BitrateTargetKbps > v.BitrateMaximumKbps { violations = append(violations, FieldViolation{Field: \"bitrate_bounds\", Code: \"invalid_order\"}) }")
|
||||
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
|
||||
lines.append("\tif v.BitrateTargetKbps > v.BitrateMaximumKbps { violations = append(violations, FieldViolation{Field: \"bitrate_bounds\", Code: \"invalid_order\"}) }")
|
||||
if name == "BitratePreference":
|
||||
lines.append("\tif v.Mode == \"auto\" && v.TargetKbps != nil || v.Mode == \"explicit\" && v.TargetKbps == nil { violations = append(violations, FieldViolation{Field: \"target_kbps\", Code: \"invalid_tagged_value\"}) }")
|
||||
if name == "GatewayQualityWorkRequest":
|
||||
lines.append("\tif v.Acquisition == \"poll\" && (v.OperationID != \"\" || v.Revision != nil || v.LeaseGeneration != nil || v.CurrentAppliedRevision != nil) { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
|
||||
lines.append("\tif v.Acquisition == \"prompt\" && (v.OperationID == \"\" || v.Revision == nil || v.LeaseGeneration != nil || v.CurrentAppliedRevision != nil) { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
|
||||
lines.append("\tif v.Acquisition == \"observation\" && (v.OperationID == \"\" || v.Revision == nil || v.LeaseGeneration == nil || v.CurrentAppliedRevision == nil) { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
|
||||
if name == "GatewayStopWorkRequest":
|
||||
lines.append("\tif v.Acquisition == \"poll\" && v.OperationID != \"\" || v.Acquisition == \"prompt\" && v.OperationID == \"\" { violations = append(violations, FieldViolation{Field: \"acquisition\", Code: \"invalid_tagged_value\"}) }")
|
||||
if name == "GatewayQualityAck":
|
||||
lines.append("\tif v.Outcome == \"applied\" && (v.CurrentAppliedRevision == nil || *v.CurrentAppliedRevision != v.Revision) { violations = append(violations, FieldViolation{Field: \"current_applied_revision\", Code: \"invalid_tagged_value\"}) }")
|
||||
lines.append("\tif v.Outcome == \"proven_prior\" && (v.CurrentAppliedRevision == nil || *v.CurrentAppliedRevision >= v.Revision) { violations = append(violations, FieldViolation{Field: \"current_applied_revision\", Code: \"invalid_tagged_value\"}) }")
|
||||
lines.append("\tif v.Outcome == \"unknown\" && v.CurrentAppliedRevision != nil { violations = append(violations, FieldViolation{Field: \"current_applied_revision\", Code: \"invalid_tagged_value\"}) }")
|
||||
if name == "GatewayRegistration":
|
||||
lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }")
|
||||
if name == "ChannelFrame":
|
||||
@@ -214,6 +234,30 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
"",
|
||||
"func (e ValidationError) Error() string { return \"protocol validation failed\" }",
|
||||
"",
|
||||
"func validCanonicalUUID(value string) bool {",
|
||||
"\tif len(value) != 36 || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' { return false }",
|
||||
"\tfor index, char := range []byte(value) { if index == 8 || index == 13 || index == 18 || index == 23 { continue }; if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { return false } }",
|
||||
"\treturn value != \"00000000-0000-0000-0000-000000000000\"",
|
||||
"}",
|
||||
"",
|
||||
"func rejectDuplicateJSONKeys(data []byte) error {",
|
||||
"\tdecoder := json.NewDecoder(bytes.NewReader(data))",
|
||||
"\tvar scan func(json.Token) error",
|
||||
"\tscan = func(token json.Token) error {",
|
||||
"\t\tdelim, ok := token.(json.Delim); if !ok { return nil }",
|
||||
"\t\tswitch delim {",
|
||||
"\t\tcase '{':",
|
||||
"\t\t\tseen := map[string]struct{}{}",
|
||||
"\t\t\tfor decoder.More() { keyToken, err := decoder.Token(); if err != nil { return err }; key, ok := keyToken.(string); if !ok { return errors.New(\"invalid JSON object key\") }; if _, exists := seen[key]; exists { return errors.New(\"duplicate JSON object key\") }; seen[key] = struct{}{}; value, err := decoder.Token(); if err != nil { return err }; if err := scan(value); err != nil { return err } }",
|
||||
"\t\t\t_, err := decoder.Token(); return err",
|
||||
"\t\tcase '[':",
|
||||
"\t\t\tfor decoder.More() { value, err := decoder.Token(); if err != nil { return err }; if err := scan(value); err != nil { return err } }; _, err := decoder.Token(); return err",
|
||||
"\t\t}",
|
||||
"\t\treturn nil",
|
||||
"\t}",
|
||||
"\ttoken, err := decoder.Token(); if err != nil { return err }; return scan(token)",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
if name == "FieldViolation":
|
||||
@@ -224,7 +268,9 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
for prop_name, prop in definition.get("properties", {}).items():
|
||||
tag = prop_name + (",omitempty" if prop_name not in required else "")
|
||||
typ = prop_type(prop, "go")
|
||||
if prop_name not in required and ref_name(prop):
|
||||
if prop.get("x-optional-pointer"):
|
||||
typ = "*" + typ
|
||||
elif prop_name not in required and ref_name(prop):
|
||||
typ = "*" + typ
|
||||
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
|
||||
out.extend(["}", ""])
|
||||
@@ -239,6 +285,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
out.append(f"func Decode{name}(data []byte) ({name}, error) {{")
|
||||
out.append(f"\tvar value {name}")
|
||||
out.append("\tif len(data) > 1024*1024 { return value, errors.New(\"protocol payload exceeds limit\") }")
|
||||
out.append("\tif err := rejectDuplicateJSONKeys(data); err != nil { return value, err }")
|
||||
out.append("\tvar fields map[string]json.RawMessage")
|
||||
out.append("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }")
|
||||
required_fields = sorted(defs[name].get("required", []))
|
||||
@@ -295,23 +342,29 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tselected := profiles[0]",
|
||||
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tcommon := append([]string(nil), selected.ClientDecode...)",
|
||||
"\tcommonVideo := append([]VideoProfile(nil), selected.VideoProfiles...)",
|
||||
"\tcommonAudio := append([]AudioProfile(nil), selected.AudioProfiles...)",
|
||||
"\tfor _, profile := range profiles[1:] {",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tnext := common[:0]",
|
||||
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }",
|
||||
"\t\tcommon = next",
|
||||
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tnextVideo := commonVideo[:0]",
|
||||
"\t\tfor _, candidate := range commonVideo { for _, offered := range profile.VideoProfiles { if candidate == offered { nextVideo = append(nextVideo, candidate); break } } }",
|
||||
"\t\tcommonVideo = nextVideo",
|
||||
"\t\tnextAudio := commonAudio[:0]",
|
||||
"\t\tfor _, candidate := range commonAudio { for _, offered := range profile.AudioProfiles { if candidate == offered { nextAudio = append(nextAudio, candidate); break } } }",
|
||||
"\t\tcommonAudio = nextAudio",
|
||||
"\t\tif len(commonVideo) == 0 || len(commonAudio) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t}",
|
||||
"\tselected.ClientDecode = common",
|
||||
"\tselected.VideoProfiles = commonVideo",
|
||||
"\tselected.AudioProfiles = commonAudio",
|
||||
"\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, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}",
|
||||
"\tfields = append(fields, v.Capabilities.ClientDecode...)",
|
||||
"\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.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.VideoProfiles)), fmt.Sprintf(\"%d\", len(v.Capabilities.AudioProfiles))}",
|
||||
"\tfor _, profile := range v.Capabilities.VideoProfiles { fields = append(fields, profile.Codec, fmt.Sprintf(\"%d\", profile.BitDepth), profile.ChromaSubsampling, profile.ColorSpace, profile.TransferFunction) }",
|
||||
"\tfor _, profile := range v.Capabilities.AudioProfiles { fields = append(fields, profile.Codec, fmt.Sprintf(\"%d\", profile.SampleRateHz), fmt.Sprintf(\"%d\", profile.Channels), profile.ChannelLayout, fmt.Sprintf(\"%d\", profile.PacketDurationMs)) }",
|
||||
"\tvar transcript strings.Builder",
|
||||
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
||||
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
||||
@@ -386,11 +439,14 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if !valid_rfc3339_utc({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_time\")); }}")
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}")
|
||||
if prop.get("format") == "uuid":
|
||||
lines.append(f" {prefix}if !valid_canonical_uuid({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_uuid\")); }}")
|
||||
if prop.get("type") == "integer":
|
||||
numeric = f"*{value}" if prop_name not in required else value
|
||||
if "minimum" in prop:
|
||||
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}")
|
||||
lines.append(f" {prefix}if {numeric} < {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\")); }}")
|
||||
lines.append(f" {prefix}if {numeric} > {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\")); }}")
|
||||
@@ -406,7 +462,7 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in items:
|
||||
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
if prop.get("uniqueItems"):
|
||||
lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
@@ -419,6 +475,22 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
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 == "SessionQualityLimits":
|
||||
lines.append(" if self.bitrateMinimumKbps > self.bitrateTargetKbps || self.bitrateTargetKbps > self.bitrateMaximumKbps { return Err(ValidationError::new(\"bitrate_bounds\", \"invalid_order\")); }")
|
||||
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
|
||||
lines.append(" if self.bitrateTargetKbps > self.bitrateMaximumKbps { return Err(ValidationError::new(\"bitrate_bounds\", \"invalid_order\")); }")
|
||||
if name == "BitratePreference":
|
||||
lines.append(" if self.mode == \"auto\" && self.targetKbps.is_some() || self.mode == \"explicit\" && self.targetKbps.is_none() { return Err(ValidationError::new(\"target_kbps\", \"invalid_tagged_value\")); }")
|
||||
if name == "GatewayQualityWorkRequest":
|
||||
lines.append(" if self.acquisition == \"poll\" && (self.operationId.is_some() || self.revision.is_some() || self.leaseGeneration.is_some() || self.currentAppliedRevision.is_some()) { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
|
||||
lines.append(" if self.acquisition == \"prompt\" && (self.operationId.is_none() || self.revision.is_none() || self.leaseGeneration.is_some() || self.currentAppliedRevision.is_some()) { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
|
||||
lines.append(" if self.acquisition == \"observation\" && (self.operationId.is_none() || self.revision.is_none() || self.leaseGeneration.is_none() || self.currentAppliedRevision.is_none()) { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
|
||||
if name == "GatewayStopWorkRequest":
|
||||
lines.append(" if self.acquisition == \"poll\" && self.operationId.is_some() || self.acquisition == \"prompt\" && self.operationId.is_none() { return Err(ValidationError::new(\"acquisition\", \"invalid_tagged_value\")); }")
|
||||
if name == "GatewayQualityAck":
|
||||
lines.append(" if self.outcome == \"applied\" && self.currentAppliedRevision != Some(self.revision) { return Err(ValidationError::new(\"current_applied_revision\", \"invalid_tagged_value\")); }")
|
||||
lines.append(" if self.outcome == \"proven_prior\" && self.currentAppliedRevision.map_or(true, |current| current >= self.revision) { return Err(ValidationError::new(\"current_applied_revision\", \"invalid_tagged_value\")); }")
|
||||
lines.append(" if self.outcome == \"unknown\" && self.currentAppliedRevision.is_some() { return Err(ValidationError::new(\"current_applied_revision\", \"invalid_tagged_value\")); }")
|
||||
if name == "GatewayRegistration":
|
||||
lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }")
|
||||
if name == "ChannelFrame":
|
||||
@@ -473,6 +545,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
" let fraction = &bytes[20..bytes.len() - 1];",
|
||||
" bytes[19] == b'.' && !fraction.is_empty() && fraction.len() <= 9 && fraction.iter().all(u8::is_ascii_digit) && *fraction.last().unwrap() != b'0'",
|
||||
"}",
|
||||
"fn valid_canonical_uuid(value: &str) -> bool {",
|
||||
" let bytes = value.as_bytes();",
|
||||
" bytes.len() == 36 && [8, 13, 18, 23].iter().all(|index| bytes[*index] == b'-') && bytes.iter().enumerate().all(|(index, byte)| [8, 13, 18, 23].contains(&index) || byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)) && value != \"00000000-0000-0000-0000-000000000000\"",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
@@ -515,9 +591,12 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
out.extend([
|
||||
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
|
||||
" let reconnect_sequence = self.reconnectSequence.to_string();",
|
||||
" let client_decode_count = self.capabilities.clientDecode.len().to_string();",
|
||||
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];",
|
||||
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));",
|
||||
" let video_count = self.capabilities.videoProfiles.len().to_string();",
|
||||
" let audio_count = self.capabilities.audioProfiles.len().to_string();",
|
||||
" let mut owned = vec![self.sessionId.clone(), self.gatewayId.clone(), self.audience.clone(), self.grant.clone(), reconnect_sequence, self.clientNonce.clone(), self.capabilities.transport.clone(), self.capabilities.framing.clone(), self.capabilities.media.clone(), self.capabilities.sourceRateControl.clone(), video_count, audio_count];",
|
||||
" for profile in &self.capabilities.videoProfiles { owned.extend([profile.codec.clone(), profile.bitDepth.to_string(), profile.chromaSubsampling.clone(), profile.colorSpace.clone(), profile.transferFunction.clone()]); }",
|
||||
" for profile in &self.capabilities.audioProfiles { owned.extend([profile.codec.clone(), profile.sampleRateHz.to_string(), profile.channels.to_string(), profile.channelLayout.clone(), profile.packetDurationMs.to_string()]); }",
|
||||
" let fields: Vec<&str> = owned.iter().map(String::as_str).collect();",
|
||||
" 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()",
|
||||
@@ -545,9 +624,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
" 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.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));",
|
||||
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" selected.videoProfiles.retain(|candidate| profile.videoProfiles.contains(candidate));",
|
||||
" selected.audioProfiles.retain(|candidate| profile.audioProfiles.contains(candidate));",
|
||||
" if selected.videoProfiles.is_empty() || selected.audioProfiles.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" }",
|
||||
" Ok(selected)",
|
||||
"}",
|
||||
@@ -586,6 +666,8 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if !validRFC3339UTC({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}")
|
||||
if prop.get("format") == "uuid":
|
||||
lines.append(f" {prefix}if !validCanonicalUUID({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_uuid\") }}")
|
||||
if prop.get("type") == "integer":
|
||||
if "minimum" in prop:
|
||||
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}")
|
||||
@@ -606,8 +688,8 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in items:
|
||||
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
|
||||
if prop.get("uniqueItems"):
|
||||
lines.append(f" {prefix}for (index, item) in {value}.enumerated() where {value}[..<index].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
|
||||
@@ -619,6 +701,22 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
name = definition["name"]
|
||||
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||
lines.append(" if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: \"bounds\", code: \"invalid_order\") }")
|
||||
if name == "SessionQualityLimits":
|
||||
lines.append(" if bitrateMinimumKbps > bitrateTargetKbps || bitrateTargetKbps > bitrateMaximumKbps { throw ContractValidationError(field: \"bitrate_bounds\", code: \"invalid_order\") }")
|
||||
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
|
||||
lines.append(" if bitrateTargetKbps > bitrateMaximumKbps { throw ContractValidationError(field: \"bitrate_bounds\", code: \"invalid_order\") }")
|
||||
if name == "BitratePreference":
|
||||
lines.append(" if mode == \"auto\" && targetKbps != nil || mode == \"explicit\" && targetKbps == nil { throw ContractValidationError(field: \"target_kbps\", code: \"invalid_tagged_value\") }")
|
||||
if name == "GatewayQualityWorkRequest":
|
||||
lines.append(" if acquisition == \"poll\" && (operationId != nil || revision != nil || leaseGeneration != nil || currentAppliedRevision != nil) { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
|
||||
lines.append(" if acquisition == \"prompt\" && (operationId == nil || revision == nil || leaseGeneration != nil || currentAppliedRevision != nil) { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
|
||||
lines.append(" if acquisition == \"observation\" && (operationId == nil || revision == nil || leaseGeneration == nil || currentAppliedRevision == nil) { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
|
||||
if name == "GatewayStopWorkRequest":
|
||||
lines.append(" if acquisition == \"poll\" && operationId != nil || acquisition == \"prompt\" && operationId == nil { throw ContractValidationError(field: \"acquisition\", code: \"invalid_tagged_value\") }")
|
||||
if name == "GatewayQualityAck":
|
||||
lines.append(" if outcome == \"applied\" && currentAppliedRevision != revision { throw ContractValidationError(field: \"current_applied_revision\", code: \"invalid_tagged_value\") }")
|
||||
lines.append(" if outcome == \"proven_prior\" && (currentAppliedRevision == nil || currentAppliedRevision! >= revision) { throw ContractValidationError(field: \"current_applied_revision\", code: \"invalid_tagged_value\") }")
|
||||
lines.append(" if outcome == \"unknown\" && currentAppliedRevision != nil { throw ContractValidationError(field: \"current_applied_revision\", code: \"invalid_tagged_value\") }")
|
||||
if name == "GatewayRegistration":
|
||||
lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }")
|
||||
if name == "ChannelFrame":
|
||||
@@ -637,6 +735,67 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
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 } }",
|
||||
"private func rejectDuplicateJSONKeys(_ data: Data) throws {",
|
||||
" guard data.count <= 1_048_576 else { throw ContractValidationError(field: \"json\", code: \"payload_too_large\") }",
|
||||
" var index = 0",
|
||||
" func skipWhitespace() { while index < data.count && [9, 10, 13, 32].contains(data[index]) { index += 1 } }",
|
||||
" func parseString() throws -> String {",
|
||||
" guard index < data.count, data[index] == 34 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" let start = index",
|
||||
" index += 1",
|
||||
" while index < data.count {",
|
||||
" if data[index] == 92 { index += 2; continue }",
|
||||
" if data[index] == 34 { index += 1; return try JSONDecoder().decode(String.self, from: data[start..<index]) }",
|
||||
" index += 1",
|
||||
" }",
|
||||
" throw ContractValidationError(field: \"json\", code: \"invalid_json\")",
|
||||
" }",
|
||||
" func parseValue(_ depth: Int) throws {",
|
||||
" guard depth <= 64 else { throw ContractValidationError(field: \"json\", code: \"nesting_too_deep\") }",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" if data[index] == 123 {",
|
||||
" index += 1",
|
||||
" var keys = Set<String>()",
|
||||
" skipWhitespace()",
|
||||
" if index < data.count, data[index] == 125 { index += 1; return }",
|
||||
" while true {",
|
||||
" skipWhitespace()",
|
||||
" let key = try parseString()",
|
||||
" guard keys.insert(key).inserted else { throw ContractValidationError(field: key, code: \"duplicate_field\") }",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count, data[index] == 58 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" index += 1",
|
||||
" try parseValue(depth + 1)",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" if data[index] == 125 { index += 1; return }",
|
||||
" guard data[index] == 44 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" index += 1",
|
||||
" }",
|
||||
" }",
|
||||
" if data[index] == 91 {",
|
||||
" index += 1",
|
||||
" skipWhitespace()",
|
||||
" if index < data.count, data[index] == 93 { index += 1; return }",
|
||||
" while true {",
|
||||
" try parseValue(depth + 1)",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" if data[index] == 93 { index += 1; return }",
|
||||
" guard data[index] == 44 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" index += 1",
|
||||
" }",
|
||||
" }",
|
||||
" if data[index] == 34 { _ = try parseString(); return }",
|
||||
" let start = index",
|
||||
" while index < data.count && ![9, 10, 13, 32, 44, 93, 125].contains(data[index]) { index += 1 }",
|
||||
" guard index > start else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" }",
|
||||
" try parseValue(0)",
|
||||
" skipWhitespace()",
|
||||
" guard index == data.count else { throw ContractValidationError(field: \"json\", code: \"trailing_json\") }",
|
||||
"}",
|
||||
"private func validBase64URL(_ value: String) -> Bool {",
|
||||
" guard !value.isEmpty, value.utf8.allSatisfy({ byte in",
|
||||
" (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || (byte >= 48 && byte <= 57) || byte == 45 || byte == 95",
|
||||
@@ -663,6 +822,11 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" let fraction = bytes[20..<(bytes.count - 1)]",
|
||||
" return bytes[19] == 46 && !fraction.isEmpty && fraction.count <= 9 && fraction.allSatisfy { $0 >= 48 && $0 <= 57 } && fraction.last != 48",
|
||||
"}",
|
||||
"private func validCanonicalUUID(_ value: String) -> Bool {",
|
||||
" let bytes = Array(value.utf8)",
|
||||
" guard bytes.count == 36, bytes[8] == 45, bytes[13] == 45, bytes[18] == 45, bytes[23] == 45, value != \"00000000-0000-0000-0000-000000000000\" else { return false }",
|
||||
" return bytes.enumerated().allSatisfy { index, byte in [8, 13, 18, 23].contains(index) || (byte >= 48 && byte <= 57) || (byte >= 97 && byte <= 102) }",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
@@ -704,7 +868,7 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
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 static func decodeJSON(_ data: Data) throws -> Self { try rejectDuplicateJSONKeys(data); return try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""])
|
||||
out.extend([
|
||||
"public func deviceRegistrationProofTranscript(serverID: Data, principalID: Data, deviceID: Data, challenge: Data, expiryUnixMilliseconds: Int64) throws -> Data {",
|
||||
" for (field, value, length) in [(\"server_id\", serverID, 16), (\"principal_id\", principalID, 16), (\"device_id\", deviceID, 16), (\"challenge\", challenge, 32)] {",
|
||||
@@ -723,8 +887,9 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
"",
|
||||
"public extension TunnelAdmissionRequest {",
|
||||
" func deviceAdmissionTranscript() -> Data {",
|
||||
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]",
|
||||
" fields.append(contentsOf: capabilities.clientDecode)",
|
||||
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.sourceRateControl, String(capabilities.videoProfiles.count), String(capabilities.audioProfiles.count)]",
|
||||
" for profile in capabilities.videoProfiles { fields.append(contentsOf: [profile.codec, String(profile.bitDepth), profile.chromaSubsampling, profile.colorSpace, profile.transferFunction]) }",
|
||||
" for profile in capabilities.audioProfiles { fields.append(contentsOf: [profile.codec, String(profile.sampleRateHz), String(profile.channels), profile.channelLayout, String(profile.packetDurationMs)]) }",
|
||||
" var transcript = \"versevdi/tunnel-admission/v1\"",
|
||||
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
||||
" return Data(transcript.utf8)",
|
||||
@@ -735,14 +900,16 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
||||
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" try selected.validate()",
|
||||
" var common = selected.clientDecode",
|
||||
" var commonVideo = selected.videoProfiles",
|
||||
" var commonAudio = selected.audioProfiles",
|
||||
" for profile in profiles.dropFirst() {",
|
||||
" try profile.validate()",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" common = common.filter { profile.clientDecode.contains($0) }",
|
||||
" if common.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" commonVideo = commonVideo.filter { profile.videoProfiles.contains($0) }",
|
||||
" commonAudio = commonAudio.filter { profile.audioProfiles.contains($0) }",
|
||||
" if commonVideo.isEmpty || commonAudio.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" }",
|
||||
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)",
|
||||
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, sourceRateControl: selected.sourceRateControl, videoProfiles: commonVideo, audioProfiles: commonAudio)",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -101,6 +102,15 @@ func evaluate(version, kind, input string) string {
|
||||
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
|
||||
Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
|
||||
CorrelationID: "correlation-1",
|
||||
SelectedDescriptor: protocol.SelectedSessionDescriptor{
|
||||
VideoProfile: protocol.VideoProfile{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"},
|
||||
AudioProfile: protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5},
|
||||
DisplayMode: protocol.DisplayMode{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60},
|
||||
BitrateTargetKbps: 12000,
|
||||
BitrateMaximumKbps: 20000,
|
||||
Adjustment: protocol.SessionAdjustment{DisplayReason: "none", BitrateReason: "none"},
|
||||
MediaTimestampBasis: "gateway-send-wall-clock-ms",
|
||||
},
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
@@ -123,6 +133,8 @@ func evaluate(version, kind, input string) string {
|
||||
value := protocol.SessionRequest{
|
||||
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
|
||||
PoolID: parts["pool_id"], IdempotencyKey: parts["idempotency_key"],
|
||||
VideoProfiles: []protocol.VideoProfile{{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}},
|
||||
BitratePreference: protocol.BitratePreference{Mode: "auto"},
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
@@ -309,6 +321,13 @@ func classifyGatewayInput(encoded string) string {
|
||||
if len(body) != 4 {
|
||||
return "invalid:length"
|
||||
}
|
||||
case 8:
|
||||
if len(body) != 8 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if body[0] > 15 || body[3] > 3 {
|
||||
return "invalid:field"
|
||||
}
|
||||
default:
|
||||
return "invalid:kind"
|
||||
}
|
||||
@@ -352,6 +371,22 @@ func classifyGatewayFeedback(encoded string) string {
|
||||
if len(body) == 0 {
|
||||
return "valid"
|
||||
}
|
||||
case 4:
|
||||
if len(body) != 24 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if binary.BigEndian.Uint64(body[16:]) == 0 || allZero(body[:16]) {
|
||||
return "invalid:field"
|
||||
}
|
||||
return "valid"
|
||||
case 5:
|
||||
if len(body) != 16 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if allZero(body) {
|
||||
return "invalid:field"
|
||||
}
|
||||
return "valid"
|
||||
default:
|
||||
return "invalid:type"
|
||||
}
|
||||
@@ -386,6 +421,15 @@ func classifyGatewayFeedback(encoded string) string {
|
||||
return "invalid:field"
|
||||
}
|
||||
|
||||
func allZero(value []byte) bool {
|
||||
for _, item := range value {
|
||||
if item != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validFECStatus(body []byte) bool {
|
||||
if len(body) != 21 || int(body[10])<<8|int(body[11]) == 0 || int(body[14])<<8|int(body[15]) > int(body[10])<<8|int(body[11]) || int(body[16])<<8|int(body[17]) > int(body[12])<<8|int(body[13]) || body[18] > 100 || body[20] == 0 || body[19] >= body[20] {
|
||||
return false
|
||||
|
||||
@@ -38,6 +38,8 @@ fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
|
||||
values.get("device_key_id").cloned().unwrap_or_default(),
|
||||
values.get("pool_id").cloned().unwrap_or_default(),
|
||||
values.get("idempotency_key").cloned().unwrap_or_default(),
|
||||
vec![VideoProfile::new("h264".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap()],
|
||||
BitratePreference::new("auto".into(), None).unwrap(),
|
||||
None,
|
||||
) {
|
||||
Ok(_) => "valid",
|
||||
@@ -184,6 +186,9 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
|
||||
}
|
||||
7 if body.len() == 4 => "valid",
|
||||
7 => "invalid:length",
|
||||
8 if body.len() != 8 => "invalid:length",
|
||||
8 if body[0] > 15 || body[3] > 3 => "invalid:field",
|
||||
8 => "valid",
|
||||
_ => "invalid:kind",
|
||||
}
|
||||
}
|
||||
@@ -219,6 +224,12 @@ fn classify_gateway_feedback(encoded: &str) -> &'static str {
|
||||
2 => "invalid:field",
|
||||
3 if body.is_empty() => "valid",
|
||||
3 => "invalid:length",
|
||||
4 if body.len() == 24 && body[..16].iter().any(|value| *value != 0) && body[16..24].iter().any(|value| *value != 0) => "valid",
|
||||
4 if body.len() != 24 => "invalid:length",
|
||||
4 => "invalid:field",
|
||||
5 if body.len() == 16 && body.iter().any(|value| *value != 0) => "valid",
|
||||
5 if body.len() != 16 => "invalid:length",
|
||||
5 => "invalid:field",
|
||||
_ => "invalid:type",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ func evaluate(_ version: String, _ kind: String, _ input: String) -> String {
|
||||
guard (try? SessionRequest(
|
||||
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
|
||||
poolId: values["pool_id"] ?? "", idempotencyKey: values["idempotency_key"] ?? "",
|
||||
videoProfiles: [try! VideoProfile(codec: "h264", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")],
|
||||
bitratePreference: try! BitratePreference(mode: "auto", targetKbps: nil),
|
||||
requestedDisplayMode: nil
|
||||
)) != nil else { return "invalid:required" }
|
||||
return "valid"
|
||||
@@ -136,6 +138,9 @@ func classifyGatewayInput(_ encoded: String) -> String {
|
||||
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"
|
||||
case 8:
|
||||
guard body.count == 8 else { return "invalid:length" }
|
||||
return body[0] <= 15 && body[3] <= 3 ? "valid" : "invalid:field"
|
||||
default: return "invalid:kind"
|
||||
}
|
||||
}
|
||||
@@ -156,6 +161,12 @@ func classifyGatewayFeedback(_ encoded: String) -> String {
|
||||
case 2:
|
||||
return validFECStatus(body) ? "valid" : "invalid:field"
|
||||
case 3: return body.isEmpty ? "valid" : "invalid:length"
|
||||
case 4:
|
||||
guard body.count == 24 else { return "invalid:length" }
|
||||
return body[0...15].contains(where: { $0 != 0 }) && body[16...23].contains(where: { $0 != 0 }) ? "valid" : "invalid:field"
|
||||
case 5:
|
||||
guard body.count == 16 else { return "invalid:length" }
|
||||
return body.contains(where: { $0 != 0 }) ? "valid" : "invalid:field"
|
||||
default: return "invalid:type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ from __future__ import annotations
|
||||
import pathlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def run(command: list[str]) -> None:
|
||||
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True)
|
||||
environment = os.environ.copy()
|
||||
cache_root = pathlib.Path(tempfile.gettempdir()) / "versevdi-protocol-module-cache"
|
||||
environment.setdefault("CLANG_MODULE_CACHE_PATH", str(cache_root / "clang"))
|
||||
environment.setdefault("SWIFT_MODULECACHE_PATH", str(cache_root / "swift"))
|
||||
result = subprocess.run(command, cwd=ROOT, env=environment, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(result.stdout + result.stderr)
|
||||
print(result.stdout.strip())
|
||||
|
||||
@@ -8,13 +8,18 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
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)
|
||||
environment = os.environ.copy()
|
||||
cache_root = pathlib.Path(tempfile.gettempdir()) / "versevdi-protocol-module-cache"
|
||||
environment.setdefault("CLANG_MODULE_CACHE_PATH", str(cache_root / "clang"))
|
||||
environment.setdefault("SWIFT_MODULECACHE_PATH", str(cache_root / "swift"))
|
||||
result = subprocess.run(command, cwd=directory, env=environment, text=True, capture_output=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
|
||||
|
||||
@@ -58,21 +63,42 @@ def main() -> int:
|
||||
swift.write_text(
|
||||
"""import Foundation
|
||||
|
||||
let video = try VideoProfile(codec: "h264", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")
|
||||
let hevc = try VideoProfile(codec: "hevc", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")
|
||||
let audio = try AudioProfile(codec: "opus", sampleRateHz: 48000, channels: 2, channelLayout: "stereo", packetDurationMs: 5)
|
||||
let display = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
|
||||
let adjustment = try SessionAdjustment(displayReason: "none", bitrateReason: "none")
|
||||
let descriptor = try SelectedSessionDescriptor(videoProfile: video, audioProfile: audio, displayMode: display, bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000, adjustment: adjustment, mediaTimestampBasis: "gateway-send-wall-clock-ms")
|
||||
let operationId = "12345678-1234-1234-1234-123456789abc"
|
||||
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "poll", operationId: nil, revision: nil, leaseGeneration: nil, currentAppliedRevision: nil)
|
||||
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "prompt", operationId: operationId, revision: 7, leaseGeneration: nil, currentAppliedRevision: nil)
|
||||
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "observation", operationId: operationId, revision: 7, leaseGeneration: 3, currentAppliedRevision: 6)
|
||||
do {
|
||||
_ = try GatewayQualityWorkRequest(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, acquisition: "poll", operationId: operationId, revision: 7, leaseGeneration: nil, currentAppliedRevision: nil)
|
||||
fatalError("poll accepted unknown operation coordinates")
|
||||
} catch { }
|
||||
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "applied", currentAppliedRevision: 7, failureCode: nil)
|
||||
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "proven_prior", currentAppliedRevision: 6, failureCode: nil)
|
||||
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "unknown", currentAppliedRevision: nil, failureCode: nil)
|
||||
do {
|
||||
_ = try GatewayQualityAck(version: "1", sessionId: "session", gatewayId: "gateway", reconnectSequence: 2, operationId: operationId, revision: 7, leaseGeneration: 3, outcome: "applied", currentAppliedRevision: 6, failureCode: nil)
|
||||
fatalError("applied ack accepted a contradictory revision")
|
||||
} catch { }
|
||||
let capability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
|
||||
)
|
||||
guard currentWireVersion == "2", nMinus1WireVersion == "1", nMinus2WireVersion == "0" else {
|
||||
fatalError("unexpected control wire compatibility declaration")
|
||||
}
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
|
||||
)
|
||||
do {
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
|
||||
)
|
||||
fatalError("unregistered framing was accepted")
|
||||
} catch { }
|
||||
@@ -82,7 +108,7 @@ let request = try TunnelAdmissionRequest(
|
||||
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:server1:19:h264-opus"
|
||||
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:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5"
|
||||
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
|
||||
fatalError("unexpected device admission transcript")
|
||||
}
|
||||
@@ -126,18 +152,18 @@ for (name, field, serverID, principalID, deviceID, challenge, expiry) in invalid
|
||||
}
|
||||
let incompatible = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [hevc], audioProfiles: [audio]
|
||||
)
|
||||
let gatewayCapability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus", "h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [hevc, video], audioProfiles: [audio]
|
||||
)
|
||||
do {
|
||||
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
|
||||
fatalError("matching capability profiles did not intersect")
|
||||
}
|
||||
} catch { fatalError("matching capability profiles did not intersect") }
|
||||
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else {
|
||||
guard try CapabilityProfile.intersection([gatewayCapability, capability]).videoProfiles == [video] else {
|
||||
fatalError("ordered registered profile intersection changed")
|
||||
}
|
||||
do {
|
||||
@@ -165,17 +191,35 @@ for invalid in [
|
||||
}
|
||||
let clientAuthority = try ClientSessionAuthority(
|
||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||
reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability
|
||||
reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability, selectedDescriptor: descriptor
|
||||
)
|
||||
let clientAuthorityJSON = try clientAuthority.encodeJSON()
|
||||
let clientAuthorityObject = try JSONSerialization.jsonObject(with: clientAuthorityJSON) as! [String: Any]
|
||||
guard Set(clientAuthorityObject.keys) == Set([
|
||||
"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"
|
||||
"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"
|
||||
]), !String(data: clientAuthorityJSON, encoding: .utf8)!.contains("provider_") else {
|
||||
fatalError("client authority was not exactly provider-free")
|
||||
}
|
||||
_ = try ClientSessionAuthority.decodeJSON(clientAuthorityJSON)
|
||||
for field in ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"] {
|
||||
let duplicateCapability = Data(#"{"transport":"quic-tls13","transport":"quic-tls13","framing":"datagram-v1","media":"encoded","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}"#.utf8)
|
||||
do {
|
||||
_ = try CapabilityProfile.decodeJSON(duplicateCapability)
|
||||
fatalError("capability accepted duplicate JSON keys")
|
||||
} catch { }
|
||||
do {
|
||||
_ = try CapabilityProfile.decodeJSON(Data(repeating: 32, count: 1_048_577))
|
||||
fatalError("capability accepted oversized JSON")
|
||||
} catch let error as ContractValidationError {
|
||||
guard error.code == "payload_too_large" else { fatalError("oversized JSON was not rejected before parsing") }
|
||||
}
|
||||
let deeplyNested = Data((String(repeating: "[", count: 65) + "null" + String(repeating: "]", count: 65)).utf8)
|
||||
do {
|
||||
_ = try CapabilityProfile.decodeJSON(deeplyNested)
|
||||
fatalError("capability accepted over-deep JSON")
|
||||
} catch let error as ContractValidationError {
|
||||
guard error.code == "nesting_too_deep" else { fatalError("over-deep JSON was not rejected before decoding") }
|
||||
}
|
||||
for field in ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"] {
|
||||
var missing = clientAuthorityObject
|
||||
missing.removeValue(forKey: field)
|
||||
do {
|
||||
@@ -217,7 +261,7 @@ do {
|
||||
)
|
||||
fatalError("invalid allocation bounds were accepted")
|
||||
} catch { }
|
||||
let displayMode = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
|
||||
let displayMode = display
|
||||
for invalid in [
|
||||
{ try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) },
|
||||
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) },
|
||||
@@ -230,14 +274,14 @@ for invalid in [
|
||||
}
|
||||
let policyFreeV2Request = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", requestedDisplayMode: nil
|
||||
idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "auto", targetKbps: nil), requestedDisplayMode: nil
|
||||
).encodeJSON()
|
||||
guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else {
|
||||
fatalError("wire-v2 request encoded an absent display mode")
|
||||
}
|
||||
let displayRequest = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", requestedDisplayMode: displayMode
|
||||
idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "explicit", targetKbps: 40000), requestedDisplayMode: displayMode
|
||||
)
|
||||
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
|
||||
fatalError("display mode did not round-trip")
|
||||
@@ -295,14 +339,14 @@ for expiresAt in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
|
||||
} catch { }
|
||||
}
|
||||
let streamPolicy = try ProviderStreamPolicy(
|
||||
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
|
||||
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
|
||||
videoProfile: hevc, audioProfile: audio, displayMode: displayMode,
|
||||
bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000
|
||||
)
|
||||
guard streamPolicy.codec == "HEVC" else { fatalError("stream policy changed") }
|
||||
guard streamPolicy.videoProfile == hevc else { fatalError("stream policy changed") }
|
||||
for invalid in [
|
||||
{ try ProviderStreamPolicy(resolutionWidth: 319, resolutionHeight: 1440, fps: 120, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
|
||||
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
|
||||
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, codec: "VP9", bitrateKbps: 40000, audioEnabled: true) },
|
||||
{ try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120), bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
|
||||
{ try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 50001, bitrateMaximumKbps: 50000) },
|
||||
{ try ProviderStreamPolicy(videoProfile: try VideoProfile(codec: "vp9", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr"), audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
|
||||
] {
|
||||
do {
|
||||
_ = try invalid()
|
||||
@@ -363,17 +407,32 @@ fn main() {
|
||||
assert_eq!(CURRENT_WIRE_VERSION, "2");
|
||||
assert_eq!(N_MINUS_1_WIRE_VERSION, "1");
|
||||
assert_eq!(N_MINUS_2_WIRE_VERSION, "0");
|
||||
let video = VideoProfile::new("h264".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap();
|
||||
let hevc = VideoProfile::new("hevc".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap();
|
||||
let audio = AudioProfile::new("opus".into(), 48000, 2, "stereo".into(), 5).unwrap();
|
||||
let display = DisplayMode::new(2560, 1440, 120).unwrap();
|
||||
let adjustment = SessionAdjustment::new("none".into(), "none".into()).unwrap();
|
||||
let descriptor = SelectedSessionDescriptor::new(video.clone(), audio.clone(), display.clone(), 40000, 50000, adjustment, "gateway-send-wall-clock-ms".into()).unwrap();
|
||||
let operation_id = "12345678-1234-1234-1234-123456789abc".to_string();
|
||||
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "poll".into(), None, None, None, None).is_ok());
|
||||
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "prompt".into(), Some(operation_id.clone()), Some(7), None, None).is_ok());
|
||||
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "observation".into(), Some(operation_id.clone()), Some(7), Some(3), Some(6)).is_ok());
|
||||
assert!(GatewayQualityWorkRequest::new("1".into(), "session".into(), "gateway".into(), 2, "poll".into(), Some(operation_id.clone()), Some(7), None, None).is_err());
|
||||
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id.clone(), 7, 3, "applied".into(), Some(7), None).is_ok());
|
||||
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id.clone(), 7, 3, "proven_prior".into(), Some(6), None).is_ok());
|
||||
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id.clone(), 7, 3, "unknown".into(), None, None).is_ok());
|
||||
assert!(GatewayQualityAck::new("1".into(), "session".into(), "gateway".into(), 2, operation_id, 7, 3, "applied".into(), Some(6), None).is_err());
|
||||
let capabilities = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
"server".into(), vec![video.clone()], vec![audio.clone()],
|
||||
).unwrap();
|
||||
assert!(CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
"server".into(), vec![video.clone()], vec![audio.clone()],
|
||||
).is_ok());
|
||||
assert!(CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
"server".into(), vec![video.clone()], vec![audio.clone()],
|
||||
).is_err());
|
||||
let request = TunnelAdmissionRequest::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
@@ -381,7 +440,7 @@ fn main() {
|
||||
).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:server1:19:h264-opus";
|
||||
+ "10:quic-tls1311:datagram-v17:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5";
|
||||
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
|
||||
let proof_server_id = vec![1u8; 16];
|
||||
let proof_principal_id = vec![2u8; 16];
|
||||
@@ -423,26 +482,26 @@ fn main() {
|
||||
).is_err());
|
||||
let client_authority = ClientSessionAuthority::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
|
||||
"2099-01-01T00:00:00Z".into(), capabilities.clone(),
|
||||
"2099-01-01T00:00:00Z".into(), capabilities.clone(), descriptor.clone(),
|
||||
).unwrap();
|
||||
assert_eq!(client_authority.sessionId(), "session");
|
||||
assert_eq!(client_authority.capabilities(), &capabilities);
|
||||
assert!(ClientSessionAuthority::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
|
||||
"not-a-time".into(), capabilities.clone(),
|
||||
"not-a-time".into(), capabilities.clone(), descriptor.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(), vec!["hevc-opus".into()],
|
||||
"server".into(), vec![hevc.clone()], vec![audio.clone()],
|
||||
).unwrap();
|
||||
let gateway_capability = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()],
|
||||
"server".into(), vec![hevc.clone(), video.clone()], vec![audio.clone()],
|
||||
).unwrap();
|
||||
assert_eq!(
|
||||
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(),
|
||||
&vec!["h264-opus".to_string()],
|
||||
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().videoProfiles(),
|
||||
&vec![video.clone()],
|
||||
);
|
||||
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
|
||||
assert!(AllocationPolicy::new(
|
||||
@@ -454,11 +513,13 @@ fn main() {
|
||||
assert!(DisplayMode::new(2560, 1440, 241).is_err());
|
||||
let policy_free_v2_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
vec![video.clone()], BitratePreference::new("auto".into(), None).unwrap(),
|
||||
None,
|
||||
).unwrap();
|
||||
assert!(policy_free_v2_request.requestedDisplayMode().is_none());
|
||||
let display_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
vec![video.clone()], BitratePreference::new("explicit".into(), Some(40000)).unwrap(),
|
||||
Some(display_mode.clone()),
|
||||
).unwrap();
|
||||
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
|
||||
@@ -486,17 +547,13 @@ fn main() {
|
||||
).is_err());
|
||||
}
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 120, "HEVC".into(), 40000, true,
|
||||
hevc.clone(), audio.clone(), display.clone(), 40000, 50000,
|
||||
).is_ok());
|
||||
assert!(DisplayMode::new(319, 1440, 120).is_err());
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
319, 1440, 120, "HEVC".into(), 40000, true,
|
||||
).is_err());
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 241, "HEVC".into(), 40000, true,
|
||||
).is_err());
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 120, "VP9".into(), 40000, true,
|
||||
video.clone(), audio.clone(), display.clone(), 50001, 50000,
|
||||
).is_err());
|
||||
assert!(VideoProfile::new("vp9".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).is_err());
|
||||
assert!(GatewayTelemetry::new(
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ready".into(),
|
||||
).is_ok());
|
||||
@@ -524,7 +581,8 @@ fn main() {
|
||||
)
|
||||
run(["rustc", str(rust), "-o", str(workspace / "rust-contracts")], ROOT)
|
||||
run([str(workspace / "rust-contracts")], ROOT)
|
||||
expected_protobuf_fields = [
|
||||
expected_protobuf_messages = {
|
||||
"ClientSessionAuthority": [
|
||||
("version", 1),
|
||||
("session_id", 2),
|
||||
("gateway_id", 3),
|
||||
@@ -532,11 +590,57 @@ fn main() {
|
||||
("reconnect_sequence", 5),
|
||||
("expires_at", 6),
|
||||
("capabilities", 7),
|
||||
]
|
||||
actual_protobuf_fields = protobuf_message_fields("ClientSessionAuthority")
|
||||
("selected_descriptor", 8),
|
||||
],
|
||||
"GatewayQualityWorkRequest": [
|
||||
("version", 1),
|
||||
("session_id", 2),
|
||||
("gateway_id", 3),
|
||||
("reconnect_sequence", 4),
|
||||
("operation_id", 5),
|
||||
("revision", 6),
|
||||
("current_applied_revision", 7),
|
||||
("acquisition", 8),
|
||||
("lease_generation", 9),
|
||||
],
|
||||
"GatewayQualityWork": [
|
||||
("version", 1),
|
||||
("session_id", 2),
|
||||
("gateway_id", 3),
|
||||
("reconnect_sequence", 4),
|
||||
("operation_id", 5),
|
||||
("revision", 6),
|
||||
("lease_expires_at", 7),
|
||||
("selected_descriptor", 8),
|
||||
("current_applied_revision", 9),
|
||||
("lease_generation", 10),
|
||||
],
|
||||
"GatewayQualityAck": [
|
||||
("version", 1),
|
||||
("session_id", 2),
|
||||
("gateway_id", 3),
|
||||
("reconnect_sequence", 4),
|
||||
("operation_id", 5),
|
||||
("revision", 6),
|
||||
("outcome", 7),
|
||||
("current_applied_revision", 8),
|
||||
("failure_code", 9),
|
||||
("lease_generation", 10),
|
||||
],
|
||||
"GatewayStopWorkRequest": [
|
||||
("version", 1),
|
||||
("session_id", 2),
|
||||
("gateway_id", 3),
|
||||
("reconnect_sequence", 4),
|
||||
("operation_id", 5),
|
||||
("acquisition", 6),
|
||||
],
|
||||
}
|
||||
for message, expected_protobuf_fields in expected_protobuf_messages.items():
|
||||
actual_protobuf_fields = protobuf_message_fields(message)
|
||||
if actual_protobuf_fields != expected_protobuf_fields:
|
||||
raise RuntimeError(
|
||||
f"ClientSessionAuthority protobuf fields = {actual_protobuf_fields}; "
|
||||
f"{message} protobuf fields = {actual_protobuf_fields}; "
|
||||
f"want {expected_protobuf_fields}"
|
||||
)
|
||||
rust_unknown = workspace / "unknown.rs"
|
||||
|
||||
@@ -12,6 +12,17 @@ import sys
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def matches_outcome_branch(instance: dict[str, object], branch: dict[str, object]) -> bool:
|
||||
required = branch.get("required", [])
|
||||
if not all(field in instance for field in required):
|
||||
return False
|
||||
outcome = branch.get("properties", {}).get("outcome", {}).get("const")
|
||||
if instance.get("outcome") != outcome:
|
||||
return False
|
||||
forbidden = branch.get("not", {}).get("required", [])
|
||||
return not all(field in instance for field in forbidden) if forbidden else True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
schema_path = ROOT / "schemas/control-v1.schema.json"
|
||||
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||
@@ -39,6 +50,65 @@ def main() -> int:
|
||||
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
|
||||
registered_features = {entry["id"] for entry in feature_registry["features"]}
|
||||
assert {"control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
||||
assert {"video.profile.v1", "session.quality.v1", "session.stop.v1", "controller.arrival.v1"}.issubset(registered_features)
|
||||
|
||||
assert defs["VideoProfile"]["required"] == ["codec", "bit_depth", "chroma_subsampling", "color_space", "transfer_function"]
|
||||
assert defs["AudioProfile"]["required"] == ["codec", "sample_rate_hz", "channels", "channel_layout", "packet_duration_ms"]
|
||||
assert defs["CapabilityProfile"]["required"] == ["transport", "framing", "media", "source_rate_control", "video_profiles", "audio_profiles"]
|
||||
assert defs["CapabilityProfile"]["properties"]["video_profiles"] == {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": True, "items": {"$ref": "#/$defs/VideoProfile"}}
|
||||
assert defs["CapabilityProfile"]["properties"]["audio_profiles"] == {"type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": True, "items": {"$ref": "#/$defs/AudioProfile"}}
|
||||
assert defs["SessionRequest"]["required"][-2:] == ["video_profiles", "bitrate_preference"]
|
||||
assert defs["ReconnectRequest"]["required"][-1] == "display_relaunch_confirmed"
|
||||
assert defs["AssignedDesktop"]["required"][-1] == "quality_limits"
|
||||
assert defs["EntitledPool"]["required"][-1] == "quality_limits"
|
||||
for owner in ("SessionAuthority", "ClientSessionAuthority"):
|
||||
assert defs[owner]["required"][-1] == "selected_descriptor"
|
||||
assert defs["ConnectionManifest"]["required"][-1] == "selected_descriptor"
|
||||
quality_request = defs["GatewayQualityWorkRequest"]
|
||||
assert quality_request["required"] == ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"]
|
||||
assert quality_request["properties"]["acquisition"]["enum"] == ["poll", "prompt", "observation"]
|
||||
assert quality_request["properties"]["lease_generation"]["minimum"] == 1
|
||||
assert defs["GatewayStopWorkRequest"]["required"] == ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"]
|
||||
assert defs["GatewayQualityWork"]["required"][-3:] == ["lease_generation", "lease_expires_at", "selected_descriptor"]
|
||||
quality_ack = defs["GatewayQualityAck"]
|
||||
assert quality_ack["required"][-3:] == ["revision", "lease_generation", "outcome"]
|
||||
assert quality_ack["properties"]["outcome"]["enum"] == ["applied", "proven_prior", "unknown"]
|
||||
assert quality_ack["description"] == (
|
||||
"Outcome invariants: applied requires current_applied_revision equal to revision; "
|
||||
"proven_prior requires current_applied_revision strictly less than revision; "
|
||||
"unknown forbids current_applied_revision and makes no applied-revision assertion."
|
||||
)
|
||||
assert quality_ack["oneOf"] == [
|
||||
{"properties": {"outcome": {"const": "applied"}}, "required": ["current_applied_revision"]},
|
||||
{"properties": {"outcome": {"const": "proven_prior"}}, "required": ["current_applied_revision"]},
|
||||
{"properties": {"outcome": {"const": "unknown"}}, "not": {"required": ["current_applied_revision"]}},
|
||||
]
|
||||
ack_branches = quality_ack["oneOf"]
|
||||
for valid_ack in (
|
||||
{"outcome": "applied", "current_applied_revision": 7},
|
||||
{"outcome": "proven_prior", "current_applied_revision": 6},
|
||||
{"outcome": "unknown"},
|
||||
):
|
||||
assert sum(matches_outcome_branch(valid_ack, branch) for branch in ack_branches) == 1, valid_ack
|
||||
for invalid_ack in (
|
||||
{"outcome": "applied"},
|
||||
{"outcome": "proven_prior"},
|
||||
{"outcome": "unknown", "current_applied_revision": 6},
|
||||
):
|
||||
assert not any(matches_outcome_branch(invalid_ack, branch) for branch in ack_branches), invalid_ack
|
||||
|
||||
operation_id_pattern = r"^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
canonical_time_pattern = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{0,8}[1-9])?Z$"
|
||||
for definition in ("QualityChangeOperation", "StopOperation", "GatewayQualityWorkRequest", "GatewayQualityWork", "GatewayQualityAck", "GatewayStopWorkRequest", "GatewayStopWork", "GatewayStopAck"):
|
||||
assert defs[definition]["properties"]["operation_id"]["pattern"] == operation_id_pattern, definition
|
||||
assert defs[definition]["x-max-bytes"] == 16384, definition
|
||||
for definition, fields in {
|
||||
"QualityChangeOperation": ("created_at", "deadline_at", "updated_at"),
|
||||
"StopOperation": ("created_at", "deadline_at", "updated_at"),
|
||||
"GatewayQualityWork": ("lease_expires_at",),
|
||||
}.items():
|
||||
for field in fields:
|
||||
assert defs[definition]["properties"][field]["pattern"] == canonical_time_pattern, (definition, field)
|
||||
|
||||
display_mode = defs["DisplayMode"]
|
||||
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
|
||||
@@ -113,6 +183,17 @@ def main() -> int:
|
||||
assert "native_identity" not in native_missing_fixture
|
||||
tunnel_credential_fixture = json.loads((ROOT / "fixtures/valid/native-tunnel-credential.json").read_text(encoding="utf-8"))
|
||||
assert set(tunnel_credential_fixture) == set(tunnel_credential["required"])
|
||||
valid_fixture_contracts = {
|
||||
"fixtures/valid/session-request.json": "SessionRequest",
|
||||
"fixtures/valid/selected-session-descriptor.json": "SelectedSessionDescriptor",
|
||||
"fixtures/valid/session-quality-limits.json": "SessionQualityLimits",
|
||||
}
|
||||
for relative, definition in valid_fixture_contracts.items():
|
||||
fixture = json.loads((ROOT / relative).read_text(encoding="utf-8"))
|
||||
assert set(fixture) == set(defs[definition]["required"]), (relative, definition)
|
||||
assert set(json.loads((ROOT / "fixtures/invalid/capability-rc5-opaque.json").read_text())) & {"audio", "client_decode"} == {"audio", "client_decode"}
|
||||
assert "video_profiles" not in json.loads((ROOT / "fixtures/invalid/session-request-rc5.json").read_text())
|
||||
assert set(json.loads((ROOT / "fixtures/invalid/provider-stream-policy-rc5.json").read_text())) == {"resolution_width", "resolution_height", "fps", "codec", "bitrate_kbps", "audio_enabled"}
|
||||
|
||||
expected_header = "id\tversion\tkind\tinput\texpected"
|
||||
ids = set()
|
||||
@@ -139,10 +220,64 @@ def main() -> int:
|
||||
fixture_hash.update((ROOT / relative).read_bytes())
|
||||
fixture_hash.update(b"\0")
|
||||
assert fixture_manifest["corpus_sha256"] == fixture_hash.hexdigest()
|
||||
json_fixture_paths = sorted(
|
||||
path.relative_to(ROOT).as_posix()
|
||||
for directory in (ROOT / "fixtures/valid", ROOT / "fixtures/invalid")
|
||||
for path in directory.glob("*.json")
|
||||
)
|
||||
assert fixture_manifest["json_files"] == json_fixture_paths
|
||||
json_fixture_hash = hashlib.sha256()
|
||||
for relative in json_fixture_paths:
|
||||
json_fixture_hash.update(relative.encode("utf-8"))
|
||||
json_fixture_hash.update(b"\0")
|
||||
json_fixture_hash.update((ROOT / relative).read_bytes())
|
||||
json_fixture_hash.update(b"\0")
|
||||
assert fixture_manifest["json_corpus_sha256"] == json_fixture_hash.hexdigest()
|
||||
|
||||
openapi = (ROOT / "openapi/control-v1.yaml").read_text(encoding="utf-8")
|
||||
assert "openapi: 3.1.0" in openapi
|
||||
assert "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi
|
||||
for route in (
|
||||
"/api/v1/session-quality-limits:",
|
||||
"/api/v1/session-quality-limits/assignments/{assignment_id}:",
|
||||
"/api/v1/session-quality-limits/pools/{pool_id}:",
|
||||
"/api/v1/admin/entitlements/{entitlement_id}/display-limit-override:",
|
||||
"/api/v1/broker/sessions/{session_id}/quality-changes:",
|
||||
"/api/v1/broker/sessions/{session_id}/quality-changes/{operation_id}:",
|
||||
"/api/v1/broker/sessions/{session_id}/stop-operations:",
|
||||
"/api/v1/broker/sessions/{session_id}/stop-operations/{operation_id}:",
|
||||
"/api/v1/gateway/quality-work:",
|
||||
"/api/v1/gateway/quality-ack:",
|
||||
"/api/v1/gateway/stop-work:",
|
||||
"/api/v1/gateway/stop-ack:",
|
||||
):
|
||||
assert route in openapi, route
|
||||
for operation_id in (
|
||||
"getSessionQualityLimits", "getAssignmentSessionQualityLimits", "getPoolSessionQualityLimits",
|
||||
"createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation",
|
||||
):
|
||||
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "nativeBearer: []" in operation and "browserSession" not in operation, operation_id
|
||||
for operation_id in (
|
||||
"acquireGatewayQualityWork", "acknowledgeGatewayQualityWork", "acquireGatewayStopWork", "acknowledgeGatewayStopWork",
|
||||
):
|
||||
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "gatewayMutualTLS: []" in operation and "nativeBearer" not in operation and "browserSession" not in operation, operation_id
|
||||
assert "certificate identity MUST match" in operation, operation_id
|
||||
assert "Maximum JSON body: 16384 bytes." in operation, operation_id
|
||||
quality_acquisition = openapi.split(" operationId: acquireGatewayQualityWork\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "`poll` acquisition omits unknown operation coordinates" in quality_acquisition
|
||||
assert "coordinates MUST match exactly" in quality_acquisition
|
||||
quality_acknowledgement = openapi.split(" operationId: acknowledgeGatewayQualityWork\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "Stale lease generations MUST be rejected" in quality_acknowledgement
|
||||
assert "`applied` requires `current_applied_revision == revision`" in quality_acknowledgement
|
||||
assert "`proven_prior` requires `current_applied_revision < revision`" in quality_acknowledgement
|
||||
assert "`unknown` forbids `current_applied_revision`" in quality_acknowledgement
|
||||
for operation_id in ("createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation"):
|
||||
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "owning principal and active device/key" in operation, operation_id
|
||||
assert openapi.count("Maximum JSON body: 16384 bytes.") >= 8
|
||||
assert operation_id_pattern in openapi
|
||||
assert "provider_url" not in openapi and "vm_address" not in openapi
|
||||
session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0]
|
||||
assert "$defs/BrowserAuthenticatedSession" in session_endpoint
|
||||
@@ -172,6 +307,9 @@ def main() -> int:
|
||||
browserCsrfCookie: []
|
||||
browserCsrfHeader: []
|
||||
"""
|
||||
admin_override = openapi.split(" operationId: updateEntitlementDisplayLimitOverride\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert browser_requirement.removeprefix(" ") in admin_override
|
||||
assert "Maximum JSON body: 16384 bytes." in admin_override
|
||||
for operation_id in (
|
||||
"issueReauthenticationGrant", "logoutSession", "registerDevice", "proveDevice", "revokeDevice",
|
||||
"requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession",
|
||||
|
||||
@@ -44,6 +44,10 @@ def classify_input(raw: bytes) -> str:
|
||||
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"
|
||||
if kind == 8:
|
||||
if len(body) != 8:
|
||||
return "invalid:length"
|
||||
return "valid" if body[0] <= 15 and body[3] <= 3 else "invalid:field"
|
||||
return "invalid:kind"
|
||||
|
||||
|
||||
@@ -68,6 +72,14 @@ def classify_feedback(raw: bytes) -> str:
|
||||
return "valid" if valid_fec_status(body) else "invalid:field"
|
||||
if kind == 3:
|
||||
return "valid" if not body else "invalid:length"
|
||||
if kind == 4:
|
||||
if len(body) != 24:
|
||||
return "invalid:length"
|
||||
return "valid" if any(body[:16]) and int.from_bytes(body[16:24], "big") > 0 else "invalid:field"
|
||||
if kind == 5:
|
||||
if len(body) != 16:
|
||||
return "invalid:length"
|
||||
return "valid" if any(body) else "invalid:field"
|
||||
return "invalid:type"
|
||||
if kind in (1, 2, 3):
|
||||
return "invalid:direction"
|
||||
|
||||
Reference in New Issue
Block a user