Skip to content

Commit 8bcd168

Browse files
committed
Update implementation
- Add #[non_exaustive] to allow adding more FileResponses in the future - Improve readability of `FileServer::handle` - Take advantage of `NameFile`'s handling missing files - Add `strip_trailing_slash` to ensure files can be read from disk
1 parent 509367a commit 8bcd168

File tree

2 files changed

+128
-54
lines changed

2 files changed

+128
-54
lines changed

core/lib/src/fs/server.rs

+66-50
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use core::fmt;
22
use std::borrow::Cow;
3-
use std::path::{PathBuf, Path};
3+
use std::ffi::OsStr;
4+
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
45
use std::sync::Arc;
56

67
use crate::fs::NamedFile;
@@ -116,11 +117,12 @@ impl fmt::Debug for DebugListRewrite<'_> {
116117
/// - [`FileServer::map_file()`]
117118
pub trait Rewriter: Send + Sync + 'static {
118119
/// Alter the [`FileResponse`] as needed.
119-
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>) -> Option<FileResponse<'p, 'h>>;
120+
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>) -> Option<FileResponse<'p, 'h>>;
120121
}
121122

122123
/// A Response from a [`FileServer`]
123124
#[derive(Debug)]
125+
#[non_exhaustive]
124126
pub enum FileResponse<'p, 'h> {
125127
/// Return the contents of the specified file.
126128
File(File<'p, 'h>),
@@ -157,8 +159,6 @@ impl<'p, 'h> From<Redirect> for Option<FileResponse<'p, 'h>> {
157159
pub struct File<'p, 'h> {
158160
/// The path to the file that [`FileServer`] will respond with.
159161
pub path: Cow<'p, Path>,
160-
/// The original uri from the [`Request`]
161-
pub full_uri: &'p Origin<'p>,
162162
/// A list of headers to be added to the generated response.
163163
pub headers: HeaderMap<'h>,
164164
}
@@ -174,7 +174,6 @@ impl<'p, 'h> File<'p, 'h> {
174174
pub fn with_path(self, path: impl Into<Cow<'p, Path>>) -> Self {
175175
Self {
176176
path: path.into(),
177-
full_uri: self.full_uri,
178177
headers: self.headers,
179178
}
180179
}
@@ -183,33 +182,53 @@ impl<'p, 'h> File<'p, 'h> {
183182
pub fn map_path<R: Into<Cow<'p, Path>>>(self, f: impl FnOnce(Cow<'p, Path>) -> R) -> Self {
184183
Self {
185184
path: f(self.path).into(),
186-
full_uri: self.full_uri,
187185
headers: self.headers,
188186
}
189187
}
190188

191-
/// Convert this `File` into a Redirect, transforming the URI.
192-
pub fn into_redirect(self, f: impl FnOnce(Origin<'static>) -> Origin<'static>)
193-
-> FileResponse<'p, 'h>
194-
{
195-
FileResponse::Redirect(Redirect::permanent(f(self.full_uri.clone().into_owned())))
189+
// /// Convert this `File` into a Redirect, transforming the URI.
190+
// pub fn into_redirect(self, f: impl FnOnce(Origin<'static>) -> Origin<'static>)
191+
// -> FileResponse<'p, 'h>
192+
// {
193+
// FileResponse::Redirect(Redirect::permanent(f(self.full_uri.clone().into_owned())))
194+
// }
195+
196+
async fn respond_to<'r>(self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> where 'h: 'r {
197+
/// Normalize paths to enable `file_root` to work properly
198+
fn strip_trailing_slash(p: &Path) -> &Path {
199+
let bytes = p.as_os_str().as_encoded_bytes();
200+
let bytes = bytes.strip_suffix(MAIN_SEPARATOR_STR.as_bytes()).unwrap_or(bytes);
201+
// SAFETY: Since we stripped a valid UTF-8 sequence (or left it unchanged),
202+
// this is still a valid OsStr.
203+
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
204+
}
205+
206+
NamedFile::open(strip_trailing_slash(self.path.as_ref()))
207+
.await
208+
.respond_to(req)
209+
.map(|mut r| {
210+
for header in self.headers {
211+
r.adjoin_raw_header(header.name.as_str().to_owned(), header.value);
212+
}
213+
r
214+
}).or_forward((data, Status::NotFound))
196215
}
197216
}
198217

199218
impl<F: Send + Sync + 'static> Rewriter for F
200-
where F: for<'r, 'h> Fn(Option<FileResponse<'r, 'h>>) -> Option<FileResponse<'r, 'h>>
219+
where F: for<'r, 'h> Fn(Option<FileResponse<'r, 'h>>, &Request<'_>) -> Option<FileResponse<'r, 'h>>
201220
{
202-
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>) -> Option<FileResponse<'p, 'h>> {
203-
self(path)
221+
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>) -> Option<FileResponse<'p, 'h>> {
222+
self(path, req)
204223
}
205224
}
206225

207226
/// Helper to implement [`FileServer::filter_file()`]
208227
struct FilterFile<F>(F);
209-
impl<F: Fn(&File<'_, '_>) -> bool + Send + Sync + 'static> Rewriter for FilterFile<F> {
210-
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>) -> Option<FileResponse<'p, 'h>> {
228+
impl<F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static> Rewriter for FilterFile<F> {
229+
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>) -> Option<FileResponse<'p, 'h>> {
211230
match path {
212-
Some(FileResponse::File(file)) if !self.0(&file) => None,
231+
Some(FileResponse::File(file)) if !self.0(&file, req) => None,
213232
path => path,
214233
}
215234
}
@@ -218,11 +237,11 @@ impl<F: Fn(&File<'_, '_>) -> bool + Send + Sync + 'static> Rewriter for FilterFi
218237
/// Helper to implement [`FileServer::map_file()`]
219238
struct MapFile<F>(F);
220239
impl<F> Rewriter for MapFile<F>
221-
where F: for<'p, 'h> Fn(File<'p, 'h>) -> FileResponse<'p, 'h> + Send + Sync + 'static,
240+
where F: for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) -> FileResponse<'p, 'h> + Send + Sync + 'static,
222241
{
223-
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>) -> Option<FileResponse<'p, 'h>> {
242+
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>) -> Option<FileResponse<'p, 'h>> {
224243
match path {
225-
Some(FileResponse::File(file)) => Some(self.0(file)),
244+
Some(FileResponse::File(file)) => Some(self.0(file, req)),
226245
path => path,
227246
}
228247
}
@@ -247,7 +266,7 @@ impl<F> Rewriter for MapFile<F>
247266
///
248267
/// Panics if `path` is not directory.
249268
pub fn dir_root(path: impl AsRef<Path>)
250-
-> impl for<'p, 'h> Fn(File<'p, 'h>) -> FileResponse<'p, 'h> + Send + Sync + 'static
269+
-> impl for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) -> FileResponse<'p, 'h> + Send + Sync + 'static
251270
{
252271
use yansi::Paint as _;
253272

@@ -259,7 +278,7 @@ pub fn dir_root(path: impl AsRef<Path>)
259278
panic!("invalid directory: refusing to continue");
260279
}
261280
let path = path.to_path_buf();
262-
move |f| {
281+
move |f, _r| {
263282
FileResponse::File(f.map_path(|p| path.join(p)))
264283
}
265284
}
@@ -280,7 +299,7 @@ pub fn dir_root(path: impl AsRef<Path>)
280299
///
281300
/// Panics if `path` does not exist.
282301
pub fn file_root(path: impl AsRef<Path>)
283-
-> impl for<'p, 'h> Fn(File<'p, 'h>) -> FileResponse<'p, 'h> + Send + Sync + 'static
302+
-> impl for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) -> FileResponse<'p, 'h> + Send + Sync + 'static
284303
{
285304
use yansi::Paint as _;
286305

@@ -292,7 +311,7 @@ pub fn file_root(path: impl AsRef<Path>)
292311
panic!("invalid file: refusing to continue");
293312
}
294313
let path = path.to_path_buf();
295-
move |f| {
314+
move |f, _r| {
296315
FileResponse::File(f.map_path(|p| path.join(p)))
297316
}
298317
}
@@ -310,10 +329,10 @@ pub fn file_root(path: impl AsRef<Path>)
310329
/// # }
311330
/// ```
312331
pub fn missing_root(path: impl AsRef<Path>)
313-
-> impl for<'p, 'h> Fn(File<'p, 'h>) -> FileResponse<'p, 'h> + Send + Sync + 'static
332+
-> impl for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) -> FileResponse<'p, 'h> + Send + Sync + 'static
314333
{
315334
let path = path.as_ref().to_path_buf();
316-
move |f| {
335+
move |f, _r| {
317336
FileResponse::File(f.map_path(|p| path.join(p)))
318337
}
319338
}
@@ -332,7 +351,7 @@ pub fn missing_root(path: impl AsRef<Path>)
332351
/// .map_file(dir_root("static"))
333352
/// # }
334353
/// ```
335-
pub fn filter_dotfiles(file: &File<'_, '_>) -> bool {
354+
pub fn filter_dotfiles(file: &File<'_, '_>, _req: &Request<'_>) -> bool {
336355
!file.path.iter().any(|s| s.as_encoded_bytes().starts_with(b"."))
337356
}
338357

@@ -352,10 +371,12 @@ pub fn filter_dotfiles(file: &File<'_, '_>) -> bool {
352371
/// .map_file(normalize_dirs)
353372
/// # }
354373
/// ```
355-
pub fn normalize_dirs<'p, 'h>(file: File<'p, 'h>) -> FileResponse<'p, 'h> {
356-
if !file.full_uri.path().raw().ends_with('/') && file.path.is_dir() {
357-
// Known good path + '/' is a good path
358-
file.into_redirect(|o| o.map_path(|p| format!("{p}/")).unwrap())
374+
pub fn normalize_dirs<'p, 'h>(file: File<'p, 'h>, req: &Request<'_>) -> FileResponse<'p, 'h> {
375+
if !req.uri().path().raw().ends_with('/') && file.path.is_dir() {
376+
FileResponse::Redirect(Redirect::permanent(
377+
// Known good path + '/' is a good path
378+
req.uri().clone().into_owned().map_path(|p| format!("{p}/")).unwrap()
379+
))
359380
} else {
360381
FileResponse::File(file)
361382
}
@@ -378,9 +399,9 @@ pub fn normalize_dirs<'p, 'h>(file: File<'p, 'h>) -> FileResponse<'p, 'h> {
378399
/// # }
379400
/// ```
380401
pub fn index(index: &'static str)
381-
-> impl for<'p, 'h> Fn(File<'p, 'h>) -> FileResponse<'p, 'h> + Send + Sync
402+
-> impl for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) -> FileResponse<'p, 'h> + Send + Sync
382403
{
383-
move |f| if f.path.is_dir() {
404+
move |f, _r| if f.path.is_dir() {
384405
FileResponse::File(f.map_path(|p| p.join(index)))
385406
} else {
386407
FileResponse::File(f)
@@ -450,8 +471,8 @@ impl FileServer {
450471
///
451472
/// Redirects all requests that have been filtered to the root of the `FileServer`.
452473
/// ```rust,no_run
453-
/// # use rocket::{fs::{FileServer, FileResponse}, response::Redirect, uri, Build, Rocket};
454-
/// fn redir_missing<'p, 'h>(p: Option<FileResponse<'p, 'h>>)
474+
/// # use rocket::{fs::{FileServer, FileResponse}, response::Redirect, uri, Build, Rocket, Request};
475+
/// fn redir_missing<'p, 'h>(p: Option<FileResponse<'p, 'h>>, _req: &Request<'_>)
455476
/// -> Option<FileResponse<'p, 'h>>
456477
/// {
457478
/// match p {
@@ -485,12 +506,12 @@ impl FileServer {
485506
/// .mount(
486507
/// "/",
487508
/// FileServer::from("static")
488-
/// .filter_file(|f| f.path.file_name() != Some("hidden".as_ref()))
509+
/// .filter_file(|f, _r| f.path.file_name() != Some("hidden".as_ref()))
489510
/// )
490511
/// # }
491512
/// ```
492513
pub fn filter_file<F>(self, f: F) -> Self
493-
where F: Fn(&File<'_, '_>) -> bool + Send + Sync + 'static
514+
where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static
494515
{
495516
self.and_rewrite(FilterFile(f))
496517
}
@@ -506,12 +527,12 @@ impl FileServer {
506527
/// rocket::build()
507528
/// .mount(
508529
/// "/",
509-
/// FileServer::from("static").map_file(|f| f.map_path(|p| p.join("hidden")).into())
530+
/// FileServer::from("static").map_file(|f, _r| f.map_path(|p| p.join("hidden")).into())
510531
/// )
511532
/// # }
512533
/// ```
513534
pub fn map_file<F>(self, f: F) -> Self
514-
where F: for<'r, 'h> Fn(File<'r, 'h>) -> FileResponse<'r, 'h> + Send + Sync + 'static
535+
where F: for<'r, 'h> Fn(File<'r, 'h>, &Request<'_>) -> FileResponse<'r, 'h> + Send + Sync + 'static
515536
{
516537
self.and_rewrite(MapFile(f))
517538
}
@@ -528,6 +549,8 @@ impl From<FileServer> for Vec<Route> {
528549
}
529550
}
530551

552+
553+
531554
#[crate::async_trait]
532555
impl Handler for FileServer {
533556
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
@@ -536,27 +559,20 @@ impl Handler for FileServer {
536559
.and_then(|segments| segments.to_path_buf(true).ok());
537560
let mut response = path.as_ref().map(|p| FileResponse::File(File {
538561
path: Cow::Borrowed(p),
539-
full_uri: req.uri(),
540562
headers: HeaderMap::new(),
541563
}));
542564

543565
for rewrite in &self.rewrites {
544-
response = rewrite.rewrite(response);
566+
response = rewrite.rewrite(response, req);
545567
}
568+
546569
match response {
547-
Some(FileResponse::File(File { path, headers, .. })) if path.is_file() => {
548-
NamedFile::open(path).await.respond_to(req).map(|mut r| {
549-
for header in headers {
550-
r.adjoin_raw_header(header.name.as_str().to_owned(), header.value);
551-
}
552-
r
553-
}).or_forward((data, Status::NotFound))
554-
},
570+
Some(FileResponse::File(file)) => file.respond_to(req, data).await,
555571
Some(FileResponse::Redirect(r)) => {
556572
r.respond_to(req)
557573
.or_forward((data, Status::InternalServerError))
558574
},
559-
_ => Outcome::forward(data, Status::NotFound),
575+
None => Outcome::forward(data, Status::NotFound),
560576
}
561577
}
562578
}

core/lib/tests/file_server.rs

+62-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::path::Path;
44
use rocket::{Rocket, Route, Build};
55
use rocket::http::Status;
66
use rocket::local::blocking::Client;
7-
use rocket::fs::{dir_root, filter_dotfiles, index, normalize_dirs, relative, FileServer};
7+
use rocket::fs::{
8+
dir_root, file_root, filter_dotfiles, index, missing_root, normalize_dirs, relative, FileServer
9+
};
810

911
fn static_root() -> &'static Path {
1012
Path::new(relative!("/tests/static"))
@@ -53,6 +55,20 @@ fn rocket() -> Rocket<Build> {
5355
.map_file(normalize_dirs)
5456
.map_file(index("index.html"))
5557
)
58+
.mount(
59+
"/index_file",
60+
FileServer::empty()
61+
.filter_file(filter_dotfiles)
62+
.map_file(file_root(root.join("other/hello.txt")))
63+
64+
)
65+
.mount(
66+
"/missing_root",
67+
FileServer::empty()
68+
.filter_file(filter_dotfiles)
69+
.map_file(missing_root(root.join("no_file")))
70+
71+
)
5672
}
5773

5874
static REGULAR_FILES: &[&str] = &[
@@ -72,13 +88,13 @@ static INDEXED_DIRECTORIES: &[&str] = &[
7288
"inner/",
7389
];
7490

75-
fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) {
91+
fn assert_file_matches(client: &Client, prefix: &str, path: &str, disk_path: Option<&str>) {
7692
let full_path = format!("/{}/{}", prefix, path);
7793
let response = client.get(full_path).dispatch();
78-
if exists {
94+
if let Some(disk_path) = disk_path {
7995
assert_eq!(response.status(), Status::Ok);
8096

81-
let mut path = static_root().join(path);
97+
let mut path = static_root().join(disk_path);
8298
if path.is_dir() {
8399
path = path.join("index.html");
84100
}
@@ -92,6 +108,14 @@ fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) {
92108
}
93109
}
94110

111+
fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) {
112+
if exists {
113+
assert_file_matches(client, prefix, path, Some(path))
114+
} else {
115+
assert_file_matches(client, prefix, path, None)
116+
}
117+
}
118+
95119
fn assert_all(client: &Client, prefix: &str, paths: &[&str], exist: bool) {
96120
for path in paths.iter() {
97121
assert_file(client, prefix, path, exist);
@@ -134,6 +158,22 @@ fn test_static_all() {
134158
assert_all(&client, "both", INDEXED_DIRECTORIES, true);
135159
}
136160

161+
#[test]
162+
fn test_alt_roots() {
163+
let client = Client::debug(rocket()).expect("valid rocket");
164+
assert_file(&client, "missing_root", "", false);
165+
assert_file_matches(&client, "index_file", "", Some("other/hello.txt"));
166+
}
167+
168+
#[test]
169+
fn test_allow_special_dotpaths() {
170+
let client = Client::debug(rocket()).expect("valid rocket");
171+
assert_file_matches(&client, "no_index", "./index.html", Some("index.html"));
172+
assert_file_matches(&client, "no_index", "foo/../index.html", Some("index.html"));
173+
assert_file_matches(&client, "no_index", "inner/./index.html", Some("inner/index.html"));
174+
assert_file_matches(&client, "no_index", "../index.html", Some("index.html"));
175+
}
176+
137177
#[test]
138178
fn test_ranking() {
139179
let root = static_root();
@@ -223,3 +263,21 @@ fn test_redirection() {
223263
assert_eq!(response.status(), Status::PermanentRedirect);
224264
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/"));
225265
}
266+
267+
#[test]
268+
#[should_panic]
269+
fn test_panic_on_missing_file() {
270+
let _ = file_root(static_root().join("missing_file"));
271+
}
272+
273+
#[test]
274+
#[should_panic]
275+
fn test_panic_on_missing_dir() {
276+
let _ = dir_root(static_root().join("missing_dir"));
277+
}
278+
279+
#[test]
280+
#[should_panic]
281+
fn test_panic_on_file_not_dir() {
282+
let _ = dir_root(static_root().join("index.html"));
283+
}

0 commit comments

Comments
 (0)