Coverage Report

Created: 2026-03-31 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pistache/src/common/mime.cc
Line
Count
Source
1
/*
2
 * SPDX-FileCopyrightText: 2015 Mathieu Stefani
3
 *
4
 * SPDX-License-Identifier: Apache-2.0
5
 */
6
7
/* mime.cc
8
   Mathieu Stefani, 29 August 2015
9
10
   Implementation of MIME Type parsing
11
*/
12
13
#include <cstring>
14
15
#include <pistache/http.h>
16
#include <pistache/mime.h>
17
18
namespace Pistache::Http::Mime
19
{
20
21
    std::string Q::toString() const
22
0
    {
23
0
        if (val_ == 0)
24
0
            return "q=0";
25
0
        else if (val_ == 100)
26
0
            return "q=1";
27
28
0
        char buff[sizeof("q=0.99")] = {};
29
0
        const char* const format    = val_ % 10 == 0 ? "q=%.1f" : "q=%.2f";
30
31
0
        const int len = snprintf(buff, sizeof(buff), format, val_ / 100.0);
32
33
0
        return std::string(buff, len);
34
0
    }
35
36
    MediaType MediaType::fromString(const std::string& str)
37
0
    {
38
0
        return fromRaw(str.c_str(), str.size());
39
0
    }
40
41
    MediaType MediaType::fromString(std::string&& str)
42
0
    {
43
0
        return fromRaw(str.c_str(), str.size());
44
0
    }
45
46
    MediaType MediaType::fromRaw(const char* str, size_t len)
47
142k
    {
48
142k
        MediaType res;
49
50
142k
        res.parseRaw(str, len);
51
142k
        return res;
52
142k
    }
53
54
    MediaType MediaType::fromFile(const char* fileName)
55
0
    {
56
0
        const char* extensionOffset = nullptr;
57
0
        const char* p               = fileName;
58
0
        while (*p)
59
0
        {
60
0
            if (*p == '.')
61
0
                extensionOffset = p;
62
0
            ++p;
63
0
        }
64
65
0
        if (!extensionOffset)
66
0
            return MediaType();
67
68
0
        ++extensionOffset;
69
70
0
        struct Extension
71
0
        {
72
0
            const char* const raw;
73
0
            Mime::Type top;
74
0
            Mime::Subtype sub;
75
0
        };
76
77
        // @Data: maybe one day try to export
78
        // http://www.iana.org/assignments/media-types/media-types.xhtml as an
79
        // item-list
80
81
0
        static constexpr Extension KnownExtensions[] = {
82
0
            { "jpg", Type::Image, Subtype::Jpeg },
83
0
            { "jpeg", Type::Image, Subtype::Jpeg },
84
0
            { "png", Type::Image, Subtype::Png },
85
0
            { "bmp", Type::Image, Subtype::Bmp },
86
87
0
            { "txt", Type::Text, Subtype::Plain },
88
0
            { "md", Type::Text, Subtype::Plain },
89
90
0
            { "bin", Type::Application, Subtype::OctetStream },
91
0
        };
92
93
0
        for (const auto& ext : KnownExtensions)
94
0
        {
95
0
            if (!strcmp(extensionOffset, ext.raw))
96
0
            {
97
0
                return MediaType(ext.top, ext.sub);
98
0
            }
99
0
        }
100
101
0
        return MediaType();
102
0
    }
103
104
    void MediaType::parseRaw(const char* str, size_t len)
105
146k
    {
106
146k
        auto raise = [](const char* str) {
107
            // TODO: eventually, we should throw a more generic exception
108
            // that could then be catched in lower stack frames to rethrow
109
            // an HttpError
110
5.57k
            throw HttpError(Http::Code::Unsupported_Media_Type, str);
111
5.57k
        };
112
113
146k
        RawStreamBuf<char> buf(const_cast<char*>(str), len);
114
146k
        StreamCursor cursor(&buf);
115
116
146k
        raw_ = std::string(str, len);
117
118
146k
        Mime::Type top = Type::None;
119
120
        // The reason we are using a do { } while (0); syntax construct here is to
121
        // emulate if / else-if. Since we are using items-list macros to compare the
122
        // strings, we want to avoid evaluating all the branches when one of them
123
        // evaluates to true.
124
        //
125
        // Instead, we break the loop when a branch evaluates to true so that we do
126
        // not evaluate all the subsequent ones.
127
        //
128
        // Watch out, this pattern is repeated throughout the function
129
146k
        do
130
146k
        {
131
146k
#define TYPE(val, s)                                                         \
132
185k
    if (match_string(s, sizeof s - 1, cursor, CaseSensitivity::Insensitive)) \
133
185k
    {                                                                        \
134
141k
        top = Type::val;                                                     \
135
141k
        break;                                                               \
136
141k
    }
137
146k
            MIME_TYPES
138
4.71k
#undef TYPE
139
4.71k
            raise("Unknown Media Type");
140
4.71k
        } while (false);
141
142
146k
        top_ = top;
143
144
146k
        if (!match_literal('/', cursor))
145
135
            raise("Malformed Media Type, expected a '/' after the top type");
146
147
146k
        if (cursor.eof())
148
19
            raise("Malformed Media type, missing subtype");
149
150
        // Parse subtype
151
146k
        Mime::Subtype sub;
152
153
146k
        StreamCursor::Token subToken(cursor);
154
155
146k
        if (match_raw("vnd.", 4, cursor))
156
210
        {
157
210
            sub = Subtype::Vendor;
158
210
        }
159
145k
        else
160
145k
        {
161
145k
            do
162
145k
            {
163
145k
#define SUB_TYPE(val, s)                                                     \
164
389k
    if (match_string(s, sizeof s - 1, cursor, CaseSensitivity::Insensitive)) \
165
389k
    {                                                                        \
166
131k
        sub = Subtype::val;                                                  \
167
131k
        break;                                                               \
168
131k
    }
169
145k
                MIME_SUBTYPES
170
14.1k
#undef SUB_TYPE
171
14.1k
                sub = Subtype::Ext;
172
14.1k
            } while (false);
173
145k
        }
174
175
146k
        if (sub == Subtype::Ext || sub == Subtype::Vendor)
176
9.52k
        {
177
9.52k
            (void)match_until({ ';', '+' }, cursor);
178
9.52k
            rawSubIndex.beg = subToken.start();
179
9.52k
            rawSubIndex.end = subToken.end() - 1;
180
9.52k
        }
181
182
146k
        sub_ = sub;
183
184
146k
        if (cursor.eof())
185
5.43k
            return;
186
187
        // Parse suffix
188
140k
        Mime::Suffix suffix = Suffix::None;
189
140k
        if (match_literal('+', cursor))
190
2.36k
        {
191
192
2.36k
            if (cursor.eof())
193
5
                raise("Malformed Media Type, expected suffix, got EOF");
194
195
2.36k
            StreamCursor::Token suffixToken(cursor);
196
197
2.36k
            do
198
2.36k
            {
199
2.36k
#define SUFFIX(val, s, _)                                                    \
200
12.3k
    if (match_string(s, sizeof s - 1, cursor, CaseSensitivity::Insensitive)) \
201
12.3k
    {                                                                        \
202
1.43k
        suffix = Suffix::val;                                                \
203
1.43k
        break;                                                               \
204
1.43k
    }
205
2.36k
                MIME_SUFFIXES
206
939
#undef SUFFIX
207
939
                suffix = Suffix::Ext;
208
939
            } while (false);
209
210
2.36k
            if (suffix == Suffix::Ext)
211
934
            {
212
934
                (void)match_until({ ';', '+' }, cursor);
213
934
                rawSuffixIndex.beg = suffixToken.start();
214
934
                rawSuffixIndex.end = suffixToken.end() - 1;
215
934
            }
216
217
2.36k
            suffix_ = suffix;
218
2.36k
        }
219
220
        // Parse parameters
221
347k
        while (!cursor.eof())
222
207k
        {
223
224
207k
            if (cursor.current() == ';' || cursor.current() == ' ')
225
39.6k
            {
226
39.6k
                int c;
227
39.6k
                if ((c = cursor.next()) == StreamCursor::Eof || c == 0)
228
124
                    raise("Malformed Media Type, expected parameter got EOF");
229
39.6k
                cursor.advance(1);
230
39.6k
            }
231
232
167k
            else if (match_literal('q', cursor))
233
2.56k
            {
234
235
2.56k
                if (cursor.eof())
236
14
                    raise("Invalid quality factor");
237
238
2.56k
                if (match_literal('=', cursor))
239
2.53k
                {
240
2.53k
                    float val;
241
2.53k
                    std::size_t qvalue_len;
242
243
2.53k
                    if (!Header::strToQvalue(cursor.offset(), &val, &qvalue_len))
244
90
                    {
245
90
                        raise("Invalid quality factor");
246
90
                    }
247
2.53k
                    cursor.advance(qvalue_len);
248
2.53k
                    q_ = Q::fromFloat(val);
249
2.53k
                }
250
32
                else
251
32
                {
252
32
                    raise("Missing quality factor");
253
32
                }
254
2.56k
            }
255
165k
            else
256
165k
            {
257
165k
                StreamCursor::Token keyToken(cursor);
258
165k
                (void)match_until('=', cursor);
259
260
165k
                int c;
261
165k
                if (cursor.eof() || (c = cursor.next()) == StreamCursor::Eof || c == 0)
262
457
                    raise("Unfinished Media Type parameter");
263
264
165k
                std::string key = keyToken.text();
265
165k
                cursor.advance(1);
266
267
165k
                StreamCursor::Token valueToken(cursor);
268
165k
                (void)match_until({ ' ', ';' }, cursor);
269
165k
                params.insert(std::make_pair(std::move(key), valueToken.text()));
270
165k
            }
271
207k
        }
272
140k
    }
273
274
0
    void MediaType::setQuality(Q quality) { q_ = quality; }
275
276
    std::optional<std::string> MediaType::getParam(const std::string& name) const
277
0
    {
278
0
        auto it = params.find(name);
279
0
        if (it == std::end(params))
280
0
        {
281
0
            return std::nullopt;
282
0
        }
283
284
0
        return std::optional<std::string>(it->second);
285
0
    }
286
287
    void MediaType::setParam(const std::string& name, std::string value)
288
0
    {
289
0
        params[name] = std::move(value);
290
0
    }
291
292
    std::string MediaType::toString() const
293
136k
    {
294
295
136k
        if (!raw_.empty())
296
136k
            return raw_;
297
298
1
        auto topString = [](Mime::Type top) -> const char* {
299
1
            switch (top)
300
1
            {
301
0
#define TYPE(val, str)    \
302
0
    case Mime::Type::val: \
303
0
        return str;
304
0
                MIME_TYPES
305
0
#undef TYPE
306
1
            default:
307
1
                return "";
308
1
            }
309
1
        };
310
311
1
        auto subString = [](Mime::Subtype sub) -> const char* {
312
1
            switch (sub)
313
1
            {
314
0
#define SUB_TYPE(val, str)   \
315
0
    case Mime::Subtype::val: \
316
0
        return str;
317
0
                MIME_SUBTYPES
318
0
#undef TYPE
319
1
            default:
320
1
                return "";
321
1
            }
322
1
        };
323
324
1
        auto suffixString = [](Mime::Suffix suffix) -> const char* {
325
0
            switch (suffix)
326
0
            {
327
0
#define SUFFIX(val, str, _) \
328
0
    case Mime::Suffix::val: \
329
0
        return "+" str;
330
0
                MIME_SUFFIXES
331
0
#undef SUFFIX
332
0
            default:
333
0
                return "";
334
0
            }
335
0
        };
336
337
1
        std::string res;
338
1
        res.reserve(128);
339
1
        res += topString(top_);
340
1
        res += "/";
341
1
        res += subString(sub_);
342
1
        if (suffix_ != Suffix::None)
343
0
        {
344
0
            res += suffixString(suffix_);
345
0
        }
346
347
1
        if (q_.has_value())
348
0
        {
349
0
            Q quality = *q_;
350
0
            res += "; ";
351
0
            res += quality.toString();
352
0
        }
353
354
1
        for (const auto& param : params)
355
0
        {
356
0
            res += "; ";
357
0
            res += param.first + "=" + param.second;
358
0
        }
359
360
1
        return res;
361
136k
    }
362
363
    bool MediaType::isValid() const
364
0
    {
365
0
        return top_ != Type::None && sub_ != Subtype::None;
366
0
    }
367
368
} // namespace Pistache::Http::Mime