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