tower_http/services/fs/serve_dir/mod.rs
1use self::future::ResponseFuture;
2use crate::{
3 body::UnsyncBoxBody,
4 content_encoding::{encodings, SupportedEncodings},
5 set_status::SetStatus,
6};
7use bytes::Bytes;
8use futures_util::FutureExt;
9use http::{header, HeaderValue, Method, Request, Response, StatusCode};
10use http_body_util::{BodyExt, Empty};
11use percent_encoding::percent_decode;
12use std::{
13 convert::Infallible,
14 io,
15 path::{Component, Path, PathBuf},
16 task::{Context, Poll},
17};
18use tower_service::Service;
19
20mod backend;
21pub(crate) mod future;
22mod headers;
23mod open_file;
24
25#[cfg(test)]
26mod tests;
27
28pub use self::backend::{Backend, File, Metadata, TokioBackend, TokioFile};
29
30// default capacity 64KiB
31const DEFAULT_CAPACITY: usize = 65536;
32
33/// Service that serves files from a given directory and all its sub directories.
34///
35/// The `Content-Type` will be guessed from the file extension.
36///
37/// An empty response with status `404 Not Found` will be returned if:
38///
39/// - The file doesn't exist
40/// - Any segment of the path contains `..`
41/// - Any segment of the path contains a backslash
42/// - On unix, any segment of the path referenced as directory is actually an
43/// existing file (`/file.html/something`)
44/// - We don't have necessary permissions to read the file
45///
46/// # Example
47///
48/// ```
49/// use tower_http::services::ServeDir;
50///
51/// // This will serve files in the "assets" directory and
52/// // its subdirectories
53/// let service = ServeDir::new("assets");
54/// ```
55#[derive(Clone, Debug)]
56pub struct ServeDir<F = DefaultServeDirFallback, B = TokioBackend> {
57 base: PathBuf,
58 redirect_path_prefix: String,
59 buf_chunk_size: usize,
60 ignore_multi_range_requests: bool,
61 precompressed_variants: Option<PrecompressedVariants>,
62 // This is used to specialize implementation for
63 // single files
64 variant: ServeVariant,
65 fallback: Option<F>,
66 call_fallback_on_method_not_allowed: bool,
67 backend: B,
68}
69
70impl ServeDir<DefaultServeDirFallback> {
71 /// Create a new [`ServeDir`].
72 pub fn new<P>(path: P) -> Self
73 where
74 P: AsRef<Path>,
75 {
76 let mut base = PathBuf::from(".");
77 base.push(path.as_ref());
78
79 Self {
80 base,
81 redirect_path_prefix: String::new(),
82 buf_chunk_size: DEFAULT_CAPACITY,
83 ignore_multi_range_requests: false,
84 precompressed_variants: None,
85 variant: ServeVariant::Directory {
86 append_index_html_on_directories: true,
87 redirect_to_trailing_slash: true,
88 html_as_default_extension: false,
89 },
90 fallback: None,
91 call_fallback_on_method_not_allowed: false,
92 backend: TokioBackend,
93 }
94 }
95
96 pub(crate) fn new_single_file<P>(path: P, mime: HeaderValue) -> Self
97 where
98 P: AsRef<Path>,
99 {
100 Self {
101 base: path.as_ref().to_owned(),
102 redirect_path_prefix: String::new(),
103 buf_chunk_size: DEFAULT_CAPACITY,
104 ignore_multi_range_requests: false,
105 precompressed_variants: None,
106 variant: ServeVariant::SingleFile { mime },
107 fallback: None,
108 call_fallback_on_method_not_allowed: false,
109 backend: TokioBackend,
110 }
111 }
112}
113
114impl<B: Backend> ServeDir<DefaultServeDirFallback, B> {
115 /// Create a new [`ServeDir`] with a custom [`Backend`].
116 ///
117 /// This allows serving files from sources other than the local filesystem.
118 pub fn with_backend<P>(path: P, backend: B) -> Self
119 where
120 P: AsRef<Path>,
121 {
122 let mut base = PathBuf::from(".");
123 base.push(path.as_ref());
124
125 ServeDir {
126 base,
127 buf_chunk_size: DEFAULT_CAPACITY,
128 ignore_multi_range_requests: false,
129 precompressed_variants: None,
130 variant: ServeVariant::Directory {
131 append_index_html_on_directories: true,
132 redirect_to_trailing_slash: true,
133 html_as_default_extension: false,
134 },
135 fallback: None,
136 call_fallback_on_method_not_allowed: false,
137 redirect_path_prefix: String::new(),
138 backend,
139 }
140 }
141}
142
143impl<F, B: Backend> ServeDir<F, B> {
144 /// If the requested path is a directory append `index.html`.
145 ///
146 /// This is useful for static sites.
147 ///
148 /// Defaults to `true`.
149 pub fn append_index_html_on_directories(mut self, append: bool) -> Self {
150 match &mut self.variant {
151 ServeVariant::Directory {
152 append_index_html_on_directories,
153 ..
154 } => {
155 *append_index_html_on_directories = append;
156 self
157 }
158 ServeVariant::SingleFile { mime: _ } => self,
159 }
160 }
161
162 /// Whether to redirect directory requests without a trailing slash.
163 ///
164 /// When enabled, a request to `/dir` redirects to `/dir/`. When disabled,
165 /// `/dir/index.html` is served directly at `/dir` if
166 /// [`append_index_html_on_directories`](Self::append_index_html_on_directories) is enabled.
167 ///
168 /// Defaults to `true`.
169 pub fn redirect_to_trailing_slash(mut self, redirect: bool) -> Self {
170 match &mut self.variant {
171 ServeVariant::Directory {
172 redirect_to_trailing_slash,
173 ..
174 } => {
175 *redirect_to_trailing_slash = redirect;
176 self
177 }
178 ServeVariant::SingleFile { mime: _ } => self,
179 }
180 }
181
182 /// If the requested path doesn't specify a file extension, append `.html`.
183 ///
184 /// Defaults to `false`.
185 pub fn html_as_default_extension(mut self, append: bool) -> Self {
186 match &mut self.variant {
187 ServeVariant::Directory {
188 html_as_default_extension,
189 ..
190 } => {
191 *html_as_default_extension = append;
192 self
193 }
194 ServeVariant::SingleFile { mime: _ } => self,
195 }
196 }
197
198 /// Sets a path to be prepended when performing a trailing slash redirect.
199 ///
200 /// This is useful when you want to serve the files at another location than `/`, for example
201 /// when you are using multiple services and want this instance to handle `/static/<path>`.
202 /// In that example, you should pass in `/static` so that a trailing slash redirect does not
203 /// redirect to `/<path>/` but instead to `/static/<path>/`
204 ///
205 /// The default is the empty string.
206 pub fn redirect_path_prefix(mut self, prefix: impl Into<String>) -> Self {
207 self.redirect_path_prefix = prefix.into();
208 self
209 }
210
211 /// Set a specific read buffer chunk size.
212 ///
213 /// The default capacity is 64kb.
214 pub fn with_buf_chunk_size(mut self, chunk_size: usize) -> Self {
215 self.buf_chunk_size = chunk_size;
216 self
217 }
218
219 /// Configure whether syntactically valid multi-range requests should be ignored.
220 ///
221 /// When enabled, a request containing multiple byte ranges is served as a normal full
222 /// response with status `200 OK`, as if the `Range` header were absent. This check happens
223 /// before semantic range validation, so overlapping, reversed, or otherwise unsatisfiable
224 /// multi-range requests are also ignored. Malformed range headers and unsatisfiable
225 /// single-range requests still result in `416 Range Not Satisfiable`.
226 ///
227 /// Defaults to `false`.
228 pub fn ignore_multi_range_requests(mut self, ignore: bool) -> Self {
229 self.ignore_multi_range_requests = ignore;
230 self
231 }
232
233 /// Informs the service that it should also look for a precompressed gzip
234 /// version of _any_ file in the directory.
235 ///
236 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
237 /// a client with an `Accept-Encoding` header that allows the gzip encoding
238 /// will receive the file `dir/foo.txt.gz` instead of `dir/foo.txt`.
239 /// If the precompressed file is not available, or the client doesn't support it,
240 /// the uncompressed version will be served instead.
241 /// Both the precompressed version and the uncompressed version are expected
242 /// to be present in the directory. Different precompressed variants can be combined.
243 pub fn precompressed_gzip(mut self) -> Self {
244 self.precompressed_variants
245 .get_or_insert(Default::default())
246 .gzip = true;
247 self
248 }
249
250 /// Informs the service that it should also look for a precompressed brotli
251 /// version of _any_ file in the directory.
252 ///
253 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
254 /// a client with an `Accept-Encoding` header that allows the brotli encoding
255 /// will receive the file `dir/foo.txt.br` instead of `dir/foo.txt`.
256 /// If the precompressed file is not available, or the client doesn't support it,
257 /// the uncompressed version will be served instead.
258 /// Both the precompressed version and the uncompressed version are expected
259 /// to be present in the directory. Different precompressed variants can be combined.
260 pub fn precompressed_br(mut self) -> Self {
261 self.precompressed_variants
262 .get_or_insert(Default::default())
263 .br = true;
264 self
265 }
266
267 /// Informs the service that it should also look for a precompressed deflate
268 /// version of _any_ file in the directory.
269 ///
270 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
271 /// a client with an `Accept-Encoding` header that allows the deflate encoding
272 /// will receive the file `dir/foo.txt.zz` instead of `dir/foo.txt`.
273 /// If the precompressed file is not available, or the client doesn't support it,
274 /// the uncompressed version will be served instead.
275 /// Both the precompressed version and the uncompressed version are expected
276 /// to be present in the directory. Different precompressed variants can be combined.
277 pub fn precompressed_deflate(mut self) -> Self {
278 self.precompressed_variants
279 .get_or_insert(Default::default())
280 .deflate = true;
281 self
282 }
283
284 /// Informs the service that it should also look for a precompressed zstd
285 /// version of _any_ file in the directory.
286 ///
287 /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
288 /// a client with an `Accept-Encoding` header that allows the zstd encoding
289 /// will receive the file `dir/foo.txt.zst` instead of `dir/foo.txt`.
290 /// If the precompressed file is not available, or the client doesn't support it,
291 /// the uncompressed version will be served instead.
292 /// Both the precompressed version and the uncompressed version are expected
293 /// to be present in the directory. Different precompressed variants can be combined.
294 pub fn precompressed_zstd(mut self) -> Self {
295 self.precompressed_variants
296 .get_or_insert(Default::default())
297 .zstd = true;
298 self
299 }
300
301 /// Set the fallback service.
302 ///
303 /// This service will be called if there is no file at the path of the request.
304 ///
305 /// The status code returned by the fallback will not be altered. Use
306 /// [`ServeDir::not_found_service`] to set a fallback and always respond with `404 Not Found`.
307 ///
308 /// # Example
309 ///
310 /// This can be used to respond with a different file:
311 ///
312 /// ```rust
313 /// use tower_http::services::{ServeDir, ServeFile};
314 ///
315 /// let service = ServeDir::new("assets")
316 /// // respond with `not_found.html` for missing files
317 /// .fallback(ServeFile::new("assets/not_found.html"));
318 /// ```
319 pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2, B> {
320 ServeDir {
321 redirect_path_prefix: self.redirect_path_prefix,
322 base: self.base,
323 buf_chunk_size: self.buf_chunk_size,
324 ignore_multi_range_requests: self.ignore_multi_range_requests,
325 precompressed_variants: self.precompressed_variants,
326 variant: self.variant,
327 fallback: Some(new_fallback),
328 call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
329 backend: self.backend,
330 }
331 }
332
333 /// Set the fallback service and override the fallback's status code to `404 Not Found`.
334 ///
335 /// This service will be called if there is no file at the path of the request.
336 ///
337 /// # Example
338 ///
339 /// This can be used to respond with a different file:
340 ///
341 /// ```rust
342 /// use tower_http::services::{ServeDir, ServeFile};
343 ///
344 /// let service = ServeDir::new("assets")
345 /// // respond with `404 Not Found` and the contents of `not_found.html` for missing files
346 /// .not_found_service(ServeFile::new("assets/not_found.html"));
347 /// ```
348 ///
349 /// Setups like this are often found in single page applications.
350 pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>, B> {
351 self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
352 }
353
354 /// Customize whether or not to call the fallback for requests that aren't `GET` or `HEAD`.
355 ///
356 /// Defaults to not calling the fallback and instead returning `405 Method Not Allowed`.
357 pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
358 self.call_fallback_on_method_not_allowed = call_fallback;
359 self
360 }
361
362 /// Call the service and get a future that contains any `std::io::Error` that might have
363 /// happened.
364 ///
365 /// This only returns I/O errors encountered while serving the request. Invalid request paths
366 /// and unsupported methods are represented as responses. When a fallback is configured,
367 /// errors that the default service would convert to `404 Not Found` call the fallback instead
368 /// of being returned.
369 ///
370 /// By default `<ServeDir as Service<_>>::call` will handle IO errors and convert them into
371 /// responses. It does that by converting [`std::io::ErrorKind::NotFound`] and
372 /// [`std::io::ErrorKind::PermissionDenied`] to `404 Not Found`. On Unix, errors indicating
373 /// that a path component is not a directory are also converted to `404 Not Found`. Any other
374 /// error is converted to `500 Internal Server Error` and will also be logged with `tracing` in
375 /// case the `tracing` crate feature is enabled.
376 ///
377 /// If you want to manually control how the error response is generated you can make a new
378 /// service that wraps a `ServeDir` and calls `try_call` instead of `call`.
379 ///
380 /// # Example
381 ///
382 /// ```
383 /// use tower_http::services::ServeDir;
384 /// use std::{io, convert::Infallible};
385 /// use http::{Request, Response, StatusCode};
386 /// use http_body::Body as _;
387 /// use http_body_util::{Full, BodyExt, combinators::UnsyncBoxBody};
388 /// use bytes::Bytes;
389 /// use tower::{service_fn, ServiceExt, BoxError};
390 ///
391 /// async fn serve_dir(
392 /// request: Request<Full<Bytes>>
393 /// ) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, Infallible> {
394 /// let mut service = ServeDir::new("assets");
395 ///
396 /// // You only need to worry about backpressure, and thus call `ServiceExt::ready`, if
397 /// // you are adding a fallback to `ServeDir` that cares about backpressure.
398 /// //
399 /// // Its shown here for demonstration but you can do `service.try_call(request)`
400 /// // otherwise
401 /// let ready_service = match ServiceExt::<Request<Full<Bytes>>>::ready(&mut service).await {
402 /// Ok(ready_service) => ready_service,
403 /// Err(infallible) => match infallible {},
404 /// };
405 ///
406 /// match ready_service.try_call(request).await {
407 /// Ok(response) => {
408 /// Ok(response.map(|body| body.map_err(Into::into).boxed_unsync()))
409 /// }
410 /// Err(err) => {
411 /// let not_found = matches!(
412 /// err.kind(),
413 /// io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
414 /// ) || cfg!(unix) && err.raw_os_error() == Some(20);
415 /// let (status, message) = if not_found {
416 /// (StatusCode::NOT_FOUND, "Not found")
417 /// } else {
418 /// (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
419 /// };
420 /// let body = Full::from(message)
421 /// .map_err(Into::into)
422 /// .boxed_unsync();
423 /// let response = Response::builder()
424 /// .status(status)
425 /// .body(body)
426 /// .unwrap();
427 /// Ok(response)
428 /// }
429 /// }
430 /// }
431 /// ```
432 pub fn try_call<ReqBody, FResBody>(
433 &mut self,
434 req: Request<ReqBody>,
435 ) -> ResponseFuture<ReqBody, F>
436 where
437 F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
438 F::Future: Send + 'static,
439 FResBody: http_body::Body<Data = Bytes> + Send + 'static,
440 FResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
441 {
442 if req.method() != Method::GET && req.method() != Method::HEAD {
443 if self.call_fallback_on_method_not_allowed {
444 if let Some(fallback) = &mut self.fallback {
445 return ResponseFuture {
446 inner: future::call_fallback(fallback, req),
447 };
448 }
449 }
450
451 return ResponseFuture::method_not_allowed();
452 }
453
454 // `ServeDir` doesn't care about the request body but the fallback might. So move out the
455 // body and pass it to the fallback, leaving an empty body in its place
456 //
457 // this is necessary because we cannot clone bodies
458 let (mut parts, body) = req.into_parts();
459 // same goes for extensions
460 let extensions = std::mem::take(&mut parts.extensions);
461 let req = Request::from_parts(parts, Empty::<Bytes>::new());
462
463 let fallback_and_request = self.fallback.as_mut().map(|fallback| {
464 let mut fallback_req = Request::new(body);
465 *fallback_req.method_mut() = req.method().clone();
466 *fallback_req.uri_mut() = req.uri().clone();
467 *fallback_req.headers_mut() = req.headers().clone();
468 *fallback_req.extensions_mut() = extensions;
469
470 // get the ready fallback and leave a non-ready clone in its place
471 let clone = fallback.clone();
472 let fallback = std::mem::replace(fallback, clone);
473
474 (fallback, fallback_req)
475 });
476
477 let path_to_file = match self
478 .variant
479 .build_and_validate_path(&self.base, req.uri().path())
480 {
481 Some(path_to_file) => path_to_file,
482 None => {
483 return ResponseFuture::invalid_path(fallback_and_request);
484 }
485 };
486
487 let redirect_path_prefix = self.redirect_path_prefix.clone();
488
489 let buf_chunk_size = self.buf_chunk_size;
490 let ignore_multi_range_requests = self.ignore_multi_range_requests;
491 let range_header = req
492 .headers()
493 .get(header::RANGE)
494 .and_then(|value| value.to_str().ok())
495 .map(|s| s.to_owned());
496
497 let precompression_configured = self.precompressed_variants.is_some();
498 let negotiated_encodings: Vec<_> = encodings(
499 req.headers(),
500 self.precompressed_variants.unwrap_or_default(),
501 )
502 .collect();
503
504 let open_file_future = Box::pin(open_file::open_file(open_file::OpenFileRequest {
505 variant: self.variant.clone(),
506 redirect_path_prefix,
507 path_to_file,
508 req,
509 negotiated_encodings,
510 range_header,
511 buf_chunk_size,
512 ignore_multi_range_requests,
513 precompression_configured,
514 backend: self.backend.clone(),
515 }));
516
517 ResponseFuture::open_file_future(open_file_future, fallback_and_request)
518 }
519}
520
521impl<ReqBody, F, FResBody, B> Service<Request<ReqBody>> for ServeDir<F, B>
522where
523 F: Service<Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
524 F::Future: Send + 'static,
525 FResBody: http_body::Body<Data = Bytes> + Send + 'static,
526 FResBody::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
527 B: Backend,
528{
529 type Response = Response<ResponseBody>;
530 type Error = Infallible;
531 type Future = InfallibleResponseFuture<ReqBody, F>;
532
533 #[inline]
534 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
535 if let Some(fallback) = &mut self.fallback {
536 fallback.poll_ready(cx)
537 } else {
538 Poll::Ready(Ok(()))
539 }
540 }
541
542 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
543 let future = self
544 .try_call(req)
545 .map(|result: Result<_, _>| -> Result<_, Infallible> {
546 let response = result.unwrap_or_else(|err| {
547 let status = if should_return_not_found(&err) {
548 StatusCode::NOT_FOUND
549 } else {
550 #[cfg(feature = "tracing")]
551 tracing::error!(error = %err, "Failed to read file");
552
553 StatusCode::INTERNAL_SERVER_ERROR
554 };
555
556 let body = ResponseBody::new(UnsyncBoxBody::from_inner(
557 Empty::new().map_err(|err| match err {}).boxed_unsync(),
558 ));
559 Response::builder().status(status).body(body).unwrap()
560 });
561 Ok(response)
562 } as _);
563
564 InfallibleResponseFuture::new(future)
565 }
566}
567
568fn should_return_not_found(err: &io::Error) -> bool {
569 #[cfg(unix)]
570 // 20 = libc::ENOTDIR => "not a directory".
571 // When `io_error_more` lands, this can be changed
572 // to checking for `io::ErrorKind::NotADirectory`.
573 // https://github.com/rust-lang/rust/issues/86442
574 let error_is_not_a_directory = err.raw_os_error() == Some(20);
575 #[cfg(not(unix))]
576 let error_is_not_a_directory = false;
577
578 matches!(
579 err.kind(),
580 io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
581 ) || error_is_not_a_directory
582}
583
584opaque_future! {
585 /// Response future of [`ServeDir`].
586 pub type InfallibleResponseFuture<ReqBody, F> =
587 futures_util::future::Map<
588 ResponseFuture<ReqBody, F>,
589 fn(Result<Response<ResponseBody>, io::Error>) -> Result<Response<ResponseBody>, Infallible>,
590 >;
591}
592
593// Allow the ServeDir service to be used in the ServeFile service
594// with almost no overhead
595#[derive(Clone, Debug)]
596enum ServeVariant {
597 Directory {
598 append_index_html_on_directories: bool,
599 redirect_to_trailing_slash: bool,
600 html_as_default_extension: bool,
601 },
602 SingleFile {
603 mime: HeaderValue,
604 },
605}
606
607impl ServeVariant {
608 fn build_and_validate_path(&self, base_path: &Path, requested_path: &str) -> Option<PathBuf> {
609 match self {
610 ServeVariant::Directory {
611 append_index_html_on_directories: _,
612 redirect_to_trailing_slash: _,
613 html_as_default_extension: _,
614 } => {
615 let path = requested_path.trim_start_matches('/');
616
617 let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
618 let path_decoded = Path::new(&*path_decoded);
619
620 let mut path_to_file = base_path.to_path_buf();
621 for component in path_decoded.components() {
622 match component {
623 Component::Normal(comp) => {
624 // protect against paths like `/foo/c:/bar/baz` (#204)
625 if Path::new(&comp)
626 .components()
627 .all(|c| matches!(c, Component::Normal(_)))
628 {
629 #[cfg(windows)]
630 {
631 use std::os::windows::ffi::OsStrExt;
632 if is_reserved_dos_name(|| comp.encode_wide()) {
633 return None;
634 }
635 }
636
637 path_to_file.push(comp)
638 } else {
639 return None;
640 }
641 }
642 Component::CurDir => {}
643 Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
644 return None;
645 }
646 }
647 }
648 Some(path_to_file)
649 }
650 ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
651 }
652 }
653}
654
655/// Check whether a component name matches a reserved Windows DOS device name.
656/// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
657///
658/// We explicitly check for Unicode superscript characters `¹` (0x00B9), `²` (0x00B2),
659/// and `³` (0x00B3) because older character tables (ISO/IEC 8859-1) define these values,
660/// which legacy Win32 file parsing resolves natively as valid port numbers (0..9).
661///
662/// This uses an iterator and stack array to avoid allocating. A closure is used because it
663/// iterates the characters twice. The closure must return the same iterator each time it is
664/// called.
665#[cfg(any(windows, test))]
666fn is_reserved_dos_name<F, I>(mut get_iter: F) -> bool
667where
668 F: FnMut() -> I,
669 I: Iterator<Item = u16>,
670{
671 const CON: [u16; 3] = [b'C' as u16, b'O' as u16, b'N' as u16];
672 const PRN: [u16; 3] = [b'P' as u16, b'R' as u16, b'N' as u16];
673 const AUX: [u16; 3] = [b'A' as u16, b'U' as u16, b'X' as u16];
674 const NUL: [u16; 3] = [b'N' as u16, b'U' as u16, b'L' as u16];
675 const CONIN: [u16; 6] = [
676 b'C' as u16,
677 b'O' as u16,
678 b'N' as u16,
679 b'I' as u16,
680 b'N' as u16,
681 b'$' as u16,
682 ];
683 const CONOUT: [u16; 7] = [
684 b'C' as u16,
685 b'O' as u16,
686 b'N' as u16,
687 b'O' as u16,
688 b'U' as u16,
689 b'T' as u16,
690 b'$' as u16,
691 ];
692
693 const COM: [u16; 3] = [b'C' as u16, b'O' as u16, b'M' as u16];
694 const LPT: [u16; 3] = [b'L' as u16, b'P' as u16, b'T' as u16];
695
696 const ZERO: u16 = b'0' as u16;
697 const NINE: u16 = b'9' as u16;
698 const SUPERSCRIPT_ONE: u16 = 0x00B9;
699 const SUPERSCRIPT_TWO: u16 = 0x00B2;
700 const SUPERSCRIPT_THREE: u16 = 0x00B3;
701
702 fn is_whitespace(c: u16) -> bool {
703 c <= 0x7F && ((c as u8).is_ascii_whitespace() || c == 0x000B)
704 }
705
706 // In a first pass over the string, obtain the length of the basename.
707 let trimmed_len = get_iter()
708 .enumerate()
709 // We want the base name, so stop at '.' or ':' characters.
710 .take_while(|&(_idx, c)| c != b'.' as u16 && c != b':' as u16)
711 // We want to trim whitespace from the end, so ignore whitespace chars.
712 .filter(|&(_idx, c)| !is_whitespace(c))
713 // Get the last non-whitespace char before the first '.'/':' character.
714 .last()
715 // Convert index of that char into length of string.
716 .map(|(idx, _)| idx + 1)
717 .unwrap_or(0);
718
719 // If the trimmed base name is longer than 7, it cannot be a reserved name.
720 if trimmed_len > 7 {
721 return false;
722 }
723
724 // At this point, we can store the string in an array, which is more convenient to work with.
725 let mut buf = [0u16; 7];
726 get_iter()
727 .take(trimmed_len)
728 .enumerate()
729 .for_each(|(i, c)| buf[i] = c);
730
731 for b in &mut buf {
732 if *b <= 0x7F {
733 *b = (*b as u8).to_ascii_uppercase() as u16;
734 }
735 if *b == SUPERSCRIPT_ONE {
736 *b = b'1' as u16;
737 }
738 if *b == SUPERSCRIPT_TWO {
739 *b = b'2' as u16;
740 }
741 if *b == SUPERSCRIPT_THREE {
742 *b = b'3' as u16;
743 }
744 }
745 let name = &buf[..trimmed_len];
746
747 // Check basic fixed-length strings
748 if name == CON || name == PRN || name == AUX || name == NUL || name == CONIN || name == CONOUT {
749 return true;
750 }
751
752 // COMx / LPTx
753 if name.len() == 4 {
754 let prefix = &name[..3];
755 let suffix = name[3];
756
757 if (prefix == COM || prefix == LPT) && matches!(suffix, ZERO..=NINE) {
758 return true;
759 }
760 }
761
762 false
763}
764
765opaque_body! {
766 /// Response body for [`ServeDir`] and [`ServeFile`][super::ServeFile].
767 #[derive(Default)]
768 pub type ResponseBody = UnsyncBoxBody<Bytes, io::Error>;
769}
770
771impl From<ResponseBody> for UnsyncBoxBody<Bytes, io::Error> {
772 fn from(body: ResponseBody) -> Self {
773 body.inner
774 }
775}
776
777/// The default fallback service used with [`ServeDir`].
778#[derive(Debug, Clone, Copy)]
779pub struct DefaultServeDirFallback(Infallible);
780
781impl<ReqBody> Service<Request<ReqBody>> for DefaultServeDirFallback
782where
783 ReqBody: Send + 'static,
784{
785 type Response = Response<ResponseBody>;
786 type Error = Infallible;
787 type Future = InfallibleResponseFuture<ReqBody, Self>;
788
789 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
790 match self.0 {}
791 }
792
793 fn call(&mut self, _req: Request<ReqBody>) -> Self::Future {
794 match self.0 {}
795 }
796}
797
798#[derive(Clone, Copy, Debug, Default)]
799struct PrecompressedVariants {
800 gzip: bool,
801 deflate: bool,
802 br: bool,
803 zstd: bool,
804}
805
806impl SupportedEncodings for PrecompressedVariants {
807 fn gzip(&self) -> bool {
808 self.gzip
809 }
810
811 fn deflate(&self) -> bool {
812 self.deflate
813 }
814
815 fn br(&self) -> bool {
816 self.br
817 }
818
819 fn zstd(&self) -> bool {
820 self.zstd
821 }
822}