1use std::collections::BTreeSet;
4
5use serde::{Deserialize, Serialize, Serializer};
6
7use crate::PageId;
8
9pub const FORM_SNAPSHOT_SCHEMA_VERSION: u16 = 1;
11pub const MAX_FORM_SNAPSHOT_FORMS: usize = 64;
12pub const MAX_FORM_SNAPSHOT_CONTROLS: usize = 512;
13pub const MAX_FORM_GROUPS: usize = 128;
14pub const MAX_FORM_OPTIONS: usize = 512;
15pub const MAX_FORM_REFERENCES: usize = 512;
16pub const MAX_FORM_ACCEPT_TYPES: usize = 128;
17pub const MAX_FORM_TARGET_PATH: usize = 8;
18pub const MAX_FORM_TARGET_ORDINAL: usize = 2_047;
19pub const MAX_FORM_ID_BYTES: usize = 128;
20pub const MAX_FORM_TEXT_BYTES: usize = 2_048;
21pub const MAX_FORM_VALUE_BYTES: usize = 4_096;
22pub const MAX_FORM_VALIDATION_MESSAGE_BYTES: usize = 1_024;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct SemanticTargetSegment {
28 pub role: String,
29 pub accessible_name: String,
30 pub ordinal: Option<usize>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct FormControlTarget {
37 pub role: String,
38 pub accessible_name: String,
39 #[serde(default)]
40 pub ordinal: Option<usize>,
41 #[serde(default)]
42 pub frame_path: Vec<SemanticTargetSegment>,
43 #[serde(default)]
44 pub shadow_path: Vec<SemanticTargetSegment>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct RevealedControl {
54 pub control_kind: FormControlKind,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub accessible_name: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub target: Option<FormControlTarget>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63#[serde(rename_all = "camelCase")]
64pub enum FormControlKind {
65 Text,
66 Email,
67 Password,
68 Search,
69 Number,
70 Checkbox,
71 Radio,
72 Switch,
73 SelectOne,
74 SelectMultiple,
75 Date,
76 Time,
77 DateTimeLocal,
78 Range,
79 File,
80 ContentEditable,
81 Combobox,
82 Listbox,
83 Submit,
84 Reset,
85 Other,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
91pub enum FormControlState {
92 Empty,
93 Text { value: String },
94 Redacted { present: bool },
95 Checked { checked: bool },
96 Selection { values: Vec<String> },
97 Files { count: usize },
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
102#[serde(rename_all = "camelCase")]
103pub enum FormControlOperation {
104 SetText,
105 SetChecked,
106 SelectOne,
107 SelectMany,
108 SetFiles,
109 Clear,
110 Activate,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115#[serde(rename_all = "camelCase", deny_unknown_fields)]
116pub struct FormControlConstraints {
117 pub required: bool,
118 pub read_only: bool,
119 pub disabled: bool,
120 pub pattern: Option<String>,
121 pub min_length: Option<u32>,
122 pub max_length: Option<u32>,
123 pub min: Option<String>,
124 pub max: Option<String>,
125 pub step: Option<String>,
126 pub multiple: bool,
127 pub accept: Vec<String>,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[serde(rename_all = "camelCase")]
133pub enum FormValidityFlag {
134 ValueMissing,
135 TypeMismatch,
136 PatternMismatch,
137 TooLong,
138 TooShort,
139 RangeUnderflow,
140 RangeOverflow,
141 StepMismatch,
142 BadInput,
143 CustomError,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
148#[serde(rename_all = "camelCase", deny_unknown_fields)]
149pub struct FormControlValidity {
150 pub will_validate: bool,
151 pub valid: bool,
152 pub flags: Vec<FormValidityFlag>,
153 pub message: Option<String>,
154 pub described_by: Vec<String>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159#[serde(rename_all = "camelCase", deny_unknown_fields)]
160pub struct FormOption {
161 pub value: String,
162 pub label: String,
163 pub disabled: bool,
164 pub selected: bool,
165 pub group_label: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct FormControl {
172 pub id: String,
173 pub form_id: Option<String>,
174 pub group_id: Option<String>,
175 pub target: Option<FormControlTarget>,
176 pub control_kind: FormControlKind,
177 pub accessible_name: Option<String>,
178 pub label: Option<String>,
179 pub description: Option<String>,
180 pub placeholder: Option<String>,
181 pub autocomplete: Option<String>,
182 pub state: FormControlState,
183 pub constraints: FormControlConstraints,
184 pub validity: FormControlValidity,
185 pub options: Vec<FormOption>,
186 pub supported_operations: Vec<FormControlOperation>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
191#[serde(rename_all = "camelCase", deny_unknown_fields)]
192pub struct FormGroup {
193 pub id: String,
194 pub label: Option<String>,
195 pub description: Option<String>,
196 pub control_ids: Vec<String>,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201#[serde(rename_all = "camelCase", deny_unknown_fields)]
202pub struct FormValidity {
203 pub valid: bool,
204 pub invalid_control_ids: Vec<String>,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209#[serde(rename_all = "camelCase", deny_unknown_fields)]
210pub struct FormDescriptor {
211 pub id: String,
212 pub target: Option<FormControlTarget>,
213 pub accessible_name: Option<String>,
214 pub description: Option<String>,
215 pub groups: Vec<FormGroup>,
216 pub controls: Vec<FormControl>,
217 pub submit_control_ids: Vec<String>,
218 pub reset_control_ids: Vec<String>,
219 pub validity: FormValidity,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225#[serde(
226 rename_all = "camelCase",
227 deny_unknown_fields,
228 try_from = "FormSnapshotWire"
229)]
230pub struct FormSnapshot {
231 pub schema_version: u16,
232 pub page_id: PageId,
233 pub forms: Vec<FormDescriptor>,
234 pub unowned_controls: Vec<FormControl>,
235 pub truncated: bool,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
240#[serde(rename_all = "camelCase", deny_unknown_fields)]
241struct FormSnapshotWire {
242 schema_version: u16,
243 page_id: PageId,
244 #[cfg_attr(feature = "schema", schemars(length(max = 64)))]
245 forms: Vec<FormDescriptor>,
246 #[cfg_attr(feature = "schema", schemars(length(max = 512)))]
247 unowned_controls: Vec<FormControl>,
248 truncated: bool,
249}
250
251impl TryFrom<FormSnapshotWire> for FormSnapshot {
252 type Error = String;
253
254 fn try_from(wire: FormSnapshotWire) -> Result<Self, Self::Error> {
255 let snapshot = Self {
256 schema_version: wire.schema_version,
257 page_id: wire.page_id,
258 forms: wire.forms,
259 unowned_controls: wire.unowned_controls,
260 truncated: wire.truncated,
261 };
262 snapshot.validate()?;
263 Ok(snapshot)
264 }
265}
266
267impl Serialize for FormSnapshot {
268 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
269 where
270 S: Serializer,
271 {
272 self.validate().map_err(serde::ser::Error::custom)?;
273 FormSnapshotWire {
274 schema_version: self.schema_version,
275 page_id: self.page_id.clone(),
276 forms: self.forms.clone(),
277 unowned_controls: self.unowned_controls.clone(),
278 truncated: self.truncated,
279 }
280 .serialize(serializer)
281 }
282}
283
284impl FormSnapshot {
285 pub fn validate(&self) -> Result<(), String> {
286 if self.schema_version != FORM_SNAPSHOT_SCHEMA_VERSION {
287 return Err("unsupported form snapshot schema version".into());
288 }
289 if self.forms.len() > MAX_FORM_SNAPSHOT_FORMS {
290 return Err("form snapshot exceeds its form bound".into());
291 }
292 let control_count = self
293 .forms
294 .iter()
295 .try_fold(self.unowned_controls.len(), |count, form| {
296 count.checked_add(form.controls.len())
297 })
298 .ok_or_else(|| "form snapshot control count overflow".to_owned())?;
299 if control_count > MAX_FORM_SNAPSHOT_CONTROLS {
300 return Err("form snapshot exceeds its control bound".into());
301 }
302
303 let mut form_ids = BTreeSet::new();
304 let mut control_ids = BTreeSet::new();
305 for form in &self.forms {
306 validate_id(&form.id, "form")?;
307 if !form_ids.insert(form.id.as_str()) {
308 return Err("form snapshot contains duplicate form IDs".into());
309 }
310 validate_optional_text(&form.accessible_name, MAX_FORM_TEXT_BYTES, "form name")?;
311 validate_optional_text(&form.description, MAX_FORM_TEXT_BYTES, "form description")?;
312 validate_target(form.target.as_ref())?;
313 validate_form(form, &mut control_ids)?;
314 }
315 for control in &self.unowned_controls {
316 if control.form_id.is_some() || control.group_id.is_some() {
317 return Err("unowned controls cannot reference a form or group".into());
318 }
319 validate_control(control)?;
320 if !control_ids.insert(control.id.as_str()) {
321 return Err("form snapshot contains duplicate control IDs".into());
322 }
323 }
324 Ok(())
325 }
326}
327
328fn validate_form<'a>(
329 form: &'a FormDescriptor,
330 all_control_ids: &mut BTreeSet<&'a str>,
331) -> Result<(), String> {
332 if form.groups.len() > MAX_FORM_GROUPS || form.controls.len() > MAX_FORM_SNAPSHOT_CONTROLS {
333 return Err("form exceeds a collection bound".into());
334 }
335 let mut local_controls = BTreeSet::new();
336 for control in &form.controls {
337 if control.form_id.as_deref() != Some(form.id.as_str()) {
338 return Err("form control references the wrong form".into());
339 }
340 validate_control(control)?;
341 if !local_controls.insert(control.id.as_str())
342 || !all_control_ids.insert(control.id.as_str())
343 {
344 return Err("form snapshot contains duplicate control IDs".into());
345 }
346 }
347
348 let mut group_ids = BTreeSet::new();
349 for group in &form.groups {
350 validate_id(&group.id, "form group")?;
351 validate_optional_text(&group.label, MAX_FORM_TEXT_BYTES, "form group label")?;
352 validate_optional_text(
353 &group.description,
354 MAX_FORM_TEXT_BYTES,
355 "form group description",
356 )?;
357 if !group_ids.insert(group.id.as_str()) || group.control_ids.len() > MAX_FORM_REFERENCES {
358 return Err("form contains invalid group references".into());
359 }
360 for id in &group.control_ids {
361 if !local_controls.contains(id.as_str()) {
362 return Err("form group references an unknown control".into());
363 }
364 let control = form
365 .controls
366 .iter()
367 .find(|control| control.id == *id)
368 .unwrap();
369 if control.group_id.as_deref() != Some(group.id.as_str()) {
370 return Err("form group membership is inconsistent".into());
371 }
372 }
373 }
374 for control in &form.controls {
375 if let Some(group_id) = &control.group_id {
376 if !group_ids.contains(group_id.as_str()) {
377 return Err("form control references an unknown group".into());
378 }
379 let group = form
380 .groups
381 .iter()
382 .find(|group| group.id == *group_id)
383 .unwrap();
384 if !group.control_ids.iter().any(|id| id == &control.id) {
385 return Err("form group membership is inconsistent".into());
386 }
387 }
388 }
389 validate_control_references(form, &local_controls)?;
390 Ok(())
391}
392
393fn validate_control_references(
394 form: &FormDescriptor,
395 controls: &BTreeSet<&str>,
396) -> Result<(), String> {
397 if form.submit_control_ids.len() > MAX_FORM_REFERENCES
398 || form.reset_control_ids.len() > MAX_FORM_REFERENCES
399 || form.validity.invalid_control_ids.len() > MAX_FORM_REFERENCES
400 {
401 return Err("form exceeds its control reference bound".into());
402 }
403 if !references_are_unique(&form.submit_control_ids)
404 || !references_are_unique(&form.reset_control_ids)
405 || !references_are_unique(&form.validity.invalid_control_ids)
406 {
407 return Err("form contains duplicate control references".into());
408 }
409 for id in &form.submit_control_ids {
410 let Some(control) = form.controls.iter().find(|control| control.id == *id) else {
411 return Err("form submit list references an unknown control".into());
412 };
413 if control.control_kind != FormControlKind::Submit {
414 return Err("form submit list references a non-submit control".into());
415 }
416 }
417 for id in &form.reset_control_ids {
418 let Some(control) = form.controls.iter().find(|control| control.id == *id) else {
419 return Err("form reset list references an unknown control".into());
420 };
421 if control.control_kind != FormControlKind::Reset {
422 return Err("form reset list references a non-reset control".into());
423 }
424 }
425 for id in &form.validity.invalid_control_ids {
426 if !controls.contains(id.as_str()) {
427 return Err("form validity references an unknown control".into());
428 }
429 }
430 Ok(())
431}
432
433fn references_are_unique(values: &[String]) -> bool {
434 values.iter().collect::<BTreeSet<_>>().len() == values.len()
435}
436
437fn validate_control(control: &FormControl) -> Result<(), String> {
438 validate_id(&control.id, "form control")?;
439 if let Some(id) = &control.form_id {
440 validate_id(id, "form")?;
441 }
442 if let Some(id) = &control.group_id {
443 validate_id(id, "form group")?;
444 }
445 validate_target(control.target.as_ref())?;
446 validate_optional_text(
447 &control.accessible_name,
448 MAX_FORM_TEXT_BYTES,
449 "control name",
450 )?;
451 validate_optional_text(&control.label, MAX_FORM_TEXT_BYTES, "control label")?;
452 validate_optional_text(
453 &control.description,
454 MAX_FORM_TEXT_BYTES,
455 "control description",
456 )?;
457 validate_optional_text(
458 &control.placeholder,
459 MAX_FORM_TEXT_BYTES,
460 "control placeholder",
461 )?;
462 validate_optional_text(
463 &control.autocomplete,
464 MAX_FORM_TEXT_BYTES,
465 "control autocomplete",
466 )?;
467 validate_state(control.control_kind, &control.state)?;
468 validate_constraints(&control.constraints)?;
469 validate_validity(&control.validity)?;
470 if control.options.len() > MAX_FORM_OPTIONS {
471 return Err("form control exceeds its option bound".into());
472 }
473 for option in &control.options {
474 validate_text(&option.value, MAX_FORM_VALUE_BYTES, "option value", true)?;
475 validate_text(&option.label, MAX_FORM_TEXT_BYTES, "option label", true)?;
476 validate_optional_text(&option.group_label, MAX_FORM_TEXT_BYTES, "option group")?;
477 }
478 let operations = control
479 .supported_operations
480 .iter()
481 .copied()
482 .collect::<BTreeSet<_>>();
483 if operations.len() != control.supported_operations.len() {
484 return Err("form control contains duplicate supported operations".into());
485 }
486 Ok(())
487}
488
489fn validate_state(kind: FormControlKind, state: &FormControlState) -> Result<(), String> {
490 match state {
491 FormControlState::Text { value } => {
492 if kind == FormControlKind::Password {
493 return Err("password controls cannot expose text state".into());
494 }
495 validate_text(value, MAX_FORM_VALUE_BYTES, "control value", true)?;
496 }
497 FormControlState::Selection { values } => {
498 if values.len() > MAX_FORM_OPTIONS {
499 return Err("selection state exceeds its value bound".into());
500 }
501 for value in values {
502 validate_text(value, MAX_FORM_VALUE_BYTES, "selection value", true)?;
503 }
504 }
505 FormControlState::Files { count } if *count > MAX_FORM_OPTIONS => {
506 return Err("file state exceeds its count bound".into());
507 }
508 _ => {}
509 }
510 Ok(())
511}
512
513fn validate_constraints(constraints: &FormControlConstraints) -> Result<(), String> {
514 validate_optional_text(&constraints.pattern, MAX_FORM_TEXT_BYTES, "control pattern")?;
515 validate_optional_text(&constraints.min, MAX_FORM_VALUE_BYTES, "control minimum")?;
516 validate_optional_text(&constraints.max, MAX_FORM_VALUE_BYTES, "control maximum")?;
517 validate_optional_text(&constraints.step, MAX_FORM_VALUE_BYTES, "control step")?;
518 if constraints.accept.len() > MAX_FORM_ACCEPT_TYPES {
519 return Err("control accept list exceeds its bound".into());
520 }
521 for accept in &constraints.accept {
522 validate_text(accept, MAX_FORM_TEXT_BYTES, "accepted type", false)?;
523 }
524 if constraints
525 .min_length
526 .zip(constraints.max_length)
527 .is_some_and(|(min, max)| min > max)
528 {
529 return Err("control minimum length exceeds maximum length".into());
530 }
531 Ok(())
532}
533
534fn validate_validity(validity: &FormControlValidity) -> Result<(), String> {
535 if validity.flags.len() > 10 || validity.described_by.len() > MAX_FORM_REFERENCES {
536 return Err("control validity exceeds a collection bound".into());
537 }
538 if validity.valid && !validity.flags.is_empty() {
539 return Err("valid control cannot carry failing validity flags".into());
540 }
541 let flags = validity.flags.iter().copied().collect::<BTreeSet<_>>();
542 if flags.len() != validity.flags.len() {
543 return Err("control validity contains duplicate flags".into());
544 }
545 validate_optional_text(
546 &validity.message,
547 MAX_FORM_VALIDATION_MESSAGE_BYTES,
548 "validation message",
549 )?;
550 for text in &validity.described_by {
551 validate_text(text, MAX_FORM_TEXT_BYTES, "described-by text", false)?;
552 }
553 Ok(())
554}
555
556fn validate_target(target: Option<&FormControlTarget>) -> Result<(), String> {
557 let Some(target) = target else {
558 return Ok(());
559 };
560 validate_text(&target.role, MAX_FORM_ID_BYTES, "target role", false)?;
561 validate_text(
562 &target.accessible_name,
563 MAX_FORM_TEXT_BYTES,
564 "target accessible name",
565 false,
566 )?;
567 if target
568 .ordinal
569 .is_some_and(|ordinal| ordinal > MAX_FORM_TARGET_ORDINAL)
570 || target.frame_path.len() > MAX_FORM_TARGET_PATH
571 || target.shadow_path.len() > MAX_FORM_TARGET_PATH
572 {
573 return Err("form target exceeds its bound".into());
574 }
575 for segment in target.frame_path.iter().chain(&target.shadow_path) {
576 validate_text(&segment.role, MAX_FORM_ID_BYTES, "target path role", false)?;
577 validate_text(
578 &segment.accessible_name,
579 MAX_FORM_TEXT_BYTES,
580 "target path name",
581 false,
582 )?;
583 if segment
584 .ordinal
585 .is_some_and(|ordinal| ordinal > MAX_FORM_TARGET_ORDINAL)
586 {
587 return Err("form target path ordinal exceeds its bound".into());
588 }
589 }
590 Ok(())
591}
592
593fn validate_id(value: &str, field: &str) -> Result<(), String> {
594 validate_text(value, MAX_FORM_ID_BYTES, field, false)
595}
596
597fn validate_optional_text(value: &Option<String>, max: usize, field: &str) -> Result<(), String> {
598 if let Some(value) = value {
599 validate_text(value, max, field, false)?;
600 }
601 Ok(())
602}
603
604fn validate_text(value: &str, max: usize, field: &str, allow_empty: bool) -> Result<(), String> {
605 if (!allow_empty && value.is_empty())
606 || value.len() > max
607 || value.chars().any(char::is_control)
608 {
609 return Err(format!(
610 "{field} is empty, oversized, or contains control characters"
611 ));
612 }
613 Ok(())
614}