bios_setting: Add a read handler

Tested:
```
~# echo -n "01234567" > /run/oem_bios_setting
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x18
 79 2b 00 18 08 30 31 32 33 34 35 36 37
~# rm /run/oem_bios_setting
~# ipmitool raw 0x2e 0x32 0x79 0x2b 0x00 0x18
Unable to send RAW command (channel=0x0 netfn=0x2e lun=0x0 cmd=0x32 rsp=0xca): Cannot return number of requested data bytes
```

Signed-off-by: Brandon Kim <brandonkim@google.com>
Change-Id: Icde2de4799a751634f56a580351bf10254dd7e4f
diff --git a/README.md b/README.md
index 5f8590c..be77961 100644
--- a/README.md
+++ b/README.md
@@ -531,3 +531,24 @@
 | 0x00              | 0x17               | Subcommand                                                       |
 | 0x01              | String Length (N)  | Number of bytes to read for property string - Size limited to 64 |
 | 0x2..0x02 + N - 1 | String of property | String, not null-terminated                                      |
+
+### ReadBiosSetting - SubCommand 0x18
+
+Read 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    | 0x18  | Subcommand |
+
+Response
+
+| Byte(s)           | Value         | Data                                         |
+| ----------------- | ------------- | -------------------------------------------- |
+| 0x00              | 0x18          | Subcommand                                   |
+| 0x01              | Length (N)    | Number of payload bytes - Size limited to 64 |
+| 0x2..0x02 + N - 1 | Payload bytes | Payload bytes                                |
diff --git a/bios_setting.cpp b/bios_setting.cpp
new file mode 100644
index 0000000..d92f786
--- /dev/null
+++ b/bios_setting.cpp
@@ -0,0 +1,76 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "bios_setting.hpp"
+
+#include "commands.hpp"
+#include "errors.hpp"
+#include "handler.hpp"
+
+#include <ipmid/api-types.hpp>
+#include <stdplus/fd/create.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/print.hpp>
+
+#include <filesystem>
+#include <fstream>
+#include <span>
+#include <vector>
+
+namespace google
+{
+namespace ipmi
+{
+
+std::vector<uint8_t> readBiosSettingFromFile(const std::string& biosSettingPath)
+{
+    std::vector<uint8_t> biosSettings;
+    try
+    {
+        stdplus::ManagedFd managedFd = stdplus::fd::open(
+            biosSettingPath,
+            stdplus::fd::OpenFlags(stdplus::fd::OpenAccess::ReadOnly));
+        biosSettings = stdplus::fd::readAll<std::vector<uint8_t>>(managedFd);
+    }
+    catch (const std::exception& e)
+    {
+        stdplus::print(stderr, "Read unsuccessful: {}\n", e.what());
+        return {};
+    }
+    return biosSettings;
+}
+
+Resp readBiosSetting(std::span<const uint8_t>, HandlerInterface*,
+                     const std::string& biosSettingPath)
+{
+    std::vector<uint8_t> biosSettings =
+        readBiosSettingFromFile(biosSettingPath);
+    size_t settingsLength = biosSettings.size();
+    if (settingsLength == 0)
+    {
+        return ::ipmi::responseRetBytesUnavailable();
+    }
+
+    // Reply format is: Length of the payload (1 byte) + payload
+    std::vector<std::uint8_t> reply;
+    reply.reserve(1 + settingsLength);
+    reply.emplace_back(static_cast<uint8_t>(settingsLength));
+    reply.insert(reply.end(), biosSettings.begin(), biosSettings.end());
+
+    return ::ipmi::responseSuccess(SysOEMCommands::SysReadBiosSetting, reply);
+}
+
+} // namespace ipmi
+} // namespace google
diff --git a/bios_setting.hpp b/bios_setting.hpp
new file mode 100644
index 0000000..7445a7a
--- /dev/null
+++ b/bios_setting.hpp
@@ -0,0 +1,38 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include "handler.hpp"
+
+#include <span>
+
+namespace google
+{
+namespace ipmi
+{
+
+#ifndef MAX_IPMI_BUFFER
+#define MAX_IPMI_BUFFER 64
+#endif
+
+// Max buffer - len
+constexpr size_t MAX_PAYLOAD_SIZE = MAX_IPMI_BUFFER - 1;
+
+Resp readBiosSetting(
+    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 0dc80e6..abe49f4 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -69,6 +69,8 @@
     SysSetAccelVrSettings = 22,
     // Get BM instance property info
     SysGetBMInstanceProperty = 23,
+    // Read OEM BIOS Setting
+    SysReadBiosSetting = 24,
 };
 
 } // namespace ipmi
diff --git a/ipmi.cpp b/ipmi.cpp
index 5c0b916..669c665 100644
--- a/ipmi.cpp
+++ b/ipmi.cpp
@@ -14,6 +14,7 @@
 
 #include "ipmi.hpp"
 
+#include "bios_setting.hpp"
 #include "bm_instance.hpp"
 #include "bmc_mode.hpp"
 #include "cable.hpp"
@@ -94,6 +95,8 @@
             return accelSetVrSettings(ctx, data, handler);
         case SysGetBMInstanceProperty:
             return getBMInstanceProperty(data, handler);
+        case SysReadBiosSetting:
+            return readBiosSetting(data, handler);
         default:
             stdplus::print(stderr, "Invalid subcommand: {:#x}\n", cmd);
             return ::ipmi::responseInvalidCommand();
diff --git a/meson.build b/meson.build
index b101757..4e07ddd 100644
--- a/meson.build
+++ b/meson.build
@@ -44,6 +44,7 @@
 
 sys_lib = static_library(
   'sys',
+  'bios_setting.cpp',
   'bm_instance.cpp',
   'bmc_mode.cpp',
   'cable.cpp',
diff --git a/test/bios_setting_unittest.cpp b/test/bios_setting_unittest.cpp
new file mode 100644
index 0000000..45373f4
--- /dev/null
+++ b/test/bios_setting_unittest.cpp
@@ -0,0 +1,88 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "bios_setting.hpp"
+#include "commands.hpp"
+#include "helper.hpp"
+
+#include <stdplus/gtest/tmp.hpp>
+
+#include <fstream>
+#include <ios>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace google
+{
+namespace ipmi
+{
+
+using testing::_;
+using ::testing::ElementsAre;
+
+class BiosSettingTest : public stdplus::gtest::TestWithTmp
+{
+  public:
+    std::string filename = std::format("{}/oem_bios_setting", CaseTmpDir());
+
+    void writeTmpFile(std::vector<uint8_t> payload)
+    {
+        std::ofstream ofs;
+        ofs.open(filename, std::ios::trunc | std::ios::binary);
+        ofs.write(reinterpret_cast<char*>(payload.data()), payload.size());
+        ofs.close();
+    }
+};
+
+TEST_F(BiosSettingTest, NoOrEmptyFileRead)
+{
+    std::vector<uint8_t> request = {};
+
+    HandlerMock hMock;
+    EXPECT_EQ(::ipmi::responseRetBytesUnavailable(),
+              readBiosSetting(request, &hMock));
+
+    // Create an empty file
+    writeTmpFile({});
+    EXPECT_EQ(::ipmi::responseRetBytesUnavailable(),
+              readBiosSetting(request, &hMock, filename));
+    std::remove(filename.c_str());
+}
+
+TEST_F(BiosSettingTest, SuccessfulRead)
+{
+    std::vector<uint8_t> request = {};
+    // Ensure 0x0A which is a new line character '\n', is read properly
+    std::vector<uint8_t> payload = {0x0A, 0xDE, 0xAD, 0xBE, 0xEF, 0x0A};
+    std::vector<uint8_t> expectedReply = {6,    0x0A, 0xDE, 0xAD,
+                                          0xBE, 0xEF, 0x0A};
+
+    writeTmpFile(payload);
+
+    HandlerMock hMock;
+    auto reply = readBiosSetting(request, &hMock, filename);
+    auto result = ValidateReply(reply);
+    auto& data = result.second;
+
+    EXPECT_EQ(SysOEMCommands::SysReadBiosSetting, result.first);
+    EXPECT_EQ(expectedReply.size() - 1, data.front());
+    EXPECT_EQ(expectedReply, data);
+    std::remove(filename.c_str());
+}
+
+} // namespace ipmi
+} // namespace google
diff --git a/test/meson.build b/test/meson.build
index f7e3bb9..061b352 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -2,7 +2,7 @@
 gmock = dependency('gmock', disabler: true, required: get_option('tests'))
 
 tests_pre = declare_dependency(
-  dependencies: [sys_dep, gtest, gmock])
+  dependencies: [sys_dep, gtest, gmock, dependency('stdplus-gtest')])
 
 tests_lib = static_library(
   'common',
@@ -32,6 +32,7 @@
   'linux_boot_done',
   'bm_mode_transition',
   'bm_instance',
+  'bios_setting',
 ]
 
 foreach t : tests