use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::error::ParseError;
use crate::framing::project_dto;
use crate::limits::Limits;
use crate::logs::{validate_log_record, LogRecord};
use crate::marker::MAX_SAFE_INTEGER;
use crate::roles::{valid_capability, Capability, ADAPTER_CAPABILITIES};
use crate::tree::Snapshot;
use crate::validate::validate_snapshot;
use crate::Violation;
pub const PROTOCOL_ID: &str = "termwright/3";
pub const PROTOCOL_VERSION: u8 = 3;
const MAX_IDENTIFIER_LENGTH: usize = 1024;
const ERROR_CODES: [&str; 7] = [
"bad-token",
"bad-version",
"malformed",
"limit-exceeded",
"duplicate-semantic-key",
"adapter-guarantee-violation",
"internal",
];
const LIMIT_FIELDS: [&str; 11] = [
"maxFrameBytes",
"maxSnapshotBytes",
"maxNodes",
"maxDepth",
"maxStringBytes",
"maxRelationTargets",
"maxQueuedFrames",
"maxPendingWaiters",
"maxSessions",
"maxLogRecordBytes",
"maxLogQueue",
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Hello {
#[serde(rename = "type")]
pub kind: String,
pub protocol: String,
pub token: String,
pub adapter: AdapterInfo,
pub capabilities: Vec<Capability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub probe: Option<ProbeInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub providers: Vec<EvidenceProviderRegistration>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EvidenceProviderRegistration {
pub id: String,
pub version: String,
pub method: String,
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProbeIdentityKind {
Stable,
FrameLocal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProbeInjectionTier {
T0,
T1,
T2,
T3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProbeSemanticClass {
A,
B,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DegradedSessionCapability {
SemanticTree,
StableIdentity,
IntendedGeometry,
ClippedGeometry,
PaintedRegion,
PointerGeometry,
PointerHitTesting,
Focus,
Scroll,
RenderOrder,
ActionStrategies,
KeyboardInput,
PointerInput,
FocusInput,
PairedRevisions,
InactiveScreenTree,
CustomContainerEnumeration,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeInstrumentation {
pub highest_tier: ProbeInjectionTier,
pub semantic_class: ProbeSemanticClass,
pub degraded_capabilities: Vec<DegradedSessionCapability>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeInfo {
pub framework: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub framework_version: Option<String>,
pub probe_version: String,
pub identity_kind: ProbeIdentityKind,
pub capabilities: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instrumentation: Option<ProbeInstrumentation>,
}
impl ProbeInfo {
pub fn validate(&self) -> Result<(), Violation> {
const CAPABILITIES: &[&str] = &[
"stable-identity",
"intended-rect",
"visible-rect",
"operations",
"annotations",
"frame-begin",
"paint-order",
];
if self.framework.is_empty() || self.probe_version.is_empty() {
return Err(Violation::new(
"schema",
"probe framework and probeVersion must be non-empty",
));
}
for (index, capability) in self.capabilities.iter().enumerate() {
if !CAPABILITIES.contains(&capability.as_str()) {
return Err(Violation::new(
"schema",
format!("unknown probe capability {capability}"),
));
}
if self.capabilities[..index].contains(capability) {
return Err(Violation::new(
"schema",
format!("duplicate probe capability {capability}"),
));
}
}
if self.identity_kind == ProbeIdentityKind::FrameLocal
&& self
.capabilities
.iter()
.any(|capability| capability == "stable-identity")
{
return Err(Violation::new(
"schema",
"frame-local identity cannot advertise stable-identity",
));
}
if self.instrumentation.is_none() {
return Err(Violation::new(
"schema",
"instrumentation is required for every probe",
));
}
if let Some(instrumentation) = &self.instrumentation {
for (index, capability) in instrumentation.degraded_capabilities.iter().enumerate() {
if instrumentation.degraded_capabilities[..index].contains(capability) {
return Err(Violation::new("schema", "duplicate degraded capability"));
}
}
if instrumentation.semantic_class == ProbeSemanticClass::B
&& (!instrumentation
.degraded_capabilities
.contains(&DegradedSessionCapability::IntendedGeometry)
|| !instrumentation
.degraded_capabilities
.contains(&DegradedSessionCapability::ClippedGeometry))
{
return Err(Violation::new(
"schema",
"semantic class B requires intended-geometry and clipped-geometry degradations",
));
}
}
Ok(())
}
}
impl Hello {
pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
Self {
kind: "hello".into(),
protocol: PROTOCOL_ID.into(),
token: token.to_owned(),
adapter: AdapterInfo {
name: name.to_owned(),
version: version.to_owned(),
},
capabilities,
probe: None,
providers: Vec::new(),
}
}
#[must_use]
pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
self.probe = Some(probe);
self
}
#[must_use]
pub fn with_providers(mut self, providers: Vec<EvidenceProviderRegistration>) -> Self {
self.providers = providers;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarkerConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogBudget {
pub enabled: bool,
pub max_records_per_second: i64,
pub burst: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HelloAck {
#[serde(rename = "type")]
pub kind: String,
pub protocol: String,
pub session_id: String,
pub limits: Limits,
pub subscribe: String,
pub marker: MarkerConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logs: Option<LogBudget>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevisionCommit {
#[serde(rename = "type")]
pub kind: &'static str,
pub revision: i64,
}
impl RevisionCommit {
pub fn new(revision: i64) -> Self {
Self {
kind: "revision-commit",
revision,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SemanticFullMessage<'a> {
#[serde(rename = "type")]
pub kind: &'static str,
pub snapshot: &'a Snapshot,
}
impl<'a> SemanticFullMessage<'a> {
pub fn new(snapshot: &'a Snapshot) -> Self {
Self {
kind: "semantic-full",
snapshot,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct LogMessage<'a> {
#[serde(rename = "type")]
pub kind: &'static str,
pub record: &'a LogRecord,
}
impl<'a> LogMessage<'a> {
pub fn new(record: &'a LogRecord) -> Self {
Self {
kind: "log",
record,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtocolErrorMessage {
#[serde(rename = "type")]
pub kind: String,
pub code: String,
pub message: String,
}
impl ProtocolErrorMessage {
pub fn new(code: &str, message: impl Into<String>) -> Self {
Self {
kind: "error".into(),
code: code.to_owned(),
message: message.into(),
}
}
}
pub fn default_capabilities() -> Vec<Capability> {
vec![
Capability::Tree,
Capability::IntendedGeometry,
Capability::ClippedGeometry,
Capability::States,
Capability::Actions,
Capability::RenderRevisions,
]
}
fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
project_dto(value, limits.max_depth).map_err(|violation| {
if violation.code == "dto-depth" {
ParseError::new("limit-exceeded", violation.to_string())
} else {
ParseError::malformed(violation.to_string())
}
})
}
fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
let object = value
.as_object()
.ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
let kind = object
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
Ok((object, kind))
}
fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
for key in required {
if !object.contains_key(*key) {
return Err(ParseError::malformed(format!("missing field \"{key}\"")));
}
}
Ok(())
}
fn require_keys(
object: &Map<String, Value>,
required: &[&str],
optional: &[&str],
) -> Result<(), ParseError> {
for key in required {
if !object.contains_key(*key) {
return Err(ParseError::malformed(format!("missing field \"{key}\"")));
}
}
for key in object.keys() {
if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
}
}
Ok(())
}
fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
let Some(text) = object.get(key).and_then(Value::as_str) else {
return Err(ParseError::malformed(format!("{key}: expected a string")));
};
if text.len() > MAX_IDENTIFIER_LENGTH {
return Err(ParseError::malformed(format!(
"{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
)));
}
if !allow_empty && text.is_empty() {
return Err(ParseError::malformed(format!(
"{key}: expected a non-empty string"
)));
}
Ok(())
}
fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
let number = object
.get(key)
.and_then(Value::as_i64)
.filter(|n| n.abs() <= MAX_SAFE_INTEGER);
match number {
Some(number) if positive && number > 0 => Ok(()),
Some(number) if !positive && number >= 0 => Ok(()),
_ if positive => Err(ParseError::malformed(format!(
"{key}: expected a positive safe integer"
))),
_ => Err(ParseError::malformed(format!(
"{key}: expected a non-negative safe integer"
))),
}
}
fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
match validate_snapshot(value, limits) {
Ok(()) => Ok(()),
Err(error) => {
let code = match error.code {
"bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
_ => "malformed",
};
Err(ParseError::new(code, format!("snapshot {error}")))
}
}
}
fn check_log_budget(value: &Value) -> Result<(), ParseError> {
let budget = value
.as_object()
.ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
if !budget["enabled"].is_boolean() {
return Err(ParseError::malformed("logs.enabled: expected a boolean"));
}
whole_number(budget, "maxRecordsPerSecond", true)?;
whole_number(budget, "burst", false)
}
fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
if strict {
require_keys(object, &["type", "code", "message"], &[])?;
} else {
required_keys(object, &["type", "code", "message"])?;
}
let code = object
.get("code")
.and_then(Value::as_str)
.unwrap_or_default();
if !ERROR_CODES.contains(&code) {
return Err(ParseError::malformed("code: unknown error code"));
}
identifier(object, "message", true)
}
fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
match object.get("protocol").and_then(Value::as_str) {
Some(protocol) if protocol != PROTOCOL_ID => Err(ParseError::new(
"bad-version",
format!("unsupported protocol {protocol}"),
)),
_ => Ok(()),
}
}
pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
project(value, limits)?;
let (object, kind) = as_message(value)?;
match kind {
"hello" => {
check_protocol_field(object)?;
require_keys(
object,
&["type", "protocol", "token", "adapter", "capabilities"],
&["probe", "providers"],
)?;
identifier(object, "token", false)?;
let adapter = object
.get("adapter")
.and_then(Value::as_object)
.ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
require_keys(adapter, &["name", "version"], &[])?;
identifier(adapter, "name", false)?;
identifier(adapter, "version", false)?;
let capabilities = object
.get("capabilities")
.and_then(Value::as_array)
.ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
if capabilities.len() > ADAPTER_CAPABILITIES.len() {
return Err(ParseError::malformed("capabilities: too many entries"));
}
for item in capabilities {
match item.as_str() {
Some(name) if valid_capability(name) => {}
_ => return Err(ParseError::malformed("capabilities: unknown capability")),
}
}
Ok(())
}
"revision-commit" => {
require_keys(object, &["type", "revision"], &[])?;
whole_number(object, "revision", true)
}
"semantic-full" => {
require_keys(object, &["type", "snapshot"], &[])?;
check_embedded_snapshot(&object["snapshot"], limits)
}
"log" => {
require_keys(object, &["type", "record"], &[])?;
check_embedded_log_record(&object["record"], limits)
}
"error" => check_error_message(object, true),
_ => Err(ParseError::malformed("unknown or missing message type")),
}
}
fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
match validate_log_record(value, limits) {
Ok(()) => Ok(()),
Err(error) => {
let code = match error.code {
"bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
_ => "malformed",
};
Err(ParseError::new(code, format!("log record {error}")))
}
}
}
pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
project(value, limits)?;
let (object, kind) = as_message(value)?;
match kind {
"hello-ack" => {
check_protocol_field(object)?;
required_keys(
object,
&[
"type",
"protocol",
"sessionId",
"limits",
"subscribe",
"marker",
],
)?;
identifier(object, "sessionId", false)?;
let limits_object = object
.get("limits")
.and_then(Value::as_object)
.ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
required_keys(limits_object, &LIMIT_FIELDS)?;
for field in LIMIT_FIELDS {
whole_number(limits_object, field, true)?;
}
match object.get("subscribe").and_then(Value::as_str) {
Some("semantic") => {}
_ => return Err(ParseError::malformed("subscribe: expected 'semantic'")),
}
let marker = object
.get("marker")
.and_then(Value::as_object)
.ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
required_keys(marker, &["enabled"])?;
if !marker["enabled"].is_boolean() {
return Err(ParseError::malformed("marker.enabled: expected a boolean"));
}
if let Some(logs) = object.get("logs") {
check_log_budget(logs)?;
}
Ok(())
}
"semantic-resync-request" => {
required_keys(
object,
&["type", "sessionId", "expectedBaseRevision", "reason"],
)?;
identifier(object, "sessionId", false)?;
if !object["expectedBaseRevision"].is_null() {
whole_number(object, "expectedBaseRevision", true)?;
}
match object["reason"].as_str() {
Some("base-mismatch" | "missing-base" | "driver-reset") => Ok(()),
_ => Err(ParseError::malformed(
"reason: unknown semantic resync reason",
)),
}
}
"error" => check_error_message(object, false),
_ => Err(ParseError::malformed("unknown or missing message type")),
}
}