| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // | ||
| 2 | // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) | ||
| 3 | // | ||
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | ||
| 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||
| 6 | // | ||
| 7 | // Official repository: https://github.com/cppalliance/http | ||
| 8 | // | ||
| 9 | |||
| 10 | #include <boost/http/server/mime_types.hpp> | ||
| 11 | #include <boost/http/server/mime_db.hpp> | ||
| 12 | #include <algorithm> | ||
| 13 | #include <cctype> | ||
| 14 | |||
| 15 | namespace boost { | ||
| 16 | namespace http { | ||
| 17 | namespace mime_types { | ||
| 18 | |||
| 19 | namespace { | ||
| 20 | |||
| 21 | struct ext_entry | ||
| 22 | { | ||
| 23 | core::string_view ext; | ||
| 24 | core::string_view type; | ||
| 25 | }; | ||
| 26 | |||
| 27 | // Sorted by extension for binary search | ||
| 28 | constexpr ext_entry ext_db[] = { | ||
| 29 | { "aac", "audio/aac" }, | ||
| 30 | { "avif", "image/avif" }, | ||
| 31 | { "bmp", "image/bmp" }, | ||
| 32 | { "bz", "application/x-bzip" }, | ||
| 33 | { "bz2", "application/x-bzip2" }, | ||
| 34 | { "cjs", "application/javascript" }, | ||
| 35 | { "css", "text/css" }, | ||
| 36 | { "csv", "text/csv" }, | ||
| 37 | { "flac", "audio/flac" }, | ||
| 38 | { "gif", "image/gif" }, | ||
| 39 | { "gz", "application/gzip" }, | ||
| 40 | { "htm", "text/html" }, | ||
| 41 | { "html", "text/html" }, | ||
| 42 | { "ico", "image/x-icon" }, | ||
| 43 | { "ics", "text/calendar" }, | ||
| 44 | { "jpeg", "image/jpeg" }, | ||
| 45 | { "jpg", "image/jpeg" }, | ||
| 46 | { "js", "text/javascript" }, | ||
| 47 | { "json", "application/json" }, | ||
| 48 | { "m4a", "audio/mp4" }, | ||
| 49 | { "m4v", "video/mp4" }, | ||
| 50 | { "manifest", "text/cache-manifest" }, | ||
| 51 | { "md", "text/markdown" }, | ||
| 52 | { "mjs", "text/javascript" }, | ||
| 53 | { "mp3", "audio/mpeg" }, | ||
| 54 | { "mp4", "video/mp4" }, | ||
| 55 | { "mpeg", "video/mpeg" }, | ||
| 56 | { "mpg", "video/mpeg" }, | ||
| 57 | { "oga", "audio/ogg" }, | ||
| 58 | { "ogg", "audio/ogg" }, | ||
| 59 | { "ogv", "video/ogg" }, | ||
| 60 | { "otf", "font/otf" }, | ||
| 61 | { "pdf", "application/pdf" }, | ||
| 62 | { "png", "image/png" }, | ||
| 63 | { "rtf", "application/rtf" }, | ||
| 64 | { "svg", "image/svg+xml" }, | ||
| 65 | { "tar", "application/x-tar" }, | ||
| 66 | { "tif", "image/tiff" }, | ||
| 67 | { "tiff", "image/tiff" }, | ||
| 68 | { "ttf", "font/ttf" }, | ||
| 69 | { "txt", "text/plain" }, | ||
| 70 | { "wasm", "application/wasm" }, | ||
| 71 | { "wav", "audio/wav" }, | ||
| 72 | { "weba", "audio/webm" }, | ||
| 73 | { "webm", "video/webm" }, | ||
| 74 | { "webp", "image/webp" }, | ||
| 75 | { "woff", "font/woff" }, | ||
| 76 | { "woff2", "font/woff2" }, | ||
| 77 | { "xhtml", "application/xhtml+xml" }, | ||
| 78 | { "xml", "application/xml" }, | ||
| 79 | { "zip", "application/zip" }, | ||
| 80 | { "7z", "application/x-7z-compressed" }, | ||
| 81 | }; | ||
| 82 | |||
| 83 | constexpr std::size_t ext_db_size = sizeof( ext_db ) / sizeof( ext_db[0] ); | ||
| 84 | |||
| 85 | // Case-insensitive comparison | ||
| 86 | int | ||
| 87 | ✗ | compare_icase( core::string_view a, core::string_view b ) noexcept | |
| 88 | { | ||
| 89 | ✗ | auto const n = ( std::min )( a.size(), b.size() ); | |
| 90 | ✗ | for( std::size_t i = 0; i < n; ++i ) | |
| 91 | { | ||
| 92 | auto const ca = static_cast<unsigned char>( | ||
| 93 | ✗ | std::tolower( static_cast<unsigned char>( a[i] ) ) ); | |
| 94 | auto const cb = static_cast<unsigned char>( | ||
| 95 | ✗ | std::tolower( static_cast<unsigned char>( b[i] ) ) ); | |
| 96 | ✗ | if( ca < cb ) | |
| 97 | ✗ | return -1; | |
| 98 | ✗ | if( ca > cb ) | |
| 99 | ✗ | return 1; | |
| 100 | } | ||
| 101 | ✗ | if( a.size() < b.size() ) | |
| 102 | ✗ | return -1; | |
| 103 | ✗ | if( a.size() > b.size() ) | |
| 104 | ✗ | return 1; | |
| 105 | ✗ | return 0; | |
| 106 | } | ||
| 107 | |||
| 108 | // Extract extension from path | ||
| 109 | core::string_view | ||
| 110 | ✗ | get_extension( core::string_view path ) noexcept | |
| 111 | { | ||
| 112 | // Find last dot | ||
| 113 | ✗ | auto const pos = path.rfind( '.' ); | |
| 114 | ✗ | if( pos == core::string_view::npos ) | |
| 115 | ✗ | return path; // Assume it's just an extension | |
| 116 | ✗ | return path.substr( pos + 1 ); | |
| 117 | } | ||
| 118 | |||
| 119 | // Binary search for extension | ||
| 120 | core::string_view | ||
| 121 | ✗ | lookup_ext( core::string_view ext ) noexcept | |
| 122 | { | ||
| 123 | ✗ | std::size_t lo = 0; | |
| 124 | ✗ | std::size_t hi = ext_db_size; | |
| 125 | ✗ | while( lo < hi ) | |
| 126 | { | ||
| 127 | ✗ | auto const mid = lo + ( hi - lo ) / 2; | |
| 128 | ✗ | auto const cmp = compare_icase( ext_db[mid].ext, ext ); | |
| 129 | ✗ | if( cmp < 0 ) | |
| 130 | ✗ | lo = mid + 1; | |
| 131 | ✗ | else if( cmp > 0 ) | |
| 132 | ✗ | hi = mid; | |
| 133 | else | ||
| 134 | ✗ | return ext_db[mid].type; | |
| 135 | } | ||
| 136 | ✗ | return {}; | |
| 137 | } | ||
| 138 | |||
| 139 | } // (anon) | ||
| 140 | |||
| 141 | core::string_view | ||
| 142 | ✗ | lookup( core::string_view path_or_ext ) noexcept | |
| 143 | { | ||
| 144 | ✗ | if( path_or_ext.empty() ) | |
| 145 | ✗ | return {}; | |
| 146 | |||
| 147 | // Skip leading dot if present | ||
| 148 | ✗ | if( path_or_ext[0] == '.' ) | |
| 149 | ✗ | path_or_ext.remove_prefix( 1 ); | |
| 150 | |||
| 151 | ✗ | auto const ext = get_extension( path_or_ext ); | |
| 152 | ✗ | return lookup_ext( ext ); | |
| 153 | } | ||
| 154 | |||
| 155 | core::string_view | ||
| 156 | ✗ | extension( core::string_view type ) noexcept | |
| 157 | { | ||
| 158 | // Linear search for type -> extension | ||
| 159 | // Could optimize with reverse map if needed | ||
| 160 | ✗ | for( std::size_t i = 0; i < ext_db_size; ++i ) | |
| 161 | { | ||
| 162 | ✗ | if( compare_icase( ext_db[i].type, type ) == 0 ) | |
| 163 | ✗ | return ext_db[i].ext; | |
| 164 | } | ||
| 165 | ✗ | return {}; | |
| 166 | } | ||
| 167 | |||
| 168 | core::string_view | ||
| 169 | ✗ | charset( core::string_view type ) noexcept | |
| 170 | { | ||
| 171 | ✗ | auto const* entry = mime_db::lookup( type ); | |
| 172 | ✗ | if( entry ) | |
| 173 | ✗ | return entry->charset; | |
| 174 | ✗ | return {}; | |
| 175 | } | ||
| 176 | |||
| 177 | std::string | ||
| 178 | ✗ | content_type( core::string_view type_or_ext ) | |
| 179 | { | ||
| 180 | ✗ | core::string_view type; | |
| 181 | |||
| 182 | // Check if it looks like an extension | ||
| 183 | ✗ | if( ! type_or_ext.empty() && | |
| 184 | ✗ | ( type_or_ext[0] == '.' || | |
| 185 | ✗ | type_or_ext.find( '/' ) == core::string_view::npos ) ) | |
| 186 | { | ||
| 187 | ✗ | type = lookup( type_or_ext ); | |
| 188 | ✗ | if( type.empty() ) | |
| 189 | ✗ | return {}; | |
| 190 | } | ||
| 191 | else | ||
| 192 | { | ||
| 193 | ✗ | type = type_or_ext; | |
| 194 | } | ||
| 195 | |||
| 196 | ✗ | auto const cs = charset( type ); | |
| 197 | ✗ | if( cs.empty() ) | |
| 198 | ✗ | return std::string( type ); | |
| 199 | |||
| 200 | ✗ | std::string result; | |
| 201 | ✗ | result.reserve( type.size() + 10 + cs.size() ); | |
| 202 | ✗ | result.append( type.data(), type.size() ); | |
| 203 | ✗ | result.append( "; charset=" ); | |
| 204 | ✗ | result.append( cs.data(), cs.size() ); | |
| 205 | ✗ | return result; | |
| 206 | ✗ | } | |
| 207 | |||
| 208 | } // mime_types | ||
| 209 | } // http | ||
| 210 | } // boost | ||
| 211 |