form_net/extract/mod.rs
1//! Typed extraction: plain Rust structs filled from a form, without serde
2//! and without a procedural macro.
3//!
4//! [`form!`](crate::form) generates a struct and its [`FromForm`]
5//! implementation; each field type implements [`FromField`]. Unknown names
6//! are refused, a scalar given twice is refused, a missing required field is
7//! refused, and every error names the field by its Rust-side name.
8
9mod file;
10mod macros;
11mod secret;
12mod value;
13
14pub use file::FileData;
15pub use secret::Secret;
16
17use crate::error::{ExtractError, FormError};
18use crate::hook::{Inspector, NoInspector};
19use crate::kind::FormKind;
20use crate::limits::{Limits, Policy};
21use crate::multipart::Part;
22
23/// Why one field's value did not convert. [`form!`](crate::form) turns it
24/// into `FormError::Extract` with the field's name.
25///
26/// # Examples
27///
28/// ```
29/// use form_net::{FromField, ValueError};
30///
31/// let mut slot = <u8 as FromField>::Slot::default();
32/// assert_eq!(u8::accept_text(&mut slot, "300"), Err(ValueError::Invalid("out of range")));
33/// ```
34#[non_exhaustive]
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum ValueError {
37 /// A required field is absent.
38 Missing,
39 /// A single-valued field was given twice.
40 Duplicate,
41 /// The value does not convert; the text says why.
42 Invalid(&'static str),
43 /// Text was given to a file field.
44 ExpectedFile,
45 /// A file was given to a text field.
46 ExpectedText,
47}
48
49/// A file field's value.
50#[non_exhaustive]
51pub enum FileValue<'a> {
52 /// A part of a buffered body.
53 Buffered(&'a Part<'a>),
54 /// A stored file from an upload.
55 #[cfg(feature = "fs")]
56 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
57 Stored(crate::store::StoredFile),
58}
59
60impl core::fmt::Debug for FileValue<'_> {
61 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
62 match self {
63 FileValue::Buffered(p) => f.debug_tuple("Buffered").field(p).finish(),
64 #[cfg(feature = "fs")]
65 FileValue::Stored(s) => f.debug_tuple("Stored").field(s).finish(),
66 }
67 }
68}
69
70/// How one field type takes its values.
71///
72/// The slot holds what has arrived so far, so every arity rule lives in one
73/// place: a scalar's slot is `Option<T>` (a second value is `Duplicate`,
74/// none is `Missing`), `Vec<T>` pushes, `bool` finishes a missing value as
75/// `false`. Implement it for an application type to validate while
76/// extracting.
77///
78/// # Examples
79///
80/// ```
81/// use form_net::{FromField, ValueError};
82///
83/// pub struct Slug(String);
84///
85/// impl FromField for Slug {
86/// type Slot = Option<Slug>;
87/// fn accept_text(slot: &mut Option<Slug>, value: &str) -> Result<(), ValueError> {
88/// if slot.is_some() { return Err(ValueError::Duplicate); }
89/// if value.is_empty() || value.len() > 64 || !value.bytes().all(|b| b.is_ascii_lowercase() || b == b'-') {
90/// return Err(ValueError::Invalid("lowercase letters and hyphens, 1 to 64"));
91/// }
92/// *slot = Some(Slug(value.to_owned()));
93/// Ok(())
94/// }
95/// fn finish(slot: Option<Slug>) -> Result<Slug, ValueError> {
96/// slot.ok_or(ValueError::Missing)
97/// }
98/// }
99/// ```
100pub trait FromField: Sized {
101 /// What has arrived so far.
102 type Slot: Default;
103
104 /// Takes one text value.
105 ///
106 /// # Errors
107 ///
108 /// `ExpectedFile` by default; the type's own conversion errors.
109 fn accept_text(slot: &mut Self::Slot, value: &str) -> Result<(), ValueError> {
110 let _ = (slot, value);
111 Err(ValueError::ExpectedFile)
112 }
113
114 /// Takes one file.
115 ///
116 /// # Errors
117 ///
118 /// `ExpectedText` by default; the type's own errors.
119 fn accept_file(slot: &mut Self::Slot, file: FileValue<'_>) -> Result<(), ValueError> {
120 let _ = (slot, file);
121 Err(ValueError::ExpectedText)
122 }
123
124 /// The value, once every field has arrived.
125 ///
126 /// # Errors
127 ///
128 /// `Missing`, or the type's own errors.
129 fn finish(slot: Self::Slot) -> Result<Self, ValueError>;
130}
131
132/// What [`form!`](crate::form) generates, and what a hand-written
133/// [`FromForm`] implementation provides. Reports problems as
134/// `FormError::Extract { reason, field }`.
135///
136/// # Examples
137///
138/// ```
139/// use form_net::{ExtractError, FileValue, FormBuilder, FormError, FromForm};
140///
141/// /// Accepts any field names, keeping the last `q`.
142/// #[derive(Default)]
143/// struct LastQ(Option<String>);
144///
145/// impl FormBuilder for LastQ {
146/// type Output = Query;
147/// fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
148/// if name == "q" { self.0 = Some(value.to_owned()); }
149/// Ok(())
150/// }
151/// fn file(&mut self, _name: &str, _file: FileValue<'_>) -> Result<(), FormError> {
152/// Err(FormError::Extract { reason: ExtractError::WrongKind, field: Some("q") })
153/// }
154/// fn finish(self) -> Result<Query, FormError> {
155/// Ok(Query { q: self.0.unwrap_or_default() })
156/// }
157/// }
158///
159/// struct Query { q: String }
160/// impl FromForm for Query { type Builder = LastQ; }
161///
162/// assert_eq!(Query::from_query(b"q=a&q=b")?.q, "b");
163/// # Ok::<(), FormError>(())
164/// ```
165pub trait FormBuilder {
166 /// The struct being built.
167 type Output;
168
169 /// One text field.
170 ///
171 /// # Errors
172 ///
173 /// `Extract { .. }`.
174 fn text(&mut self, name: &str, value: &str) -> Result<(), FormError>;
175
176 /// One file.
177 ///
178 /// # Errors
179 ///
180 /// `Extract { .. }`.
181 fn file(&mut self, name: &str, file: FileValue<'_>) -> Result<(), FormError>;
182
183 /// The struct, once every field has arrived.
184 ///
185 /// # Errors
186 ///
187 /// `Extract { Missing | Invalid, .. }`.
188 fn finish(self) -> Result<Self::Output, FormError>;
189
190 /// What a file under `name` is to the struct: `Some(true)` a declared
191 /// field, `Some(false)` an ignored name, `None` an unknown one.
192 /// `FromForm::from_upload_mut` (feature `fs`) moves only declared files out of the
193 /// upload. The default says every name is declared.
194 fn file_field(&self, name: &str) -> Option<bool> {
195 let _ = name;
196 Some(true)
197 }
198
199 /// As [`finish`](Self::finish), but records every field's error in
200 /// `errors` instead of stopping at the first; `None` when any field
201 /// failed. The default records the one error `finish` returns.
202 fn finish_all(self, errors: &mut alloc::vec::Vec<FormError>) -> Option<Self::Output>
203 where
204 Self: Sized,
205 {
206 match self.finish() {
207 Ok(v) => Some(v),
208 Err(e) => {
209 errors.push(e);
210 None
211 }
212 }
213 }
214}
215
216/// Every problem with one submitted form, for showing the form again with
217/// a message beside each field: what
218/// [`FromForm::from_body_all`] returns.
219///
220/// A parse error (a malformed body, a limit) stops the parse, so it is the
221/// only error; extraction errors are collected for every field, at most one
222/// per received value plus one per missing field. No value is ever kept.
223///
224/// # Examples
225///
226/// ```
227/// use form_net::{ExtractError, FromForm};
228///
229/// form_net::form! {
230/// #[derive(Debug)]
231/// pub struct Signup { pub email: String, pub age: u8, pub tos: bool }
232/// }
233///
234/// let errors = Signup::from_body_all(b"application/x-www-form-urlencoded", b"age=300&role=admin").unwrap_err();
235/// assert_eq!(errors.len(), 3);
236/// assert_eq!(errors.field("age"), Some(ExtractError::Invalid("out of range")));
237/// assert_eq!(errors.field("email"), Some(ExtractError::Missing));
238/// assert_eq!(errors.unknown(), 1);
239/// assert_eq!(errors.status_hint(), 422);
240/// ```
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct FormErrors(alloc::vec::Vec<FormError>);
243
244impl FormErrors {
245 /// Every error, in the order found.
246 pub fn iter(&self) -> impl Iterator<Item = &FormError> {
247 self.0.iter()
248 }
249
250 /// How many errors.
251 #[must_use]
252 pub fn len(&self) -> usize {
253 self.0.len()
254 }
255
256 /// Whether there are none (never true for a returned value).
257 #[must_use]
258 pub fn is_empty(&self) -> bool {
259 self.0.is_empty()
260 }
261
262 /// The first error.
263 #[must_use]
264 pub fn first(&self) -> Option<&FormError> {
265 self.0.first()
266 }
267
268 /// The first extraction error for the field with this Rust name.
269 #[must_use]
270 pub fn field(&self, name: &str) -> Option<ExtractError> {
271 self.0.iter().find_map(|e| match e {
272 FormError::Extract {
273 reason,
274 field: Some(f),
275 } if *f == name => Some(*reason),
276 _ => None,
277 })
278 }
279
280 /// How many received names the struct neither declares nor ignores.
281 #[must_use]
282 pub fn unknown(&self) -> usize {
283 self.0
284 .iter()
285 .filter(|e| {
286 matches!(
287 e,
288 FormError::Extract {
289 reason: ExtractError::Unknown,
290 ..
291 }
292 )
293 })
294 .count()
295 }
296
297 /// The status hint of the first error (422 for extraction errors).
298 #[must_use]
299 pub fn status_hint(&self) -> u16 {
300 self.first().map_or(422, FormError::status_hint)
301 }
302}
303
304impl From<FormError> for FormErrors {
305 fn from(e: FormError) -> FormErrors {
306 FormErrors(alloc::vec![e])
307 }
308}
309
310impl core::fmt::Display for FormErrors {
311 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312 match self.first() {
313 Some(e) if self.0.len() > 1 => {
314 write!(f, "{e} (and {} more)", self.0.len().saturating_sub(1))
315 }
316 Some(e) => write!(f, "{e}"),
317 None => f.write_str("no errors"),
318 }
319 }
320}
321
322#[cfg(feature = "std")]
323impl std::error::Error for FormErrors {}
324
325/// Feeds a builder and keeps going after extraction errors, recording them.
326struct Collect<'e, B> {
327 inner: B,
328 errors: &'e mut alloc::vec::Vec<FormError>,
329}
330
331impl<B: FormBuilder> FormBuilder for Collect<'_, B> {
332 type Output = B::Output;
333
334 fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
335 match self.inner.text(name, value) {
336 Err(e @ FormError::Extract { .. }) => {
337 self.errors.push(e);
338 Ok(())
339 }
340 other => other,
341 }
342 }
343
344 fn file(&mut self, name: &str, file: FileValue<'_>) -> Result<(), FormError> {
345 match self.inner.file(name, file) {
346 Err(e @ FormError::Extract { .. }) => {
347 self.errors.push(e);
348 Ok(())
349 }
350 other => other,
351 }
352 }
353
354 fn finish(self) -> Result<B::Output, FormError> {
355 self.inner.finish()
356 }
357}
358
359/// The bytes the decoders use without allocating.
360const STACK: usize = 512;
361
362/// Feeds a urlencoded body to `builder`, decoding each name and value into a
363/// stack buffer when it fits and a heap buffer otherwise.
364fn feed_urlencoded<B: FormBuilder, I: Inspector>(
365 builder: &mut B,
366 input: &[u8],
367 limits: &Limits,
368 policy: &Policy,
369 inspector: &mut I,
370) -> Result<(), FormError> {
371 let mut name_stack = [0u8; STACK];
372 let mut value_stack = [0u8; STACK];
373 let mut heap = alloc::vec::Vec::new();
374 let fed = (|| {
375 for pair in crate::urlencoded::raw::raw_pairs(input, limits) {
376 let pair = pair?;
377 let name_len = pair.name_raw().len();
378 let value_len = pair.value_raw().len();
379 let (name_buf, value_buf): (&mut [u8], &mut [u8]) =
380 if name_len <= STACK && value_len <= STACK {
381 (&mut name_stack, &mut value_stack)
382 } else {
383 let needed = name_len.saturating_add(value_len);
384 crate::wipe::wipe(&mut heap);
385 heap.clear();
386 if needed > heap.capacity() {
387 // A new allocation; the old one is wiped, not freed
388 // with a decoded value in it.
389 let mut old =
390 core::mem::replace(&mut heap, alloc::vec::Vec::with_capacity(needed));
391 crate::wipe::wipe_vec(&mut old);
392 }
393 heap.resize(needed, 0);
394 heap.split_at_mut(name_len)
395 };
396 let name = pair.decode_name(name_buf, policy.percent)?;
397 let value = pair.decode_value(value_buf, policy.percent)?;
398 let at = crate::error::Location::at(pair.offset());
399 match inspector.field(name, value.as_bytes()) {
400 crate::Verdict::Refuse(refusal) => return Err(FormError::Refused { refusal, at }),
401 crate::Verdict::Skip => {}
402 _ => builder.text(name, value)?,
403 }
404 }
405 Ok(())
406 })();
407 crate::wipe::wipe(&mut name_stack);
408 crate::wipe::wipe(&mut value_stack);
409 crate::wipe::wipe_vec(&mut heap);
410 fed?;
411 if let crate::Verdict::Refuse(refusal) = inspector.end() {
412 return Err(FormError::Refused {
413 refusal,
414 at: crate::error::Location::at(input.len() as u64),
415 });
416 }
417 Ok(())
418}
419
420fn feed_multipart<B: FormBuilder>(
421 builder: &mut B,
422 mp: &crate::Multipart<'_>,
423) -> Result<(), FormError> {
424 for part in mp.parts() {
425 if part.is_file() {
426 builder.file(part.name(), FileValue::Buffered(part))?;
427 } else {
428 builder.text(part.name(), part.text()?)?;
429 }
430 }
431 Ok(())
432}
433
434/// A struct that can be read from a form. Implemented by
435/// [`form!`](crate::form); every method is provided.
436///
437/// # Examples
438///
439/// ```
440/// use form_net::FromForm;
441///
442/// form_net::form! {
443/// pub struct Login {
444/// pub email: String,
445/// pub password: form_net::Secret,
446/// pub remember: bool,
447/// }
448/// ignore = ["csrf_token"];
449/// }
450///
451/// let login = Login::from_body(b"application/x-www-form-urlencoded", b"email=a%40b.example&password=x&csrf_token=t")?;
452/// assert_eq!((login.email.as_str(), login.remember), ("a@b.example", false));
453/// # Ok::<(), form_net::FormError>(())
454/// ```
455pub trait FromForm: Sized {
456 /// The builder that collects the fields.
457 type Builder: FormBuilder<Output = Self> + Default;
458
459 /// From a urlencoded body, with the default limits and policy.
460 ///
461 /// # Errors
462 ///
463 /// The parser's errors; `Extract { .. }`.
464 fn from_urlencoded(input: &[u8]) -> Result<Self, FormError> {
465 Self::from_urlencoded_with(input, &Limits::DEFAULT, &Policy::DEFAULT)
466 }
467
468 /// From a query string; one leading `?` is stripped.
469 ///
470 /// # Errors
471 ///
472 /// As [`from_urlencoded`](Self::from_urlencoded).
473 fn from_query(query: &[u8]) -> Result<Self, FormError> {
474 Self::from_urlencoded(query.strip_prefix(b"?").unwrap_or(query))
475 }
476
477 /// From a buffered `multipart/form-data` body.
478 ///
479 /// # Errors
480 ///
481 /// The parser's errors; `Extract { .. }`.
482 fn from_multipart(content_type: &[u8], body: &[u8]) -> Result<Self, FormError> {
483 let mp = crate::parse_multipart(content_type, body)?;
484 let mut b = Self::Builder::default();
485 feed_multipart(&mut b, &mp)?;
486 b.finish()
487 }
488
489 /// From either encoding, by the `Content-Type`.
490 ///
491 /// # Errors
492 ///
493 /// `UnsupportedMediaType`; the parsers' errors; `Extract { .. }`.
494 fn from_body(content_type: &[u8], body: &[u8]) -> Result<Self, FormError> {
495 Self::from_body_with(content_type, body, &Limits::DEFAULT, &Policy::DEFAULT)
496 }
497
498 /// From a urlencoded body with explicit limits and policy.
499 ///
500 /// # Errors
501 ///
502 /// As [`from_urlencoded`](Self::from_urlencoded); `InvalidLimits`.
503 fn from_urlencoded_with(
504 input: &[u8],
505 limits: &Limits,
506 policy: &Policy,
507 ) -> Result<Self, FormError> {
508 limits.validate()?;
509 let mut b = Self::Builder::default();
510 feed_urlencoded(&mut b, input, limits, policy, &mut NoInspector)?;
511 b.finish()
512 }
513
514 /// From either encoding with explicit limits and policy.
515 ///
516 /// # Errors
517 ///
518 /// As [`from_body`](Self::from_body); `InvalidLimits`.
519 fn from_body_with(
520 content_type: &[u8],
521 body: &[u8],
522 limits: &Limits,
523 policy: &Policy,
524 ) -> Result<Self, FormError> {
525 Self::from_body_inspected(content_type, body, limits, policy, &mut NoInspector)
526 }
527
528 /// From either encoding, with an inspector in the same pass: the
529 /// inspector and the struct see the same bytes.
530 ///
531 /// # Errors
532 ///
533 /// As [`from_body_with`](Self::from_body_with); `Refused`.
534 fn from_body_inspected<I: Inspector>(
535 content_type: &[u8],
536 body: &[u8],
537 limits: &Limits,
538 policy: &Policy,
539 inspector: &mut I,
540 ) -> Result<Self, FormError> {
541 let result = (|| {
542 let mut b = Self::Builder::default();
543 match FormKind::detect(content_type)? {
544 FormKind::Urlencoded => {
545 limits.validate()?;
546 feed_urlencoded(&mut b, body, limits, policy, inspector)?;
547 }
548 FormKind::Multipart(_) => {
549 let mp = crate::multipart::parse_multipart_inspected_raw(
550 content_type,
551 body,
552 limits,
553 policy,
554 inspector,
555 )?;
556 feed_multipart(&mut b, &mp)?;
557 }
558 }
559 b.finish()
560 })();
561 if let Err(e) = &result {
562 inspector.failed(e);
563 }
564 result
565 }
566
567 /// From either encoding, collecting every field's error instead of
568 /// stopping at the first (D-090), so a form can be shown again with a
569 /// message beside each field.
570 ///
571 /// # Errors
572 ///
573 /// [`FormErrors`]: the parse error alone, or every extraction error.
574 fn from_body_all(content_type: &[u8], body: &[u8]) -> Result<Self, FormErrors> {
575 Self::from_body_all_with(content_type, body, &Limits::DEFAULT, &Policy::DEFAULT)
576 }
577
578 /// As [`from_body_all`](Self::from_body_all), with explicit limits and
579 /// policy.
580 ///
581 /// # Errors
582 ///
583 /// As [`from_body_all`](Self::from_body_all); `InvalidLimits`.
584 fn from_body_all_with(
585 content_type: &[u8],
586 body: &[u8],
587 limits: &Limits,
588 policy: &Policy,
589 ) -> Result<Self, FormErrors> {
590 let mut errors = alloc::vec::Vec::new();
591 let mut b = Collect {
592 inner: Self::Builder::default(),
593 errors: &mut errors,
594 };
595 match FormKind::detect(content_type)? {
596 FormKind::Urlencoded => {
597 limits.validate()?;
598 feed_urlencoded(&mut b, body, limits, policy, &mut NoInspector)?;
599 }
600 FormKind::Multipart(_) => {
601 let mp = crate::parse_multipart_with(content_type, body, limits, policy)?;
602 feed_multipart(&mut b, &mp)?;
603 }
604 }
605 let Collect { inner, errors: _ } = b;
606 let value = inner.finish_all(&mut errors);
607 match value {
608 Some(v) if errors.is_empty() => Ok(v),
609 _ => Err(FormErrors(errors)),
610 }
611 }
612
613 /// From an upload that stays usable (D-090): text fields are read by
614 /// reference, and only the files the struct declares are moved out of
615 /// it; files under `ignore = [..]` names stay in the upload for the
616 /// caller. Unknown names are refused as in
617 /// [`from_upload`](Self::from_upload). On error, files already moved
618 /// into the builder are dropped (deleted).
619 ///
620 /// # Errors
621 ///
622 /// `Extract { .. }`.
623 ///
624 /// # Examples
625 ///
626 /// ```
627 /// use form_net::FromForm;
628 ///
629 /// form_net::form! {
630 /// pub struct Post { pub title: String, pub cover: form_net::StoredFile }
631 /// ignore = ["attachment"];
632 /// }
633 ///
634 /// let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHi\r\n--X\r\nContent-Disposition: form-data; name=cover; filename=c.png\r\n\r\nC\r\n--X\r\nContent-Disposition: form-data; name=attachment; filename=a.pdf\r\n\r\nA\r\n--X--";
635 /// let mut upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
636 /// let post = Post::from_upload_mut(&mut upload)?;
637 /// assert_eq!((post.title.as_str(), post.cover.len()), ("Hi", 1));
638 /// assert_eq!(upload.field("title"), Some("Hi"));
639 /// assert!(upload.file("cover").is_none() && upload.file("attachment").is_some());
640 /// # Ok::<(), form_net::FormError>(())
641 /// ```
642 #[cfg(feature = "fs")]
643 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
644 fn from_upload_mut(upload: &mut crate::store::Upload) -> Result<Self, FormError> {
645 let mut b = Self::Builder::default();
646 for (name, value) in upload.iter() {
647 b.text(name, value)?;
648 }
649 let files = upload.files_mut();
650 let mut i = 0usize;
651 while let Some(file) = files.get(i) {
652 match b.file_field(file.name()) {
653 Some(true) => {
654 let file = files.remove(i);
655 let name = alloc::string::String::from(file.name());
656 b.file(&name, FileValue::Stored(file))?;
657 }
658 Some(false) => i = i.wrapping_add(1),
659 None => return Err(__unknown()),
660 }
661 }
662 b.finish()
663 }
664
665 /// From a finished upload: text fields as text, files moved into the
666 /// struct as `StoredFile`s.
667 ///
668 /// # Errors
669 ///
670 /// `Extract { .. }`.
671 #[cfg(feature = "fs")]
672 #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
673 fn from_upload(upload: crate::store::Upload) -> Result<Self, FormError> {
674 let (fields, files) = upload.into_parts();
675 let mut b = Self::Builder::default();
676 for (name, value) in fields.iter() {
677 b.text(name, value)?;
678 }
679 for file in files {
680 let name = alloc::string::String::from(file.name());
681 b.file(&name, FileValue::Stored(file))?;
682 }
683 b.finish()
684 }
685}
686
687/// Maps a field's [`ValueError`] to the error `form!` returns. Not part of
688/// the API: used by the macro expansion.
689#[doc(hidden)]
690#[must_use]
691pub fn __value_error(e: ValueError, field: &'static str) -> FormError {
692 let reason = match e {
693 ValueError::Missing => ExtractError::Missing,
694 ValueError::Duplicate => ExtractError::Duplicate,
695 ValueError::Invalid(why) => ExtractError::Invalid(why),
696 _ => ExtractError::WrongKind,
697 };
698 FormError::Extract {
699 reason,
700 field: Some(field),
701 }
702}
703
704/// Records a field's error at `finish_all`, leaving out a `Missing` for a
705/// field that already failed when its value arrived. Not part of the API.
706#[doc(hidden)]
707pub fn __push_field_error(
708 errors: &mut alloc::vec::Vec<FormError>,
709 e: ValueError,
710 field: &'static str,
711) {
712 let seen = errors
713 .iter()
714 .any(|x| matches!(x, FormError::Extract { field: Some(f), .. } if *f == field));
715 if !(seen && e == ValueError::Missing) {
716 errors.push(__value_error(e, field));
717 }
718}
719
720/// The error for a name the struct does not declare. Not part of the API.
721#[doc(hidden)]
722#[must_use]
723pub const fn __unknown() -> FormError {
724 FormError::Extract {
725 reason: ExtractError::Unknown,
726 field: None,
727 }
728}