feat(protocol): negotiate registered gateway profiles
This commit is contained in:
+44
-11
@@ -149,7 +149,13 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
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", {}))
|
||||
items = prop.get("items", {})
|
||||
if "enum" in items:
|
||||
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
|
||||
reference = ref_name(prop)
|
||||
@@ -254,16 +260,23 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tselected := profiles[0]",
|
||||
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tcommon := append([]string(nil), selected.ClientDecode...)",
|
||||
"\tfor _, profile := range profiles[1:] {",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile != selected { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tnext := common[:0]",
|
||||
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }",
|
||||
"\t\tcommon = next",
|
||||
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t}",
|
||||
"\tselected.ClientDecode = common",
|
||||
"\treturn selected, nil",
|
||||
"}",
|
||||
"",
|
||||
])
|
||||
out.extend([
|
||||
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
|
||||
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}",
|
||||
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}",
|
||||
"\tfields = append(fields, v.Capabilities.ClientDecode...)",
|
||||
"\tvar transcript strings.Builder",
|
||||
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
||||
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
||||
@@ -346,7 +359,13 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
|
||||
if "maxItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
|
||||
item_ref = ref_name(prop.get("items", {}))
|
||||
items = prop.get("items", {})
|
||||
if "enum" in items:
|
||||
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ item.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_item\"))?; }}")
|
||||
reference = ref_name(prop)
|
||||
@@ -440,7 +459,9 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
out.extend([
|
||||
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
|
||||
" let reconnect_sequence = self.reconnectSequence.to_string();",
|
||||
" let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];",
|
||||
" let client_decode_count = self.capabilities.clientDecode.len().to_string();",
|
||||
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];",
|
||||
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));",
|
||||
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
|
||||
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
|
||||
" transcript.into_bytes()",
|
||||
@@ -449,11 +470,13 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
out.extend(["}", ""])
|
||||
out.extend([
|
||||
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
|
||||
" let selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
|
||||
" let mut selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
|
||||
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||
" for profile in &profiles[1..] {",
|
||||
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||
" if profile != &selected { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));",
|
||||
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" }",
|
||||
" Ok(selected)",
|
||||
"}",
|
||||
@@ -502,7 +525,13 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if {value}.count < {prop['minItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_items\") }}")
|
||||
if "maxItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
|
||||
item_ref = ref_name(prop.get("items", {}))
|
||||
items = prop.get("items", {})
|
||||
if "enum" in items:
|
||||
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
|
||||
reference = ref_name(prop)
|
||||
@@ -583,7 +612,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
out.extend([
|
||||
"public extension TunnelAdmissionRequest {",
|
||||
" func deviceAdmissionTranscript() -> Data {",
|
||||
" let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]",
|
||||
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]",
|
||||
" fields.append(contentsOf: capabilities.clientDecode)",
|
||||
" var transcript = \"versevdi/tunnel-admission/v1\"",
|
||||
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
||||
" return Data(transcript.utf8)",
|
||||
@@ -594,11 +624,14 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
||||
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" try selected.validate()",
|
||||
" var common = selected.clientDecode",
|
||||
" for profile in profiles.dropFirst() {",
|
||||
" try profile.validate()",
|
||||
" if profile != selected { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" common = common.filter { profile.clientDecode.contains($0) }",
|
||||
" if common.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" }",
|
||||
" return selected",
|
||||
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
|
||||
@@ -33,7 +33,7 @@ def main() -> int:
|
||||
|
||||
let capability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: "h264-opus"
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
)
|
||||
let request = try TunnelAdmissionRequest(
|
||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||
@@ -41,19 +41,26 @@ let request = try TunnelAdmissionRequest(
|
||||
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
|
||||
)
|
||||
_ = request
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"
|
||||
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
|
||||
fatalError("unexpected device admission transcript")
|
||||
}
|
||||
let incompatible = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus"]
|
||||
)
|
||||
let gatewayCapability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus", "h264-opus"]
|
||||
)
|
||||
do {
|
||||
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
|
||||
fatalError("matching capability profiles did not intersect")
|
||||
}
|
||||
} catch { fatalError("matching capability profiles did not intersect") }
|
||||
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else {
|
||||
fatalError("ordered registered profile intersection changed")
|
||||
}
|
||||
do {
|
||||
_ = try CapabilityProfile.intersection([capability, incompatible])
|
||||
fatalError("profiles without overlap were accepted")
|
||||
@@ -153,7 +160,7 @@ do {
|
||||
fn main() {
|
||||
let capabilities = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), "h264-opus".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
).unwrap();
|
||||
let request = TunnelAdmissionRequest::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
@@ -161,7 +168,7 @@ fn main() {
|
||||
).unwrap();
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
|
||||
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
|
||||
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus";
|
||||
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus";
|
||||
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
|
||||
assert!(TunnelAdmissionRequest::new(
|
||||
"2".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
@@ -178,8 +185,16 @@ fn main() {
|
||||
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
|
||||
let incompatible = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), "hevc-opus".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into()],
|
||||
).unwrap();
|
||||
let gateway_capability = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()],
|
||||
).unwrap();
|
||||
assert_eq!(
|
||||
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(),
|
||||
&vec!["h264-opus".to_string()],
|
||||
);
|
||||
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
|
||||
assert!(AllocationPolicy::new(
|
||||
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
|
||||
|
||||
Reference in New Issue
Block a user