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, 64 * 1024, 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    type Slot = Option<FileData>;
107
108    fn accept_file(slot: &mut Self::Slot, file: FileValue<'_>) -> Result<(), ValueError> {
109        let data = match file {
110            FileValue::Buffered(part) => {
111                let name = part.filename();
112                if name.is_some_and(|n| n.is_empty()) && part.is_empty() {
113                    // A file input left empty: as if it were absent.
114                    return Ok(());
115                }
116                let filename = match name {
117                    Some(n) => {
118                        Some(String::from(n.as_str().map_err(|_| {
119                            ValueError::Invalid("file name is not UTF-8")
120                        })?))
121                    }
122                    None => None,
123                };
124                let content_type = part
125                    .content_type()
126                    .and_then(|c| core::str::from_utf8(c.raw()).ok().map(String::from));
127                FileData::new(part.bytes().to_vec(), filename, content_type)
128            }
129            #[cfg(feature = "fs")]
130            FileValue::Stored(stored) => {
131                if stored.is_empty() && stored.filename().is_some_and(|n| n.is_empty()) {
132                    return Ok(());
133                }
134                let filename = match stored.filename() {
135                    Some(n) => {
136                        Some(String::from(n.as_str().map_err(|_| {
137                            ValueError::Invalid("file name is not UTF-8")
138                        })?))
139                    }
140                    None => None,
141                };
142                let content_type = stored
143                    .content_type()
144                    .and_then(|c| core::str::from_utf8(c.raw()).ok().map(String::from));
145                let bytes = stored
146                    .read_to_vec(stored.len())
147                    .map_err(|_| ValueError::Invalid("the stored file could not be read"))?;
148                FileData::new(bytes, filename, content_type)
149            }
150        };
151        if slot.is_some() {
152            return Err(ValueError::Duplicate);
153        }
154        *slot = Some(data);
155        Ok(())
156    }
157
158    fn finish(slot: Self::Slot) -> Result<Self, ValueError> {
159        slot.ok_or(ValueError::Missing)
160    }
161}