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