bios_setting: Add a write handler

Tested:
```
// Test that it writes a new file if it doesn't exist
~# rm /run/oem_bios_setting

// Invalid command (no size / payload)
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x19
Unable to send RAW command (channel=0x0 netfn=0x2e lun=0x0 cmd=0x32 rsp=0xc7): Request data length invalid

// Invalid command (size doesn't match)
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x19 0x01 0x30 0x31
Unable to send RAW command (channel=0x0 netfn=0x2e lun=0x0 cmd=0x32 rsp=0xc7): Request data length invalid

// Command success
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x19 0x02 0x30 0x31
 79 2b 00 19 02

// Read back
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x18
 79 2b 00 18 02 30 31
~# cat /run/oem_bios_setting
01~#

// Verify overwrite works (truncates the previous bytes)
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x19 0x01 0x33
 79 2b 00 19 01
~# cat /run/oem_bios_setting
3~#
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x18
 79 2b 00 18 01 33
```

Signed-off-by: Brandon Kim <brandonkim@google.com>
Change-Id: I84db782da9b2f121c0a81a855692b5ca25ffda54
diff --git a/README.md b/README.md
index be77961..e43454a 100644
--- a/README.md
+++ b/README.md
@@ -552,3 +552,25 @@
 | 0x00              | 0x18          | Subcommand                                   |
 | 0x01              | Length (N)    | Number of payload bytes - Size limited to 64 |
 | 0x2..0x02 + N - 1 | Payload bytes | Payload bytes                                |
+
+### WriteBiosSetting - SubCommand 0x19
+
+Write a BIOS setting, set at `/run/oem_bios_setting`.
+
+The response contains the length of the BIOS setting followed by the BIOS
+setting bytes read.
+
+Request
+
+| Byte(s)           | Value         | Data                                         |
+| ----------------- | ------------- | -------------------------------------------- |
+| 0x00              | 0x19          | Subcommand                                   |
+| 0x01              | Length (N)    | Number of payload bytes - Size limited to 64 |
+| 0x2..0x02 + N - 1 | Payload bytes | Payload bytes                                |
+
+Response
+
+| Byte(s) | Value      | Data                                 |
+| ------- | ---------- | ------------------------------------ |
+| 0x00    | 0x19       | Subcommand                           |
+| 0x01    | Length (N) | Number of bytes successfully written |
diff --git a/bios_setting.cpp b/bios_setting.cpp
index d92f786..864b178 100644
--- a/bios_setting.cpp
+++ b/bios_setting.cpp
@@ -22,7 +22,9 @@
 #include <stdplus/fd/create.hpp>
 #include <stdplus/fd/managed.hpp>
 #include <stdplus/fd/ops.hpp>
+#include <stdplus/numeric/endian.hpp>
 #include <stdplus/print.hpp>
+#include <stdplus/raw.hpp>
 
 #include <filesystem>
 #include <fstream>
@@ -72,5 +74,54 @@
     return ::ipmi::responseSuccess(SysOEMCommands::SysReadBiosSetting, reply);
 }
 
+Resp writeBiosSetting(std::span<const uint8_t> data, HandlerInterface*,
+                      const std::string& biosSettingPath)
+{
+    std::uint8_t payloadSize;
+    try
+    {
+        // This subspans the data automatically
+        payloadSize = stdplus::raw::extract<
+            stdplus::EndianPacked<decltype(payloadSize), std::endian::little>>(
+            data);
+    }
+    catch (const std::exception& e)
+    {
+        stdplus::print(stderr, "Extracting payload failed: {}\n", e.what());
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    if (data.size() != payloadSize)
+    {
+        stdplus::print(stderr, "Invalid command length {} vs. payloadSize {}\n",
+                       static_cast<uint32_t>(data.size()),
+                       static_cast<uint32_t>(payloadSize));
+        return ::ipmi::responseReqDataLenInvalid();
+    }
+
+    // Write the setting
+    try
+    {
+        stdplus::ManagedFd managedFd = stdplus::fd::open(
+            biosSettingPath,
+            stdplus::fd::OpenFlags(stdplus::fd::OpenAccess::WriteOnly)
+                .set(stdplus::fd::OpenFlag::Trunc)
+                .set(stdplus::fd::OpenFlag::Create));
+        stdplus::fd::writeExact(managedFd, data);
+    }
+    catch (const std::exception& e)
+    {
+        stdplus::print(stderr, "Write unsuccessful: {}\n", e.what());
+        return ::ipmi::responseRetBytesUnavailable();
+    }
+
+    // Reply format is: Length of the payload written
+    std::vector<std::uint8_t> reply;
+    reply.reserve(1);
+    reply.emplace_back(static_cast<uint8_t>(payloadSize));
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysWriteBiosSetting, reply);
+}
+
 } // namespace ipmi
 } // namespace google
diff --git a/bios_setting.hpp b/bios_setting.hpp
index 7445a7a..ced9dd8 100644
--- a/bios_setting.hpp
+++ b/bios_setting.hpp
@@ -33,6 +33,9 @@
 Resp readBiosSetting(
     std::span<const uint8_t> data, HandlerInterface* handler,
     const std::string& biosSettingPath = "/run/oem_bios_setting");
+Resp writeBiosSetting(
+    std::span<const uint8_t> data, HandlerInterface* handler,
+    const std::string& biosSettingPath = "/run/oem_bios_setting");
 
 } // namespace ipmi
 } // namespace google
diff --git a/commands.hpp b/commands.hpp
index abe49f4..595d683 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -71,6 +71,8 @@
     SysGetBMInstanceProperty = 23,
     // Read OEM BIOS Setting
     SysReadBiosSetting = 24,
+    // Write OEM BIOS Setting
+    SysWriteBiosSetting = 25,
 };
 
 } // namespace ipmi
diff --git a/ipmi.cpp b/ipmi.cpp
index 669c665..237b2b5 100644
--- a/ipmi.cpp
+++ b/ipmi.cpp
@@ -97,6 +97,8 @@
             return getBMInstanceProperty(data, handler);
         case SysReadBiosSetting:
             return readBiosSetting(data, handler);
+        case SysWriteBiosSetting:
+            return writeBiosSetting(data, handler);
         default:
             stdplus::print(stderr, "Invalid subcommand: {:#x}\n", cmd);
             return ::ipmi::responseInvalidCommand();
diff --git a/test/bios_setting_unittest.cpp b/test/bios_setting_unittest.cpp
index 45373f4..1d7feb8 100644
--- a/test/bios_setting_unittest.cpp
+++ b/test/bios_setting_unittest.cpp
@@ -84,5 +84,77 @@
     std::remove(filename.c_str());
 }
 
+TEST_F(BiosSettingTest, InvalidRequestWrite)
+{
+    // Empty request
+    std::vector<uint8_t> request = {};
+
+    HandlerMock hMock;
+    EXPECT_EQ(::ipmi::responseReqDataLenInvalid(),
+              writeBiosSetting(request, &hMock));
+
+    // Request with payload size 1 but no payload
+    request = {0x01};
+    EXPECT_EQ(::ipmi::responseReqDataLenInvalid(),
+              writeBiosSetting(request, &hMock));
+
+    // Request with payload size 1 but actual payload size of 2 bytes
+    request = {0x01, 0x02, 0x03};
+    EXPECT_EQ(::ipmi::responseReqDataLenInvalid(),
+              writeBiosSetting(request, &hMock));
+
+    // Request with payload size 2 but actual payload of 1 byte
+    request = {0x02, 0x02};
+    EXPECT_EQ(::ipmi::responseReqDataLenInvalid(),
+              writeBiosSetting(request, &hMock));
+}
+
+TEST_F(BiosSettingTest, SuccessfulWrite)
+{
+    std::vector<uint8_t> request = {0x02, 0xDE, 0xAD};
+
+    // Write a dummy file to get around permission issues with CI
+    // (Not needed in local CI)
+    writeTmpFile({});
+    HandlerMock hMock;
+    auto reply = writeBiosSetting(request, &hMock, filename);
+    auto result = ValidateReply(reply);
+    auto& data = result.second;
+
+    EXPECT_EQ(SysOEMCommands::SysWriteBiosSetting, result.first);
+    EXPECT_EQ(std::vector<uint8_t>{2}, data);
+
+    // Validate the payload is correct
+    reply = readBiosSetting(request, &hMock, filename);
+    result = ValidateReply(reply);
+    data = result.second;
+
+    EXPECT_EQ(SysOEMCommands::SysReadBiosSetting, result.first);
+    EXPECT_EQ(request.size() - 1, data.front());
+    EXPECT_EQ(request, data);
+
+    // Verify that we can write a shorter string and it'll replace the original
+    // content of the file
+    request = {0x01, 0x0A};
+
+    reply = writeBiosSetting(request, &hMock, filename);
+    result = ValidateReply(reply);
+    data = result.second;
+
+    EXPECT_EQ(SysOEMCommands::SysWriteBiosSetting, result.first);
+    EXPECT_EQ(std::vector<uint8_t>{1}, data);
+
+    // Validate the payload is correct
+    reply = readBiosSetting(request, &hMock, filename);
+    result = ValidateReply(reply);
+    data = result.second;
+
+    EXPECT_EQ(SysOEMCommands::SysReadBiosSetting, result.first);
+    EXPECT_EQ(request.size() - 1, data.front());
+    EXPECT_EQ(request, data);
+    // Cleanup the settings file
+    std::remove(filename.c_str());
+}
+
 } // namespace ipmi
 } // namespace google