ncsi: encapsulate NC-SI commands with NCSICommand / NCSIResponse structs

... and rename Interface::sendOemCommand to Interface::sendCommand.

This provides a more clear facility to pass command and response objects
around, for future command and transport implementations.

Change-Id: I46e594ab6467ed87cfc27189c3ec4bd321726ee5
Signed-off-by: Jeremy Kerr <jk@codeconstruct.com.au>
diff --git a/src/ncsi_netlink_main.cpp b/src/ncsi_netlink_main.cpp
index 3089b7c..ce9b3f2 100644
--- a/src/ncsi_netlink_main.cpp
+++ b/src/ncsi_netlink_main.cpp
@@ -209,16 +209,19 @@
             lg2::debug("Payload: {PAYLOAD}", "PAYLOAD", toHexStr(payload));
         }
 
-        auto cmd =
-            std::span<const unsigned char>(payload.begin(), payload.end());
-        auto resp =
-            interface.sendOemCommand(packageInt, channelInt, operationInt, cmd);
+        std::optional<uint8_t> chan = channelInt != DEFAULT_VALUE
+                                          ? std::make_optional(channelInt)
+                                          : std::nullopt;
+        NCSICommand cmd(operationInt, packageInt, chan, payload);
+
+        auto resp = interface.sendCommand(cmd);
         if (!resp)
         {
             return EXIT_FAILURE;
         }
         lg2::debug("Response {DATA_LEN} bytes: {DATA}", "DATA_LEN",
-                   resp->size(), "DATA", toHexStr(*resp));
+                   resp->full_payload.size(), "DATA",
+                   toHexStr(resp->full_payload));
     }
     else if ((options)["set"] == "true")
     {
diff --git a/src/ncsi_util.cpp b/src/ncsi_util.cpp
index 13c41f6..d8ad3b2 100644
--- a/src/ncsi_util.cpp
+++ b/src/ncsi_util.cpp
@@ -7,6 +7,8 @@
 
 #include <phosphor-logging/lg2.hpp>
 
+#include <optional>
+#include <span>
 #include <vector>
 
 namespace phosphor
@@ -16,6 +18,19 @@
 namespace ncsi
 {
 
+NCSICommand::NCSICommand(uint8_t opcode, uint8_t package,
+                         std::optional<uint8_t> channel,
+                         std::span<unsigned char> payload) :
+    opcode(opcode), package(package), channel(channel)
+{
+    this->payload.assign(payload.begin(), payload.end());
+}
+
+uint8_t NCSICommand::getChannel()
+{
+    return channel.value_or(CHANNEL_ID_NONE);
+}
+
 using CallBack = int (*)(struct nl_msg* msg, void* arg);
 
 namespace internal
@@ -33,6 +48,12 @@
     uint32_t rsvd[2];
 };
 
+struct NCSIResponsePayload
+{
+    uint16_t response;
+    uint16_t reason;
+};
+
 class NetlinkCommand
 {
   public:
@@ -203,7 +224,7 @@
 
 struct sendCallBackContext
 {
-    std::vector<unsigned char> msg;
+    NCSIResponse resp;
 };
 
 CallBack sendCallBack = [](struct nl_msg* msg, void* arg) {
@@ -237,11 +258,16 @@
         return -1;
     }
 
-    auto data_len = nla_len(tb[NCSI_ATTR_DATA]) - sizeof(NCSIPacketHeader);
-    unsigned char* data =
-        (unsigned char*)nla_data(tb[NCSI_ATTR_DATA]) + sizeof(NCSIPacketHeader);
+    size_t data_len = nla_len(tb[NCSI_ATTR_DATA]);
+    unsigned char* data = (unsigned char*)nla_data(tb[NCSI_ATTR_DATA]);
 
-    ctx->msg.assign(data, data + data_len);
+    ctx->resp.full_payload.assign(data, data + data_len);
+
+    int rc = ctx->resp.parseFullPayload();
+    if (rc)
+    {
+        return -1;
+    }
 
     return static_cast<int>(NL_STOP);
 };
@@ -399,29 +425,27 @@
     return std::to_string(interface.ifindex);
 }
 
-std::optional<std::vector<unsigned char>>
-    Interface::sendOemCommand(int package, int channel, int operation,
-                              std::span<const unsigned char> payload)
+std::optional<NCSIResponse> Interface::sendCommand(NCSICommand& cmd)
 {
-    lg2::debug("Send OEM Command, CHANNEL : {CHANNEL} , PACKAGE : {PACKAGE}, "
+    lg2::debug("Send Command, CHANNEL : {CHANNEL} , PACKAGE : {PACKAGE}, "
                "INTERFACE: {INTERFACE}",
-               "CHANNEL", lg2::hex, channel, "PACKAGE", lg2::hex, package,
-               "INTERFACE", this);
+               "CHANNEL", lg2::hex, cmd.getChannel(), "PACKAGE", lg2::hex,
+               cmd.package, "INTERFACE", this);
 
     internal::sendCallBackContext ctx{};
 
-    internal::NetlinkCommand cmd(ncsi_nl_commands::NCSI_CMD_SEND_CMD, operation,
-                                 payload);
+    internal::NetlinkCommand nl_cmd(ncsi_nl_commands::NCSI_CMD_SEND_CMD,
+                                    cmd.opcode, cmd.payload);
 
-    int rc = internal::applyCmd(*this, cmd, package, channel, NONE,
-                                internal::sendCallBack, &ctx);
+    int rc = internal::applyCmd(*this, nl_cmd, cmd.package, cmd.getChannel(),
+                                NONE, internal::sendCallBack, &ctx);
 
     if (rc < 0)
     {
         return {};
     }
 
-    return ctx.msg;
+    return ctx.resp;
 }
 
 int Interface::setChannel(int package, int channel)
@@ -497,6 +521,41 @@
     return internal::applyCmd(*this, cmd);
 }
 
+int NCSIResponse::parseFullPayload()
+{
+    if (this->full_payload.size() < sizeof(internal::NCSIPacketHeader) +
+                                        sizeof(internal::NCSIResponsePayload))
+    {
+        lg2::error("Response: Not enough data for a response message");
+        return -1;
+    }
+
+    internal::NCSIPacketHeader* respHeader =
+        reinterpret_cast<decltype(respHeader)>(this->full_payload.data());
+
+    unsigned int payloadLen = ntohs(respHeader->length & htons(0x0fff));
+    /* we have determined that the payload size is larger than *respHeader,
+     * so cannot underflow here */
+    if (payloadLen > this->full_payload.size() - sizeof(*respHeader))
+    {
+        lg2::error("Invalid header length {HDRLEN} (vs {LEN}) in response",
+                   "HDRLEN", payloadLen, "LEN",
+                   this->full_payload.size() - sizeof(*respHeader));
+        return -1;
+    }
+
+    this->opcode = respHeader->type;
+    this->payload =
+        std::span(this->full_payload.begin() + sizeof(*respHeader), payloadLen);
+
+    internal::NCSIResponsePayload* respPayload =
+        reinterpret_cast<decltype(respPayload)>(this->payload.data());
+    this->response = ntohs(respPayload->response);
+    this->reason = ntohs(respPayload->reason);
+
+    return 0;
+}
+
 } // namespace ncsi
 } // namespace network
 } // namespace phosphor
diff --git a/src/ncsi_util.hpp b/src/ncsi_util.hpp
index 048bb71..0ca0df1 100644
--- a/src/ncsi_util.hpp
+++ b/src/ncsi_util.hpp
@@ -16,6 +16,7 @@
 
 constexpr auto DEFAULT_VALUE = -1;
 constexpr auto NONE = 0;
+constexpr uint8_t CHANNEL_ID_NONE = 0x1f;
 
 struct ChannelInfo
 {
@@ -40,10 +41,37 @@
     std::vector<PackageInfo> packages;
 };
 
+struct NCSICommand
+{
+    /* constructs a message; the payload span is copied into the internal
+     * command vector */
+    NCSICommand(uint8_t opcode, uint8_t package, std::optional<uint8_t> channel,
+                std::span<unsigned char> payload);
+
+    uint8_t getChannel();
+
+    uint8_t opcode;
+    uint8_t package;
+    std::optional<uint8_t> channel;
+    std::vector<unsigned char> payload;
+};
+
+struct NCSIResponse
+{
+    uint8_t opcode;
+    uint8_t response, reason;
+    std::span<unsigned char> payload;
+    std::vector<unsigned char> full_payload;
+
+    /* Given an incoming response with full_payload set, check that we have
+     * enough data for a correct response, and populate the rest of the struct
+     * to suit
+     */
+    int parseFullPayload();
+};
+
 struct Interface
 {
-    using ncsiMessage = std::span<const unsigned char>;
-
     /* @brief  This function will ask underlying NCSI driver
      *         to send an OEM command (command type 0x50) with
      *         the specified payload as the OEM data.
@@ -55,8 +83,7 @@
      * @param[in] payload - OEM data to send.
      * @returns the NCSI response message to this command, or no value on error.
      */
-    std::optional<std::vector<unsigned char>> sendOemCommand(
-        int package, int channel, int opcode, ncsiMessage payload);
+    std::optional<NCSIResponse> sendCommand(NCSICommand& cmd);
 
     /* @brief  This function will ask underlying NCSI driver
      *         to set a specific  package or package/channel