log_services: Add download of post code log entries

- Add a GET method /redfish/v1/Systems/system/LogServices/PostCodes
  /Entries/<str>/attachment/, Get the attribute value through the
  getPostCodes method and encode it as base64, and send it off.

- This allows the use to offload error logs for analysis and further
  parsing if needed. An http header of "Accept:
  application/octet-stream" or the default "*/*" is expected.

Tested:
- Ran Redfish validator.

- Before, It broke post JSON content as HTTP
  https://gerrit.openbmc-project.xyz/c/openbmc/bmcweb/+/44660
  Now, I tested it passed.

- pldmtool raw --data 0x80 0x3F 0xC 0x0A 0x00 0x00 0x00 0x00 0x00 0x07 0x00 0x00 0x00 0x48 0x00 0x00 0x00 0x02 0x00 0x00 0x01 0x00 0x00 0x00 0x48 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x53 0x54 0x41 0x4e 0x44 0x42 0x59 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20

$curl -k https://127.0.0.1:2443/redfish/v1/Systems/system/LogServices/PostCodes/Entries/B1-1/attachment/
output:
AgAAAQAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNUQU5EQlkgICAgICAgICAgICAgICAgICAgICAgICAg

Signed-off-by: George Liu <liuxiwei@inspur.com>
Change-Id: Ide684146a4ae9d55dc95fb765927867b042fc27c
diff --git a/include/http_utility.hpp b/include/http_utility.hpp
index ef65e23..740e218 100644
--- a/include/http_utility.hpp
+++ b/include/http_utility.hpp
@@ -5,14 +5,20 @@
 
 namespace http_helpers
 {
-inline bool requestPrefersHtml(std::string_view header)
+inline std::vector<std::string> parseAccept(std::string_view header)
 {
     std::vector<std::string> encodings;
     // chrome currently sends 6 accepts headers, firefox sends 4.
     encodings.reserve(6);
     boost::split(encodings, header, boost::is_any_of(", "),
                  boost::token_compress_on);
-    for (const std::string& encoding : encodings)
+
+    return encodings;
+}
+
+inline bool requestPrefersHtml(std::string_view header)
+{
+    for (const std::string& encoding : parseAccept(header))
     {
         if (encoding == "text/html")
         {
@@ -26,6 +32,18 @@
     return false;
 }
 
+inline bool isOctetAccepted(std::string_view header)
+{
+    for (const std::string& encoding : parseAccept(header))
+    {
+        if (encoding == "*/*" || encoding == "application/octet-stream")
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
 inline std::string urlEncode(const std::string_view value)
 {
     std::ostringstream escaped;
diff --git a/include/ut/http_utility_test.cpp b/include/ut/http_utility_test.cpp
new file mode 100644
index 0000000..164f51d
--- /dev/null
+++ b/include/ut/http_utility_test.cpp
@@ -0,0 +1,26 @@
+#include <http_utility.hpp>
+
+#include "gmock/gmock.h"
+
+TEST(HttpUtility, requestPrefersHtml)
+{
+    boost::beast::http::request<boost::beast::http::string_body> req{};
+
+    req.set("Accept", "*/*, application/octet-stream");
+    crow::Request req1(req);
+    EXPECT_FALSE(
+        http_helpers::requestPrefersHtml(req1.getHeaderValue("Accept")));
+    EXPECT_TRUE(http_helpers::isOctetAccepted(req1.getHeaderValue("Accept")));
+
+    req.set("Accept", "text/html, application/json");
+    crow::Request req2(req);
+    EXPECT_TRUE(
+        http_helpers::requestPrefersHtml(req2.getHeaderValue("Accept")));
+    EXPECT_FALSE(http_helpers::isOctetAccepted(req2.getHeaderValue("Accept")));
+
+    req.set("Accept", "application/json");
+    crow::Request req3(req);
+    EXPECT_FALSE(
+        http_helpers::requestPrefersHtml(req3.getHeaderValue("Accept")));
+    EXPECT_FALSE(http_helpers::isOctetAccepted(req3.getHeaderValue("Accept")));
+}
diff --git a/meson.build b/meson.build
index f343272..2b27e86 100644
--- a/meson.build
+++ b/meson.build
@@ -360,6 +360,7 @@
                    'redfish-core/src/utils/json_utils.cpp']
 
 srcfiles_unittest = ['include/ut/dbus_utility_test.cpp',
+                     'include/ut/http_utility_test.cpp',
                      'redfish-core/ut/privileges_test.cpp',
                      'redfish-core/ut/lock_test.cpp',
                      'redfish-core/ut/configfile_test.cpp',
diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp
index 1c7b695..0a97150 100644
--- a/redfish-core/include/redfish.hpp
+++ b/redfish-core/include/redfish.hpp
@@ -93,6 +93,7 @@
 
         requestRoutesSystemLogServiceCollection(app);
         requestRoutesEventLogService(app);
+        requestRoutesPostCodesEntryAdditionalData(app);
 
         requestRoutesPostCodesLogService(app);
         requestRoutesPostCodesClear(app);
diff --git a/redfish-core/lib/log_services.hpp b/redfish-core/lib/log_services.hpp
index 943e08e..12ec64a 100644
--- a/redfish-core/lib/log_services.hpp
+++ b/redfish-core/lib/log_services.hpp
@@ -15,6 +15,7 @@
 */
 #pragma once
 
+#include "http_utility.hpp"
 #include "registries.hpp"
 #include "registries/base_message_registry.hpp"
 #include "registries/openbmc_message_registry.hpp"
@@ -33,6 +34,7 @@
 #include <error_messages.hpp>
 #include <registries/privilege_registry.hpp>
 
+#include <charconv>
 #include <filesystem>
 #include <optional>
 #include <string_view>
@@ -501,7 +503,7 @@
                     continue;
                 }
 
-                thisEntry["@odata.type"] = "#LogEntry.v1_7_0.LogEntry";
+                thisEntry["@odata.type"] = "#LogEntry.v1_8_0.LogEntry";
                 thisEntry["@odata.id"] = dumpPath + entryID;
                 thisEntry["Id"] = entryID;
                 thisEntry["EntryType"] = "Event";
@@ -655,7 +657,7 @@
                 }
 
                 asyncResp->res.jsonValue["@odata.type"] =
-                    "#LogEntry.v1_7_0.LogEntry";
+                    "#LogEntry.v1_8_0.LogEntry";
                 asyncResp->res.jsonValue["@odata.id"] = dumpPath + entryID;
                 asyncResp->res.jsonValue["Id"] = entryID;
                 asyncResp->res.jsonValue["EntryType"] = "Event";
@@ -1165,7 +1167,7 @@
 
     // Fill in the log entry with the gathered data
     logEntryJson = {
-        {"@odata.type", "#LogEntry.v1_4_0.LogEntry"},
+        {"@odata.type", "#LogEntry.v1_8_0.LogEntry"},
         {"@odata.id",
          "/redfish/v1/Systems/system/LogServices/EventLog/Entries/" +
              logEntryID},
@@ -1733,24 +1735,8 @@
                const std::string& param)
 
             {
-                std::string_view acceptHeader = req.getHeaderValue("Accept");
-                // The iterators in boost/http/rfc7230.hpp end the string if '/'
-                // is found, so replace it with arbitrary character '|' which is
-                // not part of the Accept header syntax.
-                std::string acceptStr = boost::replace_all_copy(
-                    std::string(acceptHeader), "/", "|");
-                boost::beast::http::ext_list acceptTypes{acceptStr};
-                bool supported = false;
-                for (const auto& type : acceptTypes)
-                {
-                    if ((type.first == "*|*") ||
-                        (type.first == "application|octet-stream"))
-                    {
-                        supported = true;
-                        break;
-                    }
-                }
-                if (!supported)
+                if (!http_helpers::isOctetAccepted(
+                        req.getHeaderValue("Accept")))
                 {
                     asyncResp->res.result(
                         boost::beast::http::status::bad_request);
@@ -1947,7 +1933,7 @@
 
     // Fill in the log entry with the gathered data
     bmcJournalLogEntryJson = {
-        {"@odata.type", "#LogEntry.v1_4_0.LogEntry"},
+        {"@odata.type", "#LogEntry.v1_8_0.LogEntry"},
         {"@odata.id", "/redfish/v1/Managers/bmc/LogServices/Journal/Entries/" +
                           bmcJournalLogEntryID},
         {"Name", "BMC Journal Entry"},
@@ -2982,7 +2968,7 @@
         // add to AsyncResp
         logEntryArray.push_back({});
         nlohmann::json& bmcLogEntry = logEntryArray.back();
-        bmcLogEntry = {{"@odata.type", "#LogEntry.v1_4_0.LogEntry"},
+        bmcLogEntry = {{"@odata.type", "#LogEntry.v1_8_0.LogEntry"},
                        {"@odata.id", "/redfish/v1/Systems/system/LogServices/"
                                      "PostCodes/Entries/" +
                                          postcodeEntryID},
@@ -2994,6 +2980,12 @@
                        {"EntryType", "Event"},
                        {"Severity", std::move(severity)},
                        {"Created", entryTimeStr}};
+        if (!std::get<std::vector<uint8_t>>(code.second).empty())
+        {
+            bmcLogEntry["AdditionalDataURI"] =
+                "/redfish/v1/Systems/system/LogServices/PostCodes/Entries/" +
+                postcodeEntryID + "/attachment";
+        }
     }
 }
 
@@ -3151,6 +3143,127 @@
             });
 }
 
+/**
+ * @brief Parse post code ID and get the current value and index value
+ *        eg: postCodeID=B1-2, currentValue=1, index=2
+ *
+ * @param[in]  postCodeID     Post Code ID
+ * @param[out] currentValue   Current value
+ * @param[out] index          Index value
+ *
+ * @return bool true if the parsing is successful, false the parsing fails
+ */
+inline static bool parsePostCode(const std::string& postCodeID,
+                                 uint64_t& currentValue, uint16_t& index)
+{
+    std::vector<std::string> split;
+    boost::algorithm::split(split, postCodeID, boost::is_any_of("-"));
+    if (split.size() != 2 || split[0].length() < 2 || split[0].front() != 'B')
+    {
+        return false;
+    }
+
+    const char* start = split[0].data() + 1;
+    const char* end = split[0].data() + split[0].size();
+    auto [ptrIndex, ecIndex] = std::from_chars(start, end, index);
+
+    if (ptrIndex != end || ecIndex != std::errc())
+    {
+        return false;
+    }
+
+    start = split[1].data();
+    end = split[1].data() + split[1].size();
+    auto [ptrValue, ecValue] = std::from_chars(start, end, currentValue);
+    if (ptrValue != end || ecValue != std::errc())
+    {
+        return false;
+    }
+
+    return true;
+}
+
+inline void requestRoutesPostCodesEntryAdditionalData(App& app)
+{
+    BMCWEB_ROUTE(app, "/redfish/v1/Systems/system/LogServices/PostCodes/"
+                      "Entries/<str>/attachment/")
+        .privileges(redfish::privileges::getLogEntry)
+        .methods(boost::beast::http::verb::get)(
+            [](const crow::Request& req,
+               const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+               const std::string& postCodeID) {
+                if (!http_helpers::isOctetAccepted(
+                        req.getHeaderValue("Accept")))
+                {
+                    asyncResp->res.result(
+                        boost::beast::http::status::bad_request);
+                    return;
+                }
+
+                uint64_t currentValue = 0;
+                uint16_t index = 0;
+                if (!parsePostCode(postCodeID, currentValue, index))
+                {
+                    messages::resourceNotFound(asyncResp->res, "LogEntry",
+                                               postCodeID);
+                    return;
+                }
+
+                crow::connections::systemBus->async_method_call(
+                    [asyncResp, postCodeID, currentValue](
+                        const boost::system::error_code ec,
+                        const std::vector<std::tuple<
+                            uint64_t, std::vector<uint8_t>>>& postcodes) {
+                        if (ec.value() == EBADR)
+                        {
+                            messages::resourceNotFound(asyncResp->res,
+                                                       "LogEntry", postCodeID);
+                            return;
+                        }
+                        if (ec)
+                        {
+                            BMCWEB_LOG_DEBUG << "DBUS response error " << ec;
+                            messages::internalError(asyncResp->res);
+                            return;
+                        }
+
+                        size_t value = static_cast<size_t>(currentValue) - 1;
+                        if (value == std::string::npos ||
+                            postcodes.size() < currentValue)
+                        {
+                            BMCWEB_LOG_ERROR << "Wrong currentValue value";
+                            messages::resourceNotFound(asyncResp->res,
+                                                       "LogEntry", postCodeID);
+                            return;
+                        }
+
+                        auto& [tID, code] = postcodes[value];
+                        if (code.empty())
+                        {
+                            BMCWEB_LOG_INFO << "No found post code data";
+                            messages::resourceNotFound(asyncResp->res,
+                                                       "LogEntry", postCodeID);
+                            return;
+                        }
+
+                        std::string_view strData(
+                            reinterpret_cast<const char*>(code.data()),
+                            code.size());
+
+                        asyncResp->res.addHeader("Content-Type",
+                                                 "application/octet-stream");
+                        asyncResp->res.addHeader("Content-Transfer-Encoding",
+                                                 "Base64");
+                        asyncResp->res.body() =
+                            crow::utility::base64encode(strData);
+                    },
+                    "xyz.openbmc_project.State.Boot.PostCode0",
+                    "/xyz/openbmc_project/State/Boot/PostCode0",
+                    "xyz.openbmc_project.State.Boot.PostCode", "GetPostCodes",
+                    index);
+            });
+}
+
 inline void requestRoutesPostCodesEntry(App& app)
 {
     BMCWEB_ROUTE(
@@ -3160,31 +3273,14 @@
             [](const crow::Request&,
                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
                const std::string& targetID) {
-                size_t bootPos = targetID.find('B');
-                if (bootPos == std::string::npos)
+                uint16_t bootIndex = 0;
+                uint64_t codeIndex = 0;
+                if (!parsePostCode(targetID, codeIndex, bootIndex))
                 {
                     // Requested ID was not found
                     messages::resourceMissingAtURI(asyncResp->res, targetID);
                     return;
                 }
-                std::string_view bootIndexStr(targetID);
-                bootIndexStr.remove_prefix(bootPos + 1);
-                uint16_t bootIndex = 0;
-                uint64_t codeIndex = 0;
-                size_t dashPos = bootIndexStr.find('-');
-
-                if (dashPos == std::string::npos)
-                {
-                    return;
-                }
-                std::string_view codeIndexStr(bootIndexStr);
-                bootIndexStr.remove_suffix(dashPos);
-                codeIndexStr.remove_prefix(dashPos + 1);
-
-                bootIndex = static_cast<uint16_t>(
-                    strtoul(std::string(bootIndexStr).c_str(), nullptr, 0));
-                codeIndex =
-                    strtoul(std::string(codeIndexStr).c_str(), nullptr, 0);
                 if (bootIndex == 0 || codeIndex == 0)
                 {
                     BMCWEB_LOG_DEBUG << "Get Post Code invalid entry string "