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