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}