1use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::error::ParseError;
12use crate::framing::project_dto;
13use crate::limits::Limits;
14use crate::logs::{validate_log_record, LogRecord};
15use crate::marker::MAX_SAFE_INTEGER;
16use crate::roles::{valid_capability, Capability, ADAPTER_CAPABILITIES};
17use crate::tree::Snapshot;
18use crate::validate::validate_snapshot;
19use crate::Violation;
20
21pub const PROTOCOL_ID: &str = "termwright/3";
23
24pub const PROTOCOL_VERSION: u8 = 3;
26
27const MAX_IDENTIFIER_LENGTH: usize = 1024;
29
30const ERROR_CODES: [&str; 7] = [
31 "bad-token",
32 "bad-version",
33 "malformed",
34 "limit-exceeded",
35 "duplicate-semantic-key",
36 "adapter-guarantee-violation",
37 "internal",
38];
39
40const LIMIT_FIELDS: [&str; 11] = [
41 "maxFrameBytes",
42 "maxSnapshotBytes",
43 "maxNodes",
44 "maxDepth",
45 "maxStringBytes",
46 "maxRelationTargets",
47 "maxQueuedFrames",
48 "maxPendingWaiters",
49 "maxSessions",
50 "maxLogRecordBytes",
51 "maxLogQueue",
52];
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct AdapterInfo {
57 pub name: String,
59 pub version: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct Hello {
66 #[serde(rename = "type")]
68 pub kind: String,
69 pub protocol: String,
71 pub token: String,
73 pub adapter: AdapterInfo,
75 pub capabilities: Vec<Capability>,
77 #[serde(skip_serializing_if = "Option::is_none")]
83 pub probe: Option<ProbeInfo>,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub providers: Vec<EvidenceProviderRegistration>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct EvidenceProviderRegistration {
93 pub id: String,
95 pub version: String,
97 pub method: String,
99 pub capabilities: Vec<String>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "kebab-case")]
110pub enum ProbeIdentityKind {
111 Stable,
113 FrameLocal,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119pub enum ProbeInjectionTier {
120 T0,
122 T1,
124 T2,
126 T3,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum ProbeSemanticClass {
133 A,
135 B,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "kebab-case")]
142pub enum DegradedSessionCapability {
143 SemanticTree,
145 StableIdentity,
147 IntendedGeometry,
149 ClippedGeometry,
151 PaintedRegion,
153 PointerGeometry,
155 PointerHitTesting,
157 Focus,
159 Scroll,
161 RenderOrder,
163 ActionStrategies,
165 KeyboardInput,
167 PointerInput,
169 FocusInput,
171 PairedRevisions,
173 InactiveScreenTree,
175 CustomContainerEnumeration,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct ProbeInstrumentation {
183 pub highest_tier: ProbeInjectionTier,
185 pub semantic_class: ProbeSemanticClass,
187 pub degraded_capabilities: Vec<DegradedSessionCapability>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct ProbeInfo {
195 pub framework: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub framework_version: Option<String>,
200 pub probe_version: String,
202 pub identity_kind: ProbeIdentityKind,
204 pub capabilities: Vec<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub instrumentation: Option<ProbeInstrumentation>,
209}
210
211impl ProbeInfo {
212 pub fn validate(&self) -> Result<(), Violation> {
214 const CAPABILITIES: &[&str] = &[
215 "stable-identity",
216 "intended-rect",
217 "visible-rect",
218 "operations",
219 "annotations",
220 "frame-begin",
221 "paint-order",
222 ];
223 if self.framework.is_empty() || self.probe_version.is_empty() {
224 return Err(Violation::new(
225 "schema",
226 "probe framework and probeVersion must be non-empty",
227 ));
228 }
229 for (index, capability) in self.capabilities.iter().enumerate() {
230 if !CAPABILITIES.contains(&capability.as_str()) {
231 return Err(Violation::new(
232 "schema",
233 format!("unknown probe capability {capability}"),
234 ));
235 }
236 if self.capabilities[..index].contains(capability) {
237 return Err(Violation::new(
238 "schema",
239 format!("duplicate probe capability {capability}"),
240 ));
241 }
242 }
243 if self.identity_kind == ProbeIdentityKind::FrameLocal
244 && self
245 .capabilities
246 .iter()
247 .any(|capability| capability == "stable-identity")
248 {
249 return Err(Violation::new(
250 "schema",
251 "frame-local identity cannot advertise stable-identity",
252 ));
253 }
254 if self.instrumentation.is_none() {
255 return Err(Violation::new(
256 "schema",
257 "instrumentation is required for every probe",
258 ));
259 }
260 if let Some(instrumentation) = &self.instrumentation {
261 for (index, capability) in instrumentation.degraded_capabilities.iter().enumerate() {
262 if instrumentation.degraded_capabilities[..index].contains(capability) {
263 return Err(Violation::new("schema", "duplicate degraded capability"));
264 }
265 }
266 if instrumentation.semantic_class == ProbeSemanticClass::B
267 && (!instrumentation
268 .degraded_capabilities
269 .contains(&DegradedSessionCapability::IntendedGeometry)
270 || !instrumentation
271 .degraded_capabilities
272 .contains(&DegradedSessionCapability::ClippedGeometry))
273 {
274 return Err(Violation::new(
275 "schema",
276 "semantic class B requires intended-geometry and clipped-geometry degradations",
277 ));
278 }
279 }
280 Ok(())
281 }
282}
283
284impl Hello {
285 pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
287 Self {
288 kind: "hello".into(),
289 protocol: PROTOCOL_ID.into(),
290 token: token.to_owned(),
291 adapter: AdapterInfo {
292 name: name.to_owned(),
293 version: version.to_owned(),
294 },
295 capabilities,
296 probe: None,
297 providers: Vec::new(),
298 }
299 }
300
301 #[must_use]
303 pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
304 self.probe = Some(probe);
305 self
306 }
307
308 #[must_use]
310 pub fn with_providers(mut self, providers: Vec<EvidenceProviderRegistration>) -> Self {
311 self.providers = providers;
312 self
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
318pub struct MarkerConfig {
319 pub enabled: bool,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct LogBudget {
329 pub enabled: bool,
331 pub max_records_per_second: i64,
333 pub burst: i64,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct HelloAck {
341 #[serde(rename = "type")]
343 pub kind: String,
344 pub protocol: String,
346 pub session_id: String,
348 pub limits: Limits,
350 pub subscribe: String,
352 pub marker: MarkerConfig,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub logs: Option<LogBudget>,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
361pub struct RevisionCommit {
362 #[serde(rename = "type")]
364 pub kind: &'static str,
365 pub revision: i64,
367}
368
369impl RevisionCommit {
370 pub fn new(revision: i64) -> Self {
372 Self {
373 kind: "revision-commit",
374 revision,
375 }
376 }
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
381pub struct SemanticFullMessage<'a> {
382 #[serde(rename = "type")]
384 pub kind: &'static str,
385 pub snapshot: &'a Snapshot,
387}
388
389impl<'a> SemanticFullMessage<'a> {
390 pub fn new(snapshot: &'a Snapshot) -> Self {
392 Self {
393 kind: "semantic-full",
394 snapshot,
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize)]
401pub struct LogMessage<'a> {
402 #[serde(rename = "type")]
404 pub kind: &'static str,
405 pub record: &'a LogRecord,
407}
408
409impl<'a> LogMessage<'a> {
410 pub fn new(record: &'a LogRecord) -> Self {
412 Self {
413 kind: "log",
414 record,
415 }
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct ProtocolErrorMessage {
422 #[serde(rename = "type")]
424 pub kind: String,
425 pub code: String,
427 pub message: String,
429}
430
431impl ProtocolErrorMessage {
432 pub fn new(code: &str, message: impl Into<String>) -> Self {
434 Self {
435 kind: "error".into(),
436 code: code.to_owned(),
437 message: message.into(),
438 }
439 }
440}
441
442pub fn default_capabilities() -> Vec<Capability> {
444 vec![
445 Capability::Tree,
446 Capability::IntendedGeometry,
447 Capability::ClippedGeometry,
448 Capability::States,
449 Capability::Actions,
450 Capability::RenderRevisions,
451 ]
452}
453
454fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
457 project_dto(value, limits.max_depth).map_err(|violation| {
458 if violation.code == "dto-depth" {
459 ParseError::new("limit-exceeded", violation.to_string())
460 } else {
461 ParseError::malformed(violation.to_string())
462 }
463 })
464}
465
466fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
467 let object = value
468 .as_object()
469 .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
470 let kind = object
471 .get("type")
472 .and_then(Value::as_str)
473 .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
474 Ok((object, kind))
475}
476
477fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
479 for key in required {
480 if !object.contains_key(*key) {
481 return Err(ParseError::malformed(format!("missing field \"{key}\"")));
482 }
483 }
484 Ok(())
485}
486
487fn require_keys(
488 object: &Map<String, Value>,
489 required: &[&str],
490 optional: &[&str],
491) -> Result<(), ParseError> {
492 for key in required {
493 if !object.contains_key(*key) {
494 return Err(ParseError::malformed(format!("missing field \"{key}\"")));
495 }
496 }
497 for key in object.keys() {
498 if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
499 return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
500 }
501 }
502 Ok(())
503}
504
505fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
506 let Some(text) = object.get(key).and_then(Value::as_str) else {
507 return Err(ParseError::malformed(format!("{key}: expected a string")));
508 };
509 if text.len() > MAX_IDENTIFIER_LENGTH {
510 return Err(ParseError::malformed(format!(
511 "{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
512 )));
513 }
514 if !allow_empty && text.is_empty() {
515 return Err(ParseError::malformed(format!(
516 "{key}: expected a non-empty string"
517 )));
518 }
519 Ok(())
520}
521
522fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
523 let number = object
524 .get(key)
525 .and_then(Value::as_i64)
526 .filter(|n| n.abs() <= MAX_SAFE_INTEGER);
527 match number {
528 Some(number) if positive && number > 0 => Ok(()),
529 Some(number) if !positive && number >= 0 => Ok(()),
530 _ if positive => Err(ParseError::malformed(format!(
531 "{key}: expected a positive safe integer"
532 ))),
533 _ => Err(ParseError::malformed(format!(
534 "{key}: expected a non-negative safe integer"
535 ))),
536 }
537}
538
539fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
540 match validate_snapshot(value, limits) {
541 Ok(()) => Ok(()),
542 Err(error) => {
543 let code = match error.code {
544 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
545 _ => "malformed",
546 };
547 Err(ParseError::new(code, format!("snapshot {error}")))
548 }
549 }
550}
551
552fn check_log_budget(value: &Value) -> Result<(), ParseError> {
554 let budget = value
555 .as_object()
556 .ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
557 required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
558 if !budget["enabled"].is_boolean() {
559 return Err(ParseError::malformed("logs.enabled: expected a boolean"));
560 }
561 whole_number(budget, "maxRecordsPerSecond", true)?;
562 whole_number(budget, "burst", false)
563}
564
565fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
566 if strict {
567 require_keys(object, &["type", "code", "message"], &[])?;
568 } else {
569 required_keys(object, &["type", "code", "message"])?;
570 }
571 let code = object
572 .get("code")
573 .and_then(Value::as_str)
574 .unwrap_or_default();
575 if !ERROR_CODES.contains(&code) {
576 return Err(ParseError::malformed("code: unknown error code"));
577 }
578 identifier(object, "message", true)
579}
580
581fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
582 match object.get("protocol").and_then(Value::as_str) {
583 Some(protocol) if protocol != PROTOCOL_ID => Err(ParseError::new(
584 "bad-version",
585 format!("unsupported protocol {protocol}"),
586 )),
587 _ => Ok(()),
588 }
589}
590
591pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
600 project(value, limits)?;
601 let (object, kind) = as_message(value)?;
602
603 match kind {
604 "hello" => {
605 check_protocol_field(object)?;
606 require_keys(
607 object,
608 &["type", "protocol", "token", "adapter", "capabilities"],
609 &["probe", "providers"],
610 )?;
611 identifier(object, "token", false)?;
612 let adapter = object
613 .get("adapter")
614 .and_then(Value::as_object)
615 .ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
616 require_keys(adapter, &["name", "version"], &[])?;
617 identifier(adapter, "name", false)?;
618 identifier(adapter, "version", false)?;
619 let capabilities = object
620 .get("capabilities")
621 .and_then(Value::as_array)
622 .ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
623 if capabilities.len() > ADAPTER_CAPABILITIES.len() {
624 return Err(ParseError::malformed("capabilities: too many entries"));
625 }
626 for item in capabilities {
627 match item.as_str() {
628 Some(name) if valid_capability(name) => {}
629 _ => return Err(ParseError::malformed("capabilities: unknown capability")),
630 }
631 }
632 Ok(())
633 }
634 "revision-commit" => {
635 require_keys(object, &["type", "revision"], &[])?;
636 whole_number(object, "revision", true)
637 }
638 "semantic-full" => {
639 require_keys(object, &["type", "snapshot"], &[])?;
640 check_embedded_snapshot(&object["snapshot"], limits)
641 }
642 "log" => {
643 require_keys(object, &["type", "record"], &[])?;
644 check_embedded_log_record(&object["record"], limits)
645 }
646 "error" => check_error_message(object, true),
647 _ => Err(ParseError::malformed("unknown or missing message type")),
648 }
649}
650
651fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
654 match validate_log_record(value, limits) {
655 Ok(()) => Ok(()),
656 Err(error) => {
657 let code = match error.code {
658 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
659 _ => "malformed",
660 };
661 Err(ParseError::new(code, format!("log record {error}")))
662 }
663 }
664}
665
666pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
683 project(value, limits)?;
684 let (object, kind) = as_message(value)?;
685
686 match kind {
687 "hello-ack" => {
688 check_protocol_field(object)?;
689 required_keys(
690 object,
691 &[
692 "type",
693 "protocol",
694 "sessionId",
695 "limits",
696 "subscribe",
697 "marker",
698 ],
699 )?;
700 identifier(object, "sessionId", false)?;
701 let limits_object = object
702 .get("limits")
703 .and_then(Value::as_object)
704 .ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
705 required_keys(limits_object, &LIMIT_FIELDS)?;
708 for field in LIMIT_FIELDS {
709 whole_number(limits_object, field, true)?;
710 }
711 match object.get("subscribe").and_then(Value::as_str) {
712 Some("semantic") => {}
713 _ => return Err(ParseError::malformed("subscribe: expected 'semantic'")),
714 }
715 let marker = object
716 .get("marker")
717 .and_then(Value::as_object)
718 .ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
719 required_keys(marker, &["enabled"])?;
720 if !marker["enabled"].is_boolean() {
721 return Err(ParseError::malformed("marker.enabled: expected a boolean"));
722 }
723 if let Some(logs) = object.get("logs") {
724 check_log_budget(logs)?;
725 }
726 Ok(())
727 }
728 "semantic-resync-request" => {
729 required_keys(
730 object,
731 &["type", "sessionId", "expectedBaseRevision", "reason"],
732 )?;
733 identifier(object, "sessionId", false)?;
734 if !object["expectedBaseRevision"].is_null() {
735 whole_number(object, "expectedBaseRevision", true)?;
736 }
737 match object["reason"].as_str() {
738 Some("base-mismatch" | "missing-base" | "driver-reset") => Ok(()),
739 _ => Err(ParseError::malformed(
740 "reason: unknown semantic resync reason",
741 )),
742 }
743 }
744 "error" => check_error_message(object, false),
745 _ => Err(ParseError::malformed("unknown or missing message type")),
746 }
747}