/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 |