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                let mut freed = true;
580                if let Some(p) = path {
581                    if std::fs::remove_file(&p).is_err() {
582                        freed = false;
583                        self.shared.cleanup_failed();
584                    }
585                }
586                // As `StoredFile`'s drop: only space that is free again goes
587                // back to the budget (D-101, D-143).
588                if freed {
589                    if let Some(budget) = &self.budget {
590                        budget.release(cur.on_disk);
591                    }
592                }
593            }
594        }
595    }
596}
597
598impl Drop for StoreSink {
599    fn drop(&mut self) {
600        self.abort();
601    }
602}
603
604enum Place {
605    Memory(Vec<u8>),
606    Disk {
607        file: Option<File>,
608        path: Option<PathBuf>,
609    },
610    Gone,
611}
612
613/// One received file, in memory or in a temporary file.
614///
615/// Dropping it without [`persist`](Self::persist) deletes it (closing the
616/// handle first) and wipes memory-held bytes. `Debug` never prints the file
617/// name or the data.
618///
619/// # Examples
620///
621/// ```
622/// use std::io::Read;
623///
624/// let body = b"--X\r\nContent-Disposition: form-data; name=doc; filename=a.txt\r\n\r\nhello\r\n--X--";
625/// let upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
626/// let file = upload.file("doc").unwrap();
627/// let mut text = String::new();
628/// file.reader()?.read_to_string(&mut text).unwrap();
629/// assert_eq!((text.as_str(), file.len(), file.is_in_memory()), ("hello", 5, true));
630/// # Ok::<(), form_net::FormError>(())
631/// ```
632#[must_use]
633pub struct StoredFile {
634    name: String,
635    filename: Option<(Vec<u8>, FileNameSource)>,
636    content_type: Option<Vec<u8>>,
637    len: u64,
638    place: Place,
639    shared: Option<Arc<Shared>>,
640    /// The budget this file's disk bytes were reserved from.
641    budget: Option<Arc<dyn SpillBudget>>,
642    reserved: u64,
643}
644
645impl fmt::Debug for StoredFile {
646    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647        f.debug_struct("StoredFile")
648            .field("name", &self.name)
649            .field("len", &self.len)
650            .field("in_memory", &self.is_in_memory())
651            .finish_non_exhaustive()
652    }
653}
654
655impl StoredFile {
656    /// A memory-held file not tied to a store (from a buffered part).
657    pub(crate) fn detached(
658        name: &str,
659        filename: Option<FileName<'_>>,
660        content_type: Option<&[u8]>,
661        bytes: &[u8],
662    ) -> StoredFile {
663        StoredFile {
664            name: String::from(name),
665            filename: filename.map(|f| (f.raw().to_vec(), f.source())),
666            content_type: content_type.map(<[u8]>::to_vec),
667            len: bytes.len() as u64,
668            place: Place::Memory(bytes.to_vec()),
669            shared: None,
670            budget: None,
671            reserved: 0,
672        }
673    }
674
675    /// The form field name.
676    #[must_use]
677    pub fn name(&self) -> &str {
678        &self.name
679    }
680
681    /// The client's file name: a hint, never a path.
682    #[must_use]
683    pub fn filename(&self) -> Option<FileName<'_>> {
684        self.filename
685            .as_ref()
686            .map(|(raw, source)| FileName::new(raw, *source))
687    }
688
689    /// The declared content type: a hint, never trusted.
690    #[must_use]
691    pub fn content_type(&self) -> Option<ContentType<'_>> {
692        self.content_type.as_deref().and_then(ContentType::parse)
693    }
694
695    /// The length in bytes.
696    #[must_use]
697    pub const fn len(&self) -> u64 {
698        self.len
699    }
700
701    /// Whether the file is empty.
702    #[must_use]
703    pub const fn is_empty(&self) -> bool {
704        self.len == 0
705    }
706
707    /// Whether the file is held in memory.
708    #[must_use]
709    pub const fn is_in_memory(&self) -> bool {
710        matches!(self.place, Place::Memory(_))
711    }
712
713    /// The bytes, while the file is in memory.
714    #[must_use]
715    pub fn bytes(&self) -> Option<&[u8]> {
716        match &self.place {
717            Place::Memory(b) => Some(b),
718            _ => None,
719        }
720    }
721
722    /// A reader over the file, in memory or on disk. It reads with
723    /// positional reads, so several readers can exist at once.
724    ///
725    /// # Errors
726    ///
727    /// `Io { kind }` when the file is gone.
728    pub fn reader(&self) -> Result<StoredReader<'_>, FormError> {
729        match &self.place {
730            Place::Gone => Err(FormError::Io {
731                kind: io::ErrorKind::NotFound,
732            }),
733            _ => Ok(StoredReader { file: self, pos: 0 }),
734        }
735    }
736
737    /// The whole file in a new vector, refusing a file longer than `max`
738    /// before allocating.
739    ///
740    /// # Errors
741    ///
742    /// `Limit { FileSize }`; `Io { kind }`.
743    pub fn read_to_vec(&self, max: u64) -> Result<Vec<u8>, FormError> {
744        if self.len > max {
745            return Err(FormError::limit(Limit::FileSize, Location::NONE));
746        }
747        let mut out = Vec::with_capacity(usize::try_from(self.len).unwrap_or(0));
748        self.reader()?
749            .read_to_end(&mut out)
750            .map_err(|e| temp::io(&e))?;
751        Ok(out)
752    }
753
754    /// The temporary file's path, in `SpillMode::Named` only.
755    #[must_use]
756    pub fn path(&self) -> Option<&Path> {
757        match &self.place {
758            Place::Disk { path, .. } => path.as_deref(),
759            _ => None,
760        }
761    }
762
763    fn read_at(&self, buf: &mut [u8], pos: u64) -> io::Result<usize> {
764        match &self.place {
765            Place::Memory(b) => {
766                let start = usize::try_from(pos).unwrap_or(usize::MAX).min(b.len());
767                let src = b.get(start..).unwrap_or(&[]);
768                let n = src.len().min(buf.len());
769                if let (Some(dst), Some(src)) = (buf.get_mut(..n), src.get(..n)) {
770                    dst.copy_from_slice(src);
771                }
772                Ok(n)
773            }
774            Place::Disk { file: Some(f), .. } => temp::read_at(f, buf, pos),
775            _ => Err(io::Error::from(io::ErrorKind::NotFound)),
776        }
777    }
778
779    /// Moves the finished upload to `dest`, which the caller built. It never
780    /// overwrites: a named file is hard-linked when `dest` is on the same file
781    /// system and copied otherwise; anonymous and memory files are copied
782    /// into `dest` opened with `create_new`. Whether to fsync `dest` and its
783    /// directory is the caller's choice. On error the temporary file is
784    /// deleted like any dropped file.
785    ///
786    /// # Errors
787    ///
788    /// `Store { DestinationExists }`; `Io { kind }`.
789    pub fn persist(mut self, dest: &Path) -> Result<(), FormError> {
790        let exists = |e: io::Error| {
791            if e.kind() == io::ErrorKind::AlreadyExists {
792                FormError::Store {
793                    reason: StoreError::DestinationExists,
794                }
795            } else {
796                temp::io(&e)
797            }
798        };
799        if let Place::Disk {
800            file,
801            path: Some(p),
802        } = &mut self.place
803        {
804            // Close before linking and removing: Windows needs it.
805            drop(file.take());
806            match std::fs::hard_link(&*p, dest) {
807                Ok(()) => {
808                    let p = p.clone();
809                    self.place = Place::Gone;
810                    self.release();
811                    self.release_budget();
812                    if std::fs::remove_file(&p).is_err() {
813                        if let Some(s) = &self.shared {
814                            s.cleanup_failed();
815                        }
816                    }
817                    return Ok(());
818                }
819                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => return Err(exists(e)),
820                Err(_) => {
821                    // Another file system: copy below, from the name.
822                    let f = File::open(&*p).map_err(|e| temp::io(&e))?;
823                    *file = Some(f);
824                }
825            }
826        }
827        let mut out = std::fs::OpenOptions::new()
828            .write(true)
829            .create_new(true)
830            .open(dest)
831            .map_err(exists)?;
832        let mut buf = [0u8; 64 * 1024];
833        let mut hi = 0usize;
834        let mut pos = 0u64;
835        let copied = loop {
836            let n = match self.read_at(&mut buf, pos) {
837                Ok(0) => break Ok(()),
838                Ok(n) => n,
839                Err(e) => break Err(temp::io(&e)),
840            };
841            hi = hi.max(n);
842            if let Err(e) = out.write_all(buf.get(..n).unwrap_or(&[])) {
843                break Err(temp::io(&e));
844            }
845            pos = pos.saturating_add(n as u64);
846        };
847        crate::wipe::wipe(buf.get_mut(..hi).unwrap_or(&mut []));
848        if copied.is_err() {
849            // Never leave a partial copy under the caller's name.
850            drop(out);
851            let _ = std::fs::remove_file(dest);
852        }
853        copied
854    }
855
856    /// Gives the file's disk bytes back to the shared budget, once.
857    fn release_budget(&mut self) {
858        if let Some(budget) = self.budget.take() {
859            budget.release(self.reserved);
860        }
861    }
862
863    /// Takes the file off the store's counters.
864    fn release(&mut self) {
865        if let Some(s) = &self.shared {
866            match &self.place {
867                Place::Memory(_) => {
868                    sub(&s.counters.files_in_memory, 1);
869                    sub(&s.counters.bytes_in_memory, self.len);
870                }
871                _ => {
872                    sub(&s.counters.files_on_disk, 1);
873                    sub(&s.counters.bytes_on_disk, self.len);
874                }
875            }
876        }
877    }
878}
879
880/// Wipes a file's name, file name, and declared content type.
881fn wipe_meta(
882    name: &mut String,
883    filename: &mut Option<(Vec<u8>, FileNameSource)>,
884    content_type: &mut Option<Vec<u8>>,
885) {
886    crate::wipe::wipe_string(name);
887    if let Some((mut f, _)) = filename.take() {
888        crate::wipe::wipe_vec(&mut f);
889    }
890    if let Some(mut c) = content_type.take() {
891        crate::wipe::wipe_vec(&mut c);
892    }
893}
894
895impl Drop for StoredFile {
896    fn drop(&mut self) {
897        wipe_meta(&mut self.name, &mut self.filename, &mut self.content_type);
898        match core::mem::replace(&mut self.place, Place::Gone) {
899            Place::Memory(mut b) => {
900                self.place = Place::Memory(Vec::new());
901                self.release();
902                crate::wipe::wipe_vec(&mut b);
903            }
904            Place::Disk { file, path } => {
905                self.place = Place::Disk {
906                    file: None,
907                    path: None,
908                };
909                self.release();
910                drop(file);
911                let mut freed = true;
912                if let Some(p) = path {
913                    if std::fs::remove_file(&p).is_err() {
914                        freed = false;
915                        if let Some(s) = &self.shared {
916                            s.cleanup_failed();
917                        }
918                    }
919                }
920                // Only space that is free again goes back to the budget: a
921                // file that could not be removed still uses its bytes.
922                if freed {
923                    self.release_budget();
924                }
925            }
926            Place::Gone => {}
927        }
928    }
929}
930
931/// A reader over a [`StoredFile`]; `Read + Seek` with its own position.
932#[derive(Debug)]
933pub struct StoredReader<'f> {
934    file: &'f StoredFile,
935    pos: u64,
936}
937
938impl Read for StoredReader<'_> {
939    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
940        let n = self.file.read_at(buf, self.pos)?;
941        self.pos = self.pos.saturating_add(n as u64);
942        Ok(n)
943    }
944}
945
946impl Seek for StoredReader<'_> {
947    fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
948        let target = match to {
949            SeekFrom::Start(n) => Some(n),
950            SeekFrom::End(d) => self.file.len.checked_add_signed(d),
951            SeekFrom::Current(d) => self.pos.checked_add_signed(d),
952        };
953        match target {
954            Some(t) => {
955                self.pos = t;
956                Ok(t)
957            }
958            None => Err(io::Error::from(io::ErrorKind::InvalidInput)),
959        }
960    }
961}
962
963/// A position in an upload's text arena.
964#[derive(Clone, Copy)]
965struct Span {
966    name: (usize, usize),
967    value: (usize, usize),
968}
969
970/// The result of a completed multipart parse with storage: text fields,
971/// owned and wiped on drop, and files.
972///
973/// # Examples
974///
975/// ```
976/// 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--";
977/// let mut upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
978/// assert_eq!(upload.field("title"), Some("Holiday"));
979/// let photo = upload.take_file("photo").unwrap();
980/// assert_eq!(photo.bytes(), Some(&b"JPEG"[..]));
981/// assert_eq!(upload.file_count(), 0);
982/// # Ok::<(), form_net::FormError>(())
983/// ```
984#[must_use]
985#[derive(Default)]
986pub struct Upload {
987    text: Vec<u8>,
988    spans: Vec<Span>,
989    files: Vec<StoredFile>,
990    token: Option<String>,
991}
992
993impl fmt::Debug for Upload {
994    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
995        f.debug_struct("Upload")
996            .field("fields", &self.spans.len())
997            .field("text_len", &self.text_len())
998            .field("files", &self.files)
999            .finish_non_exhaustive()
1000    }
1001}
1002
1003impl Drop for Upload {
1004    fn drop(&mut self) {
1005        crate::wipe::wipe_vec(&mut self.text);
1006        if let Some(t) = self.token.as_mut() {
1007            crate::wipe::wipe_string(t);
1008        }
1009    }
1010}
1011
1012impl Upload {
1013    fn slice(&self, (start, len): (usize, usize)) -> &str {
1014        let b = self
1015            .text
1016            .get(start..start.saturating_add(len))
1017            .unwrap_or(&[]);
1018        // Every value was checked as text when it was stored.
1019        core::str::from_utf8(b).unwrap_or("")
1020    }
1021
1022    fn push_field(&mut self, name: &str, value: &[u8], cap: usize) {
1023        let n0 = self.text.len();
1024        crate::wipe::append_wiping(&mut self.text, name.as_bytes(), 1024, cap);
1025        let v0 = self.text.len();
1026        crate::wipe::append_wiping(&mut self.text, value, 1024, cap);
1027        self.spans.push(Span {
1028            name: (n0, name.len()),
1029            value: (v0, value.len()),
1030        });
1031    }
1032
1033    /// The first text field named `name`.
1034    #[must_use]
1035    pub fn field(&self, name: &str) -> Option<&str> {
1036        self.spans
1037            .iter()
1038            .find(|s| self.slice(s.name) == name)
1039            .map(|s| self.slice(s.value))
1040    }
1041
1042    /// The text field named `name`, refusing a name given twice.
1043    ///
1044    /// # Errors
1045    ///
1046    /// `Extract { Duplicate }`.
1047    pub fn field_one(&self, name: &str) -> Result<Option<&str>, FormError> {
1048        let mut found = None;
1049        for s in self.spans.iter().filter(|s| self.slice(s.name) == name) {
1050            if found.is_some() {
1051                return Err(FormError::Extract {
1052                    reason: ExtractError::Duplicate,
1053                    field: None,
1054                });
1055            }
1056            found = Some(self.slice(s.value));
1057        }
1058        Ok(found)
1059    }
1060
1061    /// Every text field named `name`, in body order.
1062    pub fn fields<'u>(&'u self, name: &'u str) -> impl Iterator<Item = &'u str> + 'u {
1063        self.spans
1064            .iter()
1065            .filter(move |s| self.slice(s.name) == name)
1066            .map(move |s| self.slice(s.value))
1067    }
1068
1069    /// Every text field `(name, value)`, in body order.
1070    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
1071        self.spans
1072            .iter()
1073            .map(|s| (self.slice(s.name), self.slice(s.value)))
1074    }
1075
1076    /// The first file named `name`.
1077    #[must_use]
1078    pub fn file(&self, name: &str) -> Option<&StoredFile> {
1079        self.files.iter().find(|f| f.name() == name)
1080    }
1081
1082    /// Every file named `name`, in body order.
1083    pub fn files<'u>(&'u self, name: &'u str) -> impl Iterator<Item = &'u StoredFile> + 'u {
1084        self.files.iter().filter(move |f| f.name() == name)
1085    }
1086
1087    /// Removes and returns the first file named `name`.
1088    pub fn take_file(&mut self, name: &str) -> Option<StoredFile> {
1089        let i = self.files.iter().position(|f| f.name() == name)?;
1090        Some(self.files.remove(i))
1091    }
1092
1093    /// Every file, in body order.
1094    #[must_use]
1095    pub fn into_files(mut self) -> Vec<StoredFile> {
1096        core::mem::take(&mut self.files)
1097    }
1098
1099    /// One owned type for both encodings (D-090, D-098): the fields of a
1100    /// urlencoded form as an upload with no files, so a server keeps one
1101    /// `Upload` per request whatever the `Content-Type`. The policy's token
1102    /// field is captured as a multipart parse would capture it, so
1103    /// [`captured_token`](Self::captured_token) means the same for both.
1104    /// The form is wiped as it drops.
1105    ///
1106    /// # Errors
1107    ///
1108    /// `Policy { DuplicateToken }` when the token field appears twice.
1109    ///
1110    /// # Examples
1111    ///
1112    /// ```
1113    /// use form_net::{Policy, TokenField, Upload};
1114    ///
1115    /// let policy = Policy::DEFAULT.with_token_field(TokenField::new("csrf"));
1116    /// let form = form_net::parse_urlencoded(b"email=a%40b.example&csrf=t0k&tag=x&tag=y")?;
1117    /// let upload = Upload::from_form(form, &policy)?;
1118    /// assert_eq!(upload.field("email"), Some("a@b.example"));
1119    /// assert_eq!(upload.captured_token(), Some("t0k"));
1120    /// assert_eq!((upload.fields("tag").count(), upload.file_count()), (2, 0));
1121    /// # Ok::<(), form_net::FormError>(())
1122    /// ```
1123    pub fn from_form(form: crate::Form, policy: &Policy) -> Result<Upload, FormError> {
1124        let mut upload = Upload::default();
1125        let token = policy.token_field.map(|t| t.name());
1126        for (name, value) in form.iter() {
1127            if Some(name) == token {
1128                if upload.token.is_some() {
1129                    return Err(FormError::policy(
1130                        crate::error::PolicyRefusal::DuplicateToken,
1131                        Location::NONE,
1132                    ));
1133                }
1134                upload.token = Some(String::from(value));
1135            }
1136            upload.push_field(name, value.as_bytes(), usize::MAX);
1137        }
1138        Ok(upload)
1139    }
1140
1141    /// The stored files, for extraction that moves some of them out.
1142    pub(crate) fn files_mut(&mut self) -> &mut Vec<StoredFile> {
1143        &mut self.files
1144    }
1145
1146    pub(crate) fn into_parts(mut self) -> (Upload, Vec<StoredFile>) {
1147        let files = core::mem::take(&mut self.files);
1148        (self, files)
1149    }
1150
1151    /// The anti-forgery token field's value, when a token field was set.
1152    #[must_use]
1153    pub fn captured_token(&self) -> Option<&str> {
1154        self.token.as_deref()
1155    }
1156
1157    /// The bytes of all text values together.
1158    #[must_use]
1159    pub fn text_len(&self) -> u64 {
1160        self.spans.iter().map(|s| s.value.1 as u64).sum()
1161    }
1162
1163    /// The number of files.
1164    #[must_use]
1165    pub fn file_count(&self) -> usize {
1166        self.files.len()
1167    }
1168
1169    /// Reads a whole multipart body from `reader` in 64 KiB chunks, with
1170    /// explicit limits, policy, store, and inspector.
1171    ///
1172    /// # Errors
1173    ///
1174    /// Every multipart error; `Refused`; `Io { kind }`; `Store { .. }`;
1175    /// `Limit { Disk }`.
1176    ///
1177    /// # Examples
1178    ///
1179    /// ```
1180    /// use form_net::{Limits, NoInspector, Policy, Store, TokenField, Upload};
1181    ///
1182    /// let store = Store::temp()?;
1183    /// let policy = Policy::DEFAULT.with_token_field(TokenField::new("csrf_token"));
1184    /// let body = b"--X\r\nContent-Disposition: form-data; name=csrf_token\r\n\r\ntok\r\n--X--";
1185    /// let upload = Upload::parse_with(b"multipart/form-data; boundary=X", &body[..], &Limits::DEFAULT, &policy, &store, &mut NoInspector)?;
1186    /// assert_eq!(upload.captured_token(), Some("tok"));
1187    /// # Ok::<(), form_net::FormError>(())
1188    /// ```
1189    pub fn parse_with<R: Read, I: Inspector>(
1190        content_type: &[u8],
1191        mut reader: R,
1192        limits: &Limits,
1193        policy: &Policy,
1194        store: &Store,
1195        inspector: &mut I,
1196    ) -> Result<Upload, FormError> {
1197        // `failed` on every failed parse, setup included, as the buffered
1198        // entry points call it (D-043, D-144).
1199        let mut session = match UploadSession::new(content_type, limits, policy, store) {
1200            Ok(s) => s,
1201            Err(e) => {
1202                inspector.failed(&e);
1203                return Err(e);
1204            }
1205        };
1206        let mut buf = crate::wipe::ReadBuf::new(64 * 1024);
1207        loop {
1208            let n = match reader.read(&mut buf.buf) {
1209                Ok(n) => n,
1210                Err(e) if e.kind() == io::ErrorKind::Interrupted => {
1211                    buf.failed();
1212                    continue;
1213                }
1214                Err(e) => {
1215                    buf.failed();
1216                    let err = temp::io(&e);
1217                    inspector.failed(&err);
1218                    return Err(err);
1219                }
1220            };
1221            if n == 0 {
1222                break;
1223            }
1224            buf.filled(n);
1225            session.push(buf.buf.get(..n).unwrap_or(&[]), inspector)?;
1226        }
1227        drop(buf);
1228        session.finish(inspector)
1229    }
1230}
1231
1232/// The push-style form for event-driven servers: hand each body chunk to
1233/// [`push`](Self::push) as reads complete, then [`finish`](Self::finish).
1234///
1235/// Dropping a session without `finish` deletes every file it stored.
1236/// [`captured_token`](Self::captured_token) is available as soon as the
1237/// token field ends, so the token can be verified while files are still
1238/// arriving.
1239///
1240/// # Examples
1241///
1242/// ```
1243/// use form_net::{Limits, NoInspector, Policy, Store, UploadSession};
1244///
1245/// let store = Store::temp()?;
1246/// let body = b"--X\r\nContent-Disposition: form-data; name=a\r\n\r\n1\r\n--X--";
1247/// let mut session = UploadSession::new(b"multipart/form-data; boundary=X", &Limits::DEFAULT, &Policy::DEFAULT, &store)?;
1248/// for chunk in body.chunks(5) {
1249///     session.push(chunk, &mut NoInspector)?;
1250/// }
1251/// let upload = session.finish(&mut NoInspector)?;
1252/// assert_eq!(upload.field("a"), Some("1"));
1253/// # Ok::<(), form_net::FormError>(())
1254/// ```
1255pub struct UploadSession {
1256    driver: Driver<Vec<u8>, StoreSink>,
1257    upload: Upload,
1258    text_cap: usize,
1259    ended: bool,
1260    failed: bool,
1261}
1262
1263impl fmt::Debug for UploadSession {
1264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1265        f.debug_struct("UploadSession")
1266            .field("bytes_seen", &self.bytes_seen())
1267            .field("upload", &self.upload)
1268            .finish_non_exhaustive()
1269    }
1270}
1271
1272impl UploadSession {
1273    /// A session for one request body.
1274    ///
1275    /// # Errors
1276    ///
1277    /// `UnsupportedMediaType` and the boundary errors of
1278    /// [`FormKind::detect`](crate::FormKind::detect); `InvalidLimits`.
1279    pub fn new(
1280        content_type: &[u8],
1281        limits: &Limits,
1282        policy: &Policy,
1283        store: &Store,
1284    ) -> Result<Self, FormError> {
1285        let boundary = crate::Boundary::parse(content_type)?;
1286        let parser = MultipartParser::new(boundary, limits, policy)?;
1287        Ok(UploadSession {
1288            driver: Driver::new(parser, store.sink(limits)),
1289            upload: Upload::default(),
1290            text_cap: usize::try_from(limits.text_and_names()).unwrap_or(usize::MAX),
1291            ended: false,
1292            failed: false,
1293        })
1294    }
1295
1296    /// Feeds one chunk; consumes all of it or fails.
1297    ///
1298    /// # Errors
1299    ///
1300    /// Every multipart error, `Refused`, `Io`, `Store`, `Limit { Disk }`;
1301    /// `Poisoned` after an error.
1302    pub fn push<I: Inspector>(&mut self, chunk: &[u8], inspector: &mut I) -> Result<(), FormError> {
1303        self.feed(chunk, false, inspector)
1304    }
1305
1306    /// Ends the body and returns the upload.
1307    ///
1308    /// # Errors
1309    ///
1310    /// `Syntax { Truncated }` if the close delimiter was not seen; as
1311    /// [`push`](Self::push).
1312    pub fn finish<I: Inspector>(mut self, inspector: &mut I) -> Result<Upload, FormError> {
1313        if self.failed {
1314            return Err(FormError::Poisoned);
1315        }
1316        if !self.ended {
1317            self.feed(&[], true, inspector)?;
1318        }
1319        let mut upload = core::mem::take(&mut self.upload);
1320        upload.token = self.driver.parser().captured_token().map(String::from);
1321        Ok(upload)
1322    }
1323
1324    /// The token field's value, once its part has ended.
1325    #[must_use]
1326    pub fn captured_token(&self) -> Option<&str> {
1327        self.driver.parser().captured_token()
1328    }
1329
1330    /// Body bytes consumed so far.
1331    #[must_use]
1332    pub fn bytes_seen(&self) -> u64 {
1333        self.driver.parser().bytes_seen()
1334    }
1335
1336    fn feed<I: Inspector>(
1337        &mut self,
1338        chunk: &[u8],
1339        eof: bool,
1340        inspector: &mut I,
1341    ) -> Result<(), FormError> {
1342        if self.failed {
1343            return Err(FormError::Poisoned);
1344        }
1345        let r = self.feed_inner(chunk, eof, inspector);
1346        if let Err(e) = &r {
1347            self.failed = true;
1348            // The driver reports its own failures; a field that fails its
1349            // text check fails here, after the driver returned.
1350            if !self.driver.has_failed() {
1351                inspector.failed(e);
1352            }
1353            // Nothing of a refused body survives.
1354            self.upload.files.clear();
1355        }
1356        r
1357    }
1358
1359    fn feed_inner<I: Inspector>(
1360        &mut self,
1361        chunk: &[u8],
1362        eof: bool,
1363        inspector: &mut I,
1364    ) -> Result<(), FormError> {
1365        let mut pos = 0usize;
1366        loop {
1367            let rest = chunk.get(pos..).unwrap_or(&[]);
1368            let (n, progress) = self.driver.push(rest, eof, inspector)?;
1369            pos = pos.saturating_add(n);
1370            match progress {
1371                Progress::Field { name, value } => {
1372                    // Checked as text by the driver before the inspector
1373                    // saw it, as the buffered parser checks it.
1374                    let cap = self.text_cap;
1375                    self.upload.push_field(name, value, cap);
1376                }
1377                Progress::File(f) => self.upload.files.push(f),
1378                Progress::End => {
1379                    self.ended = true;
1380                    if pos >= chunk.len() {
1381                        return Ok(());
1382                    }
1383                }
1384                _ => {
1385                    if pos >= chunk.len() {
1386                        return Ok(());
1387                    }
1388                }
1389            }
1390        }
1391    }
1392}
1393
1394/// The blocking convenience: reads `body` to the end in 64 KiB chunks with
1395/// the default limits and policy, keeps files up to 64 KiB in memory, and
1396/// spills larger ones to anonymous temporary files in a private directory
1397/// that is created only when a file first spills and removed when the last
1398/// file from it is dropped. There is no process-wide state.
1399///
1400/// # Errors
1401///
1402/// Every multipart error; `Io { kind }`; `Limit { Disk }`; `Random`.
1403///
1404/// # Examples
1405///
1406/// ```
1407/// let body = b"--X\r\nContent-Disposition: form-data; name=title\r\n\r\nHi\r\n--X--";
1408/// let upload = form_net::read_multipart(b"multipart/form-data; boundary=X", &body[..])?;
1409/// assert_eq!(upload.field("title"), Some("Hi"));
1410/// # Ok::<(), form_net::FormError>(())
1411/// ```
1412pub fn read_multipart<R: Read>(content_type: &[u8], body: R) -> Result<Upload, FormError> {
1413    let store = Store::temp_lazy();
1414    Upload::parse_with(
1415        content_type,
1416        body,
1417        &Limits::DEFAULT,
1418        &Policy::DEFAULT,
1419        &store,
1420        &mut crate::NoInspector,
1421    )
1422}
1423
1424impl FileName<'_> {
1425    /// One portable file name from the client's: the HTML-unescaped base
1426    /// name with every default-ignorable character (zero width spaces and
1427    /// joiners, soft hyphens, variation selectors, tags, bidirectional
1428    /// controls) replaced by `_` (D-084), then passed through
1429    /// `path_lib::sanitize`, which replaces separators, control characters,
1430    /// Windows-reserved characters and device names, and trims trailing dots
1431    /// and spaces. With the `unicode` feature the name is put in Unicode NFC
1432    /// 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
1433    /// overwrite anything (`StoredFile::persist` refuses to).
1434    ///
1435    /// # Errors
1436    ///
1437    /// `Encoding { InvalidUtf8 }`.
1438    ///
1439    /// # Examples
1440    ///
1441    /// ```
1442    /// 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--";
1443    /// let mp = form_net::parse_multipart(b"multipart/form-data; boundary=X", body)?;
1444    /// let name = mp.file("f").unwrap().filename().unwrap();
1445    /// assert_eq!(name.sanitized()?, "Q3 report_ final_.pdf");
1446    /// # Ok::<(), form_net::FormError>(())
1447    /// ```
1448    #[cfg_attr(docsrs, doc(cfg(feature = "fs")))]
1449    pub fn sanitized(&self) -> Result<String, FormError> {
1450        let base = FileName::new(self.basename(), self.source());
1451        let text = base.html_unescaped()?;
1452        #[cfg(feature = "unicode")]
1453        let text = unicode_lang::normalize(text.as_ref(), unicode_lang::Form::Nfc);
1454        let text: &str = text.as_ref();
1455        if text.is_ascii() {
1456            return Ok(path_lib::sanitize(text));
1457        }
1458        let visible: String = text
1459            .chars()
1460            .map(|c| if is_default_ignorable(c) { '_' } else { c })
1461            .collect();
1462        Ok(path_lib::sanitize(visible))
1463    }
1464}
1465
1466/// Unicode's `Default_Ignorable_Code_Point` property (DerivedCoreProperties):
1467/// characters that render as nothing, so a name can hide them.
1468const fn is_default_ignorable(c: char) -> bool {
1469    matches!(
1470        c,
1471        '\u{00AD}'
1472            | '\u{034F}'
1473            | '\u{061C}'
1474            | '\u{115F}'..='\u{1160}'
1475            | '\u{17B4}'..='\u{17B5}'
1476            | '\u{180B}'..='\u{180F}'
1477            | '\u{200B}'..='\u{200F}'
1478            | '\u{202A}'..='\u{202E}'
1479            | '\u{2060}'..='\u{206F}'
1480            | '\u{3164}'
1481            | '\u{FE00}'..='\u{FE0F}'
1482            | '\u{FEFF}'
1483            | '\u{FFA0}'
1484            | '\u{FFF0}'..='\u{FFF8}'
1485            | '\u{1BCA0}'..='\u{1BCA3}'
1486            | '\u{1D173}'..='\u{1D17A}'
1487            | '\u{E0000}'..='\u{E0FFF}'
1488    )
1489}
1490
1491impl crate::FromField for StoredFile {
1492    /// Whether the field arrived (an empty file input counts), and the file.
1493    type Slot = (bool, Option<StoredFile>);
1494
1495    fn accept_file(
1496        slot: &mut Self::Slot,
1497        file: crate::FileValue<'_>,
1498    ) -> Result<(), crate::ValueError> {
1499        // Before the empty-input shortcut, as for `FileData` (D-137).
1500        if slot.0 {
1501            return Err(crate::ValueError::Duplicate);
1502        }
1503        slot.0 = true;
1504        let stored = match file {
1505            crate::FileValue::Stored(f) => f,
1506            crate::FileValue::Buffered(part) => StoredFile::detached(
1507                part.name(),
1508                part.filename(),
1509                part.content_type().map(|c| c.raw()),
1510                part.bytes(),
1511            ),
1512        };
1513        if stored.is_empty() && stored.filename().is_some_and(|n| n.is_empty()) {
1514            // A file input left empty: as if it were absent.
1515            return Ok(());
1516        }
1517        slot.1 = Some(stored);
1518        Ok(())
1519    }
1520
1521    fn finish(slot: Self::Slot) -> Result<Self, crate::ValueError> {
1522        slot.1.ok_or(crate::ValueError::Missing)
1523    }
1524}