protocol: add generated bindings and conformance fixtures
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn values(input: &str) -> std::collections::BTreeMap<String, String> {
|
||||
input
|
||||
.split(';')
|
||||
.filter_map(|item| item.split_once('='))
|
||||
.map(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn evaluate(kind: &str, input: &str) -> &'static str {
|
||||
let values = values(input);
|
||||
match kind {
|
||||
"version" if matches!(input, "1" | "0" | "-1") => "valid",
|
||||
"version" => "invalid:unsupported_version",
|
||||
"page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) {
|
||||
Some(limit) if (1..=100).contains(&limit) => "valid",
|
||||
_ => "invalid:invalid_limit",
|
||||
},
|
||||
"manifest" if ["provider_url", "vm_address", "password", "private_key"]
|
||||
.iter()
|
||||
.any(|key| values.contains_key(*key)) => "invalid:forbidden_field",
|
||||
"manifest"
|
||||
if values.get("version").map(String::as_str) == Some("1")
|
||||
&& values.contains_key("gateway_id")
|
||||
&& values.get("grant").map_or(false, |value| value.len() >= 43)
|
||||
&& values.get("purpose").map(String::as_str) == Some("launch") => "valid",
|
||||
"manifest" => "invalid:invalid_manifest",
|
||||
"clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8")
|
||||
&& !values.contains_key("file") => "valid",
|
||||
"clipboard" => "invalid:unsupported_clipboard",
|
||||
"event" if values.get("version").map(String::as_str) != Some("1") => {
|
||||
"invalid:unsupported_version"
|
||||
}
|
||||
"event" if values.get("after").and_then(|value| value.parse::<i64>().ok()).is_some()
|
||||
&& values.get("earliest").and_then(|value| value.parse::<i64>().ok()).is_some()
|
||||
&& values["after"].parse::<i64>().unwrap() > 0
|
||||
&& values["earliest"].parse::<i64>().unwrap() > 0
|
||||
&& values["after"].parse::<i64>().unwrap() < values["earliest"].parse::<i64>().unwrap() - 1 => {
|
||||
"invalid:gap"
|
||||
}
|
||||
"event" if values.get("payload_bytes").and_then(|value| value.parse::<usize>().ok()).map_or(true, |size| size > 16384) => {
|
||||
"invalid:payload_limit"
|
||||
}
|
||||
"event" if values.get("sequence").and_then(|value| value.parse::<i64>().ok()).map_or(true, |sequence| sequence < 1)
|
||||
|| !values.contains_key("correlation_id") => "invalid:required",
|
||||
"event" => "valid",
|
||||
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("1") | Some("0") | Some("-1"))
|
||||
&& values.get("feature").map(String::as_str) == Some("control.v1") => "valid",
|
||||
"tunnel" if values.get("feature").map(String::as_str) != Some("control.v1") => {
|
||||
"invalid:unsupported_feature"
|
||||
}
|
||||
"tunnel" => "invalid:unsupported_version",
|
||||
"datagram" => classify_datagram(values.get("hex").map(String::as_str).unwrap_or_default()),
|
||||
_ => "invalid:unknown_kind",
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_hex(input: &str) -> Option<Vec<u8>> {
|
||||
if input.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
(0..input.len())
|
||||
.step_by(2)
|
||||
.map(|index| u8::from_str_radix(&input[index..index + 2], 16).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn classify_datagram(encoded: &str) -> &'static str {
|
||||
let raw = match decode_hex(encoded) {
|
||||
Some(raw) => raw,
|
||||
None => return "invalid:hex",
|
||||
};
|
||||
if raw.len() < 21 {
|
||||
return "invalid:truncated";
|
||||
}
|
||||
if raw[0..2] != *b"VD" {
|
||||
return "invalid:magic";
|
||||
}
|
||||
if raw[2] != 1 {
|
||||
return "invalid:unsupported_version";
|
||||
}
|
||||
let limit = match raw[3] {
|
||||
1 => 1024,
|
||||
2 => 2048,
|
||||
3 => 65515,
|
||||
_ => return "invalid:unknown_channel",
|
||||
};
|
||||
if raw[4] != 0 {
|
||||
return "invalid:flags";
|
||||
}
|
||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
||||
return "invalid:fragment";
|
||||
}
|
||||
let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize;
|
||||
if payload_length > limit {
|
||||
return "invalid:payload_limit";
|
||||
}
|
||||
if raw.len() != 21 + payload_length {
|
||||
return "invalid:length_mismatch";
|
||||
}
|
||||
if raw.len() > 65536 {
|
||||
return "invalid:frame_limit";
|
||||
}
|
||||
"valid"
|
||||
}
|
||||
|
||||
fn normalized_digest(results: &[String]) -> String {
|
||||
let mut value: u64 = 14695981039346656037;
|
||||
for result in results {
|
||||
for byte in format!("{result}\n").bytes() {
|
||||
value ^= u64::from(byte);
|
||||
value = value.wrapping_mul(1099511628211);
|
||||
}
|
||||
}
|
||||
format!("{value:016x}")
|
||||
}
|
||||
|
||||
fn fixture_hash() -> String {
|
||||
let text = fs::read_to_string("fixtures/manifest.json").expect("fixture manifest");
|
||||
text.split("\"corpus_sha256\": \"")
|
||||
.nth(1)
|
||||
.and_then(|value| value.split('"').next())
|
||||
.expect("fixture hash")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut paths: Vec<PathBuf> = fs::read_dir("fixtures/conformance")
|
||||
.expect("fixture corpus")
|
||||
.map(|entry| entry.expect("fixture entry").path())
|
||||
.filter(|path| path.extension().and_then(|value| value.to_str()) == Some("tsv"))
|
||||
.collect();
|
||||
paths.sort();
|
||||
let mut results = Vec::new();
|
||||
for path in paths {
|
||||
let text = fs::read_to_string(path).expect("fixture file");
|
||||
let mut lines = text.lines();
|
||||
assert_eq!(lines.next(), Some("id\tversion\tkind\tinput\texpected"));
|
||||
for line in lines {
|
||||
let fields: Vec<&str> = line.split('\t').collect();
|
||||
assert_eq!(fields.len(), 5);
|
||||
let actual = evaluate(fields[2], fields[3]);
|
||||
assert_eq!(actual, fields[4], "{}", fields[0]);
|
||||
results.push(format!("{}\t{}", fields[0], actual));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Rust conformance passed normalized={} fixtures={}",
|
||||
normalized_digest(&results),
|
||||
fixture_hash()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user