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