xref: /openbmc/bmcweb/src/json_html_serializer.cpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
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 += "&quot";
435                 dumpEscaped(out, i.key());
436                 out += "&quot: ";
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