Skip to main content

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