feat(protocol): freeze M4 native session RC6 contract
This commit is contained in:
+11
-1
@@ -32,7 +32,17 @@ def main() -> int:
|
||||
expected = manifest.get("corpus_sha256")
|
||||
if not expected or actual != expected:
|
||||
raise ValueError(f"fixture corpus hash mismatch: {actual}")
|
||||
print(f"Fixture corpus SHA256 {actual}")
|
||||
json_paths = sorted(
|
||||
path.relative_to(ROOT).as_posix()
|
||||
for directory in (ROOT / "fixtures/valid", ROOT / "fixtures/invalid")
|
||||
for path in directory.glob("*.json")
|
||||
)
|
||||
if json_paths != manifest.get("json_files"):
|
||||
raise ValueError("JSON fixture manifest file list is stale")
|
||||
json_actual = digest(json_paths)
|
||||
if json_actual != manifest.get("json_corpus_sha256"):
|
||||
raise ValueError(f"JSON fixture corpus hash mismatch: {json_actual}")
|
||||
print(f"Fixture corpus SHA256 {actual}; JSON SHA256 {json_actual}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+168
-33
@@ -139,11 +139,15 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
)
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f"\tif v.{field} != \"\" {{ if _, err := base64.RawURLEncoding.Strict().DecodeString(v.{field}); err != nil {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_format\"}}) }} }}")
|
||||
if prop.get("format") == "uuid":
|
||||
lines.append(f"\tif v.{field} != \"\" && !validCanonicalUUID(v.{field}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_uuid\"}}) }}")
|
||||
if prop.get("type") == "integer":
|
||||
value = f"*v.{field}" if prop.get("x-optional-pointer") else f"v.{field}"
|
||||
guard = f"v.{field} != nil && " if prop.get("x-optional-pointer") else ""
|
||||
if "minimum" in prop:
|
||||
lines.append(f"\tif v.{field} != 0 && v.{field} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}")
|
||||
lines.append(f"\tif {guard}{value} != 0 && {value} < {prop['minimum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"minimum\"}}) }}")
|
||||
if "maximum" in prop:
|
||||
lines.append(f"\tif v.{field} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}")
|
||||
lines.append(f"\tif {guard}{value} > {prop['maximum']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"maximum\"}}) }}")
|
||||
if prop.get("type") == "array":
|
||||
if "minItems" in prop:
|
||||
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
|
||||
@@ -159,8 +163,8 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in items:
|
||||
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
|
||||
if prop.get("uniqueItems"):
|
||||
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if reflect.DeepEqual(item, v.{field}[prior]) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
|
||||
@@ -173,6 +177,12 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f"\t{validation}")
|
||||
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||
lines.append("\tif v.MinimumKbps > v.TargetKbps || v.TargetKbps > v.MaximumKbps { violations = append(violations, FieldViolation{Field: \"bounds\", Code: \"invalid_order\"}) }")
|
||||
if name == "SessionQualityLimits":
|
||||
lines.append("\tif v.BitrateMinimumKbps > v.BitrateTargetKbps || v.BitrateTargetKbps > v.BitrateMaximumKbps { violations = append(violations, FieldViolation{Field: \"bitrate_bounds\", Code: \"invalid_order\"}) }")
|
||||
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
|
||||
lines.append("\tif v.BitrateTargetKbps > v.BitrateMaximumKbps { violations = append(violations, FieldViolation{Field: \"bitrate_bounds\", Code: \"invalid_order\"}) }")
|
||||
if name == "BitratePreference":
|
||||
lines.append("\tif v.Mode == \"auto\" && v.TargetKbps != nil || v.Mode == \"explicit\" && v.TargetKbps == nil { violations = append(violations, FieldViolation{Field: \"target_kbps\", Code: \"invalid_tagged_value\"}) }")
|
||||
if name == "GatewayRegistration":
|
||||
lines.append("\tif v.ProtocolMinVersion > v.ProtocolMaxVersion { violations = append(violations, FieldViolation{Field: \"protocol_version\", Code: \"invalid_order\"}) }")
|
||||
if name == "ChannelFrame":
|
||||
@@ -214,6 +224,30 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
"",
|
||||
"func (e ValidationError) Error() string { return \"protocol validation failed\" }",
|
||||
"",
|
||||
"func validCanonicalUUID(value string) bool {",
|
||||
"\tif len(value) != 36 || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' { return false }",
|
||||
"\tfor index, char := range []byte(value) { if index == 8 || index == 13 || index == 18 || index == 23 { continue }; if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { return false } }",
|
||||
"\treturn value != \"00000000-0000-0000-0000-000000000000\"",
|
||||
"}",
|
||||
"",
|
||||
"func rejectDuplicateJSONKeys(data []byte) error {",
|
||||
"\tdecoder := json.NewDecoder(bytes.NewReader(data))",
|
||||
"\tvar scan func(json.Token) error",
|
||||
"\tscan = func(token json.Token) error {",
|
||||
"\t\tdelim, ok := token.(json.Delim); if !ok { return nil }",
|
||||
"\t\tswitch delim {",
|
||||
"\t\tcase '{':",
|
||||
"\t\t\tseen := map[string]struct{}{}",
|
||||
"\t\t\tfor decoder.More() { keyToken, err := decoder.Token(); if err != nil { return err }; key, ok := keyToken.(string); if !ok { return errors.New(\"invalid JSON object key\") }; if _, exists := seen[key]; exists { return errors.New(\"duplicate JSON object key\") }; seen[key] = struct{}{}; value, err := decoder.Token(); if err != nil { return err }; if err := scan(value); err != nil { return err } }",
|
||||
"\t\t\t_, err := decoder.Token(); return err",
|
||||
"\t\tcase '[':",
|
||||
"\t\t\tfor decoder.More() { value, err := decoder.Token(); if err != nil { return err }; if err := scan(value); err != nil { return err } }; _, err := decoder.Token(); return err",
|
||||
"\t\t}",
|
||||
"\t\treturn nil",
|
||||
"\t}",
|
||||
"\ttoken, err := decoder.Token(); if err != nil { return err }; return scan(token)",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
if name == "FieldViolation":
|
||||
@@ -224,7 +258,9 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
for prop_name, prop in definition.get("properties", {}).items():
|
||||
tag = prop_name + (",omitempty" if prop_name not in required else "")
|
||||
typ = prop_type(prop, "go")
|
||||
if prop_name not in required and ref_name(prop):
|
||||
if prop.get("x-optional-pointer"):
|
||||
typ = "*" + typ
|
||||
elif prop_name not in required and ref_name(prop):
|
||||
typ = "*" + typ
|
||||
out.append(f"\t{go_field(prop_name)} {typ} `json:\"{tag}\"`")
|
||||
out.extend(["}", ""])
|
||||
@@ -239,6 +275,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
out.append(f"func Decode{name}(data []byte) ({name}, error) {{")
|
||||
out.append(f"\tvar value {name}")
|
||||
out.append("\tif len(data) > 1024*1024 { return value, errors.New(\"protocol payload exceeds limit\") }")
|
||||
out.append("\tif err := rejectDuplicateJSONKeys(data); err != nil { return value, err }")
|
||||
out.append("\tvar fields map[string]json.RawMessage")
|
||||
out.append("\tif err := json.Unmarshal(data, &fields); err != nil { return value, err }")
|
||||
required_fields = sorted(defs[name].get("required", []))
|
||||
@@ -295,23 +332,29 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tselected := profiles[0]",
|
||||
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tcommon := append([]string(nil), selected.ClientDecode...)",
|
||||
"\tcommonVideo := append([]VideoProfile(nil), selected.VideoProfiles...)",
|
||||
"\tcommonAudio := append([]AudioProfile(nil), selected.AudioProfiles...)",
|
||||
"\tfor _, profile := range profiles[1:] {",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tnext := common[:0]",
|
||||
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }",
|
||||
"\t\tcommon = next",
|
||||
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tnextVideo := commonVideo[:0]",
|
||||
"\t\tfor _, candidate := range commonVideo { for _, offered := range profile.VideoProfiles { if candidate == offered { nextVideo = append(nextVideo, candidate); break } } }",
|
||||
"\t\tcommonVideo = nextVideo",
|
||||
"\t\tnextAudio := commonAudio[:0]",
|
||||
"\t\tfor _, candidate := range commonAudio { for _, offered := range profile.AudioProfiles { if candidate == offered { nextAudio = append(nextAudio, candidate); break } } }",
|
||||
"\t\tcommonAudio = nextAudio",
|
||||
"\t\tif len(commonVideo) == 0 || len(commonAudio) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t}",
|
||||
"\tselected.ClientDecode = common",
|
||||
"\tselected.VideoProfiles = commonVideo",
|
||||
"\tselected.AudioProfiles = commonAudio",
|
||||
"\treturn selected, nil",
|
||||
"}",
|
||||
"",
|
||||
])
|
||||
out.extend([
|
||||
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
|
||||
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}",
|
||||
"\tfields = append(fields, v.Capabilities.ClientDecode...)",
|
||||
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.VideoProfiles)), fmt.Sprintf(\"%d\", len(v.Capabilities.AudioProfiles))}",
|
||||
"\tfor _, profile := range v.Capabilities.VideoProfiles { fields = append(fields, profile.Codec, fmt.Sprintf(\"%d\", profile.BitDepth), profile.ChromaSubsampling, profile.ColorSpace, profile.TransferFunction) }",
|
||||
"\tfor _, profile := range v.Capabilities.AudioProfiles { fields = append(fields, profile.Codec, fmt.Sprintf(\"%d\", profile.SampleRateHz), fmt.Sprintf(\"%d\", profile.Channels), profile.ChannelLayout, fmt.Sprintf(\"%d\", profile.PacketDurationMs)) }",
|
||||
"\tvar transcript strings.Builder",
|
||||
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
||||
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
||||
@@ -386,11 +429,14 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if !valid_rfc3339_utc({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_time\")); }}")
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f" {prefix}if !valid_base64_url({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_format\")); }}")
|
||||
if prop.get("format") == "uuid":
|
||||
lines.append(f" {prefix}if !valid_canonical_uuid({value}.as_str()) {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_uuid\")); }}")
|
||||
if prop.get("type") == "integer":
|
||||
numeric = f"*{value}" if prop_name not in required else value
|
||||
if "minimum" in prop:
|
||||
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}")
|
||||
lines.append(f" {prefix}if {numeric} < {prop['minimum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"minimum\")); }}")
|
||||
if "maximum" in prop:
|
||||
lines.append(f" {prefix}if {value} > {prop['maximum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"maximum\")); }}")
|
||||
lines.append(f" {prefix}if {numeric} > {prop['maximum']} {{ return Err(ValidationError::new(\"{prop_name}\", \"maximum\")); }}")
|
||||
if prop.get("type") == "array":
|
||||
if "minItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
|
||||
@@ -406,7 +452,7 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in items:
|
||||
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
if prop.get("uniqueItems"):
|
||||
lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
@@ -419,6 +465,12 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
name = definition["name"]
|
||||
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||
lines.append(" if self.minimumKbps > self.targetKbps || self.targetKbps > self.maximumKbps { return Err(ValidationError::new(\"bounds\", \"invalid_order\")); }")
|
||||
if name == "SessionQualityLimits":
|
||||
lines.append(" if self.bitrateMinimumKbps > self.bitrateTargetKbps || self.bitrateTargetKbps > self.bitrateMaximumKbps { return Err(ValidationError::new(\"bitrate_bounds\", \"invalid_order\")); }")
|
||||
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
|
||||
lines.append(" if self.bitrateTargetKbps > self.bitrateMaximumKbps { return Err(ValidationError::new(\"bitrate_bounds\", \"invalid_order\")); }")
|
||||
if name == "BitratePreference":
|
||||
lines.append(" if self.mode == \"auto\" && self.targetKbps.is_some() || self.mode == \"explicit\" && self.targetKbps.is_none() { return Err(ValidationError::new(\"target_kbps\", \"invalid_tagged_value\")); }")
|
||||
if name == "GatewayRegistration":
|
||||
lines.append(" if self.protocolMinVersion > self.protocolMaxVersion { return Err(ValidationError::new(\"protocol_version\", \"invalid_order\")); }")
|
||||
if name == "ChannelFrame":
|
||||
@@ -473,6 +525,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
" let fraction = &bytes[20..bytes.len() - 1];",
|
||||
" bytes[19] == b'.' && !fraction.is_empty() && fraction.len() <= 9 && fraction.iter().all(u8::is_ascii_digit) && *fraction.last().unwrap() != b'0'",
|
||||
"}",
|
||||
"fn valid_canonical_uuid(value: &str) -> bool {",
|
||||
" let bytes = value.as_bytes();",
|
||||
" bytes.len() == 36 && [8, 13, 18, 23].iter().all(|index| bytes[*index] == b'-') && bytes.iter().enumerate().all(|(index, byte)| [8, 13, 18, 23].contains(&index) || byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)) && value != \"00000000-0000-0000-0000-000000000000\"",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
@@ -515,9 +571,12 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
out.extend([
|
||||
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
|
||||
" let reconnect_sequence = self.reconnectSequence.to_string();",
|
||||
" let client_decode_count = self.capabilities.clientDecode.len().to_string();",
|
||||
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];",
|
||||
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));",
|
||||
" let video_count = self.capabilities.videoProfiles.len().to_string();",
|
||||
" let audio_count = self.capabilities.audioProfiles.len().to_string();",
|
||||
" let mut owned = vec![self.sessionId.clone(), self.gatewayId.clone(), self.audience.clone(), self.grant.clone(), reconnect_sequence, self.clientNonce.clone(), self.capabilities.transport.clone(), self.capabilities.framing.clone(), self.capabilities.media.clone(), self.capabilities.sourceRateControl.clone(), video_count, audio_count];",
|
||||
" for profile in &self.capabilities.videoProfiles { owned.extend([profile.codec.clone(), profile.bitDepth.to_string(), profile.chromaSubsampling.clone(), profile.colorSpace.clone(), profile.transferFunction.clone()]); }",
|
||||
" for profile in &self.capabilities.audioProfiles { owned.extend([profile.codec.clone(), profile.sampleRateHz.to_string(), profile.channels.to_string(), profile.channelLayout.clone(), profile.packetDurationMs.to_string()]); }",
|
||||
" let fields: Vec<&str> = owned.iter().map(String::as_str).collect();",
|
||||
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
|
||||
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
|
||||
" transcript.into_bytes()",
|
||||
@@ -545,9 +604,10 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||
" for profile in &profiles[1..] {",
|
||||
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));",
|
||||
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" selected.videoProfiles.retain(|candidate| profile.videoProfiles.contains(candidate));",
|
||||
" selected.audioProfiles.retain(|candidate| profile.audioProfiles.contains(candidate));",
|
||||
" if selected.videoProfiles.is_empty() || selected.audioProfiles.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" }",
|
||||
" Ok(selected)",
|
||||
"}",
|
||||
@@ -586,6 +646,8 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if !validRFC3339UTC({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_time\") }}")
|
||||
if prop.get("format") == "base64url":
|
||||
lines.append(f" {prefix}if !validBase64URL({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_format\") }}")
|
||||
if prop.get("format") == "uuid":
|
||||
lines.append(f" {prefix}if !validCanonicalUUID({value}) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_uuid\") }}")
|
||||
if prop.get("type") == "integer":
|
||||
if "minimum" in prop:
|
||||
lines.append(f" {prefix}if {value} < {prop['minimum']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"minimum\") }}")
|
||||
@@ -606,8 +668,8 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
if "enum" in items:
|
||||
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
|
||||
if prop.get("uniqueItems"):
|
||||
lines.append(f" {prefix}for (index, item) in {value}.enumerated() where {value}[..<index].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
|
||||
@@ -619,6 +681,12 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
name = definition["name"]
|
||||
if name in {"AllocationPolicy", "ManifestBounds"}:
|
||||
lines.append(" if minimumKbps > targetKbps || targetKbps > maximumKbps { throw ContractValidationError(field: \"bounds\", code: \"invalid_order\") }")
|
||||
if name == "SessionQualityLimits":
|
||||
lines.append(" if bitrateMinimumKbps > bitrateTargetKbps || bitrateTargetKbps > bitrateMaximumKbps { throw ContractValidationError(field: \"bitrate_bounds\", code: \"invalid_order\") }")
|
||||
if name in {"SelectedSessionDescriptor", "ProviderStreamPolicy"}:
|
||||
lines.append(" if bitrateTargetKbps > bitrateMaximumKbps { throw ContractValidationError(field: \"bitrate_bounds\", code: \"invalid_order\") }")
|
||||
if name == "BitratePreference":
|
||||
lines.append(" if mode == \"auto\" && targetKbps != nil || mode == \"explicit\" && targetKbps == nil { throw ContractValidationError(field: \"target_kbps\", code: \"invalid_tagged_value\") }")
|
||||
if name == "GatewayRegistration":
|
||||
lines.append(" if protocolMinVersion > protocolMaxVersion { throw ContractValidationError(field: \"protocol_version\", code: \"invalid_order\") }")
|
||||
if name == "ChannelFrame":
|
||||
@@ -637,6 +705,65 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
f'public let nMinus2WireVersion = "{compatibility["n_minus_2"]}"',
|
||||
"public struct ContractValidationError: Error, Equatable { public let field: String; public let code: String }",
|
||||
"private struct AnyCodingKey: CodingKey { let stringValue: String; let intValue: Int?; init?(stringValue: String) { self.stringValue = stringValue; self.intValue = nil }; init?(intValue: Int) { self.stringValue = String(intValue); self.intValue = intValue } }",
|
||||
"private func rejectDuplicateJSONKeys(_ data: Data) throws {",
|
||||
" var index = 0",
|
||||
" func skipWhitespace() { while index < data.count && [9, 10, 13, 32].contains(data[index]) { index += 1 } }",
|
||||
" func parseString() throws -> String {",
|
||||
" guard index < data.count, data[index] == 34 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" let start = index",
|
||||
" index += 1",
|
||||
" while index < data.count {",
|
||||
" if data[index] == 92 { index += 2; continue }",
|
||||
" if data[index] == 34 { index += 1; return try JSONDecoder().decode(String.self, from: data[start..<index]) }",
|
||||
" index += 1",
|
||||
" }",
|
||||
" throw ContractValidationError(field: \"json\", code: \"invalid_json\")",
|
||||
" }",
|
||||
" func parseValue() throws {",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" if data[index] == 123 {",
|
||||
" index += 1",
|
||||
" var keys = Set<String>()",
|
||||
" skipWhitespace()",
|
||||
" if index < data.count, data[index] == 125 { index += 1; return }",
|
||||
" while true {",
|
||||
" skipWhitespace()",
|
||||
" let key = try parseString()",
|
||||
" guard keys.insert(key).inserted else { throw ContractValidationError(field: key, code: \"duplicate_field\") }",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count, data[index] == 58 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" index += 1",
|
||||
" try parseValue()",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" if data[index] == 125 { index += 1; return }",
|
||||
" guard data[index] == 44 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" index += 1",
|
||||
" }",
|
||||
" }",
|
||||
" if data[index] == 91 {",
|
||||
" index += 1",
|
||||
" skipWhitespace()",
|
||||
" if index < data.count, data[index] == 93 { index += 1; return }",
|
||||
" while true {",
|
||||
" try parseValue()",
|
||||
" skipWhitespace()",
|
||||
" guard index < data.count else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" if data[index] == 93 { index += 1; return }",
|
||||
" guard data[index] == 44 else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" index += 1",
|
||||
" }",
|
||||
" }",
|
||||
" if data[index] == 34 { _ = try parseString(); return }",
|
||||
" let start = index",
|
||||
" while index < data.count && ![9, 10, 13, 32, 44, 93, 125].contains(data[index]) { index += 1 }",
|
||||
" guard index > start else { throw ContractValidationError(field: \"json\", code: \"invalid_json\") }",
|
||||
" }",
|
||||
" try parseValue()",
|
||||
" skipWhitespace()",
|
||||
" guard index == data.count else { throw ContractValidationError(field: \"json\", code: \"trailing_json\") }",
|
||||
"}",
|
||||
"private func validBase64URL(_ value: String) -> Bool {",
|
||||
" guard !value.isEmpty, value.utf8.allSatisfy({ byte in",
|
||||
" (byte >= 65 && byte <= 90) || (byte >= 97 && byte <= 122) || (byte >= 48 && byte <= 57) || byte == 45 || byte == 95",
|
||||
@@ -663,6 +790,11 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" let fraction = bytes[20..<(bytes.count - 1)]",
|
||||
" return bytes[19] == 46 && !fraction.isEmpty && fraction.count <= 9 && fraction.allSatisfy { $0 >= 48 && $0 <= 57 } && fraction.last != 48",
|
||||
"}",
|
||||
"private func validCanonicalUUID(_ value: String) -> Bool {",
|
||||
" let bytes = Array(value.utf8)",
|
||||
" guard bytes.count == 36, bytes[8] == 45, bytes[13] == 45, bytes[18] == 45, bytes[23] == 45, value != \"00000000-0000-0000-0000-000000000000\" else { return false }",
|
||||
" return bytes.enumerated().allSatisfy { index, byte in [8, 13, 18, 23].contains(index) || (byte >= 48 && byte <= 57) || (byte >= 97 && byte <= 102) }",
|
||||
"}",
|
||||
"",
|
||||
]
|
||||
for name in sorted(defs):
|
||||
@@ -704,7 +836,7 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
out.append(f" try self.init({', '.join(decoded)})")
|
||||
out.extend([" }", "", " public func validate() throws {"])
|
||||
out.extend(swift_validation(definition))
|
||||
out.extend([" }", "", " public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""])
|
||||
out.extend([" }", "", " public static func decodeJSON(_ data: Data) throws -> Self { try rejectDuplicateJSONKeys(data); return try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""])
|
||||
out.extend([
|
||||
"public func deviceRegistrationProofTranscript(serverID: Data, principalID: Data, deviceID: Data, challenge: Data, expiryUnixMilliseconds: Int64) throws -> Data {",
|
||||
" for (field, value, length) in [(\"server_id\", serverID, 16), (\"principal_id\", principalID, 16), (\"device_id\", deviceID, 16), (\"challenge\", challenge, 32)] {",
|
||||
@@ -723,8 +855,9 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
"",
|
||||
"public extension TunnelAdmissionRequest {",
|
||||
" func deviceAdmissionTranscript() -> Data {",
|
||||
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]",
|
||||
" fields.append(contentsOf: capabilities.clientDecode)",
|
||||
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.sourceRateControl, String(capabilities.videoProfiles.count), String(capabilities.audioProfiles.count)]",
|
||||
" for profile in capabilities.videoProfiles { fields.append(contentsOf: [profile.codec, String(profile.bitDepth), profile.chromaSubsampling, profile.colorSpace, profile.transferFunction]) }",
|
||||
" for profile in capabilities.audioProfiles { fields.append(contentsOf: [profile.codec, String(profile.sampleRateHz), String(profile.channels), profile.channelLayout, String(profile.packetDurationMs)]) }",
|
||||
" var transcript = \"versevdi/tunnel-admission/v1\"",
|
||||
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
||||
" return Data(transcript.utf8)",
|
||||
@@ -735,14 +868,16 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
||||
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" try selected.validate()",
|
||||
" var common = selected.clientDecode",
|
||||
" var commonVideo = selected.videoProfiles",
|
||||
" var commonAudio = selected.audioProfiles",
|
||||
" for profile in profiles.dropFirst() {",
|
||||
" try profile.validate()",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" common = common.filter { profile.clientDecode.contains($0) }",
|
||||
" if common.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" commonVideo = commonVideo.filter { profile.videoProfiles.contains($0) }",
|
||||
" commonAudio = commonAudio.filter { profile.audioProfiles.contains($0) }",
|
||||
" if commonVideo.isEmpty || commonAudio.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" }",
|
||||
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)",
|
||||
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, sourceRateControl: selected.sourceRateControl, videoProfiles: commonVideo, audioProfiles: commonAudio)",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -101,6 +102,15 @@ func evaluate(version, kind, input string) string {
|
||||
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
|
||||
Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
|
||||
CorrelationID: "correlation-1",
|
||||
SelectedDescriptor: protocol.SelectedSessionDescriptor{
|
||||
VideoProfile: protocol.VideoProfile{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"},
|
||||
AudioProfile: protocol.AudioProfile{Codec: "opus", SampleRateHz: 48000, Channels: 2, ChannelLayout: "stereo", PacketDurationMs: 5},
|
||||
DisplayMode: protocol.DisplayMode{ResolutionWidth: 1920, ResolutionHeight: 1080, Fps: 60},
|
||||
BitrateTargetKbps: 12000,
|
||||
BitrateMaximumKbps: 20000,
|
||||
Adjustment: protocol.SessionAdjustment{DisplayReason: "none", BitrateReason: "none"},
|
||||
MediaTimestampBasis: "gateway-send-wall-clock-ms",
|
||||
},
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
@@ -123,6 +133,8 @@ func evaluate(version, kind, input string) string {
|
||||
value := protocol.SessionRequest{
|
||||
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
|
||||
PoolID: parts["pool_id"], IdempotencyKey: parts["idempotency_key"],
|
||||
VideoProfiles: []protocol.VideoProfile{{Codec: "h264", BitDepth: 8, ChromaSubsampling: "4:2:0", ColorSpace: "bt709-limited", TransferFunction: "sdr"}},
|
||||
BitratePreference: protocol.BitratePreference{Mode: "auto"},
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
return "valid"
|
||||
@@ -309,6 +321,13 @@ func classifyGatewayInput(encoded string) string {
|
||||
if len(body) != 4 {
|
||||
return "invalid:length"
|
||||
}
|
||||
case 8:
|
||||
if len(body) != 8 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if body[0] > 15 || body[3] > 3 {
|
||||
return "invalid:field"
|
||||
}
|
||||
default:
|
||||
return "invalid:kind"
|
||||
}
|
||||
@@ -352,6 +371,22 @@ func classifyGatewayFeedback(encoded string) string {
|
||||
if len(body) == 0 {
|
||||
return "valid"
|
||||
}
|
||||
case 4:
|
||||
if len(body) != 24 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if binary.BigEndian.Uint64(body[16:]) == 0 || allZero(body[:16]) {
|
||||
return "invalid:field"
|
||||
}
|
||||
return "valid"
|
||||
case 5:
|
||||
if len(body) != 16 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if allZero(body) {
|
||||
return "invalid:field"
|
||||
}
|
||||
return "valid"
|
||||
default:
|
||||
return "invalid:type"
|
||||
}
|
||||
@@ -386,6 +421,15 @@ func classifyGatewayFeedback(encoded string) string {
|
||||
return "invalid:field"
|
||||
}
|
||||
|
||||
func allZero(value []byte) bool {
|
||||
for _, item := range value {
|
||||
if item != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validFECStatus(body []byte) bool {
|
||||
if len(body) != 21 || int(body[10])<<8|int(body[11]) == 0 || int(body[14])<<8|int(body[15]) > int(body[10])<<8|int(body[11]) || int(body[16])<<8|int(body[17]) > int(body[12])<<8|int(body[13]) || body[18] > 100 || body[20] == 0 || body[19] >= body[20] {
|
||||
return false
|
||||
|
||||
@@ -38,6 +38,8 @@ fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
|
||||
values.get("device_key_id").cloned().unwrap_or_default(),
|
||||
values.get("pool_id").cloned().unwrap_or_default(),
|
||||
values.get("idempotency_key").cloned().unwrap_or_default(),
|
||||
vec![VideoProfile::new("h264".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap()],
|
||||
BitratePreference::new("auto".into(), None).unwrap(),
|
||||
None,
|
||||
) {
|
||||
Ok(_) => "valid",
|
||||
@@ -184,6 +186,9 @@ fn classify_gateway_input(encoded: &str) -> &'static str {
|
||||
}
|
||||
7 if body.len() == 4 => "valid",
|
||||
7 => "invalid:length",
|
||||
8 if body.len() != 8 => "invalid:length",
|
||||
8 if body[0] > 15 || body[3] > 3 => "invalid:field",
|
||||
8 => "valid",
|
||||
_ => "invalid:kind",
|
||||
}
|
||||
}
|
||||
@@ -219,6 +224,12 @@ fn classify_gateway_feedback(encoded: &str) -> &'static str {
|
||||
2 => "invalid:field",
|
||||
3 if body.is_empty() => "valid",
|
||||
3 => "invalid:length",
|
||||
4 if body.len() == 24 && body[..16].iter().any(|value| *value != 0) && body[16..24].iter().any(|value| *value != 0) => "valid",
|
||||
4 if body.len() != 24 => "invalid:length",
|
||||
4 => "invalid:field",
|
||||
5 if body.len() == 16 && body.iter().any(|value| *value != 0) => "valid",
|
||||
5 if body.len() != 16 => "invalid:length",
|
||||
5 => "invalid:field",
|
||||
_ => "invalid:type",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ func evaluate(_ version: String, _ kind: String, _ input: String) -> String {
|
||||
guard (try? SessionRequest(
|
||||
clientDeviceId: values["client_device_id"] ?? "", deviceKeyId: values["device_key_id"] ?? "",
|
||||
poolId: values["pool_id"] ?? "", idempotencyKey: values["idempotency_key"] ?? "",
|
||||
videoProfiles: [try! VideoProfile(codec: "h264", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")],
|
||||
bitratePreference: try! BitratePreference(mode: "auto", targetKbps: nil),
|
||||
requestedDisplayMode: nil
|
||||
)) != nil else { return "invalid:required" }
|
||||
return "valid"
|
||||
@@ -136,6 +138,9 @@ func classifyGatewayInput(_ encoded: String) -> String {
|
||||
let height = Int(body[6]) * 256 + Int(body[7])
|
||||
return width > 0 && height > 0 && x < width && y < height ? "valid" : "invalid:field"
|
||||
case 7: return body.count == 4 ? "valid" : "invalid:length"
|
||||
case 8:
|
||||
guard body.count == 8 else { return "invalid:length" }
|
||||
return body[0] <= 15 && body[3] <= 3 ? "valid" : "invalid:field"
|
||||
default: return "invalid:kind"
|
||||
}
|
||||
}
|
||||
@@ -156,6 +161,12 @@ func classifyGatewayFeedback(_ encoded: String) -> String {
|
||||
case 2:
|
||||
return validFECStatus(body) ? "valid" : "invalid:field"
|
||||
case 3: return body.isEmpty ? "valid" : "invalid:length"
|
||||
case 4:
|
||||
guard body.count == 24 else { return "invalid:length" }
|
||||
return body[0...15].contains(where: { $0 != 0 }) && body[16...23].contains(where: { $0 != 0 }) ? "valid" : "invalid:field"
|
||||
case 5:
|
||||
guard body.count == 16 else { return "invalid:length" }
|
||||
return body.contains(where: { $0 != 0 }) ? "valid" : "invalid:field"
|
||||
default: return "invalid:type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ from __future__ import annotations
|
||||
import pathlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def run(command: list[str]) -> None:
|
||||
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True)
|
||||
environment = os.environ.copy()
|
||||
cache_root = pathlib.Path(tempfile.gettempdir()) / "versevdi-protocol-module-cache"
|
||||
environment.setdefault("CLANG_MODULE_CACHE_PATH", str(cache_root / "clang"))
|
||||
environment.setdefault("SWIFT_MODULECACHE_PATH", str(cache_root / "swift"))
|
||||
result = subprocess.run(command, cwd=ROOT, env=environment, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(result.stdout + result.stderr)
|
||||
print(result.stdout.strip())
|
||||
|
||||
@@ -8,13 +8,18 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def run(command: list[str], directory: pathlib.Path) -> None:
|
||||
result = subprocess.run(command, cwd=directory, text=True, capture_output=True, check=False)
|
||||
environment = os.environ.copy()
|
||||
cache_root = pathlib.Path(tempfile.gettempdir()) / "versevdi-protocol-module-cache"
|
||||
environment.setdefault("CLANG_MODULE_CACHE_PATH", str(cache_root / "clang"))
|
||||
environment.setdefault("SWIFT_MODULECACHE_PATH", str(cache_root / "swift"))
|
||||
result = subprocess.run(command, cwd=directory, env=environment, text=True, capture_output=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError("%s\n%s%s" % (" ".join(command), result.stdout, result.stderr))
|
||||
|
||||
@@ -58,21 +63,27 @@ def main() -> int:
|
||||
swift.write_text(
|
||||
"""import Foundation
|
||||
|
||||
let video = try VideoProfile(codec: "h264", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")
|
||||
let hevc = try VideoProfile(codec: "hevc", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr")
|
||||
let audio = try AudioProfile(codec: "opus", sampleRateHz: 48000, channels: 2, channelLayout: "stereo", packetDurationMs: 5)
|
||||
let display = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
|
||||
let adjustment = try SessionAdjustment(displayReason: "none", bitrateReason: "none")
|
||||
let descriptor = try SelectedSessionDescriptor(videoProfile: video, audioProfile: audio, displayMode: display, bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000, adjustment: adjustment, mediaTimestampBasis: "gateway-send-wall-clock-ms")
|
||||
let capability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
|
||||
)
|
||||
guard currentWireVersion == "2", nMinus1WireVersion == "1", nMinus2WireVersion == "0" else {
|
||||
fatalError("unexpected control wire compatibility declaration")
|
||||
}
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
|
||||
)
|
||||
do {
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [video], audioProfiles: [audio]
|
||||
)
|
||||
fatalError("unregistered framing was accepted")
|
||||
} catch { }
|
||||
@@ -82,7 +93,7 @@ let request = try TunnelAdmissionRequest(
|
||||
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
|
||||
)
|
||||
_ = request
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5"
|
||||
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
|
||||
fatalError("unexpected device admission transcript")
|
||||
}
|
||||
@@ -126,18 +137,18 @@ for (name, field, serverID, principalID, deviceID, challenge, expiry) in invalid
|
||||
}
|
||||
let incompatible = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [hevc], audioProfiles: [audio]
|
||||
)
|
||||
let gatewayCapability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus", "h264-opus"]
|
||||
sourceRateControl: "server", videoProfiles: [hevc, video], audioProfiles: [audio]
|
||||
)
|
||||
do {
|
||||
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
|
||||
fatalError("matching capability profiles did not intersect")
|
||||
}
|
||||
} catch { fatalError("matching capability profiles did not intersect") }
|
||||
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else {
|
||||
guard try CapabilityProfile.intersection([gatewayCapability, capability]).videoProfiles == [video] else {
|
||||
fatalError("ordered registered profile intersection changed")
|
||||
}
|
||||
do {
|
||||
@@ -165,17 +176,22 @@ for invalid in [
|
||||
}
|
||||
let clientAuthority = try ClientSessionAuthority(
|
||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||
reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability
|
||||
reconnectSequence: 2, expiresAt: "2099-01-01T00:00:00Z", capabilities: capability, selectedDescriptor: descriptor
|
||||
)
|
||||
let clientAuthorityJSON = try clientAuthority.encodeJSON()
|
||||
let clientAuthorityObject = try JSONSerialization.jsonObject(with: clientAuthorityJSON) as! [String: Any]
|
||||
guard Set(clientAuthorityObject.keys) == Set([
|
||||
"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"
|
||||
"version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"
|
||||
]), !String(data: clientAuthorityJSON, encoding: .utf8)!.contains("provider_") else {
|
||||
fatalError("client authority was not exactly provider-free")
|
||||
}
|
||||
_ = try ClientSessionAuthority.decodeJSON(clientAuthorityJSON)
|
||||
for field in ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities"] {
|
||||
let duplicateCapability = Data(#"{"transport":"quic-tls13","transport":"quic-tls13","framing":"datagram-v1","media":"encoded","source_rate_control":"server","video_profiles":[{"codec":"h264","bit_depth":8,"chroma_subsampling":"4:2:0","color_space":"bt709-limited","transfer_function":"sdr"}],"audio_profiles":[{"codec":"opus","sample_rate_hz":48000,"channels":2,"channel_layout":"stereo","packet_duration_ms":5}]}"#.utf8)
|
||||
do {
|
||||
_ = try CapabilityProfile.decodeJSON(duplicateCapability)
|
||||
fatalError("capability accepted duplicate JSON keys")
|
||||
} catch { }
|
||||
for field in ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "selected_descriptor"] {
|
||||
var missing = clientAuthorityObject
|
||||
missing.removeValue(forKey: field)
|
||||
do {
|
||||
@@ -217,7 +233,7 @@ do {
|
||||
)
|
||||
fatalError("invalid allocation bounds were accepted")
|
||||
} catch { }
|
||||
let displayMode = try DisplayMode(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120)
|
||||
let displayMode = display
|
||||
for invalid in [
|
||||
{ try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120) },
|
||||
{ try DisplayMode(resolutionWidth: 2560, resolutionHeight: 199, fps: 120) },
|
||||
@@ -230,14 +246,14 @@ for invalid in [
|
||||
}
|
||||
let policyFreeV2Request = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", requestedDisplayMode: nil
|
||||
idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "auto", targetKbps: nil), requestedDisplayMode: nil
|
||||
).encodeJSON()
|
||||
guard !String(data: policyFreeV2Request, encoding: .utf8)!.contains("requested_display_mode") else {
|
||||
fatalError("wire-v2 request encoded an absent display mode")
|
||||
}
|
||||
let displayRequest = try SessionRequest(
|
||||
clientDeviceId: "device-1", deviceKeyId: "key-1", poolId: "pool-1",
|
||||
idempotencyKey: "request-1", requestedDisplayMode: displayMode
|
||||
idempotencyKey: "request-1", videoProfiles: [video], bitratePreference: try BitratePreference(mode: "explicit", targetKbps: 40000), requestedDisplayMode: displayMode
|
||||
)
|
||||
guard try SessionRequest.decodeJSON(displayRequest.encodeJSON()).requestedDisplayMode == displayMode else {
|
||||
fatalError("display mode did not round-trip")
|
||||
@@ -295,14 +311,14 @@ for expiresAt in ["2099-01-01T00:00:00+00:00", "2099-01-01T00:00:00.100Z"] {
|
||||
} catch { }
|
||||
}
|
||||
let streamPolicy = try ProviderStreamPolicy(
|
||||
resolutionWidth: 2560, resolutionHeight: 1440, fps: 120,
|
||||
codec: "HEVC", bitrateKbps: 40000, audioEnabled: true
|
||||
videoProfile: hevc, audioProfile: audio, displayMode: displayMode,
|
||||
bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000
|
||||
)
|
||||
guard streamPolicy.codec == "HEVC" else { fatalError("stream policy changed") }
|
||||
guard streamPolicy.videoProfile == hevc else { fatalError("stream policy changed") }
|
||||
for invalid in [
|
||||
{ try ProviderStreamPolicy(resolutionWidth: 319, resolutionHeight: 1440, fps: 120, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
|
||||
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 241, codec: "HEVC", bitrateKbps: 40000, audioEnabled: true) },
|
||||
{ try ProviderStreamPolicy(resolutionWidth: 2560, resolutionHeight: 1440, fps: 120, codec: "VP9", bitrateKbps: 40000, audioEnabled: true) },
|
||||
{ try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: try DisplayMode(resolutionWidth: 319, resolutionHeight: 1440, fps: 120), bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
|
||||
{ try ProviderStreamPolicy(videoProfile: video, audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 50001, bitrateMaximumKbps: 50000) },
|
||||
{ try ProviderStreamPolicy(videoProfile: try VideoProfile(codec: "vp9", bitDepth: 8, chromaSubsampling: "4:2:0", colorSpace: "bt709-limited", transferFunction: "sdr"), audioProfile: audio, displayMode: displayMode, bitrateTargetKbps: 40000, bitrateMaximumKbps: 50000) },
|
||||
] {
|
||||
do {
|
||||
_ = try invalid()
|
||||
@@ -363,17 +379,23 @@ fn main() {
|
||||
assert_eq!(CURRENT_WIRE_VERSION, "2");
|
||||
assert_eq!(N_MINUS_1_WIRE_VERSION, "1");
|
||||
assert_eq!(N_MINUS_2_WIRE_VERSION, "0");
|
||||
let video = VideoProfile::new("h264".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap();
|
||||
let hevc = VideoProfile::new("hevc".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).unwrap();
|
||||
let audio = AudioProfile::new("opus".into(), 48000, 2, "stereo".into(), 5).unwrap();
|
||||
let display = DisplayMode::new(2560, 1440, 120).unwrap();
|
||||
let adjustment = SessionAdjustment::new("none".into(), "none".into()).unwrap();
|
||||
let descriptor = SelectedSessionDescriptor::new(video.clone(), audio.clone(), display.clone(), 40000, 50000, adjustment, "gateway-send-wall-clock-ms".into()).unwrap();
|
||||
let capabilities = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
"server".into(), vec![video.clone()], vec![audio.clone()],
|
||||
).unwrap();
|
||||
assert!(CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
"server".into(), vec![video.clone()], vec![audio.clone()],
|
||||
).is_ok());
|
||||
assert!(CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
"server".into(), vec![video.clone()], vec![audio.clone()],
|
||||
).is_err());
|
||||
let request = TunnelAdmissionRequest::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
@@ -381,7 +403,7 @@ fn main() {
|
||||
).unwrap();
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
|
||||
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
|
||||
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus";
|
||||
+ "10:quic-tls1311:datagram-v17:encoded6:server1:11:14:h2641:85:4:2:013:bt709-limited3:sdr4:opus5:480001:26:stereo1:5";
|
||||
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
|
||||
let proof_server_id = vec![1u8; 16];
|
||||
let proof_principal_id = vec![2u8; 16];
|
||||
@@ -423,26 +445,26 @@ fn main() {
|
||||
).is_err());
|
||||
let client_authority = ClientSessionAuthority::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
|
||||
"2099-01-01T00:00:00Z".into(), capabilities.clone(),
|
||||
"2099-01-01T00:00:00Z".into(), capabilities.clone(), descriptor.clone(),
|
||||
).unwrap();
|
||||
assert_eq!(client_authority.sessionId(), "session");
|
||||
assert_eq!(client_authority.capabilities(), &capabilities);
|
||||
assert!(ClientSessionAuthority::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(), 2,
|
||||
"not-a-time".into(), capabilities.clone(),
|
||||
"not-a-time".into(), capabilities.clone(), descriptor.clone(),
|
||||
).is_err());
|
||||
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
|
||||
let incompatible = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into()],
|
||||
"server".into(), vec![hevc.clone()], vec![audio.clone()],
|
||||
).unwrap();
|
||||
let gateway_capability = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()],
|
||||
"server".into(), vec![hevc.clone(), video.clone()], vec![audio.clone()],
|
||||
).unwrap();
|
||||
assert_eq!(
|
||||
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(),
|
||||
&vec!["h264-opus".to_string()],
|
||||
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().videoProfiles(),
|
||||
&vec![video.clone()],
|
||||
);
|
||||
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
|
||||
assert!(AllocationPolicy::new(
|
||||
@@ -454,11 +476,13 @@ fn main() {
|
||||
assert!(DisplayMode::new(2560, 1440, 241).is_err());
|
||||
let policy_free_v2_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
vec![video.clone()], BitratePreference::new("auto".into(), None).unwrap(),
|
||||
None,
|
||||
).unwrap();
|
||||
assert!(policy_free_v2_request.requestedDisplayMode().is_none());
|
||||
let display_request = SessionRequest::new(
|
||||
"device-1".into(), "key-1".into(), "pool-1".into(), "request-1".into(),
|
||||
vec![video.clone()], BitratePreference::new("explicit".into(), Some(40000)).unwrap(),
|
||||
Some(display_mode.clone()),
|
||||
).unwrap();
|
||||
assert_eq!(display_request.requestedDisplayMode(), &Some(display_mode));
|
||||
@@ -486,17 +510,13 @@ fn main() {
|
||||
).is_err());
|
||||
}
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 120, "HEVC".into(), 40000, true,
|
||||
hevc.clone(), audio.clone(), display.clone(), 40000, 50000,
|
||||
).is_ok());
|
||||
assert!(DisplayMode::new(319, 1440, 120).is_err());
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
319, 1440, 120, "HEVC".into(), 40000, true,
|
||||
).is_err());
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 241, "HEVC".into(), 40000, true,
|
||||
).is_err());
|
||||
assert!(ProviderStreamPolicy::new(
|
||||
2560, 1440, 120, "VP9".into(), 40000, true,
|
||||
video.clone(), audio.clone(), display.clone(), 50001, 50000,
|
||||
).is_err());
|
||||
assert!(VideoProfile::new("vp9".into(), 8, "4:2:0".into(), "bt709-limited".into(), "sdr".into()).is_err());
|
||||
assert!(GatewayTelemetry::new(
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ready".into(),
|
||||
).is_ok());
|
||||
@@ -532,6 +552,7 @@ fn main() {
|
||||
("reconnect_sequence", 5),
|
||||
("expires_at", 6),
|
||||
("capabilities", 7),
|
||||
("selected_descriptor", 8),
|
||||
]
|
||||
actual_protobuf_fields = protobuf_message_fields("ClientSessionAuthority")
|
||||
if actual_protobuf_fields != expected_protobuf_fields:
|
||||
|
||||
@@ -39,6 +39,20 @@ def main() -> int:
|
||||
feature_registry = json.loads((ROOT / "registries/features.json").read_text(encoding="utf-8"))
|
||||
registered_features = {entry["id"] for entry in feature_registry["features"]}
|
||||
assert {"control.v1", "control.v2", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}.issubset(registered_features)
|
||||
assert {"video.profile.v1", "session.quality.v1", "session.stop.v1", "controller.arrival.v1"}.issubset(registered_features)
|
||||
|
||||
assert defs["VideoProfile"]["required"] == ["codec", "bit_depth", "chroma_subsampling", "color_space", "transfer_function"]
|
||||
assert defs["AudioProfile"]["required"] == ["codec", "sample_rate_hz", "channels", "channel_layout", "packet_duration_ms"]
|
||||
assert defs["CapabilityProfile"]["required"] == ["transport", "framing", "media", "source_rate_control", "video_profiles", "audio_profiles"]
|
||||
assert defs["CapabilityProfile"]["properties"]["video_profiles"] == {"type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": True, "items": {"$ref": "#/$defs/VideoProfile"}}
|
||||
assert defs["CapabilityProfile"]["properties"]["audio_profiles"] == {"type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": True, "items": {"$ref": "#/$defs/AudioProfile"}}
|
||||
assert defs["SessionRequest"]["required"][-2:] == ["video_profiles", "bitrate_preference"]
|
||||
assert defs["ReconnectRequest"]["required"][-1] == "display_relaunch_confirmed"
|
||||
assert defs["AssignedDesktop"]["required"][-1] == "quality_limits"
|
||||
assert defs["EntitledPool"]["required"][-1] == "quality_limits"
|
||||
for owner in ("SessionAuthority", "ClientSessionAuthority"):
|
||||
assert defs[owner]["required"][-1] == "selected_descriptor"
|
||||
assert defs["ConnectionManifest"]["required"][-1] == "selected_descriptor"
|
||||
|
||||
display_mode = defs["DisplayMode"]
|
||||
assert display_mode["required"] == ["resolution_width", "resolution_height", "fps"]
|
||||
@@ -113,6 +127,17 @@ def main() -> int:
|
||||
assert "native_identity" not in native_missing_fixture
|
||||
tunnel_credential_fixture = json.loads((ROOT / "fixtures/valid/native-tunnel-credential.json").read_text(encoding="utf-8"))
|
||||
assert set(tunnel_credential_fixture) == set(tunnel_credential["required"])
|
||||
valid_fixture_contracts = {
|
||||
"fixtures/valid/session-request.json": "SessionRequest",
|
||||
"fixtures/valid/selected-session-descriptor.json": "SelectedSessionDescriptor",
|
||||
"fixtures/valid/session-quality-limits.json": "SessionQualityLimits",
|
||||
}
|
||||
for relative, definition in valid_fixture_contracts.items():
|
||||
fixture = json.loads((ROOT / relative).read_text(encoding="utf-8"))
|
||||
assert set(fixture) == set(defs[definition]["required"]), (relative, definition)
|
||||
assert set(json.loads((ROOT / "fixtures/invalid/capability-rc5-opaque.json").read_text())) & {"audio", "client_decode"} == {"audio", "client_decode"}
|
||||
assert "video_profiles" not in json.loads((ROOT / "fixtures/invalid/session-request-rc5.json").read_text())
|
||||
assert set(json.loads((ROOT / "fixtures/invalid/provider-stream-policy-rc5.json").read_text())) == {"resolution_width", "resolution_height", "fps", "codec", "bitrate_kbps", "audio_enabled"}
|
||||
|
||||
expected_header = "id\tversion\tkind\tinput\texpected"
|
||||
ids = set()
|
||||
@@ -139,10 +164,49 @@ def main() -> int:
|
||||
fixture_hash.update((ROOT / relative).read_bytes())
|
||||
fixture_hash.update(b"\0")
|
||||
assert fixture_manifest["corpus_sha256"] == fixture_hash.hexdigest()
|
||||
json_fixture_paths = sorted(
|
||||
path.relative_to(ROOT).as_posix()
|
||||
for directory in (ROOT / "fixtures/valid", ROOT / "fixtures/invalid")
|
||||
for path in directory.glob("*.json")
|
||||
)
|
||||
assert fixture_manifest["json_files"] == json_fixture_paths
|
||||
json_fixture_hash = hashlib.sha256()
|
||||
for relative in json_fixture_paths:
|
||||
json_fixture_hash.update(relative.encode("utf-8"))
|
||||
json_fixture_hash.update(b"\0")
|
||||
json_fixture_hash.update((ROOT / relative).read_bytes())
|
||||
json_fixture_hash.update(b"\0")
|
||||
assert fixture_manifest["json_corpus_sha256"] == json_fixture_hash.hexdigest()
|
||||
|
||||
openapi = (ROOT / "openapi/control-v1.yaml").read_text(encoding="utf-8")
|
||||
assert "openapi: 3.1.0" in openapi
|
||||
assert "/api/v1/auth/refresh:" in openapi and "/api/v1/resources:" in openapi and "/api/v1/events:" in openapi
|
||||
for route in (
|
||||
"/api/v1/session-quality-limits:",
|
||||
"/api/v1/session-quality-limits/assignments/{assignment_id}:",
|
||||
"/api/v1/session-quality-limits/pools/{pool_id}:",
|
||||
"/api/v1/admin/entitlements/{entitlement_id}/display-limit-override:",
|
||||
"/api/v1/broker/sessions/{session_id}/quality-changes:",
|
||||
"/api/v1/broker/sessions/{session_id}/quality-changes/{operation_id}:",
|
||||
"/api/v1/broker/sessions/{session_id}/stop-operations:",
|
||||
"/api/v1/broker/sessions/{session_id}/stop-operations/{operation_id}:",
|
||||
"/api/v1/gateway/quality-work:",
|
||||
"/api/v1/gateway/quality-ack:",
|
||||
"/api/v1/gateway/stop-work:",
|
||||
"/api/v1/gateway/stop-ack:",
|
||||
):
|
||||
assert route in openapi, route
|
||||
for operation_id in (
|
||||
"getSessionQualityLimits", "getAssignmentSessionQualityLimits", "getPoolSessionQualityLimits",
|
||||
"createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation",
|
||||
):
|
||||
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "nativeBearer: []" in operation and "browserSession" not in operation, operation_id
|
||||
for operation_id in (
|
||||
"acquireGatewayQualityWork", "acknowledgeGatewayQualityWork", "acquireGatewayStopWork", "acknowledgeGatewayStopWork",
|
||||
):
|
||||
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert "gatewayMutualTLS: []" in operation and "nativeBearer" not in operation and "browserSession" not in operation, operation_id
|
||||
assert "provider_url" not in openapi and "vm_address" not in openapi
|
||||
session_endpoint = openapi.split(" /api/v1/auth/session:", 1)[1].split("\n /api/", 1)[0]
|
||||
assert "$defs/BrowserAuthenticatedSession" in session_endpoint
|
||||
@@ -172,6 +236,8 @@ def main() -> int:
|
||||
browserCsrfCookie: []
|
||||
browserCsrfHeader: []
|
||||
"""
|
||||
admin_override = openapi.split(" operationId: updateEntitlementDisplayLimitOverride\n", 1)[1].split(" responses:\n", 1)[0]
|
||||
assert browser_requirement.removeprefix(" ") in admin_override
|
||||
for operation_id in (
|
||||
"issueReauthenticationGrant", "logoutSession", "registerDevice", "proveDevice", "revokeDevice",
|
||||
"requestBrokerSession", "allocateBrokerSession", "reconnectBrokerSession", "cancelBrokerSession",
|
||||
|
||||
@@ -44,6 +44,10 @@ def classify_input(raw: bytes) -> str:
|
||||
return "valid" if width and height and x < width and y < height else "invalid:field"
|
||||
if kind == 7:
|
||||
return "valid" if len(body) == 4 else "invalid:length"
|
||||
if kind == 8:
|
||||
if len(body) != 8:
|
||||
return "invalid:length"
|
||||
return "valid" if body[0] <= 15 and body[3] <= 3 else "invalid:field"
|
||||
return "invalid:kind"
|
||||
|
||||
|
||||
@@ -68,6 +72,14 @@ def classify_feedback(raw: bytes) -> str:
|
||||
return "valid" if valid_fec_status(body) else "invalid:field"
|
||||
if kind == 3:
|
||||
return "valid" if not body else "invalid:length"
|
||||
if kind == 4:
|
||||
if len(body) != 24:
|
||||
return "invalid:length"
|
||||
return "valid" if any(body[:16]) and int.from_bytes(body[16:24], "big") > 0 else "invalid:field"
|
||||
if kind == 5:
|
||||
if len(body) != 16:
|
||||
return "invalid:length"
|
||||
return "valid" if any(body) else "invalid:field"
|
||||
return "invalid:type"
|
||||
if kind in (1, 2, 3):
|
||||
return "invalid:direction"
|
||||
|
||||
Reference in New Issue
Block a user