feat(protocol): negotiate display and native input
This commit is contained in:
+16
-2
@@ -160,7 +160,11 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
|
||||
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\"}}) }}")
|
||||
validation = f"if err := v.{field}.Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_object\"}}) }}"
|
||||
if prop_name not in required:
|
||||
lines.append(f"\tif v.{field} != nil {{ {validation} }}")
|
||||
else:
|
||||
lines.append(f"\t{validation}")
|
||||
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||
lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }")
|
||||
if name == "GatewayRegistration":
|
||||
@@ -212,7 +216,10 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
required = set(definition.get("required", []))
|
||||
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}\"`")
|
||||
typ = prop_type(prop, "go")
|
||||
if prop_name not in required and ref_name(prop):
|
||||
typ = "*" + typ
|
||||
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
|
||||
out.extend(["}", ""])
|
||||
for name in sorted(defs):
|
||||
out.append(f"func (v {name}) Validate() error {{")
|
||||
@@ -234,6 +241,11 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
% (prop_name, prop_name)
|
||||
)
|
||||
for prop_name, prop in defs[name].get("properties", {}).items():
|
||||
if prop_name not in required_fields and ref_name(prop):
|
||||
out.append(
|
||||
'\tif raw, ok := fields["%s"]; ok && bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "invalid_object"}}} }'
|
||||
% (prop_name, prop_name)
|
||||
)
|
||||
if "x-max-bytes" in prop and prop.get("type") != "string":
|
||||
out.append(
|
||||
'\tif raw, ok := fields["%s"]; ok && len(raw) > %d { return value, ValidationError{Violations: []FieldViolation{{Field: "%s", Code: "max_bytes"}}} }'
|
||||
@@ -603,6 +615,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
typ = swift_type(prop)
|
||||
if prop_name in required:
|
||||
decoded.append(f"{field}: try c.decode({typ}.self, forKey: .{field})")
|
||||
elif ref_name(prop):
|
||||
decoded.append(f"{field}: try c.contains(.{field}) ? c.decode({typ}.self, forKey: .{field}) : nil")
|
||||
else:
|
||||
decoded.append(f"{field}: try c.decodeIfPresent({typ}.self, forKey: .{field})")
|
||||
out.append(f" try self.init({', '.join(decoded)})")
|
||||
|
||||
@@ -125,10 +125,12 @@ func evaluate(kind, input string) string {
|
||||
}
|
||||
return "valid"
|
||||
case "tunnel":
|
||||
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && parts["feature"] == "control.v1" {
|
||||
feature := parts["feature"]
|
||||
registered := feature == "control.v1" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
|
||||
if (parts["offered"] == "1" || parts["offered"] == "0" || parts["offered"] == "-1") && registered {
|
||||
return "valid"
|
||||
}
|
||||
if parts["feature"] != "control.v1" {
|
||||
if !registered {
|
||||
return "invalid:unsupported_feature"
|
||||
}
|
||||
return "invalid:unsupported_version"
|
||||
@@ -226,6 +228,19 @@ func classifyGatewayInput(encoded string) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
case 6:
|
||||
if len(body) != 8 {
|
||||
return "invalid:length"
|
||||
}
|
||||
x, y := uint16(body[0])<<8|uint16(body[1]), uint16(body[2])<<8|uint16(body[3])
|
||||
width, height := uint16(body[4])<<8|uint16(body[5]), uint16(body[6])<<8|uint16(body[7])
|
||||
if width == 0 || height == 0 || x >= width || y >= height {
|
||||
return "invalid:field"
|
||||
}
|
||||
case 7:
|
||||
if len(body) != 4 {
|
||||
return "invalid:length"
|
||||
}
|
||||
default:
|
||||
return "invalid:kind"
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||
|| !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") => {
|
||||
&& matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => "valid",
|
||||
"tunnel" if !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => {
|
||||
"invalid:unsupported_feature"
|
||||
}
|
||||
"tunnel" => "invalid:unsupported_version",
|
||||
@@ -110,6 +110,16 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
|
||||
5 if body[0] > 15 => "invalid:field",
|
||||
5 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field",
|
||||
5 => "valid",
|
||||
6 if body.len() != 8 => "invalid:length",
|
||||
6 => {
|
||||
let x = u16::from_be_bytes([body[0], body[1]]);
|
||||
let y = u16::from_be_bytes([body[2], body[3]]);
|
||||
let width = u16::from_be_bytes([body[4], body[5]]);
|
||||
let height = u16::from_be_bytes([body[6], body[7]]);
|
||||
if width != 0 && height != 0 && x < width && y < height { "valid" } else { "invalid:field" }
|
||||
}
|
||||
7 if body.len() == 4 => "valid",
|
||||
7 => "invalid:length",
|
||||
_ => "invalid:kind",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ func evaluate(_ kind: String, _ input: String) -> String {
|
||||
guard let sequence = Int(values["sequence"] ?? ""), sequence > 0, values["correlation_id"] != nil else { return "invalid:required" }
|
||||
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"
|
||||
let registered = ["control.v1", "display.request.v1", "input.absolute.v1", "input.scroll.v1"].contains(values["feature"] ?? "")
|
||||
if ["1", "0", "-1"].contains(values["offered"] ?? "") && registered { return "valid" }
|
||||
return registered ? "invalid:unsupported_version" : "invalid:unsupported_feature"
|
||||
case "datagram": return classifyDatagram(values["hex"] ?? "")
|
||||
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
|
||||
case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "")
|
||||
@@ -82,6 +83,14 @@ func classifyGatewayInput(_ encoded: String) -> String {
|
||||
guard body[0] <= 15 else { return "invalid:field" }
|
||||
guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" }
|
||||
return "valid"
|
||||
case 6:
|
||||
guard body.count == 8 else { return "invalid:length" }
|
||||
let x = Int(body[0]) * 256 + Int(body[1])
|
||||
let y = Int(body[2]) * 256 + Int(body[3])
|
||||
let width = Int(body[4]) * 256 + Int(body[5])
|
||||
let height = Int(body[6]) * 256 + Int(body[7])
|
||||
return width > 0 && height > 0 && x < width && y < height ? "valid" : "invalid:field"
|
||||
case 7: return body.count == 4 ? "valid" : "invalid:length"
|
||||
default: return "invalid:kind"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,44 @@ do {
|
||||
)
|
||||
fatalError("invalid allocation bounds were accepted")
|
||||
} catch { }
|
||||
let displayMode = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
|
||||
for invalid in [
|
||||
{ try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) },
|
||||
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) },
|
||||
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241) },
|
||||
] {
|
||||
do {
|
||||
_ = try invalid()
|
||||
fatalError("invalid display mode was accepted")
|
||||
} catch { }
|
||||
}
|
||||
let allocationPolicy = try AllocationPolicy(
|
||||
minimumKbps: 1000, targetKbps: 2000, maximumKbps: 3000, tier: "standard",
|
||||
audience: "versevdi-gateway", protocolValue: "verse", protocolVersion: 1,
|
||||
grantTtlSeconds: 60, reservationLeaseSeconds: 300
|
||||
)
|
||||
let legacyDisplayRequest = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
|
||||
requestedDisplayMode: nil
|
||||
).encodeJSON()
|
||||
guard !String(data: legacyDisplayRequest, encoding: .utf8)!.contains("requested_display_mode") else {
|
||||
fatalError("legacy request encoded an absent display mode")
|
||||
}
|
||||
let displayRequest = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", policySnapshot: allocationPolicy,
|
||||
requestedDisplayMode: displayMode
|
||||
)
|
||||
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
|
||||
fatalError("display mode did not round-trip")
|
||||
}
|
||||
var nullDisplayRequest = try JSONSerialization.jsonObject(with: displayRequest.encodeJSON()) as! [String: Any]
|
||||
nullDisplayRequest["requested_display_mode"] = NSNull()
|
||||
do {
|
||||
_ = try SessionRequest.decodeJSON(try JSONSerialization.data(withJSONObject: nullDisplayRequest))
|
||||
fatalError("explicit null display mode was accepted")
|
||||
} catch { }
|
||||
let streamPolicy = try ProviderStreamPolicy(
|
||||
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
|
||||
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
|
||||
@@ -218,6 +256,24 @@ fn main() {
|
||||
assert!(AllocationPolicy::new(
|
||||
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
|
||||
).is_err());
|
||||
let display_mode = DisplayMode::new(2560, 1440, 120).unwrap();
|
||||
assert!(DisplayMode::new(319, 1440, 120).is_err());
|
||||
assert!(DisplayMode::new(2560, 199, 120).is_err());
|
||||
assert!(DisplayMode::new(2560, 1440, 241).is_err());
|
||||
let allocation_policy = AllocationPolicy::new(
|
||||
1000, 2000, 3000, "standard".into(), "versevdi-gateway".into(),
|
||||
"verse".into(), 1, 60, 300,
|
||||
).unwrap();
|
||||
let legacy_display_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
allocation_policy.clone(), None,
|
||||
).unwrap();
|
||||
assert!(legacy_display_request.requestedDisplayMode().is_none());
|
||||
let display_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
allocation_policy, Some(display_mode.clone()),
|
||||
).unwrap();
|
||||
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 120, "HEVC".into(), 40000, true,
|
||||
).is_ok());
|
||||
|
||||
@@ -36,6 +36,24 @@ def main() -> int:
|
||||
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
|
||||
assert isinstance(maximum, int) and 1 <= maximum <= maximum_bound
|
||||
|
||||
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
|
||||
registered_features = {entry["id"] for entry in feature_registry["features"]}
|
||||
assert {"display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
||||
|
||||
display_mode = defs["DisplayMode"]
|
||||
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
|
||||
assert display_mode["properties"]["resolution_width"] == {"type": "integer", "minimum": 320, "maximum": 16384}
|
||||
assert display_mode["properties"]["resolution_height"] == {"type": "integer", "minimum": 200, "maximum": 8640}
|
||||
assert display_mode["properties"]["fps"] == {"type": "integer", "minimum": 1, "maximum": 240}
|
||||
for owner, field in (
|
||||
("SessionRequest", "requested_display_mode"),
|
||||
("BrokerSession", "requested_display_mode"),
|
||||
("BrokerSession", "effective_display_mode"),
|
||||
("ManifestProfile", "display_mode"),
|
||||
):
|
||||
assert field not in defs[owner]["required"]
|
||||
assert defs[owner]["properties"][field] == {"$ref": "#/$defs/DisplayMode"}
|
||||
|
||||
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
|
||||
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
|
||||
forbidden = json.loads((ROOT / "fixtures/invalid/manifest-provider-field.json").read_text(encoding="utf-8"))
|
||||
|
||||
@@ -37,6 +37,13 @@ def classify_input(raw: bytes) -> str:
|
||||
return "invalid:field"
|
||||
active_mask = int.from_bytes(body[1:3], "big")
|
||||
return "valid" if active_mask or not any(body[3:]) else "invalid:field"
|
||||
if kind == 6:
|
||||
if len(body) != 8:
|
||||
return "invalid:length"
|
||||
x, y, width, height = (int.from_bytes(body[index:index + 2], "big") for index in range(0, 8, 2))
|
||||
return "valid" if width and height and x < width and y < height else "invalid:field"
|
||||
if kind == 7:
|
||||
return "valid" if len(body) == 4 else "invalid:length"
|
||||
return "invalid:kind"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user