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, skip_serializing_if = "Option::is_none")]
40 pub ordinal: Option<usize>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub frame_path: Vec<SemanticTargetSegment>,
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
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, Default, PartialEq, Eq, Serialize, Deserialize)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
116pub struct FormControlConstraints {
117 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
118 pub required: bool,
119 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
120 pub read_only: bool,
121 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
122 pub disabled: bool,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub pattern: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub min_length: Option<u32>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub max_length: Option<u32>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub min: Option<String>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub max: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub step: Option<String>,
135 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
136 pub multiple: bool,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub accept: Vec<String>,
139}
140
141impl FormControlConstraints {
142 pub fn is_default(&self) -> bool {
145 !self.required
146 && !self.read_only
147 && !self.disabled
148 && self.pattern.is_none()
149 && self.min_length.is_none()
150 && self.max_length.is_none()
151 && self.min.is_none()
152 && self.max.is_none()
153 && self.step.is_none()
154 && !self.multiple
155 && self.accept.is_empty()
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
161#[serde(rename_all = "camelCase")]
162pub enum FormValidityFlag {
163 ValueMissing,
164 TypeMismatch,
165 PatternMismatch,
166 TooLong,
167 TooShort,
168 RangeUnderflow,
169 RangeOverflow,
170 StepMismatch,
171 BadInput,
172 CustomError,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177#[serde(rename_all = "camelCase", deny_unknown_fields)]
178pub struct FormControlValidity {
179 pub will_validate: bool,
183 pub valid: bool,
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
185 pub flags: Vec<FormValidityFlag>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub message: Option<String>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
189 pub described_by: Vec<String>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
197#[serde(rename_all = "camelCase", deny_unknown_fields)]
198pub struct FormValidationIssue {
199 pub control_id: String,
200 pub control_kind: FormControlKind,
201 pub accessible_name: Option<String>,
202 pub target: Option<FormControlTarget>,
203 pub validity: FormControlValidity,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
208#[serde(rename_all = "camelCase", deny_unknown_fields)]
209pub struct FormOption {
210 pub value: String,
211 pub label: String,
212 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
213 pub disabled: bool,
214 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
215 pub selected: bool,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub group_label: Option<String>,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
222#[serde(rename_all = "camelCase", deny_unknown_fields)]
223pub struct FormControl {
224 pub id: String,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub form_id: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub group_id: Option<String>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub target: Option<FormControlTarget>,
231 pub control_kind: FormControlKind,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub accessible_name: Option<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub label: Option<String>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub description: Option<String>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub placeholder: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub autocomplete: Option<String>,
245 pub state: FormControlState,
246 #[serde(default, skip_serializing_if = "FormControlConstraints::is_default")]
247 pub constraints: FormControlConstraints,
248 pub validity: FormControlValidity,
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub options: Vec<FormOption>,
251 pub supported_operations: Vec<FormControlOperation>,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
256#[serde(rename_all = "camelCase", deny_unknown_fields)]
257pub struct FormGroup {
258 pub id: String,
259 pub label: Option<String>,
260 pub description: Option<String>,
261 pub control_ids: Vec<String>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
266#[serde(rename_all = "camelCase", deny_unknown_fields)]
267pub struct FormValidity {
268 pub valid: bool,
269 pub invalid_control_ids: Vec<String>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274#[serde(rename_all = "camelCase", deny_unknown_fields)]
275pub struct FormDescriptor {
276 pub id: String,
277 pub target: Option<FormControlTarget>,
278 pub accessible_name: Option<String>,
279 pub description: Option<String>,
280 pub groups: Vec<FormGroup>,
281 pub controls: Vec<FormControl>,
282 pub submit_control_ids: Vec<String>,
283 pub reset_control_ids: Vec<String>,
284 pub validity: FormValidity,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
290#[serde(
291 rename_all = "camelCase",
292 deny_unknown_fields,
293 try_from = "FormSnapshotWire"
294)]
295pub struct FormSnapshot {
296 pub schema_version: u16,
297 pub page_id: PageId,
298 pub forms: Vec<FormDescriptor>,
299 pub unowned_controls: Vec<FormControl>,
300 pub truncated: bool,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
305#[serde(rename_all = "camelCase", deny_unknown_fields)]
306struct FormSnapshotWire {
307 schema_version: u16,
308 page_id: PageId,
309 #[cfg_attr(feature = "schema", schemars(length(max = 64)))]
310 forms: Vec<FormDescriptor>,
311 #[cfg_attr(feature = "schema", schemars(length(max = 512)))]
312 unowned_controls: Vec<FormControl>,
313 truncated: bool,
314 #[serde(default = "page_derived_true")]
315 page_derived: bool,
316}
317
318fn page_derived_true() -> bool {
319 true
320}
321
322impl TryFrom<FormSnapshotWire> for FormSnapshot {
323 type Error = String;
324
325 fn try_from(wire: FormSnapshotWire) -> Result<Self, Self::Error> {
326 if !wire.page_derived {
327 return Err("form snapshot pageDerived must be true".into());
328 }
329 let snapshot = Self {
330 schema_version: wire.schema_version,
331 page_id: wire.page_id,
332 forms: wire.forms,
333 unowned_controls: wire.unowned_controls,
334 truncated: wire.truncated,
335 };
336 snapshot.validate()?;
337 Ok(snapshot)
338 }
339}
340
341impl Serialize for FormSnapshot {
342 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
343 where
344 S: Serializer,
345 {
346 self.validate().map_err(serde::ser::Error::custom)?;
347 FormSnapshotWire {
348 schema_version: self.schema_version,
349 page_id: self.page_id.clone(),
350 forms: self.forms.clone(),
351 unowned_controls: self.unowned_controls.clone(),
352 truncated: self.truncated,
353 page_derived: true,
354 }
355 .serialize(serializer)
356 }
357}
358
359impl FormSnapshot {
360 pub fn page_derived(&self) -> bool {
361 true
362 }
363
364 pub fn validate(&self) -> Result<(), String> {
365 if self.schema_version != FORM_SNAPSHOT_SCHEMA_VERSION {
366 return Err("unsupported form snapshot schema version".into());
367 }
368 if self.forms.len() > MAX_FORM_SNAPSHOT_FORMS {
369 return Err("form snapshot exceeds its form bound".into());
370 }
371 let control_count = self
372 .forms
373 .iter()
374 .try_fold(self.unowned_controls.len(), |count, form| {
375 count.checked_add(form.controls.len())
376 })
377 .ok_or_else(|| "form snapshot control count overflow".to_owned())?;
378 if control_count > MAX_FORM_SNAPSHOT_CONTROLS {
379 return Err("form snapshot exceeds its control bound".into());
380 }
381
382 let mut form_ids = BTreeSet::new();
383 let mut control_ids = BTreeSet::new();
384 for form in &self.forms {
385 validate_id(&form.id, "form")?;
386 if !form_ids.insert(form.id.as_str()) {
387 return Err("form snapshot contains duplicate form IDs".into());
388 }
389 validate_optional_text(&form.accessible_name, MAX_FORM_TEXT_BYTES, "form name")?;
390 validate_optional_text(&form.description, MAX_FORM_TEXT_BYTES, "form description")?;
391 validate_target(form.target.as_ref())?;
392 validate_form(form, &mut control_ids)?;
393 }
394 for control in &self.unowned_controls {
395 if control.form_id.is_some() || control.group_id.is_some() {
396 return Err("unowned controls cannot reference a form or group".into());
397 }
398 validate_control(control)?;
399 if !control_ids.insert(control.id.as_str()) {
400 return Err("form snapshot contains duplicate control IDs".into());
401 }
402 }
403 Ok(())
404 }
405}
406
407fn validate_form<'a>(
408 form: &'a FormDescriptor,
409 all_control_ids: &mut BTreeSet<&'a str>,
410) -> Result<(), String> {
411 if form.groups.len() > MAX_FORM_GROUPS || form.controls.len() > MAX_FORM_SNAPSHOT_CONTROLS {
412 return Err("form exceeds a collection bound".into());
413 }
414 let mut local_controls = BTreeSet::new();
415 for control in &form.controls {
416 if control.form_id.as_deref() != Some(form.id.as_str()) {
417 return Err("form control references the wrong form".into());
418 }
419 validate_control(control)?;
420 if !local_controls.insert(control.id.as_str())
421 || !all_control_ids.insert(control.id.as_str())
422 {
423 return Err("form snapshot contains duplicate control IDs".into());
424 }
425 }
426
427 let mut group_ids = BTreeSet::new();
428 for group in &form.groups {
429 validate_id(&group.id, "form group")?;
430 validate_optional_text(&group.label, MAX_FORM_TEXT_BYTES, "form group label")?;
431 validate_optional_text(
432 &group.description,
433 MAX_FORM_TEXT_BYTES,
434 "form group description",
435 )?;
436 if !group_ids.insert(group.id.as_str()) || group.control_ids.len() > MAX_FORM_REFERENCES {
437 return Err("form contains invalid group references".into());
438 }
439 for id in &group.control_ids {
440 if !local_controls.contains(id.as_str()) {
441 return Err("form group references an unknown control".into());
442 }
443 let control = form
444 .controls
445 .iter()
446 .find(|control| control.id == *id)
447 .unwrap();
448 if control.group_id.as_deref() != Some(group.id.as_str()) {
449 return Err("form group membership is inconsistent".into());
450 }
451 }
452 }
453 for control in &form.controls {
454 if let Some(group_id) = &control.group_id {
455 if !group_ids.contains(group_id.as_str()) {
456 return Err("form control references an unknown group".into());
457 }
458 let group = form
459 .groups
460 .iter()
461 .find(|group| group.id == *group_id)
462 .unwrap();
463 if !group.control_ids.iter().any(|id| id == &control.id) {
464 return Err("form group membership is inconsistent".into());
465 }
466 }
467 }
468 validate_control_references(form, &local_controls)?;
469 Ok(())
470}
471
472fn validate_control_references(
473 form: &FormDescriptor,
474 controls: &BTreeSet<&str>,
475) -> Result<(), String> {
476 if form.submit_control_ids.len() > MAX_FORM_REFERENCES
477 || form.reset_control_ids.len() > MAX_FORM_REFERENCES
478 || form.validity.invalid_control_ids.len() > MAX_FORM_REFERENCES
479 {
480 return Err("form exceeds its control reference bound".into());
481 }
482 if !references_are_unique(&form.submit_control_ids)
483 || !references_are_unique(&form.reset_control_ids)
484 || !references_are_unique(&form.validity.invalid_control_ids)
485 {
486 return Err("form contains duplicate control references".into());
487 }
488 for id in &form.submit_control_ids {
489 let Some(control) = form.controls.iter().find(|control| control.id == *id) else {
490 return Err("form submit list references an unknown control".into());
491 };
492 if control.control_kind != FormControlKind::Submit {
493 return Err("form submit list references a non-submit control".into());
494 }
495 }
496 for id in &form.reset_control_ids {
497 let Some(control) = form.controls.iter().find(|control| control.id == *id) else {
498 return Err("form reset list references an unknown control".into());
499 };
500 if control.control_kind != FormControlKind::Reset {
501 return Err("form reset list references a non-reset control".into());
502 }
503 }
504 for id in &form.validity.invalid_control_ids {
505 if !controls.contains(id.as_str()) {
506 return Err("form validity references an unknown control".into());
507 }
508 }
509 Ok(())
510}
511
512fn references_are_unique(values: &[String]) -> bool {
513 values.iter().collect::<BTreeSet<_>>().len() == values.len()
514}
515
516fn validate_control(control: &FormControl) -> Result<(), String> {
517 validate_id(&control.id, "form control")?;
518 if let Some(id) = &control.form_id {
519 validate_id(id, "form")?;
520 }
521 if let Some(id) = &control.group_id {
522 validate_id(id, "form group")?;
523 }
524 validate_target(control.target.as_ref())?;
525 validate_optional_text(
526 &control.accessible_name,
527 MAX_FORM_TEXT_BYTES,
528 "control name",
529 )?;
530 validate_optional_text(&control.label, MAX_FORM_TEXT_BYTES, "control label")?;
531 validate_optional_text(
532 &control.description,
533 MAX_FORM_TEXT_BYTES,
534 "control description",
535 )?;
536 validate_optional_text(
537 &control.placeholder,
538 MAX_FORM_TEXT_BYTES,
539 "control placeholder",
540 )?;
541 validate_optional_text(
542 &control.autocomplete,
543 MAX_FORM_TEXT_BYTES,
544 "control autocomplete",
545 )?;
546 validate_state(control.control_kind, &control.state)?;
547 validate_constraints(&control.constraints)?;
548 validate_validity(&control.validity)?;
549 if control.options.len() > MAX_FORM_OPTIONS {
550 return Err("form control exceeds its option bound".into());
551 }
552 for option in &control.options {
553 validate_text(&option.value, MAX_FORM_VALUE_BYTES, "option value", true)?;
554 validate_text(&option.label, MAX_FORM_TEXT_BYTES, "option label", true)?;
555 validate_optional_text(&option.group_label, MAX_FORM_TEXT_BYTES, "option group")?;
556 }
557 let operations = control
558 .supported_operations
559 .iter()
560 .copied()
561 .collect::<BTreeSet<_>>();
562 if operations.len() != control.supported_operations.len() {
563 return Err("form control contains duplicate supported operations".into());
564 }
565 Ok(())
566}
567
568fn validate_state(kind: FormControlKind, state: &FormControlState) -> Result<(), String> {
569 match state {
570 FormControlState::Text { value } => {
571 if kind == FormControlKind::Password {
572 return Err("password controls cannot expose text state".into());
573 }
574 validate_text(value, MAX_FORM_VALUE_BYTES, "control value", true)?;
575 }
576 FormControlState::Selection { values } => {
577 if values.len() > MAX_FORM_OPTIONS {
578 return Err("selection state exceeds its value bound".into());
579 }
580 for value in values {
581 validate_text(value, MAX_FORM_VALUE_BYTES, "selection value", true)?;
582 }
583 }
584 FormControlState::Files { count } if *count > MAX_FORM_OPTIONS => {
585 return Err("file state exceeds its count bound".into());
586 }
587 _ => {}
588 }
589 Ok(())
590}
591
592fn validate_constraints(constraints: &FormControlConstraints) -> Result<(), String> {
593 validate_optional_text(&constraints.pattern, MAX_FORM_TEXT_BYTES, "control pattern")?;
594 validate_optional_text(&constraints.min, MAX_FORM_VALUE_BYTES, "control minimum")?;
595 validate_optional_text(&constraints.max, MAX_FORM_VALUE_BYTES, "control maximum")?;
596 validate_optional_text(&constraints.step, MAX_FORM_VALUE_BYTES, "control step")?;
597 if constraints.accept.len() > MAX_FORM_ACCEPT_TYPES {
598 return Err("control accept list exceeds its bound".into());
599 }
600 for accept in &constraints.accept {
601 validate_text(accept, MAX_FORM_TEXT_BYTES, "accepted type", false)?;
602 }
603 if constraints
604 .min_length
605 .zip(constraints.max_length)
606 .is_some_and(|(min, max)| min > max)
607 {
608 return Err("control minimum length exceeds maximum length".into());
609 }
610 Ok(())
611}
612
613fn validate_validity(validity: &FormControlValidity) -> Result<(), String> {
614 if validity.flags.len() > 10 || validity.described_by.len() > MAX_FORM_REFERENCES {
615 return Err("control validity exceeds a collection bound".into());
616 }
617 if validity.valid && !validity.flags.is_empty() {
618 return Err("valid control cannot carry failing validity flags".into());
619 }
620 let flags = validity.flags.iter().copied().collect::<BTreeSet<_>>();
621 if flags.len() != validity.flags.len() {
622 return Err("control validity contains duplicate flags".into());
623 }
624 validate_optional_text(
625 &validity.message,
626 MAX_FORM_VALIDATION_MESSAGE_BYTES,
627 "validation message",
628 )?;
629 for text in &validity.described_by {
630 validate_text(text, MAX_FORM_TEXT_BYTES, "described-by text", false)?;
631 }
632 Ok(())
633}
634
635fn validate_target(target: Option<&FormControlTarget>) -> Result<(), String> {
636 let Some(target) = target else {
637 return Ok(());
638 };
639 validate_text(&target.role, MAX_FORM_ID_BYTES, "target role", false)?;
640 validate_text(
641 &target.accessible_name,
642 MAX_FORM_TEXT_BYTES,
643 "target accessible name",
644 false,
645 )?;
646 if target
647 .ordinal
648 .is_some_and(|ordinal| ordinal > MAX_FORM_TARGET_ORDINAL)
649 || target.frame_path.len() > MAX_FORM_TARGET_PATH
650 || target.shadow_path.len() > MAX_FORM_TARGET_PATH
651 {
652 return Err("form target exceeds its bound".into());
653 }
654 for segment in target.frame_path.iter().chain(&target.shadow_path) {
655 validate_text(&segment.role, MAX_FORM_ID_BYTES, "target path role", false)?;
656 validate_text(
657 &segment.accessible_name,
658 MAX_FORM_TEXT_BYTES,
659 "target path name",
660 false,
661 )?;
662 if segment
663 .ordinal
664 .is_some_and(|ordinal| ordinal > MAX_FORM_TARGET_ORDINAL)
665 {
666 return Err("form target path ordinal exceeds its bound".into());
667 }
668 }
669 Ok(())
670}
671
672fn validate_id(value: &str, field: &str) -> Result<(), String> {
673 validate_text(value, MAX_FORM_ID_BYTES, field, false)
674}
675
676fn validate_optional_text(value: &Option<String>, max: usize, field: &str) -> Result<(), String> {
677 if let Some(value) = value {
678 validate_text(value, max, field, false)?;
679 }
680 Ok(())
681}
682
683fn validate_text(value: &str, max: usize, field: &str, allow_empty: bool) -> Result<(), String> {
684 if (!allow_empty && value.is_empty())
685 || value.len() > max
686 || value.chars().any(char::is_control)
687 {
688 return Err(format!(
689 "{field} is empty, oversized, or contains control characters"
690 ));
691 }
692 Ok(())
693}