protocol: add generated bindings and conformance fixtures
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
id version kind input expected
|
||||||
|
version-current 1 version 1 valid
|
||||||
|
version-n-minus-1 0 version 0 valid
|
||||||
|
version-n-minus-2 -1 version -1 valid
|
||||||
|
version-unsupported 2 version 2 invalid:unsupported_version
|
||||||
|
page-valid 1 page limit=20;cursor=opaque valid
|
||||||
|
page-limit-high 1 page limit=101 invalid:invalid_limit
|
||||||
|
manifest-valid 1 manifest version=1;gateway_id=g-1;grant=opaque-one-time-grant-value-with-at-least-43-bytes;audience=versevdi-gateway;purpose=launch;protocol=verse-gateway-v1;expires_at=2099-01-01T00:00:00Z valid
|
||||||
|
manifest-provider-field 1 manifest gateway_id=g-1;grant=g-1;audience=versevdi-gateway;purpose=launch;provider_url=https://provider.invalid invalid:forbidden_field
|
||||||
|
clipboard-text-valid 1 clipboard encoding=utf-8;text=hello%20world valid
|
||||||
|
clipboard-file 1 clipboard encoding=octet-stream;file=/tmp/a invalid:unsupported_clipboard
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
id version kind input expected
|
||||||
|
valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
|
||||||
|
invalid-short 1 datagram hex=564401 invalid:truncated
|
||||||
|
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
||||||
|
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
||||||
|
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
id version kind input expected
|
||||||
|
event-valid 1 event version=1;sequence=42;correlation_id=corr-1;payload_bytes=128 valid
|
||||||
|
event-missing-correlation 1 event version=1;sequence=42;payload_bytes=128 invalid:required
|
||||||
|
event-gap 1 event version=1;sequence=42;correlation_id=corr-1;after=1;earliest=5;payload_bytes=128 invalid:gap
|
||||||
|
event-oversized 1 event version=1;sequence=42;correlation_id=corr-1;payload_bytes=16385 invalid:payload_limit
|
||||||
|
event-unsupported-version 1 event version=2;sequence=42;correlation_id=corr-1;payload_bytes=128 invalid:unsupported_version
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
id version kind input expected
|
||||||
|
tunnel-current 1 tunnel offered=1;feature=control.v1 valid
|
||||||
|
tunnel-n-minus-1 0 tunnel offered=0;feature=control.v1 valid
|
||||||
|
tunnel-n-minus-2 -1 tunnel offered=-1;feature=control.v1 valid
|
||||||
|
tunnel-unsupported 1 tunnel offered=2;feature=control.v1 invalid:unsupported_version
|
||||||
|
tunnel-no-control 1 tunnel offered=1;feature=media.video invalid:unsupported_feature
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"purpose": "launch",
|
||||||
|
"session_id": "session-1",
|
||||||
|
"reconnect_sequence": 0,
|
||||||
|
"gateway": {
|
||||||
|
"id": "gateway-1",
|
||||||
|
"addresses": ["gateway.control.test:443"],
|
||||||
|
"public_identity": "gateway-1"
|
||||||
|
},
|
||||||
|
"tunnel": {
|
||||||
|
"versions": ["verse-gateway-v1/1"],
|
||||||
|
"features": ["control.v1"]
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"id": "standard",
|
||||||
|
"bounds": {
|
||||||
|
"minimum_kbps": 1000,
|
||||||
|
"target_kbps": 5000,
|
||||||
|
"maximum_kbps": 10000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"provider_url": "https://provider.invalid"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"algorithm": "sha256(path\\0bytes\\0 sorted by path)",
|
||||||
|
"files": [
|
||||||
|
"fixtures/conformance/control-v1.tsv",
|
||||||
|
"fixtures/conformance/datagram-v1.tsv",
|
||||||
|
"fixtures/conformance/events-v1.tsv",
|
||||||
|
"fixtures/conformance/tunnel-v1.tsv"
|
||||||
|
],
|
||||||
|
"corpus_sha256": "c91a512dc67aa9912b31b21144be2adfeacf0dc80dd8515bd3b4a8f52977e761"
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"purpose": "launch",
|
||||||
|
"session_id": "session-1",
|
||||||
|
"reconnect_sequence": 0,
|
||||||
|
"gateway": {
|
||||||
|
"id": "gateway-1",
|
||||||
|
"addresses": ["gateway.control.test:443"],
|
||||||
|
"public_identity": "gateway-1"
|
||||||
|
},
|
||||||
|
"tunnel": {
|
||||||
|
"versions": ["verse-gateway-v1/1"],
|
||||||
|
"features": ["control.v1", "clipboard.text"]
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"id": "standard",
|
||||||
|
"bounds": {
|
||||||
|
"minimum_kbps": 1000,
|
||||||
|
"target_kbps": 5000,
|
||||||
|
"maximum_kbps": 10000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compatibility": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"current": "1",
|
||||||
|
"datagram_registry": "registries/datagrams.json",
|
||||||
|
"feature_registry": "registries/features.json",
|
||||||
|
"n_minus_1": "0",
|
||||||
|
"n_minus_2": "-1",
|
||||||
|
"protocol": "versevdi-control",
|
||||||
|
"unsupported": [
|
||||||
|
"-2",
|
||||||
|
"2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c",
|
||||||
|
"protocol_version": "1.0.0",
|
||||||
|
"schema_sha256": "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,256 @@
|
|||||||
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
pub const SCHEMA_SHA256: &str = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1";
|
||||||
|
pub const CURRENT_WIRE_VERSION: &str = "1";
|
||||||
|
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
||||||
|
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
||||||
|
pub type JsonObject = std::collections::BTreeMap<String, String>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AllocationPolicy {
|
||||||
|
pub minimumKbps: i64,
|
||||||
|
pub targetKbps: i64,
|
||||||
|
pub maximumKbps: i64,
|
||||||
|
pub tier: String,
|
||||||
|
pub audience: String,
|
||||||
|
pub protocol: String,
|
||||||
|
pub protocolVersion: i64,
|
||||||
|
pub grantTtlSeconds: i64,
|
||||||
|
pub reservationLeaseSeconds: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AssignedDesktop {
|
||||||
|
pub assignmentId: String,
|
||||||
|
pub poolId: String,
|
||||||
|
pub name: String,
|
||||||
|
pub availability: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct BrokerSession {
|
||||||
|
pub id: String,
|
||||||
|
pub principalId: String,
|
||||||
|
pub poolId: String,
|
||||||
|
pub assignmentId: Option<String>,
|
||||||
|
pub state: String,
|
||||||
|
pub policySnapshot: AllocationPolicy,
|
||||||
|
pub reconnectDeadline: Option<String>,
|
||||||
|
pub outcome: Option<String>,
|
||||||
|
pub failureCode: Option<String>,
|
||||||
|
pub cleanupState: String,
|
||||||
|
pub idempotencyKey: String,
|
||||||
|
pub correlationId: String,
|
||||||
|
pub requestedAt: String,
|
||||||
|
pub endedAt: Option<String>,
|
||||||
|
pub version: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ClipboardText {
|
||||||
|
pub text: String,
|
||||||
|
pub encoding: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ConnectionManifest {
|
||||||
|
pub version: String,
|
||||||
|
pub purpose: String,
|
||||||
|
pub sessionId: String,
|
||||||
|
pub reconnectSequence: i64,
|
||||||
|
pub gateway: ManifestGateway,
|
||||||
|
pub tunnel: ManifestTunnel,
|
||||||
|
pub profile: ManifestProfile,
|
||||||
|
pub grant: GrantReference,
|
||||||
|
pub correlationId: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DeviceChallenge {
|
||||||
|
pub deviceId: String,
|
||||||
|
pub serverId: String,
|
||||||
|
pub principalId: String,
|
||||||
|
pub challenge: String,
|
||||||
|
pub expiresAt: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub signatureFormat: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DeviceProofRequest {
|
||||||
|
pub challenge: String,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DeviceRegistrationRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub platform: String,
|
||||||
|
pub deviceSubject: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub publicKey: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EntitledPool {
|
||||||
|
pub poolId: String,
|
||||||
|
pub name: String,
|
||||||
|
pub assignmentState: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ErrorEnvelope {
|
||||||
|
pub status: bool,
|
||||||
|
pub error: String,
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub resolution: String,
|
||||||
|
pub requestId: String,
|
||||||
|
pub violations: Vec<FieldViolation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EventEnvelope {
|
||||||
|
pub eventId: String,
|
||||||
|
pub sequence: i64,
|
||||||
|
pub typeValue: String,
|
||||||
|
pub version: i64,
|
||||||
|
pub resource: ResourceLink,
|
||||||
|
pub occurredAt: String,
|
||||||
|
pub correlationId: String,
|
||||||
|
pub payload: JsonObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EventResume {
|
||||||
|
pub cursor: String,
|
||||||
|
pub lastSequence: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FieldViolation {
|
||||||
|
pub field: String,
|
||||||
|
pub code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GrantReference {
|
||||||
|
pub opaqueValue: String,
|
||||||
|
pub expiresAt: String,
|
||||||
|
pub audience: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
pub provider: Option<String>,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ManifestBounds {
|
||||||
|
pub minimumKbps: i64,
|
||||||
|
pub targetKbps: i64,
|
||||||
|
pub maximumKbps: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ManifestGateway {
|
||||||
|
pub id: String,
|
||||||
|
pub addresses: Vec<String>,
|
||||||
|
pub publicIdentity: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ManifestProfile {
|
||||||
|
pub id: String,
|
||||||
|
pub bounds: ManifestBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ManifestTunnel {
|
||||||
|
pub versions: Vec<String>,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NativeCredential {
|
||||||
|
pub deviceId: Option<String>,
|
||||||
|
pub familyId: String,
|
||||||
|
pub accessToken: String,
|
||||||
|
pub refreshToken: String,
|
||||||
|
pub expiresAt: String,
|
||||||
|
pub refreshExpiresAt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PageInfo {
|
||||||
|
pub limit: i64,
|
||||||
|
pub nextCursor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ReauthGrant {
|
||||||
|
pub token: String,
|
||||||
|
pub purpose: String,
|
||||||
|
pub expiresAt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ReauthRequest {
|
||||||
|
pub password: String,
|
||||||
|
pub purpose: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ReconnectRequest {
|
||||||
|
pub clientDeviceId: String,
|
||||||
|
pub deviceKeyId: String,
|
||||||
|
pub expectedVersion: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RefreshRequest {
|
||||||
|
pub familyId: String,
|
||||||
|
pub refreshToken: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Resource {
|
||||||
|
pub id: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub name: String,
|
||||||
|
pub state: String,
|
||||||
|
pub assignmentState: Option<String>,
|
||||||
|
pub version: i64,
|
||||||
|
pub links: Vec<ResourceLink>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ResourceLink {
|
||||||
|
pub typeValue: String,
|
||||||
|
pub id: String,
|
||||||
|
pub version: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ResourceList {
|
||||||
|
pub assignedDesktops: Vec<AssignedDesktop>,
|
||||||
|
pub entitledPools: Vec<EntitledPool>,
|
||||||
|
pub page: PageInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SessionRequest {
|
||||||
|
pub clientDeviceId: String,
|
||||||
|
pub deviceKeyId: String,
|
||||||
|
pub poolId: String,
|
||||||
|
pub idempotencyKey: String,
|
||||||
|
pub policySnapshot: AllocationPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct VersionNegotiation {
|
||||||
|
pub supportedVersions: Vec<String>,
|
||||||
|
pub features: Vec<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,664 @@
|
|||||||
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
|
import Foundation
|
||||||
|
public typealias JSONObject = [String: String]
|
||||||
|
public let schemaSHA256 = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
||||||
|
public let currentWireVersion = "1"
|
||||||
|
public let nMinus1WireVersion = "0"
|
||||||
|
public let nMinus2WireVersion = "-1"
|
||||||
|
|
||||||
|
public struct AllocationPolicy: Codable, Equatable {
|
||||||
|
public let minimumKbps: Int64
|
||||||
|
public let targetKbps: Int64
|
||||||
|
public let maximumKbps: Int64
|
||||||
|
public let tier: String
|
||||||
|
public let audience: String
|
||||||
|
public let protocolValue: String
|
||||||
|
public let protocolVersion: Int64
|
||||||
|
public let grantTtlSeconds: Int64
|
||||||
|
public let reservationLeaseSeconds: Int64
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case minimumKbps = "minimum_kbps"
|
||||||
|
case targetKbps = "target_kbps"
|
||||||
|
case maximumKbps = "maximum_kbps"
|
||||||
|
case tier = "tier"
|
||||||
|
case audience = "audience"
|
||||||
|
case protocolValue = "protocol"
|
||||||
|
case protocolVersion = "protocol_version"
|
||||||
|
case grantTtlSeconds = "grant_ttl_seconds"
|
||||||
|
case reservationLeaseSeconds = "reservation_lease_seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
minimumKbps = try c.decode(Int64.self, forKey: .minimumKbps)
|
||||||
|
targetKbps = try c.decode(Int64.self, forKey: .targetKbps)
|
||||||
|
maximumKbps = try c.decode(Int64.self, forKey: .maximumKbps)
|
||||||
|
tier = try c.decode(String.self, forKey: .tier)
|
||||||
|
audience = try c.decode(String.self, forKey: .audience)
|
||||||
|
protocolValue = try c.decode(String.self, forKey: .protocolValue)
|
||||||
|
protocolVersion = try c.decode(Int64.self, forKey: .protocolVersion)
|
||||||
|
grantTtlSeconds = try c.decode(Int64.self, forKey: .grantTtlSeconds)
|
||||||
|
reservationLeaseSeconds = try c.decode(Int64.self, forKey: .reservationLeaseSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AssignedDesktop: Codable, Equatable {
|
||||||
|
public let assignmentId: String
|
||||||
|
public let poolId: String
|
||||||
|
public let name: String
|
||||||
|
public let availability: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case assignmentId = "assignment_id"
|
||||||
|
case poolId = "pool_id"
|
||||||
|
case name = "name"
|
||||||
|
case availability = "availability"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
assignmentId = try c.decode(String.self, forKey: .assignmentId)
|
||||||
|
poolId = try c.decode(String.self, forKey: .poolId)
|
||||||
|
name = try c.decode(String.self, forKey: .name)
|
||||||
|
availability = try c.decode(String.self, forKey: .availability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BrokerSession: Codable, Equatable {
|
||||||
|
public let id: String
|
||||||
|
public let principalId: String
|
||||||
|
public let poolId: String
|
||||||
|
public let assignmentId: String?
|
||||||
|
public let state: String
|
||||||
|
public let policySnapshot: AllocationPolicy
|
||||||
|
public let reconnectDeadline: String?
|
||||||
|
public let outcome: String?
|
||||||
|
public let failureCode: String?
|
||||||
|
public let cleanupState: String
|
||||||
|
public let idempotencyKey: String
|
||||||
|
public let correlationId: String
|
||||||
|
public let requestedAt: String
|
||||||
|
public let endedAt: String?
|
||||||
|
public let version: Int64
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id = "id"
|
||||||
|
case principalId = "principal_id"
|
||||||
|
case poolId = "pool_id"
|
||||||
|
case assignmentId = "assignment_id"
|
||||||
|
case state = "state"
|
||||||
|
case policySnapshot = "policy_snapshot"
|
||||||
|
case reconnectDeadline = "reconnect_deadline"
|
||||||
|
case outcome = "outcome"
|
||||||
|
case failureCode = "failure_code"
|
||||||
|
case cleanupState = "cleanup_state"
|
||||||
|
case idempotencyKey = "idempotency_key"
|
||||||
|
case correlationId = "correlation_id"
|
||||||
|
case requestedAt = "requested_at"
|
||||||
|
case endedAt = "ended_at"
|
||||||
|
case version = "version"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(String.self, forKey: .id)
|
||||||
|
principalId = try c.decode(String.self, forKey: .principalId)
|
||||||
|
poolId = try c.decode(String.self, forKey: .poolId)
|
||||||
|
assignmentId = try c.decodeIfPresent(String.self, forKey: .assignmentId)
|
||||||
|
state = try c.decode(String.self, forKey: .state)
|
||||||
|
policySnapshot = try c.decode(AllocationPolicy.self, forKey: .policySnapshot)
|
||||||
|
reconnectDeadline = try c.decodeIfPresent(String.self, forKey: .reconnectDeadline)
|
||||||
|
outcome = try c.decodeIfPresent(String.self, forKey: .outcome)
|
||||||
|
failureCode = try c.decodeIfPresent(String.self, forKey: .failureCode)
|
||||||
|
cleanupState = try c.decode(String.self, forKey: .cleanupState)
|
||||||
|
idempotencyKey = try c.decode(String.self, forKey: .idempotencyKey)
|
||||||
|
correlationId = try c.decode(String.self, forKey: .correlationId)
|
||||||
|
requestedAt = try c.decode(String.self, forKey: .requestedAt)
|
||||||
|
endedAt = try c.decodeIfPresent(String.self, forKey: .endedAt)
|
||||||
|
version = try c.decode(Int64.self, forKey: .version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClipboardText: Codable, Equatable {
|
||||||
|
public let text: String
|
||||||
|
public let encoding: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case text = "text"
|
||||||
|
case encoding = "encoding"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
text = try c.decode(String.self, forKey: .text)
|
||||||
|
encoding = try c.decode(String.self, forKey: .encoding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ConnectionManifest: Codable, Equatable {
|
||||||
|
public let version: String
|
||||||
|
public let purpose: String
|
||||||
|
public let sessionId: String
|
||||||
|
public let reconnectSequence: Int64
|
||||||
|
public let gateway: ManifestGateway
|
||||||
|
public let tunnel: ManifestTunnel
|
||||||
|
public let profile: ManifestProfile
|
||||||
|
public let grant: GrantReference
|
||||||
|
public let correlationId: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version = "version"
|
||||||
|
case purpose = "purpose"
|
||||||
|
case sessionId = "session_id"
|
||||||
|
case reconnectSequence = "reconnect_sequence"
|
||||||
|
case gateway = "gateway"
|
||||||
|
case tunnel = "tunnel"
|
||||||
|
case profile = "profile"
|
||||||
|
case grant = "grant"
|
||||||
|
case correlationId = "correlation_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try c.decode(String.self, forKey: .version)
|
||||||
|
purpose = try c.decode(String.self, forKey: .purpose)
|
||||||
|
sessionId = try c.decode(String.self, forKey: .sessionId)
|
||||||
|
reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence)
|
||||||
|
gateway = try c.decode(ManifestGateway.self, forKey: .gateway)
|
||||||
|
tunnel = try c.decode(ManifestTunnel.self, forKey: .tunnel)
|
||||||
|
profile = try c.decode(ManifestProfile.self, forKey: .profile)
|
||||||
|
grant = try c.decode(GrantReference.self, forKey: .grant)
|
||||||
|
correlationId = try c.decode(String.self, forKey: .correlationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DeviceChallenge: Codable, Equatable {
|
||||||
|
public let deviceId: String
|
||||||
|
public let serverId: String
|
||||||
|
public let principalId: String
|
||||||
|
public let challenge: String
|
||||||
|
public let expiresAt: String
|
||||||
|
public let algorithm: String
|
||||||
|
public let signatureFormat: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case deviceId = "device_id"
|
||||||
|
case serverId = "server_id"
|
||||||
|
case principalId = "principal_id"
|
||||||
|
case challenge = "challenge"
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
case algorithm = "algorithm"
|
||||||
|
case signatureFormat = "signature_format"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
deviceId = try c.decode(String.self, forKey: .deviceId)
|
||||||
|
serverId = try c.decode(String.self, forKey: .serverId)
|
||||||
|
principalId = try c.decode(String.self, forKey: .principalId)
|
||||||
|
challenge = try c.decode(String.self, forKey: .challenge)
|
||||||
|
expiresAt = try c.decode(String.self, forKey: .expiresAt)
|
||||||
|
algorithm = try c.decode(String.self, forKey: .algorithm)
|
||||||
|
signatureFormat = try c.decode(String.self, forKey: .signatureFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DeviceProofRequest: Codable, Equatable {
|
||||||
|
public let challenge: String
|
||||||
|
public let signature: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case challenge = "challenge"
|
||||||
|
case signature = "signature"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
challenge = try c.decode(String.self, forKey: .challenge)
|
||||||
|
signature = try c.decode(String.self, forKey: .signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DeviceRegistrationRequest: Codable, Equatable {
|
||||||
|
public let name: String
|
||||||
|
public let platform: String
|
||||||
|
public let deviceSubject: String
|
||||||
|
public let algorithm: String
|
||||||
|
public let publicKey: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name = "name"
|
||||||
|
case platform = "platform"
|
||||||
|
case deviceSubject = "device_subject"
|
||||||
|
case algorithm = "algorithm"
|
||||||
|
case publicKey = "public_key"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
name = try c.decode(String.self, forKey: .name)
|
||||||
|
platform = try c.decode(String.self, forKey: .platform)
|
||||||
|
deviceSubject = try c.decode(String.self, forKey: .deviceSubject)
|
||||||
|
algorithm = try c.decode(String.self, forKey: .algorithm)
|
||||||
|
publicKey = try c.decode(String.self, forKey: .publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EntitledPool: Codable, Equatable {
|
||||||
|
public let poolId: String
|
||||||
|
public let name: String
|
||||||
|
public let assignmentState: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case poolId = "pool_id"
|
||||||
|
case name = "name"
|
||||||
|
case assignmentState = "assignment_state"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
poolId = try c.decode(String.self, forKey: .poolId)
|
||||||
|
name = try c.decode(String.self, forKey: .name)
|
||||||
|
assignmentState = try c.decode(String.self, forKey: .assignmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ErrorEnvelope: Codable, Equatable {
|
||||||
|
public let status: Bool
|
||||||
|
public let error: String
|
||||||
|
public let code: String
|
||||||
|
public let message: String
|
||||||
|
public let resolution: String
|
||||||
|
public let requestId: String
|
||||||
|
public let violations: [FieldViolation]
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case status = "status"
|
||||||
|
case error = "error"
|
||||||
|
case code = "code"
|
||||||
|
case message = "message"
|
||||||
|
case resolution = "resolution"
|
||||||
|
case requestId = "request_id"
|
||||||
|
case violations = "violations"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
status = try c.decode(Bool.self, forKey: .status)
|
||||||
|
error = try c.decode(String.self, forKey: .error)
|
||||||
|
code = try c.decode(String.self, forKey: .code)
|
||||||
|
message = try c.decode(String.self, forKey: .message)
|
||||||
|
resolution = try c.decode(String.self, forKey: .resolution)
|
||||||
|
requestId = try c.decode(String.self, forKey: .requestId)
|
||||||
|
violations = try c.decode([FieldViolation].self, forKey: .violations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EventEnvelope: Codable, Equatable {
|
||||||
|
public let eventId: String
|
||||||
|
public let sequence: Int64
|
||||||
|
public let type: String
|
||||||
|
public let version: Int64
|
||||||
|
public let resource: ResourceLink
|
||||||
|
public let occurredAt: String
|
||||||
|
public let correlationId: String
|
||||||
|
public let payload: JSONObject
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case eventId = "event_id"
|
||||||
|
case sequence = "sequence"
|
||||||
|
case type = "type"
|
||||||
|
case version = "version"
|
||||||
|
case resource = "resource"
|
||||||
|
case occurredAt = "occurred_at"
|
||||||
|
case correlationId = "correlation_id"
|
||||||
|
case payload = "payload"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
eventId = try c.decode(String.self, forKey: .eventId)
|
||||||
|
sequence = try c.decode(Int64.self, forKey: .sequence)
|
||||||
|
type = try c.decode(String.self, forKey: .type)
|
||||||
|
version = try c.decode(Int64.self, forKey: .version)
|
||||||
|
resource = try c.decode(ResourceLink.self, forKey: .resource)
|
||||||
|
occurredAt = try c.decode(String.self, forKey: .occurredAt)
|
||||||
|
correlationId = try c.decode(String.self, forKey: .correlationId)
|
||||||
|
payload = try c.decode(JSONObject.self, forKey: .payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EventResume: Codable, Equatable {
|
||||||
|
public let cursor: String
|
||||||
|
public let lastSequence: Int64
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case cursor = "cursor"
|
||||||
|
case lastSequence = "last_sequence"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
cursor = try c.decode(String.self, forKey: .cursor)
|
||||||
|
lastSequence = try c.decode(Int64.self, forKey: .lastSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FieldViolation: Codable, Equatable {
|
||||||
|
public let field: String
|
||||||
|
public let code: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case field = "field"
|
||||||
|
case code = "code"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
field = try c.decode(String.self, forKey: .field)
|
||||||
|
code = try c.decode(String.self, forKey: .code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GrantReference: Codable, Equatable {
|
||||||
|
public let opaqueValue: String
|
||||||
|
public let expiresAt: String
|
||||||
|
public let audience: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case opaqueValue = "opaque_value"
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
case audience = "audience"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
opaqueValue = try c.decode(String.self, forKey: .opaqueValue)
|
||||||
|
expiresAt = try c.decode(String.self, forKey: .expiresAt)
|
||||||
|
audience = try c.decode(String.self, forKey: .audience)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct LoginRequest: Codable, Equatable {
|
||||||
|
public let provider: String?
|
||||||
|
public let username: String
|
||||||
|
public let password: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case provider = "provider"
|
||||||
|
case username = "username"
|
||||||
|
case password = "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
provider = try c.decodeIfPresent(String.self, forKey: .provider)
|
||||||
|
username = try c.decode(String.self, forKey: .username)
|
||||||
|
password = try c.decode(String.self, forKey: .password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ManifestBounds: Codable, Equatable {
|
||||||
|
public let minimumKbps: Int64
|
||||||
|
public let targetKbps: Int64
|
||||||
|
public let maximumKbps: Int64
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case minimumKbps = "minimum_kbps"
|
||||||
|
case targetKbps = "target_kbps"
|
||||||
|
case maximumKbps = "maximum_kbps"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
minimumKbps = try c.decode(Int64.self, forKey: .minimumKbps)
|
||||||
|
targetKbps = try c.decode(Int64.self, forKey: .targetKbps)
|
||||||
|
maximumKbps = try c.decode(Int64.self, forKey: .maximumKbps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ManifestGateway: Codable, Equatable {
|
||||||
|
public let id: String
|
||||||
|
public let addresses: [String]
|
||||||
|
public let publicIdentity: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id = "id"
|
||||||
|
case addresses = "addresses"
|
||||||
|
case publicIdentity = "public_identity"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(String.self, forKey: .id)
|
||||||
|
addresses = try c.decode([String].self, forKey: .addresses)
|
||||||
|
publicIdentity = try c.decode(String.self, forKey: .publicIdentity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ManifestProfile: Codable, Equatable {
|
||||||
|
public let id: String
|
||||||
|
public let bounds: ManifestBounds
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id = "id"
|
||||||
|
case bounds = "bounds"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(String.self, forKey: .id)
|
||||||
|
bounds = try c.decode(ManifestBounds.self, forKey: .bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ManifestTunnel: Codable, Equatable {
|
||||||
|
public let versions: [String]
|
||||||
|
public let features: [String]
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case versions = "versions"
|
||||||
|
case features = "features"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
versions = try c.decode([String].self, forKey: .versions)
|
||||||
|
features = try c.decode([String].self, forKey: .features)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NativeCredential: Codable, Equatable {
|
||||||
|
public let deviceId: String?
|
||||||
|
public let familyId: String
|
||||||
|
public let accessToken: String
|
||||||
|
public let refreshToken: String
|
||||||
|
public let expiresAt: String
|
||||||
|
public let refreshExpiresAt: String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case deviceId = "device_id"
|
||||||
|
case familyId = "family_id"
|
||||||
|
case accessToken = "access_token"
|
||||||
|
case refreshToken = "refresh_token"
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
case refreshExpiresAt = "refresh_expires_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
deviceId = try c.decodeIfPresent(String.self, forKey: .deviceId)
|
||||||
|
familyId = try c.decode(String.self, forKey: .familyId)
|
||||||
|
accessToken = try c.decode(String.self, forKey: .accessToken)
|
||||||
|
refreshToken = try c.decode(String.self, forKey: .refreshToken)
|
||||||
|
expiresAt = try c.decode(String.self, forKey: .expiresAt)
|
||||||
|
refreshExpiresAt = try c.decodeIfPresent(String.self, forKey: .refreshExpiresAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PageInfo: Codable, Equatable {
|
||||||
|
public let limit: Int64
|
||||||
|
public let nextCursor: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case limit = "limit"
|
||||||
|
case nextCursor = "next_cursor"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
limit = try c.decode(Int64.self, forKey: .limit)
|
||||||
|
nextCursor = try c.decode(String.self, forKey: .nextCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ReauthGrant: Codable, Equatable {
|
||||||
|
public let token: String
|
||||||
|
public let purpose: String
|
||||||
|
public let expiresAt: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case token = "token"
|
||||||
|
case purpose = "purpose"
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
token = try c.decode(String.self, forKey: .token)
|
||||||
|
purpose = try c.decode(String.self, forKey: .purpose)
|
||||||
|
expiresAt = try c.decode(String.self, forKey: .expiresAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ReauthRequest: Codable, Equatable {
|
||||||
|
public let password: String
|
||||||
|
public let purpose: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case password = "password"
|
||||||
|
case purpose = "purpose"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
password = try c.decode(String.self, forKey: .password)
|
||||||
|
purpose = try c.decode(String.self, forKey: .purpose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ReconnectRequest: Codable, Equatable {
|
||||||
|
public let clientDeviceId: String
|
||||||
|
public let deviceKeyId: String
|
||||||
|
public let expectedVersion: Int64
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case clientDeviceId = "client_device_id"
|
||||||
|
case deviceKeyId = "device_key_id"
|
||||||
|
case expectedVersion = "expected_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
clientDeviceId = try c.decode(String.self, forKey: .clientDeviceId)
|
||||||
|
deviceKeyId = try c.decode(String.self, forKey: .deviceKeyId)
|
||||||
|
expectedVersion = try c.decode(Int64.self, forKey: .expectedVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct RefreshRequest: Codable, Equatable {
|
||||||
|
public let familyId: String
|
||||||
|
public let refreshToken: String
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case familyId = "family_id"
|
||||||
|
case refreshToken = "refresh_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
familyId = try c.decode(String.self, forKey: .familyId)
|
||||||
|
refreshToken = try c.decode(String.self, forKey: .refreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Resource: Codable, Equatable {
|
||||||
|
public let id: String
|
||||||
|
public let kind: String
|
||||||
|
public let name: String
|
||||||
|
public let state: String
|
||||||
|
public let assignmentState: String?
|
||||||
|
public let version: Int64
|
||||||
|
public let links: [ResourceLink]
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id = "id"
|
||||||
|
case kind = "kind"
|
||||||
|
case name = "name"
|
||||||
|
case state = "state"
|
||||||
|
case assignmentState = "assignment_state"
|
||||||
|
case version = "version"
|
||||||
|
case links = "links"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try c.decode(String.self, forKey: .id)
|
||||||
|
kind = try c.decode(String.self, forKey: .kind)
|
||||||
|
name = try c.decode(String.self, forKey: .name)
|
||||||
|
state = try c.decode(String.self, forKey: .state)
|
||||||
|
assignmentState = try c.decodeIfPresent(String.self, forKey: .assignmentState)
|
||||||
|
version = try c.decode(Int64.self, forKey: .version)
|
||||||
|
links = try c.decode([ResourceLink].self, forKey: .links)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ResourceLink: Codable, Equatable {
|
||||||
|
public let type: String
|
||||||
|
public let id: String
|
||||||
|
public let version: Int64
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case type = "type"
|
||||||
|
case id = "id"
|
||||||
|
case version = "version"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
type = try c.decode(String.self, forKey: .type)
|
||||||
|
id = try c.decode(String.self, forKey: .id)
|
||||||
|
version = try c.decode(Int64.self, forKey: .version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ResourceList: Codable, Equatable {
|
||||||
|
public let assignedDesktops: [AssignedDesktop]
|
||||||
|
public let entitledPools: [EntitledPool]
|
||||||
|
public let page: PageInfo
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case assignedDesktops = "assigned_desktops"
|
||||||
|
case entitledPools = "entitled_pools"
|
||||||
|
case page = "page"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
assignedDesktops = try c.decode([AssignedDesktop].self, forKey: .assignedDesktops)
|
||||||
|
entitledPools = try c.decode([EntitledPool].self, forKey: .entitledPools)
|
||||||
|
page = try c.decode(PageInfo.self, forKey: .page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SessionRequest: Codable, Equatable {
|
||||||
|
public let clientDeviceId: String
|
||||||
|
public let deviceKeyId: String
|
||||||
|
public let poolId: String
|
||||||
|
public let idempotencyKey: String
|
||||||
|
public let policySnapshot: AllocationPolicy
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case clientDeviceId = "client_device_id"
|
||||||
|
case deviceKeyId = "device_key_id"
|
||||||
|
case poolId = "pool_id"
|
||||||
|
case idempotencyKey = "idempotency_key"
|
||||||
|
case policySnapshot = "policy_snapshot"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
clientDeviceId = try c.decode(String.self, forKey: .clientDeviceId)
|
||||||
|
deviceKeyId = try c.decode(String.self, forKey: .deviceKeyId)
|
||||||
|
poolId = try c.decode(String.self, forKey: .poolId)
|
||||||
|
idempotencyKey = try c.decode(String.self, forKey: .idempotencyKey)
|
||||||
|
policySnapshot = try c.decode(AllocationPolicy.self, forKey: .policySnapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct VersionNegotiation: Codable, Equatable {
|
||||||
|
public let supportedVersions: [String]
|
||||||
|
public let features: [String]
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case supportedVersions = "supported_versions"
|
||||||
|
case features = "features"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
supportedVersions = try c.decode([String].self, forKey: .supportedVersions)
|
||||||
|
features = try c.decode([String].self, forKey: .features)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package protocol_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
protocol "github.com/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
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-1"},"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"}`
|
||||||
|
manifest, err := protocol.DecodeConnectionManifest([]byte(valid))
|
||||||
|
if err != nil || manifest.Gateway.ID != "gateway-1" {
|
||||||
|
t.Fatalf("valid manifest = %+v, err = %v", manifest, err)
|
||||||
|
}
|
||||||
|
for _, field := range []string{"provider_url", "vm_address", "password"} {
|
||||||
|
payload := strings.Replace(valid, `"correlation_id":"correlation-1"`, `"correlation_id":"correlation-1","`+field+`":"forbidden"`, 1)
|
||||||
|
if _, err := protocol.DecodeConnectionManifest([]byte(payload)); err == nil {
|
||||||
|
t.Fatalf("DecodeConnectionManifest accepted forbidden field %q", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageInfoRejectsOutOfBoundsLimit(t *testing.T) {
|
||||||
|
if _, err := protocol.DecodePageInfo([]byte(`{"limit":101,"next_cursor":""}`)); err == nil {
|
||||||
|
t.Fatal("DecodePageInfo accepted limit above the contract maximum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testing.T) {
|
||||||
|
if _, err := protocol.DecodeErrorEnvelope([]byte(`{"status":false,"error":"safe","code":"invalid_request","message":"safe","resolution":"retry","violations":[]} {}`)); err == nil {
|
||||||
|
t.Fatal("DecodeErrorEnvelope accepted a trailing JSON value")
|
||||||
|
}
|
||||||
|
if _, err := protocol.DecodeErrorEnvelope([]byte(`{"error":"safe","code":"invalid_request","message":"safe","resolution":"retry","request_id":"req-1","violations":[]}`)); err == nil {
|
||||||
|
t.Fatal("DecodeErrorEnvelope accepted a missing required boolean")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check the content-addressed Phase 3A conformance corpus."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
MANIFEST = ROOT / "fixtures/manifest.json"
|
||||||
|
|
||||||
|
|
||||||
|
def digest(paths: list[str]) -> str:
|
||||||
|
value = hashlib.sha256()
|
||||||
|
for relative in paths:
|
||||||
|
path = ROOT / relative
|
||||||
|
value.update(relative.encode("utf-8"))
|
||||||
|
value.update(b"\0")
|
||||||
|
value.update(path.read_bytes())
|
||||||
|
value.update(b"\0")
|
||||||
|
return value.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||||
|
paths = sorted(path.relative_to(ROOT).as_posix() for path in (ROOT / "fixtures/conformance").glob("*.tsv"))
|
||||||
|
if paths != manifest.get("files"):
|
||||||
|
raise ValueError("fixture manifest file list is stale")
|
||||||
|
actual = digest(paths)
|
||||||
|
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}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||||
|
print(f"fixture_digest: {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministically generate the small language-neutral Phase 3A bindings."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
SCHEMA_PATH = ROOT / "schemas/control-v1.schema.json"
|
||||||
|
VERSION_PATH = ROOT / "VERSION"
|
||||||
|
COMPATIBILITY_PATH = ROOT / "compatibility.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema() -> tuple[dict[str, Any], str, str, dict[str, Any]]:
|
||||||
|
raw = SCHEMA_PATH.read_bytes()
|
||||||
|
schema = json.loads(raw)
|
||||||
|
version = VERSION_PATH.read_text(encoding="utf-8").strip()
|
||||||
|
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
|
||||||
|
raise ValueError("VERSION must be semantic version text")
|
||||||
|
compatibility = json.loads(COMPATIBILITY_PATH.read_text(encoding="utf-8"))
|
||||||
|
for key in ("current", "n_minus_1", "n_minus_2"):
|
||||||
|
if not isinstance(compatibility.get(key), str):
|
||||||
|
raise ValueError("compatibility declaration is incomplete")
|
||||||
|
return schema, hashlib.sha256(raw).hexdigest(), version, compatibility
|
||||||
|
|
||||||
|
|
||||||
|
def pascal(name: str) -> str:
|
||||||
|
return "".join(part[:1].upper() + part[1:] for part in re.split(r"[_-]", name))
|
||||||
|
|
||||||
|
|
||||||
|
def go_field(name: str) -> str:
|
||||||
|
initialisms = {"api": "API", "http": "HTTP", "id": "ID", "ip": "IP", "sha": "SHA", "ttl": "TTL", "url": "URL", "uuid": "UUID"}
|
||||||
|
return "".join(
|
||||||
|
initialisms.get(part, part[:1].upper() + part[1:])
|
||||||
|
for part in re.split(r"[_-]", name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def camel(name: str) -> str:
|
||||||
|
parts = re.split(r"[_-]", name)
|
||||||
|
return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def swift_field(name: str) -> str:
|
||||||
|
value = camel(name)
|
||||||
|
if value in {"protocol", "class", "struct", "enum", "extension", "private", "public", "internal", "fileprivate", "open", "func", "let", "var", "import", "switch", "case", "default", "operator", "where", "repeat", "return", "throw", "throws", "try", "catch", "defer", "in", "is", "as"}:
|
||||||
|
return value + "Value"
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
RUST_KEYWORDS = {
|
||||||
|
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
|
||||||
|
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
|
||||||
|
"move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
|
||||||
|
"super", "trait", "true", "type", "unsafe", "use", "where", "while", "async",
|
||||||
|
"await", "dyn", "abstract", "become", "box", "do", "final", "macro", "override",
|
||||||
|
"priv", "typeof", "unsized", "virtual", "yield", "try", "union",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rust_field(name: str) -> str:
|
||||||
|
value = camel(name)
|
||||||
|
return value + "Value" if value in RUST_KEYWORDS else value
|
||||||
|
|
||||||
|
|
||||||
|
def ref_name(value: Any) -> str | None:
|
||||||
|
if isinstance(value, dict) and isinstance(value.get("$ref"), str):
|
||||||
|
return value["$ref"].split("/")[-1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def prop_type(prop: dict[str, Any], language: str) -> str:
|
||||||
|
reference = ref_name(prop)
|
||||||
|
if reference:
|
||||||
|
return reference if language == "go" else (reference if language == "rust" else reference)
|
||||||
|
if prop.get("type") == "array":
|
||||||
|
item = prop.get("items", {})
|
||||||
|
item_type = prop_type(item, language)
|
||||||
|
if language == "go":
|
||||||
|
return f"[]{item_type}"
|
||||||
|
if language == "rust":
|
||||||
|
return f"Vec<{item_type}>"
|
||||||
|
return f"[{item_type}]"
|
||||||
|
if prop.get("type") == "integer":
|
||||||
|
return "int64" if language == "go" else ("i64" if language == "rust" else "Int64")
|
||||||
|
if prop.get("type") == "boolean":
|
||||||
|
return "bool" if language != "swift" else "Bool"
|
||||||
|
if prop.get("type") == "object":
|
||||||
|
return "map[string]any" if language == "go" else ("JsonObject" if language == "rust" else "JSONObject")
|
||||||
|
return "string" if language == "go" else ("String" if language == "rust" else "String")
|
||||||
|
|
||||||
|
|
||||||
|
def go_zero(prop: dict[str, Any], field: str) -> str:
|
||||||
|
typ = prop_type(prop, "go")
|
||||||
|
if typ in {"string", "int64", "bool"}:
|
||||||
|
zero = {"string": '""', "int64": "0", "bool": "false"}[typ]
|
||||||
|
return f"v.{field} == {zero}"
|
||||||
|
if typ.startswith("[]"):
|
||||||
|
return f"v.{field} == nil"
|
||||||
|
if typ == "map[string]any":
|
||||||
|
return f"v.{field} == nil"
|
||||||
|
return f"reflect.DeepEqual(v.{field}, {typ}{{}})"
|
||||||
|
|
||||||
|
|
||||||
|
def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||||
|
lines: list[str] = []
|
||||||
|
name = definition["name"]
|
||||||
|
required = set(definition.get("required", []))
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = go_field(prop_name)
|
||||||
|
required_zero_is_valid = prop.get("type") == "integer" and prop.get("minimum") == 0
|
||||||
|
if prop_name in required and prop.get("type") != "boolean" and not required_zero_is_valid:
|
||||||
|
zero = go_zero(prop, field)
|
||||||
|
lines.append(f"\tif {zero} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"required\"}}) }}")
|
||||||
|
if prop.get("type") == "string":
|
||||||
|
if "minLength" in prop:
|
||||||
|
lines.append(f"\tif len(v.{field}) < {prop['minLength']} && v.{field} != \"\" {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_length\"}}) }}")
|
||||||
|
if "maxLength" in prop:
|
||||||
|
lines.append(f"\tif len(v.{field}) > {prop['maxLength']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_length\"}}) }}")
|
||||||
|
if "const" in prop:
|
||||||
|
lines.append(f"\tif v.{field} != \"{prop['const']}\" && v.{field} != \"\" {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_value\"}}) }}")
|
||||||
|
if "enum" in prop:
|
||||||
|
allowed = " || ".join(f'v.{field} == "{value}"' for value in prop["enum"])
|
||||||
|
lines.append(f"\tif v.{field} != \"\" && !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_value\"}}) }}")
|
||||||
|
if prop.get("format") == "date-time":
|
||||||
|
lines.append(
|
||||||
|
'\tif v.%s != "" { if parsed, err := time.Parse(time.RFC3339Nano, v.%s); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.%s { violations = append(violations, FieldViolation{Field: "%s", Code: "invalid_time"}) } }'
|
||||||
|
% (field, field, field, prop_name)
|
||||||
|
)
|
||||||
|
if prop.get("type") == "integer":
|
||||||
|
if "minimum" in prop:
|
||||||
|
lines.append(f"\tif v.{field} != 0 && v.{field} < {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\"}}) }}")
|
||||||
|
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\"}}) }}")
|
||||||
|
if "maxItems" in prop:
|
||||||
|
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
|
||||||
|
item_ref = ref_name(prop.get("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\"}}) }} }}")
|
||||||
|
reference = ref_name(prop)
|
||||||
|
if reference:
|
||||||
|
lines.append(f"\tif err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str, compatibility: dict[str, Any]) -> str:
|
||||||
|
out = [
|
||||||
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
||||||
|
"package protocol",
|
||||||
|
"",
|
||||||
|
"import (",
|
||||||
|
"\"bytes\"",
|
||||||
|
"\"encoding/json\"",
|
||||||
|
"\"errors\"",
|
||||||
|
"\"fmt\"",
|
||||||
|
"\"reflect\"",
|
||||||
|
"\"time\"",
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
f'const SchemaSHA256 = "{schema_hash}"',
|
||||||
|
f'const ProtocolVersion = "{version}"',
|
||||||
|
f'const CurrentWireVersion = "{compatibility["current"]}"',
|
||||||
|
f'const NMinus1WireVersion = "{compatibility["n_minus_1"]}"',
|
||||||
|
f'const NMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
||||||
|
"",
|
||||||
|
"type FieldViolation struct {",
|
||||||
|
"\tField string `json:\"field\"`",
|
||||||
|
"\tCode string `json:\"code\"`",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"type ValidationError struct {",
|
||||||
|
"\tViolations []FieldViolation",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"func (e ValidationError) Error() string { return \"protocol validation failed\" }",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for name in sorted(defs):
|
||||||
|
if name == "FieldViolation":
|
||||||
|
continue
|
||||||
|
definition = defs[name]
|
||||||
|
out.append(f"type {name} struct {{")
|
||||||
|
required = set(definition.get("required", []))
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
tag = prop_name + (",omitempty" if prop_name not in required else "")
|
||||||
|
out.append(f"\t{go_field(prop_name)} {prop_type(prop, 'go')} `json:\"{tag}\"`")
|
||||||
|
out.extend(["}", ""])
|
||||||
|
for name in sorted(defs):
|
||||||
|
out.append(f"func (v {name}) Validate() error {{")
|
||||||
|
out.append("\tvar violations []FieldViolation")
|
||||||
|
out.extend(go_validation(defs[name]))
|
||||||
|
out.append("\tif len(violations) > 0 { return ValidationError{Violations: violations} }")
|
||||||
|
out.append("\treturn nil")
|
||||||
|
out.append("}")
|
||||||
|
out.append("")
|
||||||
|
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("\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", []))
|
||||||
|
for prop_name in required_fields:
|
||||||
|
out.append(
|
||||||
|
'\tif raw, ok := fields["%s"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "required"}}} }'
|
||||||
|
% (prop_name, prop_name)
|
||||||
|
)
|
||||||
|
for prop_name, prop in defs[name].get("properties", {}).items():
|
||||||
|
if "x-max-bytes" in prop:
|
||||||
|
out.append(
|
||||||
|
'\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }'
|
||||||
|
% (prop_name, prop["x-max-bytes"], prop_name)
|
||||||
|
)
|
||||||
|
out.append("\tdecoder := json.NewDecoder(bytes.NewReader(data))")
|
||||||
|
out.append("\tdecoder.DisallowUnknownFields()")
|
||||||
|
out.append("\tif err := decoder.Decode(&value); err != nil { return value, err }")
|
||||||
|
out.append("\tvar trailing any")
|
||||||
|
out.append("\tif err := decoder.Decode(&trailing); err != io.EOF { if err == nil { return value, errors.New(\"trailing JSON value\") }; return value, err }")
|
||||||
|
out.append("\tif err := value.Validate(); err != nil { return value, err }")
|
||||||
|
out.append("\treturn value, nil")
|
||||||
|
out.append("}")
|
||||||
|
out.append("")
|
||||||
|
out.append(f"func Encode{name}(value {name}) ([]byte, error) {{")
|
||||||
|
out.append("\tif err := value.Validate(); err != nil { return nil, err }")
|
||||||
|
out.append("\treturn json.Marshal(value)")
|
||||||
|
out.append("}")
|
||||||
|
out.append("")
|
||||||
|
# Use io.EOF in generated code without making every generated decoder depend on
|
||||||
|
# error-string comparison; replace the deliberately compact placeholder.
|
||||||
|
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
|
||||||
|
text = text.replace('!errors.Is(err, errors.New("EOF")) && err != nil', '!errors.Is(err, io.EOF)')
|
||||||
|
return text + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def rust_type(prop: dict[str, Any]) -> str:
|
||||||
|
reference = ref_name(prop)
|
||||||
|
if reference:
|
||||||
|
return reference
|
||||||
|
if prop.get("type") == "array":
|
||||||
|
return f"Vec<{rust_type(prop.get('items', {}))}>"
|
||||||
|
if prop.get("type") == "integer":
|
||||||
|
return "i64"
|
||||||
|
if prop.get("type") == "boolean":
|
||||||
|
return "bool"
|
||||||
|
if prop.get("type") == "object":
|
||||||
|
return "JsonObject"
|
||||||
|
return "String"
|
||||||
|
|
||||||
|
|
||||||
|
def swift_type(prop: dict[str, Any]) -> str:
|
||||||
|
reference = ref_name(prop)
|
||||||
|
if reference:
|
||||||
|
return reference
|
||||||
|
if prop.get("type") == "array":
|
||||||
|
return f"[{swift_type(prop.get('items', {}))}]"
|
||||||
|
if prop.get("type") == "integer":
|
||||||
|
return "Int64"
|
||||||
|
if prop.get("type") == "boolean":
|
||||||
|
return "Bool"
|
||||||
|
if prop.get("type") == "object":
|
||||||
|
return "JSONObject"
|
||||||
|
return "String"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
||||||
|
out = [
|
||||||
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
||||||
|
"#![allow(non_snake_case)]",
|
||||||
|
"pub const SCHEMA_SHA256: &str = \"" + schema_hash + "\";",
|
||||||
|
f'pub const CURRENT_WIRE_VERSION: &str = "{compatibility["current"]}";',
|
||||||
|
f'pub const N_MINUS_1_WIRE_VERSION: &str = "{compatibility["n_minus_1"]}";',
|
||||||
|
f'pub const N_MINUS_2_WIRE_VERSION: &str = "{compatibility["n_minus_2"]}";',
|
||||||
|
"pub type JsonObject = std::collections::BTreeMap<String, String>;",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for name in sorted(defs):
|
||||||
|
definition = defs[name]
|
||||||
|
out.extend(["#[derive(Debug, Clone, PartialEq, Eq)]", f"pub struct {name} {{"])
|
||||||
|
required = set(definition.get("required", []))
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = rust_field(prop_name)
|
||||||
|
typ = rust_type(prop)
|
||||||
|
if prop_name not in required:
|
||||||
|
typ = f"Option<{typ}>"
|
||||||
|
out.append(f" pub {field}: {typ},")
|
||||||
|
out.extend(["}", ""])
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibility: dict[str, Any]) -> str:
|
||||||
|
out = [
|
||||||
|
"// Code generated by tools/generate.py; DO NOT EDIT.",
|
||||||
|
"import Foundation",
|
||||||
|
"public typealias JSONObject = [String: String]",
|
||||||
|
f"public let schemaSHA256 = \"{schema_hash}\"",
|
||||||
|
f'public let currentWireVersion = "{compatibility["current"]}"',
|
||||||
|
f'public let nMinus1WireVersion = "{compatibility["n_minus_1"]}"',
|
||||||
|
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for name in sorted(defs):
|
||||||
|
definition = defs[name]
|
||||||
|
required = set(definition.get("required", []))
|
||||||
|
out.extend(["public struct " + name + ": Codable, Equatable {",])
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
typ = swift_type(prop)
|
||||||
|
if prop_name not in required:
|
||||||
|
typ += "?"
|
||||||
|
out.append(f" public let {swift_field(prop_name)}: {typ}")
|
||||||
|
out.append(" enum CodingKeys: String, CodingKey {")
|
||||||
|
for prop_name in definition.get("properties", {}):
|
||||||
|
out.append(f" case {swift_field(prop_name)} = \"{prop_name}\"")
|
||||||
|
out.extend([" }", "", " public init(from decoder: Decoder) throws {",])
|
||||||
|
out.append(" let c = try decoder.container(keyedBy: CodingKeys.self)")
|
||||||
|
for prop_name, prop in definition.get("properties", {}).items():
|
||||||
|
field = swift_field(prop_name)
|
||||||
|
typ = swift_type(prop)
|
||||||
|
if prop_name in required:
|
||||||
|
out.append(f" {field} = try c.decode({typ}.self, forKey: .{field})")
|
||||||
|
else:
|
||||||
|
out.append(f" {field} = try c.decodeIfPresent({typ}.self, forKey: .{field})")
|
||||||
|
out.extend([" }", "}", ""])
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def write_or_check(path: pathlib.Path, content: str, check: bool) -> None:
|
||||||
|
if check:
|
||||||
|
if not path.exists() or path.read_text(encoding="utf-8") != content:
|
||||||
|
raise ValueError(f"generated output differs: {path.relative_to(ROOT)}")
|
||||||
|
return
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def format_go(content: str) -> str:
|
||||||
|
result = subprocess.run(["gofmt"], input=content, text=True, capture_output=True, check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise ValueError(f"gofmt failed: {result.stderr.strip()}")
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--check", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
schema, schema_hash, version, compatibility = load_schema()
|
||||||
|
generator_hash = hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest()
|
||||||
|
defs = schema.get("$defs")
|
||||||
|
if not isinstance(defs, dict) or not defs:
|
||||||
|
raise ValueError("schema must contain non-empty $defs")
|
||||||
|
normalized = {name: dict(value, name=name) for name, value in defs.items()}
|
||||||
|
outputs = {
|
||||||
|
ROOT / "gen/go/protocol/protocol.go": format_go(generate_go(normalized, schema_hash, version, compatibility)),
|
||||||
|
ROOT / "gen/rust/protocol.rs": generate_rust(normalized, schema_hash, compatibility).rstrip() + "\n",
|
||||||
|
ROOT / "gen/swift/Protocol.swift": generate_swift(normalized, schema_hash, compatibility).rstrip() + "\n",
|
||||||
|
ROOT / "gen/manifest.json": json.dumps({
|
||||||
|
"generator_sha256": generator_hash,
|
||||||
|
"schema_sha256": schema_hash,
|
||||||
|
"protocol_version": version,
|
||||||
|
"compatibility": compatibility,
|
||||||
|
}, sort_keys=True, indent=2) + "\n",
|
||||||
|
}
|
||||||
|
for path, content in outputs.items():
|
||||||
|
write_or_check(path, content, args.check)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||||
|
print(f"generate: {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
protocol "github.com/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
datagramHeaderBytes = 21
|
||||||
|
maximumFrameBytes = 65536
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
entries, err := os.ReadDir("fixtures/conformance")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var results []string
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || filepath.Ext(entry.Name()) != ".tsv" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join("fixtures/conformance", entry.Name())
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
|
||||||
|
if len(lines) == 0 || lines[0] != "id\tversion\tkind\tinput\texpected" {
|
||||||
|
panic("invalid fixture header")
|
||||||
|
}
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
fields := strings.Split(line, "\t")
|
||||||
|
if len(fields) != 5 {
|
||||||
|
panic("invalid fixture row")
|
||||||
|
}
|
||||||
|
actual := evaluate(fields[2], fields[3])
|
||||||
|
if actual != fields[4] {
|
||||||
|
panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4]))
|
||||||
|
}
|
||||||
|
results = append(results, fields[0]+"\t"+actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fixtureHash := readFixtureHash()
|
||||||
|
fmt.Printf("Go conformance passed normalized=%s fixtures=%s\n", normalizedDigest(results), fixtureHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(kind, input string) string {
|
||||||
|
parts := map[string]string{}
|
||||||
|
for _, item := range strings.Split(input, ";") {
|
||||||
|
pair := strings.SplitN(item, "=", 2)
|
||||||
|
if len(pair) == 2 {
|
||||||
|
parts[pair[0]] = pair[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch kind {
|
||||||
|
case "version":
|
||||||
|
if input == "1" || input == "0" || input == "-1" {
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
return "invalid:unsupported_version"
|
||||||
|
case "page":
|
||||||
|
limit, err := strconv.Atoi(parts["limit"])
|
||||||
|
value := protocol.PageInfo{Limit: int64(limit), NextCursor: parts["cursor"]}
|
||||||
|
if err == nil && value.Validate() == nil {
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
return "invalid:invalid_limit"
|
||||||
|
case "manifest":
|
||||||
|
forbidden := []string{"provider_url", "vm_address", "password", "private_key"}
|
||||||
|
for _, key := range forbidden {
|
||||||
|
if _, ok := parts[key]; ok {
|
||||||
|
return "invalid:forbidden_field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := protocol.ConnectionManifest{
|
||||||
|
Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1",
|
||||||
|
ReconnectSequence: 0,
|
||||||
|
Gateway: protocol.ManifestGateway{
|
||||||
|
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["gateway_id"],
|
||||||
|
},
|
||||||
|
Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}},
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
if value.Validate() == nil {
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
return "invalid:invalid_manifest"
|
||||||
|
case "clipboard":
|
||||||
|
_, hasFile := parts["file"]
|
||||||
|
value := protocol.ClipboardText{Text: parts["text"], Encoding: parts["encoding"]}
|
||||||
|
if !hasFile && value.Validate() == nil {
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
return "invalid:unsupported_clipboard"
|
||||||
|
case "event":
|
||||||
|
sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64)
|
||||||
|
payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"])
|
||||||
|
if parts["version"] != "1" {
|
||||||
|
return "invalid:unsupported_version"
|
||||||
|
}
|
||||||
|
if parts["after"] != "" && parts["earliest"] != "" {
|
||||||
|
after, afterErr := strconv.ParseInt(parts["after"], 10, 64)
|
||||||
|
earliest, earliestErr := strconv.ParseInt(parts["earliest"], 10, 64)
|
||||||
|
if afterErr == nil && earliestErr == nil && after > 0 && earliest > 0 && after < earliest-1 {
|
||||||
|
return "invalid:gap"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := protocol.EventEnvelope{
|
||||||
|
EventID: "event-1", Sequence: sequence, Type: "broker.session.changed", Version: 1,
|
||||||
|
Resource: protocol.ResourceLink{Type: "broker_session", ID: "session-1", Version: 1},
|
||||||
|
OccurredAt: "2099-01-01T00:00:00Z", CorrelationID: parts["correlation_id"], Payload: map[string]any{},
|
||||||
|
}
|
||||||
|
if payloadErr != nil || payloadBytes > 16384 {
|
||||||
|
return "invalid:payload_limit"
|
||||||
|
}
|
||||||
|
if sequenceErr != nil || value.Validate() != nil {
|
||||||
|
return "invalid:required"
|
||||||
|
}
|
||||||
|
return "valid"
|
||||||
|
case "tunnel":
|
||||||
|
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && parts["feature"] == "control.v1" {
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
if parts["feature"] != "control.v1" {
|
||||||
|
return "invalid:unsupported_feature"
|
||||||
|
}
|
||||||
|
return "invalid:unsupported_version"
|
||||||
|
case "datagram":
|
||||||
|
return classifyDatagram(parts["hex"])
|
||||||
|
default:
|
||||||
|
return "invalid:unknown_kind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyDatagram(encoded string) string {
|
||||||
|
raw, err := hex.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return "invalid:hex"
|
||||||
|
}
|
||||||
|
if len(raw) < datagramHeaderBytes {
|
||||||
|
return "invalid:truncated"
|
||||||
|
}
|
||||||
|
if string(raw[:2]) != "VD" {
|
||||||
|
return "invalid:magic"
|
||||||
|
}
|
||||||
|
if raw[2] != 1 {
|
||||||
|
return "invalid:unsupported_version"
|
||||||
|
}
|
||||||
|
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515}
|
||||||
|
limit, ok := limits[raw[3]]
|
||||||
|
if !ok {
|
||||||
|
return "invalid:unknown_channel"
|
||||||
|
}
|
||||||
|
if raw[4] != 0 {
|
||||||
|
return "invalid:flags"
|
||||||
|
}
|
||||||
|
if raw[18] == 0 || raw[17] >= raw[18] {
|
||||||
|
return "invalid:fragment"
|
||||||
|
}
|
||||||
|
payloadLength := int(raw[19])<<8 | int(raw[20])
|
||||||
|
if payloadLength > limit {
|
||||||
|
return "invalid:payload_limit"
|
||||||
|
}
|
||||||
|
if len(raw) != datagramHeaderBytes+payloadLength {
|
||||||
|
return "invalid:length_mismatch"
|
||||||
|
}
|
||||||
|
if len(raw) > maximumFrameBytes {
|
||||||
|
return "invalid:frame_limit"
|
||||||
|
}
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedDigest(results []string) string {
|
||||||
|
const offset = uint64(14695981039346656037)
|
||||||
|
const prime = uint64(1099511628211)
|
||||||
|
value := offset
|
||||||
|
for _, result := range results {
|
||||||
|
for _, byteValue := range []byte(result + "\n") {
|
||||||
|
value ^= uint64(byteValue)
|
||||||
|
value *= prime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%016x", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFixtureHash() string {
|
||||||
|
data, err := os.ReadFile("fixtures/manifest.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var manifest struct {
|
||||||
|
CorpusSHA256 string `json:"corpus_sha256"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &manifest); err != nil || len(manifest.CorpusSHA256) != sha256.Size*2 {
|
||||||
|
panic("invalid fixture manifest")
|
||||||
|
}
|
||||||
|
return manifest.CorpusSHA256
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn values(input: &str) -> std::collections::BTreeMap<String, String> {
|
||||||
|
input
|
||||||
|
.split(';')
|
||||||
|
.filter_map(|item| item.split_once('='))
|
||||||
|
.map(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||||
|
let values = values(input);
|
||||||
|
match kind {
|
||||||
|
"version" if matches!(input, "1" | "0" | "-1") => "valid",
|
||||||
|
"version" => "invalid:unsupported_version",
|
||||||
|
"page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) {
|
||||||
|
Some(limit) if (1..=100).contains(&limit) => "valid",
|
||||||
|
_ => "invalid:invalid_limit",
|
||||||
|
},
|
||||||
|
"manifest" if ["provider_url", "vm_address", "password", "private_key"]
|
||||||
|
.iter()
|
||||||
|
.any(|key| values.contains_key(*key)) => "invalid:forbidden_field",
|
||||||
|
"manifest"
|
||||||
|
if values.get("version").map(String::as_str) == Some("1")
|
||||||
|
&& values.contains_key("gateway_id")
|
||||||
|
&& values.get("grant").map_or(false, |value| value.len() >= 43)
|
||||||
|
&& values.get("purpose").map(String::as_str) == Some("launch") => "valid",
|
||||||
|
"manifest" => "invalid:invalid_manifest",
|
||||||
|
"clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8")
|
||||||
|
&& !values.contains_key("file") => "valid",
|
||||||
|
"clipboard" => "invalid:unsupported_clipboard",
|
||||||
|
"event" if values.get("version").map(String::as_str) != Some("1") => {
|
||||||
|
"invalid:unsupported_version"
|
||||||
|
}
|
||||||
|
"event" if values.get("after").and_then(|value| value.parse::<i64>().ok()).is_some()
|
||||||
|
&& values.get("earliest").and_then(|value| value.parse::<i64>().ok()).is_some()
|
||||||
|
&& values["after"].parse::<i64>().unwrap() > 0
|
||||||
|
&& values["earliest"].parse::<i64>().unwrap() > 0
|
||||||
|
&& values["after"].parse::<i64>().unwrap() < values["earliest"].parse::<i64>().unwrap() - 1 => {
|
||||||
|
"invalid:gap"
|
||||||
|
}
|
||||||
|
"event" if values.get("payload_bytes").and_then(|value| value.parse::<usize>().ok()).map_or(true, |size| size > 16384) => {
|
||||||
|
"invalid:payload_limit"
|
||||||
|
}
|
||||||
|
"event" if values.get("sequence").and_then(|value| value.parse::<i64>().ok()).map_or(true, |sequence| sequence < 1)
|
||||||
|
|| !values.contains_key("correlation_id") => "invalid:required",
|
||||||
|
"event" => "valid",
|
||||||
|
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1"))
|
||||||
|
&& values.get("feature").map(String::as_str) == Some("control.v1") => "valid",
|
||||||
|
"tunnel" if values.get("feature").map(String::as_str) != Some("control.v1") => {
|
||||||
|
"invalid:unsupported_feature"
|
||||||
|
}
|
||||||
|
"tunnel" => "invalid:unsupported_version",
|
||||||
|
"datagram" => classify_datagram(values.get("hex").map(String::as_str).unwrap_or_default()),
|
||||||
|
_ => "invalid:unknown_kind",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_hex(input: &str) -> Option<Vec<u8>> {
|
||||||
|
if input.len() % 2 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(0..input.len())
|
||||||
|
.step_by(2)
|
||||||
|
.map(|index| u8::from_str_radix(&input[index..index + 2], 16).ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_datagram(encoded: &str) -> &'static str {
|
||||||
|
let raw = match decode_hex(encoded) {
|
||||||
|
Some(raw) => raw,
|
||||||
|
None => return "invalid:hex",
|
||||||
|
};
|
||||||
|
if raw.len() < 21 {
|
||||||
|
return "invalid:truncated";
|
||||||
|
}
|
||||||
|
if raw[0..2] != *b"VD" {
|
||||||
|
return "invalid:magic";
|
||||||
|
}
|
||||||
|
if raw[2] != 1 {
|
||||||
|
return "invalid:unsupported_version";
|
||||||
|
}
|
||||||
|
let limit = match raw[3] {
|
||||||
|
1 => 1024,
|
||||||
|
2 => 2048,
|
||||||
|
3 => 65515,
|
||||||
|
_ => return "invalid:unknown_channel",
|
||||||
|
};
|
||||||
|
if raw[4] != 0 {
|
||||||
|
return "invalid:flags";
|
||||||
|
}
|
||||||
|
if raw[18] == 0 || raw[17] >= raw[18] {
|
||||||
|
return "invalid:fragment";
|
||||||
|
}
|
||||||
|
let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize;
|
||||||
|
if payload_length > limit {
|
||||||
|
return "invalid:payload_limit";
|
||||||
|
}
|
||||||
|
if raw.len() != 21 + payload_length {
|
||||||
|
return "invalid:length_mismatch";
|
||||||
|
}
|
||||||
|
if raw.len() > 65536 {
|
||||||
|
return "invalid:frame_limit";
|
||||||
|
}
|
||||||
|
"valid"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalized_digest(results: &[String]) -> String {
|
||||||
|
let mut value: u64 = 14695981039346656037;
|
||||||
|
for result in results {
|
||||||
|
for byte in format!("{result}\n").bytes() {
|
||||||
|
value ^= u64::from(byte);
|
||||||
|
value = value.wrapping_mul(1099511628211);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format!("{value:016x}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_hash() -> String {
|
||||||
|
let text = fs::read_to_string("fixtures/manifest.json").expect("fixture manifest");
|
||||||
|
text.split("\"corpus_sha256\": \"")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|value| value.split('"').next())
|
||||||
|
.expect("fixture hash")
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut paths: Vec<PathBuf> = fs::read_dir("fixtures/conformance")
|
||||||
|
.expect("fixture corpus")
|
||||||
|
.map(|entry| entry.expect("fixture entry").path())
|
||||||
|
.filter(|path| path.extension().and_then(|value| value.to_str()) == Some("tsv"))
|
||||||
|
.collect();
|
||||||
|
paths.sort();
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for path in paths {
|
||||||
|
let text = fs::read_to_string(path).expect("fixture file");
|
||||||
|
let mut lines = text.lines();
|
||||||
|
assert_eq!(lines.next(), Some("id\tversion\tkind\tinput\texpected"));
|
||||||
|
for line in lines {
|
||||||
|
let fields: Vec<&str> = line.split('\t').collect();
|
||||||
|
assert_eq!(fields.len(), 5);
|
||||||
|
let actual = evaluate(fields[2], fields[3]);
|
||||||
|
assert_eq!(actual, fields[4], "{}", fields[0]);
|
||||||
|
results.push(format!("{}\t{}", fields[0], actual));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Rust conformance passed normalized={} fixtures={}",
|
||||||
|
normalized_digest(&results),
|
||||||
|
fixture_hash()
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
func values(_ input: String) -> [String: String] {
|
||||||
|
var result: [String: String] = [:]
|
||||||
|
for item in input.split(separator: ";") {
|
||||||
|
let pair = item.split(separator: "=", maxSplits: 1).map(String.init)
|
||||||
|
if pair.count == 2 { result[pair[0]] = pair[1] }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(_ kind: String, _ input: String) -> String {
|
||||||
|
let values = values(input)
|
||||||
|
switch kind {
|
||||||
|
case "version": return ["1", "0", "-1"].contains(input) ? "valid" : "invalid:unsupported_version"
|
||||||
|
case "page":
|
||||||
|
guard let raw = values["limit"], let limit = Int(raw), (1...100).contains(limit) else { return "invalid:invalid_limit" }
|
||||||
|
return "valid"
|
||||||
|
case "manifest":
|
||||||
|
for key in ["provider_url", "vm_address", "password", "private_key"] where values[key] != nil { return "invalid:forbidden_field" }
|
||||||
|
return values["version"] == "1" && values["gateway_id"] != nil && (values["grant"]?.utf8.count ?? 0) >= 43 && values["purpose"] == "launch" ? "valid" : "invalid:invalid_manifest"
|
||||||
|
case "clipboard": return values["encoding"] == "utf-8" && values["file"] == nil ? "valid" : "invalid:unsupported_clipboard"
|
||||||
|
case "event":
|
||||||
|
guard values["version"] == "1" else { return "invalid:unsupported_version" }
|
||||||
|
if let after = Int(values["after"] ?? ""), let earliest = Int(values["earliest"] ?? ""), after > 0, earliest > 0, after < earliest - 1 { return "invalid:gap" }
|
||||||
|
if (Int(values["payload_bytes"] ?? "") ?? Int.max) > 16384 { return "invalid:payload_limit" }
|
||||||
|
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
|
||||||
|
return "valid"
|
||||||
|
case "tunnel":
|
||||||
|
if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" }
|
||||||
|
return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_feature"
|
||||||
|
case "datagram": return classifyDatagram(values["hex"] ?? "")
|
||||||
|
default: return "invalid:unknown_kind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyDatagram(_ encoded: String) -> String {
|
||||||
|
let characters = Array(encoded)
|
||||||
|
guard characters.count % 2 == 0 else { return "invalid:hex" }
|
||||||
|
var raw: [UInt8] = []
|
||||||
|
for index in stride(from: 0, to: characters.count, by: 2) {
|
||||||
|
guard let byte = UInt8(String(characters[index...index + 1]), radix: 16) else { return "invalid:hex" }
|
||||||
|
raw.append(byte)
|
||||||
|
}
|
||||||
|
guard raw.count >= 21 else { return "invalid:truncated" }
|
||||||
|
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
||||||
|
guard raw[2] == 1 else { return "invalid:unsupported_version" }
|
||||||
|
let limit: Int
|
||||||
|
switch raw[3] {
|
||||||
|
case 1: limit = 1024
|
||||||
|
case 2: limit = 2048
|
||||||
|
case 3: limit = 65515
|
||||||
|
default: return "invalid:unknown_channel"
|
||||||
|
}
|
||||||
|
guard raw[4] == 0 else { return "invalid:flags" }
|
||||||
|
guard raw[18] > 0 && raw[17] < raw[18] else { return "invalid:fragment" }
|
||||||
|
let payloadLength = Int(raw[19]) * 256 + Int(raw[20])
|
||||||
|
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
||||||
|
guard raw.count == 21 + payloadLength else { return "invalid:length_mismatch" }
|
||||||
|
guard raw.count <= 65536 else { return "invalid:frame_limit" }
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedDigest(_ results: [String]) -> String {
|
||||||
|
var value: UInt64 = 14695981039346656037
|
||||||
|
for result in results {
|
||||||
|
for byte in Array("\(result)\n".utf8) {
|
||||||
|
value ^= UInt64(byte)
|
||||||
|
value = value &* 1099511628211
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(format: "%016llx", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixtureHash() -> String {
|
||||||
|
let text = try! String(contentsOfFile: "fixtures/manifest.json", encoding: .utf8)
|
||||||
|
let marker = "\"corpus_sha256\": \""
|
||||||
|
guard let start = text.range(of: marker)?.upperBound else { fatalError("fixture hash") }
|
||||||
|
let suffix = text[start...]
|
||||||
|
guard let end = suffix.firstIndex(of: "\"") else { fatalError("fixture hash") }
|
||||||
|
return String(suffix[..<end])
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct ConformanceMain {
|
||||||
|
static func main() {
|
||||||
|
let names = try! FileManager.default.contentsOfDirectory(atPath: "fixtures/conformance").filter { $0.hasSuffix(".tsv") }.sorted()
|
||||||
|
var results: [String] = []
|
||||||
|
for name in names {
|
||||||
|
let text = try! String(contentsOfFile: "fixtures/conformance/\(name)", encoding: .utf8)
|
||||||
|
var lines = text.split(whereSeparator: \.isNewline).map(String.init)
|
||||||
|
precondition(lines.removeFirst() == "id\tversion\tkind\tinput\texpected")
|
||||||
|
for line in lines {
|
||||||
|
let fields = line.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
|
||||||
|
precondition(fields.count == 5)
|
||||||
|
let actual = evaluate(fields[2], fields[3])
|
||||||
|
precondition(actual == fields[4], fields[0])
|
||||||
|
results.append("\(fields[0])\t\(actual)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("Swift conformance passed normalized=\(normalizedDigest(results)) fixtures=\(fixtureHash())")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: list[str]) -> None:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(result.stdout + result.stderr)
|
||||||
|
print(result.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="versevdi-protocol-conformance-") as directory:
|
||||||
|
temp = pathlib.Path(directory)
|
||||||
|
rust_bin = temp / "rust-conformance"
|
||||||
|
swift_bin = temp / "swift-conformance"
|
||||||
|
run(["rustc", "tools/native_conformance.rs", "-O", "-o", str(rust_bin)])
|
||||||
|
run([str(rust_bin)])
|
||||||
|
main_source = temp / "main.swift"
|
||||||
|
main_source.write_text((ROOT / "tools/native_conformance.swift").read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
run(["swiftc", "-parse-as-library", "gen/swift/Protocol.swift", str(main_source), "-o", str(swift_bin)])
|
||||||
|
run([str(swift_bin)])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dependency-free structural and scope validation for the Protocol sources."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
schema_path = ROOT / "schemas/control-v1.schema.json"
|
||||||
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||||
|
assert schema["$schema"].endswith("2020-12/schema")
|
||||||
|
defs = schema["$defs"]
|
||||||
|
assert len(defs) >= 16
|
||||||
|
for name, definition in defs.items():
|
||||||
|
assert definition["type"] == "object", name
|
||||||
|
assert definition["additionalProperties"] is False, name
|
||||||
|
assert set(definition["required"]).issubset(definition["properties"]), name
|
||||||
|
|
||||||
|
compatibility = json.loads((ROOT / "compatibility.json").read_text(encoding="utf-8"))
|
||||||
|
assert set([compatibility["current"], compatibility["n_minus_1"], compatibility["n_minus_2"]]) == {"1", "0", "-1"}
|
||||||
|
assert len(set(compatibility["unsupported"])) == len(compatibility["unsupported"])
|
||||||
|
|
||||||
|
for registry in ("registries/features.json", "registries/datagrams.json"):
|
||||||
|
value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
|
||||||
|
entries = value.get("features", value.get("datagrams"))
|
||||||
|
assert entries and len({entry["id"] for entry in entries}) == len(entries)
|
||||||
|
for entry in entries:
|
||||||
|
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
|
||||||
|
assert isinstance(maximum, int) and 1 <= maximum <= 65536
|
||||||
|
|
||||||
|
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
|
||||||
|
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
|
||||||
|
forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8"))
|
||||||
|
assert "provider_url" not in defs["ConnectionManifest"]["properties"] and "provider_url" in forbidden
|
||||||
|
|
||||||
|
expected_header = "id\tversion\tkind\tinput\texpected"
|
||||||
|
ids = set()
|
||||||
|
for fixture_path in sorted((ROOT / "fixtures/conformance").glob("*.tsv")):
|
||||||
|
lines = fixture_path.read_text(encoding="utf-8").splitlines()
|
||||||
|
assert lines and lines[0] == expected_header, fixture_path
|
||||||
|
for line in lines[1:]:
|
||||||
|
fields = line.split("\t")
|
||||||
|
assert len(fields) == 5, line
|
||||||
|
assert fields[0] not in ids, fields[0]
|
||||||
|
ids.add(fields[0])
|
||||||
|
assert fields[4] == "valid" or fields[4].startswith("invalid:"), line
|
||||||
|
|
||||||
|
fixture_manifest = json.loads((ROOT / "fixtures/manifest.json").read_text(encoding="utf-8"))
|
||||||
|
assert fixture_manifest["files"] == sorted(
|
||||||
|
path.relative_to(ROOT).as_posix() for path in (ROOT / "fixtures/conformance").glob("*.tsv")
|
||||||
|
)
|
||||||
|
fixture_hash = hashlib.sha256()
|
||||||
|
for relative in fixture_manifest["files"]:
|
||||||
|
fixture_hash.update(relative.encode("utf-8"))
|
||||||
|
fixture_hash.update(b"\0")
|
||||||
|
fixture_hash.update((ROOT / relative).read_bytes())
|
||||||
|
fixture_hash.update(b"\0")
|
||||||
|
assert fixture_manifest["corpus_sha256"] == 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
|
||||||
|
assert "provider_url" not in openapi and "vm_address" not in openapi
|
||||||
|
print("Protocol source validation passed")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except (AssertionError, OSError, json.JSONDecodeError) as exc:
|
||||||
|
print(f"validate: {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate the bounded Phase 3A datagram header and fixture corpus."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
HEADER_BYTES = 21
|
||||||
|
MAX_FRAME_BYTES = 65536
|
||||||
|
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515}
|
||||||
|
|
||||||
|
|
||||||
|
def classify(raw: bytes) -> str:
|
||||||
|
if len(raw) < HEADER_BYTES:
|
||||||
|
return "invalid:truncated"
|
||||||
|
if raw[:2] != b"VD":
|
||||||
|
return "invalid:magic"
|
||||||
|
if raw[2] != 1:
|
||||||
|
return "invalid:unsupported_version"
|
||||||
|
if raw[3] not in CHANNEL_LIMITS:
|
||||||
|
return "invalid:unknown_channel"
|
||||||
|
if raw[4] != 0:
|
||||||
|
return "invalid:flags"
|
||||||
|
fragment_index, fragment_count = raw[17], raw[18]
|
||||||
|
if fragment_count == 0 or fragment_index >= fragment_count:
|
||||||
|
return "invalid:fragment"
|
||||||
|
payload_length = int.from_bytes(raw[19:21], "big")
|
||||||
|
if payload_length > CHANNEL_LIMITS[raw[3]]:
|
||||||
|
return "invalid:payload_limit"
|
||||||
|
if len(raw) != HEADER_BYTES + payload_length:
|
||||||
|
return "invalid:length_mismatch"
|
||||||
|
if len(raw) > MAX_FRAME_BYTES:
|
||||||
|
return "invalid:frame_limit"
|
||||||
|
return "valid"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
lines = (ROOT / "fixtures/conformance/datagram-v1.tsv").read_text(encoding="utf-8").splitlines()
|
||||||
|
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
||||||
|
for line in lines[1:]:
|
||||||
|
identifier, version, kind, input_value, expected = line.split("\t")
|
||||||
|
assert kind == "datagram" and version == "1"
|
||||||
|
encoded = input_value.removeprefix("hex=")
|
||||||
|
try:
|
||||||
|
actual = classify(binascii.unhexlify(encoded))
|
||||||
|
except binascii.Error:
|
||||||
|
actual = "invalid:hex"
|
||||||
|
assert actual == expected, f"{identifier}: {actual} != {expected}"
|
||||||
|
print("Datagram frame validation passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user