feat(protocol): define gateway control envelopes
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
@@ -87,9 +88,9 @@ func evaluate(kind, input string) string {
|
||||
Gateway: protocol.ManifestGateway{
|
||||
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["gateway_id"],
|
||||
},
|
||||
Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}},
|
||||
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
|
||||
Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
|
||||
Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}},
|
||||
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
|
||||
Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
|
||||
CorrelationID: "correlation-1",
|
||||
}
|
||||
if value.Validate() == nil {
|
||||
@@ -118,7 +119,7 @@ func evaluate(kind, input string) string {
|
||||
}
|
||||
value := protocol.EventEnvelope{
|
||||
EventID: "event-1", Sequence: sequence, Type: "broker.session.changed", Version: 1,
|
||||
Resource: protocol.ResourceLink{Type: "broker_session", ID: "session-1", Version: 1},
|
||||
Resource: protocol.ResourceLink{Type: "broker_session", ID: "session-1", Version: 1},
|
||||
OccurredAt: "2099-01-01T00:00:00Z", CorrelationID: parts["correlation_id"], Payload: map[string]any{},
|
||||
}
|
||||
if payloadErr != nil || payloadBytes > 16384 {
|
||||
@@ -138,11 +139,178 @@ func evaluate(kind, input string) string {
|
||||
return "invalid:unsupported_version"
|
||||
case "datagram":
|
||||
return classifyDatagram(parts["hex"])
|
||||
case "gateway_input":
|
||||
return classifyGatewayInput(parts["hex"])
|
||||
case "gateway_feedback":
|
||||
return classifyGatewayFeedback(parts["hex"])
|
||||
case "gateway_clipboard":
|
||||
if _, hasFile := parts["file"]; hasFile {
|
||||
return "invalid:forbidden"
|
||||
}
|
||||
value := protocol.GatewayClipboardText{Direction: parts["direction"], Text: parts["text"], Encoding: parts["encoding"], LoopToken: parts["loop_token"]}
|
||||
if value.Validate() != nil {
|
||||
return "invalid:clipboard"
|
||||
}
|
||||
return "valid"
|
||||
case "gateway_clipboard_audit":
|
||||
if _, hasText := parts["text"]; hasText {
|
||||
return "invalid:forbidden"
|
||||
}
|
||||
textBytes, err := strconv.ParseInt(parts["text_bytes"], 10, 64)
|
||||
if err != nil {
|
||||
return "invalid:clipboard_audit"
|
||||
}
|
||||
value := protocol.GatewayClipboardAudit{Version: "1", SessionID: "fixture-session", Direction: parts["direction"], Outcome: parts["outcome"], TextBytes: textBytes, Reason: parts["reason"]}
|
||||
if value.Validate() != nil {
|
||||
return "invalid:clipboard_audit"
|
||||
}
|
||||
return "valid"
|
||||
default:
|
||||
return "invalid:unknown_kind"
|
||||
}
|
||||
}
|
||||
|
||||
func decodeGatewayHex(encoded string) ([]byte, string) {
|
||||
raw, err := hex.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, "invalid:hex"
|
||||
}
|
||||
return raw, ""
|
||||
}
|
||||
|
||||
func classifyGatewayInput(encoded string) string {
|
||||
raw, invalid := decodeGatewayHex(encoded)
|
||||
if invalid != "" {
|
||||
return invalid
|
||||
}
|
||||
if len(raw) < 6 {
|
||||
return "invalid:truncated"
|
||||
}
|
||||
if string(raw[:4]) != "VGI1" {
|
||||
return "invalid:magic"
|
||||
}
|
||||
kind, length := raw[4], int(raw[5])
|
||||
if len(raw) != 6+length {
|
||||
return "invalid:length"
|
||||
}
|
||||
body := raw[6:]
|
||||
switch kind {
|
||||
case 1:
|
||||
if len(body) != 4 || body[0] > 1 || (body[2] == 0 && body[3] == 0) {
|
||||
return "invalid:field"
|
||||
}
|
||||
case 2:
|
||||
if len(body) != 3 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if body[0] > 1 || body[1] < 1 || body[1] > 5 {
|
||||
return "invalid:field"
|
||||
}
|
||||
if body[2] != 0 {
|
||||
return "invalid:reserved"
|
||||
}
|
||||
case 3:
|
||||
if len(body) != 4 {
|
||||
return "invalid:length"
|
||||
}
|
||||
case 4:
|
||||
if len(body) < 1 || len(body) > 4 || !utf8.Valid(body) || utf8.RuneCount(body) != 1 {
|
||||
return "invalid:utf8"
|
||||
}
|
||||
case 5:
|
||||
if len(body) != 17 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if body[0] > 15 {
|
||||
return "invalid:field"
|
||||
}
|
||||
if body[1] == 0 && body[2] == 0 {
|
||||
for _, value := range body[3:] {
|
||||
if value != 0 {
|
||||
return "invalid:field"
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return "invalid:kind"
|
||||
}
|
||||
return "valid"
|
||||
}
|
||||
|
||||
func classifyGatewayFeedback(encoded string) string {
|
||||
raw, invalid := decodeGatewayHex(encoded)
|
||||
if invalid != "" {
|
||||
return invalid
|
||||
}
|
||||
if len(raw) < 8 {
|
||||
return "invalid:truncated"
|
||||
}
|
||||
if string(raw[:4]) != "VGF1" {
|
||||
return "invalid:magic"
|
||||
}
|
||||
direction, kind := raw[4], raw[5]
|
||||
if len(raw) != 8+(int(raw[6])<<8)+int(raw[7]) {
|
||||
return "invalid:length"
|
||||
}
|
||||
if direction != 0 && direction != 1 {
|
||||
return "invalid:direction"
|
||||
}
|
||||
body := raw[8:]
|
||||
if direction == 0 {
|
||||
if kind >= 0x10 && kind <= 0x12 {
|
||||
return "invalid:direction"
|
||||
}
|
||||
switch kind {
|
||||
case 1:
|
||||
if len(body) == 0 {
|
||||
return "valid"
|
||||
}
|
||||
case 2:
|
||||
if validFECStatus(body) {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:field"
|
||||
default:
|
||||
return "invalid:type"
|
||||
}
|
||||
return "invalid:length"
|
||||
}
|
||||
if kind == 1 || kind == 2 {
|
||||
return "invalid:direction"
|
||||
}
|
||||
switch kind {
|
||||
case 0x10:
|
||||
if len(body) == 4 {
|
||||
return "valid"
|
||||
}
|
||||
return "invalid:length"
|
||||
case 0x11:
|
||||
if len(body) != 5 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if body[0] <= 15 {
|
||||
return "valid"
|
||||
}
|
||||
case 0x12:
|
||||
if len(body) != 1 {
|
||||
return "invalid:length"
|
||||
}
|
||||
if body[0] <= 1 {
|
||||
return "valid"
|
||||
}
|
||||
default:
|
||||
return "invalid:type"
|
||||
}
|
||||
return "invalid:field"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func classifyDatagram(encoded string) string {
|
||||
raw, err := hex.DecodeString(encoded)
|
||||
if err != nil {
|
||||
|
||||
@@ -53,10 +53,119 @@ fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||
}
|
||||
"tunnel" => "invalid:unsupported_version",
|
||||
"datagram" => classify_datagram(values.get("hex").map(String::as_str).unwrap_or_default()),
|
||||
"gateway_input" => classify_gateway_input(values.get("hex").map(String::as_str).unwrap_or_default()),
|
||||
"gateway_feedback" => classify_gateway_feedback(values.get("hex").map(String::as_str).unwrap_or_default()),
|
||||
"gateway_clipboard" if values.contains_key("file") => "invalid:forbidden",
|
||||
"gateway_clipboard"
|
||||
if matches!(values.get("direction").map(String::as_str), Some("client_to_provider") | Some("provider_to_client"))
|
||||
&& values.get("encoding").map(String::as_str) == Some("utf-8")
|
||||
&& values.get("loop_token").map_or(false, |value| (16..=128).contains(&value.len()))
|
||||
&& values.get("text").map_or(false, |value| value.len() <= 65536) => "valid",
|
||||
"gateway_clipboard" => "invalid:clipboard",
|
||||
"gateway_clipboard_audit" if values.contains_key("text") => "invalid:forbidden",
|
||||
"gateway_clipboard_audit"
|
||||
if matches!(values.get("direction").map(String::as_str), Some("client_to_provider") | Some("provider_to_client"))
|
||||
&& matches!(values.get("outcome").map(String::as_str), Some("forwarded") | Some("suppressed") | Some("rejected"))
|
||||
&& matches!(values.get("reason").map(String::as_str), Some("forwarded") | Some("loop") | Some("policy") | Some("rate") | Some("provider") | Some("malformed"))
|
||||
&& values.get("text_bytes").and_then(|value| value.parse::<usize>().ok()).map_or(false, |size| size <= 65536) => "valid",
|
||||
"gateway_clipboard_audit" => "invalid:clipboard_audit",
|
||||
_ => "invalid:unknown_kind",
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_gateway_input(encoded: &str) -> &'static str {
|
||||
let raw = match decode_hex(encoded) {
|
||||
Some(raw) => raw,
|
||||
None => return "invalid:hex",
|
||||
};
|
||||
if raw.len() < 6 {
|
||||
return "invalid:truncated";
|
||||
}
|
||||
if raw[0..4] != *b"VGI1" {
|
||||
return "invalid:magic";
|
||||
}
|
||||
let kind = raw[4];
|
||||
let body = &raw[6..];
|
||||
if body.len() != raw[5] as usize {
|
||||
return "invalid:length";
|
||||
}
|
||||
match kind {
|
||||
1 if body.len() == 4 && body[0] <= 1 && (body[2] != 0 || body[3] != 0) => "valid",
|
||||
1 => "invalid:field",
|
||||
2 if body.len() != 3 => "invalid:length",
|
||||
2 if body[0] > 1 || !(1..=5).contains(&body[1]) => "invalid:field",
|
||||
2 if body[2] != 0 => "invalid:reserved",
|
||||
2 => "valid",
|
||||
3 if body.len() == 4 => "valid",
|
||||
3 => "invalid:length",
|
||||
4 if (1..=4).contains(&body.len()) && std::str::from_utf8(body).ok().map_or(false, |value| value.chars().count() == 1) => "valid",
|
||||
4 => "invalid:utf8",
|
||||
5 if body.len() != 17 => "invalid:length",
|
||||
5 if body[0] > 15 => "invalid:field",
|
||||
5 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field",
|
||||
5 => "valid",
|
||||
_ => "invalid:kind",
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_gateway_feedback(encoded: &str) -> &'static str {
|
||||
let raw = match decode_hex(encoded) {
|
||||
Some(raw) => raw,
|
||||
None => return "invalid:hex",
|
||||
};
|
||||
if raw.len() < 8 {
|
||||
return "invalid:truncated";
|
||||
}
|
||||
if raw[0..4] != *b"VGF1" {
|
||||
return "invalid:magic";
|
||||
}
|
||||
let direction = raw[4];
|
||||
let kind = raw[5];
|
||||
let body = &raw[8..];
|
||||
if body.len() != ((raw[6] as usize) << 8 | raw[7] as usize) {
|
||||
return "invalid:length";
|
||||
}
|
||||
if direction > 1 {
|
||||
return "invalid:direction";
|
||||
}
|
||||
if direction == 0 {
|
||||
if (0x10..=0x12).contains(&kind) {
|
||||
return "invalid:direction";
|
||||
}
|
||||
return match kind {
|
||||
1 if body.is_empty() => "valid",
|
||||
1 => "invalid:length",
|
||||
2 if valid_fec_status(body) => "valid",
|
||||
2 => "invalid:field",
|
||||
_ => "invalid:type",
|
||||
};
|
||||
}
|
||||
if kind == 1 || kind == 2 {
|
||||
return "invalid:direction";
|
||||
}
|
||||
match kind {
|
||||
0x10 if body.len() == 4 => "valid",
|
||||
0x10 => "invalid:length",
|
||||
0x11 if body.len() != 5 => "invalid:length",
|
||||
0x11 if body[0] <= 15 => "valid",
|
||||
0x11 => "invalid:field",
|
||||
0x12 if body.len() != 1 => "invalid:length",
|
||||
0x12 if body[0] <= 1 => "valid",
|
||||
0x12 => "invalid:field",
|
||||
_ => "invalid:type",
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_fec_status(body: &[u8]) -> bool {
|
||||
body.len() == 21
|
||||
&& ((body[10] as u16) << 8 | body[11] as u16) > 0
|
||||
&& ((body[14] as u16) << 8 | body[15] as u16) <= ((body[10] as u16) << 8 | body[11] as u16)
|
||||
&& ((body[16] as u16) << 8 | body[17] as u16) <= ((body[12] as u16) << 8 | body[13] as u16)
|
||||
&& body[18] <= 100
|
||||
&& body[20] > 0
|
||||
&& body[19] < body[20]
|
||||
}
|
||||
|
||||
fn decode_hex(input: &str) -> Option<Vec<u8>> {
|
||||
if input.len() % 2 != 0 {
|
||||
return None;
|
||||
|
||||
@@ -30,18 +30,100 @@ func evaluate(_ kind: String, _ input: String) -> String {
|
||||
if ["1", "0", "-1"].contains(values["offered"] ?? "") && values["feature"] == "control.v1" { return "valid" }
|
||||
return values["feature"] == "control.v1" ? "invalid:unsupported_version" : "invalid:unsupported_feature"
|
||||
case "datagram": return classifyDatagram(values["hex"] ?? "")
|
||||
case "gateway_input": return classifyGatewayInput(values["hex"] ?? "")
|
||||
case "gateway_feedback": return classifyGatewayFeedback(values["hex"] ?? "")
|
||||
case "gateway_clipboard":
|
||||
if values["file"] != nil { return "invalid:forbidden" }
|
||||
guard ["client_to_provider", "provider_to_client"].contains(values["direction"] ?? ""), values["encoding"] == "utf-8", let token = values["loop_token"], (16...128).contains(token.utf8.count), let text = values["text"], text.utf8.count <= 65536 else { return "invalid:clipboard" }
|
||||
return "valid"
|
||||
case "gateway_clipboard_audit":
|
||||
if values["text"] != nil { return "invalid:forbidden" }
|
||||
guard ["client_to_provider", "provider_to_client"].contains(values["direction"] ?? ""), ["forwarded", "suppressed", "rejected"].contains(values["outcome"] ?? ""), ["forwarded", "loop", "policy", "rate", "provider", "malformed"].contains(values["reason"] ?? ""), let textBytes = Int(values["text_bytes"] ?? ""), (0...65536).contains(textBytes) else { return "invalid:clipboard_audit" }
|
||||
return "valid"
|
||||
default: return "invalid:unknown_kind"
|
||||
}
|
||||
}
|
||||
|
||||
func classifyDatagram(_ encoded: String) -> String {
|
||||
func decodeHex(_ encoded: String) -> [UInt8]? {
|
||||
let characters = Array(encoded)
|
||||
guard characters.count % 2 == 0 else { return "invalid:hex" }
|
||||
guard characters.count % 2 == 0 else { return nil }
|
||||
var raw: [UInt8] = []
|
||||
for index in stride(from: 0, to: characters.count, by: 2) {
|
||||
guard let byte = UInt8(String(characters[index...index + 1]), radix: 16) else { return "invalid:hex" }
|
||||
guard let byte = UInt8(String(characters[index...index + 1]), radix: 16) else { return nil }
|
||||
raw.append(byte)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func classifyGatewayInput(_ encoded: String) -> String {
|
||||
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
||||
guard raw.count >= 6 else { return "invalid:truncated" }
|
||||
guard Array(raw[0..<4]) == Array("VGI1".utf8) else { return "invalid:magic" }
|
||||
let kind = raw[4]
|
||||
let body = Array(raw.dropFirst(6))
|
||||
guard body.count == Int(raw[5]) else { return "invalid:length" }
|
||||
switch kind {
|
||||
case 1:
|
||||
return body.count == 4 && body[0] <= 1 && (body[2] != 0 || body[3] != 0) ? "valid" : "invalid:field"
|
||||
case 2:
|
||||
guard body.count == 3 else { return "invalid:length" }
|
||||
guard body[0] <= 1 && (1...5).contains(body[1]) else { return "invalid:field" }
|
||||
return body[2] == 0 ? "valid" : "invalid:reserved"
|
||||
case 3: return body.count == 4 ? "valid" : "invalid:length"
|
||||
case 4:
|
||||
guard (1...4).contains(body.count), let scalar = String(bytes: body, encoding: .utf8), scalar.unicodeScalars.count == 1 else { return "invalid:utf8" }
|
||||
return "valid"
|
||||
case 5:
|
||||
guard body.count == 17 else { return "invalid:length" }
|
||||
guard body[0] <= 15 else { return "invalid:field" }
|
||||
guard body[1] != 0 || body[2] != 0 || body.dropFirst(3).allSatisfy({ $0 == 0 }) else { return "invalid:field" }
|
||||
return "valid"
|
||||
default: return "invalid:kind"
|
||||
}
|
||||
}
|
||||
|
||||
func classifyGatewayFeedback(_ encoded: String) -> String {
|
||||
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
||||
guard raw.count >= 8 else { return "invalid:truncated" }
|
||||
guard Array(raw[0..<4]) == Array("VGF1".utf8) else { return "invalid:magic" }
|
||||
let direction = raw[4]
|
||||
let kind = raw[5]
|
||||
let body = Array(raw.dropFirst(8))
|
||||
guard body.count == Int(raw[6]) * 256 + Int(raw[7]) else { return "invalid:length" }
|
||||
guard direction <= 1 else { return "invalid:direction" }
|
||||
if direction == 0 {
|
||||
if (0x10...0x12).contains(kind) { return "invalid:direction" }
|
||||
switch kind {
|
||||
case 1: return body.isEmpty ? "valid" : "invalid:length"
|
||||
case 2:
|
||||
return validFECStatus(body) ? "valid" : "invalid:field"
|
||||
default: return "invalid:type"
|
||||
}
|
||||
}
|
||||
if kind == 1 || kind == 2 { return "invalid:direction" }
|
||||
switch kind {
|
||||
case 0x10: return body.count == 4 ? "valid" : "invalid:length"
|
||||
case 0x11:
|
||||
guard body.count == 5 else { return "invalid:length" }
|
||||
return body[0] <= 15 ? "valid" : "invalid:field"
|
||||
case 0x12:
|
||||
guard body.count == 1 else { return "invalid:length" }
|
||||
return body[0] <= 1 ? "valid" : "invalid:field"
|
||||
default: return "invalid:type"
|
||||
}
|
||||
}
|
||||
|
||||
func validFECStatus(_ body: [UInt8]) -> Bool {
|
||||
guard body.count == 21 else { return false }
|
||||
let totalData = Int(body[10]) * 256 + Int(body[11])
|
||||
let totalParity = Int(body[12]) * 256 + Int(body[13])
|
||||
let receivedData = Int(body[14]) * 256 + Int(body[15])
|
||||
let receivedParity = Int(body[16]) * 256 + Int(body[17])
|
||||
return totalData > 0 && receivedData <= totalData && receivedParity <= totalParity && body[18] <= 100 && body[20] > 0 && body[19] < body[20]
|
||||
}
|
||||
|
||||
func classifyDatagram(_ encoded: String) -> String {
|
||||
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
||||
guard raw.count >= 21 else { return "invalid:truncated" }
|
||||
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
||||
guard raw[2] == 1 else { return "invalid:unsupported_version" }
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate fixed-byte Phase 3C gateway input and feedback envelopes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def classify_input(raw: bytes) -> str:
|
||||
if len(raw) < 6:
|
||||
return "invalid:truncated"
|
||||
if raw[:4] != b"VGI1":
|
||||
return "invalid:magic"
|
||||
kind, length = raw[4], raw[5]
|
||||
if len(raw) != 6 + length:
|
||||
return "invalid:length"
|
||||
body = raw[6:]
|
||||
if kind == 1:
|
||||
return "valid" if len(body) == 4 and body[0] <= 1 and body[2:4] != b"\0\0" else "invalid:field"
|
||||
if kind == 2:
|
||||
return "valid" if len(body) == 3 and body[0] <= 1 and 1 <= body[1] <= 5 and body[2] == 0 else "invalid:reserved"
|
||||
if kind == 3:
|
||||
return "valid" if len(body) == 4 else "invalid:length"
|
||||
if kind == 4:
|
||||
try:
|
||||
decoded = body.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "invalid:utf8"
|
||||
return "valid" if 1 <= len(body) <= 4 and len(decoded) == 1 else "invalid:utf8"
|
||||
if kind == 5:
|
||||
if len(body) != 17:
|
||||
return "invalid:length"
|
||||
if body[0] > 15:
|
||||
return "invalid:field"
|
||||
active_mask = int.from_bytes(body[1:3], "big")
|
||||
return "valid" if active_mask or not any(body[3:]) else "invalid:field"
|
||||
return "invalid:kind"
|
||||
|
||||
|
||||
def classify_feedback(raw: bytes) -> str:
|
||||
if len(raw) < 8:
|
||||
return "invalid:truncated"
|
||||
if raw[:4] != b"VGF1":
|
||||
return "invalid:magic"
|
||||
direction, kind = raw[4], raw[5]
|
||||
length = int.from_bytes(raw[6:8], "big")
|
||||
if len(raw) != 8 + length:
|
||||
return "invalid:length"
|
||||
if direction not in (0, 1):
|
||||
return "invalid:direction"
|
||||
body = raw[8:]
|
||||
if direction == 0:
|
||||
if kind in (0x10, 0x11, 0x12):
|
||||
return "invalid:direction"
|
||||
if kind == 1:
|
||||
return "valid" if not body else "invalid:length"
|
||||
if kind == 2:
|
||||
return "valid" if valid_fec_status(body) else "invalid:field"
|
||||
return "invalid:type"
|
||||
if kind in (1, 2):
|
||||
return "invalid:direction"
|
||||
if kind == 0x10:
|
||||
return "valid" if len(body) == 4 else "invalid:length"
|
||||
if kind == 0x11:
|
||||
return "valid" if len(body) == 5 and body[0] <= 15 else "invalid:field"
|
||||
if kind == 0x12:
|
||||
return "valid" if len(body) == 1 and body[0] <= 1 else "invalid:field"
|
||||
return "invalid:type"
|
||||
|
||||
|
||||
def valid_fec_status(body: bytes) -> bool:
|
||||
return len(body) == 21 and int.from_bytes(body[10:12], "big") > 0 and int.from_bytes(body[14:16], "big") <= int.from_bytes(body[10:12], "big") and int.from_bytes(body[16:18], "big") <= int.from_bytes(body[12:14], "big") and body[18] <= 100 and body[20] > 0 and body[19] < body[20]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = (ROOT / "fixtures/conformance/gateway-input-feedback-v1.tsv").read_text(encoding="utf-8").splitlines()
|
||||
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
||||
for line in lines[1:]:
|
||||
identifier, version, kind, input_value, expected = line.split("\t")
|
||||
assert version == "1"
|
||||
try:
|
||||
raw = binascii.unhexlify(input_value.removeprefix("hex="))
|
||||
except binascii.Error:
|
||||
actual = "invalid:hex"
|
||||
else:
|
||||
actual = classify_input(raw) if kind == "gateway_input" else classify_feedback(raw)
|
||||
assert actual == expected, f"{identifier}: {actual} != {expected}"
|
||||
print("Gateway input/feedback envelope validation passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user