Skip to main content

form_net/extract/
file.rs

1//! `FileData`: an owned file for buffered bodies.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::fmt;
6
7use super::{FileValue, FromField, ValueError};
8
9/// An owned file: its bytes, and the file name and content type the client
10/// sent, when they are text. The output of [`MemorySink`](crate::MemorySink)
11/// and a field type for typed extraction from buffered bodies.
12///
13/// The bytes are wiped on drop. `Debug` prints the length only.
14///
15/// # Examples
16///
17/// ```
18/// form_net::form! {
19///     pub struct Avatar {
20///         pub image: form_net::FileData,
21///     }
22/// }
23/// use form_net::FromForm;
24///
25/// let body = b"--X\r\nContent-Disposition: form-data; name=image; filename=\"me.png\"\r\nContent-Type: image/png\r\n\r\nPNG\r\n--X--";
26/// let a = Avatar::from_multipart(b"multipart/form-data; boundary=X", body)?;
27/// assert_eq!((a.image.bytes(), a.image.filename()), (&b"PNG"[..], Some("me.png")));
28/// # Ok::<(), form_net::FormError>(())
29/// ```
30#[derive(Default)]
31pub struct FileData {
32    bytes: Vec<u8>,
33    filename: Option<String>,
34    content_type: Option<String>,
35}
36
37impl fmt::Debug for FileData {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "FileData({} bytes)", self.bytes.len())
40    }
41}
42
43impl FileData {
44    pub(crate) fn new(
45        bytes: Vec<u8>,
46        filename: Option<String>,
47        content_type: Option<String>,
48    ) -> Self {
49        FileData {
50            bytes,
51            filename,
52            content_type,
53        }
54    }
55
56    pub(crate) fn append(&mut self, chunk: &[u8]) {
57        crate::wipe::append_wiping(&mut self.bytes, chunk, 4096, usize::MAX);
58    }
59
60    /// The file's bytes.
61    #[must_use]
62    pub fn bytes(&self) -> &[u8] {
63        &self.bytes
64    }
65
66    /// The file's bytes, owned. They are no longer wiped by form-net.
67    #[must_use]
68    pub fn into_bytes(mut self) -> Vec<u8> {
69        core::mem::take(&mut self.bytes)
70    }
71
72    /// The length in bytes.
73    #[must_use]
74    pub fn len(&self) -> usize {
75        self.bytes.len()
76    }
77
78    /// Whether the file is empty.
79    #[must_use]
80    pub fn is_empty(&self) -> bool {
81        self.bytes.is_empty()
82    }
83
84    /// The client's file name, as sent: never sanitized, never a path.
85    #[must_use]
86    pub fn filename(&self) -> Option<&str> {
87        self.filename.as_deref()
88    }
89
90    /// The declared content type: a hint, never trusted.
91    #[must_use]
92    pub fn content_type(&self) -> Option<&str> {
93        self.content_type.as_deref()
94    }
95}
96
97impl Drop for FileData {
98    fn drop(&mut self) {
99        crate::wipe::wipe_vec(&mut self.bytes);
100        crate::wipe::wipe_opt_string(&mut self.filename);
101        crate::wipe::wipe_opt_string(&mut self.content_type);
102    }
103}
104
105impl FromField for FileData {
106    /// Whether the field arrived (an empty file input counts), and the file.
107    type Slot = (bool, Option<FileData>);
108
109    fn accept_file(slot: &mut Self::Slot, file: FileValue<'_>) -> Result<(), ValueError> {
110        // Checked before the empty-input shortcut: a real file and an empty
111        // input under one name is two values, read differently by a
112        // last-wins reader (D-137).
113        if slot.0 {
114            return Err(ValueError::Duplicate);
115        }
116        slot.0 = true;
117        let data = match file {
118            FileValue::Buffered(part) => {
119                let name = part.filename();
120                if name.is_some_and(|n| n.is_empty()) && part.is_empty() {
121                    // A file input left empty: as if it were absent.
122                    return Ok(());
123                }
124                let filename = match name {
125                    Some(n) => {
126                        Some(String::from(n.as_str().map_err(|_| {
127                            ValueError::Invalid("file name is not UTF-8")
128                        })?))
129                    }
130                    None => None,
131                };
132                let content_type = part
133                    .content_type()
134                    .and_then(|c| core::str::from_utf8(c.raw()).ok().map(String::from));
135                FileData::new(part.bytes().to_vec(), filename, content_type)
136            }
137            #[cfg(feature = "fs")]
138            FileValue::Stored(stored) => {
139                if stored.is_empty() && stored.filename().is_some_and(|n| n.is_empty()) {
140                    return Ok(());
141                }
142                let filename = match stored.filename() {
143                    Some(n) => {
144                        Some(String::from(n.as_str().map_err(|_| {
145                            ValueError::Invalid("file name is not UTF-8")
146                        })?))
147                    }
148                    None => None,
149                };
150                let content_type = stored
151                    .content_type()
152                    .and_then(|c| core::str::from_utf8(c.raw()).ok().map(String::from));
153                let bytes = stored
154                    .read_to_vec(stored.len())
155                    .map_err(|_| ValueError::Invalid("the stored file could not be read"))?;
156                FileData::new(bytes, filename, content_type)
157            }
158        };
159        slot.1 = Some(data);
160        Ok(())
161    }
162
163    fn finish(slot: Self::Slot) -> Result<Self, ValueError> {
164        slot.1.ok_or(ValueError::Missing)
165    }
166}