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