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}