diff --git a/README.md b/README.md index 85afb62d..8d9c001a 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,7 @@ svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c"); svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h"); ``` -Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. A user mapping is looked up by the exact extension first, then by the extension in lowercase. +Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. The extension of a user mapping can be given in any case, with or without a leading `.`. The following are built-in mappings: diff --git a/httplib.h b/httplib.h index 7eec7a85..b78c9463 100644 --- a/httplib.h +++ b/httplib.h @@ -7748,13 +7748,10 @@ inline std::string find_content_type(const std::string &path, const std::map &user_data, const std::string &default_content_type) { - auto ext = file_extension(path); + // Extensions are matched regardless of case, as web servers do + auto ext = case_ignore::to_lower(file_extension(path)); - // Extensions are matched regardless of case, as web servers do, with an - // exact match to a user mapping first - auto lower_ext = case_ignore::to_lower(ext); auto it = user_data.find(ext); - if (it == user_data.end()) { it = user_data.find(lower_ext); } if (it != user_data.end()) { return it->second; } static const struct { @@ -7813,7 +7810,7 @@ find_content_type(const std::string &path, }; for (const auto &t : types) { - if (lower_ext == t.ext) { return t.type; } + if (ext == t.ext) { return t.type; } } return default_content_type; } @@ -13736,7 +13733,10 @@ inline bool Server::remove_mount_point(const std::string &mount_point) { inline Server & Server::set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime) { - file_extension_and_mimetype_map_[ext] = mime; + // Keyed as find_content_type() looks it up + auto key = detail::case_ignore::to_lower(ext); + if (!key.empty() && key[0] == '.') { key.erase(0, 1); } + file_extension_and_mimetype_map_[key] = mime; return *this; } diff --git a/test/test.cc b/test/test.cc index 8d34661f..d1d0fec2 100644 --- a/test/test.cc +++ b/test/test.cc @@ -12641,6 +12641,32 @@ TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) { } } +TEST(UserDefinedMIMETypeMappingTest, IgnoresCaseAndLeadingDot) { + Server svr; + svr.set_mount_point("/", "./www"); + svr.set_file_extension_and_mimetype_mapping(".abcde", "text/abcde"); + svr.set_file_extension_and_mimetype_mapping("HTML", "text/x-html"); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_GT(port, 0); + thread t = thread([&] { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + svr.wait_until_ready(); + + Client cli(HOST, port); + auto res = cli.Get("/dir/test.abcde"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("text/abcde", res->get_header_value("Content-Type")); + + res = cli.Get("/dir/test.html"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_EQ("text/x-html", res->get_header_value("Content-Type")); +} + #ifdef _WIN32 // Issue #2435: mmap::open() must succeed even when another handle holds // the file open for writing (e.g. an active log file).