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/2";
23
24pub const PROTOCOL_VERSION: u8 = 2;
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 let Some(instrumentation) = &self.instrumentation {
255 for (index, capability) in instrumentation.degraded_capabilities.iter().enumerate() {
256 if instrumentation.degraded_capabilities[..index].contains(capability) {
257 return Err(Violation::new("schema", "duplicate degraded capability"));
258 }
259 }
260 if instrumentation.semantic_class == ProbeSemanticClass::B
261 && (!instrumentation
262 .degraded_capabilities
263 .contains(&DegradedSessionCapability::IntendedGeometry)
264 || !instrumentation
265 .degraded_capabilities
266 .contains(&DegradedSessionCapability::ClippedGeometry))
267 {
268 return Err(Violation::new(
269 "schema",
270 "semantic class B requires intended-geometry and clipped-geometry degradations",
271 ));
272 }
273 }
274 Ok(())
275 }
276}
277
278impl Hello {
279 pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
281 Self {
282 kind: "hello".into(),
283 protocol: PROTOCOL_ID.into(),
284 token: token.to_owned(),
285 adapter: AdapterInfo {
286 name: name.to_owned(),
287 version: version.to_owned(),
288 },
289 capabilities,
290 probe: None,
291 providers: Vec::new(),
292 }
293 }
294
295 #[must_use]
297 pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
298 self.probe = Some(probe);
299 self
300 }
301
302 #[must_use]
304 pub fn with_providers(mut self, providers: Vec<EvidenceProviderRegistration>) -> Self {
305 self.providers = providers;
306 self
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312pub struct MarkerConfig {
313 pub enabled: bool,
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct LogBudget {
323 pub enabled: bool,
325 pub max_records_per_second: i64,
327 pub burst: i64,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334pub struct HelloAck {
335 #[serde(rename = "type")]
337 pub kind: String,
338 pub protocol: String,
340 pub session_id: String,
342 pub limits: Limits,
344 pub subscribe: String,
346 pub marker: MarkerConfig,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub logs: Option<LogBudget>,
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
355pub struct RevisionCommit {
356 #[serde(rename = "type")]
358 pub kind: &'static str,
359 pub revision: i64,
361}
362
363impl RevisionCommit {
364 pub fn new(revision: i64) -> Self {
366 Self {
367 kind: "revision-commit",
368 revision,
369 }
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
375pub struct SnapshotMessage<'a> {
376 #[serde(rename = "type")]
378 pub kind: &'static str,
379 pub snapshot: &'a Snapshot,
381}
382
383impl<'a> SnapshotMessage<'a> {
384 pub fn new(snapshot: &'a Snapshot) -> Self {
386 Self {
387 kind: "snapshot",
388 snapshot,
389 }
390 }
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize)]
395pub struct LogMessage<'a> {
396 #[serde(rename = "type")]
398 pub kind: &'static str,
399 pub record: &'a LogRecord,
401}
402
403impl<'a> LogMessage<'a> {
404 pub fn new(record: &'a LogRecord) -> Self {
406 Self {
407 kind: "log",
408 record,
409 }
410 }
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct ProtocolErrorMessage {
416 #[serde(rename = "type")]
418 pub kind: String,
419 pub code: String,
421 pub message: String,
423}
424
425impl ProtocolErrorMessage {
426 pub fn new(code: &str, message: impl Into<String>) -> Self {
428 Self {
429 kind: "error".into(),
430 code: code.to_owned(),
431 message: message.into(),
432 }
433 }
434}
435
436pub fn default_capabilities() -> Vec<Capability> {
438 vec![
439 Capability::Tree,
440 Capability::IntendedGeometry,
441 Capability::ClippedGeometry,
442 Capability::States,
443 Capability::Actions,
444 Capability::RenderRevisions,
445 ]
446}
447
448fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
451 project_dto(value, limits.max_depth).map_err(|violation| {
452 if violation.code == "dto-depth" {
453 ParseError::new("limit-exceeded", violation.to_string())
454 } else {
455 ParseError::malformed(violation.to_string())
456 }
457 })
458}
459
460fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
461 let object = value
462 .as_object()
463 .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
464 let kind = object
465 .get("type")
466 .and_then(Value::as_str)
467 .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
468 Ok((object, kind))
469}
470
471fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
473 for key in required {
474 if !object.contains_key(*key) {
475 return Err(ParseError::malformed(format!("missing field \"{key}\"")));
476 }
477 }
478 Ok(())
479}
480
481fn require_keys(
482 object: &Map<String, Value>,
483 required: &[&str],
484 optional: &[&str],
485) -> Result<(), ParseError> {
486 for key in required {
487 if !object.contains_key(*key) {
488 return Err(ParseError::malformed(format!("missing field \"{key}\"")));
489 }
490 }
491 for key in object.keys() {
492 if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
493 return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
494 }
495 }
496 Ok(())
497}
498
499fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
500 let Some(text) = object.get(key).and_then(Value::as_str) else {
501 return Err(ParseError::malformed(format!("{key}: expected a string")));
502 };
503 if text.len() > MAX_IDENTIFIER_LENGTH {
504 return Err(ParseError::malformed(format!(
505 "{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
506 )));
507 }
508 if !allow_empty && text.is_empty() {
509 return Err(ParseError::malformed(format!(
510 "{key}: expected a non-empty string"
511 )));
512 }
513 Ok(())
514}
515
516fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
517 let number = object
518 .get(key)
519 .and_then(Value::as_i64)
520 .filter(|n| n.abs() <= MAX_SAFE_INTEGER);
521 match number {
522 Some(number) if positive && number > 0 => Ok(()),
523 Some(number) if !positive && number >= 0 => Ok(()),
524 _ if positive => Err(ParseError::malformed(format!(
525 "{key}: expected a positive safe integer"
526 ))),
527 _ => Err(ParseError::malformed(format!(
528 "{key}: expected a non-negative safe integer"
529 ))),
530 }
531}
532
533fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
534 match validate_snapshot(value, limits) {
535 Ok(()) => Ok(()),
536 Err(error) => {
537 let code = match error.code {
538 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
539 _ => "malformed",
540 };
541 Err(ParseError::new(code, format!("snapshot {error}")))
542 }
543 }
544}
545
546fn check_log_budget(value: &Value) -> Result<(), ParseError> {
548 let budget = value
549 .as_object()
550 .ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
551 required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
552 if !budget["enabled"].is_boolean() {
553 return Err(ParseError::malformed("logs.enabled: expected a boolean"));
554 }
555 whole_number(budget, "maxRecordsPerSecond", true)?;
556 whole_number(budget, "burst", false)
557}
558
559fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
560 if strict {
561 require_keys(object, &["type", "code", "message"], &[])?;
562 } else {
563 required_keys(object, &["type", "code", "message"])?;
564 }
565 let code = object
566 .get("code")
567 .and_then(Value::as_str)
568 .unwrap_or_default();
569 if !ERROR_CODES.contains(&code) {
570 return Err(ParseError::malformed("code: unknown error code"));
571 }
572 identifier(object, "message", true)
573}
574
575fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
576 match object.get("protocol").and_then(Value::as_str) {
577 Some(protocol) if protocol != PROTOCOL_ID => Err(ParseError::new(
578 "bad-version",
579 format!("unsupported protocol {protocol}"),
580 )),
581 _ => Ok(()),
582 }
583}
584
585pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
594 project(value, limits)?;
595 let (object, kind) = as_message(value)?;
596
597 match kind {
598 "hello" => {
599 check_protocol_field(object)?;
600 require_keys(
601 object,
602 &["type", "protocol", "token", "adapter", "capabilities"],
603 &["probe", "providers"],
604 )?;
605 identifier(object, "token", false)?;
606 let adapter = object
607 .get("adapter")
608 .and_then(Value::as_object)
609 .ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
610 require_keys(adapter, &["name", "version"], &[])?;
611 identifier(adapter, "name", false)?;
612 identifier(adapter, "version", false)?;
613 let capabilities = object
614 .get("capabilities")
615 .and_then(Value::as_array)
616 .ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
617 if capabilities.len() > ADAPTER_CAPABILITIES.len() {
618 return Err(ParseError::malformed("capabilities: too many entries"));
619 }
620 for item in capabilities {
621 match item.as_str() {
622 Some(name) if valid_capability(name) => {}
623 _ => return Err(ParseError::malformed("capabilities: unknown capability")),
624 }
625 }
626 Ok(())
627 }
628 "revision-commit" => {
629 require_keys(object, &["type", "revision"], &[])?;
630 whole_number(object, "revision", true)
631 }
632 "snapshot" => {
633 require_keys(object, &["type", "snapshot"], &[])?;
634 check_embedded_snapshot(&object["snapshot"], limits)
635 }
636 "log" => {
637 require_keys(object, &["type", "record"], &[])?;
638 check_embedded_log_record(&object["record"], limits)
639 }
640 "error" => check_error_message(object, true),
641 _ => Err(ParseError::malformed("unknown or missing message type")),
642 }
643}
644
645fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
648 match validate_log_record(value, limits) {
649 Ok(()) => Ok(()),
650 Err(error) => {
651 let code = match error.code {
652 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
653 _ => "malformed",
654 };
655 Err(ParseError::new(code, format!("log record {error}")))
656 }
657 }
658}
659
660pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
677 project(value, limits)?;
678 let (object, kind) = as_message(value)?;
679
680 match kind {
681 "hello-ack" => {
682 check_protocol_field(object)?;
683 required_keys(
684 object,
685 &[
686 "type",
687 "protocol",
688 "sessionId",
689 "limits",
690 "subscribe",
691 "marker",
692 ],
693 )?;
694 identifier(object, "sessionId", false)?;
695 let limits_object = object
696 .get("limits")
697 .and_then(Value::as_object)
698 .ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
699 required_keys(limits_object, &LIMIT_FIELDS)?;
702 for field in LIMIT_FIELDS {
703 whole_number(limits_object, field, true)?;
704 }
705 match object.get("subscribe").and_then(Value::as_str) {
706 Some("snapshots") | Some("revisions") => {}
707 _ => {
708 return Err(ParseError::malformed(
709 "subscribe: expected 'snapshots' or 'revisions'",
710 ))
711 }
712 }
713 let marker = object
714 .get("marker")
715 .and_then(Value::as_object)
716 .ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
717 required_keys(marker, &["enabled"])?;
718 if !marker["enabled"].is_boolean() {
719 return Err(ParseError::malformed("marker.enabled: expected a boolean"));
720 }
721 if let Some(logs) = object.get("logs") {
722 check_log_budget(logs)?;
723 }
724 Ok(())
725 }
726 "error" => check_error_message(object, false),
727 _ => Err(ParseError::malformed("unknown or missing message type")),
728 }
729}