diff options
author | Ed Tanous <edtanous@google.com> | 2023-05-31 21:57:43 +0300 |
---|---|---|
committer | Ed Tanous <ed@tanous.net> | 2023-06-05 23:27:48 +0300 |
commit | 61e349acc34787ad80aebf7809ab73c4f03c7520 (patch) | |
tree | 2b2625ed96b3eeb0c839201b3891d99eb185651b /src | |
parent | 5e44e3d85bae016c7ccc27e9ee65627919a51898 (diff) | |
download | bmcweb-61e349acc34787ad80aebf7809ab73c4f03c7520.tar.xz |
Break out serializer into its own cpp file
This commit is entirely just moving code, such that not all compile
units need to pull in the full html serializer.
Tested: Unit tests pass. Pretty good coverage.
Redfish service validator passes.
Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: Ifaebe9534c0693dc678fd994517563b89aca0cc5
Diffstat (limited to 'src')
-rw-r--r-- | src/json_html_serializer.cpp | 571 |
1 files changed, 571 insertions, 0 deletions
diff --git a/src/json_html_serializer.cpp b/src/json_html_serializer.cpp new file mode 100644 index 0000000000..405061beee --- /dev/null +++ b/src/json_html_serializer.cpp @@ -0,0 +1,571 @@ +#include "json_html_serializer.hpp" + +#include "http_response.hpp" + +#include <nlohmann/json.hpp> + +#include <algorithm> +#include <array> +#include <limits> + +namespace json_html_util +{ + +static constexpr uint8_t utf8Accept = 0; +static constexpr uint8_t utf8Reject = 1; + +static uint8_t decode(uint8_t& state, uint32_t& codePoint, + const uint8_t byte) noexcept +{ + // clang-format off + static const std::array<std::uint8_t, 400> utf8d = + { + { + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF + 0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 + 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 + 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 + 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 + 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 + } + }; + // clang-format on + + if (state > 0x8) + { + return state; + } + + const uint8_t type = utf8d[byte]; + + codePoint = (state != utf8Accept) + ? (byte & 0x3fU) | (codePoint << 6) + : static_cast<uint32_t>(0xff >> type) & (byte); + + state = utf8d[256U + state * 16U + type]; + return state; +} + +static void dumpEscaped(std::string& out, const std::string& str) +{ + std::array<char, 512> stringBuffer{{}}; + uint32_t codePoint = 0; + uint8_t state = utf8Accept; + std::size_t bytes = 0; // number of bytes written to string_buffer + + // number of bytes written at the point of the last valid byte + std::size_t bytesAfterLastAccept = 0; + std::size_t undumpedChars = 0; + + for (std::size_t i = 0; i < str.size(); ++i) + { + const uint8_t byte = static_cast<uint8_t>(str[i]); + + switch (decode(state, codePoint, byte)) + { + case utf8Accept: // decode found a new code point + { + switch (codePoint) + { + case 0x08: // backspace + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'b'; + break; + } + + case 0x09: // horizontal tab + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 't'; + break; + } + + case 0x0A: // newline + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'n'; + break; + } + + case 0x0C: // formfeed + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'f'; + break; + } + + case 0x0D: // carriage return + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'r'; + break; + } + + case 0x22: // quotation mark + { + stringBuffer[bytes++] = '&'; + stringBuffer[bytes++] = 'q'; + stringBuffer[bytes++] = 'u'; + stringBuffer[bytes++] = 'o'; + stringBuffer[bytes++] = 't'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x27: // apostrophe + { + stringBuffer[bytes++] = '&'; + stringBuffer[bytes++] = 'a'; + stringBuffer[bytes++] = 'p'; + stringBuffer[bytes++] = 'o'; + stringBuffer[bytes++] = 's'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x26: // ampersand + { + stringBuffer[bytes++] = '&'; + stringBuffer[bytes++] = 'a'; + stringBuffer[bytes++] = 'm'; + stringBuffer[bytes++] = 'p'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x3C: // less than + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'l'; + stringBuffer[bytes++] = 't'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x3E: // greater than + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'g'; + stringBuffer[bytes++] = 't'; + stringBuffer[bytes++] = ';'; + break; + } + + default: + { + // escape control characters (0x00..0x1F) + if ((codePoint <= 0x1F) or (codePoint >= 0x7F)) + { + if (codePoint <= 0xFFFF) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + std::snprintf(&stringBuffer[bytes], 7, + "\\u%04x", + static_cast<uint16_t>(codePoint)); + bytes += 6; + } + else + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + std::snprintf( + &stringBuffer[bytes], 13, "\\u%04x\\u%04x", + static_cast<uint16_t>(0xD7C0 + + (codePoint >> 10)), + static_cast<uint16_t>(0xDC00 + + (codePoint & 0x3FF))); + bytes += 12; + } + } + else + { + // copy byte to buffer (all previous bytes + // been copied have in default case above) + stringBuffer[bytes++] = str[i]; + } + break; + } + } + + // write buffer and reset index; there must be 13 bytes + // left, as this is the maximal number of bytes to be + // written ("\uxxxx\uxxxx\0") for one code point + if (stringBuffer.size() - bytes < 13) + { + out.append(stringBuffer.data(), bytes); + bytes = 0; + } + + // remember the byte position of this accept + bytesAfterLastAccept = bytes; + undumpedChars = 0; + break; + } + + case utf8Reject: // decode found invalid UTF-8 byte + { + // in case we saw this character the first time, we + // would like to read it again, because the byte + // may be OK for itself, but just not OK for the + // previous sequence + if (undumpedChars > 0) + { + --i; + } + + // reset length buffer to the last accepted index; + // thus removing/ignoring the invalid characters + bytes = bytesAfterLastAccept; + + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'u'; + stringBuffer[bytes++] = 'f'; + stringBuffer[bytes++] = 'f'; + stringBuffer[bytes++] = 'f'; + stringBuffer[bytes++] = 'd'; + + bytesAfterLastAccept = bytes; + + undumpedChars = 0; + + // continue processing the string + state = utf8Accept; + break; + } + + default: // decode found yet incomplete multi-byte code point + { + ++undumpedChars; + break; + } + } + } + + // we finished processing the string + if (state == utf8Accept) + { + // write buffer + if (bytes > 0) + { + out.append(stringBuffer.data(), bytes); + } + } + else + { + // write all accepted bytes + out.append(stringBuffer.data(), bytesAfterLastAccept); + out += "\\ufffd"; + } +} + +static unsigned int countDigits(uint64_t number) noexcept +{ + unsigned int nDigits = 1; + for (;;) + { + if (number < 10) + { + return nDigits; + } + if (number < 100) + { + return nDigits + 1; + } + if (number < 1000) + { + return nDigits + 2; + } + if (number < 10000) + { + return nDigits + 3; + } + number = number / 10000U; + nDigits += 4; + } +} + +template <typename NumberType, + std::enable_if_t<std::is_same<NumberType, uint64_t>::value or + std::is_same<NumberType, int64_t>::value, + int> = 0> +void dumpInteger(std::string& out, NumberType number) +{ + std::array<char, 64> numberbuffer{{}}; + + static constexpr std::array<std::array<char, 2>, 100> digitsTo99{{ + {'0', '0'}, {'0', '1'}, {'0', '2'}, {'0', '3'}, {'0', '4'}, {'0', '5'}, + {'0', '6'}, {'0', '7'}, {'0', '8'}, {'0', '9'}, {'1', '0'}, {'1', '1'}, + {'1', '2'}, {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, {'1', '7'}, + {'1', '8'}, {'1', '9'}, {'2', '0'}, {'2', '1'}, {'2', '2'}, {'2', '3'}, + {'2', '4'}, {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, {'2', '9'}, + {'3', '0'}, {'3', '1'}, {'3', '2'}, {'3', '3'}, {'3', '4'}, {'3', '5'}, + {'3', '6'}, {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, {'4', '1'}, + {'4', '2'}, {'4', '3'}, {'4', '4'}, {'4', '5'}, {'4', '6'}, {'4', '7'}, + {'4', '8'}, {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, {'5', '3'}, + {'5', '4'}, {'5', '5'}, {'5', '6'}, {'5', '7'}, {'5', '8'}, {'5', '9'}, + {'6', '0'}, {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, {'6', '5'}, + {'6', '6'}, {'6', '7'}, {'6', '8'}, {'6', '9'}, {'7', '0'}, {'7', '1'}, + {'7', '2'}, {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, {'7', '7'}, + {'7', '8'}, {'7', '9'}, {'8', '0'}, {'8', '1'}, {'8', '2'}, {'8', '3'}, + {'8', '4'}, {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, {'8', '9'}, + {'9', '0'}, {'9', '1'}, {'9', '2'}, {'9', '3'}, {'9', '4'}, {'9', '5'}, + {'9', '6'}, {'9', '7'}, {'9', '8'}, {'9', '9'}, + }}; + + // special case for "0" + if (number == 0) + { + out += '0'; + return; + } + + // use a pointer to fill the buffer + auto* bufferPtr = numberbuffer.begin(); + + const bool isNegative = std::is_same<NumberType, int64_t>::value && + !(number >= 0); // see issue #755 + uint64_t absValue = 0; + + unsigned int nChars = 0; + + if (isNegative) + { + *bufferPtr = '-'; + absValue = static_cast<uint64_t>(0 - number); + + // account one more byte for the minus sign + nChars = 1 + countDigits(absValue); + } + else + { + absValue = static_cast<uint64_t>(number); + nChars = countDigits(absValue); + } + + // spare 1 byte for '\0' + if (nChars >= numberbuffer.size() - 1) + { + return; + } + + // jump to the end to generate the string from backward + // so we later avoid reversing the result + std::advance(bufferPtr, nChars - 1); + + // Fast int2ascii implementation inspired by "Fastware" talk by Andrei + // Alexandrescu See: https://www.youtube.com/watch?v=o4-CwDo2zpg + while (absValue >= 100) + { + const auto digitsIndex = static_cast<unsigned>((absValue % 100)); + absValue /= 100; + *bufferPtr = digitsTo99[digitsIndex][1]; + bufferPtr = std::prev(bufferPtr); + *bufferPtr = digitsTo99[digitsIndex][0]; + bufferPtr = std::prev(bufferPtr); + } + + if (absValue >= 10) + { + const auto digitsIndex = static_cast<unsigned>(absValue); + *bufferPtr = digitsTo99[digitsIndex][1]; + bufferPtr = std::prev(bufferPtr); + *bufferPtr = digitsTo99[digitsIndex][0]; + // assignment never used: bufferPtr = std::prev(bufferPtr); + } + else + { + *bufferPtr = static_cast<char>('0' + absValue); + // assignment never used: bufferPtr = std::prev(bufferPtr); + } + + out.append(numberbuffer.data(), nChars); +} + +static void dumpfloat(std::string& out, double number) +{ + // NaN / inf + if (!std::isfinite(number)) + { + out += "null"; + return; + } + std::array<char, 64> numberbuffer{{}}; + + ::nlohmann::detail::to_chars(numberbuffer.begin(), numberbuffer.end(), + number); + + out += numberbuffer.data(); +} + +static void dump(std::string& out, const nlohmann::json& val) +{ + switch (val.type()) + { + case nlohmann::json::value_t::object: + { + if (val.empty()) + { + out += "{}"; + return; + } + + out += "{"; + + out += "<div class=tab>"; + for (auto i = val.begin(); i != val.end();) + { + out += """; + dumpEscaped(out, i.key()); + out += "": "; + + bool inATag = false; + if (i.key() == "@odata.id" || i.key() == "@odata.context" || + i.key() == "Members@odata.nextLink" || i.key() == "Uri") + { + inATag = true; + out += "<a href=\""; + dumpEscaped(out, i.value()); + out += "\">"; + } + dump(out, i.value()); + if (inATag) + { + out += "</a>"; + } + i++; + if (i != val.end()) + { + out += ","; + } + out += "<br>"; + } + out += "</div>"; + out += '}'; + + return; + } + + case nlohmann::json::value_t::array: + { + if (val.empty()) + { + out += "[]"; + return; + } + + out += "["; + + out += "<div class=tab>"; + + // first n-1 elements + for (auto i = val.cbegin(); i != val.cend() - 1; ++i) + { + dump(out, *i); + out += ",<br>"; + } + + // last element + dump(out, val.back()); + + out += "</div>"; + out += ']'; + + return; + } + + case nlohmann::json::value_t::string: + { + out += '\"'; + const std::string* ptr = val.get_ptr<const std::string*>(); + dumpEscaped(out, *ptr); + out += '\"'; + return; + } + + case nlohmann::json::value_t::boolean: + { + if (*(val.get_ptr<const bool*>())) + { + out += "true"; + } + else + { + out += "false"; + } + return; + } + + case nlohmann::json::value_t::number_integer: + { + dumpInteger(out, *(val.get_ptr<const int64_t*>())); + return; + } + + case nlohmann::json::value_t::number_unsigned: + { + dumpInteger(out, *(val.get_ptr<const uint64_t*>())); + return; + } + + case nlohmann::json::value_t::number_float: + { + dumpfloat(out, *(val.get_ptr<const double*>())); + return; + } + + case nlohmann::json::value_t::discarded: + { + out += "<discarded>"; + return; + } + + case nlohmann::json::value_t::null: + { + out += "null"; + return; + } + case nlohmann::json::value_t::binary: + { + // Do nothing; Should never happen. + return; + } + } +} + +void dumpHtml(std::string& out, const nlohmann::json& json) +{ + out += "<html>\n" + "<head>\n" + "<title>Redfish API</title>\n" + "<link href=\"/redfish.css\" rel=\"stylesheet\">\n" + "</head>\n" + "<body>\n" + "<div class=\"container\">\n" + "<img src=\"/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" " + "height=\"406px\" " + "width=\"576px\">\n" + "<div class=\"content\">\n"; + dump(out, json); + out += "</div>\n" + "</div>\n" + "</body>\n" + "</html>\n"; +} + +void prettyPrintJson(crow::Response& res) +{ + json_html_util::dumpHtml(res.body(), res.jsonValue); + + res.addHeader(boost::beast::http::field::content_type, + "text/html;charset=UTF-8"); +} + +} // namespace json_html_util |