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