Skip to main content

form_net/store/
mod.rs

1//! Upload storage: files kept in memory up to a threshold and spilled to
2//! private temporary files above it. No client byte is ever part of a path.
3//!
4//! Spilling is blocking file I/O: a server with an event loop runs a parse
5//! that stores uploads where blocking is allowed, or gives the driver its
6//! own [`Sink`](crate::Sink).
7
8mod quota;
9mod sweep;
10mod temp;
11
12pub use quota::{SpillBudget, SpillQuota};
13pub use sweep::sweep;
14
15use alloc::borrow::ToOwned;
16use alloc::string::String;
17use alloc::sync::Arc;
18use alloc::vec::Vec;
19use core::fmt;
20use core::sync::atomic::{AtomicU64, Ordering};
21use std::fs::File;
22use std::io::{self, Read, Seek, SeekFrom, Write};
23use std::path::{Path, PathBuf};
24use std::sync::OnceLock;
25
26use crate::error::{ExtractError, FormError, Limit, Location};
27use crate::hook::{Inspector, Sink};
28use crate::limits::{Limits, Policy};
29use crate::multipart::driver::{Driver, Progress};
30use crate::multipart::filename::{FileName, FileNameSource};
31use crate::multipart::parser::{ContentType, MultipartParser, PartHead};
32
33/// What went wrong with storage.
34#[non_exhaustive]
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum StoreError {
37    /// The shared spill budget is full (status hint 503).
38    QuotaExceeded,
39    /// `Store::in_dir` or `sweep` was given a non-directory or a link.
40    NotADirectory,
41    /// `persist`'s destination exists; nothing was overwritten.
42    DestinationExists,
43}
44
45impl StoreError {
46    #[cfg(target_has_atomic = "32")]
47    pub(crate) const fn index(self) -> usize {
48        match self {
49            Self::QuotaExceeded => 0,
50            Self::NotADirectory => 1,
51            Self::DestinationExists => 2,
52        }
53    }
54
55    pub(crate) const fn code(self) -> &'static str {
56        match self {
57            Self::QuotaExceeded => "store.quota_exceeded",
58            Self::NotADirectory => "store.not_a_directory",
59            Self::DestinationExists => "store.destination_exists",
60        }
61    }
62
63    pub(crate) const fn describe(self) -> &'static str {
64        match self {
65            Self::QuotaExceeded => "the shared upload budget is full; retry later",
66            Self::NotADirectory => "not a directory, or a link to one",
67            Self::DestinationExists => "the destination exists; nothing was overwritten",
68        }
69    }
70}
71
72/// How spilled files are kept.
73///
74/// | | `Anonymous` (default) | `Named` |
75/// |---|---|---|
76/// | Unix | unlinked right after creation | keeps its random name until dropped or persisted |
77/// | Windows | delete-on-close, no sharing | deleted on drop |
78/// | after a crash | nothing is left | leftovers are removed by `sweep` (0.5) |
79/// | `persist` | a copy | a hard link on the same file system, else a copy |
80#[non_exhaustive]
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
82pub enum SpillMode {
83    /// The file has no name the moment it exists.
84    #[default]
85    Anonymous,
86    /// The file keeps a random name in the store's directory.
87    Named,
88}
89
90/// A snapshot of what a store holds, read from atomic counters.
91#[non_exhaustive]
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct StoreStats {
94    /// Files held in memory now.
95    pub files_in_memory: u64,
96    /// Files on disk now.
97    pub files_on_disk: u64,
98    /// Bytes held in memory now.
99    pub bytes_in_memory: u64,
100    /// Bytes on disk now.
101    pub bytes_on_disk: u64,
102    /// Files spilled to disk since the store was created.
103    pub files_spilled: u64,
104    /// Deletions that failed during a drop, since the store was created.
105    pub cleanup_failures: u64,
106}
107
108#[derive(Default)]
109struct Counters {
110    files_in_memory: AtomicU64,
111    files_on_disk: AtomicU64,
112    bytes_in_memory: AtomicU64,
113    bytes_on_disk: AtomicU64,
114    files_spilled: AtomicU64,
115    cleanup_failures: AtomicU64,
116}
117
118fn add(c: &AtomicU64, n: u64) {
119    c.fetch_add(n, Ordering::Relaxed);
120}
121
122fn sub(c: &AtomicU64, n: u64) {
123    // Saturating: a counter never wraps below zero.
124    let mut v = c.load(Ordering::Relaxed);
125    loop {
126        match c.compare_exchange_weak(v, v.saturating_sub(n), Ordering::Relaxed, Ordering::Relaxed)
127        {
128            Ok(_) => return,
129            Err(now) => v = now,
130        }
131    }
132}
133
134/// What files share with their store: the directory and the counters. A
135/// store's own directory is removed (if empty) when the store and all its
136/// files are gone.
137struct Shared {
138    dir: OnceLock<PathBuf>,
139    owns_dir: bool,
140    counters: Counters,
141}
142
143impl Drop for Shared {
144    fn drop(&mut self) {
145        if self.owns_dir {
146            if let Some(dir) = self.dir.get() {
147                let _ = std::fs::remove_dir(dir);
148            }
149        }
150    }
151}
152
153impl Shared {
154    fn dir(&self) -> Result<&Path, FormError> {
155        if let Some(d) = self.dir.get() {
156            return Ok(d);
157        }
158        let created = temp::create_private_dir()?;
159        if let Err(lost) = self.dir.set(created) {
160            // Another thread created one first; remove ours.
161            let _ = std::fs::remove_dir(lost);
162        }
163        self.dir.get().map(PathBuf::as_path).ok_or(FormError::Io {
164            kind: io::ErrorKind::Other,
165        })
166    }
167
168    fn cleanup_failed(&self) {
169        add(&self.counters.cleanup_failures, 1);
170    }
171}
172
173/// Where uploads are kept. `Send + Sync`; create one per process or per
174/// route and share it by reference.
175///
176/// # Examples
177///
178/// ```
179/// use form_net::{SpillMode, Store};
180///
181/// // Files over 64 KiB spill to anonymous temporary files.
182/// let store = Store::temp()?;
183/// assert!(store.dir().is_some());
184///
185/// // Never touch the file system.
186/// let memory = Store::memory_only().memory_threshold(1 << 20);
187/// assert!(memory.dir().is_none());
188/// # Ok::<(), form_net::FormError>(())
189/// ```
190#[derive(Clone)]
191pub struct Store {
192    shared: Arc<Shared>,
193    memory_only: bool,
194    threshold: u64,
195    max_disk: Option<u64>,
196    mode: SpillMode,
197    budget: Option<Arc<dyn SpillBudget>>,
198}
199
200impl fmt::Debug for Store {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.debug_struct("Store")
203            .field("memory_only", &self.memory_only)
204            .field("threshold", &self.threshold)
205            .field("mode", &self.mode)
206            .field("quota", &self.budget.is_some())
207            .field("stats", &self.stats())
208            .finish_non_exhaustive()
209    }
210}
211
212const DEFAULT_THRESHOLD: u64 = 64 * 1024;
213
214impl Store {
215    fn with(dir: Option<PathBuf>, owns_dir: bool, memory_only: bool) -> Store {
216        let lock = OnceLock::new();
217        if let Some(d) = dir {
218            let _ = lock.set(d);
219        }
220        Store {
221            shared: Arc::new(Shared {
222                dir: lock,
223                owns_dir,
224                counters: Counters::default(),
225            }),
226            memory_only,
227            threshold: DEFAULT_THRESHOLD,
228            max_disk: None,
229            mode: SpillMode::Anonymous,
230            budget: None,
231        }
232    }
233
234    /// A store with its own private directory under the system temporary
235    /// directory, created now: `form-net-<32 hex>`, owner-only on Unix,
236    /// failing if the path exists. Files up to 64 KiB stay in memory;
237    /// larger ones spill to anonymous temporary files.
238    ///
239    /// # Errors
240    ///
241    /// `Io { kind }` when the directory cannot be created; `Random` when the
242    /// OS random source fails (no predictable fallback name).
243    pub fn temp() -> Result<Store, FormError> {
244        let dir = temp::create_private_dir()?;
245        Ok(Store::with(Some(dir), true, false))
246    }
247
248    /// A store whose private directory is created only when a file first
249    /// spills (for one-shot parses).
250    pub(crate) fn temp_lazy() -> Store {
251        Store::with(None, true, false)
252    }
253
254    /// A store that never touches the file system: a file larger than
255    /// `max_file_bytes` is refused, never spilled.
256    #[must_use]
257    pub fn memory_only() -> Store {
258        let mut s = Store::with(None, false, true);
259        s.threshold = u64::MAX;
260        s
261    }
262
263    /// A store that spills into `dir`, a directory the caller made. It must
264    /// exist and be a directory, not a link to one.
265    ///
266    /// # Errors
267    ///
268    /// `Store { NotADirectory }`; `Io { kind }`.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// let dir = std::env::temp_dir();
274    /// let store = form_net::Store::in_dir(&dir)?;
275    /// assert_eq!(store.dir(), Some(dir.as_path()));
276    /// # Ok::<(), form_net::FormError>(())
277    /// ```
278    pub fn in_dir(dir: impl AsRef<Path>) -> Result<Store, FormError> {
279        let dir = dir.as_ref();
280        temp::check_dir(dir)?;
281        Ok(Store::with(Some(dir.to_owned()), false, false))
282    }
283
284    /// Keeps each file in memory up to `bytes`; a file whose next write would
285    /// pass it spills first. `0` spills every file at its first byte.
286    #[must_use]
287    pub fn memory_threshold(mut self, bytes: u64) -> Store {
288        if !self.memory_only {
289            self.threshold = bytes;
290        }
291        self
292    }
293
294    /// Caps the bytes one request may put on disk (`Limit { Disk }` beyond).
295    /// Unset, the cap is the request's `max_body_bytes`.
296    #[must_use]
297    pub fn max_disk_bytes(mut self, bytes: u64) -> Store {
298        self.max_disk = Some(bytes);
299        self
300    }
301
302    /// How spilled files are kept.
303    #[must_use]
304    pub fn mode(mut self, mode: SpillMode) -> Store {
305        self.mode = mode;
306        self
307    }
308
309    /// The directory files spill into, once it exists.
310    #[must_use]
311    pub fn dir(&self) -> Option<&Path> {
312        self.shared.dir.get().map(PathBuf::as_path)
313    }
314
315    /// Deletions that failed during a drop (they cannot be returned).
316    #[must_use]
317    pub fn cleanup_failures(&self) -> u64 {
318        self.shared
319            .counters
320            .cleanup_failures
321            .load(Ordering::Relaxed)
322    }
323
324    /// What the store holds now.
325    #[must_use]
326    pub fn stats(&self) -> StoreStats {
327        let c = &self.shared.counters;
328        StoreStats {
329            files_in_memory: c.files_in_memory.load(Ordering::Relaxed),
330            files_on_disk: c.files_on_disk.load(Ordering::Relaxed),
331            bytes_in_memory: c.bytes_in_memory.load(Ordering::Relaxed),
332            bytes_on_disk: c.bytes_on_disk.load(Ordering::Relaxed),
333            files_spilled: c.files_spilled.load(Ordering::Relaxed),
334            cleanup_failures: c.cleanup_failures.load(Ordering::Relaxed),
335        }
336    }
337
338    /// Adds a disk budget shared across requests (and across stores given
339    /// the same budget): bytes are reserved from it before every disk write,
340    /// and a request that would cross it is refused with
341    /// `Store { QuotaExceeded }` (status hint 503). Replaces an earlier one.
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use form_net::{Limits, NoInspector, Policy, SpillQuota, Store, Upload};
347    ///
348    /// let quota = SpillQuota::new(4);
349    /// let store = Store::temp()?.memory_threshold(0).quota(quota.clone());
350    /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=a\r\n\r\n12345\r\n--X--";
351    /// let e = Upload::parse_with(b"multipart/form-data; boundary=X", &body[..], &Limits::DEFAULT, &Policy::DEFAULT, &store, &mut NoInspector).unwrap_err();
352    /// assert_eq!(e.status_hint(), 503);
353    /// assert_eq!(quota.in_use(), 0, "the refused file gave its bytes back");
354    /// # Ok::<(), form_net::FormError>(())
355    /// ```
356    pub fn quota(mut self, budget: impl SpillBudget) -> Store {
357        self.budget = Some(Arc::new(budget));
358        self
359    }
360
361    /// The worst-case bytes one request through this store can hold in
362    /// memory under `limits`: [`Limits::memory_bound`] with this store's
363    /// memory threshold (`u64::MAX` for a memory-only store).
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use form_net::{Limits, Store};
369    ///
370    /// let limits = Limits::DEFAULT;
371    /// assert!(Store::temp()?.memory_bound(&limits) < Store::memory_only().memory_bound(&limits));
372    /// # Ok::<(), form_net::FormError>(())
373    /// ```
374    #[must_use]
375    pub fn memory_bound(&self, limits: &Limits) -> u64 {
376        let in_memory = if self.memory_only {
377            u64::MAX
378        } else {
379            self.threshold
380        };
381        limits.memory_bound(in_memory)
382    }
383
384    /// The `Sink` that stores the files of one request under `limits`.
385    #[must_use]
386    pub fn sink(&self, limits: &Limits) -> StoreSink {
387        StoreSink {
388            budget: self.budget.clone(),
389            shared: Arc::clone(&self.shared),
390            memory_only: self.memory_only,
391            threshold: self.threshold,
392            // A memory file never holds more than this: its buffer is
393            // capped here, so `memory_bound` is exact.
394            memory_cap: usize::try_from(self.threshold.min(limits.file())).unwrap_or(usize::MAX),
395            mode: self.mode,
396            max_disk: self.max_disk.unwrap_or(limits.max_body_bytes),
397            used_disk: 0,
398            current: None,
399        }
400    }
401}
402
403/// One file being written.
404struct Current {
405    name: String,
406    filename: Option<(Vec<u8>, FileNameSource)>,
407    content_type: Option<Vec<u8>>,
408    memory: Vec<u8>,
409    disk: Option<(File, Option<PathBuf>)>,
410    len: u64,
411    /// Bytes of this file written to disk (and added to the counters).
412    on_disk: u64,
413}
414
415/// The [`Sink`] a [`Store`] hands out for one request.
416pub struct StoreSink {
417    budget: Option<Arc<dyn SpillBudget>>,
418    shared: Arc<Shared>,
419    memory_only: bool,
420    threshold: u64,
421    memory_cap: usize,
422    mode: SpillMode,
423    max_disk: u64,
424    used_disk: u64,
425    current: Option<Current>,
426}
427
428impl fmt::Debug for StoreSink {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        f.debug_struct("StoreSink")
431            .field("used_disk", &self.used_disk)
432            .field("max_disk", &self.max_disk)
433            .finish_non_exhaustive()
434    }
435}
436
437impl StoreSink {
438    /// Bytes this request has put on disk so far.
439    pub(crate) const fn used_disk(&self) -> u64 {
440        self.used_disk
441    }
442
443    /// Continues a request that already put `bytes` on disk.
444    pub(crate) const fn with_used_disk(mut self, bytes: u64) -> StoreSink {
445        self.used_disk = bytes;
446        self
447    }
448
449    fn disk_write(&mut self, bytes: &[u8]) -> Result<(), FormError> {
450        let used = self.used_disk.saturating_add(bytes.len() as u64);
451        if used > self.max_disk {
452            return Err(FormError::limit(Limit::Disk, Location::NONE));
453        }
454        let Some(cur) = self.current.as_mut() else {
455            return Ok(());
456        };
457        let Some((file, _)) = cur.disk.as_mut() else {
458            return Ok(());
459        };
460        let n = bytes.len() as u64;
461        if let Some(budget) = &self.budget {
462            if !budget.reserve(n) {
463                return Err(FormError::Store {
464                    reason: StoreError::QuotaExceeded,
465                });
466            }
467        }
468        if let Err(e) = file.write_all(bytes) {
469            // Reserved, never written: give the bytes back now. (A partial
470            // write is removed with the file on abort, which releases only
471            // `on_disk`.)
472            if let Some(budget) = &self.budget {
473                budget.release(n);
474            }
475            return Err(temp::io(&e));
476        }
477        self.used_disk = used;
478        cur.on_disk = cur.on_disk.saturating_add(bytes.len() as u64);
479        add(&self.shared.counters.bytes_on_disk, bytes.len() as u64);
480        Ok(())
481    }
482
483    fn spill(&mut self) -> Result<(), FormError> {
484        let dir = self.shared.dir()?.to_owned();
485        let anonymous = self.mode == SpillMode::Anonymous;
486        let (file, path) = temp::create_file(&dir, anonymous)?;
487        add(&self.shared.counters.files_on_disk, 1);
488        add(&self.shared.counters.files_spilled, 1);
489        let prefix = match self.current.as_mut() {
490            Some(cur) => {
491                cur.disk = Some((file, path));
492                core::mem::take(&mut cur.memory)
493            }
494            None => Vec::new(),
495        };
496        let mut prefix = prefix;
497        let r = self.disk_write(&prefix);
498        crate::wipe::wipe_vec(&mut prefix);
499        r
500    }
501}
502
503impl Sink for StoreSink {
504    type Output = StoredFile;
505
506    fn open(&mut self, head: &PartHead<'_>) -> Result<(), FormError> {
507        self.abort();
508        self.current = Some(Current {
509            name: String::from(head.name()),
510            filename: head.filename().map(|f| (f.raw().to_vec(), f.source())),
511            content_type: head.content_type().map(|c| c.raw().to_vec()),
512            memory: Vec::new(),
513            disk: None,
514            len: 0,
515            on_disk: 0,
516        });
517        Ok(())
518    }
519
520    fn write(&mut self, chunk: &[u8]) -> Result<(), FormError> {
521        let (on_disk, len) = match self.current.as_ref() {
522            Some(c) => (c.disk.is_some(), c.len),
523            None => return Ok(()),
524        };
525        let new_len = len.saturating_add(chunk.len() as u64);
526        if !on_disk && !self.memory_only && new_len > self.threshold {
527            self.spill()?;
528        }
529        let on_disk = self.current.as_ref().is_some_and(|c| c.disk.is_some());
530        if on_disk {
531            self.disk_write(chunk)?;
532        } else if let Some(cur) = self.current.as_mut() {
533            crate::wipe::append_wiping(&mut cur.memory, chunk, 4096, self.memory_cap);
534        }
535        if let Some(cur) = self.current.as_mut() {
536            cur.len = new_len;
537        }
538        Ok(())
539    }
540
541    fn close(&mut self) -> Result<StoredFile, FormError> {
542        let Some(mut cur) = self.current.take() else {
543            return Err(FormError::Poisoned);
544        };
545        let on_disk = cur.disk.is_some();
546        let place = match cur.disk.take() {
547            Some((file, path)) => Place::Disk {
548                file: Some(file),
549                path,
550            },
551            None => {
552                add(&self.shared.counters.files_in_memory, 1);
553                add(&self.shared.counters.bytes_in_memory, cur.len);
554                Place::Memory(core::mem::take(&mut cur.memory))
555            }
556        };
557        Ok(StoredFile {
558            name: core::mem::take(&mut cur.name),
559            filename: cur.filename.take(),
560            content_type: cur.content_type.take(),
561            len: cur.len,
562            place,
563            shared: Some(Arc::clone(&self.shared)),
564            budget: if on_disk { self.budget.clone() } else { None },
565            reserved: cur.on_disk,
566        })
567    }
568
569    fn abort(&mut self) {
570        if let Some(mut cur) = self.current.take() {
571            crate::wipe::wipe_vec(&mut cur.memory);
572            wipe_meta(&mut cur.name, &mut cur.filename, &mut cur.content_type);
573            if let Some((file, path)) = cur.disk.take() {
574                // Close first, then remove the name: an open file cannot be
575                // removed on Windows.
576                drop(file);
577                sub(&self.shared.counters.files_on_disk, 1);
578                sub(&self.shared.counters.bytes_on_disk, cur.on_disk);
579                if let Some(budget) = &self.budget {
580                    budget.release(cur.on_disk);
581                }
582                if let Some(p) = path {
583                    if std::fs::remove_file(&p).is_err() {
584                        self.shared.cleanup_failed();
585                    }
586                }
587            }
588        }
589    }
590}
591
592impl Drop for StoreSink {
593    fn drop(&mut self) {
594        self.abort();
595    }
596}
597
598enum Place {
599    Memory(Vec<u8>),
600    Disk {
601        file: Option<File>,
602        path: Option<PathBuf>,
603    },
604    Gone,
605}
606
607/// One received file, in memory or in a temporary file.
608///
609/// Dropping it without [`persist`](Self::persist) deletes it (closing the
610/// handle first) and wipes memory-held bytes. `Debug` never prints the file
611/// name or the data.
612///
613/// # Examples
614///
615/// ```
616/// use std::io::Read;
617///
618/// let body = b"--X\r\nContent-Disposition: form-data; name=doc; filename=a.txt\r\n\r\nhello\r\n--X--";
619/// let upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
620/// let file = upload.file("doc").unwrap();
621/// let mut text = String::new();
622/// file.reader()?.read_to_string(&mut text).unwrap();
623/// assert_eq!((text.as_str(), file.len(), file.is_in_memory()), ("hello", 5, true));
624/// # Ok::<(), form_net::FormError>(())
625/// ```
626#[must_use]
627pub struct StoredFile {
628    name: String,
629    filename: Option<(Vec<u8>, FileNameSource)>,
630    content_type: Option<Vec<u8>>,
631    len: u64,
632    place: Place,
633    shared: Option<Arc<Shared>>,
634    /// The budget this file's disk bytes were reserved from.
635    budget: Option<Arc<dyn SpillBudget>>,
636    reserved: u64,
637}
638
639impl fmt::Debug for StoredFile {
640    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641        f.debug_struct("StoredFile")
642            .field("name", &self.name)
643            .field("len", &self.len)
644            .field("in_memory", &self.is_in_memory())
645            .finish_non_exhaustive()
646    }
647}
648
649impl StoredFile {
650    /// A memory-held file not tied to a store (from a buffered part).
651    pub(crate) fn detached(
652        name: &str,
653        filename: Option<FileName<'_>>,
654        content_type: Option<&[u8]>,
655        bytes: &[u8],
656    ) -> StoredFile {
657        StoredFile {
658            name: String::from(name),
659            filename: filename.map(|f| (f.raw().to_vec(), f.source())),
660            content_type: content_type.map(<[u8]>::to_vec),
661            len: bytes.len() as u64,
662            place: Place::Memory(bytes.to_vec()),
663            shared: None,
664            budget: None,
665            reserved: 0,
666        }
667    }
668
669    /// The form field name.
670    #[must_use]
671    pub fn name(&self) -> &str {
672        &self.name
673    }
674
675    /// The client's file name: a hint, never a path.
676    #[must_use]
677    pub fn filename(&self) -> Option<FileName<'_>> {
678        self.filename
679            .as_ref()
680            .map(|(raw, source)| FileName::new(raw, *source))
681    }
682
683    /// The declared content type: a hint, never trusted.
684    #[must_use]
685    pub fn content_type(&self) -> Option<ContentType<'_>> {
686        self.content_type.as_deref().and_then(ContentType::parse)
687    }
688
689    /// The length in bytes.
690    #[must_use]
691    pub const fn len(&self) -> u64 {
692        self.len
693    }
694
695    /// Whether the file is empty.
696    #[must_use]
697    pub const fn is_empty(&self) -> bool {
698        self.len == 0
699    }
700
701    /// Whether the file is held in memory.
702    #[must_use]
703    pub const fn is_in_memory(&self) -> bool {
704        matches!(self.place, Place::Memory(_))
705    }
706
707    /// The bytes, while the file is in memory.
708    #[must_use]
709    pub fn bytes(&self) -> Option<&[u8]> {
710        match &self.place {
711            Place::Memory(b) => Some(b),
712            _ => None,
713        }
714    }
715
716    /// A reader over the file, in memory or on disk. It reads with
717    /// positional reads, so several readers can exist at once.
718    ///
719    /// # Errors
720    ///
721    /// `Io { kind }` when the file is gone.
722    pub fn reader(&self) -> Result<StoredReader<'_>, FormError> {
723        match &self.place {
724            Place::Gone => Err(FormError::Io {
725                kind: io::ErrorKind::NotFound,
726            }),
727            _ => Ok(StoredReader { file: self, pos: 0 }),
728        }
729    }
730
731    /// The whole file in a new vector, refusing a file longer than `max`
732    /// before allocating.
733    ///
734    /// # Errors
735    ///
736    /// `Limit { FileSize }`; `Io { kind }`.
737    pub fn read_to_vec(&self, max: u64) -> Result<Vec<u8>, FormError> {
738        if self.len > max {
739            return Err(FormError::limit(Limit::FileSize, Location::NONE));
740        }
741        let mut out = Vec::with_capacity(usize::try_from(self.len).unwrap_or(0));
742        self.reader()?
743            .read_to_end(&mut out)
744            .map_err(|e| temp::io(&e))?;
745        Ok(out)
746    }
747
748    /// The temporary file's path, in `SpillMode::Named` only.
749    #[must_use]
750    pub fn path(&self) -> Option<&Path> {
751        match &self.place {
752            Place::Disk { path, .. } => path.as_deref(),
753            _ => None,
754        }
755    }
756
757    fn read_at(&self, buf: &mut [u8], pos: u64) -> io::Result<usize> {
758        match &self.place {
759            Place::Memory(b) => {
760                let start = usize::try_from(pos).unwrap_or(usize::MAX).min(b.len());
761                let src = b.get(start..).unwrap_or(&[]);
762                let n = src.len().min(buf.len());
763                if let (Some(dst), Some(src)) = (buf.get_mut(..n), src.get(..n)) {
764                    dst.copy_from_slice(src);
765                }
766                Ok(n)
767            }
768            Place::Disk { file: Some(f), .. } => temp::read_at(f, buf, pos),
769            _ => Err(io::Error::from(io::ErrorKind::NotFound)),
770        }
771    }
772
773    /// Moves the finished upload to `dest`, which the caller built. It never
774    /// overwrites: a named file is hard-linked when `dest` is on the same file
775    /// system and copied otherwise; anonymous and memory files are copied
776    /// into `dest` opened with `create_new`. Whether to fsync `dest` and its
777    /// directory is the caller's choice. On error the temporary file is
778    /// deleted like any dropped file.
779    ///
780    /// # Errors
781    ///
782    /// `Store { DestinationExists }`; `Io { kind }`.
783    pub fn persist(mut self, dest: &Path) -> Result<(), FormError> {
784        let exists = |e: io::Error| {
785            if e.kind() == io::ErrorKind::AlreadyExists {
786                FormError::Store {
787                    reason: StoreError::DestinationExists,
788                }
789            } else {
790                temp::io(&e)
791            }
792        };
793        if let Place::Disk {
794            file,
795            path: Some(p),
796        } = &mut self.place
797        {
798            // Close before linking and removing: Windows needs it.
799            drop(file.take());
800            match std::fs::hard_link(&*p, dest) {
801                Ok(()) => {
802                    let p = p.clone();
803                    self.place = Place::Gone;
804                    self.release();
805                    self.release_budget();
806                    if std::fs::remove_file(&p).is_err() {
807                        if let Some(s) = &self.shared {
808                            s.cleanup_failed();
809                        }
810                    }
811                    return Ok(());
812                }
813                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => return Err(exists(e)),
814                Err(_) => {
815                    // Another file system: copy below, from the name.
816                    let f = File::open(&*p).map_err(|e| temp::io(&e))?;
817                    *file = Some(f);
818                }
819            }
820        }
821        let mut out = std::fs::OpenOptions::new()
822            .write(true)
823            .create_new(true)
824            .open(dest)
825            .map_err(exists)?;
826        let mut buf = [0u8; 64 * 1024];
827        let mut hi = 0usize;
828        let mut pos = 0u64;
829        let copied = loop {
830            let n = match self.read_at(&mut buf, pos) {
831                Ok(0) => break Ok(()),
832                Ok(n) => n,
833                Err(e) => break Err(temp::io(&e)),
834            };
835            hi = hi.max(n);
836            if let Err(e) = out.write_all(buf.get(..n).unwrap_or(&[])) {
837                break Err(temp::io(&e));
838            }
839            pos = pos.saturating_add(n as u64);
840        };
841        crate::wipe::wipe(buf.get_mut(..hi).unwrap_or(&mut []));
842        if copied.is_err() {
843            // Never leave a partial copy under the caller's name.
844            drop(out);
845            let _ = std::fs::remove_file(dest);
846        }
847        copied
848    }
849
850    /// Gives the file's disk bytes back to the shared budget, once.
851    fn release_budget(&mut self) {
852        if let Some(budget) = self.budget.take() {
853            budget.release(self.reserved);
854        }
855    }
856
857    /// Takes the file off the store's counters.
858    fn release(&mut self) {
859        if let Some(s) = &self.shared {
860            match &self.place {
861                Place::Memory(_) => {
862                    sub(&s.counters.files_in_memory, 1);
863                    sub(&s.counters.bytes_in_memory, self.len);
864                }
865                _ => {
866                    sub(&s.counters.files_on_disk, 1);
867                    sub(&s.counters.bytes_on_disk, self.len);
868                }
869            }
870        }
871    }
872}
873
874/// Wipes a file's name, file name, and declared content type.
875fn wipe_meta(
876    name: &mut String,
877    filename: &mut Option<(Vec<u8>, FileNameSource)>,
878    content_type: &mut Option<Vec<u8>>,
879) {
880    crate::wipe::wipe_string(name);
881    if let Some((mut f, _)) = filename.take() {
882        crate::wipe::wipe_vec(&mut f);
883    }
884    if let Some(mut c) = content_type.take() {
885        crate::wipe::wipe_vec(&mut c);
886    }
887}
888
889impl Drop for StoredFile {
890    fn drop(&mut self) {
891        wipe_meta(&mut self.name, &mut self.filename, &mut self.content_type);
892        match core::mem::replace(&mut self.place, Place::Gone) {
893            Place::Memory(mut b) => {
894                self.place = Place::Memory(Vec::new());
895                self.release();
896                crate::wipe::wipe_vec(&mut b);
897            }
898            Place::Disk { file, path } => {
899                self.place = Place::Disk {
900                    file: None,
901                    path: None,
902                };
903                self.release();
904                drop(file);
905                let mut freed = true;
906                if let Some(p) = path {
907                    if std::fs::remove_file(&p).is_err() {
908                        freed = false;
909                        if let Some(s) = &self.shared {
910                            s.cleanup_failed();
911                        }
912                    }
913                }
914                // Only space that is free again goes back to the budget: a
915                // file that could not be removed still uses its bytes.
916                if freed {
917                    self.release_budget();
918                }
919            }
920            Place::Gone => {}
921        }
922    }
923}
924
925/// A reader over a [`StoredFile`]; `Read + Seek` with its own position.
926#[derive(Debug)]
927pub struct StoredReader<'f> {
928    file: &'f StoredFile,
929    pos: u64,
930}
931
932impl Read for StoredReader<'_> {
933    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
934        let n = self.file.read_at(buf, self.pos)?;
935        self.pos = self.pos.saturating_add(n as u64);
936        Ok(n)
937    }
938}
939
940impl Seek for StoredReader<'_> {
941    fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
942        let target = match to {
943            SeekFrom::Start(n) => Some(n),
944            SeekFrom::End(d) => self.file.len.checked_add_signed(d),
945            SeekFrom::Current(d) => self.pos.checked_add_signed(d),
946        };
947        match target {
948            Some(t) => {
949                self.pos = t;
950                Ok(t)
951            }
952            None => Err(io::Error::from(io::ErrorKind::InvalidInput)),
953        }
954    }
955}
956
957/// A position in an upload's text arena.
958#[derive(Clone, Copy)]
959struct Span {
960    name: (usize, usize),
961    value: (usize, usize),
962}
963
964/// The result of a completed multipart parse with storage: text fields,
965/// owned and wiped on drop, and files.
966///
967/// # Examples
968///
969/// ```
970/// let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHoliday\r\n--X\r\nContent-Disposition: form-data; name=photo; filename=a.jpg\r\n\r\nJPEG\r\n--X--";
971/// let mut upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
972/// assert_eq!(upload.field("title"), Some("Holiday"));
973/// let photo = upload.take_file("photo").unwrap();
974/// assert_eq!(photo.bytes(), Some(&b"JPEG"[..]));
975/// assert_eq!(upload.file_count(), 0);
976/// # Ok::<(), form_net::FormError>(())
977/// ```
978#[must_use]
979#[derive(Default)]
980pub struct Upload {
981    text: Vec<u8>,
982    spans: Vec<Span>,
983    files: Vec<StoredFile>,
984    token: Option<String>,
985}
986
987impl fmt::Debug for Upload {
988    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
989        f.debug_struct("Upload")
990            .field("fields", &self.spans.len())
991            .field("text_len", &self.text_len())
992            .field("files", &self.files)
993            .finish_non_exhaustive()
994    }
995}
996
997impl Drop for Upload {
998    fn drop(&mut self) {
999        crate::wipe::wipe_vec(&mut self.text);
1000        if let Some(t) = self.token.as_mut() {
1001            crate::wipe::wipe_string(t);
1002        }
1003    }
1004}
1005
1006impl Upload {
1007    fn slice(&self, (start, len): (usize, usize)) -> &str {
1008        let b = self
1009            .text
1010            .get(start..start.saturating_add(len))
1011            .unwrap_or(&[]);
1012        // Every value was checked as text when it was stored.
1013        core::str::from_utf8(b).unwrap_or("")
1014    }
1015
1016    fn push_field(&mut self, name: &str, value: &[u8], cap: usize) {
1017        let n0 = self.text.len();
1018        crate::wipe::append_wiping(&mut self.text, name.as_bytes(), 1024, cap);
1019        let v0 = self.text.len();
1020        crate::wipe::append_wiping(&mut self.text, value, 1024, cap);
1021        self.spans.push(Span {
1022            name: (n0, name.len()),
1023            value: (v0, value.len()),
1024        });
1025    }
1026
1027    /// The first text field named `name`.
1028    #[must_use]
1029    pub fn field(&self, name: &str) -> Option<&str> {
1030        self.spans
1031            .iter()
1032            .find(|s| self.slice(s.name) == name)
1033            .map(|s| self.slice(s.value))
1034    }
1035
1036    /// The text field named `name`, refusing a name given twice.
1037    ///
1038    /// # Errors
1039    ///
1040    /// `Extract { Duplicate }`.
1041    pub fn field_one(&self, name: &str) -> Result<Option<&str>, FormError> {
1042        let mut found = None;
1043        for s in self.spans.iter().filter(|s| self.slice(s.name) == name) {
1044            if found.is_some() {
1045                return Err(FormError::Extract {
1046                    reason: ExtractError::Duplicate,
1047                    field: None,
1048                });
1049            }
1050            found = Some(self.slice(s.value));
1051        }
1052        Ok(found)
1053    }
1054
1055    /// Every text field named `name`, in body order.
1056    pub fn fields<'u>(&'u self, name: &'u str) -> impl Iterator<Item = &'u str> + 'u {
1057        self.spans
1058            .iter()
1059            .filter(move |s| self.slice(s.name) == name)
1060            .map(move |s| self.slice(s.value))
1061    }
1062
1063    /// Every text field `(name, value)`, in body order.
1064    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
1065        self.spans
1066            .iter()
1067            .map(|s| (self.slice(s.name), self.slice(s.value)))
1068    }
1069
1070    /// The first file named `name`.
1071    #[must_use]
1072    pub fn file(&self, name: &str) -> Option<&StoredFile> {
1073        self.files.iter().find(|f| f.name() == name)
1074    }
1075
1076    /// Every file named `name`, in body order.
1077    pub fn files<'u>(&'u self, name: &'u str) -> impl Iterator<Item = &'u StoredFile> + 'u {
1078        self.files.iter().filter(move |f| f.name() == name)
1079    }
1080
1081    /// Removes and returns the first file named `name`.
1082    pub fn take_file(&mut self, name: &str) -> Option<StoredFile> {
1083        let i = self.files.iter().position(|f| f.name() == name)?;
1084        Some(self.files.remove(i))
1085    }
1086
1087    /// Every file, in body order.
1088    #[must_use]
1089    pub fn into_files(mut self) -> Vec<StoredFile> {
1090        core::mem::take(&mut self.files)
1091    }
1092
1093    /// One owned type for both encodings (D-090, D-098): the fields of a
1094    /// urlencoded form as an upload with no files, so a server keeps one
1095    /// `Upload` per request whatever the `Content-Type`. The policy's token
1096    /// field is captured as a multipart parse would capture it, so
1097    /// [`captured_token`](Self::captured_token) means the same for both.
1098    /// The form is wiped as it drops.
1099    ///
1100    /// # Errors
1101    ///
1102    /// `Policy { DuplicateToken }` when the token field appears twice.
1103    ///
1104    /// # Examples
1105    ///
1106    /// ```
1107    /// use form_net::{Policy, TokenField, Upload};
1108    ///
1109    /// let policy = Policy::DEFAULT.with_token_field(TokenField::new("csrf"));
1110    /// let form = form_net::parse_urlencoded(b"email=a%40b.example&csrf=t0k&tag=x&tag=y")?;
1111    /// let upload = Upload::from_form(form, &policy)?;
1112    /// assert_eq!(upload.field("email"), Some("a@b.example"));
1113    /// assert_eq!(upload.captured_token(), Some("t0k"));
1114    /// assert_eq!((upload.fields("tag").count(), upload.file_count()), (2, 0));
1115    /// # Ok::<(), form_net::FormError>(())
1116    /// ```
1117    pub fn from_form(form: crate::Form, policy: &Policy) -> Result<Upload, FormError> {
1118        let mut upload = Upload::default();
1119        let token = policy.token_field.map(|t| t.name());
1120        for (name, value) in form.iter() {
1121            if Some(name) == token {
1122                if upload.token.is_some() {
1123                    return Err(FormError::policy(
1124                        crate::error::PolicyRefusal::DuplicateToken,
1125                        Location::NONE,
1126                    ));
1127                }
1128                upload.token = Some(String::from(value));
1129            }
1130            upload.push_field(name, value.as_bytes(), usize::MAX);
1131        }
1132        Ok(upload)
1133    }
1134
1135    /// The stored files, for extraction that moves some of them out.
1136    pub(crate) fn files_mut(&mut self) -> &mut Vec<StoredFile> {
1137        &mut self.files
1138    }
1139
1140    pub(crate) fn into_parts(mut self) -> (Upload, Vec<StoredFile>) {
1141        let files = core::mem::take(&mut self.files);
1142        (self, files)
1143    }
1144
1145    /// The anti-forgery token field's value, when a token field was set.
1146    #[must_use]
1147    pub fn captured_token(&self) -> Option<&str> {
1148        self.token.as_deref()
1149    }
1150
1151    /// The bytes of all text values together.
1152    #[must_use]
1153    pub fn text_len(&self) -> u64 {
1154        self.spans.iter().map(|s| s.value.1 as u64).sum()
1155    }
1156
1157    /// The number of files.
1158    #[must_use]
1159    pub fn file_count(&self) -> usize {
1160        self.files.len()
1161    }
1162
1163    /// Reads a whole multipart body from `reader` in 64 KiB chunks, with
1164    /// explicit limits, policy, store, and inspector.
1165    ///
1166    /// # Errors
1167    ///
1168    /// Every multipart error; `Refused`; `Io { kind }`; `Store { .. }`;
1169    /// `Limit { Disk }`.
1170    ///
1171    /// # Examples
1172    ///
1173    /// ```
1174    /// use form_net::{Limits, NoInspector, Policy, Store, TokenField, Upload};
1175    ///
1176    /// let store = Store::temp()?;
1177    /// let policy = Policy::DEFAULT.with_token_field(TokenField::new("csrf_token"));
1178    /// let body = b"--X\r\nContent-Disposition: form-data; name=csrf_token\r\n\r\ntok\r\n--X--";
1179    /// let upload = Upload::parse_with(b"multipart/form-data; boundary=X", &body[..], &Limits::DEFAULT, &policy, &store, &mut NoInspector)?;
1180    /// assert_eq!(upload.captured_token(), Some("tok"));
1181    /// # Ok::<(), form_net::FormError>(())
1182    /// ```
1183    pub fn parse_with<R: Read, I: Inspector>(
1184        content_type: &[u8],
1185        mut reader: R,
1186        limits: &Limits,
1187        policy: &Policy,
1188        store: &Store,
1189        inspector: &mut I,
1190    ) -> Result<Upload, FormError> {
1191        let mut session = UploadSession::new(content_type, limits, policy, store)?;
1192        let mut buf = crate::wipe::ReadBuf::new(64 * 1024);
1193        loop {
1194            let n = match reader.read(&mut buf.buf) {
1195                Ok(n) => n,
1196                Err(e) if e.kind() == io::ErrorKind::Interrupted => {
1197                    buf.failed();
1198                    continue;
1199                }
1200                Err(e) => {
1201                    buf.failed();
1202                    let err = temp::io(&e);
1203                    inspector.failed(&err);
1204                    return Err(err);
1205                }
1206            };
1207            if n == 0 {
1208                break;
1209            }
1210            buf.filled(n);
1211            session.push(buf.buf.get(..n).unwrap_or(&[]), inspector)?;
1212        }
1213        drop(buf);
1214        session.finish(inspector)
1215    }
1216}
1217
1218/// The push-style form for event-driven servers: hand each body chunk to
1219/// [`push`](Self::push) as reads complete, then [`finish`](Self::finish).
1220///
1221/// Dropping a session without `finish` deletes every file it stored.
1222/// [`captured_token`](Self::captured_token) is available as soon as the
1223/// token field ends, so the token can be verified while files are still
1224/// arriving.
1225///
1226/// # Examples
1227///
1228/// ```
1229/// use form_net::{Limits, NoInspector, Policy, Store, UploadSession};
1230///
1231/// let store = Store::temp()?;
1232/// let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X--";
1233/// let mut session = UploadSession::new(b"multipart/form-data; boundary=X", &Limits::DEFAULT, &Policy::DEFAULT, &store)?;
1234/// for chunk in body.chunks(5) {
1235///     session.push(chunk, &mut NoInspector)?;
1236/// }
1237/// let upload = session.finish(&mut NoInspector)?;
1238/// assert_eq!(upload.field("a"), Some("1"));
1239/// # Ok::<(), form_net::FormError>(())
1240/// ```
1241pub struct UploadSession {
1242    driver: Driver<Vec<u8>, StoreSink>,
1243    upload: Upload,
1244    text_cap: usize,
1245    ended: bool,
1246    failed: bool,
1247}
1248
1249impl fmt::Debug for UploadSession {
1250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1251        f.debug_struct("UploadSession")
1252            .field("bytes_seen", &self.bytes_seen())
1253            .field("upload", &self.upload)
1254            .finish_non_exhaustive()
1255    }
1256}
1257
1258impl UploadSession {
1259    /// A session for one request body.
1260    ///
1261    /// # Errors
1262    ///
1263    /// `UnsupportedMediaType` and the boundary errors of
1264    /// [`FormKind::detect`](crate::FormKind::detect); `InvalidLimits`.
1265    pub fn new(
1266        content_type: &[u8],
1267        limits: &Limits,
1268        policy: &Policy,
1269        store: &Store,
1270    ) -> Result<Self, FormError> {
1271        let boundary = crate::Boundary::parse(content_type)?;
1272        let parser = MultipartParser::new(boundary, limits, policy)?;
1273        Ok(UploadSession {
1274            driver: Driver::new(parser, store.sink(limits)),
1275            upload: Upload::default(),
1276            text_cap: usize::try_from(limits.text_and_names()).unwrap_or(usize::MAX),
1277            ended: false,
1278            failed: false,
1279        })
1280    }
1281
1282    /// Feeds one chunk; consumes all of it or fails.
1283    ///
1284    /// # Errors
1285    ///
1286    /// Every multipart error, `Refused`, `Io`, `Store`, `Limit { Disk }`;
1287    /// `Poisoned` after an error.
1288    pub fn push<I: Inspector>(&mut self, chunk: &[u8], inspector: &mut I) -> Result<(), FormError> {
1289        self.feed(chunk, false, inspector)
1290    }
1291
1292    /// Ends the body and returns the upload.
1293    ///
1294    /// # Errors
1295    ///
1296    /// `Syntax { Truncated }` if the close delimiter was not seen; as
1297    /// [`push`](Self::push).
1298    pub fn finish<I: Inspector>(mut self, inspector: &mut I) -> Result<Upload, FormError> {
1299        if self.failed {
1300            return Err(FormError::Poisoned);
1301        }
1302        if !self.ended {
1303            self.feed(&[], true, inspector)?;
1304        }
1305        let mut upload = core::mem::take(&mut self.upload);
1306        upload.token = self.driver.parser().captured_token().map(String::from);
1307        Ok(upload)
1308    }
1309
1310    /// The token field's value, once its part has ended.
1311    #[must_use]
1312    pub fn captured_token(&self) -> Option<&str> {
1313        self.driver.parser().captured_token()
1314    }
1315
1316    /// Body bytes consumed so far.
1317    #[must_use]
1318    pub fn bytes_seen(&self) -> u64 {
1319        self.driver.parser().bytes_seen()
1320    }
1321
1322    fn feed<I: Inspector>(
1323        &mut self,
1324        chunk: &[u8],
1325        eof: bool,
1326        inspector: &mut I,
1327    ) -> Result<(), FormError> {
1328        if self.failed {
1329            return Err(FormError::Poisoned);
1330        }
1331        let r = self.feed_inner(chunk, eof, inspector);
1332        if let Err(e) = &r {
1333            self.failed = true;
1334            // The driver reports its own failures; a field that fails its
1335            // text check fails here, after the driver returned.
1336            if !self.driver.has_failed() {
1337                inspector.failed(e);
1338            }
1339            // Nothing of a refused body survives.
1340            self.upload.files.clear();
1341        }
1342        r
1343    }
1344
1345    fn feed_inner<I: Inspector>(
1346        &mut self,
1347        chunk: &[u8],
1348        eof: bool,
1349        inspector: &mut I,
1350    ) -> Result<(), FormError> {
1351        let mut pos = 0usize;
1352        loop {
1353            let rest = chunk.get(pos..).unwrap_or(&[]);
1354            let (n, progress) = self.driver.push(rest, eof, inspector)?;
1355            pos = pos.saturating_add(n);
1356            match progress {
1357                Progress::Field { name, value } => {
1358                    // Checked as text by the driver before the inspector
1359                    // saw it, as the buffered parser checks it.
1360                    let cap = self.text_cap;
1361                    self.upload.push_field(name, value, cap);
1362                }
1363                Progress::File(f) => self.upload.files.push(f),
1364                Progress::End => {
1365                    self.ended = true;
1366                    if pos >= chunk.len() {
1367                        return Ok(());
1368                    }
1369                }
1370                _ => {
1371                    if pos >= chunk.len() {
1372                        return Ok(());
1373                    }
1374                }
1375            }
1376        }
1377    }
1378}
1379
1380/// The blocking convenience: reads `body` to the end in 64 KiB chunks with
1381/// the default limits and policy, keeps files up to 64 KiB in memory, and
1382/// spills larger ones to anonymous temporary files in a private directory
1383/// that is created only when a file first spills and removed when the last
1384/// file from it is dropped. There is no process-wide state.
1385///
1386/// # Errors
1387///
1388/// Every multipart error; `Io { kind }`; `Limit { Disk }`; `Random`.
1389///
1390/// # Examples
1391///
1392/// ```
1393/// let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHi\r\n--X--";
1394/// let upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
1395/// assert_eq!(upload.field("title"), Some("Hi"));
1396/// # Ok::<(), form_net::FormError>(())
1397/// ```
1398pub fn read_multipart<R: Read>(content_type: &[u8], body: R) -> Result<Upload, FormError> {
1399    let store = Store::temp_lazy();
1400    Upload::parse_with(
1401        content_type,
1402        body,
1403        &Limits::DEFAULT,
1404        &Policy::DEFAULT,
1405        &store,
1406        &mut crate::NoInspector,
1407    )
1408}
1409
1410impl FileName<'_> {
1411    /// One portable file name from the client's: the HTML-unescaped base
1412    /// name with every default-ignorable character (zero width spaces and
1413    /// joiners, soft hyphens, variation selectors, tags, bidirectional
1414    /// controls) replaced by `_` (D-084), then passed through
1415    /// `path_lib::sanitize`, which replaces separators, control characters,
1416    /// Windows-reserved characters and device names, and trims trailing dots
1417    /// and spaces. With the `unicode` feature the name is put in Unicode NFC
1418    /// first, so an NFD name from a macOS client sanitizes to the same bytes. It is still a name the client chose: never let it
1419    /// overwrite anything (`StoredFile::persist` refuses to).
1420    ///
1421    /// # Errors
1422    ///
1423    /// `Encoding { InvalidUtf8 }`.
1424    ///
1425    /// # Examples
1426    ///
1427    /// ```
1428    /// let body = b"--X\r\nContent-Disposition: form-data; name=f; filename=\"C:\\x\\Q3 report: final?.pdf\"\r\n\r\n\r\n--X--";
1429    /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
1430    /// let name = mp.file("f").unwrap().filename().unwrap();
1431    /// assert_eq!(name.sanitized()?, "Q3 report_ final_.pdf");
1432    /// # Ok::<(), form_net::FormError>(())
1433    /// ```
1434    #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
1435    pub fn sanitized(&self) -> Result<String, FormError> {
1436        let base = FileName::new(self.basename(), self.source());
1437        let text = base.html_unescaped()?;
1438        #[cfg(feature = "unicode")]
1439        let text = unicode_lang::normalize(text.as_ref(), unicode_lang::Form::Nfc);
1440        let text: &str = text.as_ref();
1441        if text.is_ascii() {
1442            return Ok(path_lib::sanitize(text));
1443        }
1444        let visible: String = text
1445            .chars()
1446            .map(|c| if is_default_ignorable(c) { '_' } else { c })
1447            .collect();
1448        Ok(path_lib::sanitize(visible))
1449    }
1450}
1451
1452/// Unicode's `Default_Ignorable_Code_Point` property (DerivedCoreProperties):
1453/// characters that render as nothing, so a name can hide them.
1454const fn is_default_ignorable(c: char) -> bool {
1455    matches!(
1456        c,
1457        '\u{00AD}'
1458            | '\u{034F}'
1459            | '\u{061C}'
1460            | '\u{115F}'..='\u{1160}'
1461            | '\u{17B4}'..='\u{17B5}'
1462            | '\u{180B}'..='\u{180F}'
1463            | '\u{200B}'..='\u{200F}'
1464            | '\u{202A}'..='\u{202E}'
1465            | '\u{2060}'..='\u{206F}'
1466            | '\u{3164}'
1467            | '\u{FE00}'..='\u{FE0F}'
1468            | '\u{FEFF}'
1469            | '\u{FFA0}'
1470            | '\u{FFF0}'..='\u{FFF8}'
1471            | '\u{1BCA0}'..='\u{1BCA3}'
1472            | '\u{1D173}'..='\u{1D17A}'
1473            | '\u{E0000}'..='\u{E0FFF}'
1474    )
1475}
1476
1477impl crate::FromField for StoredFile {
1478    type Slot = Option<StoredFile>;
1479
1480    fn accept_file(
1481        slot: &mut Self::Slot,
1482        file: crate::FileValue<'_>,
1483    ) -> Result<(), crate::ValueError> {
1484        let stored = match file {
1485            crate::FileValue::Stored(f) => f,
1486            crate::FileValue::Buffered(part) => StoredFile::detached(
1487                part.name(),
1488                part.filename(),
1489                part.content_type().map(|c| c.raw()),
1490                part.bytes(),
1491            ),
1492        };
1493        if stored.is_empty() && stored.filename().is_some_and(|n| n.is_empty()) {
1494            // A file input left empty: as if it were absent.
1495            return Ok(());
1496        }
1497        if slot.is_some() {
1498            return Err(crate::ValueError::Duplicate);
1499        }
1500        *slot = Some(stored);
1501        Ok(())
1502    }
1503
1504    fn finish(slot: Self::Slot) -> Result<Self, crate::ValueError> {
1505        slot.ok_or(crate::ValueError::Missing)
1506    }
1507}