Move hoth-ipmi-blobs to gbmc public
Tested: Presubmit is successful
Google-Bug-Id: 340634286
Change-Id: Ia5d53239b07d97bcd328242d2109d40377c1b643
Signed-off-by: Vamsy Krishna Nooney <vamsykn@google.com>
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ac9f720
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+Hoth IPMI Blob Handler
diff --git a/command/dbus_command.cpp b/command/dbus_command.cpp
new file mode 100644
index 0000000..c61910a
--- /dev/null
+++ b/command/dbus_command.cpp
@@ -0,0 +1,69 @@
+// 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 "dbus_command.hpp"
+
+#include "dbus_command_impl.hpp"
+
+#include <fmt/format.h>
+
+#include <stdplus/cancel.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+stdplus::Cancel DbusCommandImpl::SendHostCommand(
+ std::string_view hothId, const std::vector<uint8_t>& cmd, Cb&& cb)
+{
+ auto req = newHothdCall(hothId, "SendHostCommand");
+ req.append(cmd);
+ auto cHothId = std::string(hothId);
+ return stdplus::Cancel(new BusCall(req.call_async(
+ [cb = std::move(cb), hothId = std::move(cHothId)](
+ sdbusplus::message::message m) mutable noexcept {
+ if (m.is_method_error())
+ {
+ auto err = m.get_error();
+ fmt::print(stderr, "SendHostCommand failed on `{}`: {}: {}\n",
+ hothId, err->name, err->message);
+ cb(std::nullopt);
+ return;
+ }
+ try
+ {
+ std::vector<uint8_t> rsp;
+ m.read(rsp);
+ cb(std::move(rsp));
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(stderr,
+ "SendHostCommand failed unpacking on `{}`: {}\n",
+ hothId, e.what());
+ cb(std::nullopt);
+ }
+ },
+ asyncCallTimeout)));
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/dbus_command.hpp b/command/dbus_command.hpp
new file mode 100644
index 0000000..cafbae7
--- /dev/null
+++ b/command/dbus_command.hpp
@@ -0,0 +1,53 @@
+// 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 "dbus.hpp"
+
+#include <function2/function2.hpp>
+#include <stdplus/cancel.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusCommand
+ * @brief Overridable D-Bus interface for command handler
+ */
+class DbusCommand : public virtual Dbus
+{
+ public:
+ using Cb = fu2::unique_function<void(std::optional<std::vector<uint8_t>>)>;
+
+ /** @brief Implementation for asynchronous SendHostCommand
+ * Send a host command to Hoth and run a callback when it responds.
+ *
+ * @param[in] hothId - The identifier of the targeted hoth instance.
+ * @param[in] command - Data to write to Hoth SPI host command offset.
+ * @param[in] cb - The callback to execute.
+ * @return cancelable[stdplus::Cancel] - Drop to cancel the command early.
+ */
+ virtual stdplus::Cancel SendHostCommand(
+ std::string_view hothId, const std::vector<uint8_t>&, Cb&&) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/dbus_command_impl.hpp b/command/dbus_command_impl.hpp
new file mode 100644
index 0000000..cf25106
--- /dev/null
+++ b/command/dbus_command_impl.hpp
@@ -0,0 +1,44 @@
+// 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 "dbus_command.hpp"
+#include "dbus_impl.hpp"
+
+#include <sdbusplus/bus.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusImpl
+ * @brief D-Bus concrete implementation
+ * @details Pass through all calls to the default D-Bus instance
+ */
+class DbusCommandImpl : public DbusCommand, public DbusImpl
+{
+ public:
+ stdplus::Cancel SendHostCommand(std::string_view hothId,
+ const std::vector<uint8_t>&, Cb&&) override;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/hoth_command.cpp b/command/hoth_command.cpp
new file mode 100644
index 0000000..931d5a8
--- /dev/null
+++ b/command/hoth_command.cpp
@@ -0,0 +1,94 @@
+// 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 "hoth_command.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <vector>
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+
+using level = phosphor::logging::level;
+using SdBusError = sdbusplus::exception::SdBusError;
+
+namespace ipmi_hoth
+{
+
+bool HothCommandBlobHandler::stat(const std::string&, blobs::BlobMeta*)
+{
+ /* Hoth command handler does not support a global blob stat. */
+ return false;
+}
+
+bool HothCommandBlobHandler::commit(uint16_t session,
+ const std::vector<uint8_t>& data)
+{
+ if (!data.empty())
+ {
+ log<level::ERR>("Unexpected data provided to commit call");
+ return false;
+ }
+
+ HothBlob* sess = getSession(session);
+ if (!sess)
+ {
+ return false;
+ }
+
+ // If commit is called multiple times, return the same result as last time
+ if (sess->state &
+ (blobs::StateFlags::committing | blobs::StateFlags::committed))
+ {
+ return true;
+ }
+
+ sess->state &= ~blobs::StateFlags::commit_error;
+ sess->state |= blobs::StateFlags::committing;
+ sess->outstanding = dbus_->SendHostCommand(
+ sess->hothId, sess->buffer,
+ [sess](std::optional<std::vector<uint8_t>> rsp) {
+ auto outstanding = std::move(sess->outstanding);
+ sess->state &= ~blobs::StateFlags::committing;
+ if (!rsp)
+ {
+ sess->state |= blobs::StateFlags::commit_error;
+ return;
+ }
+ sess->buffer = std::move(*rsp);
+ sess->state |= blobs::StateFlags::committed;
+ });
+ return true;
+}
+
+bool HothCommandBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+ HothBlob* sess = getSession(session);
+ if (!sess)
+ {
+ return false;
+ }
+
+ meta->size = sess->buffer.size();
+ meta->blobState = sess->state;
+ return true;
+}
+
+} // namespace ipmi_hoth
diff --git a/command/hoth_command.hpp b/command/hoth_command.hpp
new file mode 100644
index 0000000..64e9a43
--- /dev/null
+++ b/command/hoth_command.hpp
@@ -0,0 +1,67 @@
+// 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 "dbus_command.hpp"
+#include "hoth.hpp"
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+class HothCommandBlobHandler : public HothBlobHandler
+{
+ public:
+ explicit HothCommandBlobHandler(internal::DbusCommand* dbus) : dbus_(dbus)
+ {}
+
+ /* Our callbacks require pinned memory */
+ HothCommandBlobHandler(HothCommandBlobHandler&&) = delete;
+ HothCommandBlobHandler& operator=(HothCommandBlobHandler&&) = delete;
+
+ bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+ bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+ bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+
+ internal::Dbus& dbus() override
+ {
+ return *dbus_;
+ }
+ std::string_view pathSuffix() const override
+ {
+ return "command_passthru";
+ }
+ uint16_t requiredFlags() const override
+ {
+ return blobs::OpenFlags::read | blobs::OpenFlags::write;
+ }
+ uint16_t maxSessions() const override
+ {
+ return 10;
+ }
+ uint32_t maxBufferSize() const override
+ {
+ return 1024;
+ }
+
+ private:
+ internal::DbusCommand* dbus_;
+};
+
+} // namespace ipmi_hoth
diff --git a/command/main_command.cpp b/command/main_command.cpp
new file mode 100644
index 0000000..f7dfdee
--- /dev/null
+++ b/command/main_command.cpp
@@ -0,0 +1,29 @@
+// 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 "dbus_command_impl.hpp"
+#include "hoth_command.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+ /** @brief Default instantiation of Dbus */
+ static ipmi_hoth::internal::DbusCommandImpl dbusCommand_impl;
+
+ return std::make_unique<ipmi_hoth::HothCommandBlobHandler>(
+ &dbusCommand_impl);
+}
diff --git a/command/meson.build b/command/meson.build
new file mode 100644
index 0000000..63360ca
--- /dev/null
+++ b/command/meson.build
@@ -0,0 +1,40 @@
+# Function2 might not have a pkg-config. It is header only so just make
+# sure we can access the needed symbols from the header.
+function2_dep = dependency('function2', required: false)
+has_function2 = meson.get_compiler('cpp').has_header_symbol(
+ 'function2/function2.hpp',
+ 'fu2::unique_function',
+ dependencies: function2_dep)
+
+hothcommand_pre = declare_dependency(
+ include_directories: include_directories('.'),
+ dependencies: [
+ function2_dep,
+ hothblob_dep,
+ phosphor_logging_dep,
+ sdbusplus_dep,
+ stdplus_dep,
+ ])
+
+hothcommand_lib = static_library(
+ 'hothcommand',
+ 'dbus_command.cpp',
+ 'hoth_command.cpp',
+ implicit_include_directories: false,
+ dependencies: hothcommand_pre)
+
+hothcommand_dep = declare_dependency(
+ link_with: hothcommand_lib,
+ dependencies: hothcommand_pre)
+
+shared_module(
+ 'hothcommand',
+ 'main_command.cpp',
+ implicit_include_directories: false,
+ dependencies: hothcommand_dep,
+ install: true,
+ install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+ subdir('test')
+endif
diff --git a/command/test/dbus_command_mock.hpp b/command/test/dbus_command_mock.hpp
new file mode 100644
index 0000000..5231ca3
--- /dev/null
+++ b/command/test/dbus_command_mock.hpp
@@ -0,0 +1,41 @@
+// 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 "dbus_command.hpp"
+#include "dbus_mock.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class DbusCommandMock : public DbusCommand, public DbusMock
+{
+ public:
+ MOCK_METHOD(stdplus::Cancel, SendHostCommand,
+ (std::string_view, const std::vector<uint8_t>&, Cb&&),
+ (override));
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_close_unittest.cpp b/command/test/hoth_command_close_unittest.cpp
new file mode 100644
index 0000000..841ff39
--- /dev/null
+++ b/command/test/hoth_command_close_unittest.cpp
@@ -0,0 +1,46 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandCloseTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandCloseTest, CloseWithInvalidSessionFails)
+{
+ // Verify you cannot close an invalid session.
+
+ EXPECT_FALSE(hvn.close(session_));
+}
+
+TEST_F(HothCommandCloseTest, CloseWithValidSessionSuccess)
+{
+ // Verify you can close a valid session.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.close(session_));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_commit_unittest.cpp b/command/test/hoth_command_commit_unittest.cpp
new file mode 100644
index 0000000..495aa59
--- /dev/null
+++ b/command/test/hoth_command_commit_unittest.cpp
@@ -0,0 +1,241 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+using ::testing::Return;
+
+using namespace std::literals;
+
+namespace ipmi_hoth
+{
+
+const auto static test_str = "Hello, world!"s;
+const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end());
+const auto static test2_str = "Good morning, world!"s;
+const std::vector<uint8_t> static test2_buf(test2_str.begin(), test2_str.end());
+
+sdbusplus::SdBusMock sdbusIntf;
+
+ACTION(SdbusThrow)
+{
+ EXPECT_CALL(sdbusIntf, sd_bus_error_set_errno(NotNull(), _))
+ .WillOnce(Return(0));
+ EXPECT_CALL(sdbusIntf, sd_bus_error_is_set(NotNull())).WillOnce(Return(1));
+ EXPECT_CALL(sdbusIntf, sd_bus_error_free(NotNull())).Times(1);
+
+ throw sdbusplus::exception::SdBusError(0, "", &sdbusIntf);
+}
+
+class HothCommandCommitTest : public HothCommandTest
+{
+ protected:
+ void expectValidCommand(std::string_view name,
+ const std::vector<uint8_t>& input)
+ {
+ EXPECT_CALL(dbus, SendHostCommand(name, ContainerEq(input), _))
+ .WillOnce([&](auto, const auto&, auto cb) {
+ this->cb = std::move(cb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ }
+ internal::DbusCommand::Cb cb;
+};
+
+TEST_F(HothCommandCommitTest, InvalidSessionCommitIsRejected)
+{
+ // Verify the hoth command handler checks for a valid session.
+
+ EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>()));
+}
+
+TEST_F(HothCommandCommitTest, UnexpectedDataParam)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3})));
+}
+
+TEST_F(HothCommandCommitTest, DbusCallFail)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectValidCommand("", test_buf);
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+
+ cb(std::nullopt);
+
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::commit_error | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+}
+
+TEST_F(HothCommandCommitTest, EmptyLegacyHoth)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectValidCommand("", {});
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+
+ cb(std::vector<uint8_t>{});
+
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(0, meta.size);
+ EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+}
+
+TEST_F(HothCommandCommitTest, EmptyNamedHoth)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(name))).WillOnce(Return(true));
+ expectValidCommand(name, {});
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), namedPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+
+ cb(std::vector<uint8_t>{});
+
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(0, meta.size);
+ EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+}
+
+// Tests the full commit process with example data
+TEST_F(HothCommandCommitTest, HappyPath)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectValidCommand("", test_buf);
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ cb(test2_buf);
+
+ std::vector<uint8_t> result = hvn.read(session_, 0, test2_buf.size());
+ EXPECT_THAT(result, ContainerEq(test2_buf));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+}
+
+// Tests that repeated commits only result in one D-Bus call
+TEST_F(HothCommandCommitTest, IdempotentSuccess)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectValidCommand("", {});
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+
+ cb(std::vector<uint8_t>{});
+
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+}
+
+// Tests that repeated commits will retry DBus calls if there is an error
+TEST_F(HothCommandCommitTest, ErrorRetry)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectValidCommand("", {});
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+
+ cb(std::nullopt);
+
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::commit_error | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+
+ expectValidCommand("", {});
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_read |
+ blobs::StateFlags::open_write,
+ meta.blobState);
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_delete_unittest.cpp b/command/test/hoth_command_delete_unittest.cpp
new file mode 100644
index 0000000..5af5d49
--- /dev/null
+++ b/command/test/hoth_command_delete_unittest.cpp
@@ -0,0 +1,32 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothCommandDeleteTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandDeleteTest, VerifyHothDeleteFails)
+{
+ // The hoth command handler does not support delete.
+
+ EXPECT_FALSE(hvn.deleteBlob(legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_open_unittest.cpp b/command/test/hoth_command_open_unittest.cpp
new file mode 100644
index 0000000..4420b9a
--- /dev/null
+++ b/command/test/hoth_command_open_unittest.cpp
@@ -0,0 +1,96 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandOpenTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandOpenTest, OpenWithBadFlagsFails)
+{
+ // Hoth command handler open requires both read & write set.
+
+ EXPECT_FALSE(hvn.open(session_, blobs::OpenFlags::read, legacyPath));
+}
+
+TEST_F(HothCommandOpenTest, OpenWithNoHothd)
+{
+ // Hoth command handler open without a backing hoth daemon present.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(false));
+ EXPECT_FALSE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothCommandOpenTest, OpenEverythingSucceeds)
+{
+ // Hoth command handler open with everything correct.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothCommandOpenTest, OpenEleventhSessionFails)
+{
+ // Hoth command handler only allows ten open sessions, verifies the 11th
+ // fails.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+ EXPECT_CALL(dbus, pingHothd(std::string_view(name)))
+ .WillRepeatedly(Return(true));
+ size_t sessId = 0;
+
+ for (int i = 0; i < hvn.maxSessions(); i++)
+ {
+ EXPECT_TRUE(hvn.open(sessId++, hvn.requiredFlags(), legacyPath));
+ }
+
+ for (int i = 0; i < hvn.maxSessions() - 3; i++)
+ {
+ EXPECT_TRUE(hvn.open(sessId++, hvn.requiredFlags(), namedPath));
+ }
+
+ EXPECT_FALSE(hvn.open(sessId++, hvn.requiredFlags(), legacyPath));
+
+ for (int i = hvn.maxSessions() - 3; i < hvn.maxSessions(); i++)
+ {
+ EXPECT_TRUE(hvn.open(sessId++, hvn.requiredFlags(), namedPath));
+ }
+
+ EXPECT_FALSE(hvn.open(sessId++, hvn.requiredFlags(), namedPath));
+}
+
+TEST_F(HothCommandOpenTest, CannotOpenSameSessionTwice)
+{
+ // Verify the hoth command handler won't let you open the same session
+ // twice.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_read_unittest.cpp b/command/test/hoth_command_read_unittest.cpp
new file mode 100644
index 0000000..bb9c2b7
--- /dev/null
+++ b/command/test/hoth_command_read_unittest.cpp
@@ -0,0 +1,122 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <ipmid/handler.hpp>
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Return;
+
+namespace ipmi_hoth
+{
+
+class HothCommandReadTest : public HothCommandTest
+{
+ protected:
+ const uint32_t testOffset_ = 0;
+ const std::vector<uint8_t> testData_ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ void openAndWriteTestData()
+ {
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ EXPECT_TRUE(hvn.write(session_, testOffset_, testData_));
+ }
+};
+
+TEST_F(HothCommandReadTest, InvalidSessionReadIsRejected)
+{
+ // Verify that read checks for a valid session, returns empty buffer for
+ // failed check.
+
+ openAndWriteTestData();
+
+ uint16_t wrongSession = session_ + 1;
+ EXPECT_THROW(hvn.read(wrongSession, testOffset_, testData_.size()),
+ ipmi::HandlerCompletion);
+}
+
+TEST_F(HothCommandReadTest, ReadOffsetBeyondBufferSizeReturnsEmpty)
+{
+ // Verify that read with offset beyond buffer size returns empty buffer.
+
+ openAndWriteTestData();
+
+ uint32_t offsetBeyondBuffer = testData_.size();
+ EXPECT_THAT(hvn.read(session_, offsetBeyondBuffer, testData_.size()),
+ IsEmpty());
+}
+
+TEST_F(HothCommandReadTest, ReadFullWrittenData)
+{
+ // Verify that the read successfully reads back the written data.
+
+ openAndWriteTestData();
+
+ EXPECT_EQ(testData_, hvn.read(session_, testOffset_, testData_.size()));
+}
+
+TEST_F(HothCommandReadTest, ReadWrittenDataAtOffset)
+{
+ // Verify that the read with offset returns the expected data.
+
+ openAndWriteTestData();
+
+ EXPECT_EQ(testData_, hvn.read(session_, testOffset_, testData_.size()));
+
+ // Try reading the written data byte by byte at each offset
+ for (size_t i = 0; i < testData_.size(); ++i)
+ {
+ EXPECT_THAT(hvn.read(session_, i, 1), ElementsAre(testData_[i]));
+ }
+}
+
+TEST_F(HothCommandReadTest, ReadFullWrittenDataWithBiggerRequestedSize)
+{
+ // Verify that read with requested size bigger than the written data will
+ // return a response buffer up to the end of the written buffer.
+
+ openAndWriteTestData();
+
+ uint32_t requestedSizeBeyondBuffer = testData_.size() + 1;
+ EXPECT_EQ(testData_,
+ hvn.read(session_, testOffset_, requestedSizeBeyondBuffer));
+}
+
+TEST_F(HothCommandReadTest, ReadWrittenDataAtOffsetWithBiggerRequestedSize)
+{
+ // Verify that read with requested size bigger than the written data at an
+ // offset will return a response buffer up to the end of the written buffer.
+
+ openAndWriteTestData();
+
+ uint32_t newOffset = testData_.size() / 2;
+ uint32_t requestedSizeBeyondBuffer = testData_.size() + 1;
+ std::vector<uint8_t> expectedData =
+ std::vector<uint8_t>(testData_.begin() + newOffset, testData_.end());
+
+ EXPECT_EQ(expectedData,
+ hvn.read(session_, newOffset, requestedSizeBeyondBuffer));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_sessionstat_unittest.cpp b/command/test/hoth_command_sessionstat_unittest.cpp
new file mode 100644
index 0000000..7f6876f
--- /dev/null
+++ b/command/test/hoth_command_sessionstat_unittest.cpp
@@ -0,0 +1,78 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandSessionStatTest : public HothCommandTest
+{
+ protected:
+ blobs::BlobMeta meta_;
+ // Initialize expected_meta_ with empty members
+ blobs::BlobMeta expected_meta_ = {};
+};
+
+TEST_F(HothCommandSessionStatTest, InvalidSessionStatIsRejected)
+{
+ // Verify the hoth command handler checks for a valid session.
+
+ EXPECT_FALSE(hvn.stat(session_, &meta_));
+}
+
+TEST_F(HothCommandSessionStatTest, SessionStatAlwaysInitialReadAndWrite)
+{
+ // Verify the session stat returns the information for a session.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+ expected_meta_.blobState =
+ blobs::StateFlags::open_read | blobs::StateFlags::open_write;
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothCommandSessionStatTest, AfterWriteMetadataLengthMatches)
+{
+ // Verify that after writes, the length returned matches.
+
+ std::vector<uint8_t> data = {0x01};
+ EXPECT_EQ(1, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+ // We wrote one byte to the last index, making the length the buffer size.
+ expected_meta_.size = hvn.maxBufferSize();
+ expected_meta_.blobState =
+ blobs::StateFlags::open_read | blobs::StateFlags::open_write;
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_unittest.cpp b/command/test/hoth_command_unittest.cpp
new file mode 100644
index 0000000..97e1bf3
--- /dev/null
+++ b/command/test/hoth_command_unittest.cpp
@@ -0,0 +1,84 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothCommandBasicTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandBasicTest, PathToHothId)
+{
+ EXPECT_EQ("", hvn.pathToHothId("/dev/hoth/command_passthru"));
+ EXPECT_EQ("hoth0", hvn.pathToHothId("/dev/hoth/hoth0/command_passthru"));
+ EXPECT_EQ("", hvn.pathToHothId("/dev/hoth//command_passthru"));
+ EXPECT_EQ("", hvn.pathToHothId("/dev/hoth////command_passthru"));
+}
+
+TEST_F(HothCommandBasicTest, HothIdToPath)
+{
+ EXPECT_EQ("/dev/hoth/command_passthru", hvn.hothIdToPath(""));
+ EXPECT_EQ("/dev/hoth/hoth0/command_passthru", hvn.hothIdToPath("hoth0"));
+}
+
+TEST_F(HothCommandBasicTest, CanHandleBlobChecksNameInvalid)
+{
+ // Verify canHandleBlob checks and returns false on an invalid name.
+
+ EXPECT_FALSE(hvn.canHandleBlob("asdf"));
+ EXPECT_FALSE(hvn.canHandleBlob("dev/hoth/command_passthru"));
+ EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/command_passthru2"));
+ EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/hoth0/t/command_passthru"));
+ EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/firmware_update"));
+}
+
+TEST_F(HothCommandBasicTest, CanHandleBlobChecksNameValid)
+{
+ // Verify canHandleBlob checks and returns true on the valid name.
+
+ EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/command_passthru"));
+ EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/hoth0/command_passthru"));
+}
+
+TEST_F(HothCommandBasicTest, VerifyBehaviorOfBlobIds)
+{
+ // Verify the correct BlobIds is returned from the hoth command handler.
+ internal::Dbus::SubTreeMapping mapping = {
+ {"/xyz/openbmc_project/Control", {}},
+ {"/xyz/openbmc_project/Control/Hoth2nologue", {}},
+ {"/xyz/openbmc_project/Control/Hoth/nologue/2", {}},
+ {"/xyz/openbmc_project/Control/Hoth", {}},
+ {"/xyz/openbmc_project/Control/Hoth/hoth0", {}},
+ {"/xyz/openbmc_project/Control/Hoth/hoth1", {}},
+ };
+ EXPECT_CALL(dbus, getHothdMapping()).WillOnce(Return(mapping));
+ std::vector<std::string> expected = {
+ "/dev/hoth/command_passthru",
+ "/dev/hoth/hoth0/command_passthru",
+ "/dev/hoth/hoth1/command_passthru",
+ };
+ EXPECT_THAT(hvn.getBlobIds(), UnorderedElementsAreArray(expected));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_unittest.hpp b/command/test/hoth_command_unittest.hpp
new file mode 100644
index 0000000..bf64731
--- /dev/null
+++ b/command/test/hoth_command_unittest.hpp
@@ -0,0 +1,48 @@
+// 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 "dbus_command_mock.hpp"
+#include "hoth_command.hpp"
+
+#include <stdplus/util/string.hpp>
+
+#include <cstdint>
+#include <string>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothCommandTest : public ::testing::Test
+{
+ protected:
+ explicit HothCommandTest() : hvn(&dbus) {}
+
+ // dbus mock object
+ testing::StrictMock<internal::DbusCommandMock> dbus;
+
+ HothCommandBlobHandler hvn;
+
+ const uint16_t session_ = 0;
+ const std::string legacyPath = stdplus::util::strCat(
+ HothCommandBlobHandler::hothPathPrefix, hvn.pathSuffix());
+ const std::string name = "hoth0";
+ const std::string namedPath = stdplus::util::strCat(
+ HothCommandBlobHandler::hothPathPrefix, name, "/", hvn.pathSuffix());
+};
+
+} // namespace ipmi_hoth
diff --git a/command/test/hoth_command_write_unittest.cpp b/command/test/hoth_command_write_unittest.cpp
new file mode 100644
index 0000000..702e633
--- /dev/null
+++ b/command/test/hoth_command_write_unittest.cpp
@@ -0,0 +1,133 @@
+// 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 "hoth_command_unittest.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothCommandWriteTest : public HothCommandTest
+{};
+
+TEST_F(HothCommandWriteTest, InvalidSessionWriteIsRejected)
+{
+ // Verify the hoth command handler checks for a valid session.
+
+ std::vector<uint8_t> data = {0x1, 0x2};
+
+ EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingTooMuchByOneByteFails)
+{
+ // Test the edge case of writing 1 byte too much with an offset of 0.
+ // writing 1025 at offset 0 is invalid.
+
+ int bytes = hvn.maxBufferSize() + 1;
+ std::vector<uint8_t> data(0x11);
+ data.resize(bytes);
+ ASSERT_EQ(bytes, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingTooMuchByOffsetOfOne)
+{
+ // Test the edge case of writing 1024 bytes (which is fine) but at the
+ // offset 1, which makes it go over by 1 byte.
+ // writing 1024 at offset 1 is invalid.
+
+ int bytes = hvn.maxBufferSize();
+ std::vector<uint8_t> data(0x11);
+ data.resize(bytes);
+ ASSERT_EQ(bytes, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ // offset of 1.
+ EXPECT_FALSE(hvn.write(session_, 1, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+ // Test the edge case of writing to the last byte offset but trying to
+ // write two bytes.
+ // writing 2 bytes at 1023 is invalid.
+
+ std::vector<uint8_t> data = {0x01, 0x02};
+ ASSERT_EQ(2, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+TEST_F(HothCommandWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+ // Test the edge case of writing one byte but exactly one byte beyond the
+ // buffer.
+ // writing 1 byte at 1024 is invalid.
+
+ std::vector<uint8_t> data = {0x01};
+ ASSERT_EQ(1, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.write(session_, hvn.maxBufferSize(), data));
+}
+
+TEST_F(HothCommandWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+ // Test the case where you write the full buffer length at once to the 0th
+ // offset.
+
+ int bytes = hvn.maxBufferSize();
+ std::vector<uint8_t> data = {0x01};
+ data.resize(bytes);
+ ASSERT_EQ(bytes, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothCommandWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+ // Test the case where you write the last byte.
+
+ std::vector<uint8_t> data = {0x01};
+ ASSERT_EQ(1, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+} // namespace ipmi_hoth
diff --git a/command/test/meson.build b/command/test/meson.build
new file mode 100644
index 0000000..2adea16
--- /dev/null
+++ b/command/test/meson.build
@@ -0,0 +1,20 @@
+tests = [
+ 'hoth_command_unittest',
+ 'hoth_command_close_unittest',
+ 'hoth_command_commit_unittest',
+ 'hoth_command_delete_unittest',
+ 'hoth_command_open_unittest',
+ 'hoth_command_read_unittest',
+ 'hoth_command_sessionstat_unittest',
+ 'hoth_command_write_unittest',
+]
+
+foreach t : tests
+ test(
+ t,
+ executable(
+ t.underscorify(),
+ t + '.cpp',
+ implicit_include_directories: false,
+ dependencies: [hothcommand_dep, test_dep]))
+endforeach
diff --git a/dbus.cpp b/dbus.cpp
new file mode 100644
index 0000000..3a38ac8
--- /dev/null
+++ b/dbus.cpp
@@ -0,0 +1,98 @@
+// 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 "dbus.hpp"
+
+#include "dbus_impl.hpp"
+
+#include <ipmid/api.h>
+
+#include <sdbusplus/exception.hpp>
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+DbusImpl::DbusImpl() : bus(ipmid_get_sd_bus_connection()) {}
+
+Dbus::SubTreeMapping DbusImpl::getHothdMapping()
+{
+ auto req =
+ bus.new_method_call("xyz.openbmc_project.ObjectMapper",
+ "/xyz/openbmc_project/object_mapper",
+ "xyz.openbmc_project.ObjectMapper", "GetSubTree");
+ req.append("/");
+ req.append(0);
+ req.append(std::vector<std::string>({"xyz.openbmc_project.Control.Hoth"}));
+ auto rsp = bus.call(req, kTimeout);
+ SubTreeMapping mapping;
+ rsp.read(mapping);
+ return mapping;
+}
+
+std::string hothIdToSvc(std::string_view hothId)
+{
+ std::string svc = "xyz.openbmc_project.Control.Hoth";
+ if (!hothId.empty())
+ {
+ svc += ".";
+ svc += hothId;
+ }
+ return svc;
+}
+
+bool DbusImpl::pingHothd(std::string_view hothId)
+{
+ try
+ {
+ auto req =
+ bus.new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus",
+ "org.freedesktop.DBus", "GetNameOwner");
+ req.append(hothIdToSvc(hothId));
+ bus.call(req, kTimeout);
+ return true;
+ }
+ catch (const sdbusplus::exception::SdBusError& e)
+ {
+ return false;
+ }
+}
+
+sdbusplus::message::message DbusImpl::newHothdCall(
+ std::string_view hothId, const char* intf, const char* method)
+{
+ std::string svc = hothIdToSvc(hothId);
+ std::string obj = "/xyz/openbmc_project/Control/Hoth";
+ if (!hothId.empty())
+ {
+ obj += "/";
+ obj += hothId;
+ }
+ return bus.new_method_call(svc.c_str(), obj.c_str(), intf, method);
+}
+
+sdbusplus::message::message
+ DbusImpl::newHothdCall(std::string_view hothId, const char* method)
+{
+ return newHothdCall(hothId, "xyz.openbmc_project.Control.Hoth", method);
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/dbus.hpp b/dbus.hpp
new file mode 100644
index 0000000..b01ad1d
--- /dev/null
+++ b/dbus.hpp
@@ -0,0 +1,74 @@
+// 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 <sdbusplus/sdbus.hpp>
+#include <sdbusplus/slot.hpp>
+#include <stdplus/cancel.hpp>
+
+#include <chrono>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+struct BusCall : public stdplus::Cancelable
+{
+ sdbusplus::slot::slot slot;
+ explicit BusCall(sdbusplus::slot::slot&& slot) : slot(std::move(slot)) {}
+ void cancel() noexcept override
+ {
+ delete this;
+ }
+};
+
+/** @class Dbus
+ * @brief Overridable D-Bus interface for generic handler
+ */
+class Dbus
+{
+ public:
+ /** An arbitrary timeout to ensure that clients don't linger forever */
+ static constexpr auto asyncCallTimeout = std::chrono::minutes(3);
+
+ using SubTreeMapping = std::unordered_map<
+ std::string, std::unordered_map<std::string, std::vector<std::string>>>;
+
+ virtual ~Dbus() = default;
+
+ /** @brief Gets the D-Bus mapper information for all hoth instances
+ *
+ * @throw exception::SdBusError - All dbus exceptions will be thrown
+ * with this exception
+ * @return The mapping of object paths and services to hoth interfaces.
+ */
+ virtual SubTreeMapping getHothdMapping() = 0;
+
+ /** @brief Determines if a hothd instance is running on the system
+ *
+ * @return True if the hoth is running, false for other errors.
+ */
+ virtual bool pingHothd(std::string_view hothId) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/dbus_impl.hpp b/dbus_impl.hpp
new file mode 100644
index 0000000..022c0b3
--- /dev/null
+++ b/dbus_impl.hpp
@@ -0,0 +1,73 @@
+// 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 "dbus.hpp"
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <chrono>
+#include <string_view>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusImpl
+ * @brief D-Bus concrete implementation
+ * @details Pass through all calls to the default D-Bus instance
+ */
+class DbusImpl : public virtual Dbus
+{
+ public:
+ /** @brief Timeout suitable for responding to IPMI queries before
+ * the sending mechanism like kcsbridge issues a retry.
+ */
+ static constexpr auto kTimeout = std::chrono::seconds(4);
+
+ DbusImpl();
+
+ SubTreeMapping getHothdMapping() override;
+ bool pingHothd(std::string_view hothId) override;
+
+ protected:
+ /** @brief Helper for building a message targeting a hoth daemon
+ *
+ * @param[in] hothId - The identifier of the hoth daemon
+ * @param[in] intf - The name of the D-Bus interface being called
+ * @param[in] method - The D-Bus method being called on the interface
+ * @return The empty message which can be sent.
+ */
+ sdbusplus::message::message newHothdCall(
+ std::string_view hothId, const char* intf, const char* method);
+
+ /** @brief Helper for building a message targeting a hoth daemon
+ * assuming the call is for the hoth interface.
+ *
+ * @param[in] hothId - The identifier of the hoth daemon
+ * @param[in] method - The D-Bus method being called on the interface
+ * @return The empty message which can be sent.
+ */
+ sdbusplus::message::message newHothdCall(std::string_view hothId,
+ const char* method);
+
+ sdbusplus::bus::bus bus;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/hoth.cpp b/hoth.cpp
new file mode 100644
index 0000000..d3918e7
--- /dev/null
+++ b/hoth.cpp
@@ -0,0 +1,267 @@
+// 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 "hoth.hpp"
+
+#include <ipmid/api-types.hpp>
+#include <ipmid/handler.hpp>
+#include <stdplus/util/string.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+std::string_view HothBlobHandler::pathToHothId(std::string_view path)
+{
+ if (path.starts_with(hothPathPrefix) && path.ends_with(pathSuffix()))
+ {
+ auto id = path.substr(
+ hothPathPrefix.size(),
+ path.size() - pathSuffix().size() - hothPathPrefix.size());
+ auto count = std::count(id.begin(), id.end(), '/');
+
+ // There can only be prefix + pathSuffix or
+ // prefix + id/pathSuffix.
+ // prefix already has one `/`
+ if (count == 1)
+ {
+ return id.substr(0, id.size() - 1);
+ }
+ else if (count > 1)
+ {
+ return "";
+ }
+
+ return id;
+ }
+ return "";
+}
+
+std::string HothBlobHandler::hothIdToPath(std::string_view hothId)
+{
+ if (hothId.empty())
+ {
+ return stdplus::util::strCat(hothPathPrefix, pathSuffix());
+ }
+ return stdplus::util::strCat(hothPathPrefix, hothId, "/", pathSuffix());
+}
+
+HothBlob* HothBlobHandler::getSession(uint16_t id)
+{
+ auto search = sessions.find(id);
+ if (search == sessions.end())
+ {
+ return nullptr;
+ }
+
+ /* Not thread-safe, however, the blob handler deliberately assumes serial
+ * execution. */
+ return search->second.get();
+}
+
+std::optional<uint16_t> HothBlobHandler::getOnlySession(std::string_view hothId)
+{
+ /* This method is only valid if we only allow 1 session */
+ if (maxSessions() != 1)
+ {
+ return std::nullopt;
+ }
+
+ for (auto& session : pathSessions[std::string(hothId)])
+ {
+ return session;
+ }
+
+ return std::nullopt;
+}
+
+bool HothBlobHandler::canHandleBlob(const std::string& path)
+{
+ if (path.starts_with(hothPathPrefix) && path.ends_with(pathSuffix()))
+ {
+ // There can only be hothPathPrefix + pathSuffix or
+ // hothPathPrefix + id/pathSuffix
+ // hothPathPrefix already has one `/`
+ return std::count(path.begin() + hothPathPrefix.size(),
+ path.end() - pathSuffix().size(), '/') <= 1;
+ }
+ return false;
+}
+
+std::vector<std::string> HothBlobHandler::getBlobIds()
+{
+ std::vector<std::string> ret;
+ for (const auto& obj : dbus().getHothdMapping())
+ {
+ std::string_view objView = obj.first;
+ std::string_view objPrefix = "/xyz/openbmc_project/Control/Hoth";
+ if (objView.substr(0, objPrefix.size()) != objPrefix)
+ {
+ continue;
+ }
+ objView.remove_prefix(objPrefix.size());
+ if (objView.empty())
+ {
+ ret.push_back(hothIdToPath(""));
+ continue;
+ }
+ if (objView[0] != '/')
+ {
+ continue;
+ }
+ objView.remove_prefix(1);
+ auto sep = objView.find('/');
+ if (sep != std::string_view::npos)
+ {
+ continue;
+ }
+ ret.push_back(hothIdToPath(objView));
+ }
+ return ret;
+}
+
+bool HothBlobHandler::deleteBlob(const std::string&)
+{
+ /* Hoth blob handler does not support a blob delete. */
+ return false;
+}
+
+bool HothBlobHandler::open(uint16_t session, uint16_t flags,
+ const std::string& path)
+{
+ /* We require both flags set. */
+ if ((flags & requiredFlags()) != requiredFlags())
+ {
+ /* Both flags not set. */
+ return false;
+ }
+
+ auto findSess = sessions.find(session);
+ if (findSess != sessions.end())
+ {
+ /* This session is already active. */
+ return false;
+ }
+
+ auto hothId = std::string(pathToHothId(path));
+ auto pathSession = pathSessions.find(hothId);
+ if (pathSession != pathSessions.end() &&
+ pathSession->second.size() >= maxSessions())
+ {
+ return false;
+ }
+
+ // Prevent host from adding lots of bad entries to the table by verifying
+ // the hoth exists.
+ if (pathSession == pathSessions.end() && !dbus().pingHothd(hothId))
+ {
+ return false;
+ }
+
+ pathSessions[hothId].emplace(session);
+ sessions.emplace(session,
+ std::make_unique<HothBlob>(session, std::move(hothId),
+ flags, maxBufferSize()));
+ return true;
+}
+
+std::vector<uint8_t> HothBlobHandler::read(uint16_t session, uint32_t offset,
+ uint32_t requestedSize)
+{
+ HothBlob* sess = getSession(session);
+ if (!sess || !(sess->state & blobs::StateFlags::open_read) ||
+ offset > sess->buffer.size())
+ {
+ throw ipmi::HandlerCompletion(ipmi::ccUnspecifiedError);
+ }
+
+ if (sess->buffer.size() > offset)
+ {
+ std::vector<uint8_t> ret(
+ std::min<size_t>(requestedSize, sess->buffer.size() - offset));
+ std::memcpy(ret.data(), sess->buffer.data() + offset, ret.size());
+ return ret;
+ }
+ return {};
+}
+
+bool HothBlobHandler::write(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data)
+{
+ uint32_t newBufferSize = data.size() + offset;
+ HothBlob* sess = getSession(session);
+ if (!sess || !(sess->state & blobs::StateFlags::open_write) ||
+ newBufferSize > maxBufferSize())
+ {
+ return false;
+ }
+
+ /* Resize the buffer if what we're writing will go over the size */
+ if (newBufferSize > sess->buffer.size())
+ {
+ sess->buffer.resize(newBufferSize);
+ sess->state &= ~blobs::StateFlags::committed;
+ }
+
+ /* Clear the comitted bit if our data isn't identical to existing data */
+ if (std::memcmp(sess->buffer.data() + offset, data.data(), data.size()))
+ {
+ sess->state &= ~blobs::StateFlags::committed;
+ }
+ if (!data.empty())
+ {
+ std::memcpy(sess->buffer.data() + offset, data.data(), data.size());
+ }
+ return true;
+}
+
+bool HothBlobHandler::writeMeta(uint16_t, uint32_t, const std::vector<uint8_t>&)
+{
+ /* Hoth blob handler does not support meta write. */
+ return false;
+}
+
+bool HothBlobHandler::close(uint16_t session)
+{
+ auto session_it = sessions.find(session);
+ if (session_it == sessions.end())
+ {
+ return false;
+ }
+
+ auto path_it = pathSessions.find(session_it->second->hothId);
+ path_it->second.erase(session);
+ if (path_it->second.empty())
+ {
+ pathSessions.erase(path_it);
+ }
+
+ sessions.erase(session_it);
+ return true;
+}
+
+bool HothBlobHandler::expire(uint16_t session)
+{
+ return close(session);
+}
+
+} // namespace ipmi_hoth
diff --git a/hoth.hpp b/hoth.hpp
new file mode 100644
index 0000000..37dac70
--- /dev/null
+++ b/hoth.hpp
@@ -0,0 +1,128 @@
+// 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 "dbus.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+#include <stdplus/cancel.hpp>
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+struct HothBlob
+{
+ HothBlob(uint16_t id, std::string&& hothId, uint16_t flags,
+ uint32_t maxBufferSize) :
+ sessionId(id), hothId(std::move(hothId)), state(0)
+ {
+ if (flags & blobs::OpenFlags::read)
+ {
+ state |= blobs::StateFlags::open_read;
+ }
+ if (flags & blobs::OpenFlags::write)
+ {
+ state |= blobs::StateFlags::open_write;
+ }
+
+ /* Pre-allocate the buffer.capacity() with maxBufferSize */
+ buffer.reserve(maxBufferSize);
+ }
+ ~HothBlob()
+ {
+ /* We want to deliberately wipe the buffer to avoid leaking any
+ * sensitive ProdID secrets.
+ */
+ buffer.assign(buffer.capacity(), 0);
+ }
+
+ /* The blob handler session id. */
+ uint16_t sessionId;
+
+ /* The identifier for the hoth */
+ std::string hothId;
+
+ /* The current state. */
+ uint16_t state;
+
+ /* The staging buffer. */
+ std::vector<uint8_t> buffer;
+
+ /* Outstanding async operation */
+ stdplus::Cancel outstanding;
+};
+
+class HothBlobHandler : public blobs::GenericBlobInterface
+{
+ public:
+ static constexpr std::string_view hothPathPrefix = "/dev/hoth/";
+
+ bool canHandleBlob(const std::string& path) override;
+ std::vector<std::string> getBlobIds() override;
+ bool deleteBlob(const std::string& path) override;
+ virtual bool stat(const std::string& path, blobs::BlobMeta* meta) = 0;
+ bool open(uint16_t session, uint16_t flags,
+ const std::string& path) override;
+ std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+ uint32_t requestedSize) override;
+ bool write(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data) override;
+ bool writeMeta(uint16_t session, uint32_t offset,
+ const std::vector<uint8_t>& data) override;
+ virtual bool commit(uint16_t session, const std::vector<uint8_t>& data) = 0;
+ bool close(uint16_t session) override;
+ virtual bool stat(uint16_t session, blobs::BlobMeta* meta) = 0;
+ bool expire(uint16_t session) override;
+
+ virtual internal::Dbus& dbus() = 0;
+ virtual std::string_view pathSuffix() const = 0;
+ virtual uint16_t requiredFlags() const = 0;
+ virtual uint16_t maxSessions() const = 0;
+ virtual uint32_t maxBufferSize() const = 0;
+
+ /** @brief Takes a valid hoth blob path and turns it into a hoth id
+ *
+ * @param[in] path - Hoth blob path to parse
+ * @return The hoth id represented by the path
+ */
+ std::string_view pathToHothId(std::string_view path);
+
+ /** @brief Takes a hoth id and turns it into a fully qualified path for
+ * the current hoth handler.
+ *
+ * @param[in] hothId - The hoth id
+ * @return The fully qualified blob path
+ */
+ std::string hothIdToPath(std::string_view hothId);
+
+ protected:
+ HothBlob* getSession(uint16_t id);
+ std::optional<uint16_t> getOnlySession(std::string_view hothId);
+
+ private:
+ std::unordered_map<std::string, std::unordered_set<uint16_t>> pathSessions;
+ std::unordered_map<uint16_t, std::unique_ptr<HothBlob>> sessions;
+};
+
+} // namespace ipmi_hoth
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..cdab3c1
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,54 @@
+project(
+ 'hoth-ipmi-blobs',
+ 'cpp',
+ version: '0.1',
+ meson_version: '>=1.1.1',
+ default_options: [
+ 'b_lundef=false',
+ 'cpp_std=c++23',
+ 'warning_level=3',
+ 'werror=true',
+ ])
+
+sdbusplus_dep = dependency('sdbusplus', fallback: ['sdbusplus','sdbusplus_dep'])
+stdplus_dep = dependency('stdplus', fallback: ['stdplus','stdplus_dep'])
+ipmi_blob_dep = dependency('phosphor-ipmi-blobs')
+phosphor_logging_dep = dependency('phosphor-logging')
+
+meson.get_compiler('cpp').has_header_symbol(
+ 'ipmid/api.h',
+ 'ipmid_get_sd_bus_connection')
+
+hothblob_pre = declare_dependency(
+ include_directories: include_directories('.'),
+ dependencies: [
+ ipmi_blob_dep,
+ sdbusplus_dep,
+ stdplus_dep,
+ ])
+
+hothblob_lib = library(
+ 'hothblob',
+ 'dbus.cpp',
+ 'hoth.cpp',
+ implicit_include_directories: false,
+ dependencies: hothblob_pre,
+ version: meson.project_version(),
+ install: true)
+
+hothblob_dep = declare_dependency(
+ link_with: hothblob_lib,
+ dependencies: hothblob_pre)
+
+if not get_option('tests').disabled()
+ subdir('test')
+endif
+
+subdir('command')
+if get_option('skm')
+ subdir('skmhss')
+endif
+
+if get_option('hoth-inband-update').allowed()
+ subdir('update')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..86a8705
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,4 @@
+option('tests', type: 'feature', description: 'Build tests')
+option('skm', type: 'boolean', description: 'Support SKM')
+option('skmhss-hothid-config', type: 'string', value: '/usr/share/hoth-ipmi-blobs/hothid_basepaths.json', description: 'Path to basepath to hothId mapping Json config')
+option('hoth-inband-update', type: 'feature', description: 'Support hoth firmware update via IPMI')
diff --git a/skmhss/google3/g_ec_commands.h b/skmhss/google3/g_ec_commands.h
new file mode 100644
index 0000000..9a93e3c
--- /dev/null
+++ b/skmhss/google3/g_ec_commands.h
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+#ifndef SECURITY_CRYPTA_COMMANDS_CHROMIUMOS_G_EC_COMMANDS_H_
+#define SECURITY_CRYPTA_COMMANDS_CHROMIUMOS_G_EC_COMMANDS_H_
+
+#include <stdint.h>
+
+#define G_EC_HOST_REQUEST_VERSION 3
+#define G_EC_HOST_RESPONSE_VERSION 3
+
+#define G_EC_CMD_BOARD_SPECIFIC_BASE 0x3E00
+
+/* Given the private host command offset, calculate the true private host
+ * command value.
+ */
+#define G_EC_PRIVATE_HOST_COMMAND_VALUE(command) \
+ (G_EC_CMD_BOARD_SPECIFIC_BASE + (command))
+
+/* Version 3 request from host */
+typedef struct
+{
+ /* Structure version (=3)
+ *
+ * EC will return EC_RES_INVALID_HEADER if it receives a header with a
+ * version it doesn't know how to parse.
+ */
+ uint8_t struct_version;
+
+ /* Checksum of request and data; sum of all bytes including checksum should
+ * total to 0.
+ */
+ uint8_t checksum;
+
+ /* Command code */
+ uint16_t command;
+
+ /* Command version */
+ uint8_t command_version;
+
+ /* Unused byte in current protocol version; set to 0 */
+ uint8_t reserved;
+
+ /* Length of data which follows this header */
+ uint16_t data_len;
+} __attribute__((packed)) g_ec_host_request;
+
+/* Version 3 response from EC */
+typedef struct
+{
+ /* Structure version (=3) */
+ uint8_t struct_version;
+
+ /* Checksum of response and data; sum of all bytes including checksum should
+ * total to 0.
+ */
+ uint8_t checksum;
+
+ /* Result code (EC_RES_*) */
+ uint16_t result;
+
+ /* Length of data which follows this header */
+ uint16_t data_len;
+
+ /* Unused bytes in current protocol version; set to 0 */
+ uint16_t reserved;
+} __attribute__((packed)) g_ec_host_response;
+
+#endif // SECURITY_CRYPTA_COMMANDS_CHROMIUMOS_G_EC_COMMANDS_H_
diff --git a/skmhss/google3/host_commands.h b/skmhss/google3/host_commands.h
new file mode 100644
index 0000000..087f71c
--- /dev/null
+++ b/skmhss/google3/host_commands.h
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+#ifndef __PRIVATE_CR51_INCLUDE_CR51_HOST_COMMANDS_H
+#define __PRIVATE_CR51_INCLUDE_CR51_HOST_COMMANDS_H
+
+#include <stdint.h>
+
+/* Read/Write/Erase Hoth's copy of SKM HSS persisted to gNVRAM. */
+#define EC_PRV_CMD_HOTH_SKM_HSS 0x0015
+
+#define SKM_HSS_STRUCT_SIZE 64
+enum skm_hss_op
+{
+ SKM_HSS_READ = 0,
+ SKM_HSS_WRITE = 1,
+ SKM_HSS_DELETE = 2,
+};
+
+/* Serves as the response to SKM_HSS_READ as well as payload to SKM_HSS_WRITE.
+ */
+struct ec_payload_skm_hss
+{
+ uint8_t data[SKM_HSS_STRUCT_SIZE];
+} __attribute__((packed));
+
+struct ec_request_skm_hss
+{
+ /* The operation is one of skm_hss_op. */
+ uint32_t operation;
+ /* Choose which (0-3) HSS to operate on. */
+ uint32_t slot;
+ struct ec_payload_skm_hss payload;
+} __attribute__((packed));
+
+#endif /* __PRIVATE_CR51_INCLUDE_CR51_HOST_COMMANDS_H */
diff --git a/skmhss/hoth_skmhss.cpp b/skmhss/hoth_skmhss.cpp
new file mode 100644
index 0000000..fc33250
--- /dev/null
+++ b/skmhss/hoth_skmhss.cpp
@@ -0,0 +1,464 @@
+// 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 "hoth_skmhss.hpp"
+
+#include "config.hpp"
+
+#include <fmt/format.h>
+#include <google3/host_commands.h>
+
+#include <blobs-ipmid/blobs.hpp>
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/log.hpp>
+#include <stdplus/cancel.hpp>
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <algorithm>
+#include <charconv>
+#include <cstring>
+#include <fstream>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <vector>
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+
+using level = phosphor::logging::level;
+using SdBusError = sdbusplus::exception::SdBusError;
+using CommandFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::CommandFailure;
+using ResponseFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+using json = nlohmann::json;
+
+namespace ipmi_hoth
+{
+
+namespace internal
+{
+
+/**
+ * @brief: Get baseId from a blob id string
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ * characters and '/'.
+ * @returns: The baseId containing the blobId, stripping all contents from the
+ * last '/'. If no '/' is present, an empty string is returned.
+ */
+static std::string_view getBaseFromID(std::string_view blobId)
+{
+ return blobId.substr(0, blobId.find_last_of('/') + 1);
+}
+
+/**
+ * @brief: Get the blob identifer under the baseId
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ * characters and '/'.
+ * @returns: The truncated blobId, by deleting the baseId prefix
+ * in the input blobId.
+ */
+static std::string_view stripBaseFromID(std::string_view blobId)
+{
+ return blobId.substr(blobId.find_last_of('/') + 1);
+}
+
+/**
+ * @brief: Get the slot number from a blob id string
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ * characters and '/'.
+ * @returns: The slot number if the blob id has a tailing integer; otherwise
+ * returns a null pointer;
+ */
+static std::optional<uint32_t> extractSlotFromID(std::string_view blobId)
+{
+ std::string_view pathId = internal::stripBaseFromID(blobId);
+ uint32_t slot;
+ const auto res =
+ std::from_chars(pathId.data(), pathId.data() + pathId.size(), slot);
+
+ if (res.ec != std::errc() || res.ptr != pathId.data() + pathId.size())
+ {
+ return std::nullopt;
+ }
+
+ return slot;
+}
+
+} // namespace internal
+
+void HothSKMHSSBlobHandler::loadHSS()
+{
+ readBasePathConfig();
+ for (auto& [basePath, hothId] : hothIdBasePaths)
+ {
+ slotPopulated[basePath] = {};
+ inits[basePath] = {};
+ dels[basePath] = {};
+ /* Iterate all slots and find valid HSS copies */
+ for (uint32_t slot = 0; slot < maxNumHSSSlot; slot++)
+ {
+ auto& init = inits[basePath][slot];
+ init = readSKMHSSSlot(
+ basePath, slot, hothId,
+ [&](std::span<const uint8_t>) noexcept { init.reset(); });
+ }
+ }
+}
+
+/* read the hothid_basepaths.config and store it in hothIdBasePaths*/
+/**
+ * @brief: Read the hothid_basepaths.config and store it in a bimap.
+ * It populates basePath <--> hothId mapping
+ * @param :
+ * @returns:
+ */
+void HothSKMHSSBlobHandler::readBasePathConfig()
+{
+ std::ifstream configFile(HOTHID_CONFIG);
+
+ // Check if the config file exists
+ if (configFile.fail())
+ {
+ log<level::ERR>(
+ "config file does not exist, populate map with defaults");
+
+ // There is no config file,
+ // So, populate with default values
+ hothIdBasePaths.insert({"/skm/hss-backup/", ""});
+ return;
+ }
+
+ try
+ {
+ json data = json::parse(configFile);
+ for (const auto& [basePath, hothId] : data.items())
+ {
+ hothIdBasePaths.insert({basePath, hothId});
+ }
+ }
+ catch (json::parse_error& ex)
+ {
+ log<level::ERR>("config parse error at ", entry("ERR_MSG=%d", ex.byte));
+ }
+}
+
+/**
+ * @brief: Get the hothId from a blob id string
+ * @param blobId: Input blob id which is expected to only contain alphanumerical
+ * characters and '/'.
+ * @returns: The hothId from the map hothIdBasePaths if it exists; otherwise
+ * empty string
+ */
+std::string_view
+ HothSKMHSSBlobHandler::getHothIdFromPath(std::string_view blobId)
+{
+ auto base = internal::getBaseFromID(blobId);
+ auto it = hothIdBasePaths.left.find(static_cast<std::string>(base));
+
+ if (it != hothIdBasePaths.left.end())
+ {
+ return it->second;
+ }
+ return "";
+}
+
+/**
+ * @brief: Get the base path from a hothId string
+ * @param hothId: Input hothId string
+ * @returns: The hothId from the bimap hothIdBasePaths if it exists; otherwise
+ * empty string
+ */
+std::string_view
+ HothSKMHSSBlobHandler::getBaseFromHothId(std::string_view hothId)
+{
+ auto it = hothIdBasePaths.right.find(static_cast<std::string>(hothId));
+ if (it != hothIdBasePaths.right.end())
+ {
+ return it->second;
+ }
+ return "";
+}
+
+stdplus::Cancel HothSKMHSSBlobHandler::readSKMHSSSlot(
+ std::string_view base, uint32_t slot, std::string_view hothId,
+ fu2::unique_function<void(std::span<const uint8_t>)>&& cb)
+{
+ auto req = hothUtil_->readSKMHSS(slot);
+ return dbus_->SendHostCommand(
+ hothId, req,
+ [this, base, slot, cb = std::move(cb)](
+ std::optional<std::vector<uint8_t>> rsp) mutable noexcept {
+ try
+ {
+ auto data = hothUtil_->payloadECResponse(rsp.value());
+ this->slotPopulated[base][slot] =
+ data.size() == SKM_HSS_STRUCT_SIZE;
+ cb(data);
+ return;
+ }
+ catch (const ResponseFailure& e)
+ {
+ log<level::ERR>("Invalid Hoth response",
+ entry("ERR_MSG=%s", e.what()));
+ }
+ catch (...)
+ {}
+ cb({});
+ });
+}
+
+bool HothSKMHSSBlobHandler::canHandleBlob(const std::string& path)
+{
+ auto base = internal::getBaseFromID(path);
+ if (hothIdBasePaths.left.find(static_cast<std::string>(base)) ==
+ hothIdBasePaths.left.end())
+ {
+ return false;
+ }
+
+ auto hothId = getHothIdFromPath(path);
+ if (!dbus_->pingHothd(hothId))
+ {
+ return false;
+ }
+
+ /* The HSS path is expected to be "/skm/hss-backup/{0,1,2,...}" */
+ std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+ if (!slotPtr)
+ {
+ return false;
+ }
+ return *slotPtr < maxNumHSSSlot;
+}
+
+std::vector<std::string> HothSKMHSSBlobHandler::getBlobIds()
+{
+ std::vector<std::string> result;
+ for (auto& [basePath, hothId] : hothIdBasePaths)
+ {
+ if (!dbus_->pingHothd(hothId))
+ {
+ continue;
+ }
+
+ result.emplace_back(basePath);
+ for (size_t i = 0; i < slotPopulated[basePath].size(); ++i)
+ {
+ if (slotPopulated[basePath][i])
+ {
+ result.emplace_back(fmt::format("{}{}", basePath, i));
+ }
+ }
+ }
+ return result;
+}
+
+bool HothSKMHSSBlobHandler::stat(const std::string& path, blobs::BlobMeta* meta)
+{
+ std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+ auto base = internal::getBaseFromID(path);
+ if (!slotPtr)
+ {
+ return false;
+ }
+ *meta = {};
+ return slotPopulated[base][*slotPtr];
+}
+
+bool HothSKMHSSBlobHandler::open(uint16_t session, uint16_t flags,
+ const std::string& path)
+{
+ std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+ if (!slotPtr)
+ {
+ return false;
+ }
+ std::string_view hothId = getHothIdFromPath(path);
+ if (!HothBlobHandler::open(session, flags, hothIdToPath(hothId)))
+ {
+ return false;
+ }
+
+ std::string_view base = internal::getBaseFromID(path);
+ sessionSlot[base].emplace(session, *slotPtr);
+ HothBlob* blobIt = getSession(session);
+
+ if (!(blobIt->state & blobs::StateFlags::open_read))
+ {
+ return true;
+ }
+ auto finalState = blobIt->state;
+ blobIt->state = blobs::StateFlags::committing;
+ blobIt->outstanding = readSKMHSSSlot(
+ base, *slotPtr, hothId,
+ [blobIt, finalState](std::span<const uint8_t> data) noexcept {
+ auto outstanding = std::move(blobIt->outstanding);
+ if (data.size() == SKM_HSS_STRUCT_SIZE)
+ {
+ /* Set committed to denote that our buffer is consistent with
+ * the remote copy, making commits a no-op until a write changes
+ * it.
+ */
+ blobIt->state = finalState | blobs::StateFlags::committed;
+ blobIt->buffer = std::vector<uint8_t>(data.begin(), data.end());
+ return;
+ }
+ blobIt->state = blobs::StateFlags::commit_error;
+ if (data.data() != nullptr &&
+ finalState & blobs::StateFlags::open_write)
+ {
+ /* If the call succeeded, but we were uninitialized we want to
+ * allow a write to continue accessing the blob. This requires
+ * us to populate the open_* flags that were originally set on
+ * the blob. We don't do this for RO blobs since they can't
+ * fill in valid data to read back.
+ */
+ blobIt->state |= finalState;
+ }
+ });
+ return true;
+}
+
+bool HothSKMHSSBlobHandler::commit(uint16_t session,
+ const std::vector<uint8_t>& data)
+{
+ if (!data.empty())
+ {
+ log<level::ERR>("Unexpected data provided to commit call");
+ return false;
+ }
+
+ HothBlob* blobIt = getSession(session);
+ if (!blobIt)
+ {
+ return false;
+ }
+ if (blobIt->state &
+ (blobs::StateFlags::committed | blobs::StateFlags::committing))
+ {
+ return true;
+ }
+
+ std::string_view base = getBaseFromHothId(blobIt->hothId);
+ /* Clear remaining commit bits */
+ blobIt->state &= ~blobs::StateFlags::commit_error;
+ uint32_t slot = sessionSlot[base][session];
+ std::vector<uint8_t> req;
+ try
+ {
+ req = hothUtil_->writeSKMHSS(slot, blobIt->buffer);
+ }
+ catch (const CommandFailure& e)
+ {
+ blobIt->state |= blobs::StateFlags::commit_error;
+ log<level::ERR>("Hoth command failed", entry("ERR_MSG=%s", e.what()));
+ return false;
+ }
+
+ blobIt->state |= blobs::StateFlags::committing;
+ blobIt->outstanding = dbus_->SendHostCommand(
+ blobIt->hothId, req,
+ [this, base, slot,
+ blobIt](std::optional<std::vector<uint8_t>> rsp) noexcept {
+ auto outstanding = std::move(blobIt->outstanding);
+ blobIt->state &= ~blobs::StateFlags::committing;
+ try
+ {
+ hothUtil_->payloadECResponse(rsp.value());
+ blobIt->state |= blobs::StateFlags::committed;
+ slotPopulated[base][slot] = true;
+ return;
+ }
+ catch (const ResponseFailure& e)
+ {
+ log<level::ERR>("Invalid Hoth response",
+ entry("ERR_MSG=%s", e.what()));
+ }
+ catch (...)
+ {}
+ blobIt->state |= blobs::StateFlags::commit_error;
+ });
+ return true;
+}
+
+bool HothSKMHSSBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+ HothBlob* blobIt = getSession(session);
+ if (!blobIt)
+ {
+ return false;
+ }
+
+ meta->size = blobIt->buffer.size();
+ meta->blobState = blobIt->state;
+ return true;
+}
+
+bool HothSKMHSSBlobHandler::deleteBlob(const std::string& path)
+{
+ std::optional<uint32_t> slotPtr = internal::extractSlotFromID(path);
+ std::string_view hothId = getHothIdFromPath(path);
+ auto base = internal::getBaseFromID(path);
+ if (!slotPtr)
+ {
+ return false;
+ }
+ auto& del = dels[base][*slotPtr];
+ if (del)
+ {
+ return true;
+ }
+ auto& pop = slotPopulated[base][*slotPtr];
+ del = dbus_->SendHostCommand(
+ hothId, hothUtil_->deleteSKMHSS(*slotPtr),
+ [&pop, &del, this](std::optional<std::vector<uint8_t>> rsp) noexcept {
+ auto adel = std::move(del);
+ try
+ {
+ hothUtil_->payloadECResponse(rsp.value());
+ pop = false;
+ return;
+ }
+ catch (const ResponseFailure& e)
+ {
+ log<level::ERR>("Invalid Hoth response",
+ entry("ERR_MSG=%s", e.what()));
+ }
+ catch (...)
+ {}
+ });
+ return true;
+}
+
+bool HothSKMHSSBlobHandler::close(uint16_t session)
+{
+ HothBlob* blobIt = getSession(session);
+ if (!blobIt)
+ {
+ return false;
+ }
+
+ std::string_view base = getBaseFromHothId(blobIt->hothId);
+ if (HothBlobHandler::close(session))
+ {
+ sessionSlot[base].erase(session);
+ return true;
+ }
+ return false;
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_skmhss.hpp b/skmhss/hoth_skmhss.hpp
new file mode 100644
index 0000000..6fa84a0
--- /dev/null
+++ b/skmhss/hoth_skmhss.hpp
@@ -0,0 +1,127 @@
+// 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 "command/dbus_command.hpp"
+#include "hoth.hpp"
+#include "hoth_util.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+#include <boost/bimap.hpp>
+#include <function2/function2.hpp>
+
+#include <array>
+#include <span>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+class HothSKMHSSBlobHandler : public HothBlobHandler
+{
+ public:
+ explicit HothSKMHSSBlobHandler(internal::DbusCommand* dbus,
+ internal::HothUtil* hvnutil) :
+ dbus_(dbus), hothUtil_(hvnutil)
+ {
+ loadHSS();
+ }
+
+ /* Our callbacks require pinned memory */
+ HothSKMHSSBlobHandler(HothSKMHSSBlobHandler&&) = delete;
+ HothSKMHSSBlobHandler& operator=(HothSKMHSSBlobHandler&&) = delete;
+
+ bool canHandleBlob(const std::string& path) override;
+ std::vector<std::string> getBlobIds() override;
+ bool deleteBlob(const std::string& path) override;
+ bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+ bool open(uint16_t session, uint16_t flags, const std::string& path);
+ bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+ bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+ bool close(uint16_t session) override;
+
+ internal::Dbus& dbus()
+ {
+ return *dbus_;
+ }
+
+ std::string_view pathSuffix() const override
+ {
+ return "skm/hss";
+ }
+ uint16_t requiredFlags() const override
+ {
+ return 0;
+ }
+ uint16_t maxSessions() const override
+ {
+ /* We should allow a limited number of sessions per-blob */
+ return maxNumHSSSlot << 2;
+ }
+
+ /* the max buffer size is the constant size of HSS. */
+ uint32_t maxBufferSize() const override
+ {
+ return 64;
+ }
+
+ private:
+ /** @brief Reads the HSS slot and store the HSS data
+ * The return data can be null, meaning the request failed. It can also
+ * be an empty buffer, so the caller needs to check that it has enough
+ * bytes to be a valid HSS.
+ *
+ * @param[in] slot: the slot number in Hoth
+ * @param[in] cb: Run when the request completes with the HSS bytes
+ * @returns An object to cancel the request early.
+ */
+ stdplus::Cancel readSKMHSSSlot(
+ std::string_view base, uint32_t slot, std::string_view hothId,
+ fu2::unique_function<void(std::span<const uint8_t>)>&& cb);
+
+ /* Load HSS on Hoth storage through reading all slots. */
+ void loadHSS();
+
+ /* read the hothid_basepaths.config and store it in basePathMap*/
+ void readBasePathConfig();
+
+ /* get the hoth Id from the path from hothIdBasePaths map */
+ std::string_view getHothIdFromPath(std::string_view path);
+
+ /* get the base path from the hoth Id from hothIdBasePaths map */
+ std::string_view getBaseFromHothId(std::string_view hothId);
+
+ /* Number of HSS seeds stored in Hoth */
+ static constexpr uint32_t maxNumHSSSlot = 4;
+
+ std::unordered_map<std::string_view, std::unordered_map<uint16_t, uint32_t>>
+ sessionSlot;
+ std::unordered_map<std::string_view, std::array<bool, maxNumHSSSlot>>
+ slotPopulated;
+ std::unordered_map<std::string_view,
+ std::array<stdplus::Cancel, maxNumHSSSlot>>
+ inits, dels;
+
+ /* basePath <---> hothId mapping */
+ boost::bimap<std::string, std::string> hothIdBasePaths;
+
+ internal::DbusCommand* dbus_;
+ internal::HothUtil* hothUtil_;
+};
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_util.hpp b/skmhss/hoth_util.hpp
new file mode 100644
index 0000000..c8e9da7
--- /dev/null
+++ b/skmhss/hoth_util.hpp
@@ -0,0 +1,91 @@
+// 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 <stdplus/raw.hpp>
+
+#include <cstdint>
+#include <numeric>
+#include <span>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class HothUtil
+ * @brief An interface to Hoth SKM HSS operation
+ * @details Translate Hoth requests to bytes array commands and analyze
+ * Hoth responses.
+ */
+class HothUtil
+{
+ public:
+ virtual ~HothUtil() {};
+
+ /** @brief Calculates the checksum for an EC request with the given
+ * |contents|
+ * @param one or more spans (or vectors) of bytes
+ * @returns the checksum for a message with the given |contents|
+ */
+
+ template <typename... Ts>
+ static uint8_t calculateChecksum(Ts&&... ts)
+ {
+ return (std::accumulate(std::begin(ts), std::end(ts), 0) + ...);
+ }
+
+ /** @brief Wrap the EC SKM HSS request by serializing the concatenation of
+ * an EC request header and payload
+ * @param request: the EC SKM HSS request
+ * @returns the serialized EC request
+ */
+ virtual std::vector<uint8_t>
+ wrapECSKMHSSRequest(std::span<const uint8_t> request) = 0;
+
+ /** @brief Wrap the EC SKM HSS read request
+ * @param slot: the slot number in Hoth
+ * @returns the serialized EC request
+ */
+ virtual std::vector<uint8_t> readSKMHSS(uint32_t slot) = 0;
+
+ /** @brief Wrap the EC SKM HSS write request
+ * @param slot: the slot number in Hoth
+ * @data data: SKM HSS bytes
+ * @returns the serialized EC request
+ */
+ virtual std::vector<uint8_t> writeSKMHSS(uint32_t slot,
+ std::span<const uint8_t> data) = 0;
+
+ /** @brief Wrap the EC SKM HSS delete request
+ * @param slot: the slot number in Hoth
+ * @returns the serialized EC request
+ */
+ virtual std::vector<uint8_t> deleteSKMHSS(uint32_t slot) = 0;
+
+ /** @brief Validate the EC response and extract the payload by stripping off
+ * the header
+ * @param rsp: bytes array representing response received from EC
+ * @returns return the payload in an EC response iff the EC response is
+ * valid.
+ */
+ virtual std::span<const uint8_t>
+ payloadECResponse(std::span<const uint8_t> rsp) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_util_impl.cpp b/skmhss/hoth_util_impl.cpp
new file mode 100644
index 0000000..bdb3c2b
--- /dev/null
+++ b/skmhss/hoth_util_impl.cpp
@@ -0,0 +1,138 @@
+// 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 "hoth_util_impl.hpp"
+
+#include <google3/g_ec_commands.h>
+#include <google3/host_commands.h>
+
+#include <boost/endian/conversion.hpp>
+#include <phosphor-logging/log.hpp>
+#include <stdplus/raw.hpp>
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <span>
+#include <vector>
+
+using CommandFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::CommandFailure;
+using ResponseFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+using namespace phosphor::logging;
+
+std::vector<uint8_t>
+ HothUtilImpl::wrapECSKMHSSRequest(std::span<const uint8_t> request)
+{
+ // Populate EC request header based on the request
+ g_ec_host_request skmHssHeader = {
+ /*struct_version*/ G_EC_HOST_REQUEST_VERSION,
+ /*checksum*/ 0,
+ /*command*/ G_EC_PRIVATE_HOST_COMMAND_VALUE(EC_PRV_CMD_HOTH_SKM_HSS),
+ /*command_version*/ 0,
+ /*reserved*/ 0,
+ /*data_len*/ static_cast<uint16_t>(request.size())};
+
+ boost::endian::native_to_little_inplace(skmHssHeader);
+ skmHssHeader.checksum -=
+ calculateChecksum(stdplus::raw::asSpan<uint8_t>(skmHssHeader), request);
+
+ // Concatenate the request header and the request into one uint8_t vector to
+ // be sent to EC
+ const auto requestHeaderPtr = reinterpret_cast<uint8_t*>(&skmHssHeader);
+ std::vector<uint8_t> hothCommand(requestHeaderPtr,
+ requestHeaderPtr + sizeof(skmHssHeader));
+ hothCommand.insert(hothCommand.end(), request.begin(), request.end());
+ return hothCommand;
+}
+
+std::vector<uint8_t> HothUtilImpl::readSKMHSS(uint32_t slot)
+{
+ struct ec_request_skm_hss request = {};
+ request.operation = SKM_HSS_READ;
+ request.slot = slot;
+ return wrapECSKMHSSRequest(stdplus::raw::asSpan<const uint8_t>(request));
+}
+
+std::vector<uint8_t>
+ HothUtilImpl::writeSKMHSS(uint32_t slot, std::span<const uint8_t> data)
+{
+ if (data.empty() || data.size() > SKM_HSS_STRUCT_SIZE)
+ {
+ log<level::ERR>("Bad HSS payload size",
+ entry("REQ_SIZE=%lu SKM_HSS_SIZE=%lu",
+ static_cast<unsigned long>(data.size()),
+ static_cast<unsigned long>(SKM_HSS_STRUCT_SIZE)));
+ throw CommandFailure();
+ }
+ struct ec_request_skm_hss request = {};
+ request.operation = SKM_HSS_WRITE;
+ request.slot = slot;
+ std::memcpy(request.payload.data, data.data(), data.size());
+ return wrapECSKMHSSRequest(stdplus::raw::asSpan<const uint8_t>(request));
+}
+
+std::vector<uint8_t> HothUtilImpl::deleteSKMHSS(uint32_t slot)
+{
+ struct ec_request_skm_hss request = {};
+ request.operation = SKM_HSS_DELETE;
+ request.slot = slot;
+ return wrapECSKMHSSRequest(stdplus::raw::asSpan<const uint8_t>(request));
+}
+
+std::span<const uint8_t>
+ HothUtilImpl::payloadECResponse(std::span<const uint8_t> rsp)
+{
+ g_ec_host_response header{};
+
+ if (rsp.size() < sizeof(header))
+ {
+ log<level::ERR>("Response is too short",
+ entry("LENGTH=%d", rsp.size()));
+ throw ResponseFailure();
+ }
+
+ std::memcpy(&header, rsp.data(), sizeof(header));
+ boost::endian::native_to_little_inplace(header);
+
+ if (header.struct_version != G_EC_HOST_RESPONSE_VERSION)
+ {
+ log<level::ERR>(
+ "Unsupported stuct_version in response",
+ entry("VERSION=%d", static_cast<uint8_t>(header.struct_version)));
+ throw ResponseFailure();
+ }
+
+ if (header.result != EC_RES_SUCCESS && header.result != EC_RES_UNAVAILABLE)
+ {
+ log<level::ERR>(
+ "Unexpected return code",
+ entry("RESULT=%d", static_cast<uint16_t>(header.result)));
+ throw ResponseFailure();
+ }
+
+ return rsp.last(rsp.size() - sizeof(header));
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/skmhss/hoth_util_impl.hpp b/skmhss/hoth_util_impl.hpp
new file mode 100644
index 0000000..96a1876
--- /dev/null
+++ b/skmhss/hoth_util_impl.hpp
@@ -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.
+
+#pragma once
+
+#include "hoth_util.hpp"
+
+#include <stdplus/raw.hpp>
+
+#include <cstdint>
+#include <span>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/* Host command response codes (16-bit). Note that response codes should be
+ * stored in a uint16_t rather than directly in a value of this type.
+ */
+enum ec_status
+{
+ EC_RES_SUCCESS = 0,
+ EC_RES_INVALID_COMMAND = 1,
+ EC_RES_ERROR = 2,
+ EC_RES_INVALID_PARAM = 3,
+ EC_RES_ACCESS_DENIED = 4,
+ EC_RES_INVALID_RESPONSE = 5,
+ EC_RES_INVALID_VERSION = 6,
+ EC_RES_INVALID_CHECKSUM = 7,
+ EC_RES_IN_PROGRESS = 8, /* Accepted, command in progress */
+ EC_RES_UNAVAILABLE = 9, /* No response available */
+ EC_RES_TIMEOUT = 10, /* We got a timeout */
+ EC_RES_OVERFLOW = 11, /* Table / data overflow */
+ EC_RES_INVALID_HEADER = 12, /* Header contains invalid data */
+ EC_RES_REQUEST_TRUNCATED = 13, /* Didn't get the entire request */
+ EC_RES_RESPONSE_TOO_BIG = 14, /* Response was too big to handle */
+ EC_RES_BUS_ERROR = 15, /* Communications bus error */
+ EC_RES_BUSY = 16, /* Up but too busy. Should retry */
+ EC_RES_INVALID_HEADER_VERSION = 17, /* Header version invalid */
+ EC_RES_INVALID_HEADER_CRC = 18, /* Header CRC invalid */
+ EC_RES_INVALID_DATA_CRC = 19, /* Data CRC invalid */
+ EC_RES_DUP_UNAVAILABLE = 20, /* Can't resend response */
+};
+
+/** @class HothUtilImpl
+ * @brief Implementation of HothUtil interface
+ */
+class HothUtilImpl : public HothUtil
+{
+ public:
+ std::vector<uint8_t>
+ wrapECSKMHSSRequest(std::span<const uint8_t> request) override;
+ std::vector<uint8_t> readSKMHSS(uint32_t slot) override;
+ std::vector<uint8_t> writeSKMHSS(uint32_t slot,
+ std::span<const uint8_t> data) override;
+ std::vector<uint8_t> deleteSKMHSS(uint32_t slot) override;
+ std::span<const uint8_t>
+ payloadECResponse(std::span<const uint8_t> rsp) override;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/skmhss/main_skmhss.cpp b/skmhss/main_skmhss.cpp
new file mode 100644
index 0000000..48bd97b
--- /dev/null
+++ b/skmhss/main_skmhss.cpp
@@ -0,0 +1,31 @@
+// 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 "command/dbus_command_impl.hpp"
+#include "hoth_skmhss.hpp"
+#include "hoth_util_impl.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+ /** @brief Default instantiation of Dbus and HothUtil */
+ static ipmi_hoth::internal::DbusCommandImpl dbusCommand_impl;
+ static ipmi_hoth::internal::HothUtilImpl hothUtil_impl;
+
+ return std::make_unique<ipmi_hoth::HothSKMHSSBlobHandler>(
+ &dbusCommand_impl, &hothUtil_impl);
+}
diff --git a/skmhss/meson.build b/skmhss/meson.build
new file mode 100644
index 0000000..7a713a5
--- /dev/null
+++ b/skmhss/meson.build
@@ -0,0 +1,45 @@
+meson.get_compiler('cpp').has_header_symbol(
+ 'boost/endian/conversion.hpp',
+ 'boost::endian::native_to_little_inplace')
+
+conf_data = configuration_data()
+conf_data.set_quoted('HOTHID_CONFIG', get_option('skmhss-hothid-config'))
+conf_hpp = configure_file(
+ output: 'config.hpp',
+ configuration: conf_data)
+
+hothskmhss_pre = declare_dependency(
+ include_directories: include_directories('.'),
+ dependencies: [
+ hothblob_dep,
+ hothcommand_dep,
+ dependency('hothd-dbus'),
+ ipmi_blob_dep,
+ phosphor_logging_dep,
+ sdbusplus_dep,
+ stdplus_dep,
+ ])
+
+hothskmhss_lib = static_library(
+ 'hothskmhss',
+ 'hoth_skmhss.cpp',
+ 'hoth_util_impl.cpp',
+ conf_hpp,
+ implicit_include_directories: false,
+ dependencies: hothskmhss_pre)
+
+hothskmhss_dep = declare_dependency(
+ link_with: hothskmhss_lib,
+ dependencies: hothskmhss_pre)
+
+shared_module(
+ 'hothskmhss',
+ 'main_skmhss.cpp',
+ implicit_include_directories: false,
+ dependencies: hothskmhss_dep,
+ install: true,
+ install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+ subdir('test')
+endif
diff --git a/skmhss/test/hoth_skmhss_close_unittest.cpp b/skmhss/test/hoth_skmhss_close_unittest.cpp
new file mode 100644
index 0000000..924a51c
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_close_unittest.cpp
@@ -0,0 +1,50 @@
+// 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HothSKMHSSCloseTest : public HothSKMHSSBasicTest
+{};
+
+TEST_F(HothSKMHSSCloseTest, CloseWithInvalidSessionFails)
+{
+ // Verify you cannot close an invalid session.
+
+ EXPECT_FALSE(hvn->close(session));
+}
+
+TEST_F(HothSKMHSSCloseTest, CloseWithValidSessionSuccess)
+{
+ // Verify you can close a valid session.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::write, legacyPath[2]));
+ EXPECT_TRUE(hvn->close(session));
+ EXPECT_FALSE(hvn->close(session));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_commit_unittest.cpp b/skmhss/test/hoth_skmhss_commit_unittest.cpp
new file mode 100644
index 0000000..a2aa971
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_commit_unittest.cpp
@@ -0,0 +1,120 @@
+// 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothSKMHSSCommitTest : public HothSKMHSSBasicTest
+{
+ protected:
+ void openAndWriteRandomHss(int idx)
+ {
+ std::vector<uint8_t> data(64);
+ std::srand(1);
+ std::generate(data.begin(), data.end(), std::rand);
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillOnce(Return(true));
+ fu2::unique_function<void()> cb;
+ testing::StrictMock<MockCancel> cancel;
+ expectRead(idx, cb, cancel, data);
+ EXPECT_TRUE(
+ hvn->open(session, blobs::OpenFlags::read | blobs::OpenFlags::write,
+ legacyPath[idx]));
+ cb();
+ }
+};
+
+TEST_F(HothSKMHSSCommitTest, InvalidSessionCommitIsRejected)
+{
+ // Verify the hoth command handler checks for a valid session.
+
+ EXPECT_FALSE(hvn->commit(session, std::vector<uint8_t>()));
+}
+
+TEST_F(HothSKMHSSCommitTest, CommitWithUnexpectedDataParamReturnsFalse)
+{
+ openAndWriteRandomHss(0);
+ EXPECT_FALSE(hvn->commit(session, std::vector<uint8_t>({1, 2, 3})));
+}
+
+TEST_F(HothSKMHSSCommitTest, MultiCommit)
+{
+ openAndWriteRandomHss(1);
+
+ // No change does nothing
+ EXPECT_TRUE(hvn->commit(session, {}));
+
+ EXPECT_TRUE(hvn->write(session, 0, {0}));
+
+ // Test an error case
+ EXPECT_CALL(hvnutil, writeSKMHSS(1, _))
+ .WillOnce(Return(std::vector<uint8_t>{11}));
+ fu2::unique_function<void()> cb;
+ testing::StrictMock<MockCancel> cancel;
+ expectCmd(/*slot=*/1, cb, cancel, /*data=*/{});
+ EXPECT_TRUE(hvn->commit(session, {}));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_write |
+ blobs::StateFlags::open_read,
+ meta.blobState);
+
+ cb();
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(blobs::StateFlags::commit_error | blobs::StateFlags::open_write |
+ blobs::StateFlags::open_read,
+ meta.blobState);
+
+ // Test repeat commit translates into success
+ EXPECT_CALL(hvnutil, writeSKMHSS(1, _))
+ .WillOnce(Return(std::vector<uint8_t>{11}));
+ std::array<uint8_t, 1> arr;
+ expectCmd(/*slot=*/1, cb, cancel,
+ /*data=*/std::span<uint8_t>(arr).subspan(0, 0));
+ EXPECT_TRUE(hvn->commit(session, {}));
+
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(blobs::StateFlags::committing | blobs::StateFlags::open_write |
+ blobs::StateFlags::open_read,
+ meta.blobState);
+
+ cb();
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_write |
+ blobs::StateFlags::open_read,
+ meta.blobState);
+
+ // No change does nothing
+ EXPECT_TRUE(hvn->commit(session, {}));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_delete_unittest.cpp b/skmhss/test/hoth_skmhss_delete_unittest.cpp
new file mode 100644
index 0000000..a3518bf
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_delete_unittest.cpp
@@ -0,0 +1,107 @@
+// 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothSKMHSSDeleteTest : public HothSKMHSSBasicTest
+{};
+
+TEST_F(HothSKMHSSDeleteTest, DeleteWithNoHothd)
+{
+ DbusCommand::Cb cb;
+ testing::StrictMock<MockCancel> cancel;
+ EXPECT_CALL(hvnutil, deleteSKMHSS).WillOnce(Return(std::vector<uint8_t>()));
+ EXPECT_CALL(dbus, SendHostCommand("", testing::ElementsAre(), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&,
+ DbusCommand::Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(&cancel);
+ });
+ EXPECT_TRUE(hvn->deleteBlob("/skm/hss-backup/0"));
+ // Delete while delete in progress should not trigger a new delete
+ EXPECT_TRUE(hvn->deleteBlob("/skm/hss-backup/0"));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat("/skm/hss-backup/0", &meta));
+ EXPECT_CALL(cancel, cancel());
+ cb(std::nullopt);
+ EXPECT_TRUE(hvn->stat("/skm/hss-backup/0", &meta));
+}
+
+TEST_F(HothSKMHSSDeleteTest, DeleteSlotSucceeds)
+{
+ // Also check the cached blob list is updated.
+ std::vector<std::string> expected = {
+ "/skm/hss-backup/", "/skm/hss-backup/0", "/skm/hss-backup/1",
+ "/skm/hss-backup/2", "/skm/hss-backup/3",
+ };
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+ EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat("/skm/hss-backup/0", &meta));
+
+ DbusCommand::Cb cb;
+ testing::StrictMock<MockCancel> cancel;
+ EXPECT_CALL(hvnutil, deleteSKMHSS).WillOnce(Return(std::vector<uint8_t>()));
+ EXPECT_CALL(dbus, SendHostCommand("", testing::ElementsAre(), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&,
+ DbusCommand::Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(&cancel);
+ });
+ EXPECT_CALL(hvnutil, payloadECResponse(_))
+ .WillOnce(Return(std::vector<uint8_t>()));
+
+ EXPECT_TRUE(hvn->deleteBlob(legacyPath[3]));
+ EXPECT_TRUE(hvn->stat("/skm/hss-backup/3", &meta));
+ EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+ EXPECT_CALL(cancel, cancel());
+ cb(std::vector<uint8_t>());
+ expected.pop_back();
+ EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+ EXPECT_FALSE(hvn->stat("/skm/hss-backup/3", &meta));
+
+ // Deleting a nonexistent slot will fall through to hoth
+ EXPECT_CALL(hvnutil, deleteSKMHSS).WillOnce(Return(std::vector<uint8_t>()));
+ EXPECT_CALL(dbus, SendHostCommand("", testing::ElementsAre(), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&,
+ DbusCommand::Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(&cancel);
+ });
+ EXPECT_TRUE(hvn->deleteBlob(legacyPath[3]));
+ EXPECT_CALL(cancel, cancel());
+ cb(std::nullopt);
+
+ // Opening deleted slot succeeds
+ EXPECT_CALL(dbus, pingHothd("")).WillOnce(Return(true));
+ EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::write, legacyPath[3]));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_open_unittest.cpp b/skmhss/test/hoth_skmhss_open_unittest.cpp
new file mode 100644
index 0000000..033492f
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_open_unittest.cpp
@@ -0,0 +1,121 @@
+// 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <span>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HothSKMHSSOpenTest : public HothSKMHSSBasicTest
+{};
+
+TEST_F(HothSKMHSSOpenTest, OpenWithNoHothd)
+{
+ /* Hoth SKM HSS handler opens without a backing hoth daemon present. */
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(false));
+ EXPECT_FALSE(hvn->open(session, hvn->requiredFlags(), legacyPath[1]));
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenRWUninitializedBlobSucceeds)
+{
+ /* Hoth SKM HSS handler opens an uninitialized path. */
+ hvn = createHandlerWithEmptyCache();
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ fu2::unique_function<void()> cb;
+ testing::StrictMock<MockCancel> cancel;
+ std::array<uint8_t, 1> rspdata;
+ expectRead(0, cb, cancel, std::span<uint8_t>(rspdata).subspan(0, 0));
+ EXPECT_TRUE(
+ hvn->open(session, blobs::OpenFlags::read | blobs::OpenFlags::write,
+ legacyPath[0]));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.blobState, blobs::StateFlags::committing);
+ cb();
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_read | blobs::StateFlags::open_write |
+ blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenROUninitializedBlobFails)
+{
+ /* Hoth SKM HSS handler opens an uninitialized path. */
+ hvn = createHandlerWithEmptyCache();
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ fu2::unique_function<void()> cb;
+ testing::StrictMock<MockCancel> cancel;
+ std::array<uint8_t, 1> rspdata;
+ expectRead(0, cb, cancel, std::span<uint8_t>(rspdata).subspan(0, 0));
+ EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::read, legacyPath[0]));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.blobState, blobs::StateFlags::committing);
+ cb();
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.blobState, blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenBlobWithSdBusErrorFails)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ fu2::unique_function<void()> cb;
+ testing::StrictMock<MockCancel> cancel;
+ expectRead(0, cb, cancel, {});
+ EXPECT_TRUE(
+ hvn->open(session, blobs::OpenFlags::read | blobs::OpenFlags::write,
+ legacyPath[0]));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.blobState, blobs::StateFlags::committing);
+ cb();
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.blobState, blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothSKMHSSOpenTest, OpenOverloadedSessionFails)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+ uint16_t sessId = 0;
+ int low = std::min((uint16_t)legacyPath.size(), hvn->maxSessions());
+
+ for (int i = 0; i < low; i++)
+ {
+ EXPECT_TRUE(
+ hvn->open(sessId++, blobs::OpenFlags::write, legacyPath[i]));
+ }
+
+ for (int i = low; i < hvn->maxSessions(); i++)
+ {
+ EXPECT_TRUE(
+ hvn->open(sessId++, blobs::OpenFlags::write, legacyPath[0]));
+ }
+
+ EXPECT_FALSE(hvn->open(sessId++, blobs::OpenFlags::write, legacyPath[0]));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_read_unittest.cpp b/skmhss/test/hoth_skmhss_read_unittest.cpp
new file mode 100644
index 0000000..c47eef9
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_read_unittest.cpp
@@ -0,0 +1,154 @@
+// 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+#include <ipmid/handler.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Return;
+
+class HothSKMHSSReadTest : public HothSKMHSSBasicTest
+{
+ protected:
+ const uint32_t testOffset = 0;
+ const std::vector<uint8_t> testData = [] {
+ std::vector<uint8_t> ret;
+ for (uint8_t i = 0; i < 64; ++i)
+ ret.push_back(i);
+ return ret;
+ }();
+
+ void openUninitializedBlob(int idx)
+ {
+ // Any valid blobs have uninitialized buffer.
+ hvn = createHandlerWithEmptyCache();
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillOnce(Return(true));
+ fu2::unique_function<void()> cb;
+ testing::StrictMock<MockCancel> cancel;
+ expectRead(idx, cb, cancel, testData);
+ EXPECT_TRUE(
+ hvn->open(session, blobs::OpenFlags::read, legacyPath[idx]));
+ cb();
+ }
+};
+
+TEST_F(HothSKMHSSReadTest, InvalidSessionReadIsRejected)
+{
+ // Verify that read checks for a valid session, returns empty buffer for
+ // failed check.
+
+ openUninitializedBlob(0);
+ uint16_t wrongSession = session + 1;
+ EXPECT_THROW(hvn->read(wrongSession, testOffset, testData.size()),
+ ipmi::HandlerCompletion);
+}
+
+TEST_F(HothSKMHSSReadTest, ReadOpenBlobWithoutReadFlagReturnsError)
+{
+ // Verify that read checks for a valid state with open_read flag set.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn->open(session, 0, legacyPath[1]));
+ EXPECT_THROW(hvn->read(session, testOffset, testData.size()),
+ ipmi::HandlerCompletion);
+}
+
+TEST_F(HothSKMHSSReadTest, ReadOffsetBeyondBufferSizeReturnsEmpty)
+{
+ // Verify that read with offset beyond buffer size returns empty buffer.
+
+ openUninitializedBlob(2);
+ uint32_t offsetBeyondBuffer = testData.size();
+ EXPECT_THAT(hvn->read(session, offsetBeyondBuffer, testData.size()),
+ IsEmpty());
+}
+
+TEST_F(HothSKMHSSReadTest, ReadFullWrittenData)
+{
+ // Verify that the read successfully reads back the written data.
+
+ openUninitializedBlob(3);
+ EXPECT_THAT(hvn->read(session, testOffset, testData.size()),
+ ContainerEq(testData));
+}
+
+TEST_F(HothSKMHSSReadTest, ReadWrittenDataAtOffset)
+{
+ // Verify that the read with offset returns the expected data.
+
+ openUninitializedBlob(0);
+ EXPECT_THAT(hvn->read(session, testOffset, testData.size()),
+ ContainerEq(testData));
+
+ // Try reading the written data byte by byte at each offset
+ for (size_t i = 0; i < testData.size(); ++i)
+ {
+ EXPECT_THAT(hvn->read(session, i, 1), ElementsAre(testData[i]));
+ }
+}
+
+TEST_F(HothSKMHSSReadTest, ReadFullWrittenDataWithBiggerRequestedSize)
+{
+ // Verify that read with requested size bigger than the written data will
+ // return a response buffer up to the end of the written buffer.
+
+ openUninitializedBlob(0);
+ uint32_t requestedSizeBeyondBuffer = testData.size() + 1;
+ EXPECT_THAT(hvn->read(session, testOffset, requestedSizeBeyondBuffer),
+ ContainerEq(testData));
+}
+
+TEST_F(HothSKMHSSReadTest, ReadWrittenDataAtOffsetWithBiggerRequestedSize)
+{
+ // Verify that read with requested size bigger than the written data at an
+ // offset will return a response buffer up to the end of the written buffer.
+
+ openUninitializedBlob(1);
+ uint32_t newOffset = testData.size() / 2;
+ uint32_t requestedSizeBeyondBuffer = testData.size() + 1;
+ std::vector<uint8_t> expectedData =
+ std::vector<uint8_t>(testData.begin() + newOffset, testData.end());
+
+ EXPECT_THAT(hvn->read(session, newOffset, requestedSizeBeyondBuffer),
+ ContainerEq(expectedData));
+}
+
+TEST_F(HothSKMHSSReadTest, ReadFullWrittenDataBeyondMaxBufferSize)
+{
+ // Verify that read with requested size bigger than the maximum buffer size
+ // will return a response buffer up to the end of the written buffer.
+
+ openUninitializedBlob(2);
+ uint32_t requestedSize = hvn->maxBufferSize() + 1;
+ EXPECT_THAT(hvn->read(session, testOffset, requestedSize),
+ ContainerEq(testData));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_unittest.cpp b/skmhss/test/hoth_skmhss_unittest.cpp
new file mode 100644
index 0000000..4784603
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_unittest.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 "hoth_skmhss_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+TEST_F(HothSKMHSSBasicTest, HothIdMainOnly)
+{
+ EXPECT_EQ("", hvn->pathToHothId(hvn->hothIdToPath("")));
+}
+
+TEST_F(HothSKMHSSBasicTest, CanHandleBlobChecksNameInvalid)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+ EXPECT_FALSE(hvn->canHandleBlob("asdf"));
+ EXPECT_FALSE(hvn->canHandleBlob("/skm/hss-backup"));
+ EXPECT_FALSE(hvn->canHandleBlob("/skm/hss-backup/4"));
+ EXPECT_FALSE(hvn->canHandleBlob("/skm/hss-backup/15"));
+ EXPECT_FALSE(hvn->canHandleBlob("/dev/hoth/command_passthru"));
+}
+
+TEST_F(HothSKMHSSBasicTest, CanHandleBlobChecksNameValid)
+{
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+ EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/0"));
+ EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/1"));
+ EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/2"));
+ EXPECT_TRUE(hvn->canHandleBlob("/skm/hss-backup/3"));
+}
+
+TEST_F(HothSKMHSSBasicTest, GetBlobIdsAsHssUnintialized)
+{
+ hvn = createHandlerWithEmptyCache();
+ std::vector<std::string> expected = {"/skm/hss-backup/"};
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_THAT(hvn->getBlobIds(), ContainerEq(expected));
+}
+
+TEST_F(HothSKMHSSBasicTest, GetBlobIdsAsHssCommited)
+{
+ std::vector<std::string> expected = {
+ "/skm/hss-backup/", "/skm/hss-backup/0", "/skm/hss-backup/1",
+ "/skm/hss-backup/2", "/skm/hss-backup/3",
+ };
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+ EXPECT_THAT(hvn->getBlobIds(), UnorderedElementsAreArray(expected));
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_unittest.hpp b/skmhss/test/hoth_skmhss_unittest.hpp
new file mode 100644
index 0000000..0973205
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_unittest.hpp
@@ -0,0 +1,154 @@
+// 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 "command/test/dbus_command_mock.hpp"
+#include "hoth_skmhss.hpp"
+#include "hoth_util_mock.hpp"
+
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using ipmi_hoth::internal::DbusCommand;
+using ::testing::_;
+using ::testing::NotNull;
+using ::testing::Return;
+using ResponseFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+
+namespace ipmi_hoth
+{
+
+struct MockCancel : public stdplus::Cancelable
+{
+ MOCK_METHOD(void, cancel, (), (noexcept, override));
+};
+
+class HothSKMHSSTest : public ::testing::Test
+{
+ public:
+ void expectCmd(uint8_t i, fu2::unique_function<void()>& cb,
+ MockCancel& cancel, std::span<const uint8_t> data)
+ {
+ bool has_data = data.data() != nullptr;
+ EXPECT_CALL(dbus,
+ SendHostCommand(
+ "", testing::ElementsAre(static_cast<uint8_t>(10 + i)),
+ _))
+ .WillOnce([&cb, &cancel, i,
+ has_data](std::string_view, const std::vector<uint8_t>&,
+ DbusCommand::Cb&& icb) {
+ if (has_data)
+ {
+ cb = [icb = std::move(icb), i]() mutable {
+ icb(std::vector<uint8_t>{static_cast<uint8_t>(20 + i)});
+ };
+ }
+ else
+ {
+ cb = [icb = std::move(icb)]() mutable {
+ icb(std::nullopt);
+ };
+ }
+ return stdplus::Cancel(&cancel);
+ });
+ if (has_data)
+ {
+ EXPECT_CALL(hvnutil, payloadECResponse(testing::ElementsAre(
+ static_cast<uint8_t>(20 + i))))
+ .WillOnce(Return(data));
+ }
+ EXPECT_CALL(cancel, cancel()).WillOnce(Return());
+ }
+
+ void expectRead(uint8_t i, fu2::unique_function<void()>& cb,
+ MockCancel& cancel, std::span<const uint8_t> data)
+ {
+ EXPECT_CALL(hvnutil, readSKMHSS(i))
+ .WillOnce(
+ Return(std::vector<uint8_t>{static_cast<uint8_t>(10 + i)}));
+ expectCmd(i, cb, cancel, data);
+ }
+
+ std::unique_ptr<HothSKMHSSBlobHandler>
+ createHandler(const std::vector<uint8_t>& data)
+ {
+ std::array<fu2::unique_function<void()>, 4> cbs;
+ std::array<testing::StrictMock<MockCancel>, 4> cancels;
+ for (uint8_t i = 0; i < 4; ++i)
+ {
+ expectRead(i, cbs[i], cancels[i], data);
+ }
+ auto ret = std::make_unique<HothSKMHSSBlobHandler>(&dbus, &hvnutil);
+ for (uint8_t i = 0; i < 4; ++i)
+ {
+ cbs[i]();
+ }
+ testing::Mock::VerifyAndClearExpectations(&hvnutil);
+ testing::Mock::VerifyAndClearExpectations(&dbus);
+ return ret;
+ }
+
+ // Create a handler with empty HSS cache.
+ std::unique_ptr<HothSKMHSSBlobHandler> createHandlerWithEmptyCache()
+ {
+ return createHandler({});
+ }
+
+ // Create a handler with test HSS cache.
+ std::unique_ptr<HothSKMHSSBlobHandler> createHandlerWithTestCache()
+ {
+ return createHandler(testHSSBytes);
+ }
+
+ protected:
+ // dbus mock object
+ testing::StrictMock<internal::DbusCommandMock> dbus;
+
+ // Hoth utility mock object
+ testing::StrictMock<internal::HothUtilMock> hvnutil;
+
+ const uint16_t session = 0;
+ const std::vector<std::string> legacyPath = {
+ "/skm/hss-backup/0",
+ "/skm/hss-backup/1",
+ "/skm/hss-backup/2",
+ "/skm/hss-backup/3",
+ };
+ const std::vector<uint8_t> testHSSBytes = std::vector<uint8_t>(64, 0x0b);
+};
+
+class HothSKMHSSBasicTest : public HothSKMHSSTest
+{
+ public:
+ HothSKMHSSBasicTest() :
+ hvn(HothSKMHSSBasicTest::createHandlerWithTestCache())
+ {}
+ std::unique_ptr<HothSKMHSSBlobHandler> hvn;
+};
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_skmhss_write_unittest.cpp b/skmhss/test/hoth_skmhss_write_unittest.cpp
new file mode 100644
index 0000000..7098550
--- /dev/null
+++ b/skmhss/test/hoth_skmhss_write_unittest.cpp
@@ -0,0 +1,119 @@
+// 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 "hoth_skmhss_unittest.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HothSKMHSSWriteTest : public HothSKMHSSBasicTest
+{
+ protected:
+ void openWriteCachedBlob(int)
+ {
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillOnce(Return(true));
+ EXPECT_TRUE(hvn->open(session, blobs::OpenFlags::write, legacyPath[1]));
+ }
+};
+
+TEST_F(HothSKMHSSWriteTest, InvalidSessionWriteIsRejected)
+{
+ // Verify the hoth command handler checks for a valid session.
+
+ std::vector<uint8_t> data = {0x1, 0x2};
+
+ EXPECT_FALSE(hvn->write(session, 0, data));
+ blobs::BlobMeta meta;
+ EXPECT_FALSE(hvn->stat(session, &meta));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingTooMuchByOneByteFails)
+{
+ // Test the edge case of writing 1 byte too much with an offset of 0.
+ // writing 65 at offset 0 is invalid.
+
+ openWriteCachedBlob(0);
+ std::vector<uint8_t> data(hvn->maxBufferSize() + 1, 0x11);
+ EXPECT_FALSE(hvn->write(session, 0, data));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.size, 0);
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingTooMuchByOffsetOfOneFails)
+{
+ // Test the edge case of writing 64 bytes (which is fine) but at the
+ // offset 1, which makes it go over by 1 byte. Writing 64 at offset 1 is
+ // invalid.
+
+ openWriteCachedBlob(1);
+ std::vector<uint8_t> data(hvn->maxBufferSize(), 0x11);
+ EXPECT_FALSE(hvn->write(session, 1, data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+ // Test the edge case of writing to the last byte offset but trying to
+ // write two bytes. Writing 2 bytes at 63 is invalid.
+
+ openWriteCachedBlob(2);
+ std::vector<uint8_t> data = {0x01, 0x02};
+ EXPECT_FALSE(hvn->write(session, (hvn->maxBufferSize() - 1), data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+ // Test the edge case of writing one byte but exactly one byte beyond the
+ // buffer. Writing 1 byte at 64 is invalid.
+
+ openWriteCachedBlob(3);
+ std::vector<uint8_t> data = {0x01};
+ EXPECT_FALSE(hvn->write(session, hvn->maxBufferSize(), data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+ // Test the case where you write the full buffer length at once to the 0th
+ // offset.
+
+ openWriteCachedBlob(0);
+ std::vector<uint8_t> data(hvn->maxBufferSize(), 0x11);
+ EXPECT_TRUE(hvn->write(session, 0, data));
+}
+
+TEST_F(HothSKMHSSWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+ // Test the case where you write the last byte.
+
+ openWriteCachedBlob(0);
+ std::vector<uint8_t> data = {0x01};
+ EXPECT_TRUE(hvn->write(session, (hvn->maxBufferSize() - 1), data));
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn->stat(session, &meta));
+ EXPECT_EQ(meta.size, 64);
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_util_mock.hpp b/skmhss/test/hoth_util_mock.hpp
new file mode 100644
index 0000000..61334df
--- /dev/null
+++ b/skmhss/test/hoth_util_mock.hpp
@@ -0,0 +1,47 @@
+// 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 "hoth_util.hpp"
+
+#include <stdplus/raw.hpp>
+
+#include <cstdint>
+#include <span>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class HothUtilMock : public HothUtil
+{
+ public:
+ MOCK_METHOD1(wrapECSKMHSSRequest,
+ std::vector<uint8_t>(std::span<const uint8_t> request));
+ MOCK_METHOD1(readSKMHSS, std::vector<uint8_t>(uint32_t slot));
+ MOCK_METHOD2(writeSKMHSS,
+ std::vector<uint8_t>(uint32_t slot,
+ std::span<const uint8_t> data));
+ MOCK_METHOD1(deleteSKMHSS, std::vector<uint8_t>(uint32_t slot));
+ MOCK_METHOD1(payloadECResponse,
+ std::span<const uint8_t>(std::span<const uint8_t> rsp));
+};
+
+} // namespace internal
+} // namespace ipmi_hoth
diff --git a/skmhss/test/hoth_util_unittest.cpp b/skmhss/test/hoth_util_unittest.cpp
new file mode 100644
index 0000000..0b71526
--- /dev/null
+++ b/skmhss/test/hoth_util_unittest.cpp
@@ -0,0 +1,173 @@
+// 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 "dbus_mock.hpp"
+#include "hoth_util.hpp"
+#include "hoth_util_impl.hpp"
+
+#include <xyz/openbmc_project/Control/Hoth/error.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <numeric>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace ::testing;
+using CommandFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::CommandFailure;
+using ResponseFailure =
+ sdbusplus::xyz::openbmc_project::Control::Hoth::Error::ResponseFailure;
+
+namespace ipmi_hoth
+{
+
+using namespace internal;
+
+class HothUtilTest : public ::testing::Test
+{
+ protected:
+ HothUtilTest() : hothUtil(std::make_unique<HothUtilImpl>(HothUtilImpl())) {}
+
+ std::unique_ptr<HothUtil> hothUtil;
+};
+
+TEST_F(HothUtilTest, Checksum)
+{
+ uint8_t header[] = {1, 3, 5, 7};
+ uint8_t body[] = {2, 4, 6, 8};
+
+ // (1 + 3 + 5 + 7) + (2 + 4 + 6 + 8) = 36
+ EXPECT_EQ(36, HothUtil::calculateChecksum(header, body));
+
+ // Now test what happens if the sum exceeds 256.
+ body[0] = 200;
+ body[1] = 202;
+
+ // (1 + 3 + 5 + 7) + (200 + 202 + 6 + 8) = 432
+ // 432 % 256 = 176
+ EXPECT_EQ(176, HothUtil::calculateChecksum(header, body));
+}
+
+TEST_F(HothUtilTest, WrapECSKMHSSRequest)
+{
+ std::vector<uint8_t> request(15);
+ std::generate(request.begin(), request.end(), std::rand);
+ std::vector<uint8_t> reqBytes;
+ reqBytes = std::move(hothUtil->wrapECSKMHSSRequest(request));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+}
+
+TEST_F(HothUtilTest, ReadSKMHSS)
+{
+ std::vector<uint8_t> reqBytes;
+ reqBytes = std::move(hothUtil->readSKMHSS(0));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ reqBytes = std::move(hothUtil->readSKMHSS(1));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ reqBytes = std::move(hothUtil->readSKMHSS(2));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ reqBytes = std::move(hothUtil->readSKMHSS(3));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+}
+
+TEST_F(HothUtilTest, WriteSKMHSS)
+{
+ std::vector<uint8_t> payload(128);
+ std::vector<uint8_t> reqBytes;
+ std::generate(payload.begin(), payload.end(), std::rand);
+ EXPECT_THROW(hothUtil->writeSKMHSS(0, payload), CommandFailure);
+ EXPECT_TRUE(reqBytes.empty());
+
+ payload.resize(64);
+ reqBytes = std::move(hothUtil->writeSKMHSS(1, payload));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ payload = {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,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 1, 110, 205, 147, 54, 43, 0, 0, 1, 1, 175, 58, 9, 239};
+ EXPECT_NO_THROW(hothUtil->writeSKMHSS(0, payload));
+ EXPECT_NO_THROW(hothUtil->writeSKMHSS(1, payload));
+ EXPECT_NO_THROW(hothUtil->writeSKMHSS(2, payload));
+ EXPECT_NO_THROW(hothUtil->writeSKMHSS(3, payload));
+}
+
+TEST_F(HothUtilTest, DeleteSKMHSS)
+{
+ std::vector<uint8_t> reqBytes;
+ reqBytes = std::move(hothUtil->deleteSKMHSS(0));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ reqBytes = std::move(hothUtil->deleteSKMHSS(1));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ reqBytes = std::move(hothUtil->deleteSKMHSS(2));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+
+ reqBytes = std::move(hothUtil->deleteSKMHSS(3));
+ EXPECT_FALSE(reqBytes.empty());
+ EXPECT_EQ(0, HothUtil::calculateChecksum(reqBytes));
+}
+
+TEST_F(HothUtilTest, PayloadECResponse)
+{
+ std::vector<uint8_t> validRsp = {0x03, 0xfd, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00};
+ EXPECT_NO_THROW(hothUtil->payloadECResponse(validRsp));
+
+ std::vector<uint8_t> rspShortLength = {0x03, 0xfd};
+ EXPECT_THROW(hothUtil->payloadECResponse(rspShortLength), ResponseFailure);
+
+ std::vector<uint8_t> rspVersionErr = {0x05, 0xfb, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00};
+ EXPECT_THROW(hothUtil->payloadECResponse(rspVersionErr), ResponseFailure);
+
+ std::vector<uint8_t> rspNonZeroRC = {0x03, 0xfc, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00};
+ EXPECT_THROW(hothUtil->payloadECResponse(rspNonZeroRC), ResponseFailure);
+
+ std::vector<uint8_t> rspExpectedNonZeroRC = {0x03, 0xf4, 0x09, 0x00,
+ 0x00, 0x00, 0x00, 0x00};
+ EXPECT_NO_THROW(hothUtil->payloadECResponse(rspExpectedNonZeroRC));
+
+ std::vector<uint8_t> payload(64);
+ std::generate(payload.begin(), payload.end(), std::rand);
+ uint8_t payloadSum =
+ std::accumulate(std::begin(payload), std::end(payload), 0) % 256;
+ uint8_t checksum = 0x100 - (0x03 + 0x40 + payloadSum) % 256;
+ std::vector<uint8_t> validRspWithRandomHss = {0x03, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00};
+ validRspWithRandomHss[1] = checksum;
+ validRspWithRandomHss.insert(validRspWithRandomHss.end(), payload.begin(),
+ payload.end());
+ auto data = hothUtil->payloadECResponse(validRspWithRandomHss);
+ EXPECT_EQ(std::vector<uint8_t>(data.begin(), data.end()), payload);
+}
+
+} // namespace ipmi_hoth
diff --git a/skmhss/test/meson.build b/skmhss/test/meson.build
new file mode 100644
index 0000000..89bc57c
--- /dev/null
+++ b/skmhss/test/meson.build
@@ -0,0 +1,20 @@
+tests = [
+ 'hoth_skmhss_unittest',
+ 'hoth_skmhss_close_unittest',
+ 'hoth_skmhss_commit_unittest',
+ 'hoth_skmhss_delete_unittest',
+ 'hoth_skmhss_open_unittest',
+ 'hoth_skmhss_read_unittest',
+ 'hoth_skmhss_write_unittest',
+ 'hoth_util_unittest',
+]
+
+foreach t : tests
+ test(
+ t,
+ executable(
+ t.underscorify(),
+ t + '.cpp',
+ implicit_include_directories: false,
+ dependencies: [hothskmhss_dep, test_dep]))
+endforeach
diff --git a/test/dbus_mock.hpp b/test/dbus_mock.hpp
new file mode 100644
index 0000000..a2a9ed3
--- /dev/null
+++ b/test/dbus_mock.hpp
@@ -0,0 +1,45 @@
+// 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 "dbus.hpp"
+
+#include <ipmid/api.h>
+#include <systemd/sd-bus.h>
+
+#include <string_view>
+
+#include <gmock/gmock.h>
+
+sd_bus* ipmid_get_sd_bus_connection()
+{
+ return nullptr;
+}
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class DbusMock : public virtual Dbus
+{
+ public:
+ MOCK_METHOD0(getHothdMapping, SubTreeMapping());
+ MOCK_METHOD1(pingHothd, bool(std::string_view));
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..4eed4c0
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,6 @@
+gtest = dependency('gtest', main: true, disabler: true, required: get_option('tests'))
+gmock = dependency('gmock', disabler: true, required: get_option('tests'))
+
+test_dep = declare_dependency(
+ include_directories: include_directories('.'),
+ dependencies: [gtest, gmock])
diff --git a/update/dbus_update.hpp b/update/dbus_update.hpp
new file mode 100644
index 0000000..7ec3b23
--- /dev/null
+++ b/update/dbus_update.hpp
@@ -0,0 +1,73 @@
+// 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 "dbus.hpp"
+
+#include <function2/function2.hpp>
+#include <stdplus/cancel.hpp>
+#include <xyz/openbmc_project/Control/Hoth/server.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusUpdate
+ * @brief Overridable D-Bus interface for command handler
+ */
+class DbusUpdate : public virtual Dbus
+{
+ public:
+ using FirmwareUpdateStatus = sdbusplus::xyz::openbmc_project::Control::
+ server::Hoth::FirmwareUpdateStatus;
+
+ using Cb = fu2::unique_function<void(FirmwareUpdateStatus)>;
+
+ /** @brief Implementation for UpdateFirmware
+ * Executes hothd's updateFirmware with the given firmware binary. This
+ * is an asynchronous call and should not return anything. Poll the status
+ * of the update with GetFirmwareUpdateStatus.
+ *
+ * @param[in] hothId - The identifier of the targeted hoth instance.
+ * @param[in] firmwareData - Firmware binary to write to Hoth's
+ * firmware update SPI partition
+ * @param[in] cb - The callback to execute when the request completes.
+ * @return cancelable[stdplus::Cancel] - Drop to cancel the command early.
+ */
+ virtual stdplus::Cancel
+ UpdateFirmware(std::string_view hothId,
+ const std::vector<uint8_t>& firmwareData, Cb&& cb) = 0;
+
+ /** @brief Implementation for GetFirmwareUpdateStatus
+ * Executes hothd's getFirmwareUpdateStatus. This is an asynchronous call
+ * that returns the status of the write to Hoth's firmware update SPI
+ * partition.
+ *
+ * @param[in] hothId - The identifier of the targeted hoth instance.
+ * @param[in] cb - The callback to execute when the request completes.
+ * @return cancelable[stdplus::Cancel] - Drop to cancel the command early.
+ */
+ virtual stdplus::Cancel
+ GetFirmwareUpdateStatus(std::string_view hothId, Cb&& cb) = 0;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/dbus_update_impl.cpp b/update/dbus_update_impl.cpp
new file mode 100644
index 0000000..c739ddf
--- /dev/null
+++ b/update/dbus_update_impl.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 "dbus_update_impl.hpp"
+
+#include <fmt/format.h>
+
+#include <optional>
+#include <string>
+#include <utility>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+stdplus::Cancel DbusUpdateImpl::UpdateFirmware(
+ std::string_view hothId, const std::vector<uint8_t>& firmwareData, Cb&& cb)
+{
+ auto req = newHothdCall(hothId, "UpdateFirmware");
+ req.append(firmwareData);
+ auto cHothId = std::string(hothId);
+ return stdplus::Cancel(new BusCall(req.call_async(
+ [cb = std::move(cb), hothId = std::move(cHothId)](
+ sdbusplus::message::message m) mutable noexcept {
+ if (m.is_method_error())
+ {
+ auto err = m.get_error();
+ fmt::print(stderr, "UpdateFirmware failed on `{}`: {}: {}\n",
+ hothId, err->name, err->message);
+ cb(FirmwareUpdateStatus::Error);
+ return;
+ }
+ cb(FirmwareUpdateStatus::InProgress);
+ },
+ asyncCallTimeout)));
+}
+
+stdplus::Cancel
+ DbusUpdateImpl::GetFirmwareUpdateStatus(std::string_view hothId, Cb&& cb)
+{
+ auto req = newHothdCall(hothId, "GetFirmwareUpdateStatus");
+ auto cHothId = std::string(hothId);
+ return stdplus::Cancel(new BusCall(req.call_async(
+ [cb = std::move(cb), hothId = std::move(cHothId)](
+ sdbusplus::message::message m) mutable noexcept {
+ if (m.is_method_error())
+ {
+ auto err = m.get_error();
+ fmt::print(stderr,
+ "GetFirmwareUpdateStatus failed on `{}`: {}: {}\n",
+ hothId, err->name, err->message);
+ cb(FirmwareUpdateStatus::Error);
+ return;
+ }
+ try
+ {
+ std::string rsp;
+ m.read(rsp);
+ cb(sdbusplus::xyz::openbmc_project::Control::server::Hoth::
+ convertFirmwareUpdateStatusFromString(rsp));
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(
+ stderr,
+ "GetFirmwareUpdateStatus failed unpacking on `{}`: {}\n",
+ hothId, e.what());
+ cb(FirmwareUpdateStatus::Error);
+ }
+ },
+ asyncCallTimeout)));
+}
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/dbus_update_impl.hpp b/update/dbus_update_impl.hpp
new file mode 100644
index 0000000..60a2370
--- /dev/null
+++ b/update/dbus_update_impl.hpp
@@ -0,0 +1,45 @@
+// 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 "dbus_impl.hpp"
+#include "dbus_update.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+/** @class DbusImpl
+ * @brief D-Bus concrete implementation
+ * @details Pass through all calls to the default D-Bus instance
+ */
+class DbusUpdateImpl : public DbusUpdate, public DbusImpl
+{
+ public:
+ stdplus::Cancel UpdateFirmware(std::string_view hothId,
+ const std::vector<uint8_t>& firmwareData,
+ Cb&& cb) override;
+ stdplus::Cancel GetFirmwareUpdateStatus(std::string_view hothId,
+ Cb&& cb) override;
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/hoth_update.cpp b/update/hoth_update.cpp
new file mode 100644
index 0000000..59a6e33
--- /dev/null
+++ b/update/hoth_update.cpp
@@ -0,0 +1,130 @@
+// 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 "hoth_update.hpp"
+
+#include <phosphor-logging/log.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+using phosphor::logging::entry;
+using phosphor::logging::log;
+
+using level = phosphor::logging::level;
+using SdBusError = sdbusplus::exception::SdBusError;
+
+namespace ipmi_hoth
+{
+
+void updateBlobState(uint16_t& state,
+ internal::DbusUpdate::FirmwareUpdateStatus st)
+{
+ state &= ~(blobs::StateFlags::commit_error | blobs::StateFlags::committing |
+ blobs::StateFlags::committed);
+ switch (st)
+ {
+ case internal::DbusUpdate::FirmwareUpdateStatus::None:
+ break;
+ case internal::DbusUpdate::FirmwareUpdateStatus::InProgress:
+ state |= blobs::StateFlags::committing;
+ break;
+ case internal::DbusUpdate::FirmwareUpdateStatus::Error:
+ state |= blobs::StateFlags::commit_error;
+ break;
+ case internal::DbusUpdate::FirmwareUpdateStatus::Done:
+ state |= blobs::StateFlags::committed;
+ break;
+ }
+}
+
+bool HothUpdateBlobHandler::stat(const std::string& path, blobs::BlobMeta* meta)
+{
+ std::optional<uint16_t> sess = getOnlySession(pathToHothId(path));
+ if (!sess)
+ {
+ return false;
+ }
+
+ /* Call session stat with the session ID of the only session */
+ return stat(*sess, meta);
+}
+
+bool HothUpdateBlobHandler::commit(uint16_t session,
+ const std::vector<uint8_t>& data)
+{
+ if (!data.empty())
+ {
+ log<level::ERR>("Unexpected data provided to commit call");
+ return false;
+ }
+
+ HothBlob* sess = getSession(session);
+ if (!sess)
+ {
+ return false;
+ }
+
+ // If commit is called multiple times, return the same result as last time
+ if (sess->state &
+ (blobs::StateFlags::committing | blobs::StateFlags::committed))
+ {
+ return true;
+ }
+
+ updateBlobState(sess->state,
+ internal::DbusUpdate::FirmwareUpdateStatus::InProgress);
+ sess->outstanding = dbus_->UpdateFirmware(
+ sess->hothId, sess->buffer,
+ [sess](internal::DbusUpdate::FirmwareUpdateStatus rsp) {
+ auto outstanding = std::move(sess->outstanding);
+ updateBlobState(sess->state, rsp);
+ });
+ return true;
+}
+
+bool HothUpdateBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+ HothBlob* sess = getSession(session);
+ if (!sess)
+ {
+ // Return false since blobs::BlobMeta was not updated
+ return false;
+ }
+
+ // We only want to start a new status check if we don't have a pending job
+ // and we hoth't receivied a completion state (error / done).
+ if (!sess->outstanding && (sess->state & blobs::StateFlags::committing))
+ {
+ sess->outstanding = dbus_->GetFirmwareUpdateStatus(
+ sess->hothId,
+ [sess](internal::DbusUpdate::FirmwareUpdateStatus rsp) {
+ auto outstanding = std::move(sess->outstanding);
+ updateBlobState(sess->state, rsp);
+ });
+ }
+
+ meta->size = sess->buffer.size();
+ meta->blobState = sess->state;
+ // Return true since blobs::BlobMeta was updated
+ return true;
+}
+
+} // namespace ipmi_hoth
diff --git a/update/hoth_update.hpp b/update/hoth_update.hpp
new file mode 100644
index 0000000..1dab418
--- /dev/null
+++ b/update/hoth_update.hpp
@@ -0,0 +1,68 @@
+// 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 "dbus_update.hpp"
+#include "hoth.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ipmi_hoth
+{
+
+class HothUpdateBlobHandler : public HothBlobHandler
+{
+ public:
+ explicit HothUpdateBlobHandler(internal::DbusUpdate* dbus) : dbus_(dbus) {}
+
+ /* Our callbacks require pinned memory */
+ HothUpdateBlobHandler(HothUpdateBlobHandler&&) = delete;
+ HothUpdateBlobHandler& operator=(HothUpdateBlobHandler&&) = delete;
+
+ bool stat(const std::string& path, blobs::BlobMeta* meta) override;
+ bool commit(uint16_t session, const std::vector<uint8_t>& data) override;
+ bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+
+ internal::Dbus& dbus() override
+ {
+ return *dbus_;
+ }
+ std::string_view pathSuffix() const override
+ {
+ return "firmware_update";
+ }
+ uint16_t requiredFlags() const override
+ {
+ return blobs::OpenFlags::write;
+ }
+ uint16_t maxSessions() const override
+ {
+ return 1;
+ }
+ uint32_t maxBufferSize() const override
+ {
+ return 1024 * 1024;
+ }
+
+ private:
+ internal::DbusUpdate* dbus_;
+};
+
+} // namespace ipmi_hoth
diff --git a/update/main_update.cpp b/update/main_update.cpp
new file mode 100644
index 0000000..fee3735
--- /dev/null
+++ b/update/main_update.cpp
@@ -0,0 +1,28 @@
+// 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 "dbus_update_impl.hpp"
+#include "hoth_update.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+ /** @brief Default instantiation of Dbus */
+ static ipmi_hoth::internal::DbusUpdateImpl dbusUpdate_impl;
+
+ return std::make_unique<ipmi_hoth::HothUpdateBlobHandler>(&dbusUpdate_impl);
+}
diff --git a/update/meson.build b/update/meson.build
new file mode 100644
index 0000000..1d2ae5d
--- /dev/null
+++ b/update/meson.build
@@ -0,0 +1,31 @@
+hothupdate_pre = declare_dependency(
+ include_directories: include_directories('.'),
+ dependencies: [
+ hothblob_dep,
+ dependency('hothd-dbus'),
+ phosphor_logging_dep,
+ sdbusplus_dep,
+ ])
+
+hothupdate_lib = static_library(
+ 'hothupdate',
+ 'dbus_update_impl.cpp',
+ 'hoth_update.cpp',
+ implicit_include_directories: false,
+ dependencies: hothupdate_pre)
+
+hothupdate_dep = declare_dependency(
+ link_with: hothupdate_lib,
+ dependencies: hothupdate_pre)
+
+shared_module(
+ 'hothupdate',
+ 'main_update.cpp',
+ implicit_include_directories: false,
+ dependencies: hothupdate_dep,
+ install: true,
+ install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+ subdir('test')
+endif
diff --git a/update/test/dbus_update_mock.hpp b/update/test/dbus_update_mock.hpp
new file mode 100644
index 0000000..334d4be
--- /dev/null
+++ b/update/test/dbus_update_mock.hpp
@@ -0,0 +1,40 @@
+// 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 "dbus_mock.hpp"
+#include "dbus_update.hpp"
+
+#include <gmock/gmock.h>
+
+namespace ipmi_hoth
+{
+namespace internal
+{
+
+class DbusUpdateMock : public DbusUpdate, public DbusMock
+{
+ public:
+ MOCK_METHOD(stdplus::Cancel, UpdateFirmware,
+ (std::string_view hothId,
+ const std::vector<uint8_t>& firmwareData, Cb&& cb),
+ (override));
+ MOCK_METHOD(stdplus::Cancel, GetFirmwareUpdateStatus,
+ (std::string_view hothId, Cb&& cb), (override));
+};
+
+} // namespace internal
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_close_unittest.cpp b/update/test/hoth_update_close_unittest.cpp
new file mode 100644
index 0000000..c069a9b
--- /dev/null
+++ b/update/test/hoth_update_close_unittest.cpp
@@ -0,0 +1,58 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateCloseTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateCloseTest, CloseWithInvalidSessionFails)
+{
+ // Verify you cannot close an invalid session.
+
+ EXPECT_FALSE(hvn.close(session_));
+}
+
+TEST_F(HothUpdateCloseTest, CloseWithValidSessionSuccess)
+{
+ // Verify you can close a valid session.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.close(session_));
+}
+
+TEST_F(HothUpdateCloseTest, CloseValidSessionTwiceFails)
+{
+ // Verify that close actually closes the session, expect the second close
+ // of the same session to fail.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.close(session_));
+ EXPECT_FALSE(hvn.close(session_));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_commit_unittest.cpp b/update/test/hoth_update_commit_unittest.cpp
new file mode 100644
index 0000000..1421c74
--- /dev/null
+++ b/update/test/hoth_update_commit_unittest.cpp
@@ -0,0 +1,216 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+using ::testing::Return;
+
+using namespace std::literals;
+
+namespace ipmi_hoth
+{
+
+using Cb = internal::DbusUpdate::Cb;
+using FirmwareUpdateStatus = internal::DbusUpdate::FirmwareUpdateStatus;
+
+class HothUpdateCommitTest : public HothUpdateTest
+{
+ protected:
+ template <typename Req>
+ void expectUpdate(std::string_view hothId, Req&& req)
+ {
+ EXPECT_CALL(dbus, UpdateFirmware(hothId, req, _))
+ .WillOnce(
+ [&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ }
+
+ Cb cb;
+};
+
+const auto static test_str = "Hello,\0 world!"s;
+const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end());
+
+TEST_F(HothUpdateCommitTest, InvalidSessionCommitIsRejected)
+{
+ // Verify the hoth update handler checks for a valid session.
+
+ EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>()));
+}
+
+TEST_F(HothUpdateCommitTest, NonEmptyDataParamFails)
+{
+ // Verify that we do not accept any data passed into the commit call.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3})));
+}
+
+TEST_F(HothUpdateCommitTest, DbusExceptionSetsCommitErrorStateAndFails)
+{
+ // Verify that when the dbus call hits an exception, commit method fails
+ // and that the state is set to blobs::StateFlags::commit_error
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectUpdate(/*hothId=*/"", ContainerEq(test_buf));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Error);
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothUpdateCommitTest, EmptyBufferCommitLegacyHothSuccessful)
+{
+ // Verify that we are able to succssfully commit an empty buffer
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectUpdate(/*hothId=*/"", IsEmpty());
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // No write call to populate the buffer here
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Done);
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.size, 0);
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, EmptyBufferCommitNamedHothSuccessful)
+{
+ // Verify that we are able to succssfully commit an empty buffer
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(name))).WillOnce(Return(true));
+ expectUpdate(/*hothId=*/name, IsEmpty());
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), namedPath));
+ // No write call to populate the buffer here
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Done);
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.size, 0);
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, NonEmptyBufferCommitSuccessful)
+{
+ // Verify that we are able to succssfully commit a buffer with valid data
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectUpdate(/*hothId=*/"", ContainerEq(test_buf));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Done);
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.size, test_buf.size());
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, IdempotentSuccess)
+{
+ // Verify that repeated successful commits only result in one D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+ expectUpdate(/*hothId=*/"", IsEmpty());
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // No write call to populate the buffer here
+
+ // These calls will return due to the state being "committing". Calling
+ // stat is the only way to check if the state has changed to "committed"
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Done);
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ // These calls will return due to the state being "committed"
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ // Stat is also idempotent, no need to mock the D-Bus call here
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.size, 0);
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::committed);
+}
+
+TEST_F(HothUpdateCommitTest, ErrorRetry)
+{
+ // Verify that commit after error retries the update D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ // These calls will return due to the state being "commit_error"
+ expectUpdate(/*hothId=*/"", IsEmpty());
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Error);
+ expectUpdate(/*hothId=*/"", IsEmpty());
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Error);
+ expectUpdate(/*hothId=*/"", IsEmpty());
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Error);
+
+ blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::commit_error);
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_delete_unittest.cpp b/update/test/hoth_update_delete_unittest.cpp
new file mode 100644
index 0000000..41aa16e
--- /dev/null
+++ b/update/test/hoth_update_delete_unittest.cpp
@@ -0,0 +1,32 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothUpdateDeleteTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateDeleteTest, VerifyHothDeleteFails)
+{
+ // The hoth update handler does not support delete.
+
+ EXPECT_FALSE(hvn.deleteBlob(legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_open_unittest.cpp b/update/test/hoth_update_open_unittest.cpp
new file mode 100644
index 0000000..f3aeaae
--- /dev/null
+++ b/update/test/hoth_update_open_unittest.cpp
@@ -0,0 +1,75 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateOpenTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateOpenTest, OpenWithBadFlagsFails)
+{
+ // Hoth update handler open requires the write flag.
+
+ EXPECT_FALSE(hvn.open(session_, blobs::OpenFlags::read, legacyPath));
+}
+
+TEST_F(HothUpdateOpenTest, OpenEverythingSucceeds)
+{
+ // Hoth update handler open with everything correct.
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothUpdateOpenTest, OpenSecondSessionFails)
+{
+ // Hoth update handler only allows 1 open session, verify second one fails
+ // regardless of whether the session ID is the same or not
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.open(session_ + 1, hvn.requiredFlags(), legacyPath));
+}
+
+TEST_F(HothUpdateOpenTest, OpenCloseOpenSucceeds)
+{
+ // Hoth update handler should be able to open a session after closing an
+ // open one
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view("")))
+ .WillRepeatedly(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.close(session_));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_read_unittest.cpp b/update/test/hoth_update_read_unittest.cpp
new file mode 100644
index 0000000..faa05b2
--- /dev/null
+++ b/update/test/hoth_update_read_unittest.cpp
@@ -0,0 +1,44 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <ipmid/handler.hpp>
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateReadTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateReadTest, VerifyHothReadFails)
+{
+ // The hoth update handler does not support read, expect empty vector
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_THROW(hvn.read(session_, 0, 1), ipmi::HandlerCompletion);
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_stat_unittest.cpp b/update/test/hoth_update_stat_unittest.cpp
new file mode 100644
index 0000000..54b66f1
--- /dev/null
+++ b/update/test/hoth_update_stat_unittest.cpp
@@ -0,0 +1,379 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <string_view>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+using ::testing::_;
+using ::testing::ContainerEq;
+using ::testing::NotNull;
+using ::testing::Return;
+
+using namespace std::literals;
+
+namespace ipmi_hoth
+{
+
+using Cb = internal::DbusUpdate::Cb;
+using FirmwareUpdateStatus = internal::DbusUpdate::FirmwareUpdateStatus;
+
+class HothUpdateStatTest : public HothUpdateTest
+{
+ protected:
+ blobs::BlobMeta meta_;
+ // Initialize expected_meta_ with empty members
+ blobs::BlobMeta expected_meta_;
+};
+class HothUpdateSessionStatTest : public HothUpdateStatTest
+{};
+
+struct MockCancel : stdplus::Cancelable
+{
+ MOCK_METHOD(void, cancel, (), (noexcept, override));
+};
+
+const auto static test_str = "Hello,\0 world!"s;
+const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end());
+
+TEST_F(HothUpdateStatTest, InvalidStatIsRejected)
+{
+ // Verify the hoth update handler checks for a valid session.
+
+ EXPECT_FALSE(hvn.stat(legacyPath, &meta_));
+}
+
+TEST_F(HothUpdateStatTest, StatBeforeCommitReturnsInitialState)
+{
+ // Verify stat returns initial state before commit
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.stat(legacyPath, &meta_));
+
+ expected_meta_.size = 0;
+ expected_meta_.blobState = blobs::StateFlags::open_write;
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+// Rest of the tests will be using session stat, since
+// stat with blob ID calls session ID after the initial checks
+
+TEST_F(HothUpdateSessionStatTest, InvalidSessionStatIsRejected)
+{
+ // Verify the hoth update handler checks for a valid session.
+
+ EXPECT_FALSE(hvn.stat(0, &meta_));
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatBeforeCommitReturnsInitialState)
+{
+ // Verify the session stat before commit returns initial state
+ // without any D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+ expected_meta_.size = 0;
+ expected_meta_.blobState = blobs::StateFlags::open_write;
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatAfterWriteMetadataLengthMatches)
+{
+ // Verify that after writes, the length returned matches
+ // without any D-bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+ // We wrote one byte to the last index, making the length the buffer size.
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState = blobs::StateFlags::open_write;
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatAfterErrorCommitReturnsStatus)
+{
+ // Verify that after commit errors out early, the session stat
+ // returns the initial status without any D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+ EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3})));
+
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState = blobs::StateFlags::open_write;
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatErrorReturnsCommitError)
+{
+ // Verify that mocking GetFirmwareUpdateStatus ouptut to Error after
+ // a successful commit makes session stat return 'commit_error' status
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ Cb cb;
+ EXPECT_CALL(dbus,
+ UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::InProgress);
+
+ EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+ .WillOnce([&](std::string_view, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ struct blobs::BlobMeta meta;
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+
+ EXPECT_EQ(meta.size, test_buf.size());
+ EXPECT_EQ(meta.metadata.size(), 0);
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::committing);
+
+ cb(FirmwareUpdateStatus::Error);
+ EXPECT_TRUE(hvn.stat(session_, &meta));
+ EXPECT_EQ(meta.blobState,
+ blobs::StateFlags::open_write | blobs::StateFlags::commit_error);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatInProgressReturnsCommitting)
+{
+ // Verify that mocking GetFirmwareUpdateStatus ouptut to InProgress after
+ // a successful commit makes session stat return 'committing' status
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ Cb cb;
+ EXPECT_CALL(dbus,
+ UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::InProgress);
+
+ EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+ .Times(2)
+ .WillRepeatedly([&](std::string_view, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ cb(FirmwareUpdateStatus::InProgress);
+
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState =
+ blobs::StateFlags::open_write | blobs::StateFlags::committing;
+ EXPECT_EQ(meta_, expected_meta_);
+
+ // Check that repeated stats after callback trigger another call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, SessionStatDoneReturnsCommitted)
+{
+ // Verify that mocking GetFirmwareUpdateStatus ouptut to Done after
+ // a successful commit makes session stat return 'committed' status
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ Cb cb;
+ EXPECT_CALL(dbus,
+ UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::InProgress);
+
+ EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+ .WillOnce([&](std::string_view, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState =
+ blobs::StateFlags::open_write | blobs::StateFlags::committing;
+ EXPECT_EQ(meta_, expected_meta_);
+
+ cb(FirmwareUpdateStatus::Done);
+ expected_meta_.blobState =
+ blobs::StateFlags::open_write | blobs::StateFlags::committed;
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, MultipleInProgressReturnsCommitting)
+{
+ // Verify that repeated session stat calls while committing
+ // result in multiple D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ testing::StrictMock<MockCancel> c;
+ Cb cb;
+ EXPECT_CALL(dbus,
+ UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(&c);
+ });
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+
+ // If commit is still outstanding we should not issue a new command
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState =
+ blobs::StateFlags::open_write | blobs::StateFlags::committing;
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+
+ EXPECT_CALL(c, cancel());
+ cb(FirmwareUpdateStatus::InProgress);
+ testing::Mock::VerifyAndClearExpectations(&c);
+
+ EXPECT_CALL(dbus, GetFirmwareUpdateStatus(std::string_view(""), _))
+ .WillOnce([&](std::string_view, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(&c);
+ });
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+
+ EXPECT_CALL(c, cancel());
+ cb(FirmwareUpdateStatus::InProgress);
+}
+
+TEST_F(HothUpdateSessionStatTest, IdempotentDoneReturnsCommitted)
+{
+ // Verify that repeated session stat calls while status is committed
+ // result in one D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ Cb cb;
+ EXPECT_CALL(dbus,
+ UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Done);
+
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState =
+ blobs::StateFlags::open_write | blobs::StateFlags::committed;
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+TEST_F(HothUpdateSessionStatTest, IdempotentErrorReturnsCommitError)
+{
+ // Verify that repeated session stat calls while status is commit_error
+ // result in one D-Bus call
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+ // session, offset, data
+ EXPECT_TRUE(hvn.write(session_, 0, test_buf));
+
+ Cb cb;
+ EXPECT_CALL(dbus,
+ UpdateFirmware(std::string_view(""), ContainerEq(test_buf), _))
+ .WillOnce([&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) {
+ cb = std::move(icb);
+ return stdplus::Cancel(std::nullopt);
+ });
+ EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>()));
+ cb(FirmwareUpdateStatus::Error);
+
+ expected_meta_.size = test_buf.size();
+ expected_meta_.blobState =
+ blobs::StateFlags::open_write | blobs::StateFlags::commit_error;
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+ // Shouldn't trigger a new call
+ EXPECT_TRUE(hvn.stat(session_, &meta_));
+ EXPECT_EQ(meta_, expected_meta_);
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_unittest.cpp b/update/test/hoth_update_unittest.cpp
new file mode 100644
index 0000000..87ef59a
--- /dev/null
+++ b/update/test/hoth_update_unittest.cpp
@@ -0,0 +1,70 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+namespace ipmi_hoth
+{
+
+class HothUpdateBasicTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateBasicTest, CanHandleBlobChecksNameInvalid)
+{
+ // Verify canHandleBlob checks and returns false on an invalid name.
+
+ EXPECT_FALSE(hvn.canHandleBlob("asdf"));
+ EXPECT_FALSE(hvn.canHandleBlob("dev/hoth/firmware_update"));
+ EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/firmware_update2"));
+ EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/hoth0/t/firmware_update"));
+ EXPECT_FALSE(hvn.canHandleBlob("/dev/hoth/command_passthru"));
+}
+
+TEST_F(HothUpdateBasicTest, CanHandleBlobChecksNameValid)
+{
+ // Verify canHandleBlob checks and returns true on the valid name.
+
+ EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/firmware_update"));
+ EXPECT_TRUE(hvn.canHandleBlob("/dev/hoth/hoth0/firmware_update"));
+}
+
+TEST_F(HothUpdateBasicTest, VerifyBehaviorOfBlobIds)
+{
+ // Verify the correct BlobIds is returned from the hoth update handler.
+ internal::Dbus::SubTreeMapping mapping = {
+ {"/xyz/openbmc_project/Control", {}},
+ {"/xyz/openbmc_project/Control/Hoth2nologue", {}},
+ {"/xyz/openbmc_project/Control/Hoth/nologue/2", {}},
+ {"/xyz/openbmc_project/Control/Hoth", {}},
+ {"/xyz/openbmc_project/Control/Hoth/hoth0", {}},
+ {"/xyz/openbmc_project/Control/Hoth/hoth1", {}},
+ };
+ EXPECT_CALL(dbus, getHothdMapping()).WillOnce(Return(mapping));
+ std::vector<std::string> expected = {
+ "/dev/hoth/firmware_update",
+ "/dev/hoth/hoth0/firmware_update",
+ "/dev/hoth/hoth1/firmware_update",
+ };
+ EXPECT_THAT(hvn.getBlobIds(), UnorderedElementsAreArray(expected));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_unittest.hpp b/update/test/hoth_update_unittest.hpp
new file mode 100644
index 0000000..56bb72b
--- /dev/null
+++ b/update/test/hoth_update_unittest.hpp
@@ -0,0 +1,48 @@
+// 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 "dbus_update_mock.hpp"
+#include "hoth_update.hpp"
+
+#include <stdplus/util/string.hpp>
+
+#include <cstdint>
+#include <string>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+class HothUpdateTest : public ::testing::Test
+{
+ protected:
+ explicit HothUpdateTest() : hvn(&dbus) {}
+
+ // dbus mock object
+ testing::StrictMock<internal::DbusUpdateMock> dbus;
+
+ HothUpdateBlobHandler hvn;
+
+ const uint16_t session_ = 0;
+ const std::string legacyPath = stdplus::util::strCat(
+ HothUpdateBlobHandler::hothPathPrefix, hvn.pathSuffix());
+ const std::string name = "hoth0";
+ const std::string namedPath = stdplus::util::strCat(
+ HothUpdateBlobHandler::hothPathPrefix, name, "/", hvn.pathSuffix());
+};
+
+} // namespace ipmi_hoth
diff --git a/update/test/hoth_update_write_unittest.cpp b/update/test/hoth_update_write_unittest.cpp
new file mode 100644
index 0000000..0b4a3e6
--- /dev/null
+++ b/update/test/hoth_update_write_unittest.cpp
@@ -0,0 +1,139 @@
+// 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 "hoth_update_unittest.hpp"
+
+#include <cstdint>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_hoth
+{
+
+using ::testing::Return;
+
+class HothUpdateWriteTest : public HothUpdateTest
+{};
+
+TEST_F(HothUpdateWriteTest, InvalidSessionWriteIsRejected)
+{
+ // Verify the hoth update handler checks for a valid session.
+
+ std::vector<uint8_t> data = {0x1, 0x2};
+
+ EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingTooMuchByOneByteFails)
+{
+ // Test the edge case of writing 1 byte too much with an offset of 0.
+ // writing hvn.maxBufferSize + 1 at offset 0 is invalid.
+
+ int bytes = hvn.maxBufferSize() + 1;
+ std::vector<uint8_t> data(0x11);
+ data.resize(bytes);
+ ASSERT_EQ(bytes, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingTooMuchByOffsetOfOne)
+{
+ // Test the edge case of writing hvn.maxBufferSize bytes (which is fine)
+ // but at the offset 1, which makes it go over by 1 byte.
+ // writing hvn.maxBufferSize + offset 1 is invalid.
+
+ int bytes = hvn.maxBufferSize();
+ std::vector<uint8_t> data(0x11);
+ data.resize(bytes);
+ ASSERT_EQ(bytes, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ // offset of 1.
+ EXPECT_FALSE(hvn.write(session_, 1, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingOneByteBeyondEndFromOffsetFails)
+{
+ // Test the edge case of writing to the last byte offset but trying to
+ // write two bytes.
+ // writing 2 bytes at hvn.maxBufferSize - 1 is invalid.
+
+ std::vector<uint8_t> data = {0x01, 0x02};
+ ASSERT_EQ(2, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingOneByteAtOffsetBeyondEndFails)
+{
+ // Test the edge case of writing one byte but exactly one byte beyond the
+ // buffer.
+ // writing 1 byte at hvn.maxBufferSize is invalid.
+
+ std::vector<uint8_t> data = {0x01};
+ ASSERT_EQ(1, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_FALSE(hvn.write(session_, hvn.maxBufferSize(), data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingFullBufferAtOffsetZeroSucceeds)
+{
+ // Test the case where you write the full buffer length at once to the 0th
+ // offset.
+
+ int bytes = hvn.maxBufferSize();
+ std::vector<uint8_t> data = {0x01};
+ data.resize(bytes);
+ ASSERT_EQ(bytes, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.write(session_, 0, data));
+}
+
+TEST_F(HothUpdateWriteTest, WritingOneByteToTheLastOffsetSucceeds)
+{
+ // Test the case where you write the last byte.
+
+ std::vector<uint8_t> data = {0x01};
+ ASSERT_EQ(1, data.size());
+
+ EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true));
+
+ EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath));
+
+ EXPECT_TRUE(hvn.write(session_, (hvn.maxBufferSize() - 1), data));
+}
+
+} // namespace ipmi_hoth
diff --git a/update/test/meson.build b/update/test/meson.build
new file mode 100644
index 0000000..9cfe4f5
--- /dev/null
+++ b/update/test/meson.build
@@ -0,0 +1,20 @@
+tests = [
+ 'hoth_update_unittest',
+ 'hoth_update_close_unittest',
+ 'hoth_update_commit_unittest',
+ 'hoth_update_delete_unittest',
+ 'hoth_update_open_unittest',
+ 'hoth_update_read_unittest',
+ 'hoth_update_stat_unittest',
+ 'hoth_update_write_unittest',
+]
+
+foreach t : tests
+ test(
+ t,
+ executable(
+ t.underscorify(),
+ t + '.cpp',
+ implicit_include_directories: false,
+ dependencies: [hothupdate_dep, test_dep]))
+endforeach