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