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    ///
62    /// # Examples
63    ///
64    /// ```
65    /// # use form_net::FromForm;
66    /// # form_net::form! { pub struct Avatar { pub image: form_net::FileData } }
67    /// # let ct = b"multipart/form-data; boundary=X";
68    /// # 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--";
69    /// let file = Avatar::from_multipart(ct, body)?.image;
70    /// assert_eq!(file.bytes(), b"PNG");
71    /// # Ok::<(), form_net::FormError>(())
72    /// ```
73    #[must_use]
74    pub fn bytes(&self) -> &[u8] {
75        &self.bytes
76    }
77
78    /// The file's bytes, owned. They are no longer wiped by form-net.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// # use form_net::FromForm;
84    /// # form_net::form! { pub struct Avatar { pub image: form_net::FileData } }
85    /// # let ct = b"multipart/form-data; boundary=X";
86    /// # 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--";
87    /// let file = Avatar::from_multipart(ct, body)?.image;
88    /// let bytes: Vec<u8> = file.into_bytes();
89    /// assert_eq!(bytes, b"PNG");
90    /// # Ok::<(), form_net::FormError>(())
91    /// ```
92    #[must_use]
93    pub fn into_bytes(mut self) -> Vec<u8> {
94        core::mem::take(&mut self.bytes)
95    }
96
97    /// The length in bytes.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// # use form_net::FromForm;
103    /// # form_net::form! { pub struct Avatar { pub image: form_net::FileData } }
104    /// # let ct = b"multipart/form-data; boundary=X";
105    /// # 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--";
106    /// let file = Avatar::from_multipart(ct, body)?.image;
107    /// assert_eq!(file.len(), 3);
108    /// # Ok::<(), form_net::FormError>(())
109    /// ```
110    #[must_use]
111    pub fn len(&self) -> usize {
112        self.bytes.len()
113    }
114
115    /// Whether the file is empty.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// # use form_net::FromForm;
121    /// # form_net::form! { pub struct Avatar { pub image: form_net::FileData } }
122    /// # let ct = b"multipart/form-data; boundary=X";
123    /// # 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--";
124    /// let file = Avatar::from_multipart(ct, body)?.image;
125    /// assert!(!file.is_empty());
126    /// assert!(form_net::FileData::default().is_empty());
127    /// # Ok::<(), form_net::FormError>(())
128    /// ```
129    #[must_use]
130    pub fn is_empty(&self) -> bool {
131        self.bytes.is_empty()
132    }
133
134    /// The client's file name, as sent: never sanitized, never a path.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// # use form_net::FromForm;
140    /// # form_net::form! { pub struct Avatar { pub image: form_net::FileData } }
141    /// # let ct = b"multipart/form-data; boundary=X";
142    /// # 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--";
143    /// let file = Avatar::from_multipart(ct, body)?.image;
144    /// // As sent: sanitize it before using it as a path.
145    /// assert_eq!(file.filename(), Some("me.png"));
146    /// # Ok::<(), form_net::FormError>(())
147    /// ```
148    #[must_use]
149    pub fn filename(&self) -> Option<&str> {
150        self.filename.as_deref()
151    }
152
153    /// The declared content type: a hint, never trusted.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// # use form_net::FromForm;
159    /// # form_net::form! { pub struct Avatar { pub image: form_net::FileData } }
160    /// # let ct = b"multipart/form-data; boundary=X";
161    /// # 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--";
162    /// let file = Avatar::from_multipart(ct, body)?.image;
163    /// // The client's claim; check the bytes before trusting it.
164    /// assert_eq!(file.content_type(), Some("image/png"));
165    /// # Ok::<(), form_net::FormError>(())
166    /// ```
167    #[must_use]
168    pub fn content_type(&self) -> Option<&str> {
169        self.content_type.as_deref()
170    }
171}
172
173impl Drop for FileData {
174    fn drop(&mut self) {
175        crate::wipe::wipe_vec(&mut self.bytes);
176        crate::wipe::wipe_opt_string(&mut self.filename);
177        crate::wipe::wipe_opt_string(&mut self.content_type);
178    }
179}
180
181impl FromField for FileData {
182    /// Whether the field arrived (an empty file input counts), and the file.
183    type Slot = (bool, Option<FileData>);
184
185    fn accept_file(slot: &mut Self::Slot, file: FileValue<'_>) -> Result<(), ValueError> {
186        // Checked before the empty-input shortcut: a real file and an empty
187        // input under one name is two values, read differently by a
188        // last-wins reader (D-137).
189        if slot.0 {
190            return Err(ValueError::Duplicate);
191        }
192        slot.0 = true;
193        let data = match file {
194            FileValue::Buffered(part) => {
195                let name = part.filename();
196                if name.is_some_and(|n| n.is_empty()) && part.is_empty() {
197                    // A file input left empty: as if it were absent.
198                    return Ok(());
199                }
200                let filename = match name {
201                    Some(n) => {
202                        Some(String::from(n.as_str().map_err(|_| {
203                            ValueError::Invalid("file name is not UTF-8")
204                        })?))
205                    }
206                    None => None,
207                };
208                let content_type = part
209                    .content_type()
210                    .and_then(|c| core::str::from_utf8(c.raw()).ok().map(String::from));
211                FileData::new(part.bytes().to_vec(), filename, content_type)
212            }
213            #[cfg(feature = "fs")]
214            FileValue::Stored(stored) => {
215                if stored.is_empty() && stored.filename().is_some_and(|n| n.is_empty()) {
216                    return Ok(());
217                }
218                let filename = match stored.filename() {
219                    Some(n) => {
220                        Some(String::from(n.as_str().map_err(|_| {
221                            ValueError::Invalid("file name is not UTF-8")
222                        })?))
223                    }
224                    None => None,
225                };
226                let content_type = stored
227                    .content_type()
228                    .and_then(|c| core::str::from_utf8(c.raw()).ok().map(String::from));
229                let bytes = stored
230                    .read_to_vec(stored.len())
231                    .map_err(|_| ValueError::Invalid("the stored file could not be read"))?;
232                FileData::new(bytes, filename, content_type)
233            }
234        };
235        slot.1 = Some(data);
236        Ok(())
237    }
238
239    fn finish(slot: Self::Slot) -> Result<Self, ValueError> {
240        slot.1.ok_or(ValueError::Missing)
241    }
242}