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}