Skip to main content

actix_files/
lib.rs

1//! Static file serving for Actix Web.
2//!
3//! Provides a non-blocking service for serving static files from disk.
4//!
5//! # Examples
6//! ```
7//! use actix_web::App;
8//! use actix_files::Files;
9//!
10//! let app = App::new()
11//!     .service(Files::new("/static", ".").prefer_utf8(true));
12//! ```
13
14#![warn(missing_docs, missing_debug_implementations)]
15#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
16#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
17#![cfg_attr(docsrs, feature(doc_cfg))]
18
19use std::path::Path;
20
21use actix_service::boxed::{BoxService, BoxServiceFactory};
22use actix_web::{
23    dev::{RequestHead, ServiceRequest, ServiceResponse},
24    error::Error,
25    http::header::DispositionType,
26};
27use mime_guess::from_ext;
28
29mod chunked;
30mod directory;
31mod encoding;
32mod error;
33mod files;
34mod named;
35mod path_buf;
36mod range;
37mod service;
38
39pub use self::{
40    chunked::ChunkedReadFile,
41    directory::Directory,
42    error::UriSegmentError,
43    files::{Files, FilesDirs},
44    named::NamedFile,
45    path_buf::PathBufWrap,
46    range::HttpRange,
47    service::FilesService,
48};
49use self::{
50    directory::{directory_listing, DirectoryRenderer},
51    error::FilesError,
52};
53
54type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
55type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
56
57/// Return the MIME type associated with a filename extension (case-insensitive).
58/// If `ext` is empty or no associated type for the extension was found, returns
59/// the type `application/octet-stream`.
60#[inline]
61pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
62    from_ext(ext).first_or_octet_stream()
63}
64
65type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
66
67type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
68
69#[cfg(test)]
70mod tests {
71    use std::{
72        ffi::OsString,
73        fmt::Write as _,
74        fs::{self},
75        ops::Add,
76        path::PathBuf,
77        time::{Duration, SystemTime},
78    };
79
80    use actix_http::header::HttpDate;
81    use actix_web::{
82        body,
83        dev::ServiceFactory,
84        guard,
85        http::{
86            header::{self, ContentDisposition, DispositionParam},
87            Method, StatusCode,
88        },
89        middleware::Compress,
90        test::{self, TestRequest},
91        web::{self, Bytes},
92        App, HttpResponse, Responder,
93    };
94
95    use super::*;
96    use crate::named::File;
97
98    #[actix_web::test]
99    async fn test_file_extension_to_mime() {
100        let m = file_extension_to_mime("");
101        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
102
103        let m = file_extension_to_mime("jpg");
104        assert_eq!(m, mime::IMAGE_JPEG);
105
106        let m = file_extension_to_mime("invalid extension!!");
107        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
108
109        let m = file_extension_to_mime("");
110        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
111    }
112
113    #[actix_rt::test]
114    async fn test_if_none_match_get_and_head() {
115        let file = NamedFile::open("Cargo.toml").unwrap();
116        let etag = file.etag().unwrap().to_string();
117
118        for method in [Method::GET, Method::HEAD] {
119            for value in [etag.as_str(), "*"] {
120                let req = TestRequest::default()
121                    .method(method.clone())
122                    .insert_header((header::IF_NONE_MATCH, value))
123                    .to_http_request();
124                let resp = NamedFile::open("Cargo.toml").unwrap().into_response(&req);
125
126                assert_eq!(
127                    resp.status(),
128                    StatusCode::NOT_MODIFIED,
129                    "{method} with matching If-None-Match {value} should return 304"
130                );
131                assert!(
132                    body::to_bytes(resp.into_body()).await.unwrap().is_empty(),
133                    "{method} with matching If-None-Match {value} should have an empty body"
134                );
135            }
136        }
137    }
138
139    #[actix_rt::test]
140    async fn test_if_none_match_post() {
141        let file = NamedFile::open("Cargo.toml").unwrap();
142        let etag = file.etag().unwrap().to_string();
143
144        for value in [etag.as_str(), "*"] {
145            let req = TestRequest::post()
146                .insert_header((header::IF_NONE_MATCH, value))
147                .to_http_request();
148            let resp = NamedFile::open("Cargo.toml").unwrap().into_response(&req);
149
150            assert_eq!(
151                resp.status(),
152                StatusCode::PRECONDITION_FAILED,
153                "POST with matching If-None-Match {value} should return 412"
154            );
155            assert!(
156                body::to_bytes(resp.into_body()).await.unwrap().is_empty(),
157                "POST with matching If-None-Match {value} should have an empty body"
158            );
159        }
160    }
161
162    #[actix_rt::test]
163    async fn test_if_modified_since_get_and_head() {
164        let file = NamedFile::open("Cargo.toml").unwrap();
165        let since = file.last_modified().unwrap();
166
167        for method in [Method::GET, Method::HEAD] {
168            let req = TestRequest::default()
169                .method(method.clone())
170                .insert_header((header::IF_MODIFIED_SINCE, since))
171                .to_http_request();
172            let resp = NamedFile::open("Cargo.toml").unwrap().into_response(&req);
173
174            assert_eq!(
175                resp.status(),
176                StatusCode::NOT_MODIFIED,
177                "{method} with matching If-Modified-Since should return 304"
178            );
179            assert!(
180                body::to_bytes(resp.into_body()).await.unwrap().is_empty(),
181                "{method} with matching If-Modified-Since should have an empty body"
182            );
183        }
184    }
185
186    #[actix_rt::test]
187    async fn test_if_modified_since_post() {
188        let file = NamedFile::open("Cargo.toml").unwrap();
189        let since = file.last_modified().unwrap();
190
191        let req = TestRequest::post()
192            .insert_header((header::IF_MODIFIED_SINCE, since))
193            .to_http_request();
194        let resp = file.into_response(&req);
195
196        assert_eq!(
197            resp.status(),
198            StatusCode::OK,
199            "POST should ignore If-Modified-Since and return 200"
200        );
201
202        let bytes = body::to_bytes(resp.into_body()).await.unwrap();
203
204        assert_eq!(
205            bytes.as_ref(),
206            fs::read("Cargo.toml").unwrap(),
207            "POST should return the full file despite If-Modified-Since"
208        );
209    }
210
211    #[actix_rt::test]
212    async fn test_if_modified_since_without_if_none_match() {
213        let file = NamedFile::open("Cargo.toml").unwrap();
214        let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
215
216        let req = TestRequest::default()
217            .insert_header((header::IF_MODIFIED_SINCE, since))
218            .to_http_request();
219        let resp = file.respond_to(&req);
220        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
221    }
222
223    #[actix_rt::test]
224    async fn test_if_modified_since_without_if_none_match_same() {
225        let file = NamedFile::open("Cargo.toml").unwrap();
226        let since = file.last_modified().unwrap();
227
228        let req = TestRequest::default()
229            .insert_header((header::IF_MODIFIED_SINCE, since))
230            .to_http_request();
231        let resp = file.respond_to(&req);
232        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
233    }
234
235    #[actix_rt::test]
236    async fn test_if_modified_since_with_if_none_match() {
237        let file = NamedFile::open("Cargo.toml").unwrap();
238        let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
239
240        let req = TestRequest::default()
241            .insert_header((header::IF_NONE_MATCH, "miss_etag"))
242            .insert_header((header::IF_MODIFIED_SINCE, since))
243            .to_http_request();
244        let resp = file.respond_to(&req);
245        assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
246    }
247
248    #[actix_rt::test]
249    async fn test_if_match_overrides_if_unmodified_since() {
250        let file = NamedFile::open("Cargo.toml").unwrap();
251        let etag = file.etag().unwrap();
252        let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
253
254        let req = TestRequest::default()
255            .insert_header((header::IF_MATCH, etag))
256            .insert_header((header::IF_UNMODIFIED_SINCE, since))
257            .to_http_request();
258        let resp = file.respond_to(&req);
259        assert_eq!(resp.status(), StatusCode::OK);
260    }
261
262    #[actix_rt::test]
263    async fn test_if_match_failure_with_if_unmodified_since() {
264        let file = NamedFile::open("Cargo.toml").unwrap();
265        let since = file.last_modified().unwrap();
266
267        let req = TestRequest::default()
268            .insert_header((header::IF_MATCH, "\"nonmatching-etag\""))
269            .insert_header((header::IF_UNMODIFIED_SINCE, since))
270            .to_http_request();
271        let resp = file.respond_to(&req);
272        assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
273    }
274
275    #[actix_rt::test]
276    async fn test_if_unmodified_since() {
277        let file = NamedFile::open("Cargo.toml").unwrap();
278        let since = file.last_modified().unwrap();
279
280        let req = TestRequest::default()
281            .insert_header((header::IF_UNMODIFIED_SINCE, since))
282            .to_http_request();
283        let resp = file.respond_to(&req);
284        assert_eq!(resp.status(), StatusCode::OK);
285    }
286
287    #[actix_rt::test]
288    async fn test_if_unmodified_since_failed() {
289        let file = NamedFile::open("Cargo.toml").unwrap();
290        let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
291
292        let req = TestRequest::default()
293            .insert_header((header::IF_UNMODIFIED_SINCE, since))
294            .to_http_request();
295        let resp = file.respond_to(&req);
296        assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
297    }
298
299    #[actix_rt::test]
300    async fn test_named_file_text() {
301        assert!(NamedFile::open("test--").is_err());
302        let mut file = NamedFile::open("Cargo.toml").unwrap();
303        {
304            file.file();
305            let _f: &File = &file;
306        }
307        {
308            let _f: &mut File = &mut file;
309        }
310
311        let req = TestRequest::default().to_http_request();
312        let resp = file.respond_to(&req);
313        assert_eq!(
314            resp.headers().get(header::CONTENT_TYPE).unwrap(),
315            "text/x-toml"
316        );
317        assert_eq!(
318            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
319            "inline; filename=\"Cargo.toml\""
320        );
321    }
322
323    #[actix_rt::test]
324    async fn test_named_file_content_disposition() {
325        assert!(NamedFile::open("test--").is_err());
326        let mut file = NamedFile::open("Cargo.toml").unwrap();
327        {
328            file.file();
329            let _f: &File = &file;
330        }
331        {
332            let _f: &mut File = &mut file;
333        }
334
335        let req = TestRequest::default().to_http_request();
336        let resp = file.respond_to(&req);
337        assert_eq!(
338            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
339            "inline; filename=\"Cargo.toml\""
340        );
341
342        let file = NamedFile::open("Cargo.toml")
343            .unwrap()
344            .disable_content_disposition();
345        let req = TestRequest::default().to_http_request();
346        let resp = file.respond_to(&req);
347        assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
348    }
349
350    #[actix_rt::test]
351    async fn test_named_file_non_ascii_file_name() {
352        let file = crate::named::File::open("Cargo.toml").unwrap();
353
354        let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
355        {
356            file.file();
357            let _f: &File = &file;
358        }
359        {
360            let _f: &mut File = &mut file;
361        }
362
363        let req = TestRequest::default().to_http_request();
364        let resp = file.respond_to(&req);
365        assert_eq!(
366            resp.headers().get(header::CONTENT_TYPE).unwrap(),
367            "text/x-toml"
368        );
369        assert_eq!(
370            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
371            "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml"
372        );
373    }
374
375    #[actix_rt::test]
376    async fn test_named_file_set_content_type() {
377        let mut file = NamedFile::open("Cargo.toml")
378            .unwrap()
379            .set_content_type(mime::TEXT_XML);
380        {
381            file.file();
382            let _f: &File = &file;
383        }
384        {
385            let _f: &mut File = &mut file;
386        }
387
388        let req = TestRequest::default().to_http_request();
389        let resp = file.respond_to(&req);
390        assert_eq!(
391            resp.headers().get(header::CONTENT_TYPE).unwrap(),
392            "text/xml"
393        );
394        assert_eq!(
395            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
396            "inline; filename=\"Cargo.toml\""
397        );
398    }
399
400    #[actix_rt::test]
401    async fn test_named_file_image() {
402        let mut file = NamedFile::open("tests/test.png").unwrap();
403        {
404            file.file();
405            let _f: &File = &file;
406        }
407        {
408            let _f: &mut File = &mut file;
409        }
410
411        let req = TestRequest::default().to_http_request();
412        let resp = file.respond_to(&req);
413        assert_eq!(
414            resp.headers().get(header::CONTENT_TYPE).unwrap(),
415            "image/png"
416        );
417        assert_eq!(
418            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
419            "inline; filename=\"test.png\""
420        );
421    }
422
423    #[actix_rt::test]
424    async fn test_named_file_javascript() {
425        let file = NamedFile::open("tests/test.js").unwrap();
426
427        let req = TestRequest::default().to_http_request();
428        let resp = file.respond_to(&req);
429        assert_eq!(
430            resp.headers().get(header::CONTENT_TYPE).unwrap(),
431            "text/javascript",
432        );
433        assert_eq!(
434            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
435            "inline; filename=\"test.js\"",
436        );
437    }
438
439    #[actix_rt::test]
440    async fn test_named_file_image_attachment() {
441        let cd = ContentDisposition {
442            disposition: DispositionType::Attachment,
443            parameters: vec![DispositionParam::Filename(String::from("test.png"))],
444        };
445        let mut file = NamedFile::open("tests/test.png")
446            .unwrap()
447            .set_content_disposition(cd);
448        {
449            file.file();
450            let _f: &File = &file;
451        }
452        {
453            let _f: &mut File = &mut file;
454        }
455
456        let req = TestRequest::default().to_http_request();
457        let resp = file.respond_to(&req);
458        assert_eq!(
459            resp.headers().get(header::CONTENT_TYPE).unwrap(),
460            "image/png"
461        );
462        assert_eq!(
463            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
464            "attachment; filename=\"test.png\""
465        );
466    }
467
468    #[actix_rt::test]
469    async fn test_named_file_binary() {
470        let mut file = NamedFile::open("tests/test.binary").unwrap();
471        {
472            file.file();
473            let _f: &File = &file;
474        }
475        {
476            let _f: &mut File = &mut file;
477        }
478
479        let req = TestRequest::default().to_http_request();
480        let resp = file.respond_to(&req);
481        assert_eq!(
482            resp.headers().get(header::CONTENT_TYPE).unwrap(),
483            "application/octet-stream"
484        );
485        assert_eq!(
486            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
487            "attachment; filename=\"test.binary\""
488        );
489    }
490
491    #[allow(deprecated)]
492    #[actix_rt::test]
493    async fn status_code_customize_same_output() {
494        let file1 = NamedFile::open("Cargo.toml")
495            .unwrap()
496            .set_status_code(StatusCode::NOT_FOUND);
497
498        let file2 = NamedFile::open("Cargo.toml")
499            .unwrap()
500            .customize()
501            .with_status(StatusCode::NOT_FOUND);
502
503        let req = TestRequest::default().to_http_request();
504        let res1 = file1.respond_to(&req);
505        let res2 = file2.respond_to(&req);
506
507        assert_eq!(res1.status(), StatusCode::NOT_FOUND);
508        assert_eq!(res2.status(), StatusCode::NOT_FOUND);
509    }
510
511    #[actix_rt::test]
512    async fn test_named_file_status_code_text() {
513        let mut file = NamedFile::open("Cargo.toml").unwrap();
514
515        {
516            file.file();
517            let _f: &File = &file;
518        }
519
520        {
521            let _f: &mut File = &mut file;
522        }
523
524        let file = file.customize().with_status(StatusCode::NOT_FOUND);
525
526        let req = TestRequest::default().to_http_request();
527        let resp = file.respond_to(&req);
528        assert_eq!(
529            resp.headers().get(header::CONTENT_TYPE).unwrap(),
530            "text/x-toml"
531        );
532        assert_eq!(
533            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
534            "inline; filename=\"Cargo.toml\""
535        );
536        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
537    }
538
539    #[actix_rt::test]
540    async fn test_mime_override() {
541        fn all_attachment(_: &mime::Name<'_>) -> DispositionType {
542            DispositionType::Attachment
543        }
544
545        let srv = test::init_service(
546            App::new().service(
547                Files::new("/", ".")
548                    .mime_override(all_attachment)
549                    .index_file("Cargo.toml"),
550            ),
551        )
552        .await;
553
554        let request = TestRequest::get().uri("/").to_request();
555        let response = test::call_service(&srv, request).await;
556        assert_eq!(response.status(), StatusCode::OK);
557
558        let content_disposition = response
559            .headers()
560            .get(header::CONTENT_DISPOSITION)
561            .expect("To have CONTENT_DISPOSITION");
562        let content_disposition = content_disposition
563            .to_str()
564            .expect("Convert CONTENT_DISPOSITION to str");
565        assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\"");
566    }
567
568    #[actix_rt::test]
569    async fn test_named_file_ranges_status_code() {
570        let srv = test::init_service(
571            App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
572        )
573        .await;
574
575        // Valid range header
576        let request = TestRequest::get()
577            .uri("/t%65st/Cargo.toml")
578            .insert_header((header::RANGE, "bytes=10-20"))
579            .to_request();
580        let response = test::call_service(&srv, request).await;
581        assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
582
583        // Invalid range header
584        let request = TestRequest::get()
585            .uri("/t%65st/Cargo.toml")
586            .insert_header((header::RANGE, "bytes=1-0"))
587            .to_request();
588        let response = test::call_service(&srv, request).await;
589
590        assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
591    }
592
593    #[actix_rt::test]
594    async fn test_named_file_if_none_match_before_range() {
595        let req = TestRequest::get().to_http_request();
596        let resp = NamedFile::open("Cargo.toml").unwrap().into_response(&req);
597        let etag = resp.headers().get(header::ETAG).unwrap().clone();
598
599        for range in ["bytes=999999999-", "bytes=0-9"] {
600            let req = TestRequest::get()
601                .insert_header((header::IF_NONE_MATCH, etag.clone()))
602                .insert_header((header::RANGE, range))
603                .to_http_request();
604            let resp = NamedFile::open("Cargo.toml").unwrap().into_response(&req);
605
606            assert_eq!(
607                resp.status(),
608                StatusCode::NOT_MODIFIED,
609                "matching If-None-Match with {range} should return 304"
610            );
611            assert!(
612                !resp.headers().contains_key(header::CONTENT_RANGE),
613                "conditional response with {range} should omit Content-Range"
614            );
615            assert!(
616                body::to_bytes(resp.into_body()).await.unwrap().is_empty(),
617                "conditional response with {range} should have an empty body"
618            );
619        }
620    }
621
622    #[actix_rt::test]
623    async fn test_named_file_if_match_before_range() {
624        for range in ["bytes=999999999-", "bytes=0-9"] {
625            let req = TestRequest::get()
626                .insert_header((header::IF_MATCH, "\"stale\""))
627                .insert_header((header::RANGE, range))
628                .to_http_request();
629            let resp = NamedFile::open("Cargo.toml").unwrap().into_response(&req);
630
631            assert_eq!(
632                resp.status(),
633                StatusCode::PRECONDITION_FAILED,
634                "nonmatching If-Match with {range} should return 412"
635            );
636            assert!(
637                !resp.headers().contains_key(header::CONTENT_RANGE),
638                "conditional response with {range} should omit Content-Range"
639            );
640            assert!(
641                body::to_bytes(resp.into_body()).await.unwrap().is_empty(),
642                "conditional response with {range} should have an empty body"
643            );
644        }
645    }
646
647    #[actix_web::test]
648    async fn byte_range_units_are_case_insensitive() {
649        let file = tempfile::NamedTempFile::new().unwrap();
650        fs::write(file.path(), b"0123456789").unwrap();
651
652        for range in ["Bytes=2-4", "BYTES=2-4", "bYtEs=2-4", "bytes=2-4"] {
653            let req = TestRequest::get()
654                .insert_header((header::RANGE, range))
655                .to_http_request();
656            let response = NamedFile::open(file.path()).unwrap().into_response(&req);
657
658            assert_eq!(
659                response.status(),
660                StatusCode::PARTIAL_CONTENT,
661                "range {range} should produce the expected status"
662            );
663            assert_eq!(
664                response.headers().get(header::CONTENT_RANGE).unwrap(),
665                "bytes 2-4/10"
666            );
667            assert_eq!(
668                body::to_bytes(response.into_body()).await.unwrap(),
669                b"234"[..]
670            );
671        }
672    }
673
674    #[actix_web::test]
675    async fn unknown_range_units_are_ignored() {
676        let file = tempfile::NamedTempFile::new().unwrap();
677        fs::write(file.path(), b"0123456789").unwrap();
678
679        for range in ["items=2-4", "BYTESX=2-4", "items=first", "items=100-200"] {
680            let req = TestRequest::get()
681                .insert_header((header::RANGE, range))
682                .to_http_request();
683            let response = NamedFile::open(file.path()).unwrap().into_response(&req);
684
685            assert_eq!(
686                response.status(),
687                StatusCode::OK,
688                "range {range} should produce the expected status"
689            );
690            assert!(!response.headers().contains_key(header::CONTENT_RANGE));
691            assert_eq!(
692                body::to_bytes(response.into_body()).await.unwrap(),
693                b"0123456789"[..]
694            );
695        }
696    }
697
698    #[actix_rt::test]
699    async fn test_named_file_if_range_etag() {
700        let file = NamedFile::open("Cargo.toml").unwrap();
701        let etag = file.etag().unwrap();
702
703        let req = TestRequest::default()
704            .insert_header((header::RANGE, "bytes=0-9"))
705            .insert_header((header::IF_RANGE, etag.to_string()))
706            .to_http_request();
707
708        let resp = file.respond_to(&req);
709        assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
710
711        // a validator that no longer matches must yield the whole current representation
712        for stale in ["\"stale\"", "W/\"stale\"", "not a validator"] {
713            let file = NamedFile::open("Cargo.toml").unwrap();
714            let content = fs::read("Cargo.toml").unwrap();
715
716            let req = TestRequest::default()
717                .insert_header((header::RANGE, "bytes=0-9"))
718                .insert_header((header::IF_RANGE, stale))
719                .to_http_request();
720
721            let resp = file.respond_to(&req);
722            assert_eq!(resp.status(), StatusCode::OK, "If-Range: {stale}");
723            assert!(!resp.headers().contains_key(header::CONTENT_RANGE));
724
725            let body = body::to_bytes(resp.into_body()).await.unwrap();
726
727            assert_eq!(
728                body, content,
729                "Response body and full file content should match",
730            );
731        }
732    }
733
734    #[actix_rt::test]
735    async fn test_named_file_if_range_date() {
736        let file = NamedFile::open("Cargo.toml").unwrap();
737        let content = fs::read("Cargo.toml").unwrap();
738        let modified = file.last_modified().unwrap();
739
740        let req = TestRequest::default()
741            .insert_header((header::RANGE, "bytes=0-9"))
742            .insert_header((header::IF_RANGE, modified))
743            .to_http_request();
744
745        let resp = file.respond_to(&req);
746        assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
747
748        let file = NamedFile::open("Cargo.toml").unwrap();
749        let later = HttpDate::from(SystemTime::from(modified).add(Duration::from_secs(60)));
750        let req = TestRequest::default()
751            .insert_header((header::RANGE, "bytes=0-9"))
752            .insert_header((header::IF_RANGE, later))
753            .to_http_request();
754
755        let resp = file.respond_to(&req);
756        assert_eq!(resp.status(), StatusCode::OK);
757        assert!(!resp.headers().contains_key(header::CONTENT_RANGE));
758
759        let body = body::to_bytes(resp.into_body()).await.unwrap();
760
761        assert_eq!(
762            body, content,
763            "Response body and full file content should match",
764        );
765    }
766
767    #[actix_rt::test]
768    async fn test_named_file_empty_range_headers() {
769        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
770
771        for range in ["", "bytes="] {
772            let response = srv
773                .get("/tests/test.binary")
774                .insert_header((header::RANGE, range))
775                .send()
776                .await
777                .unwrap();
778
779            assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
780            let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
781            assert_eq!(content_range.to_str().unwrap(), "bytes */100");
782        }
783    }
784
785    #[actix_rt::test]
786    async fn test_named_file_content_range_headers() {
787        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
788
789        // Valid range header
790        let response = srv
791            .get("/tests/test.binary")
792            .insert_header((header::RANGE, "bytes=10-20"))
793            .send()
794            .await
795            .unwrap();
796        let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
797        assert_eq!(content_range.to_str().unwrap(), "bytes 10-20/100");
798
799        // Invalid range header
800        let response = srv
801            .get("/tests/test.binary")
802            .insert_header((header::RANGE, "bytes=10-5"))
803            .send()
804            .await
805            .unwrap();
806        let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
807        assert_eq!(content_range.to_str().unwrap(), "bytes */100");
808    }
809
810    #[actix_rt::test]
811    async fn test_named_file_range_header_from_zero_to_end_returns_partial_content() {
812        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
813
814        let response = srv
815            .get("/tests/test.binary")
816            .insert_header((header::RANGE, "bytes=0-"))
817            .send()
818            .await
819            .unwrap();
820
821        assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
822
823        let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
824        assert_eq!(content_range.to_str().unwrap(), "bytes 0-99/100");
825
826        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
827        assert_eq!(content_length.to_str().unwrap(), "100");
828
829        // Should be no transfer-encoding
830        let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING);
831        assert!(transfer_encoding.is_none());
832    }
833
834    #[actix_rt::test]
835    async fn test_named_file_content_length_headers() {
836        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
837
838        // Valid range header
839        let response = srv
840            .get("/tests/test.binary")
841            .insert_header((header::RANGE, "bytes=10-20"))
842            .send()
843            .await
844            .unwrap();
845        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
846        assert_eq!(content_length.to_str().unwrap(), "11");
847
848        // Valid range header, starting from 0
849        let response = srv
850            .get("/tests/test.binary")
851            .insert_header((header::RANGE, "bytes=0-20"))
852            .send()
853            .await
854            .unwrap();
855        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
856        assert_eq!(content_length.to_str().unwrap(), "21");
857
858        // Without range header
859        let mut response = srv.get("/tests/test.binary").send().await.unwrap();
860        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
861        assert_eq!(content_length.to_str().unwrap(), "100");
862
863        // Should be no transfer-encoding
864        let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING);
865        assert!(transfer_encoding.is_none());
866
867        // Check file contents
868        let bytes = response.body().await.unwrap();
869        let data = web::Bytes::from(fs::read("tests/test.binary").unwrap());
870        assert_eq!(bytes, data);
871    }
872
873    #[actix_rt::test]
874    async fn test_head_content_length_headers() {
875        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
876
877        let response = srv.head("/tests/test.binary").send().await.unwrap();
878
879        let content_length = response
880            .headers()
881            .get(header::CONTENT_LENGTH)
882            .unwrap()
883            .to_str()
884            .unwrap();
885
886        assert_eq!(content_length, "100");
887    }
888
889    #[actix_rt::test]
890    async fn test_static_files_with_spaces() {
891        let srv =
892            test::init_service(App::new().service(Files::new("/", ".").index_file("Cargo.toml")))
893                .await;
894        let request = TestRequest::get()
895            .uri("/tests/test%20space.binary")
896            .to_request();
897        let response = test::call_service(&srv, request).await;
898        assert_eq!(response.status(), StatusCode::OK);
899
900        let bytes = test::read_body(response).await;
901        let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap());
902        assert_eq!(bytes, data);
903    }
904
905    #[cfg(not(target_os = "windows"))]
906    #[actix_rt::test]
907    async fn test_static_files_with_special_characters() {
908        // Create the file we want to test against ad-hoc. We can't check it in as otherwise
909        // Windows can't even checkout this repository.
910        let temp_dir = tempfile::tempdir().unwrap();
911        let file_with_newlines = temp_dir.path().join("test\n\x0B\x0C\rnewline.text");
912        fs::write(&file_with_newlines, "Look at my newlines").unwrap();
913
914        let srv = test::init_service(
915            App::new().service(Files::new("/", temp_dir.path()).index_file("Cargo.toml")),
916        )
917        .await;
918        let request = TestRequest::get()
919            .uri("/test%0A%0B%0C%0Dnewline.text")
920            .to_request();
921        let response = test::call_service(&srv, request).await;
922        assert_eq!(response.status(), StatusCode::OK);
923
924        let bytes = test::read_body(response).await;
925        let data = web::Bytes::from(fs::read(file_with_newlines).unwrap());
926        assert_eq!(bytes, data);
927    }
928
929    #[actix_rt::test]
930    async fn test_files_not_allowed() {
931        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
932
933        let req = TestRequest::default()
934            .uri("/Cargo.toml")
935            .method(Method::POST)
936            .to_request();
937
938        let resp = test::call_service(&srv, req).await;
939        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
940
941        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
942        let req = TestRequest::default()
943            .method(Method::PUT)
944            .uri("/Cargo.toml")
945            .to_request();
946        let resp = test::call_service(&srv, req).await;
947        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
948    }
949
950    #[actix_rt::test]
951    async fn test_files_guards() {
952        let srv = test::init_service(
953            App::new().service(Files::new("/", ".").method_guard(guard::Post())),
954        )
955        .await;
956
957        let req = TestRequest::default()
958            .uri("/Cargo.toml")
959            .method(Method::POST)
960            .to_request();
961
962        let resp = test::call_service(&srv, req).await;
963        assert_eq!(resp.status(), StatusCode::OK);
964    }
965
966    #[actix_rt::test]
967    async fn test_named_file_content_encoding() {
968        let srv = test::init_service(App::new().wrap(Compress::default()).service(
969            web::resource("/").to(|| async {
970                NamedFile::open("Cargo.toml")
971                    .unwrap()
972                    .set_content_encoding(header::ContentEncoding::Identity)
973            }),
974        ))
975        .await;
976
977        let request = TestRequest::get()
978            .uri("/")
979            .insert_header((header::ACCEPT_ENCODING, "gzip"))
980            .to_request();
981        let res = test::call_service(&srv, request).await;
982        assert_eq!(res.status(), StatusCode::OK);
983        assert!(res.headers().contains_key(header::CONTENT_ENCODING));
984        assert!(!test::read_body(res).await.is_empty());
985    }
986
987    #[actix_rt::test]
988    async fn test_named_file_content_encoding_gzip() {
989        let srv = test::init_service(App::new().wrap(Compress::default()).service(
990            web::resource("/").to(|| async {
991                NamedFile::open("Cargo.toml")
992                    .unwrap()
993                    .set_content_encoding(header::ContentEncoding::Gzip)
994            }),
995        ))
996        .await;
997
998        let request = TestRequest::get()
999            .uri("/")
1000            .insert_header((header::ACCEPT_ENCODING, "gzip"))
1001            .to_request();
1002        let res = test::call_service(&srv, request).await;
1003        assert_eq!(res.status(), StatusCode::OK);
1004        assert_eq!(
1005            res.headers()
1006                .get(header::CONTENT_ENCODING)
1007                .unwrap()
1008                .to_str()
1009                .unwrap(),
1010            "gzip"
1011        );
1012    }
1013
1014    #[actix_rt::test]
1015    async fn test_named_file_allowed_method() {
1016        let req = TestRequest::default().method(Method::GET).to_http_request();
1017        let file = NamedFile::open("Cargo.toml").unwrap();
1018        let resp = file.respond_to(&req);
1019        assert_eq!(resp.status(), StatusCode::OK);
1020    }
1021
1022    #[actix_rt::test]
1023    async fn test_static_files() {
1024        let srv =
1025            test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await;
1026        let req = TestRequest::with_uri("/missing").to_request();
1027
1028        let resp = test::call_service(&srv, req).await;
1029        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1030
1031        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1032
1033        let req = TestRequest::default().to_request();
1034        let resp = test::call_service(&srv, req).await;
1035        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1036
1037        let srv =
1038            test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await;
1039        let req = TestRequest::with_uri("/tests").to_request();
1040        let resp = test::call_service(&srv, req).await;
1041        assert_eq!(
1042            resp.headers().get(header::CONTENT_TYPE).unwrap(),
1043            "text/html; charset=utf-8"
1044        );
1045
1046        let bytes = test::read_body(resp).await;
1047        assert!(format!("{:?}", bytes).contains("/tests/test.png"));
1048    }
1049
1050    #[actix_rt::test]
1051    async fn test_redirect_to_slash_directory() {
1052        // should not redirect if no index and files listing is disabled
1053        let srv = test::init_service(
1054            App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
1055        )
1056        .await;
1057        let req = TestRequest::with_uri("/tests").to_request();
1058        let resp = test::call_service(&srv, req).await;
1059        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1060
1061        // should redirect if index present
1062        let srv = test::init_service(
1063            App::new().service(
1064                Files::new("/", ".")
1065                    .index_file("test.png")
1066                    .redirect_to_slash_directory(),
1067            ),
1068        )
1069        .await;
1070        let req = TestRequest::with_uri("/tests").to_request();
1071        let resp = test::call_service(&srv, req).await;
1072        assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
1073
1074        // should redirect if index present with permanent redirect
1075        let srv = test::init_service(
1076            App::new().service(
1077                Files::new("/", ".")
1078                    .index_file("test.png")
1079                    .redirect_to_slash_directory()
1080                    .with_permanent_redirect(),
1081            ),
1082        )
1083        .await;
1084        let req = TestRequest::with_uri("/tests").to_request();
1085        let resp = test::call_service(&srv, req).await;
1086        assert_eq!(resp.status(), StatusCode::PERMANENT_REDIRECT);
1087
1088        // should redirect if files listing is enabled
1089        let srv = test::init_service(
1090            App::new().service(
1091                Files::new("/", ".")
1092                    .show_files_listing()
1093                    .redirect_to_slash_directory(),
1094            ),
1095        )
1096        .await;
1097        let req = TestRequest::with_uri("/tests").to_request();
1098        let resp = test::call_service(&srv, req).await;
1099        assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
1100
1101        // should not redirect if the path is wrong
1102        let req = TestRequest::with_uri("/not_existing").to_request();
1103        let resp = test::call_service(&srv, req).await;
1104        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1105    }
1106
1107    #[actix_rt::test]
1108    async fn test_static_files_bad_directory() {
1109        let service = Files::new("/", "./missing").new_service(()).await.unwrap();
1110
1111        let req = TestRequest::with_uri("/").to_srv_request();
1112        let resp = test::call_service(&service, req).await;
1113
1114        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1115    }
1116
1117    #[actix_rt::test]
1118    async fn test_static_files_bad_directory_does_not_serve_cwd_files() {
1119        let service = Files::new("/", "./missing").new_service(()).await.unwrap();
1120
1121        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1122        let resp = test::call_service(&service, req).await;
1123
1124        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1125    }
1126
1127    #[actix_rt::test]
1128    async fn test_static_files_accepts_borrowed_os_string_directory() {
1129        let dir = OsString::from(".");
1130        let service = Files::new("/", &dir).new_service(()).await.unwrap();
1131
1132        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1133        let resp = test::call_service(&service, req).await;
1134
1135        assert_eq!(resp.status(), StatusCode::OK);
1136    }
1137
1138    #[actix_rt::test]
1139    async fn test_static_files_empty_directories() {
1140        let service = Files::new("/", Vec::<PathBuf>::new())
1141            .new_service(())
1142            .await
1143            .unwrap();
1144
1145        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1146        let resp = test::call_service(&service, req).await;
1147        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1148
1149        let service = Files::new("/", Vec::<PathBuf>::new())
1150            .default_handler(|req: ServiceRequest| async {
1151                Ok(req.into_response(HttpResponse::Ok().body("default content")))
1152            })
1153            .new_service(())
1154            .await
1155            .unwrap();
1156
1157        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1158        let resp = test::call_service(&service, req).await;
1159        assert_eq!(resp.status(), StatusCode::OK);
1160        assert_eq!(
1161            test::read_body(resp).await,
1162            Bytes::from_static(b"default content")
1163        );
1164    }
1165
1166    #[actix_rt::test]
1167    async fn test_static_files_multiple_directories() {
1168        let first_dir = tempfile::tempdir().unwrap();
1169        let second_dir = tempfile::tempdir().unwrap();
1170
1171        fs::write(first_dir.path().join("shared.txt"), "first").unwrap();
1172        fs::write(second_dir.path().join("shared.txt"), "second").unwrap();
1173        fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
1174
1175        let service = Files::new("/", [first_dir.path(), second_dir.path()])
1176            .new_service(())
1177            .await
1178            .unwrap();
1179
1180        let req = TestRequest::with_uri("/shared.txt").to_srv_request();
1181        let resp = test::call_service(&service, req).await;
1182        assert_eq!(resp.status(), StatusCode::OK);
1183        assert_eq!(test::read_body(resp).await, Bytes::from_static(b"first"));
1184
1185        let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
1186        let resp = test::call_service(&service, req).await;
1187        assert_eq!(resp.status(), StatusCode::OK);
1188        assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
1189    }
1190
1191    #[actix_rt::test]
1192    async fn test_static_files_multiple_directories_file_as_parent_falls_back() {
1193        let first_dir = tempfile::tempdir().unwrap();
1194        let second_dir = tempfile::tempdir().unwrap();
1195
1196        fs::write(first_dir.path().join("assets"), "file").unwrap();
1197        fs::create_dir(second_dir.path().join("assets")).unwrap();
1198        fs::write(
1199            second_dir.path().join("assets").join("fallback.txt"),
1200            "fallback",
1201        )
1202        .unwrap();
1203
1204        let service = Files::new("/", [first_dir.path(), second_dir.path()])
1205            .new_service(())
1206            .await
1207            .unwrap();
1208
1209        let req = TestRequest::with_uri("/assets/fallback.txt").to_srv_request();
1210        let resp = test::call_service(&service, req).await;
1211        assert_eq!(resp.status(), StatusCode::OK);
1212        assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
1213    }
1214
1215    #[actix_rt::test]
1216    async fn test_static_files_multiple_directories_default_handler() {
1217        let first_dir = tempfile::tempdir().unwrap();
1218        let second_dir = tempfile::tempdir().unwrap();
1219
1220        fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
1221
1222        let service = Files::new("/", vec![first_dir.path(), second_dir.path()])
1223            .default_handler(|req: ServiceRequest| async {
1224                Ok(req.into_response(HttpResponse::Ok().body("default content")))
1225            })
1226            .new_service(())
1227            .await
1228            .unwrap();
1229
1230        let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
1231        let resp = test::call_service(&service, req).await;
1232        assert_eq!(resp.status(), StatusCode::OK);
1233        assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
1234
1235        let req = TestRequest::with_uri("/missing.txt").to_srv_request();
1236        let resp = test::call_service(&service, req).await;
1237        assert_eq!(resp.status(), StatusCode::OK);
1238        assert_eq!(
1239            test::read_body(resp).await,
1240            Bytes::from_static(b"default content")
1241        );
1242    }
1243
1244    #[actix_rt::test]
1245    async fn test_static_files_multiple_directories_index_file() {
1246        let first_dir = tempfile::tempdir().unwrap();
1247        let second_dir = tempfile::tempdir().unwrap();
1248
1249        fs::create_dir(first_dir.path().join("nested")).unwrap();
1250        fs::create_dir(second_dir.path().join("nested")).unwrap();
1251        fs::write(
1252            second_dir.path().join("nested").join("index.html"),
1253            "second index",
1254        )
1255        .unwrap();
1256
1257        let service = Files::new("/", [first_dir.path(), second_dir.path()])
1258            .index_file("index.html")
1259            .new_service(())
1260            .await
1261            .unwrap();
1262
1263        let req = TestRequest::with_uri("/nested/").to_srv_request();
1264        let resp = test::call_service(&service, req).await;
1265        assert_eq!(resp.status(), StatusCode::OK);
1266        assert_eq!(
1267            test::read_body(resp).await,
1268            Bytes::from_static(b"second index")
1269        );
1270    }
1271
1272    #[actix_rt::test]
1273    async fn test_static_files_multiple_directories_index_file_as_parent_falls_back() {
1274        let first_dir = tempfile::tempdir().unwrap();
1275        let second_dir = tempfile::tempdir().unwrap();
1276
1277        fs::create_dir(first_dir.path().join("nested")).unwrap();
1278        fs::write(first_dir.path().join("nested").join("index.html"), "file").unwrap();
1279        fs::create_dir(second_dir.path().join("nested")).unwrap();
1280        fs::create_dir(second_dir.path().join("nested").join("index.html")).unwrap();
1281        fs::write(
1282            second_dir
1283                .path()
1284                .join("nested")
1285                .join("index.html")
1286                .join("fallback.txt"),
1287            "fallback",
1288        )
1289        .unwrap();
1290
1291        let service = Files::new("/", [first_dir.path(), second_dir.path()])
1292            .index_file("index.html/fallback.txt")
1293            .new_service(())
1294            .await
1295            .unwrap();
1296
1297        let req = TestRequest::with_uri("/nested/").to_srv_request();
1298        let resp = test::call_service(&service, req).await;
1299        assert_eq!(resp.status(), StatusCode::OK);
1300        assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
1301    }
1302
1303    #[actix_rt::test]
1304    async fn test_static_files_index_file_error_falls_back_to_listing() {
1305        let dir = tempfile::tempdir().unwrap();
1306
1307        fs::write(dir.path().join("listed.txt"), "listed").unwrap();
1308
1309        let service = Files::new("/", dir.path())
1310            .index_file("index.html\0")
1311            .show_files_listing()
1312            .new_service(())
1313            .await
1314            .unwrap();
1315
1316        let req = TestRequest::with_uri("/").to_srv_request();
1317        let resp = test::call_service(&service, req).await;
1318        assert_eq!(resp.status(), StatusCode::OK);
1319        let bytes = test::read_body(resp).await;
1320        assert!(format!("{bytes:?}").contains("listed.txt"));
1321    }
1322
1323    #[actix_rt::test]
1324    async fn test_static_files_multiple_directories_show_files_listing() {
1325        let first_dir = tempfile::tempdir().unwrap();
1326        let second_dir = tempfile::tempdir().unwrap();
1327
1328        fs::write(first_dir.path().join("listed.txt"), "listed").unwrap();
1329
1330        let service = Files::new("/", [first_dir.path(), second_dir.path()])
1331            .show_files_listing()
1332            .new_service(())
1333            .await
1334            .unwrap();
1335
1336        let req = TestRequest::with_uri("/").to_srv_request();
1337        let resp = test::call_service(&service, req).await;
1338        assert_eq!(resp.status(), StatusCode::OK);
1339        let bytes = test::read_body(resp).await;
1340        assert!(format!("{bytes:?}").contains("listed.txt"));
1341    }
1342
1343    #[actix_rt::test]
1344    async fn test_static_files_multiple_directories_redirect_precedence() {
1345        let first_dir = tempfile::tempdir().unwrap();
1346        let second_dir = tempfile::tempdir().unwrap();
1347
1348        fs::create_dir(first_dir.path().join("item")).unwrap();
1349        fs::write(second_dir.path().join("item"), "file").unwrap();
1350
1351        let service = Files::new("/", [first_dir.path(), second_dir.path()])
1352            .show_files_listing()
1353            .redirect_to_slash_directory()
1354            .new_service(())
1355            .await
1356            .unwrap();
1357
1358        let req = TestRequest::with_uri("/item").to_srv_request();
1359        let resp = test::call_service(&service, req).await;
1360        assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
1361        assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/item/");
1362    }
1363
1364    #[actix_rt::test]
1365    async fn test_default_handler_file_missing() {
1366        let st = Files::new("/", ".")
1367            .default_handler(|req: ServiceRequest| async {
1368                Ok(req.into_response(HttpResponse::Ok().body("default content")))
1369            })
1370            .new_service(())
1371            .await
1372            .unwrap();
1373        let req = TestRequest::with_uri("/missing").to_srv_request();
1374        let resp = test::call_service(&st, req).await;
1375
1376        assert_eq!(resp.status(), StatusCode::OK);
1377        let bytes = test::read_body(resp).await;
1378        assert_eq!(bytes, web::Bytes::from_static(b"default content"));
1379    }
1380
1381    #[actix_rt::test]
1382    async fn test_hidden_files_reject_cur_dir_segment() {
1383        let service = Files::new("/", Vec::<PathBuf>::new())
1384            .use_hidden_files()
1385            .default_handler(Files::new("/", ".").use_hidden_files())
1386            .new_service(())
1387            .await
1388            .unwrap();
1389
1390        let req = TestRequest::with_uri("/./Cargo.toml").to_srv_request();
1391        let resp = test::call_service(&service, req).await;
1392
1393        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1394    }
1395
1396    #[actix_rt::test]
1397    async fn test_serve_index_nested() {
1398        let service = Files::new(".", ".")
1399            .index_file("lib.rs")
1400            .new_service(())
1401            .await
1402            .unwrap();
1403
1404        let req = TestRequest::default().uri("/src").to_srv_request();
1405        let resp = test::call_service(&service, req).await;
1406
1407        assert_eq!(resp.status(), StatusCode::OK);
1408        assert_eq!(
1409            resp.headers().get(header::CONTENT_TYPE).unwrap(),
1410            "text/x-rust"
1411        );
1412        assert_eq!(
1413            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
1414            "inline; filename=\"lib.rs\""
1415        );
1416    }
1417
1418    #[actix_rt::test]
1419    async fn integration_serve_index() {
1420        let srv = test::init_service(
1421            App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
1422        )
1423        .await;
1424
1425        let req = TestRequest::get().uri("/test").to_request();
1426        let res = test::call_service(&srv, req).await;
1427        assert_eq!(res.status(), StatusCode::OK);
1428
1429        let bytes = test::read_body(res).await;
1430
1431        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1432        assert_eq!(bytes, data);
1433
1434        let req = TestRequest::get().uri("/test/").to_request();
1435        let res = test::call_service(&srv, req).await;
1436        assert_eq!(res.status(), StatusCode::OK);
1437
1438        let bytes = test::read_body(res).await;
1439        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1440        assert_eq!(bytes, data);
1441
1442        // nonexistent index file
1443        let req = TestRequest::get().uri("/test/unknown").to_request();
1444        let res = test::call_service(&srv, req).await;
1445        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1446
1447        let req = TestRequest::get().uri("/test/unknown/").to_request();
1448        let res = test::call_service(&srv, req).await;
1449        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1450    }
1451
1452    #[actix_rt::test]
1453    async fn integration_percent_encoded() {
1454        let srv = test::init_service(
1455            App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
1456        )
1457        .await;
1458
1459        let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
1460        let res = test::call_service(&srv, req).await;
1461        assert_eq!(res.status(), StatusCode::OK);
1462
1463        // `%2F` == `/`
1464        let req = TestRequest::get().uri("/test%2Ftest.binary").to_request();
1465        let res = test::call_service(&srv, req).await;
1466        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1467
1468        let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request();
1469        let res = test::call_service(&srv, req).await;
1470        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1471    }
1472
1473    #[actix_rt::test]
1474    async fn test_percent_encoding_2() {
1475        let temp_dir = tempfile::tempdir().unwrap();
1476        let filename = match cfg!(unix) {
1477            true => "ض:?#[]{}<>()@!$&'`|*+,;= %20\n.test",
1478            false => "ض#[]{}()@!$&'`+,;= %20.test",
1479        };
1480        let filename_encoded = filename
1481            .as_bytes()
1482            .iter()
1483            .fold(String::new(), |mut buf, c| {
1484                write!(&mut buf, "%{:02X}", c).unwrap();
1485                buf
1486            });
1487        std::fs::File::create(temp_dir.path().join(filename)).unwrap();
1488
1489        let srv = test::init_service(App::new().service(Files::new("/", temp_dir.path()))).await;
1490
1491        let req = TestRequest::get()
1492            .uri(&format!("/{}", filename_encoded))
1493            .to_request();
1494        let res = test::call_service(&srv, req).await;
1495        assert_eq!(res.status(), StatusCode::OK);
1496    }
1497
1498    #[actix_rt::test]
1499    async fn test_serve_named_file() {
1500        let factory = NamedFile::open("Cargo.toml").unwrap();
1501        let srv = test::init_service(App::new().service(factory)).await;
1502
1503        let req = TestRequest::get().uri("/Cargo.toml").to_request();
1504        let res = test::call_service(&srv, req).await;
1505        assert_eq!(res.status(), StatusCode::OK);
1506
1507        let bytes = test::read_body(res).await;
1508        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1509        assert_eq!(bytes, data);
1510
1511        let req = TestRequest::get().uri("/test/unknown").to_request();
1512        let res = test::call_service(&srv, req).await;
1513        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1514    }
1515
1516    #[actix_rt::test]
1517    async fn test_serve_named_file_prefix() {
1518        let factory = NamedFile::open("Cargo.toml").unwrap();
1519        let srv =
1520            test::init_service(App::new().service(web::scope("/test").service(factory))).await;
1521
1522        let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
1523        let res = test::call_service(&srv, req).await;
1524        assert_eq!(res.status(), StatusCode::OK);
1525
1526        let bytes = test::read_body(res).await;
1527        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1528        assert_eq!(bytes, data);
1529
1530        let req = TestRequest::get().uri("/Cargo.toml").to_request();
1531        let res = test::call_service(&srv, req).await;
1532        assert_eq!(res.status(), StatusCode::NOT_FOUND);
1533    }
1534
1535    #[actix_rt::test]
1536    async fn test_named_file_default_service() {
1537        let factory = NamedFile::open("Cargo.toml").unwrap();
1538        let srv = test::init_service(App::new().default_service(factory)).await;
1539
1540        for route in ["/foobar", "/baz", "/"].iter() {
1541            let req = TestRequest::get().uri(route).to_request();
1542            let res = test::call_service(&srv, req).await;
1543            assert_eq!(res.status(), StatusCode::OK);
1544
1545            let bytes = test::read_body(res).await;
1546            let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1547            assert_eq!(bytes, data);
1548        }
1549    }
1550
1551    #[actix_rt::test]
1552    async fn test_default_handler_named_file() {
1553        let factory = NamedFile::open("Cargo.toml").unwrap();
1554        let st = Files::new("/", ".")
1555            .default_handler(factory)
1556            .new_service(())
1557            .await
1558            .unwrap();
1559        let req = TestRequest::with_uri("/missing").to_srv_request();
1560        let resp = test::call_service(&st, req).await;
1561
1562        assert_eq!(resp.status(), StatusCode::OK);
1563        let bytes = test::read_body(resp).await;
1564        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1565        assert_eq!(bytes, data);
1566    }
1567
1568    #[actix_rt::test]
1569    async fn test_symlinks() {
1570        let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
1571
1572        let req = TestRequest::get()
1573            .uri("/test/tests/symlink-test.png")
1574            .to_request();
1575        let res = test::call_service(&srv, req).await;
1576        assert_eq!(res.status(), StatusCode::OK);
1577        assert_eq!(
1578            res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
1579            "inline; filename=\"symlink-test.png\""
1580        );
1581    }
1582
1583    #[actix_rt::test]
1584    async fn test_index_with_show_files_listing() {
1585        let service = Files::new(".", ".")
1586            .index_file("lib.rs")
1587            .show_files_listing()
1588            .new_service(())
1589            .await
1590            .unwrap();
1591
1592        // Serve the index if exists
1593        let req = TestRequest::default().uri("/src").to_srv_request();
1594        let resp = test::call_service(&service, req).await;
1595        assert_eq!(resp.status(), StatusCode::OK);
1596        assert_eq!(
1597            resp.headers().get(header::CONTENT_TYPE).unwrap(),
1598            "text/x-rust"
1599        );
1600
1601        // Show files listing, otherwise.
1602        let req = TestRequest::default().uri("/tests").to_srv_request();
1603        let resp = test::call_service(&service, req).await;
1604        assert_eq!(
1605            resp.headers().get(header::CONTENT_TYPE).unwrap(),
1606            "text/html; charset=utf-8"
1607        );
1608        let bytes = test::read_body(resp).await;
1609        assert!(format!("{:?}", bytes).contains("/tests/test.png"));
1610    }
1611
1612    #[actix_rt::test]
1613    async fn test_path_filter() {
1614        // prevent searching subdirectories
1615        let st = Files::new("/", ".")
1616            .path_filter(|path, _| path.components().count() == 1)
1617            .new_service(())
1618            .await
1619            .unwrap();
1620
1621        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1622        let resp = test::call_service(&st, req).await;
1623        assert_eq!(resp.status(), StatusCode::OK);
1624
1625        let req = TestRequest::with_uri("/src/lib.rs").to_srv_request();
1626        let resp = test::call_service(&st, req).await;
1627        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1628    }
1629
1630    #[actix_rt::test]
1631    async fn test_default_handler_filter() {
1632        let st = Files::new("/", ".")
1633            .default_handler(|req: ServiceRequest| async {
1634                Ok(req.into_response(HttpResponse::Ok().body("default content")))
1635            })
1636            .path_filter(|path, _| path.extension() == Some("png".as_ref()))
1637            .new_service(())
1638            .await
1639            .unwrap();
1640        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1641        let resp = test::call_service(&st, req).await;
1642
1643        assert_eq!(resp.status(), StatusCode::OK);
1644        let bytes = test::read_body(resp).await;
1645        assert_eq!(bytes, web::Bytes::from_static(b"default content"));
1646    }
1647}