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 dumpEscaped(out, i.value());
445 out += "\">";
446 }
447 dump(out, i.value());
448 if (inATag)
449 {
450 out += "</a>";
451 }
452 i++;
453 if (i != val.end())
454 {
455 out += ",";
456 }
457 out += "<br>";
458 }
459 out += "</div>";
460 out += '}';
461
462 return;
463 }
464
465 case nlohmann::json::value_t::array:
466 {
467 if (val.empty())
468 {
469 out += "[]";
470 return;
471 }
472
473 out += "[";
474
475 out += "<div class=tab>";
476
477 // first n-1 elements
478 for (auto i = val.cbegin(); i != val.cend() - 1; ++i)
479 {
480 dump(out, *i);
481 out += ",<br>";
482 }
483
484 // last element
485 dump(out, val.back());
486
487 out += "</div>";
488 out += ']';
489
490 return;
491 }
492
493 case nlohmann::json::value_t::string:
494 {
495 out += '\"';
496 const std::string* ptr = val.get_ptr<const std::string*>();
497 if (ptr == nullptr)
498 {
499 return;
500 }
501 dumpEscaped(out, *ptr);
502 out += '\"';
503 return;
504 }
505
506 case nlohmann::json::value_t::boolean:
507 {
508 if (*(val.get_ptr<const bool*>()))
509 {
510 out += "true";
511 }
512 else
513 {
514 out += "false";
515 }
516 return;
517 }
518
519 case nlohmann::json::value_t::number_integer:
520 {
521 dumpInteger(out, *(val.get_ptr<const int64_t*>()));
522 return;
523 }
524
525 case nlohmann::json::value_t::number_unsigned:
526 {
527 dumpInteger(out, *(val.get_ptr<const uint64_t*>()));
528 return;
529 }
530
531 case nlohmann::json::value_t::number_float:
532 {
533 dumpfloat(out, *(val.get_ptr<const double*>()));
534 return;
535 }
536
537 case nlohmann::json::value_t::discarded:
538 {
539 out += "<discarded>";
540 return;
541 }
542
543 case nlohmann::json::value_t::null:
544 {
545 out += "null";
546 return;
547 }
548 default:
549 {
550 // Do nothing; Should never happen.
551 return;
552 }
553 }
554 }
555
dumpHtml(std::string & out,const nlohmann::json & json)556 void dumpHtml(std::string& out, const nlohmann::json& json)
557 {
558 out += "<html>\n"
559 "<head>\n"
560 "<title>Redfish API</title>\n"
561 "<link href=\"/styles/redfish.css\" rel=\"stylesheet\">\n"
562 "</head>\n"
563 "<body>\n"
564 "<div class=\"container\">\n"
565 "<img src=\"/images/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" "
566 "height=\"406px\" "
567 "width=\"576px\">\n"
568 "<div class=\"content\">\n";
569 dump(out, json);
570 out += "</div>\n"
571 "</div>\n"
572 "</body>\n"
573 "</html>\n";
574 }
575
prettyPrintJson(crow::Response & res)576 void prettyPrintJson(crow::Response& res)
577 {
578 std::string html;
579 json_html_util::dumpHtml(html, res.jsonValue);
580
581 res.write(std::move(html));
582 res.addHeader(boost::beast::http::field::content_type,
583 "text/html;charset=UTF-8");
584 }
585
586 } // namespace json_html_util
587