Line data Source code
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 0 : compare_icase( core::string_view a, core::string_view b ) noexcept
88 : {
89 0 : auto const n = ( std::min )( a.size(), b.size() );
90 0 : for( std::size_t i = 0; i < n; ++i )
91 : {
92 : auto const ca = static_cast<unsigned char>(
93 0 : std::tolower( static_cast<unsigned char>( a[i] ) ) );
94 : auto const cb = static_cast<unsigned char>(
95 0 : std::tolower( static_cast<unsigned char>( b[i] ) ) );
96 0 : if( ca < cb )
97 0 : return -1;
98 0 : if( ca > cb )
99 0 : return 1;
100 : }
101 0 : if( a.size() < b.size() )
102 0 : return -1;
103 0 : if( a.size() > b.size() )
104 0 : return 1;
105 0 : return 0;
106 : }
107 :
108 : // Extract extension from path
109 : core::string_view
110 0 : get_extension( core::string_view path ) noexcept
111 : {
112 : // Find last dot
113 0 : auto const pos = path.rfind( '.' );
114 0 : if( pos == core::string_view::npos )
115 0 : return path; // Assume it's just an extension
116 0 : return path.substr( pos + 1 );
117 : }
118 :
119 : // Binary search for extension
120 : core::string_view
121 0 : lookup_ext( core::string_view ext ) noexcept
122 : {
123 0 : std::size_t lo = 0;
124 0 : std::size_t hi = ext_db_size;
125 0 : while( lo < hi )
126 : {
127 0 : auto const mid = lo + ( hi - lo ) / 2;
128 0 : auto const cmp = compare_icase( ext_db[mid].ext, ext );
129 0 : if( cmp < 0 )
130 0 : lo = mid + 1;
131 0 : else if( cmp > 0 )
132 0 : hi = mid;
133 : else
134 0 : return ext_db[mid].type;
135 : }
136 0 : return {};
137 : }
138 :
139 : } // (anon)
140 :
141 : core::string_view
142 0 : lookup( core::string_view path_or_ext ) noexcept
143 : {
144 0 : if( path_or_ext.empty() )
145 0 : return {};
146 :
147 : // Skip leading dot if present
148 0 : if( path_or_ext[0] == '.' )
149 0 : path_or_ext.remove_prefix( 1 );
150 :
151 0 : auto const ext = get_extension( path_or_ext );
152 0 : return lookup_ext( ext );
153 : }
154 :
155 : core::string_view
156 0 : extension( core::string_view type ) noexcept
157 : {
158 : // Linear search for type -> extension
159 : // Could optimize with reverse map if needed
160 0 : for( std::size_t i = 0; i < ext_db_size; ++i )
161 : {
162 0 : if( compare_icase( ext_db[i].type, type ) == 0 )
163 0 : return ext_db[i].ext;
164 : }
165 0 : return {};
166 : }
167 :
168 : core::string_view
169 0 : charset( core::string_view type ) noexcept
170 : {
171 0 : auto const* entry = mime_db::lookup( type );
172 0 : if( entry )
173 0 : return entry->charset;
174 0 : return {};
175 : }
176 :
177 : std::string
178 0 : content_type( core::string_view type_or_ext )
179 : {
180 0 : core::string_view type;
181 :
182 : // Check if it looks like an extension
183 0 : if( ! type_or_ext.empty() &&
184 0 : ( type_or_ext[0] == '.' ||
185 0 : type_or_ext.find( '/' ) == core::string_view::npos ) )
186 : {
187 0 : type = lookup( type_or_ext );
188 0 : if( type.empty() )
189 0 : return {};
190 : }
191 : else
192 : {
193 0 : type = type_or_ext;
194 : }
195 :
196 0 : auto const cs = charset( type );
197 0 : if( cs.empty() )
198 0 : return std::string( type );
199 :
200 0 : std::string result;
201 0 : result.reserve( type.size() + 10 + cs.size() );
202 0 : result.append( type.data(), type.size() );
203 0 : result.append( "; charset=" );
204 0 : result.append( cs.data(), cs.size() );
205 0 : return result;
206 0 : }
207 :
208 : } // mime_types
209 : } // http
210 : } // boost
|