rocket_community/fs/
file_name.rs

1use ref_cast::RefCast;
2
3use crate::http::RawStr;
4
5/// A file name in a [`TempFile`] or multipart [`DataField`].
6///
7/// A `Content-Disposition` header, either in a response or a multipart field,
8/// can optionally specify a `filename` directive as identifying information for
9/// the attached file. This type represents the value of that directive.
10///
11/// # Safety
12///
13/// There are no restrictions on the value of the directive. In particular, the
14/// value can be wholly unsafe to use as a file name in common contexts. As
15/// such, Rocket sanitizes the value into a version that _is_ safe to use as a
16/// file name in common contexts; this sanitized version can be retrieved via
17/// [`FileName::as_str()`] and is returned by [`TempFile::name()`].
18///
19/// You will likely want to prepend or append random or user-specific components
20/// to the name to avoid collisions; UUIDs make for a good "random" data. You
21/// may also prefer to avoid the value in the directive entirely by using a
22/// safe, application-generated name instead.
23///
24/// [`TempFile::name()`]: crate::fs::TempFile::name
25/// [`DataField`]: crate::form::DataField
26/// [`TempFile`]: crate::fs::TempFile
27#[repr(transparent)]
28#[derive(RefCast, Debug)]
29pub struct FileName(str);
30
31impl FileName {
32    /// Wraps a string as a `FileName`. This is cost-free.
33    ///
34    /// # Example
35    ///
36    /// ```rust
37    /// # extern crate rocket_community as rocket;
38    /// use rocket::fs::FileName;
39    ///
40    /// let name = FileName::new("some-file.txt");
41    /// assert_eq!(name.as_str(), Some("some-file"));
42    ///
43    /// let name = FileName::new("some-file.txt");
44    /// assert_eq!(name.dangerous_unsafe_unsanitized_raw(), "some-file.txt");
45    /// ```
46    pub fn new<S: AsRef<str> + ?Sized>(string: &S) -> &FileName {
47        FileName::ref_cast(string.as_ref())
48    }
49
50    /// The sanitized file name, stripped of any file extension and special
51    /// characters, safe for use as a file name.
52    ///
53    /// # Sanitization
54    ///
55    /// A "sanitized" file name is a non-empty string, stripped of its file
56    /// extension, which is not a platform-specific reserved name and does not
57    /// contain any platform-specific special characters.
58    ///
59    /// On Unix, these are the characters `'.', '/', '\\', '<', '>', '|', ':',
60    /// '(', ')', '&', ';', '#', '?', '*'`.
61    ///
62    /// On Windows (and non-Unix OSs), these are the characters `'.', '<', '>',
63    /// ':', '"', '/', '\', '|', '?', '*', ',', ';', '=', '(', ')', '&', '#'`,
64    /// and the reserved names `"CON", "PRN", "AUX", "NUL", "COM1", "COM2",
65    /// "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2",
66    /// "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"`.
67    ///
68    /// Additionally, all control characters are considered "special".
69    ///
70    /// An attempt is made to transform the raw file name into a sanitized
71    /// version by identifying a valid substring of the raw file name that meets
72    /// this criteria. If none is found, `None` is returned.
73    ///
74    /// # Example
75    ///
76    /// ```rust
77    /// # extern crate rocket_community as rocket;
78    /// use rocket::fs::FileName;
79    ///
80    /// let name = FileName::new("some-file.txt");
81    /// assert_eq!(name.as_str(), Some("some-file"));
82    ///
83    /// let name = FileName::new("some-file.txt.zip");
84    /// assert_eq!(name.as_str(), Some("some-file"));
85    ///
86    /// let name = FileName::new("../../../../etc/shadow");
87    /// assert_eq!(name.as_str(), Some("shadow"));
88    ///
89    /// let name = FileName::new("/etc/.shadow");
90    /// assert_eq!(name.as_str(), Some("shadow"));
91    ///
92    /// let name = FileName::new("/a/b/some/file.txt.zip");
93    /// assert_eq!(name.as_str(), Some("file"));
94    ///
95    /// let name = FileName::new("/a/b/some/.file.txt.zip");
96    /// assert_eq!(name.as_str(), Some("file"));
97    ///
98    /// let name = FileName::new("/a/b/some/.*file.txt.zip");
99    /// assert_eq!(name.as_str(), Some("file"));
100    ///
101    /// let name = FileName::new("a/\\b/some/.*file<.txt.zip");
102    /// assert_eq!(name.as_str(), Some("file"));
103    ///
104    /// let name = FileName::new(">>>.foo.txt");
105    /// assert_eq!(name.as_str(), Some("foo"));
106    ///
107    /// let name = FileName::new("b:c");
108    /// #[cfg(unix)] assert_eq!(name.as_str(), Some("b"));
109    /// #[cfg(not(unix))] assert_eq!(name.as_str(), Some("c"));
110    ///
111    /// let name = FileName::new("//./.<>");
112    /// assert_eq!(name.as_str(), None);
113    /// ```
114    pub fn as_str(&self) -> Option<&str> {
115        #[cfg(not(unix))]
116        let (bad_char, bad_name) = {
117            static BAD_CHARS: &[char] = &[
118                // Microsoft says these are invalid.
119                '.', '<', '>', ':', '"', '/', '\\', '|', '?', '*',
120                // `cmd.exe` treats these specially.
121                ',', ';', '=', // These are treated specially by unix-like shells.
122                '(', ')', '&', '#',
123            ];
124
125            // Microsoft says these are reserved.
126            static BAD_NAMES: &[&str] = &[
127                "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
128                "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
129                "LPT9",
130            ];
131
132            let bad_char = |c| BAD_CHARS.contains(&c) || c.is_control();
133            let bad_name = |n| BAD_NAMES.contains(&n);
134            (bad_char, bad_name)
135        };
136
137        #[cfg(unix)]
138        let (bad_char, bad_name) = {
139            static BAD_CHARS: &[char] = &[
140                // These have special meaning in a file name.
141                '.', '/', '\\', // These are treated specially by shells.
142                '<', '>', '|', ':', '(', ')', '&', ';', '#', '?', '*',
143            ];
144
145            let bad_char = |c| BAD_CHARS.contains(&c) || c.is_control();
146            let bad_name = |_| false;
147            (bad_char, bad_name)
148        };
149
150        // Get the file name as a `str` without any extension(s).
151        let file_name = std::path::Path::new(&self.0)
152            .file_name()
153            .and_then(|n| n.to_str())
154            .and_then(|n| n.split(bad_char).find(|s| !s.is_empty()))?;
155
156        // At this point, `file_name` can't contain `bad_chars` because of
157        // `.split()`, but it can be empty or reserved.
158        if file_name.is_empty() || bad_name(file_name) {
159            return None;
160        }
161
162        Some(file_name)
163    }
164
165    /// Returns `true` if the _complete_ raw file name is safe.
166    ///
167    /// Note that `.as_str()` returns a safe _subset_ of the raw file name, if
168    /// there is one. If this method returns `true`, then that subset is the
169    /// complete raw file name.
170    ///
171    /// This method should be use sparingly. In particular, there is no
172    /// advantage to calling `is_safe()` prior to calling `as_str()`; simply
173    /// call `as_str()`.
174    ///
175    /// # Example
176    ///
177    /// ```rust
178    /// # extern crate rocket_community as rocket;
179    /// use rocket::fs::FileName;
180    ///
181    /// let name = FileName::new("some-file.txt");
182    /// assert_eq!(name.as_str(), Some("some-file"));
183    /// assert!(!name.is_safe());
184    ///
185    /// let name = FileName::new("some-file");
186    /// assert_eq!(name.as_str(), Some("some-file"));
187    /// assert!(name.is_safe());
188    /// ```
189    pub fn is_safe(&self) -> bool {
190        self.as_str() == Some(&self.0)
191    }
192
193    /// The raw, unsanitized, potentially unsafe file name. Prefer to use
194    /// [`FileName::as_str()`], always.
195    ///
196    /// # ⚠️ DANGER ⚠️
197    ///
198    /// This method returns the file name exactly as it was specified by the
199    /// client. You should **_not_** use this name _unless_ you require the
200    /// originally specified `filename` _and_ it is known not to contain
201    /// special, potentially dangerous characters, _and_:
202    ///
203    ///   1. All clients are known to be trusted, perhaps because the server
204    ///      only runs locally, serving known, local requests, or...
205    ///
206    ///   2. You will not use the file name to store a file on disk or any
207    ///      context that expects a file name _and_ you will not use the
208    ///      extension to determine how to handle/parse the data, or...
209    ///
210    ///   3. You will expertly process the raw name into a sanitized version for
211    ///      use in specific contexts.
212    ///
213    /// If not all of these cases apply, use [`FileName::as_str()`].
214    ///
215    /// # Example
216    ///
217    /// ```rust
218    /// # extern crate rocket_community as rocket;
219    /// use rocket::fs::FileName;
220    ///
221    /// let name = FileName::new("some-file.txt");
222    /// assert_eq!(name.dangerous_unsafe_unsanitized_raw(), "some-file.txt");
223    ///
224    /// let name = FileName::new("../../../../etc/shadow");
225    /// assert_eq!(name.dangerous_unsafe_unsanitized_raw(), "../../../../etc/shadow");
226    ///
227    /// let name = FileName::new("../../.ssh/id_rsa");
228    /// assert_eq!(name.dangerous_unsafe_unsanitized_raw(), "../../.ssh/id_rsa");
229    ///
230    /// let name = FileName::new("/Rocket.toml");
231    /// assert_eq!(name.dangerous_unsafe_unsanitized_raw(), "/Rocket.toml");
232    /// ```
233    pub fn dangerous_unsafe_unsanitized_raw(&self) -> &RawStr {
234        self.0.into()
235    }
236}
237
238impl<'a, S: AsRef<str> + ?Sized> From<&'a S> for &'a FileName {
239    #[inline]
240    fn from(string: &'a S) -> Self {
241        FileName::new(string)
242    }
243}