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 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 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 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> 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 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 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 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 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