1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
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)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/http
7  
// Official repository: https://github.com/cppalliance/http
8  
//
8  
//
9  

9  

10  
#include <boost/http/server/mime_types.hpp>
10  
#include <boost/http/server/mime_types.hpp>
11  
#include <boost/http/server/mime_db.hpp>
11  
#include <boost/http/server/mime_db.hpp>
12  
#include <algorithm>
12  
#include <algorithm>
13  
#include <cctype>
13  
#include <cctype>
14  

14  

15  
namespace boost {
15  
namespace boost {
16  
namespace http {
16  
namespace http {
17  
namespace mime_types {
17  
namespace mime_types {
18  

18  

19  
namespace {
19  
namespace {
20  

20  

21  
struct ext_entry
21  
struct ext_entry
22  
{
22  
{
23  
    core::string_view ext;
23  
    core::string_view ext;
24  
    core::string_view type;
24  
    core::string_view type;
25  
};
25  
};
26  

26  

27  
// Sorted by extension for binary search
27  
// Sorted by extension for binary search
28  
constexpr ext_entry ext_db[] = {
28  
constexpr ext_entry ext_db[] = {
29  
    { "aac", "audio/aac" },
29  
    { "aac", "audio/aac" },
30  
    { "avif", "image/avif" },
30  
    { "avif", "image/avif" },
31  
    { "bmp", "image/bmp" },
31  
    { "bmp", "image/bmp" },
32  
    { "bz", "application/x-bzip" },
32  
    { "bz", "application/x-bzip" },
33  
    { "bz2", "application/x-bzip2" },
33  
    { "bz2", "application/x-bzip2" },
34  
    { "cjs", "application/javascript" },
34  
    { "cjs", "application/javascript" },
35  
    { "css", "text/css" },
35  
    { "css", "text/css" },
36  
    { "csv", "text/csv" },
36  
    { "csv", "text/csv" },
37  
    { "flac", "audio/flac" },
37  
    { "flac", "audio/flac" },
38  
    { "gif", "image/gif" },
38  
    { "gif", "image/gif" },
39  
    { "gz", "application/gzip" },
39  
    { "gz", "application/gzip" },
40  
    { "htm", "text/html" },
40  
    { "htm", "text/html" },
41  
    { "html", "text/html" },
41  
    { "html", "text/html" },
42  
    { "ico", "image/x-icon" },
42  
    { "ico", "image/x-icon" },
43  
    { "ics", "text/calendar" },
43  
    { "ics", "text/calendar" },
44  
    { "jpeg", "image/jpeg" },
44  
    { "jpeg", "image/jpeg" },
45  
    { "jpg", "image/jpeg" },
45  
    { "jpg", "image/jpeg" },
46  
    { "js", "text/javascript" },
46  
    { "js", "text/javascript" },
47  
    { "json", "application/json" },
47  
    { "json", "application/json" },
48  
    { "m4a", "audio/mp4" },
48  
    { "m4a", "audio/mp4" },
49  
    { "m4v", "video/mp4" },
49  
    { "m4v", "video/mp4" },
50  
    { "manifest", "text/cache-manifest" },
50  
    { "manifest", "text/cache-manifest" },
51  
    { "md", "text/markdown" },
51  
    { "md", "text/markdown" },
52  
    { "mjs", "text/javascript" },
52  
    { "mjs", "text/javascript" },
53  
    { "mp3", "audio/mpeg" },
53  
    { "mp3", "audio/mpeg" },
54  
    { "mp4", "video/mp4" },
54  
    { "mp4", "video/mp4" },
55  
    { "mpeg", "video/mpeg" },
55  
    { "mpeg", "video/mpeg" },
56  
    { "mpg", "video/mpeg" },
56  
    { "mpg", "video/mpeg" },
57  
    { "oga", "audio/ogg" },
57  
    { "oga", "audio/ogg" },
58  
    { "ogg", "audio/ogg" },
58  
    { "ogg", "audio/ogg" },
59  
    { "ogv", "video/ogg" },
59  
    { "ogv", "video/ogg" },
60  
    { "otf", "font/otf" },
60  
    { "otf", "font/otf" },
61  
    { "pdf", "application/pdf" },
61  
    { "pdf", "application/pdf" },
62  
    { "png", "image/png" },
62  
    { "png", "image/png" },
63  
    { "rtf", "application/rtf" },
63  
    { "rtf", "application/rtf" },
64  
    { "svg", "image/svg+xml" },
64  
    { "svg", "image/svg+xml" },
65  
    { "tar", "application/x-tar" },
65  
    { "tar", "application/x-tar" },
66  
    { "tif", "image/tiff" },
66  
    { "tif", "image/tiff" },
67  
    { "tiff", "image/tiff" },
67  
    { "tiff", "image/tiff" },
68  
    { "ttf", "font/ttf" },
68  
    { "ttf", "font/ttf" },
69  
    { "txt", "text/plain" },
69  
    { "txt", "text/plain" },
70  
    { "wasm", "application/wasm" },
70  
    { "wasm", "application/wasm" },
71  
    { "wav", "audio/wav" },
71  
    { "wav", "audio/wav" },
72  
    { "weba", "audio/webm" },
72  
    { "weba", "audio/webm" },
73  
    { "webm", "video/webm" },
73  
    { "webm", "video/webm" },
74  
    { "webp", "image/webp" },
74  
    { "webp", "image/webp" },
75  
    { "woff", "font/woff" },
75  
    { "woff", "font/woff" },
76  
    { "woff2", "font/woff2" },
76  
    { "woff2", "font/woff2" },
77  
    { "xhtml", "application/xhtml+xml" },
77  
    { "xhtml", "application/xhtml+xml" },
78  
    { "xml", "application/xml" },
78  
    { "xml", "application/xml" },
79  
    { "zip", "application/zip" },
79  
    { "zip", "application/zip" },
80  
    { "7z", "application/x-7z-compressed" },
80  
    { "7z", "application/x-7z-compressed" },
81  
};
81  
};
82  

82  

83  
constexpr std::size_t ext_db_size = sizeof( ext_db ) / sizeof( ext_db[0] );
83  
constexpr std::size_t ext_db_size = sizeof( ext_db ) / sizeof( ext_db[0] );
84  

84  

85  
// Case-insensitive comparison
85  
// Case-insensitive comparison
86  
int
86  
int
87  
compare_icase( core::string_view a, core::string_view b ) noexcept
87  
compare_icase( core::string_view a, core::string_view b ) noexcept
88  
{
88  
{
89  
    auto const n = ( std::min )( a.size(), b.size() );
89  
    auto const n = ( std::min )( a.size(), b.size() );
90  
    for( std::size_t i = 0; i < n; ++i )
90  
    for( std::size_t i = 0; i < n; ++i )
91  
    {
91  
    {
92  
        auto const ca = static_cast<unsigned char>(
92  
        auto const ca = static_cast<unsigned char>(
93  
            std::tolower( static_cast<unsigned char>( a[i] ) ) );
93  
            std::tolower( static_cast<unsigned char>( a[i] ) ) );
94  
        auto const cb = static_cast<unsigned char>(
94  
        auto const cb = static_cast<unsigned char>(
95  
            std::tolower( static_cast<unsigned char>( b[i] ) ) );
95  
            std::tolower( static_cast<unsigned char>( b[i] ) ) );
96  
        if( ca < cb )
96  
        if( ca < cb )
97  
            return -1;
97  
            return -1;
98  
        if( ca > cb )
98  
        if( ca > cb )
99  
            return 1;
99  
            return 1;
100  
    }
100  
    }
101  
    if( a.size() < b.size() )
101  
    if( a.size() < b.size() )
102  
        return -1;
102  
        return -1;
103  
    if( a.size() > b.size() )
103  
    if( a.size() > b.size() )
104  
        return 1;
104  
        return 1;
105  
    return 0;
105  
    return 0;
106  
}
106  
}
107  

107  

108  
// Extract extension from path
108  
// Extract extension from path
109  
core::string_view
109  
core::string_view
110  
get_extension( core::string_view path ) noexcept
110  
get_extension( core::string_view path ) noexcept
111  
{
111  
{
112  
    // Find last dot
112  
    // Find last dot
113  
    auto const pos = path.rfind( '.' );
113  
    auto const pos = path.rfind( '.' );
114  
    if( pos == core::string_view::npos )
114  
    if( pos == core::string_view::npos )
115  
        return path; // Assume it's just an extension
115  
        return path; // Assume it's just an extension
116  
    return path.substr( pos + 1 );
116  
    return path.substr( pos + 1 );
117  
}
117  
}
118  

118  

119  
// Binary search for extension
119  
// Binary search for extension
120  
core::string_view
120  
core::string_view
121  
lookup_ext( core::string_view ext ) noexcept
121  
lookup_ext( core::string_view ext ) noexcept
122  
{
122  
{
123  
    std::size_t lo = 0;
123  
    std::size_t lo = 0;
124  
    std::size_t hi = ext_db_size;
124  
    std::size_t hi = ext_db_size;
125  
    while( lo < hi )
125  
    while( lo < hi )
126  
    {
126  
    {
127  
        auto const mid = lo + ( hi - lo ) / 2;
127  
        auto const mid = lo + ( hi - lo ) / 2;
128  
        auto const cmp = compare_icase( ext_db[mid].ext, ext );
128  
        auto const cmp = compare_icase( ext_db[mid].ext, ext );
129  
        if( cmp < 0 )
129  
        if( cmp < 0 )
130  
            lo = mid + 1;
130  
            lo = mid + 1;
131  
        else if( cmp > 0 )
131  
        else if( cmp > 0 )
132  
            hi = mid;
132  
            hi = mid;
133  
        else
133  
        else
134  
            return ext_db[mid].type;
134  
            return ext_db[mid].type;
135  
    }
135  
    }
136  
    return {};
136  
    return {};
137  
}
137  
}
138  

138  

139  
} // (anon)
139  
} // (anon)
140  

140  

141  
core::string_view
141  
core::string_view
142  
lookup( core::string_view path_or_ext ) noexcept
142  
lookup( core::string_view path_or_ext ) noexcept
143  
{
143  
{
144  
    if( path_or_ext.empty() )
144  
    if( path_or_ext.empty() )
145  
        return {};
145  
        return {};
146  

146  

147  
    // Skip leading dot if present
147  
    // Skip leading dot if present
148  
    if( path_or_ext[0] == '.' )
148  
    if( path_or_ext[0] == '.' )
149  
        path_or_ext.remove_prefix( 1 );
149  
        path_or_ext.remove_prefix( 1 );
150  

150  

151  
    auto const ext = get_extension( path_or_ext );
151  
    auto const ext = get_extension( path_or_ext );
152  
    return lookup_ext( ext );
152  
    return lookup_ext( ext );
153  
}
153  
}
154  

154  

155  
core::string_view
155  
core::string_view
156  
extension( core::string_view type ) noexcept
156  
extension( core::string_view type ) noexcept
157  
{
157  
{
158  
    // Linear search for type -> extension
158  
    // Linear search for type -> extension
159  
    // Could optimize with reverse map if needed
159  
    // Could optimize with reverse map if needed
160  
    for( std::size_t i = 0; i < ext_db_size; ++i )
160  
    for( std::size_t i = 0; i < ext_db_size; ++i )
161  
    {
161  
    {
162  
        if( compare_icase( ext_db[i].type, type ) == 0 )
162  
        if( compare_icase( ext_db[i].type, type ) == 0 )
163  
            return ext_db[i].ext;
163  
            return ext_db[i].ext;
164  
    }
164  
    }
165  
    return {};
165  
    return {};
166  
}
166  
}
167  

167  

168  
core::string_view
168  
core::string_view
169  
charset( core::string_view type ) noexcept
169  
charset( core::string_view type ) noexcept
170  
{
170  
{
171  
    auto const* entry = mime_db::lookup( type );
171  
    auto const* entry = mime_db::lookup( type );
172  
    if( entry )
172  
    if( entry )
173  
        return entry->charset;
173  
        return entry->charset;
174  
    return {};
174  
    return {};
175  
}
175  
}
176  

176  

177  
std::string
177  
std::string
178  
content_type( core::string_view type_or_ext )
178  
content_type( core::string_view type_or_ext )
179  
{
179  
{
180  
    core::string_view type;
180  
    core::string_view type;
181  

181  

182  
    // Check if it looks like an extension
182  
    // Check if it looks like an extension
183  
    if( ! type_or_ext.empty() &&
183  
    if( ! type_or_ext.empty() &&
184  
        ( type_or_ext[0] == '.' ||
184  
        ( type_or_ext[0] == '.' ||
185  
          type_or_ext.find( '/' ) == core::string_view::npos ) )
185  
          type_or_ext.find( '/' ) == core::string_view::npos ) )
186  
    {
186  
    {
187  
        type = lookup( type_or_ext );
187  
        type = lookup( type_or_ext );
188  
        if( type.empty() )
188  
        if( type.empty() )
189  
            return {};
189  
            return {};
190  
    }
190  
    }
191  
    else
191  
    else
192  
    {
192  
    {
193  
        type = type_or_ext;
193  
        type = type_or_ext;
194  
    }
194  
    }
195  

195  

196  
    auto const cs = charset( type );
196  
    auto const cs = charset( type );
197  
    if( cs.empty() )
197  
    if( cs.empty() )
198  
        return std::string( type );
198  
        return std::string( type );
199  

199  

200  
    std::string result;
200  
    std::string result;
201  
    result.reserve( type.size() + 10 + cs.size() );
201  
    result.reserve( type.size() + 10 + cs.size() );
202  
    result.append( type.data(), type.size() );
202  
    result.append( type.data(), type.size() );
203  
    result.append( "; charset=" );
203  
    result.append( "; charset=" );
204  
    result.append( cs.data(), cs.size() );
204  
    result.append( cs.data(), cs.size() );
205  
    return result;
205  
    return result;
206  
}
206  
}
207  

207  

208  
} // mime_types
208  
} // mime_types
209  
} // http
209  
} // http
210  
} // boost
210  
} // boost