blob: 8322f1dd8a7ddb26d2d4a4f7fba4848ad9a243a6 [file] [log] [blame]
Ed Tanous61e349a2023-05-31 11:57:43 -07001#include "json_html_serializer.hpp"
2
3#include "http_response.hpp"
4
5#include <nlohmann/json.hpp>
6
7#include <algorithm>
8#include <array>
9#include <limits>
10
11namespace json_html_util
12{
13
14static constexpr uint8_t utf8Accept = 0;
15static constexpr uint8_t utf8Reject = 1;
16
17static uint8_t decode(uint8_t& state, uint32_t& codePoint,
18 const uint8_t byte) noexcept
19{
20 // clang-format off
21 static const std::array<std::uint8_t, 400> utf8d =
22 {
23 {
24 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1F
25 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3F
26 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5F
27 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7F
28 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9F
29 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // A0..BF
30 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C0..DF
31 0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF
32 0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF
33 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
34 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
35 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
36 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
37 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8
38 }
39 };
40 // clang-format on
41
42 if (state > 0x8)
43 {
44 return state;
45 }
46
47 const uint8_t type = utf8d[byte];
48
49 codePoint = (state != utf8Accept)
50 ? (byte & 0x3fU) | (codePoint << 6)
51 : static_cast<uint32_t>(0xff >> type) & (byte);
52
53 state = utf8d[256U + state * 16U + type];
54 return state;
55}
56
57static void dumpEscaped(std::string& out, const std::string& str)
58{
59 std::array<char, 512> stringBuffer{{}};
60 uint32_t codePoint = 0;
61 uint8_t state = utf8Accept;
62 std::size_t bytes = 0; // number of bytes written to string_buffer
63
64 // number of bytes written at the point of the last valid byte
65 std::size_t bytesAfterLastAccept = 0;
66 std::size_t undumpedChars = 0;
67
68 for (std::size_t i = 0; i < str.size(); ++i)
69 {
70 const uint8_t byte = static_cast<uint8_t>(str[i]);
71
72 switch (decode(state, codePoint, byte))
73 {
74 case utf8Accept: // decode found a new code point
75 {
76 switch (codePoint)
77 {
78 case 0x08: // backspace
79 {
80 stringBuffer[bytes++] = '\\';
81 stringBuffer[bytes++] = 'b';
82 break;
83 }
84
85 case 0x09: // horizontal tab
86 {
87 stringBuffer[bytes++] = '\\';
88 stringBuffer[bytes++] = 't';
89 break;
90 }
91
92 case 0x0A: // newline
93 {
94 stringBuffer[bytes++] = '\\';
95 stringBuffer[bytes++] = 'n';
96 break;
97 }
98
99 case 0x0C: // formfeed
100 {
101 stringBuffer[bytes++] = '\\';
102 stringBuffer[bytes++] = 'f';
103 break;
104 }
105
106 case 0x0D: // carriage return
107 {
108 stringBuffer[bytes++] = '\\';
109 stringBuffer[bytes++] = 'r';
110 break;
111 }
112
113 case 0x22: // quotation mark
114 {
115 stringBuffer[bytes++] = '&';
116 stringBuffer[bytes++] = 'q';
117 stringBuffer[bytes++] = 'u';
118 stringBuffer[bytes++] = 'o';
119 stringBuffer[bytes++] = 't';
120 stringBuffer[bytes++] = ';';
121 break;
122 }
123
124 case 0x27: // apostrophe
125 {
126 stringBuffer[bytes++] = '&';
127 stringBuffer[bytes++] = 'a';
128 stringBuffer[bytes++] = 'p';
129 stringBuffer[bytes++] = 'o';
130 stringBuffer[bytes++] = 's';
131 stringBuffer[bytes++] = ';';
132 break;
133 }
134
135 case 0x26: // ampersand
136 {
137 stringBuffer[bytes++] = '&';
138 stringBuffer[bytes++] = 'a';
139 stringBuffer[bytes++] = 'm';
140 stringBuffer[bytes++] = 'p';
141 stringBuffer[bytes++] = ';';
142 break;
143 }
144
145 case 0x3C: // less than
146 {
147 stringBuffer[bytes++] = '\\';
148 stringBuffer[bytes++] = 'l';
149 stringBuffer[bytes++] = 't';
150 stringBuffer[bytes++] = ';';
151 break;
152 }
153
154 case 0x3E: // greater than
155 {
156 stringBuffer[bytes++] = '\\';
157 stringBuffer[bytes++] = 'g';
158 stringBuffer[bytes++] = 't';
159 stringBuffer[bytes++] = ';';
160 break;
161 }
162
163 default:
164 {
165 // escape control characters (0x00..0x1F)
166 if ((codePoint <= 0x1F) or (codePoint >= 0x7F))
167 {
168 if (codePoint <= 0xFFFF)
169 {
170 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
171 std::snprintf(&stringBuffer[bytes], 7,
172 "\\u%04x",
173 static_cast<uint16_t>(codePoint));
174 bytes += 6;
175 }
176 else
177 {
178 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
179 std::snprintf(
180 &stringBuffer[bytes], 13, "\\u%04x\\u%04x",
181 static_cast<uint16_t>(0xD7C0 +
182 (codePoint >> 10)),
183 static_cast<uint16_t>(0xDC00 +
184 (codePoint & 0x3FF)));
185 bytes += 12;
186 }
187 }
188 else
189 {
190 // copy byte to buffer (all previous bytes
191 // been copied have in default case above)
192 stringBuffer[bytes++] = str[i];
193 }
194 break;
195 }
196 }
197
198 // write buffer and reset index; there must be 13 bytes
199 // left, as this is the maximal number of bytes to be
200 // written ("\uxxxx\uxxxx\0") for one code point
201 if (stringBuffer.size() - bytes < 13)
202 {
203 out.append(stringBuffer.data(), bytes);
204 bytes = 0;
205 }
206
207 // remember the byte position of this accept
208 bytesAfterLastAccept = bytes;
209 undumpedChars = 0;
210 break;
211 }
212
213 case utf8Reject: // decode found invalid UTF-8 byte
214 {
215 // in case we saw this character the first time, we
216 // would like to read it again, because the byte
217 // may be OK for itself, but just not OK for the
218 // previous sequence
219 if (undumpedChars > 0)
220 {
221 --i;
222 }
223
224 // reset length buffer to the last accepted index;
225 // thus removing/ignoring the invalid characters
226 bytes = bytesAfterLastAccept;
227
228 stringBuffer[bytes++] = '\\';
229 stringBuffer[bytes++] = 'u';
230 stringBuffer[bytes++] = 'f';
231 stringBuffer[bytes++] = 'f';
232 stringBuffer[bytes++] = 'f';
233 stringBuffer[bytes++] = 'd';
234
235 bytesAfterLastAccept = bytes;
236
237 undumpedChars = 0;
238
239 // continue processing the string
240 state = utf8Accept;
241 break;
242 }
243
244 default: // decode found yet incomplete multi-byte code point
245 {
246 ++undumpedChars;
247 break;
248 }
249 }
250 }
251
252 // we finished processing the string
253 if (state == utf8Accept)
254 {
255 // write buffer
256 if (bytes > 0)
257 {
258 out.append(stringBuffer.data(), bytes);
259 }
260 }
261 else
262 {
263 // write all accepted bytes
264 out.append(stringBuffer.data(), bytesAfterLastAccept);
265 out += "\\ufffd";
266 }
267}
268
269static unsigned int countDigits(uint64_t number) noexcept
270{
271 unsigned int nDigits = 1;
272 for (;;)
273 {
274 if (number < 10)
275 {
276 return nDigits;
277 }
278 if (number < 100)
279 {
280 return nDigits + 1;
281 }
282 if (number < 1000)
283 {
284 return nDigits + 2;
285 }
286 if (number < 10000)
287 {
288 return nDigits + 3;
289 }
290 number = number / 10000U;
291 nDigits += 4;
292 }
293}
294
295template <typename NumberType,
296 std::enable_if_t<std::is_same<NumberType, uint64_t>::value or
297 std::is_same<NumberType, int64_t>::value,
298 int> = 0>
299void dumpInteger(std::string& out, NumberType number)
300{
301 std::array<char, 64> numberbuffer{{}};
302
303 static constexpr std::array<std::array<char, 2>, 100> digitsTo99{{
304 {'0', '0'}, {'0', '1'}, {'0', '2'}, {'0', '3'}, {'0', '4'}, {'0', '5'},
305 {'0', '6'}, {'0', '7'}, {'0', '8'}, {'0', '9'}, {'1', '0'}, {'1', '1'},
306 {'1', '2'}, {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, {'1', '7'},
307 {'1', '8'}, {'1', '9'}, {'2', '0'}, {'2', '1'}, {'2', '2'}, {'2', '3'},
308 {'2', '4'}, {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, {'2', '9'},
309 {'3', '0'}, {'3', '1'}, {'3', '2'}, {'3', '3'}, {'3', '4'}, {'3', '5'},
310 {'3', '6'}, {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, {'4', '1'},
311 {'4', '2'}, {'4', '3'}, {'4', '4'}, {'4', '5'}, {'4', '6'}, {'4', '7'},
312 {'4', '8'}, {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, {'5', '3'},
313 {'5', '4'}, {'5', '5'}, {'5', '6'}, {'5', '7'}, {'5', '8'}, {'5', '9'},
314 {'6', '0'}, {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, {'6', '5'},
315 {'6', '6'}, {'6', '7'}, {'6', '8'}, {'6', '9'}, {'7', '0'}, {'7', '1'},
316 {'7', '2'}, {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, {'7', '7'},
317 {'7', '8'}, {'7', '9'}, {'8', '0'}, {'8', '1'}, {'8', '2'}, {'8', '3'},
318 {'8', '4'}, {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, {'8', '9'},
319 {'9', '0'}, {'9', '1'}, {'9', '2'}, {'9', '3'}, {'9', '4'}, {'9', '5'},
320 {'9', '6'}, {'9', '7'}, {'9', '8'}, {'9', '9'},
321 }};
322
323 // special case for "0"
324 if (number == 0)
325 {
326 out += '0';
327 return;
328 }
329
330 // use a pointer to fill the buffer
331 auto* bufferPtr = numberbuffer.begin();
332
333 const bool isNegative = std::is_same<NumberType, int64_t>::value &&
334 !(number >= 0); // see issue #755
335 uint64_t absValue = 0;
336
337 unsigned int nChars = 0;
338
339 if (isNegative)
340 {
341 *bufferPtr = '-';
342 absValue = static_cast<uint64_t>(0 - number);
343
344 // account one more byte for the minus sign
345 nChars = 1 + countDigits(absValue);
346 }
347 else
348 {
349 absValue = static_cast<uint64_t>(number);
350 nChars = countDigits(absValue);
351 }
352
353 // spare 1 byte for '\0'
354 if (nChars >= numberbuffer.size() - 1)
355 {
356 return;
357 }
358
359 // jump to the end to generate the string from backward
360 // so we later avoid reversing the result
361 std::advance(bufferPtr, nChars - 1);
362
363 // Fast int2ascii implementation inspired by "Fastware" talk by Andrei
364 // Alexandrescu See: https://www.youtube.com/watch?v=o4-CwDo2zpg
365 while (absValue >= 100)
366 {
367 const auto digitsIndex = static_cast<unsigned>((absValue % 100));
368 absValue /= 100;
369 *bufferPtr = digitsTo99[digitsIndex][1];
370 bufferPtr = std::prev(bufferPtr);
371 *bufferPtr = digitsTo99[digitsIndex][0];
372 bufferPtr = std::prev(bufferPtr);
373 }
374
375 if (absValue >= 10)
376 {
377 const auto digitsIndex = static_cast<unsigned>(absValue);
378 *bufferPtr = digitsTo99[digitsIndex][1];
379 bufferPtr = std::prev(bufferPtr);
380 *bufferPtr = digitsTo99[digitsIndex][0];
381 // assignment never used: bufferPtr = std::prev(bufferPtr);
382 }
383 else
384 {
385 *bufferPtr = static_cast<char>('0' + absValue);
386 // assignment never used: bufferPtr = std::prev(bufferPtr);
387 }
388
389 out.append(numberbuffer.data(), nChars);
390}
391
392static void dumpfloat(std::string& out, double number)
393{
394 // NaN / inf
395 if (!std::isfinite(number))
396 {
397 out += "null";
398 return;
399 }
400 std::array<char, 64> numberbuffer{{}};
401
402 ::nlohmann::detail::to_chars(numberbuffer.begin(), numberbuffer.end(),
403 number);
404
405 out += numberbuffer.data();
406}
407
408static void dump(std::string& out, const nlohmann::json& val)
409{
410 switch (val.type())
411 {
412 case nlohmann::json::value_t::object:
413 {
414 if (val.empty())
415 {
416 out += "{}";
417 return;
418 }
419
420 out += "{";
421
422 out += "<div class=tab>";
423 for (auto i = val.begin(); i != val.end();)
424 {
425 out += "&quot";
426 dumpEscaped(out, i.key());
427 out += "&quot: ";
428
429 bool inATag = false;
430 if (i.key() == "@odata.id" || i.key() == "@odata.context" ||
431 i.key() == "Members@odata.nextLink" || i.key() == "Uri")
432 {
433 inATag = true;
434 out += "<a href=\"";
435 dumpEscaped(out, i.value());
436 out += "\">";
437 }
438 dump(out, i.value());
439 if (inATag)
440 {
441 out += "</a>";
442 }
443 i++;
444 if (i != val.end())
445 {
446 out += ",";
447 }
448 out += "<br>";
449 }
450 out += "</div>";
451 out += '}';
452
453 return;
454 }
455
456 case nlohmann::json::value_t::array:
457 {
458 if (val.empty())
459 {
460 out += "[]";
461 return;
462 }
463
464 out += "[";
465
466 out += "<div class=tab>";
467
468 // first n-1 elements
469 for (auto i = val.cbegin(); i != val.cend() - 1; ++i)
470 {
471 dump(out, *i);
472 out += ",<br>";
473 }
474
475 // last element
476 dump(out, val.back());
477
478 out += "</div>";
479 out += ']';
480
481 return;
482 }
483
484 case nlohmann::json::value_t::string:
485 {
486 out += '\"';
487 const std::string* ptr = val.get_ptr<const std::string*>();
488 dumpEscaped(out, *ptr);
489 out += '\"';
490 return;
491 }
492
493 case nlohmann::json::value_t::boolean:
494 {
495 if (*(val.get_ptr<const bool*>()))
496 {
497 out += "true";
498 }
499 else
500 {
501 out += "false";
502 }
503 return;
504 }
505
506 case nlohmann::json::value_t::number_integer:
507 {
508 dumpInteger(out, *(val.get_ptr<const int64_t*>()));
509 return;
510 }
511
512 case nlohmann::json::value_t::number_unsigned:
513 {
514 dumpInteger(out, *(val.get_ptr<const uint64_t*>()));
515 return;
516 }
517
518 case nlohmann::json::value_t::number_float:
519 {
520 dumpfloat(out, *(val.get_ptr<const double*>()));
521 return;
522 }
523
524 case nlohmann::json::value_t::discarded:
525 {
526 out += "<discarded>";
527 return;
528 }
529
530 case nlohmann::json::value_t::null:
531 {
532 out += "null";
533 return;
534 }
535 case nlohmann::json::value_t::binary:
536 {
537 // Do nothing; Should never happen.
538 return;
539 }
540 }
541}
542
543void dumpHtml(std::string& out, const nlohmann::json& json)
544{
545 out += "<html>\n"
546 "<head>\n"
547 "<title>Redfish API</title>\n"
548 "<link href=\"/redfish.css\" rel=\"stylesheet\">\n"
549 "</head>\n"
550 "<body>\n"
551 "<div class=\"container\">\n"
552 "<img src=\"/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" "
553 "height=\"406px\" "
554 "width=\"576px\">\n"
555 "<div class=\"content\">\n";
556 dump(out, json);
557 out += "</div>\n"
558 "</div>\n"
559 "</body>\n"
560 "</html>\n";
561}
562
563void prettyPrintJson(crow::Response& res)
564{
Ed Tanous27b0cf92023-08-07 12:02:40 -0700565 std::string html;
566 json_html_util::dumpHtml(html, res.jsonValue);
Ed Tanous61e349a2023-05-31 11:57:43 -0700567
Ed Tanous27b0cf92023-08-07 12:02:40 -0700568 res.write(std::move(html));
Ed Tanous61e349a2023-05-31 11:57:43 -0700569 res.addHeader(boost::beast::http::field::content_type,
570 "text/html;charset=UTF-8");
571}
572
573} // namespace json_html_util