feat(protocol): freeze M4 native session RC6 contract
This commit is contained in:
+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)",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user