From ef3aeadc9be37c47d0627e576e81a74a5bb9e94f Mon Sep 17 00:00:00 2001 From: Patrick Venture Date: Wed, 12 Sep 2018 08:53:29 -0700 Subject: initial drop of phosphor-ipmi-blobs This implements a majority of the OEM IPMI BLOBS protocol. The only piece missing from this is the timed expiration of sessions. Change-Id: I82c9d17b625c94fc3340edcfabbbf1ffeb5ad7ac Signed-off-by: Patrick Venture --- .clang-format | 99 ++++++++++ .gitignore | 64 +++++++ MAINTAINERS | 46 +++++ Makefile.am | 16 ++ blobs.hpp | 143 ++++++++++++++ bootstrap.sh | 18 ++ configure.ac | 58 ++++++ crc.cpp | 66 +++++++ crc.hpp | 60 ++++++ ipmi.cpp | 324 +++++++++++++++++++++++++++++++ ipmi.hpp | 227 ++++++++++++++++++++++ main.cpp | 77 ++++++++ manager.cpp | 347 ++++++++++++++++++++++++++++++++++ manager.hpp | 254 +++++++++++++++++++++++++ process.cpp | 162 ++++++++++++++++ process.hpp | 46 +++++ test/Makefile.am | 110 +++++++++++ test/blob_mock.hpp | 27 +++ test/crc_mock.hpp | 19 ++ test/crc_unittest.cpp | 44 +++++ test/ipmi_close_unittest.cpp | 66 +++++++ test/ipmi_commit_unittest.cpp | 112 +++++++++++ test/ipmi_delete_unittest.cpp | 89 +++++++++ test/ipmi_enumerate_unittest.cpp | 65 +++++++ test/ipmi_getcount_unittest.cpp | 72 +++++++ test/ipmi_open_unittest.cpp | 108 +++++++++++ test/ipmi_read_unittest.cpp | 78 ++++++++ test/ipmi_sessionstat_unittest.cpp | 121 ++++++++++++ test/ipmi_stat_unittest.cpp | 157 +++++++++++++++ test/ipmi_unittest.cpp | 60 ++++++ test/ipmi_validate_unittest.cpp | 44 +++++ test/ipmi_write_unittest.cpp | 73 +++++++ test/manager_close_unittest.cpp | 66 +++++++ test/manager_commit_unittest.cpp | 68 +++++++ test/manager_delete_unittest.cpp | 87 +++++++++ test/manager_getsession_unittest.cpp | 24 +++ test/manager_mock.hpp | 31 +++ test/manager_open_unittest.cpp | 85 +++++++++ test/manager_read_unittest.cpp | 78 ++++++++ test/manager_sessionstat_unittest.cpp | 66 +++++++ test/manager_stat_unittest.cpp | 60 ++++++ test/manager_unittest.cpp | 172 +++++++++++++++++ test/manager_write_unittest.cpp | 90 +++++++++ test/process_unittest.cpp | 290 ++++++++++++++++++++++++++++ 44 files changed, 4369 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 MAINTAINERS create mode 100644 Makefile.am create mode 100644 blobs.hpp create mode 100755 bootstrap.sh create mode 100644 configure.ac create mode 100644 crc.cpp create mode 100644 crc.hpp create mode 100644 ipmi.cpp create mode 100644 ipmi.hpp create mode 100644 main.cpp create mode 100644 manager.cpp create mode 100644 manager.hpp create mode 100644 process.cpp create mode 100644 process.hpp create mode 100644 test/Makefile.am create mode 100644 test/blob_mock.hpp create mode 100644 test/crc_mock.hpp create mode 100644 test/crc_unittest.cpp create mode 100644 test/ipmi_close_unittest.cpp create mode 100644 test/ipmi_commit_unittest.cpp create mode 100644 test/ipmi_delete_unittest.cpp create mode 100644 test/ipmi_enumerate_unittest.cpp create mode 100644 test/ipmi_getcount_unittest.cpp create mode 100644 test/ipmi_open_unittest.cpp create mode 100644 test/ipmi_read_unittest.cpp create mode 100644 test/ipmi_sessionstat_unittest.cpp create mode 100644 test/ipmi_stat_unittest.cpp create mode 100644 test/ipmi_unittest.cpp create mode 100644 test/ipmi_validate_unittest.cpp create mode 100644 test/ipmi_write_unittest.cpp create mode 100644 test/manager_close_unittest.cpp create mode 100644 test/manager_commit_unittest.cpp create mode 100644 test/manager_delete_unittest.cpp create mode 100644 test/manager_getsession_unittest.cpp create mode 100644 test/manager_mock.hpp create mode 100644 test/manager_open_unittest.cpp create mode 100644 test/manager_read_unittest.cpp create mode 100644 test/manager_sessionstat_unittest.cpp create mode 100644 test/manager_stat_unittest.cpp create mode 100644 test/manager_unittest.cpp create mode 100644 test/manager_write_unittest.cpp create mode 100644 test/process_unittest.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ea71ad6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,99 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +PointerAlignment: Left +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^[<"](gtest|gmock)' + Priority: 5 + - Regex: '^"config.h"' + Priority: -1 + - Regex: '^".*\.hpp"' + Priority: 1 + - Regex: '^<.*\.h>' + Priority: 2 + - Regex: '^<.*' + Priority: 3 + - Regex: '.*' + Priority: 4 +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never +... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbd5f0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Template from: +# https://github.com/github/gitignore/blob/master/Autotools.gitignore + +# http://www.gnu.org/software/automake + +Makefile.in +/ar-lib +/mdate-sh +/py-compile +/test-driver +/ylwrap + +# http://www.gnu.org/software/autoconf + +/autom4te.cache +/autoscan.log +/autoscan-*.log +/aclocal.m4 +/compile +/config.guess +/config.h.in +/config.sub +/configure +/configure.scan +/depcomp +/install-sh +/missing +/stamp-h1 + +# https://www.gnu.org/software/libtool/ + +/ltmain.sh + +# http://www.gnu.org/software/texinfo + +/texinfo.tex + +# Repo Specific Items +*.o +/config.h +/config.h.in~ +/config.log +/config.status +Makefile +.deps +.dirstamp +/lib* +.libs/ +/*-libtool +/ipmid +.project +/test/*_unittest +/test/*.log +/test/*.trs + +# ignore vim swap files +.*.sw* +# failures from patch +*.orig +*.rej +# backup files from some editors +*~ +.cscope/ +build/ diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..2f27cc0 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,46 @@ +How to use this list: + Find the most specific section entry (described below) that matches where + your change lives and add the reviewers (R) and maintainers (M) as + reviewers. You can use the same method to track down who knows a particular + code base best. + + Your change/query may span multiple entries; that is okay. + + If you do not find an entry that describes your request at all, someone + forgot to update this list; please at least file an issue or send an email + to a maintainer, but preferably you should just update this document. + +Description of section entries: + + Section entries are structured according to the following scheme: + + X: NAME + X: ... + . + . + . + + Where REPO_NAME is the name of the repository within the OpenBMC GitHub + organization; FILE_PATH is a file path within the repository, possibly with + wildcards; X is a tag of one of the following types: + + M: Denotes maintainer; has fields NAME ; + if omitted from an entry, assume one of the maintainers from the + MAINTAINERS entry. + R: Denotes reviewer; has fields NAME ; + these people are to be added as reviewers for a change matching the repo + path. + F: Denotes forked from an external repository; has fields URL. + + Line comments are to be denoted "# SOME COMMENT" (typical shell style + comment); it is important to follow the correct syntax and semantics as we + may want to use automated tools with this file in the future. + + A change cannot be added to an OpenBMC repository without a MAINTAINER's + approval; thus, a MAINTAINER should always be listed as a reviewer. + +START OF MAINTAINERS LIST +------------------------- + +M: Patrick Venture +M: Kun Yi diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..624bbf2 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,16 @@ +AM_DEFAULT_SOURCE_EXT = .cpp + +libblobcmdsdir = ${libdir}/ipmid-providers +libblobcmds_LTLIBRARIES = libblobcmds.la +libblobcmds_la_SOURCES = main.cpp \ + ipmi.cpp \ + manager.cpp \ + process.cpp \ + crc.cpp + +libblobcmds_la_LDFLAGS = $(SYSTEMD_LIBS) \ + -version-info 0:0:0 -shared + +libblobcmds_la_CXXFLAGS = $(SYSTEMD_CFLAGS) + +SUBDIRS = . test diff --git a/blobs.hpp b/blobs.hpp new file mode 100644 index 0000000..b6672b7 --- /dev/null +++ b/blobs.hpp @@ -0,0 +1,143 @@ +#pragma once + +#include +#include + +namespace blobs +{ + +enum OpenFlags +{ + read = (1 << 0), + write = (1 << 1), + /* bits 3-7 reserved. */ + /* bits 8-15 given blob-specific definitions */ +}; + +enum StateFlags +{ + open_read = (1 << 0), + open_write = (1 << 1), + committing = (1 << 2), + committed = (1 << 3), + commit_error = (1 << 4), +}; + +struct BlobMeta +{ + uint16_t blobState; + uint32_t size; + std::vector metadata; +}; + +/* + * All blob specific objects implement this interface. + */ +class GenericBlobInterface +{ + public: + virtual ~GenericBlobInterface() = default; + + /** + * Checks if the handler will manage this file path. + * + * @param[in] blobId. + * @return bool whether it will manage the file path. + */ + virtual bool canHandleBlob(const std::string& path) = 0; + + /** + * Return the name(s) of the blob(s). Used during GetCount. + * + * @return List of blobIds this handler manages. + */ + virtual std::vector getBlobIds() = 0; + + /** + * Attempt to delete the blob specified by the path. + * + * @param[in] path - the blobId to try and delete. + * @return bool - whether it was able to delete the blob. + */ + virtual bool deleteBlob(const std::string& path) = 0; + + /** + * Return metadata about the blob. + * + * @param[in] path - the blobId for metadata. + * @param[in,out] meta - a pointer to a blobmeta. + * @return bool - true if it was successful. + */ + virtual bool stat(const std::string& path, struct BlobMeta* meta) = 0; + + /* The methods below are per session. */ + + /** + * Attempt to open a session from this path. + * + * @param[in] session - the session id. + * @param[in] flags - the open flags. + * @param[in] path - the blob path. + * @return bool - was able to open the session. + */ + virtual bool open(uint16_t session, uint16_t flags, + const std::string& path) = 0; + + /** + * Attempt to read from a blob. + * + * @param[in] session - the session id. + * @param[in] offset - offset into the blob. + * @param[in] requestedSize - number of bytes to read. + * @return Bytes read back (0 length on error). + */ + virtual std::vector read(uint16_t session, uint32_t offset, + uint32_t requestedSize) = 0; + + /** + * Attempt to write to a blob. + * + * @param[in] session - the session id. + * @param[in] offset - offset into the blob. + * @param[in] data - the data to write. + * @return bool - was able to write. + */ + virtual bool write(uint16_t session, uint32_t offset, + const std::vector& data) = 0; + + /** + * Attempt to commit to a blob. + * + * @param[in] session - the session id. + * @param[in] data - optional commit data. + * @return bool - was able to start commit. + */ + virtual bool commit(uint16_t session, const std::vector& data) = 0; + + /** + * Attempt to close your session. + * + * @param[in] session - the session id. + * @return bool - was able to close session. + */ + virtual bool close(uint16_t session) = 0; + + /** + * Attempt to return metadata for the session's view of the blob. + * + * @param[in] session - the session id. + * @param[in,out] meta - pointer to update with the BlobMeta. + * @return bool - wether it was successful. + */ + virtual bool stat(uint16_t session, struct BlobMeta* meta) = 0; + + /** + * Attempt to expire a session. This is called when a session has been + * inactive for at least 10 minutes. + * + * @param[in] session - the session id. + * @return bool - whether the session was able to be closed. + */ + virtual bool expire(uint16_t session) = 0; +}; +} // namespace blobs diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..50b75b7 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +AUTOCONF_FILES="Makefile.in aclocal.m4 ar-lib autom4te.cache compile \ + config.guess config.h.in config.sub configure depcomp install-sh \ + ltmain.sh missing *libtool test-driver" + +case $1 in + clean) + test -f Makefile && make maintainer-clean + for file in ${AUTOCONF_FILES}; do + find -name "$file" | xargs -r rm -rf + done + exit 0 + ;; +esac + +autoreconf -i +echo 'Run "./configure ${CONFIGURE_FLAGS} && make"' diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..e81c806 --- /dev/null +++ b/configure.ac @@ -0,0 +1,58 @@ +# Initialization +AC_PREREQ([2.69]) +AC_INIT([phosphor-ipmi-blobs], [1.0], [https://github.com/openbmc/phosphor-ipmi-blobs/issues]) +AC_LANG([C++]) +AC_CONFIG_HEADERS([config.h]) +AM_INIT_AUTOMAKE([subdir-objects -Wall -Werror foreign dist-xz]) +AM_SILENT_RULES([yes]) + +# Checks for programs. +AC_PROG_CXX +AM_PROG_AR +AC_PROG_INSTALL +AC_PROG_MAKE_SET + +# Checks for typedefs, structures, and compiler characteristics. +AX_CXX_COMPILE_STDCXX_14([noext]) +AX_APPEND_COMPILE_FLAGS([-Wall -Werror], [CXXFLAGS]) + +# Checks for libraries. +PKG_CHECK_MODULES([SYSTEMD], [libsystemd >= 221], [], [AC_MSG_ERROR(["systemd required and not found"])]) +AC_CHECK_HEADER([host-ipmid], [AC_MSG_ERROR(["phosphor-host-ipmid required and not found."])]) +AX_PTHREAD([], [AC_MSG_ERROR(["pthread required and not found"])]) + +# Checks for library functions. +LT_INIT # Required for systemd linking + +# Check/set gtest specific functions. +PKG_CHECK_MODULES([GTEST], [gtest], [], [AC_MSG_NOTICE([gtest not found, tests will not build])]) +PKG_CHECK_MODULES([GMOCK], [gmock], [], [AC_MSG_NOTICE([gmock not found, tests will not build])]) +PKG_CHECK_MODULES([GTEST_MAIN], [gtest_main], [], [AC_MSG_NOTICE([gtest_main not found, tests will not build])]) + +# Add --enable-oe-sdk flag to configure script +AC_ARG_ENABLE([oe-sdk], + AS_HELP_STRING([--enable-oe-sdk], [Link testcases absolutely against OE SDK so they can be ran within it.]) +) + +# Check for OECORE_TARGET_SYSROOT in the environment. +AC_ARG_VAR(OECORE_TARGET_SYSROOT, + [Path to the OE SDK SYSROOT]) + +# Configure OESDK_TESTCASE_FLAGS environment variable, which will be later +# used in test/Makefile.am +AS_IF([test "x$enable_oe_sdk" == "xyes"], + AS_IF([test "x$OECORE_TARGET_SYSROOT" == "x"], + AC_MSG_ERROR([OECORE_TARGET_SYSROOT must be set with --enable-oe-sdk]) + ) + AC_MSG_NOTICE([Enabling OE-SDK at $OECORE_TARGET_SYSROOT]) + [ + testcase_flags="-Wl,-rpath,\${OECORE_TARGET_SYSROOT}/lib" + testcase_flags="${testcase_flags} -Wl,-rpath,\${OECORE_TARGET_SYSROOT}/usr/lib" + testcase_flags="${testcase_flags} -Wl,-dynamic-linker,`find \${OECORE_TARGET_SYSROOT}/lib/ld-*.so | sort -r -n | head -n1`" + ] + AC_SUBST([OESDK_TESTCASE_FLAGS], [$testcase_flags]) +) + +# Create configured output +AC_CONFIG_FILES([Makefile test/Makefile]) +AC_OUTPUT diff --git a/crc.cpp b/crc.cpp new file mode 100644 index 0000000..5fc4558 --- /dev/null +++ b/crc.cpp @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 "crc.hpp" + +namespace blobs +{ + +void Crc16::clear() +{ + value = crc16Initial; +} + +// Origin: security/crypta/ipmi/portable/ipmi_utils.c +void Crc16::compute(const uint8_t* bytes, uint32_t length) +{ + if (!bytes) + { + return; + } + + const int kExtraRounds = 2; + const uint16_t kLeftBit = 0x8000; + uint16_t crc = value; + size_t i, j; + + for (i = 0; i < length + kExtraRounds; ++i) + { + for (j = 0; j < 8; ++j) + { + bool xor_flag = crc & kLeftBit; + crc <<= 1; + // If this isn't an extra round and the current byte's j'th bit + // from the left is set, increment the CRC. + if (i < length && bytes[i] & (1 << (7 - j))) + { + crc++; + } + if (xor_flag) + { + crc ^= crc16Ccitt; + } + } + } + + value = crc; +} + +uint16_t Crc16::get() const +{ + return value; +} +} // namespace blobs diff --git a/crc.hpp b/crc.hpp new file mode 100644 index 0000000..3793d9a --- /dev/null +++ b/crc.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +namespace blobs +{ + +using std::size_t; +using std::uint16_t; +using std::uint32_t; +using std::uint8_t; + +constexpr uint16_t crc16Ccitt = 0x1021; +/* Value from: http://srecord.sourceforge.net/crc16-ccitt.html for + * implementation without explicit bit adding. + */ +constexpr uint16_t crc16Initial = 0xFFFF; + +class CrcInterface +{ + public: + virtual ~CrcInterface() = default; + + /** + * Reset the crc. + */ + virtual void clear() = 0; + + /** + * Provide bytes against which to compute the crc. This method is + * meant to be only called once between clear() and get(). + * + * @param[in] bytes - the data against which to compute. + * @param[in] length - the number of bytes. + */ + virtual void compute(const uint8_t* bytes, uint32_t length) = 0; + + /** + * Read back the current crc value. + * + * @return the crc16 value. + */ + virtual uint16_t get() const = 0; +}; + +class Crc16 : public CrcInterface +{ + public: + Crc16() : poly(crc16Ccitt), value(crc16Initial){}; + ~Crc16() = default; + + void clear() override; + void compute(const uint8_t* bytes, uint32_t length) override; + uint16_t get() const override; + + private: + uint16_t poly; + uint16_t value; +}; +} // namespace blobs diff --git a/ipmi.cpp b/ipmi.cpp new file mode 100644 index 0000000..6942b11 --- /dev/null +++ b/ipmi.cpp @@ -0,0 +1,324 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 "ipmi.hpp" + +#include +#include +#include + +namespace blobs +{ + +bool validateRequestLength(BlobOEMCommands command, size_t requestLen) +{ + /* The smallest string is one letter and the nul-terminator. */ + static const int kMinStrLen = 2; + + static const std::unordered_map minimumLengths = { + {BlobOEMCommands::bmcBlobEnumerate, sizeof(struct BmcBlobEnumerateTx)}, + {BlobOEMCommands::bmcBlobOpen, + sizeof(struct BmcBlobOpenTx) + kMinStrLen}, + {BlobOEMCommands::bmcBlobClose, sizeof(struct BmcBlobCloseTx)}, + {BlobOEMCommands::bmcBlobDelete, + sizeof(struct BmcBlobDeleteTx) + kMinStrLen}, + {BlobOEMCommands::bmcBlobStat, + sizeof(struct BmcBlobStatTx) + kMinStrLen}, + {BlobOEMCommands::bmcBlobSessionStat, + sizeof(struct BmcBlobSessionStatTx)}, + {BlobOEMCommands::bmcBlobCommit, sizeof(struct BmcBlobCommitTx)}, + {BlobOEMCommands::bmcBlobRead, sizeof(struct BmcBlobReadTx)}, + {BlobOEMCommands::bmcBlobWrite, + sizeof(struct BmcBlobWriteTx) + sizeof(uint8_t)}, + }; + + auto results = minimumLengths.find(command); + if (results == minimumLengths.end()) + { + /* Valid length by default if we don't care. */ + return true; + } + + /* If the request is shorter than the minimum, it's invalid. */ + if (requestLen < results->second) + { + return false; + } + + return true; +} + +std::string stringFromBuffer(const char* start, size_t length) +{ + if (!start) + { + return ""; + } + + auto end = static_cast(std::memchr(start, '\0', length)); + if (end != &start[length - 1]) + { + return ""; + } + + return (end == nullptr) ? std::string() : std::string(start, end); +} + +ipmi_ret_t getBlobCount(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + struct BmcBlobCountRx resp; + resp.crc = 0; + resp.blobCount = mgr->buildBlobList(); + + /* Copy the response into the reply buffer */ + std::memcpy(replyCmdBuf, &resp, sizeof(resp)); + (*dataLen) = sizeof(resp); + + return IPMI_CC_OK; +} + +ipmi_ret_t enumerateBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + /* Verify datalen is >= sizeof(request) */ + struct BmcBlobEnumerateTx request; + auto reply = reinterpret_cast(replyCmdBuf); + + std::memcpy(&request, reqBuf, sizeof(request)); + + std::string blobId = mgr->getBlobId(request.blobIdx); + if (blobId == "") + { + return IPMI_CC_INVALID; + } + + /* TODO(venture): Need to do a hard-code check against the maximum + * reply buffer size. */ + reply->crc = 0; + /* Explicilty copies the NUL-terminator. */ + std::memcpy(&reply->blobId, blobId.c_str(), blobId.length() + 1); + + (*dataLen) = sizeof(reply->crc) + blobId.length() + 1; + + return IPMI_CC_OK; +} + +ipmi_ret_t openBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + size_t requestLen = (*dataLen); + auto request = reinterpret_cast(reqBuf); + uint16_t session; + + std::string path = stringFromBuffer( + request->blobId, (requestLen - sizeof(struct BmcBlobOpenTx))); + if (path.empty()) + { + return IPMI_CC_INVALID; + } + + /* Attempt to open. */ + if (!mgr->open(request->flags, path, &session)) + { + return IPMI_CC_INVALID; + } + + struct BmcBlobOpenRx reply; + reply.crc = 0; + reply.sessionId = session; + + std::memcpy(replyCmdBuf, &reply, sizeof(reply)); + (*dataLen) = sizeof(reply); + + return IPMI_CC_OK; +} + +ipmi_ret_t closeBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + struct BmcBlobCloseTx request; + std::memcpy(&request, reqBuf, sizeof(request)); + + /* Attempt to close. */ + if (!mgr->close(request.sessionId)) + { + return IPMI_CC_INVALID; + } + + (*dataLen) = 0; + return IPMI_CC_OK; +} + +ipmi_ret_t deleteBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + size_t requestLen = (*dataLen); + auto request = reinterpret_cast(reqBuf); + + std::string path = stringFromBuffer( + request->blobId, (requestLen - sizeof(struct BmcBlobDeleteTx))); + if (path.empty()) + { + return IPMI_CC_INVALID; + } + + /* Attempt to delete. */ + if (!mgr->deleteBlob(path)) + { + return IPMI_CC_INVALID; + } + + (*dataLen) = 0; + return IPMI_CC_OK; +} + +static ipmi_ret_t returnStatBlob(struct BlobMeta* meta, uint8_t* replyCmdBuf, + size_t* dataLen) +{ + struct BmcBlobStatRx reply; + reply.crc = 0; + reply.blobState = meta->blobState; + reply.size = meta->size; + reply.metadataLen = meta->metadata.size(); + + std::memcpy(replyCmdBuf, &reply, sizeof(reply)); + + /* If there is metadata, copy it over. */ + if (meta->metadata.size()) + { + uint8_t* metadata = &replyCmdBuf[sizeof(reply)]; + std::memcpy(metadata, meta->metadata.data(), reply.metadataLen); + } + + (*dataLen) = sizeof(reply) + reply.metadataLen; + return IPMI_CC_OK; +} + +ipmi_ret_t statBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + size_t requestLen = (*dataLen); + auto request = reinterpret_cast(reqBuf); + + std::string path = stringFromBuffer( + request->blobId, (requestLen - sizeof(struct BmcBlobStatTx))); + if (path.empty()) + { + return IPMI_CC_INVALID; + } + + /* Attempt to stat. */ + struct BlobMeta meta; + if (!mgr->stat(path, &meta)) + { + return IPMI_CC_INVALID; + } + + return returnStatBlob(&meta, replyCmdBuf, dataLen); +} + +ipmi_ret_t sessionStatBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + struct BmcBlobSessionStatTx request; + std::memcpy(&request, reqBuf, sizeof(request)); + + /* Attempt to stat. */ + struct BlobMeta meta; + + if (!mgr->stat(request.sessionId, &meta)) + { + return IPMI_CC_INVALID; + } + + return returnStatBlob(&meta, replyCmdBuf, dataLen); +} + +ipmi_ret_t commitBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + size_t requestLen = (*dataLen); + auto request = reinterpret_cast(reqBuf); + + /* Sanity check the commitDataLen */ + if (request->commitDataLen > (requestLen - sizeof(struct BmcBlobCommitTx))) + { + return IPMI_CC_INVALID; + } + + std::vector data(request->commitDataLen); + std::memcpy(data.data(), request->commitData, request->commitDataLen); + + if (!mgr->commit(request->sessionId, data)) + { + return IPMI_CC_INVALID; + } + + (*dataLen) = 0; + return IPMI_CC_OK; +} + +ipmi_ret_t readBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + struct BmcBlobReadTx request; + std::memcpy(&request, reqBuf, sizeof(request)); + + /* TODO(venture): Verify requestedSize can fit in a returned IPMI packet. + */ + + std::vector result = + mgr->read(request.sessionId, request.offset, request.requestedSize); + + /* If the Read fails, it returns success but with only the crc and 0 bytes + * of data. + * If there was data returned, copy into the reply buffer. + */ + (*dataLen) = sizeof(struct BmcBlobReadRx); + + if (result.size()) + { + uint8_t* output = &replyCmdBuf[sizeof(struct BmcBlobReadRx)]; + std::memcpy(output, result.data(), result.size()); + + (*dataLen) = sizeof(struct BmcBlobReadRx) + result.size(); + } + + return IPMI_CC_OK; +} + +ipmi_ret_t writeBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + size_t requestLen = (*dataLen); + auto request = reinterpret_cast(reqBuf); + + uint32_t size = requestLen - sizeof(struct BmcBlobWriteTx); + std::vector data(size); + + std::memcpy(data.data(), request->data, size); + + /* Attempt to write the bytes. */ + if (!mgr->write(request->sessionId, request->offset, data)) + { + return IPMI_CC_INVALID; + } + + return IPMI_CC_OK; +} + +} // namespace blobs diff --git a/ipmi.hpp b/ipmi.hpp new file mode 100644 index 0000000..c8b6c24 --- /dev/null +++ b/ipmi.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include "manager.hpp" + +#include + +#include + +namespace blobs +{ + +enum BlobOEMCommands +{ + bmcBlobGetCount = 0, + bmcBlobEnumerate = 1, + bmcBlobOpen = 2, + bmcBlobRead = 3, + bmcBlobWrite = 4, + bmcBlobCommit = 5, + bmcBlobClose = 6, + bmcBlobDelete = 7, + bmcBlobStat = 8, + bmcBlobSessionStat = 9, +}; + +/* Used by bmcBlobGetCount */ +struct BmcBlobCountTx +{ + uint8_t cmd; /* bmcBlobGetCount */ +} __attribute__((packed)); + +struct BmcBlobCountRx +{ + uint16_t crc; + uint32_t blobCount; +} __attribute__((packed)); + +/* Used by bmcBlobEnumerate */ +struct BmcBlobEnumerateTx +{ + uint8_t cmd; /* bmcBlobEnumerate */ + uint16_t crc; + uint32_t blobIdx; +} __attribute__((packed)); + +struct BmcBlobEnumerateRx +{ + uint16_t crc; + char blobId[]; +} __attribute__((packed)); + +/* Used by bmcBlobOpen */ +struct BmcBlobOpenTx +{ + uint8_t cmd; /* bmcBlobOpen */ + uint16_t crc; + uint16_t flags; + char blobId[]; /* Must correspond to a valid blob. */ +} __attribute__((packed)); + +struct BmcBlobOpenRx +{ + uint16_t crc; + uint16_t sessionId; +} __attribute__((packed)); + +/* Used by bmcBlobClose */ +struct BmcBlobCloseTx +{ + uint8_t cmd; /* bmcBlobClose */ + uint16_t crc; + uint16_t sessionId; /* Returned from BmcBlobOpen. */ +} __attribute__((packed)); + +/* Used by bmcBlobDelete */ +struct BmcBlobDeleteTx +{ + uint8_t cmd; /* bmcBlobDelete */ + uint16_t crc; + char blobId[]; +} __attribute__((packed)); + +/* Used by bmcBlobStat */ +struct BmcBlobStatTx +{ + uint8_t cmd; /* bmcBlobStat */ + uint16_t crc; + char blobId[]; +} __attribute__((packed)); + +struct BmcBlobStatRx +{ + uint16_t crc; + uint16_t blobState; + uint32_t size; /* Size in bytes of the blob. */ + uint8_t metadataLen; + uint8_t metadata[]; /* Optional blob-specific metadata. */ +} __attribute__((packed)); + +/* Used by bmcBlobSessionStat */ +struct BmcBlobSessionStatTx +{ + uint8_t cmd; /* bmcBlobSessionStat */ + uint16_t crc; + uint16_t sessionId; +} __attribute__((packed)); + +/* Used by bmcBlobCommit */ +struct BmcBlobCommitTx +{ + uint8_t cmd; /* bmcBlobCommit */ + uint16_t crc; + uint16_t sessionId; + uint8_t commitDataLen; + uint8_t commitData[]; /* Optional blob-specific commit data. */ +} __attribute__((packed)); + +/* Used by bmcBlobRead */ +struct BmcBlobReadTx +{ + uint8_t cmd; /* bmcBlobRead */ + uint16_t crc; + uint16_t sessionId; + uint32_t offset; /* The byte sequence start, 0-based. */ + uint32_t requestedSize; /* The number of bytes requested for reading. */ +} __attribute__((packed)); + +struct BmcBlobReadRx +{ + uint16_t crc; + uint8_t data[]; +} __attribute__((packed)); + +/* Used by bmcBlobWrite */ +struct BmcBlobWriteTx +{ + uint8_t cmd; /* bmcBlobWrite */ + uint16_t crc; + uint16_t sessionId; + uint32_t offset; /* The byte sequence start, 0-based. */ + uint8_t data[]; +} __attribute__((packed)); + +/** + * Validate the minimum request length if there is one. + * + * @param[in] subcommand - the command + * @param[in] requestLength - the length of the request + * @return bool - true if valid. + */ +bool validateRequestLength(BlobOEMCommands command, size_t requestLen); + +/** + * Given a pointer into an IPMI request buffer and the length of the remaining + * buffer, builds a string. This does no string validation w.r.t content. + * + * @param[in] start - the start of the expected string. + * @param[in] length - the number of bytes remaining in the buffer. + * @return the string if valid otherwise an empty string. + */ +std::string stringFromBuffer(const char* start, size_t length); + +/** + * Writes out a BmcBlobCountRx structure and returns IPMI_OK. + */ +ipmi_ret_t getBlobCount(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Writes out a BmcBlobEnumerateRx in response to a BmcBlobEnumerateTx + * request. If the index does not correspond to a blob, then this will + * return failure. + * + * It will also return failure if the response buffer is of an invalid + * length. + */ +ipmi_ret_t enumerateBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempts to open the blobId specified and associate with a session id. + */ +ipmi_ret_t openBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempts to close the session specified. + */ +ipmi_ret_t closeBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempts to delete the blobId specified. + */ +ipmi_ret_t deleteBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempts to retrieve the Stat for the blobId specified. + */ +ipmi_ret_t statBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempts to retrieve the Stat for the session specified. + */ +ipmi_ret_t sessionStatBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempts to commit the data in the blob. + */ +ipmi_ret_t commitBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempt to read data from the blob. + */ +ipmi_ret_t readBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Attempt to write data to the blob. + */ +ipmi_ret_t writeBlob(ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); +} // namespace blobs diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..e7c1247 --- /dev/null +++ b/main.cpp @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 "config.h" + +#include "ipmi.hpp" +#include "process.hpp" + +#include + +#include +#include +#include +#include + +/* TODO: Swap out once https://gerrit.openbmc-project.xyz/12743 is merged */ +namespace oem +{ +constexpr auto blobTransferCmd = 128; +} // namespace oem + +namespace blobs +{ + +static std::unique_ptr manager; + +static ipmi_ret_t handleBlobCommand(ipmi_cmd_t cmd, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + /* It's holding at least a sub-command. The OEN is trimmed from the bytes + * before this is called. + */ + if ((*dataLen) < 1) + { + return IPMI_CC_INVALID; + } + + Crc16 crc; + IpmiBlobHandler command = + validateBlobCommand(&crc, reqBuf, replyCmdBuf, dataLen); + if (command == nullptr) + { + return IPMI_CC_INVALID; + } + + return processBlobCommand(command, manager.get(), &crc, reqBuf, replyCmdBuf, + dataLen); +} + +void setupBlobGlobalHandler() __attribute__((constructor)); + +void setupBlobGlobalHandler() +{ + oem::Router* oemRouter = oem::mutableRouter(); + std::fprintf(stderr, + "Registering OEM:[%#08X], Cmd:[%#04X] for Blob Commands\n", + oem::obmcOemNumber, oem::blobTransferCmd); + + oemRouter->registerHandler(oem::obmcOemNumber, oem::blobTransferCmd, + handleBlobCommand); + + manager = std::make_unique(); +} +} // namespace blobs diff --git a/manager.cpp b/manager.cpp new file mode 100644 index 0000000..b6311b4 --- /dev/null +++ b/manager.cpp @@ -0,0 +1,347 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 "manager.hpp" + +#include +#include + +namespace blobs +{ + +void BlobManager::incrementOpen(const std::string& path) +{ + if (path.empty()) + { + return; + } + + openFiles[path] += 1; +} + +void BlobManager::decrementOpen(const std::string& path) +{ + if (path.empty()) + { + return; + } + + /* TODO(venture): Check into the iterator from find, does it makes sense + * to just update it directly? */ + auto entry = openFiles.find(path); + if (entry != openFiles.end()) + { + /* found it, decrement it and remove it if 0. */ + openFiles[path] -= 1; + if (openFiles[path] == 0) + { + openFiles.erase(path); + } + } +} + +int BlobManager::getOpen(const std::string& path) const +{ + /* No need to input check on the read-only call. */ + auto entry = openFiles.find(path); + if (entry != openFiles.end()) + { + return entry->second; + } + + return 0; +} + +bool BlobManager::registerHandler(std::unique_ptr handler) +{ + if (!handler) + { + return false; + } + + handlers.push_back(std::move(handler)); + return true; +} + +uint32_t BlobManager::buildBlobList() +{ + /* Clear out the current list (IPMI handler is presently single-threaded). + */ + ids.clear(); + + /* Grab the list of blobs and extend the local list */ + for (auto& h : handlers) + { + std::vector blobs = h->getBlobIds(); + ids.insert(ids.end(), blobs.begin(), blobs.end()); + } + + return ids.size(); +} + +std::string BlobManager::getBlobId(uint32_t index) +{ + /* Range check. */ + if (index >= ids.size()) + { + return ""; + } + + return ids[index]; +} + +bool BlobManager::open(uint16_t flags, const std::string& path, + uint16_t* session) +{ + GenericBlobInterface* handler = getHandler(path); + + /* No handler found. */ + if (!handler) + { + return false; + } + + /* No sessions availabe... */ + if (!getSession(session)) + { + return false; + } + + /* Verify flags - must be at least read or write */ + if (!(flags & (OpenFlags::read | OpenFlags::write))) + { + /* Neither read not write set, which means calls to Read/Write will + * reject. */ + return false; + } + + if (!handler->open(*session, flags, path)) + { + return false; + } + + /* Associate session with handler */ + sessions[*session] = SessionInfo(path, handler, flags); + incrementOpen(path); + return true; +} + +GenericBlobInterface* BlobManager::getHandler(const std::string& path) +{ + /* Find a handler. */ + GenericBlobInterface* handler = nullptr; + + for (auto& h : handlers) + { + if (h->canHandleBlob(path)) + { + handler = h.get(); + break; + } + } + + return handler; +} + +GenericBlobInterface* BlobManager::getHandler(uint16_t session) +{ + auto item = sessions.find(session); + if (item == sessions.end()) + { + return nullptr; + } + + return item->second.handler; +} + +SessionInfo* BlobManager::getSessionInfo(uint16_t session) +{ + auto item = sessions.find(session); + if (item == sessions.end()) + { + return nullptr; + } + + /* If we go to multi-threaded, this pointer can be invalidated and this + * method will need to change. + */ + return &item->second; +} + +std::string BlobManager::getPath(uint16_t session) const +{ + auto item = sessions.find(session); + if (item == sessions.end()) + { + return ""; + } + + return item->second.blobId; +} + +bool BlobManager::stat(const std::string& path, struct BlobMeta* meta) +{ + /* meta should never be NULL. */ + GenericBlobInterface* handler = getHandler(path); + + /* No handler found. */ + if (!handler) + { + return false; + } + + return handler->stat(path, meta); +} + +bool BlobManager::stat(uint16_t session, struct BlobMeta* meta) +{ + /* meta should never be NULL. */ + GenericBlobInterface* handler = getHandler(session); + + /* No handler found. */ + if (!handler) + { + return false; + } + + return handler->stat(session, meta); +} + +bool BlobManager::commit(uint16_t session, const std::vector& data) +{ + GenericBlobInterface* handler = getHandler(session); + + /* No handler found. */ + if (!handler) + { + return false; + } + + return handler->commit(session, data); +} + +bool BlobManager::close(uint16_t session) +{ + GenericBlobInterface* handler = getHandler(session); + + /* No handler found. */ + if (!handler) + { + return false; + } + + /* Handler returns failure */ + if (!handler->close(session)) + { + return false; + } + + sessions.erase(session); + decrementOpen(getPath(session)); + return true; +} + +std::vector BlobManager::read(uint16_t session, uint32_t offset, + uint32_t requestedSize) +{ + SessionInfo* info = getSessionInfo(session); + + /* No session found. */ + if (!info) + { + return std::vector(); + } + + /* Check flags. */ + if (!(info->flags & OpenFlags::read)) + { + return std::vector(); + } + + /* Try reading from it. */ + return info->handler->read(session, offset, requestedSize); +} + +bool BlobManager::write(uint16_t session, uint32_t offset, + const std::vector& data) +{ + SessionInfo* info = getSessionInfo(session); + + /* No session found. */ + if (!info) + { + return false; + } + + /* Check flags. */ + if (!(info->flags & OpenFlags::write)) + { + return false; + } + + /* Try writing to it. */ + return info->handler->write(session, offset, data); +} + +bool BlobManager::deleteBlob(const std::string& path) +{ + GenericBlobInterface* handler = getHandler(path); + + /* No handler found. */ + if (!handler) + { + return false; + } + + /* Check if the file has any open handles. */ + if (getOpen(path) > 0) + { + return false; + } + + /* Try deleting it. */ + return handler->deleteBlob(path); +} + +bool BlobManager::getSession(uint16_t* sess) +{ + uint16_t tries = 0; + uint16_t lsess; + + if (!sess) + { + return false; + } + + /* This is not meant to fail as you have 64KiB values available. */ + + /* TODO(venture): We could just count the keys in the session map to know + * if it's full. + */ + do + { + lsess = next++; + if (!sessions.count(lsess)) + { + /* value not in use, return it. */ + (*sess) = lsess; + return true; + } + } while (++tries < 0xffff); + + return false; +} +} // namespace blobs diff --git a/manager.hpp b/manager.hpp new file mode 100644 index 0000000..cfa62f5 --- /dev/null +++ b/manager.hpp @@ -0,0 +1,254 @@ +#pragma once + +#include "blobs.hpp" + +#include +#include +#include +#include +#include + +namespace blobs +{ + +struct SessionInfo +{ + SessionInfo() = default; + SessionInfo(const std::string& path, GenericBlobInterface* handler, + uint16_t flags) : + blobId(path), + handler(handler), flags(flags) + { + } + ~SessionInfo() = default; + + std::string blobId; + GenericBlobInterface* handler; + uint16_t flags; +}; + +class ManagerInterface +{ + public: + virtual ~ManagerInterface() = default; + + virtual bool + registerHandler(std::unique_ptr handler) = 0; + + virtual uint32_t buildBlobList() = 0; + + virtual std::string getBlobId(uint32_t index) = 0; + + virtual bool open(uint16_t flags, const std::string& path, + uint16_t* session) = 0; + + virtual bool stat(const std::string& path, struct BlobMeta* meta) = 0; + + virtual bool stat(uint16_t session, struct BlobMeta* meta) = 0; + + virtual bool commit(uint16_t session, const std::vector& data) = 0; + + virtual bool close(uint16_t session) = 0; + + virtual std::vector read(uint16_t session, uint32_t offset, + uint32_t requestedSize) = 0; + + virtual bool write(uint16_t session, uint32_t offset, + const std::vector& data) = 0; + + virtual bool deleteBlob(const std::string& path) = 0; +}; + +/** + * Blob Manager used to store handlers and sessions. + */ +class BlobManager : public ManagerInterface +{ + public: + BlobManager() + { + next = static_cast(std::time(nullptr)); + }; + + ~BlobManager() = default; + /* delete copy constructor & assignment operator, only support move + * operations. + */ + BlobManager(const BlobManager&) = delete; + BlobManager& operator=(const BlobManager&) = delete; + BlobManager(BlobManager&&) = default; + BlobManager& operator=(BlobManager&&) = default; + + /** + * Register a handler. We own the pointer. + * + * @param[in] handler - a pointer to a blob handler. + * @return bool - true if registered. + */ + bool + registerHandler(std::unique_ptr handler) override; + + /** + * Builds the blobId list for enumeration. + * + * @return lowest value returned is 0, otherwise the number of + * blobIds. + */ + uint32_t buildBlobList() override; + + /** + * Grabs the blobId for the indexed blobId. + * + * @param[in] index - the index into the blobId cache. + * @return string - the blobId or empty string on failure. + */ + std::string getBlobId(uint32_t index) override; + + /** + * Attempts to open the file specified and associates with a session. + * + * @param[in] flags - the flags to pass to open. + * @param[in] path - the file path to open. + * @param[in,out] session - pointer to store the session on success. + * @return bool - true if able to open. + */ + bool open(uint16_t flags, const std::string& path, + uint16_t* session) override; + + /** + * Attempts to retrieve a BlobMeta for the specified path. + * + * @param[in] path - the file path for stat(). + * @param[in,out] meta - a pointer to store the metadata. + * @return bool - true if able to retrieve the information. + */ + bool stat(const std::string& path, struct BlobMeta* meta) override; + + /** + * Attempts to retrieve a BlobMeta for a given session. + * + * @param[in] session - the session for this command. + * @param[in,out] meta - a pointer to store the metadata. + * @return bool - true if able to retrieve the information. + */ + bool stat(uint16_t session, struct BlobMeta* meta) override; + + /** + * Attempt to commit a blob for a given session. + * + * @param[in] session - the session for this command. + * @param[in] data - an optional commit blob. + * @return bool - true if the commit succeeds. + */ + bool commit(uint16_t session, const std::vector& data) override; + + /** + * Attempt to close a session. If the handler returns a failure + * in closing, the session is kept open. + * + * @param[in] session - the session for this command. + * @return bool - true if the session was closed. + */ + bool close(uint16_t session) override; + + /** + * Attempt to read bytes from the blob. If there's a failure, such as + * an invalid offset it'll just return 0 bytes. + * + * @param[in] session - the session for this command. + * @param[in] offset - the offset from which to read. + * @param[in] requestedSize - the number of bytes to try and read. + * @return the bytes read. + */ + std::vector read(uint16_t session, uint32_t offset, + uint32_t requestedSize) override; + + /** + * Attempt to write to a blob. The manager does not track whether + * the session opened the file for writing. + * + * @param[in] session - the session for this command. + * @param[in] offset - the offset into the blob to write. + * @param[in] data - the bytes to write to the blob. + * @return bool - true if the write succeeded. + */ + bool write(uint16_t session, uint32_t offset, + const std::vector& data) override; + + /** + * Attempt to delete a blobId. This method will just call the + * handler, which will return failure if the blob doesn't support + * deletion. This command will also fail if there are any open + * sessions against the specific blob. + * + * In the case where they specify a folder, such as /blob/skm where + * the "real" blobIds are /blob/skm/1, or /blob/skm/2, the manager + * may see there are on open sessions to that specific path and will + * call the handler. In this case, the handler is responsible for + * handling any checks or logic. + * + * @param[in] path - the blobId path. + * @return bool - true if delete was successful. + */ + bool deleteBlob(const std::string& path) override; + + /** + * Attempts to return a valid unique session id. + * + * @param[in,out] - pointer to the session. + * @return bool - true if able to allocate. + */ + bool getSession(uint16_t* session); + + /** + * Given a file path will return first handler to answer that it owns + * it. + * + * @param[in] path - the file path. + * @return pointer to the handler or nullptr if not found. + */ + GenericBlobInterface* getHandler(const std::string& path); + + /** + * Given a session id will return associated handler. + * + * @param[in] session - the session. + * @return pointer to the handler or nullptr if not found. + */ + GenericBlobInterface* getHandler(uint16_t session); + + /** + * Given a session id will return associated metadata, including + * the handler and the flags passed into open. + * + * @param[in] session - the session. + * @return pointer to the information or nullptr if not found. + */ + SessionInfo* getSessionInfo(uint16_t session); + + /** + * Given a session id will return associated path. + * + * @param[in] session - the session. + * @return the path or "" on failure. + */ + std::string getPath(uint16_t session) const; + + private: + void incrementOpen(const std::string& path); + void decrementOpen(const std::string& path); + int getOpen(const std::string& path) const; + + /* The next session ID to use */ + uint16_t next; + /* Temporary list of blobIds used for enumeration. */ + std::vector ids; + /* List of Blob handler. */ + std::vector> handlers; + /* Mapping of session ids to blob handlers and the path used with open. + */ + std::unordered_map sessions; + /* Mapping of open blobIds */ + std::unordered_map openFiles; +}; +} // namespace blobs diff --git a/process.cpp b/process.cpp new file mode 100644 index 0000000..595b5c2 --- /dev/null +++ b/process.cpp @@ -0,0 +1,162 @@ +/* + * Copyright 2018 Google Inc. + * + * 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 "process.hpp" + +#include "ipmi.hpp" + +#include +#include + +namespace blobs +{ + +/* Used by all commands with data. */ +struct BmcRx +{ + uint8_t cmd; + uint16_t crc; + uint8_t data; /* one byte minimum of data. */ +} __attribute__((packed)); + +IpmiBlobHandler validateBlobCommand(CrcInterface* crc, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + IpmiBlobHandler cmd; + size_t requestLength = (*dataLen); + /* We know dataLen is at least 1 already */ + auto command = static_cast(reqBuf[0]); + + /* Validate it's at least well-formed. */ + if (!validateRequestLength(command, requestLength)) + { + return nullptr; + } + + /* If there is a payload. */ + if (requestLength > sizeof(uint8_t)) + { + /* Verify the request includes: command, crc16, data */ + if (requestLength < sizeof(struct BmcRx)) + { + return nullptr; + } + + /* We don't include the command byte at offset 0 as part of the crc + * payload area or the crc bytes at the beginning. + */ + size_t requestBodyLen = requestLength - 3; + + /* We start after the command byte. */ + std::vector bytes(requestBodyLen); + + /* It likely has a well-formed payload. */ + struct BmcRx request; + std::memcpy(&request, reqBuf, sizeof(request)); + uint16_t crcValue = request.crc; + + /* Set the in-place CRC to zero. */ + std::memcpy(bytes.data(), &reqBuf[3], requestBodyLen); + + crc->clear(); + crc->compute(bytes.data(), bytes.size()); + + /* Crc expected but didn't match. */ + if (crcValue != crc->get()) + { + return nullptr; + } + } + + /* Grab the corresponding handler for the command (could do map or array + * of function pointer lookup). + */ + switch (command) + { + case BlobOEMCommands::bmcBlobGetCount: + cmd = getBlobCount; + break; + case BlobOEMCommands::bmcBlobEnumerate: + cmd = enumerateBlob; + break; + case BlobOEMCommands::bmcBlobOpen: + cmd = openBlob; + break; + case BlobOEMCommands::bmcBlobRead: + cmd = readBlob; + break; + case BlobOEMCommands::bmcBlobWrite: + cmd = writeBlob; + break; + case BlobOEMCommands::bmcBlobCommit: + cmd = commitBlob; + break; + case BlobOEMCommands::bmcBlobClose: + cmd = closeBlob; + break; + case BlobOEMCommands::bmcBlobDelete: + cmd = deleteBlob; + break; + case BlobOEMCommands::bmcBlobStat: + cmd = statBlob; + break; + case BlobOEMCommands::bmcBlobSessionStat: + cmd = sessionStatBlob; + break; + default: + return nullptr; + } + + return cmd; +} + +ipmi_ret_t processBlobCommand(IpmiBlobHandler cmd, ManagerInterface* mgr, + CrcInterface* crc, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) +{ + ipmi_ret_t result = cmd(mgr, reqBuf, replyCmdBuf, dataLen); + if (result != IPMI_CC_OK) + { + return result; + } + + size_t replyLength = (*dataLen); + + /* The command, whatever it was, returned success. */ + if (replyLength == 0) + { + return result; + } + + /* The response, if it has one byte, has three, to include the crc16. */ + if (replyLength < (sizeof(uint16_t) + 1)) + { + return IPMI_CC_INVALID; + } + + /* The command, whatever it was, replied, so let's set the CRC. */ + crc->clear(); + replyCmdBuf[0] = 0x00; + replyCmdBuf[1] = 0x00; + crc->compute(replyCmdBuf, replyLength); + + /* Copy the CRC into place. */ + uint16_t crcValue = crc->get(); + std::memcpy(replyCmdBuf, &crcValue, sizeof(crcValue)); + + return result; +} +} // namespace blobs diff --git a/process.hpp b/process.hpp new file mode 100644 index 0000000..e8a9906 --- /dev/null +++ b/process.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "crc.hpp" +#include "manager.hpp" + +#include + +#include + +namespace blobs +{ + +using IpmiBlobHandler = + std::function; + +/** + * Validate the IPMI request and determine routing. + * + * @param[in] crc - a pointer to the crc interface. + * @param[in] reqBuf - a pointer to the ipmi request packet buffer. + * @param[in,out] replyCmdBuf - a pointer to the ipmi reply packet buffer. + * @param[in,out] dataLen - initially the request length, set to reply length + * on return. + * @return the ipmi command handler. + */ +IpmiBlobHandler validateBlobCommand(CrcInterface* crc, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); + +/** + * Call the IPMI command and process the result, including running the CRC + * computation for the reply message if there is one. + * + * @param[in] cmd - a funtion pointer to the ipmi command to process. + * @param[in] mgr - a pointer to the manager interface. + * @param[in] crc - a pointer to the crc interface. + * @param[in] reqBuf - a pointer to the ipmi request packet buffer. + * @param[in,out] replyCmdBuf - a pointer to the ipmi reply packet buffer. + * @param[in,out] dataLen - initially the request length, set to reply length + * on return. + * @return the ipmi command result. + */ +ipmi_ret_t processBlobCommand(IpmiBlobHandler cmd, ManagerInterface* mgr, + CrcInterface* crc, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen); +} // namespace blobs diff --git a/test/Makefile.am b/test/Makefile.am new file mode 100644 index 0000000..29586bf --- /dev/null +++ b/test/Makefile.am @@ -0,0 +1,110 @@ +AM_CPPFLAGS = -I$(top_srcdir)/ \ + $(GTEST_CFLAGS) \ + $(GMOCK_CFLAGS) +AM_CXXFLAGS = \ + $(GTEST_MAIN_CFLAGS) +AM_LDFLAGS = \ + $(GMOCK_LIBS) \ + $(GTEST_MAIN_LIBS) \ + $(OESDK_TESTCASE_FLAGS) + +# Run all 'check' test programs +check_PROGRAMS = \ + ipmi_unittest \ + ipmi_getcount_unittest \ + ipmi_enumerate_unittest \ + ipmi_open_unittest \ + ipmi_close_unittest \ + ipmi_delete_unittest \ + ipmi_stat_unittest \ + ipmi_sessionstat_unittest \ + ipmi_commit_unittest \ + ipmi_read_unittest \ + ipmi_write_unittest \ + ipmi_validate_unittest \ + manager_unittest \ + manager_getsession_unittest \ + manager_open_unittest \ + manager_stat_unittest \ + manager_sessionstat_unittest \ + manager_commit_unittest \ + manager_close_unittest \ + manager_delete_unittest \ + manager_write_unittest \ + manager_read_unittest \ + process_unittest \ + crc_unittest +TESTS = $(check_PROGRAMS) + +ipmi_unittest_SOURCES = ipmi_unittest.cpp +ipmi_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_getcount_unittest_SOURCES = ipmi_getcount_unittest.cpp +ipmi_getcount_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_enumerate_unittest_SOURCES = ipmi_enumerate_unittest.cpp +ipmi_enumerate_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_open_unittest_SOURCES = ipmi_open_unittest.cpp +ipmi_open_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_close_unittest_SOURCES = ipmi_close_unittest.cpp +ipmi_close_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_delete_unittest_SOURCES = ipmi_delete_unittest.cpp +ipmi_delete_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_stat_unittest_SOURCES = ipmi_stat_unittest.cpp +ipmi_stat_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_sessionstat_unittest_SOURCES = ipmi_sessionstat_unittest.cpp +ipmi_sessionstat_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_commit_unittest_SOURCES = ipmi_commit_unittest.cpp +ipmi_commit_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_read_unittest_SOURCES = ipmi_read_unittest.cpp +ipmi_read_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_write_unittest_SOURCES = ipmi_write_unittest.cpp +ipmi_write_unittest_LDADD = $(top_builddir)/ipmi.o + +ipmi_validate_unittest_SOURCES = ipmi_validate_unittest.cpp +ipmi_validate_unittest_LDADD = $(top_builddir)/ipmi.o + +manager_unittest_SOURCES = manager_unittest.cpp +manager_unittest_LDADD = $(top_builddir)/manager.o + +manager_getsession_unittest_SOURCES = manager_getsession_unittest.cpp +manager_getsession_unittest_LDADD = $(top_builddir)/manager.o + +manager_open_unittest_SOURCES = manager_open_unittest.cpp +manager_open_unittest_LDADD = $(top_builddir)/manager.o + +manager_stat_unittest_SOURCES = manager_stat_unittest.cpp +manager_stat_unittest_LDADD = $(top_builddir)/manager.o + +manager_sessionstat_unittest_SOURCES = manager_sessionstat_unittest.cpp +manager_sessionstat_unittest_LDADD = $(top_builddir)/manager.o + +manager_commit_unittest_SOURCES = manager_commit_unittest.cpp +manager_commit_unittest_LDADD = $(top_builddir)/manager.o + +manager_close_unittest_SOURCES = manager_close_unittest.cpp +manager_close_unittest_LDADD = $(top_builddir)/manager.o + +manager_delete_unittest_SOURCES = manager_delete_unittest.cpp +manager_delete_unittest_LDADD = $(top_builddir)/manager.o + +manager_write_unittest_SOURCES = manager_write_unittest.cpp +manager_write_unittest_LDADD = $(top_builddir)/manager.o + +manager_read_unittest_SOURCES = manager_read_unittest.cpp +manager_read_unittest_LDADD = $(top_builddir)/manager.o + +process_unittest_SOURCES = process_unittest.cpp +process_unittest_LDADD = $(top_builddir)/process.o $(top_builddir)/ipmi.o \ + $(top_builddir)/crc.o + +crc_unittest_SOURCES = crc_unittest.cpp +crc_unittest_LDADD = $(top_builddir)/crc.o diff --git a/test/blob_mock.hpp b/test/blob_mock.hpp new file mode 100644 index 0000000..6c21c65 --- /dev/null +++ b/test/blob_mock.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "blobs.hpp" + +#include + +namespace blobs +{ + +class BlobMock : public GenericBlobInterface +{ + public: + virtual ~BlobMock() = default; + + MOCK_METHOD1(canHandleBlob, bool(const std::string&)); + MOCK_METHOD0(getBlobIds, std::vector()); + MOCK_METHOD1(deleteBlob, bool(const std::string&)); + MOCK_METHOD2(stat, bool(const std::string&, struct BlobMeta*)); + MOCK_METHOD3(open, bool(uint16_t, uint16_t, const std::string&)); + MOCK_METHOD3(read, std::vector(uint16_t, uint32_t, uint32_t)); + MOCK_METHOD3(write, bool(uint16_t, uint32_t, const std::vector&)); + MOCK_METHOD2(commit, bool(uint16_t, const std::vector&)); + MOCK_METHOD1(close, bool(uint16_t)); + MOCK_METHOD2(stat, bool(uint16_t, struct BlobMeta*)); + MOCK_METHOD1(expire, bool(uint16_t)); +}; +} // namespace blobs diff --git a/test/crc_mock.hpp b/test/crc_mock.hpp new file mode 100644 index 0000000..1562200 --- /dev/null +++ b/test/crc_mock.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "crc.hpp" + +#include + +namespace blobs +{ + +class CrcMock : public CrcInterface +{ + public: + virtual ~CrcMock() = default; + + MOCK_METHOD0(clear, void()); + MOCK_METHOD2(compute, void(const uint8_t*, uint32_t)); + MOCK_CONST_METHOD0(get, uint16_t()); +}; +} // namespace blobs diff --git a/test/crc_unittest.cpp b/test/crc_unittest.cpp new file mode 100644 index 0000000..fb69cb4 --- /dev/null +++ b/test/crc_unittest.cpp @@ -0,0 +1,44 @@ +#include "crc.hpp" + +#include +#include + +#include + +namespace blobs +{ + +TEST(Crc16Test, VerifyCrcValue) +{ + // Verify the crc16 is producing the value we expect. + + // Origin: security/crypta/ipmi/portable/ipmi_utils_test.cc + struct CrcTestVector + { + std::string input; + uint16_t output; + }; + + std::string longString = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAA"; + + std::vector vectors({{"", 0x1D0F}, + {"A", 0x9479}, + {"123456789", 0xE5CC}, + {longString, 0xE938}}); + + Crc16 crc; + + for (const CrcTestVector& testVector : vectors) + { + crc.clear(); + auto data = reinterpret_cast(testVector.input.data()); + crc.compute(data, testVector.input.size()); + EXPECT_EQ(crc.get(), testVector.output); + } +} +} // namespace blobs diff --git a/test/ipmi_close_unittest.cpp b/test/ipmi_close_unittest.cpp new file mode 100644 index 0000000..e34f731 --- /dev/null +++ b/test/ipmi_close_unittest.cpp @@ -0,0 +1,66 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::Invoke; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::StrEq; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobCloseTest, ManagerRejectsCloseReturnsFailure) +{ + // The session manager returned failure to close, which we need to pass on. + + ManagerMock mgr; + uint16_t sessionId = 0x54; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + struct BmcBlobCloseTx req; + + req.cmd = BlobOEMCommands::bmcBlobClose; + req.crc = 0; + req.sessionId = sessionId; + + dataLen = sizeof(req); + + std::memcpy(request, &req, sizeof(req)); + + EXPECT_CALL(mgr, close(sessionId)).WillOnce(Return(false)); + EXPECT_EQ(IPMI_CC_INVALID, closeBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobCloseTest, BlobClosedReturnsSuccess) +{ + // Verify that if all goes right, success is returned. + + ManagerMock mgr; + uint16_t sessionId = 0x54; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + struct BmcBlobCloseTx req; + + req.cmd = BlobOEMCommands::bmcBlobClose; + req.crc = 0; + req.sessionId = sessionId; + + dataLen = sizeof(req); + + std::memcpy(request, &req, sizeof(req)); + + EXPECT_CALL(mgr, close(sessionId)).WillOnce(Return(true)); + EXPECT_EQ(IPMI_CC_OK, closeBlob(&mgr, request, reply, &dataLen)); +} +} // namespace blobs diff --git a/test/ipmi_commit_unittest.cpp b/test/ipmi_commit_unittest.cpp new file mode 100644 index 0000000..1cc47a4 --- /dev/null +++ b/test/ipmi_commit_unittest.cpp @@ -0,0 +1,112 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::ElementsAreArray; +using ::testing::Return; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobCommitTest, InvalidCommitDataLengthReturnsFailure) +{ + // The commit command supports an optional commit blob. This test verifies + // we sanity check the length of that blob. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobCommit; + req->crc = 0; + req->sessionId = 0x54; + req->commitDataLen = + 1; // It's one byte, but that's more than the packet size. + + dataLen = sizeof(struct BmcBlobCommitTx); + + EXPECT_EQ(IPMI_CC_INVALID, commitBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobCommitTest, ValidCommitNoDataHandlerRejectsReturnsFailure) +{ + // The commit packet is valid and the manager's commit call returns failure. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobCommit; + req->crc = 0; + req->sessionId = 0x54; + req->commitDataLen = 0; + + dataLen = sizeof(struct BmcBlobCommitTx); + + EXPECT_CALL(mgr, commit(req->sessionId, _)).WillOnce(Return(false)); + + EXPECT_EQ(IPMI_CC_INVALID, commitBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobCommitTest, ValidCommitNoDataHandlerAcceptsReturnsSuccess) +{ + // Commit called with no data and everything returns success. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobCommit; + req->crc = 0; + req->sessionId = 0x54; + req->commitDataLen = 0; + + dataLen = sizeof(struct BmcBlobCommitTx); + + EXPECT_CALL(mgr, commit(req->sessionId, _)).WillOnce(Return(true)); + + EXPECT_EQ(IPMI_CC_OK, commitBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobCommitTest, ValidCommitWithDataHandlerAcceptsReturnsSuccess) +{ + // Commit called with extra data and everything returns success. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + uint8_t expectedBlob[4] = {0x25, 0x33, 0x45, 0x67}; + + req->cmd = BlobOEMCommands::bmcBlobCommit; + req->crc = 0; + req->sessionId = 0x54; + req->commitDataLen = sizeof(expectedBlob); + std::memcpy(req->commitData, &expectedBlob[0], sizeof(expectedBlob)); + + dataLen = sizeof(struct BmcBlobCommitTx) + sizeof(expectedBlob); + + EXPECT_CALL(mgr, + commit(req->sessionId, + ElementsAreArray(expectedBlob, sizeof(expectedBlob)))) + .WillOnce(Return(true)); + + EXPECT_EQ(IPMI_CC_OK, commitBlob(&mgr, request, reply, &dataLen)); +} +} // namespace blobs diff --git a/test/ipmi_delete_unittest.cpp b/test/ipmi_delete_unittest.cpp new file mode 100644 index 0000000..25fb06b --- /dev/null +++ b/test/ipmi_delete_unittest.cpp @@ -0,0 +1,89 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; +using ::testing::StrEq; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobDeleteTest, InvalidRequestLengthReturnsFailure) +{ + // There is a minimum blobId length of one character, this test verifies + // we check that. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "abc"; + + req->cmd = BlobOEMCommands::bmcBlobDelete; + req->crc = 0; + // length() doesn't include the nul-terminator. + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobDeleteTx) + blobId.length(); + + EXPECT_EQ(IPMI_CC_INVALID, deleteBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobDeleteTest, RequestRejectedReturnsFailure) +{ + // The blobId is rejected for any reason. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobDelete; + req->crc = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobDeleteTx) + blobId.length() + 1; + + EXPECT_CALL(mgr, deleteBlob(StrEq(blobId))).WillOnce(Return(false)); + + EXPECT_EQ(IPMI_CC_INVALID, deleteBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobDeleteTest, BlobDeleteReturnsOk) +{ + // The boring case where the blobId is deleted. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobDelete; + req->crc = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobDeleteTx) + blobId.length() + 1; + + EXPECT_CALL(mgr, deleteBlob(StrEq(blobId))).WillOnce(Return(true)); + + EXPECT_EQ(IPMI_CC_OK, deleteBlob(&mgr, request, reply, &dataLen)); +} +} // namespace blobs diff --git a/test/ipmi_enumerate_unittest.cpp b/test/ipmi_enumerate_unittest.cpp new file mode 100644 index 0000000..232fe7a --- /dev/null +++ b/test/ipmi_enumerate_unittest.cpp @@ -0,0 +1,65 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::Return; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobEnumerateTest, VerifyIfRequestByIdInvalidReturnsFailure) +{ + // This tests to verify that if the index is invalid, it'll return failure. + + ManagerMock mgr; + size_t dataLen; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + struct BmcBlobEnumerateTx req; + uint8_t* request = reinterpret_cast(&req); + + req.cmd = BlobOEMCommands::bmcBlobEnumerate; + req.blobIdx = 0; + dataLen = sizeof(struct BmcBlobEnumerateTx); + + EXPECT_CALL(mgr, getBlobId(req.blobIdx)).WillOnce(Return("")); + + EXPECT_EQ(IPMI_CC_INVALID, enumerateBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobEnumerateTest, BoringRequestByIdAndReceive) +{ + // This tests that if an index into the blob_id cache is valid, the command + // will return the blobId. + + ManagerMock mgr; + size_t dataLen; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + struct BmcBlobEnumerateTx req; + struct BmcBlobEnumerateRx* rep; + uint8_t* request = reinterpret_cast(&req); + std::string blobId = "/asdf"; + + req.cmd = BlobOEMCommands::bmcBlobEnumerate; + req.blobIdx = 0; + dataLen = sizeof(struct BmcBlobEnumerateTx); + + EXPECT_CALL(mgr, getBlobId(req.blobIdx)).WillOnce(Return(blobId)); + + EXPECT_EQ(IPMI_CC_OK, enumerateBlob(&mgr, request, reply, &dataLen)); + + // We're expecting this as a response. + // blobId.length + 1 + sizeof(uint16_t); + EXPECT_EQ(blobId.length() + 1 + sizeof(uint16_t), dataLen); + + rep = reinterpret_cast(reply); + EXPECT_EQ(0, std::memcmp(rep->blobId, blobId.c_str(), blobId.length() + 1)); +} +} // namespace blobs diff --git a/test/ipmi_getcount_unittest.cpp b/test/ipmi_getcount_unittest.cpp new file mode 100644 index 0000000..c6d74e6 --- /dev/null +++ b/test/ipmi_getcount_unittest.cpp @@ -0,0 +1,72 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::Return; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +// the request here is only the subcommand byte and therefore there's no invalid +// length check, etc to handle within the method. + +TEST(BlobCountTest, ReturnsZeroBlobs) +{ + // Calling BmcBlobGetCount if there are no handlers registered should just + // return that there are 0 blobs. + + ManagerMock mgr; + size_t dataLen; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + struct BmcBlobCountTx req; + struct BmcBlobCountRx rep; + uint8_t* request = reinterpret_cast(&req); + + req.cmd = BlobOEMCommands::bmcBlobGetCount; + dataLen = sizeof(req); + + rep.crc = 0; + rep.blobCount = 0; + + EXPECT_CALL(mgr, buildBlobList()).WillOnce(Return(0)); + + EXPECT_EQ(IPMI_CC_OK, getBlobCount(&mgr, request, reply, &dataLen)); + + EXPECT_EQ(sizeof(rep), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); +} + +TEST(BlobCountTest, ReturnsTwoBlobs) +{ + // Calling BmcBlobGetCount with one handler registered that knows of two + // blobs will return that it found two blobs. + + ManagerMock mgr; + size_t dataLen; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + struct BmcBlobCountTx req; + struct BmcBlobCountRx rep; + uint8_t* request = reinterpret_cast(&req); + + req.cmd = BlobOEMCommands::bmcBlobGetCount; + dataLen = sizeof(req); + + rep.crc = 0; + rep.blobCount = 2; + + EXPECT_CALL(mgr, buildBlobList()).WillOnce(Return(2)); + + EXPECT_EQ(IPMI_CC_OK, getBlobCount(&mgr, request, reply, &dataLen)); + + EXPECT_EQ(sizeof(rep), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); +} +} // namespace blobs diff --git a/test/ipmi_open_unittest.cpp b/test/ipmi_open_unittest.cpp new file mode 100644 index 0000000..db2a34f --- /dev/null +++ b/test/ipmi_open_unittest.cpp @@ -0,0 +1,108 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::StrEq; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobOpenTest, InvalidRequestLengthReturnsFailure) +{ + // There is a minimum blobId length of one character, this test verifies + // we check that. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "abc"; + + req->cmd = BlobOEMCommands::bmcBlobOpen; + req->crc = 0; + req->flags = 0; + // length() doesn't include the nul-terminator. + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobOpenTx) + blobId.length(); + + EXPECT_EQ(IPMI_CC_INVALID, openBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobOpenTest, RequestRejectedReturnsFailure) +{ + // The blobId is rejected for any reason. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobOpen; + req->crc = 0; + req->flags = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobOpenTx) + blobId.length() + 1; + + EXPECT_CALL(mgr, open(req->flags, StrEq(blobId), _)) + .WillOnce(Return(false)); + + EXPECT_EQ(IPMI_CC_INVALID, openBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobOpenTest, BlobOpenReturnsOk) +{ + // The boring case where the blobId opens. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + struct BmcBlobOpenRx rep; + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobOpen; + req->crc = 0; + req->flags = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobOpenTx) + blobId.length() + 1; + uint16_t returnedSession = 0x54; + + EXPECT_CALL(mgr, open(req->flags, StrEq(blobId), NotNull())) + .WillOnce(Invoke( + [&](uint16_t flags, const std::string& path, uint16_t* session) { + (*session) = returnedSession; + return true; + })); + + EXPECT_EQ(IPMI_CC_OK, openBlob(&mgr, request, reply, &dataLen)); + + rep.crc = 0; + rep.sessionId = returnedSession; + + EXPECT_EQ(sizeof(rep), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); +} +} // namespace blobs diff --git a/test/ipmi_read_unittest.cpp b/test/ipmi_read_unittest.cpp new file mode 100644 index 0000000..b6dab55 --- /dev/null +++ b/test/ipmi_read_unittest.cpp @@ -0,0 +1,78 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::Return; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobReadTest, ManagerReturnsNoData) +{ + // Verify that if no data is returned the IPMI command reply has no + // payload. The manager, in all failures, will just return 0 bytes. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobRead; + req->crc = 0; + req->sessionId = 0x54; + req->offset = 0x100; + req->requestedSize = 0x10; + + dataLen = sizeof(struct BmcBlobReadTx); + + std::vector data; + + EXPECT_CALL(mgr, read(req->sessionId, req->offset, req->requestedSize)) + .WillOnce(Return(data)); + + EXPECT_EQ(IPMI_CC_OK, readBlob(&mgr, request, reply, &dataLen)); + EXPECT_EQ(sizeof(struct BmcBlobReadRx), dataLen); +} + +TEST(BlobReadTest, ManagerReturnsData) +{ + // Verify that if data is returned, it's placed in the expected location. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobRead; + req->crc = 0; + req->sessionId = 0x54; + req->offset = 0x100; + req->requestedSize = 0x10; + + dataLen = sizeof(struct BmcBlobReadTx); + + std::vector data = {0x02, 0x03, 0x05, 0x06}; + + EXPECT_CALL(mgr, read(req->sessionId, req->offset, req->requestedSize)) + .WillOnce(Return(data)); + + EXPECT_EQ(IPMI_CC_OK, readBlob(&mgr, request, reply, &dataLen)); + EXPECT_EQ(sizeof(struct BmcBlobReadRx) + data.size(), dataLen); + EXPECT_EQ(0, std::memcmp(&reply[sizeof(struct BmcBlobReadRx)], data.data(), + data.size())); +} + +/* TODO(venture): We need a test that handles other checks such as if the size + * requested won't fit into a packet response. + */ +} // namespace blobs diff --git a/test/ipmi_sessionstat_unittest.cpp b/test/ipmi_sessionstat_unittest.cpp new file mode 100644 index 0000000..e1e1aad --- /dev/null +++ b/test/ipmi_sessionstat_unittest.cpp @@ -0,0 +1,121 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::Matcher; +using ::testing::NotNull; +using ::testing::Return; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobSessionStatTest, RequestRejectedByManagerReturnsFailure) +{ + // If the session ID is invalid, the request must fail. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + req->cmd = BlobOEMCommands::bmcBlobSessionStat; + req->crc = 0; + req->sessionId = 0x54; + + dataLen = sizeof(struct BmcBlobSessionStatTx); + + EXPECT_CALL(mgr, stat(Matcher(req->sessionId), + Matcher(_))) + .WillOnce(Return(false)); + + EXPECT_EQ(IPMI_CC_INVALID, sessionStatBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobSessionStatTest, RequestSucceedsNoMetadata) +{ + // Stat request succeeeds but there were no metadata bytes. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + req->cmd = BlobOEMCommands::bmcBlobSessionStat; + req->crc = 0; + req->sessionId = 0x54; + + dataLen = sizeof(struct BmcBlobSessionStatTx); + + struct BmcBlobStatRx rep; + rep.crc = 0x00; + rep.blobState = 0x01; + rep.size = 0x100; + rep.metadataLen = 0x00; + + EXPECT_CALL(mgr, stat(Matcher(req->sessionId), + Matcher(NotNull()))) + .WillOnce(Invoke([&](uint16_t session, struct BlobMeta* meta) { + meta->blobState = rep.blobState; + meta->size = rep.size; + return true; + })); + + EXPECT_EQ(IPMI_CC_OK, sessionStatBlob(&mgr, request, reply, &dataLen)); + + EXPECT_EQ(sizeof(rep), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); +} + +TEST(BlobSessionStatTest, RequestSucceedsWithMetadata) +{ + // Stat request succeeds and there were metadata bytes. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + req->cmd = BlobOEMCommands::bmcBlobSessionStat; + req->crc = 0; + req->sessionId = 0x54; + + dataLen = sizeof(struct BmcBlobSessionStatTx); + + struct BlobMeta lmeta; + lmeta.blobState = 0x01; + lmeta.size = 0x100; + lmeta.metadata.push_back(0x01); + lmeta.metadata.push_back(0x02); + lmeta.metadata.push_back(0x03); + lmeta.metadata.push_back(0x04); + + struct BmcBlobStatRx rep; + rep.crc = 0x00; + rep.blobState = lmeta.blobState; + rep.size = lmeta.size; + rep.metadataLen = lmeta.metadata.size(); + + EXPECT_CALL(mgr, stat(Matcher(req->sessionId), + Matcher(NotNull()))) + .WillOnce(Invoke([&](uint16_t session, struct BlobMeta* meta) { + (*meta) = lmeta; + return true; + })); + + EXPECT_EQ(IPMI_CC_OK, sessionStatBlob(&mgr, request, reply, &dataLen)); + + EXPECT_EQ(sizeof(rep) + lmeta.metadata.size(), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); + EXPECT_EQ(0, std::memcmp(reply + sizeof(rep), lmeta.metadata.data(), + lmeta.metadata.size())); +} +} // namespace blobs diff --git a/test/ipmi_stat_unittest.cpp b/test/ipmi_stat_unittest.cpp new file mode 100644 index 0000000..a6f1dfe --- /dev/null +++ b/test/ipmi_stat_unittest.cpp @@ -0,0 +1,157 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::Matcher; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::StrEq; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobStatTest, InvalidRequestLengthReturnsFailure) +{ + // There is a minimum blobId length of one character, this test verifies + // we check that. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "abc"; + + req->cmd = BlobOEMCommands::bmcBlobStat; + req->crc = 0; + // length() doesn't include the nul-terminator. + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobStatTx) + blobId.length(); + + EXPECT_EQ(IPMI_CC_INVALID, statBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobStatTest, RequestRejectedReturnsFailure) +{ + // The blobId is rejected for any reason. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobStat; + req->crc = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobStatTx) + blobId.length() + 1; + + EXPECT_CALL(mgr, stat(Matcher(StrEq(blobId)), + Matcher(_))) + .WillOnce(Return(false)); + + EXPECT_EQ(IPMI_CC_INVALID, statBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobStatTest, RequestSucceedsNoMetadata) +{ + // Stat request succeeeds but there were no metadata bytes. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobStat; + req->crc = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobStatTx) + blobId.length() + 1; + + struct BmcBlobStatRx rep; + rep.crc = 0x00; + rep.blobState = 0x01; + rep.size = 0x100; + rep.metadataLen = 0x00; + + EXPECT_CALL(mgr, stat(Matcher(StrEq(blobId)), + Matcher(NotNull()))) + .WillOnce(Invoke([&](const std::string& path, struct BlobMeta* meta) { + meta->blobState = rep.blobState; + meta->size = rep.size; + return true; + })); + + EXPECT_EQ(IPMI_CC_OK, statBlob(&mgr, request, reply, &dataLen)); + + EXPECT_EQ(sizeof(rep), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); +} + +TEST(BlobStatTest, RequestSucceedsWithMetadata) +{ + // Stat request succeeds and there were metadata bytes. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + std::string blobId = "a"; + + req->cmd = BlobOEMCommands::bmcBlobStat; + req->crc = 0; + // length() doesn't include the nul-terminator, request buff is initialized + // to 0s + std::memcpy(req->blobId, blobId.c_str(), blobId.length()); + + dataLen = sizeof(struct BmcBlobStatTx) + blobId.length() + 1; + + struct BlobMeta lmeta; + lmeta.blobState = 0x01; + lmeta.size = 0x100; + lmeta.metadata.push_back(0x01); + lmeta.metadata.push_back(0x02); + lmeta.metadata.push_back(0x03); + lmeta.metadata.push_back(0x04); + + struct BmcBlobStatRx rep; + rep.crc = 0x00; + rep.blobState = lmeta.blobState; + rep.size = lmeta.size; + rep.metadataLen = lmeta.metadata.size(); + + EXPECT_CALL(mgr, stat(Matcher(StrEq(blobId)), + Matcher(NotNull()))) + .WillOnce(Invoke([&](const std::string& path, struct BlobMeta* meta) { + (*meta) = lmeta; + return true; + })); + + EXPECT_EQ(IPMI_CC_OK, statBlob(&mgr, request, reply, &dataLen)); + + EXPECT_EQ(sizeof(rep) + lmeta.metadata.size(), dataLen); + EXPECT_EQ(0, std::memcmp(reply, &rep, sizeof(rep))); + EXPECT_EQ(0, std::memcmp(reply + sizeof(rep), lmeta.metadata.data(), + lmeta.metadata.size())); +} +} // namespace blobs diff --git a/test/ipmi_unittest.cpp b/test/ipmi_unittest.cpp new file mode 100644 index 0000000..8f27ed7 --- /dev/null +++ b/test/ipmi_unittest.cpp @@ -0,0 +1,60 @@ +#include "ipmi.hpp" + +#include + +#include + +namespace blobs +{ + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(StringInputTest, NullPointerInput) +{ + // The method should verify it did receive a non-null input pointer. + + EXPECT_STREQ("", stringFromBuffer(NULL, 5).c_str()); +} + +TEST(StringInputTest, ZeroBytesInput) +{ + // Verify that if the input length is 0 that it'll return the empty string. + + const char* request = "asdf"; + EXPECT_STREQ("", stringFromBuffer(request, 0).c_str()); +} + +TEST(StringInputTest, NulTerminatorNotFound) +{ + // Verify that if there isn't a nul-terminator found in an otherwise valid + // string, it'll return the emptry string. + + char request[MAX_IPMI_BUFFER]; + std::memset(request, 'a', sizeof(request)); + EXPECT_STREQ("", stringFromBuffer(request, sizeof(request)).c_str()); +} + +TEST(StringInputTest, TwoNulsFound) +{ + // Verify it makes you use the entire data region for the string. + char request[MAX_IPMI_BUFFER]; + request[0] = 'a'; + request[1] = 0; + std::memset(&request[2], 'b', sizeof(request) - 2); + request[MAX_IPMI_BUFFER - 1] = 0; + + // This case has two strings, and the last character is a nul-terminator. + EXPECT_STREQ("", stringFromBuffer(request, sizeof(request)).c_str()); +} + +TEST(StringInputTest, NulTerminatorFound) +{ + // Verify that if it's provided a valid nul-terminated string, it'll + // return it. + + const char* request = "asdf"; + EXPECT_STREQ("asdf", stringFromBuffer(request, 5).c_str()); +} +} // namespace blobs diff --git a/test/ipmi_validate_unittest.cpp b/test/ipmi_validate_unittest.cpp new file mode 100644 index 0000000..6bf4200 --- /dev/null +++ b/test/ipmi_validate_unittest.cpp @@ -0,0 +1,44 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include + +#include + +namespace blobs +{ + +TEST(IpmiValidateTest, VerifyCommandMinimumLengths) +{ + + struct TestCase + { + BlobOEMCommands cmd; + size_t len; + bool expect; + }; + + std::vector tests = { + {BlobOEMCommands::bmcBlobClose, sizeof(struct BmcBlobCloseTx) - 1, + false}, + {BlobOEMCommands::bmcBlobCommit, sizeof(struct BmcBlobCommitTx) - 1, + false}, + {BlobOEMCommands::bmcBlobDelete, sizeof(struct BmcBlobDeleteTx) + 1, + false}, + {BlobOEMCommands::bmcBlobEnumerate, + sizeof(struct BmcBlobEnumerateTx) - 1, false}, + {BlobOEMCommands::bmcBlobOpen, sizeof(struct BmcBlobOpenTx) + 1, false}, + {BlobOEMCommands::bmcBlobRead, sizeof(struct BmcBlobReadTx) - 1, false}, + {BlobOEMCommands::bmcBlobSessionStat, + sizeof(struct BmcBlobSessionStatTx) - 1, false}, + {BlobOEMCommands::bmcBlobStat, sizeof(struct BmcBlobStatTx) + 1, false}, + {BlobOEMCommands::bmcBlobWrite, sizeof(struct BmcBlobWriteTx), false}, + }; + + for (const auto& test : tests) + { + bool result = validateRequestLength(test.cmd, test.len); + EXPECT_EQ(result, test.expect); + } +} +} // namespace blobs diff --git a/test/ipmi_write_unittest.cpp b/test/ipmi_write_unittest.cpp new file mode 100644 index 0000000..55a1e3b --- /dev/null +++ b/test/ipmi_write_unittest.cpp @@ -0,0 +1,73 @@ +#include "ipmi.hpp" +#include "manager_mock.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::ElementsAreArray; +using ::testing::Return; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +TEST(BlobWriteTest, ManagerReturnsFailureReturnsFailure) +{ + // This verifies a failure from the manager is passed back. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobWrite; + req->crc = 0; + req->sessionId = 0x54; + req->offset = 0x100; + + uint8_t expectedBytes[2] = {0x66, 0x67}; + std::memcpy(req->data, &expectedBytes[0], sizeof(expectedBytes)); + + dataLen = sizeof(struct BmcBlobWriteTx) + sizeof(expectedBytes); + + EXPECT_CALL(mgr, + write(req->sessionId, req->offset, + ElementsAreArray(expectedBytes, sizeof(expectedBytes)))) + .WillOnce(Return(false)); + + EXPECT_EQ(IPMI_CC_INVALID, writeBlob(&mgr, request, reply, &dataLen)); +} + +TEST(BlobWriteTest, ManagerReturnsTrueWriteSucceeds) +{ + // The case where everything works. + + ManagerMock mgr; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + auto req = reinterpret_cast(request); + + req->cmd = BlobOEMCommands::bmcBlobWrite; + req->crc = 0; + req->sessionId = 0x54; + req->offset = 0x100; + + uint8_t expectedBytes[2] = {0x66, 0x67}; + std::memcpy(req->data, &expectedBytes[0], sizeof(expectedBytes)); + + dataLen = sizeof(struct BmcBlobWriteTx) + sizeof(expectedBytes); + + EXPECT_CALL(mgr, + write(req->sessionId, req->offset, + ElementsAreArray(expectedBytes, sizeof(expectedBytes)))) + .WillOnce(Return(true)); + + EXPECT_EQ(IPMI_CC_OK, writeBlob(&mgr, request, reply, &dataLen)); +} +} // namespace blobs diff --git a/test/manager_close_unittest.cpp b/test/manager_close_unittest.cpp new file mode 100644 index 0000000..47c9264 --- /dev/null +++ b/test/manager_close_unittest.cpp @@ -0,0 +1,66 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; + +TEST(ManagerCloseTest, CloseNoSessionReturnsFalse) +{ + // Calling Close on a session that doesn't exist should return false. + + BlobManager mgr; + uint16_t sess = 1; + + EXPECT_FALSE(mgr.close(sess)); +} + +TEST(ManagerCloseTest, CloseSessionFoundButHandlerReturnsFalse) +{ + // The handler was found but it returned failure. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + EXPECT_CALL(*m1ptr, close(sess)).WillOnce(Return(false)); + + EXPECT_FALSE(mgr.close(sess)); + + // TODO(venture): The session wasn't closed, need to verify. Could call + // public GetHandler method. +} + +TEST(ManagerCloseTest, CloseSessionFoundAndHandlerReturnsSuccess) +{ + // The handler was found and returned success. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + EXPECT_CALL(*m1ptr, close(sess)).WillOnce(Return(true)); + + EXPECT_TRUE(mgr.close(sess)); +} +} // namespace blobs diff --git a/test/manager_commit_unittest.cpp b/test/manager_commit_unittest.cpp new file mode 100644 index 0000000..b1b3c8c --- /dev/null +++ b/test/manager_commit_unittest.cpp @@ -0,0 +1,68 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; + +TEST(ManagerCommitTest, CommitNoSessionReturnsFalse) +{ + // Calling Commit on a session that doesn't exist should return false. + + BlobManager mgr; + uint16_t sess = 1; + std::vector data; + + EXPECT_FALSE(mgr.commit(sess, data)); +} + +TEST(ManagerCommitTest, CommitSessionFoundButHandlerReturnsFalse) +{ + // The handler was found but it returned failure. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::write, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + std::vector data; + EXPECT_CALL(*m1ptr, commit(sess, data)).WillOnce(Return(false)); + + EXPECT_FALSE(mgr.commit(sess, data)); +} + +TEST(ManagerCommitTest, CommitSessionFoundAndHandlerReturnsSuccess) +{ + // The handler was found and returned success. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::write, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + std::vector data; + EXPECT_CALL(*m1ptr, commit(sess, data)).WillOnce(Return(true)); + + EXPECT_TRUE(mgr.commit(sess, data)); +} +} // namespace blobs diff --git a/test/manager_delete_unittest.cpp b/test/manager_delete_unittest.cpp new file mode 100644 index 0000000..9ad3afd --- /dev/null +++ b/test/manager_delete_unittest.cpp @@ -0,0 +1,87 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; + +TEST(ManagerDeleteTest, FileIsOpenReturnsFailure) +{ + // The blob manager maintains a naive list of open files and will + // return failure if you try to delete an open file. + + // Open the file. + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillRepeatedly(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + // Try to delete the file. + EXPECT_FALSE(mgr.deleteBlob(path)); +} + +TEST(ManagerDeleteTest, FileHasNoHandler) +{ + // The blob manager cannot find any handler. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(false)); + + // Try to delete the file. + EXPECT_FALSE(mgr.deleteBlob(path)); +} + +TEST(ManagerDeleteTest, FileIsNotOpenButHandlerDeleteFails) +{ + // The Blob manager finds the handler but the handler returns failure + // on delete. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, deleteBlob(path)).WillOnce(Return(false)); + + // Try to delete the file. + EXPECT_FALSE(mgr.deleteBlob(path)); +} + +TEST(ManagerDeleteTest, FileIsNotOpenAndHandlerSucceeds) +{ + // The Blob manager finds the handler and the handler returns success. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, deleteBlob(path)).WillOnce(Return(true)); + + // Try to delete the file. + EXPECT_TRUE(mgr.deleteBlob(path)); +} +} // namespace blobs diff --git a/test/manager_getsession_unittest.cpp b/test/manager_getsession_unittest.cpp new file mode 100644 index 0000000..e66729a --- /dev/null +++ b/test/manager_getsession_unittest.cpp @@ -0,0 +1,24 @@ +#include "manager.hpp" + +#include + +namespace blobs +{ + +TEST(ManagerGetSessionTest, NextSessionReturned) +{ + // This test verifies the next session ID is returned. + BlobManager mgr; + + uint16_t first, second; + EXPECT_TRUE(mgr.getSession(&first)); + EXPECT_TRUE(mgr.getSession(&second)); + EXPECT_FALSE(first == second); +} + +TEST(ManagerGetSessionTest, SessionsCheckedAgainstList) +{ + // TODO(venture): Need a test that verifies the session ids are checked + // against open sessions. +} +} // namespace blobs diff --git a/test/manager_mock.hpp b/test/manager_mock.hpp new file mode 100644 index 0000000..41979ac --- /dev/null +++ b/test/manager_mock.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "blobs.hpp" +#include "manager.hpp" + +#include +#include + +#include + +namespace blobs +{ + +class ManagerMock : public ManagerInterface +{ + public: + virtual ~ManagerMock() = default; + + MOCK_METHOD1(registerHandler, bool(std::unique_ptr)); + MOCK_METHOD0(buildBlobList, uint32_t()); + MOCK_METHOD1(getBlobId, std::string(uint32_t)); + MOCK_METHOD3(open, bool(uint16_t, const std::string&, uint16_t*)); + MOCK_METHOD2(stat, bool(const std::string&, struct BlobMeta*)); + MOCK_METHOD2(stat, bool(uint16_t, struct BlobMeta*)); + MOCK_METHOD2(commit, bool(uint16_t, const std::vector&)); + MOCK_METHOD1(close, bool(uint16_t)); + MOCK_METHOD3(read, std::vector(uint16_t, uint32_t, uint32_t)); + MOCK_METHOD3(write, bool(uint16_t, uint32_t, const std::vector&)); + MOCK_METHOD1(deleteBlob, bool(const std::string&)); +}; +} // namespace blobs diff --git a/test/manager_open_unittest.cpp b/test/manager_open_unittest.cpp new file mode 100644 index 0000000..309d3f6 --- /dev/null +++ b/test/manager_open_unittest.cpp @@ -0,0 +1,85 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; + +TEST(ManagerOpenTest, OpenButNoHandler) +{ + // No handler claims to be able to open the file. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(false)); + EXPECT_FALSE(mgr.open(flags, path, &sess)); +} + +TEST(ManagerOpenTest, OpenButHandlerFailsOpen) +{ + // The handler is found but Open fails. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(false)); + EXPECT_FALSE(mgr.open(flags, path, &sess)); +} + +TEST(ManagerOpenTest, OpenFailsMustSupplyAtLeastReadOrWriteFlag) +{ + // One must supply either read or write in the flags for the session to + // open. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = 0, sess; + std::string path = "/asdf/asdf"; + + /* It checks if someone can handle the blob before it checks the flags. */ + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + + EXPECT_FALSE(mgr.open(flags, path, &sess)); +} + +TEST(ManagerOpenTest, OpenSucceeds) +{ + // The handler is found and Open succeeds. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + // TODO(venture): Need a way to verify the session is associated with it, + // maybe just call Read() or SessionStat() +} +} // namespace blobs diff --git a/test/manager_read_unittest.cpp b/test/manager_read_unittest.cpp new file mode 100644 index 0000000..1d40f5d --- /dev/null +++ b/test/manager_read_unittest.cpp @@ -0,0 +1,78 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; + +TEST(ManagerReadTest, ReadNoSessionReturnsFalse) +{ + // Calling Read on a session that doesn't exist should return false. + + BlobManager mgr; + uint16_t sess = 1; + uint32_t ofs = 0x54; + uint32_t requested = 0x100; + + std::vector result = mgr.read(sess, ofs, requested); + EXPECT_EQ(0, result.size()); +} + +TEST(ManagerReadTest, ReadFromWriteOnlyFails) +{ + // The session manager will not route a Read call to a blob if the session + // was opened as write-only. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t sess = 1; + uint32_t ofs = 0x54; + uint32_t requested = 0x100; + uint16_t flags = OpenFlags::write; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + std::vector result = mgr.read(sess, ofs, requested); + EXPECT_EQ(0, result.size()); +} + +TEST(ManagerReadTest, ReadFromHandlerReturnsData) +{ + // There is no logic in this as it's just as a pass-thru command, however + // we want to verify this behavior doesn't change. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t sess = 1; + uint32_t ofs = 0x54; + uint32_t requested = 0x100; + uint16_t flags = OpenFlags::read; + std::string path = "/asdf/asdf"; + std::vector data = {0x12, 0x14, 0x15, 0x16}; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + EXPECT_CALL(*m1ptr, read(sess, ofs, requested)).WillOnce(Return(data)); + + std::vector result = mgr.read(sess, ofs, requested); + EXPECT_EQ(data.size(), result.size()); + EXPECT_EQ(result, data); +} +} // namespace blobs diff --git a/test/manager_sessionstat_unittest.cpp b/test/manager_sessionstat_unittest.cpp new file mode 100644 index 0000000..6ae27d3 --- /dev/null +++ b/test/manager_sessionstat_unittest.cpp @@ -0,0 +1,66 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Return; + +TEST(ManagerSessionStatTest, StatNoSessionReturnsFalse) +{ + // Calling Stat on a session that doesn't exist should return false. + + BlobManager mgr; + struct BlobMeta meta; + uint16_t sess = 1; + + EXPECT_FALSE(mgr.stat(sess, &meta)); +} + +TEST(ManagerSessionStatTest, StatSessionFoundButHandlerReturnsFalse) +{ + // The handler was found but it returned failure. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + struct BlobMeta meta; + EXPECT_CALL(*m1ptr, stat(sess, &meta)).WillOnce(Return(false)); + + EXPECT_FALSE(mgr.stat(sess, &meta)); +} + +TEST(ManagerSessionStatTest, StatSessionFoundAndHandlerReturnsSuccess) +{ + // The handler was found and returned success. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + struct BlobMeta meta; + EXPECT_CALL(*m1ptr, stat(sess, &meta)).WillOnce(Return(true)); + + EXPECT_TRUE(mgr.stat(sess, &meta)); +} +} // namespace blobs diff --git a/test/manager_stat_unittest.cpp b/test/manager_stat_unittest.cpp new file mode 100644 index 0000000..a13a66d --- /dev/null +++ b/test/manager_stat_unittest.cpp @@ -0,0 +1,60 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +namespace blobs +{ + +using ::testing::Return; + +TEST(ManagerStatTest, StatNoHandler) +{ + // There is no handler for this path. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + struct BlobMeta meta; + std::string path = "/asdf/asdf"; + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(false)); + + EXPECT_FALSE(mgr.stat(path, &meta)); +} + +TEST(ManagerStatTest, StatHandlerFoundButFails) +{ + // There is a handler for this path but Stat fails. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + struct BlobMeta meta; + std::string path = "/asdf/asdf"; + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, stat(path, &meta)).WillOnce(Return(false)); + + EXPECT_FALSE(mgr.stat(path, &meta)); +} + +TEST(ManagerStatTest, StatHandlerFoundAndSucceeds) +{ + // There is a handler and Stat succeeds. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + struct BlobMeta meta; + std::string path = "/asdf/asdf"; + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, stat(path, &meta)).WillOnce(Return(true)); + + EXPECT_TRUE(mgr.stat(path, &meta)); +} +} // namespace blobs diff --git a/test/manager_unittest.cpp b/test/manager_unittest.cpp new file mode 100644 index 0000000..7d4d49e --- /dev/null +++ b/test/manager_unittest.cpp @@ -0,0 +1,172 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include +#include +#include + +#include + +namespace blobs +{ + +using ::testing::Return; + +TEST(BlobsTest, RegisterNullPointerFails) +{ + // The only invalid pointer really is a null one. + + BlobManager mgr; + EXPECT_FALSE(mgr.registerHandler(nullptr)); +} + +TEST(BlobsTest, RegisterNonNullPointerPasses) +{ + // Test that the valid pointer is boringly registered. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); +} + +TEST(BlobsTest, GetCountNoBlobsRegistered) +{ + // Request the Blob Count when there are no blobs. + + BlobManager mgr; + EXPECT_EQ(0, mgr.buildBlobList()); +} + +TEST(BlobsTest, GetCountBlobRegisteredReturnsOne) +{ + // Request the blob count and verify the list is of length one. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + std::vector v = {"item"}; + + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + // We expect it to ask for the list. + EXPECT_CALL(*m1ptr, getBlobIds()).WillOnce(Return(v)); + + EXPECT_EQ(1, mgr.buildBlobList()); +} + +TEST(BlobsTest, GetCountBlobsRegisteredEachReturnsOne) +{ + // Request the blob count and verify the list is of length two. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + std::unique_ptr m2 = std::make_unique(); + auto m1ptr = m1.get(); + auto m2ptr = m2.get(); + std::vector v1, v2; + + v1.push_back("asdf"); + v2.push_back("ghjk"); + + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + EXPECT_TRUE(mgr.registerHandler(std::move(m2))); + + // We expect it to ask for the list. + EXPECT_CALL(*m1ptr, getBlobIds()).WillOnce(Return(v1)); + EXPECT_CALL(*m2ptr, getBlobIds()).WillOnce(Return(v2)); + + EXPECT_EQ(2, mgr.buildBlobList()); +} + +TEST(BlobsTest, EnumerateBlobZerothEntry) +{ + // Validate that you can read back the 0th blobId. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + std::unique_ptr m2 = std::make_unique(); + auto m1ptr = m1.get(); + auto m2ptr = m2.get(); + std::vector v1, v2; + + v1.push_back("asdf"); + v2.push_back("ghjk"); + + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + EXPECT_TRUE(mgr.registerHandler(std::move(m2))); + + // We expect it to ask for the list. + EXPECT_CALL(*m1ptr, getBlobIds()).WillOnce(Return(v1)); + EXPECT_CALL(*m2ptr, getBlobIds()).WillOnce(Return(v2)); + + EXPECT_EQ(2, mgr.buildBlobList()); + + std::string result = mgr.getBlobId(0); + // The exact order the blobIds is returned is not guaranteed to never + // change. + EXPECT_TRUE("asdf" == result || "ghjk" == result); +} + +TEST(BlobsTest, EnumerateBlobFirstEntry) +{ + // Validate you can read back the two real entries. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + std::unique_ptr m2 = std::make_unique(); + auto m1ptr = m1.get(); + auto m2ptr = m2.get(); + std::vector v1, v2; + + v1.push_back("asdf"); + v2.push_back("ghjk"); + + // Presently the list of blobs is read and appended in a specific order, + // but I don't want to rely on that. + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + EXPECT_TRUE(mgr.registerHandler(std::move(m2))); + + // We expect it to ask for the list. + EXPECT_CALL(*m1ptr, getBlobIds()).WillOnce(Return(v1)); + EXPECT_CALL(*m2ptr, getBlobIds()).WillOnce(Return(v2)); + + EXPECT_EQ(2, mgr.buildBlobList()); + + // Try to grab the two blobIds and verify they're in the list. + std::vector results; + results.push_back(mgr.getBlobId(0)); + results.push_back(mgr.getBlobId(1)); + EXPECT_EQ(2, results.size()); + EXPECT_TRUE(std::find(results.begin(), results.end(), "asdf") != + results.end()); + EXPECT_TRUE(std::find(results.begin(), results.end(), "ghjk") != + results.end()); +} + +TEST(BlobTest, EnumerateBlobInvalidEntry) +{ + // Validate trying to read an invalid entry fails expectedly. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + std::unique_ptr m2 = std::make_unique(); + auto m1ptr = m1.get(); + auto m2ptr = m2.get(); + std::vector v1, v2; + + v1.push_back("asdf"); + v2.push_back("ghjk"); + + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + EXPECT_TRUE(mgr.registerHandler(std::move(m2))); + + // We expect it to ask for the list. + EXPECT_CALL(*m1ptr, getBlobIds()).WillOnce(Return(v1)); + EXPECT_CALL(*m2ptr, getBlobIds()).WillOnce(Return(v2)); + + EXPECT_EQ(2, mgr.buildBlobList()); + + // Grabs the third entry which isn't valid. + EXPECT_STREQ("", mgr.getBlobId(2).c_str()); +} +} // namespace blobs diff --git a/test/manager_write_unittest.cpp b/test/manager_write_unittest.cpp new file mode 100644 index 0000000..33c6d5a --- /dev/null +++ b/test/manager_write_unittest.cpp @@ -0,0 +1,90 @@ +#include "blob_mock.hpp" +#include "manager.hpp" + +#include + +using ::testing::_; +using ::testing::Return; + +namespace blobs +{ + +TEST(ManagerWriteTest, WriteNoSessionReturnsFalse) +{ + // Calling Write on a session that doesn't exist should return false. + + BlobManager mgr; + uint16_t sess = 1; + uint32_t ofs = 0x54; + std::vector data = {0x11, 0x22}; + + EXPECT_FALSE(mgr.write(sess, ofs, data)); +} + +TEST(ManagerWriteTest, WriteSessionFoundButHandlerReturnsFalse) +{ + // The handler was found but it returned failure. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::write, sess; + std::string path = "/asdf/asdf"; + uint32_t ofs = 0x54; + std::vector data = {0x11, 0x22}; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + EXPECT_CALL(*m1ptr, write(sess, ofs, data)).WillOnce(Return(false)); + + EXPECT_FALSE(mgr.write(sess, ofs, data)); +} + +TEST(ManagerWriteTest, WriteFailsBecauseFileOpenedReadOnly) +{ + // The manager will not route a write call to a file opened read-only. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::read, sess; + std::string path = "/asdf/asdf"; + uint32_t ofs = 0x54; + std::vector data = {0x11, 0x22}; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + EXPECT_FALSE(mgr.write(sess, ofs, data)); +} + +TEST(ManagerWriteTest, WriteSessionFoundAndHandlerReturnsSuccess) +{ + // The handler was found and returned success. + + BlobManager mgr; + std::unique_ptr m1 = std::make_unique(); + auto m1ptr = m1.get(); + EXPECT_TRUE(mgr.registerHandler(std::move(m1))); + + uint16_t flags = OpenFlags::write, sess; + std::string path = "/asdf/asdf"; + uint32_t ofs = 0x54; + std::vector data = {0x11, 0x22}; + + EXPECT_CALL(*m1ptr, canHandleBlob(path)).WillOnce(Return(true)); + EXPECT_CALL(*m1ptr, open(_, flags, path)).WillOnce(Return(true)); + EXPECT_TRUE(mgr.open(flags, path, &sess)); + + EXPECT_CALL(*m1ptr, write(sess, ofs, data)).WillOnce(Return(true)); + + EXPECT_TRUE(mgr.write(sess, ofs, data)); +} +} // namespace blobs diff --git a/test/process_unittest.cpp b/test/process_unittest.cpp new file mode 100644 index 0000000..2ffb023 --- /dev/null +++ b/test/process_unittest.cpp @@ -0,0 +1,290 @@ +#include "crc.hpp" +#include "crc_mock.hpp" +#include "ipmi.hpp" +#include "manager_mock.hpp" +#include "process.hpp" + +#include + +#include + +namespace blobs +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::Return; +using ::testing::StrictMock; + +// ipmid.hpp isn't installed where we can grab it and this value is per BMC +// SoC. +#define MAX_IPMI_BUFFER 64 + +namespace +{ + +void EqualFunctions(IpmiBlobHandler lhs, IpmiBlobHandler rhs) +{ + EXPECT_FALSE(lhs == nullptr); + EXPECT_FALSE(rhs == nullptr); + + ipmi_ret_t (*const* lPtr)(ManagerInterface*, const uint8_t*, uint8_t*, + size_t*) = + lhs.target(); + + ipmi_ret_t (*const* rPtr)(ManagerInterface*, const uint8_t*, uint8_t*, + size_t*) = + rhs.target(); + + EXPECT_TRUE(lPtr); + EXPECT_TRUE(rPtr); + EXPECT_EQ(*lPtr, *rPtr); + return; +} + +} // namespace + +TEST(ValidateBlobCommandTest, InvalidCommandReturnsFailure) +{ + // Verify we handle an invalid command. + + StrictMock crc; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + request[0] = 0xff; // There is no command 0xff. + dataLen = sizeof(uint8_t); // There is no payload for CRC. + + EXPECT_EQ(nullptr, validateBlobCommand(&crc, request, reply, &dataLen)); +} + +TEST(ValidateBlobCommandTest, ValidCommandWithoutPayload) +{ + // Verify we handle a valid command that doesn't have a payload. + + StrictMock crc; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + request[0] = BlobOEMCommands::bmcBlobGetCount; + dataLen = sizeof(uint8_t); // There is no payload for CRC. + + IpmiBlobHandler res = validateBlobCommand(&crc, request, reply, &dataLen); + EXPECT_FALSE(res == nullptr); + EqualFunctions(getBlobCount, res); +} + +TEST(ValidateBlobCommandTest, WithPayloadMinimumLengthIs3VerifyChecks) +{ + // Verify that if there's a payload, it's at least one command byte and + // two bytes for the crc16 and then one data byte. + + StrictMock crc; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + request[0] = BlobOEMCommands::bmcBlobGetCount; + dataLen = sizeof(uint8_t) + sizeof(uint16_t); + // There is a payload, but there are insufficient bytes. + + EXPECT_EQ(nullptr, validateBlobCommand(&crc, request, reply, &dataLen)); +} + +TEST(ValidateBlobCommandTest, WithPayloadAndInvalidCrc) +{ + // Verify that the CRC is checked, and failure is reported. + + StrictMock crc; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + auto req = reinterpret_cast(request); + req->cmd = BlobOEMCommands::bmcBlobWrite; + req->crc = 0x34; + req->sessionId = 0x54; + req->offset = 0x100; + + uint8_t expectedBytes[2] = {0x66, 0x67}; + std::memcpy(req->data, &expectedBytes[0], sizeof(expectedBytes)); + + dataLen = sizeof(struct BmcBlobWriteTx) + sizeof(expectedBytes); + + // skip over cmd and crc. + size_t expectedLen = dataLen - 3; + + EXPECT_CALL(crc, clear()); + EXPECT_CALL(crc, compute(_, expectedLen)) + .WillOnce(Invoke([&](const uint8_t* bytes, uint32_t length) { + EXPECT_EQ(0, std::memcmp(&request[3], bytes, length)); + })); + EXPECT_CALL(crc, get()).WillOnce(Return(0x1234)); + + EXPECT_EQ(nullptr, validateBlobCommand(&crc, request, reply, &dataLen)); +} + +TEST(ValidateBlobCommandTest, WithPayloadAndValidCrc) +{ + // Verify the CRC is checked and if it matches, return the handler. + + StrictMock crc; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + auto req = reinterpret_cast(request); + req->cmd = BlobOEMCommands::bmcBlobWrite; + req->crc = 0x3412; + req->sessionId = 0x54; + req->offset = 0x100; + + uint8_t expectedBytes[2] = {0x66, 0x67}; + std::memcpy(req->data, &expectedBytes[0], sizeof(expectedBytes)); + + dataLen = sizeof(struct BmcBlobWriteTx) + sizeof(expectedBytes); + + // skip over cmd and crc. + size_t expectedLen = dataLen - 3; + + EXPECT_CALL(crc, clear()); + EXPECT_CALL(crc, compute(_, expectedLen)) + .WillOnce(Invoke([&](const uint8_t* bytes, uint32_t length) { + EXPECT_EQ(0, std::memcmp(&request[3], bytes, length)); + })); + EXPECT_CALL(crc, get()).WillOnce(Return(0x3412)); + + IpmiBlobHandler res = validateBlobCommand(&crc, request, reply, &dataLen); + EXPECT_FALSE(res == nullptr); + EqualFunctions(writeBlob, res); +} + +TEST(ValidateBlobCommandTest, InputIntegrationTest) +{ + // Given a request buffer generated by the host-side utility, verify it is + // properly routed. + + Crc16 crc; + size_t dataLen; + uint8_t request[] = {0x02, 0x88, 0x21, 0x03, 0x00, 0x2f, 0x64, 0x65, 0x76, + 0x2f, 0x68, 0x61, 0x76, 0x65, 0x6e, 0x2f, 0x63, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x74, 0x68, 0x72, 0x75, 0x00}; + + // The above request to open a file for reading & writing named: + // "/dev/haven/command_passthru" + + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + dataLen = sizeof(request); + + IpmiBlobHandler res = validateBlobCommand(&crc, request, reply, &dataLen); + EXPECT_FALSE(res == nullptr); + EqualFunctions(openBlob, res); +} + +TEST(ProcessBlobCommandTest, CommandReturnsNotOk) +{ + // Verify that if the IPMI command handler returns not OK that this is + // noticed and returned. + + StrictMock crc; + StrictMock manager; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + IpmiBlobHandler h = [](ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, + size_t* dataLen) { return IPMI_CC_INVALID; }; + + dataLen = sizeof(request); + + EXPECT_EQ(IPMI_CC_INVALID, + processBlobCommand(h, &manager, &crc, request, reply, &dataLen)); +} + +TEST(ProcessBlobCommandTest, CommandReturnsOkWithNoPayload) +{ + // Verify that if the IPMI command handler returns OK but without a payload + // it doesn't try to compute a CRC. + + StrictMock crc; + StrictMock manager; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + IpmiBlobHandler h = [](ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) { + (*dataLen) = 0; + return IPMI_CC_OK; + }; + + dataLen = sizeof(request); + + EXPECT_EQ(IPMI_CC_OK, + processBlobCommand(h, &manager, &crc, request, reply, &dataLen)); +} + +TEST(ProcessBlobCommandTest, CommandReturnsOkWithInvalidPayloadLength) +{ + // There is a minimum payload length of 3 bytes, this command returns a + // payload of 2 bytes. + + StrictMock crc; + StrictMock manager; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + + IpmiBlobHandler h = [](ManagerInterface* mgr, const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) { + (*dataLen) = sizeof(uint16_t); + return IPMI_CC_OK; + }; + + dataLen = sizeof(request); + + EXPECT_EQ(IPMI_CC_INVALID, + processBlobCommand(h, &manager, &crc, request, reply, &dataLen)); +} + +TEST(ProcessBlobCommandTest, CommandReturnsOkWithValidPayloadLength) +{ + // There is a minimum payload length of 3 bytes, this command returns a + // payload of 3 bytes and the crc code is called to process the payload. + + StrictMock crc; + StrictMock manager; + size_t dataLen; + uint8_t request[MAX_IPMI_BUFFER] = {0}; + uint8_t reply[MAX_IPMI_BUFFER] = {0}; + uint32_t payloadLen = sizeof(uint16_t) + sizeof(uint8_t); + + IpmiBlobHandler h = [payloadLen](ManagerInterface* mgr, + const uint8_t* reqBuf, + uint8_t* replyCmdBuf, size_t* dataLen) { + (*dataLen) = payloadLen; + replyCmdBuf[2] = 0x56; + return IPMI_CC_OK; + }; + + dataLen = sizeof(request); + + EXPECT_CALL(crc, clear()); + EXPECT_CALL(crc, compute(_, payloadLen)); + EXPECT_CALL(crc, get()).WillOnce(Return(0x3412)); + + EXPECT_EQ(IPMI_CC_OK, + processBlobCommand(h, &manager, &crc, request, reply, &dataLen)); + EXPECT_EQ(dataLen, payloadLen); + + uint8_t expectedBytes[3] = {0x12, 0x34, 0x56}; + EXPECT_EQ(0, std::memcmp(expectedBytes, reply, sizeof(expectedBytes))); +} +} // namespace blobs -- cgit v1.2.3