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 <venture@google.com>
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 <EMAIL_USERNAME@DOMAIN> <IRC_USERNAME!>
+    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 <EMAIL_USERNAME@DOMAIN> <IRC_USERNAME!>;
+        if omitted from an entry, assume one of the maintainers from the
+        MAINTAINERS entry.
+    R:  Denotes reviewer; has fields NAME <EMAIL_USERNAME@DOMAIN> <IRC_USERNAME!>;
+        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 <venture@google.com> <venture!>
+M:  Kun Yi <kunyi731@gmail.com> <kunyi!>
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 <string>
+#include <vector>
+
+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<uint8_t> 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<std::string> 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<uint8_t> 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<uint8_t>& 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<uint8_t>& 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 <cstdint>
+
+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 <cstring>
+#include <string>
+#include <unordered_map>
+
+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<BlobOEMCommands, size_t> 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<const char*>(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<struct BmcBlobEnumerateRx*>(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<const struct BmcBlobOpenTx*>(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<const struct BmcBlobDeleteTx*>(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<const struct BmcBlobStatTx*>(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<const struct BmcBlobCommitTx*>(reqBuf);
+
+    /* Sanity check the commitDataLen */
+    if (request->commitDataLen > (requestLen - sizeof(struct BmcBlobCommitTx)))
+    {
+        return IPMI_CC_INVALID;
+    }
+
+    std::vector<uint8_t> 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<uint8_t> 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<const struct BmcBlobWriteTx*>(reqBuf);
+
+    uint32_t size = requestLen - sizeof(struct BmcBlobWriteTx);
+    std::vector<uint8_t> 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 <host-ipmid/ipmid-api.h>
+
+#include <string>
+
+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 <host-ipmid/ipmid-api.h>
+
+#include <cstdio>
+#include <host-ipmid/iana.hpp>
+#include <host-ipmid/oemrouter.hpp>
+#include <memory>
+
+/* 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<BlobManager> 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<BlobManager>();
+}
+} // 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 <string>
+#include <vector>
+
+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<GenericBlobInterface> 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<std::string> 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<uint8_t>& 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<uint8_t> BlobManager::read(uint16_t session, uint32_t offset,
+                                       uint32_t requestedSize)
+{
+    SessionInfo* info = getSessionInfo(session);
+
+    /* No session found. */
+    if (!info)
+    {
+        return std::vector<uint8_t>();
+    }
+
+    /* Check flags. */
+    if (!(info->flags & OpenFlags::read))
+    {
+        return std::vector<uint8_t>();
+    }
+
+    /* Try reading from it. */
+    return info->handler->read(session, offset, requestedSize);
+}
+
+bool BlobManager::write(uint16_t session, uint32_t offset,
+                        const std::vector<uint8_t>& 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 <ctime>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+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<GenericBlobInterface> 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<uint8_t>& data) = 0;
+
+    virtual bool close(uint16_t session) = 0;
+
+    virtual std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+                                      uint32_t requestedSize) = 0;
+
+    virtual bool write(uint16_t session, uint32_t offset,
+                       const std::vector<uint8_t>& 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<uint16_t>(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<GenericBlobInterface> 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<uint8_t>& 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<uint8_t> 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<uint8_t>& 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<std::string> ids;
+    /* List of Blob handler. */
+    std::vector<std::unique_ptr<GenericBlobInterface>> handlers;
+    /* Mapping of session ids to blob handlers and the path used with open.
+     */
+    std::unordered_map<uint16_t, SessionInfo> sessions;
+    /* Mapping of open blobIds */
+    std::unordered_map<std::string, int> 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 <cstring>
+#include <vector>
+
+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<BlobOEMCommands>(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<uint8_t> 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 <host-ipmid/ipmid-api.h>
+
+#include <functional>
+
+namespace blobs
+{
+
+using IpmiBlobHandler =
+    std::function<ipmi_ret_t(ManagerInterface* mgr, const uint8_t* reqBuf,
+                             uint8_t* replyCmdBuf, size_t* dataLen)>;
+
+/**
+ * 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 <gmock/gmock.h>
+
+namespace blobs
+{
+
+class BlobMock : public GenericBlobInterface
+{
+  public:
+    virtual ~BlobMock() = default;
+
+    MOCK_METHOD1(canHandleBlob, bool(const std::string&));
+    MOCK_METHOD0(getBlobIds, std::vector<std::string>());
+    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<uint8_t>(uint16_t, uint32_t, uint32_t));
+    MOCK_METHOD3(write, bool(uint16_t, uint32_t, const std::vector<uint8_t>&));
+    MOCK_METHOD2(commit, bool(uint16_t, const std::vector<uint8_t>&));
+    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 <gmock/gmock.h>
+
+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 <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+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<CrcTestVector> vectors({{"", 0x1D0F},
+                                        {"A", 0x9479},
+                                        {"123456789", 0xE5CC},
+                                        {longString, 0xE938}});
+
+    Crc16 crc;
+
+    for (const CrcTestVector& testVector : vectors)
+    {
+        crc.clear();
+        auto data = reinterpret_cast<const uint8_t*>(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 <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+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 <cstring>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobCommitTx*>(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<struct BmcBlobCommitTx*>(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<struct BmcBlobCommitTx*>(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<struct BmcBlobCommitTx*>(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 <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobDeleteTx*>(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<struct BmcBlobDeleteTx*>(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<struct BmcBlobDeleteTx*>(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 <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+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<uint8_t*>(&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<uint8_t*>(&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<struct BmcBlobEnumerateRx*>(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 <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+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<uint8_t*>(&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<uint8_t*>(&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 <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobOpenTx*>(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<struct BmcBlobOpenTx*>(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<struct BmcBlobOpenTx*>(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 <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobReadTx*>(request);
+
+    req->cmd = BlobOEMCommands::bmcBlobRead;
+    req->crc = 0;
+    req->sessionId = 0x54;
+    req->offset = 0x100;
+    req->requestedSize = 0x10;
+
+    dataLen = sizeof(struct BmcBlobReadTx);
+
+    std::vector<uint8_t> 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<struct BmcBlobReadTx*>(request);
+
+    req->cmd = BlobOEMCommands::bmcBlobRead;
+    req->crc = 0;
+    req->sessionId = 0x54;
+    req->offset = 0x100;
+    req->requestedSize = 0x10;
+
+    dataLen = sizeof(struct BmcBlobReadTx);
+
+    std::vector<uint8_t> 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 <cstring>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobSessionStatTx*>(request);
+    req->cmd = BlobOEMCommands::bmcBlobSessionStat;
+    req->crc = 0;
+    req->sessionId = 0x54;
+
+    dataLen = sizeof(struct BmcBlobSessionStatTx);
+
+    EXPECT_CALL(mgr, stat(Matcher<uint16_t>(req->sessionId),
+                          Matcher<struct BlobMeta*>(_)))
+        .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<struct BmcBlobSessionStatTx*>(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<uint16_t>(req->sessionId),
+                          Matcher<struct BlobMeta*>(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<struct BmcBlobSessionStatTx*>(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<uint16_t>(req->sessionId),
+                          Matcher<struct BlobMeta*>(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 <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobStatTx*>(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<struct BmcBlobStatTx*>(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<const std::string&>(StrEq(blobId)),
+                          Matcher<struct BlobMeta*>(_)))
+        .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<struct BmcBlobStatTx*>(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<const std::string&>(StrEq(blobId)),
+                          Matcher<struct BlobMeta*>(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<struct BmcBlobStatTx*>(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<const std::string&>(StrEq(blobId)),
+                          Matcher<struct BlobMeta*>(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 <cstring>
+
+#include <gtest/gtest.h>
+
+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 <vector>
+
+#include <gtest/gtest.h>
+
+namespace blobs
+{
+
+TEST(IpmiValidateTest, VerifyCommandMinimumLengths)
+{
+
+    struct TestCase
+    {
+        BlobOEMCommands cmd;
+        size_t len;
+        bool expect;
+    };
+
+    std::vector<TestCase> 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 <cstring>
+
+#include <gtest/gtest.h>
+
+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<struct BmcBlobWriteTx*>(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<struct BmcBlobWriteTx*>(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 <gtest/gtest.h>
+
+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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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 <vector>
+
+#include <gtest/gtest.h>
+
+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<uint8_t> data;
+
+    EXPECT_FALSE(mgr.commit(sess, data));
+}
+
+TEST(ManagerCommitTest, CommitSessionFoundButHandlerReturnsFalse)
+{
+    // The handler was found but it returned failure.
+
+    BlobManager mgr;
+    std::unique_ptr<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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 <gtest/gtest.h>
+
+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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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 <gtest/gtest.h>
+
+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 <memory>
+#include <string>
+
+#include <gmock/gmock.h>
+
+namespace blobs
+{
+
+class ManagerMock : public ManagerInterface
+{
+  public:
+    virtual ~ManagerMock() = default;
+
+    MOCK_METHOD1(registerHandler, bool(std::unique_ptr<GenericBlobInterface>));
+    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<uint8_t>&));
+    MOCK_METHOD1(close, bool(uint16_t));
+    MOCK_METHOD3(read, std::vector<uint8_t>(uint16_t, uint32_t, uint32_t));
+    MOCK_METHOD3(write, bool(uint16_t, uint32_t, const std::vector<uint8_t>&));
+    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 <string>
+
+#include <gtest/gtest.h>
+
+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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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 <vector>
+
+#include <gtest/gtest.h>
+
+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<uint8_t> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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<uint8_t> 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 <gtest/gtest.h>
+
+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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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 <gtest/gtest.h>
+
+namespace blobs
+{
+
+using ::testing::Return;
+
+TEST(ManagerStatTest, StatNoHandler)
+{
+    // There is no handler for this path.
+
+    BlobManager mgr;
+    std::unique_ptr<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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 <algorithm>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<BlobMock> m1 = std::make_unique<BlobMock>();
+    auto m1ptr = m1.get();
+    std::vector<std::string> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    std::unique_ptr<BlobMock> m2 = std::make_unique<BlobMock>();
+    auto m1ptr = m1.get();
+    auto m2ptr = m2.get();
+    std::vector<std::string> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    std::unique_ptr<BlobMock> m2 = std::make_unique<BlobMock>();
+    auto m1ptr = m1.get();
+    auto m2ptr = m2.get();
+    std::vector<std::string> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    std::unique_ptr<BlobMock> m2 = std::make_unique<BlobMock>();
+    auto m1ptr = m1.get();
+    auto m2ptr = m2.get();
+    std::vector<std::string> 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<std::string> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    std::unique_ptr<BlobMock> m2 = std::make_unique<BlobMock>();
+    auto m1ptr = m1.get();
+    auto m2ptr = m2.get();
+    std::vector<std::string> 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 <gtest/gtest.h>
+
+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<uint8_t> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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<BlobMock> m1 = std::make_unique<BlobMock>();
+    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<uint8_t> 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 <cstring>
+
+#include <gtest/gtest.h>
+
+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 (*)(ManagerInterface*, const uint8_t*, uint8_t*,
+                                  size_t*)>();
+
+    ipmi_ret_t (*const* rPtr)(ManagerInterface*, const uint8_t*, uint8_t*,
+                              size_t*) =
+        rhs.target<ipmi_ret_t (*)(ManagerInterface*, const uint8_t*, uint8_t*,
+                                  size_t*)>();
+
+    EXPECT_TRUE(lPtr);
+    EXPECT_TRUE(rPtr);
+    EXPECT_EQ(*lPtr, *rPtr);
+    return;
+}
+
+} // namespace
+
+TEST(ValidateBlobCommandTest, InvalidCommandReturnsFailure)
+{
+    // Verify we handle an invalid command.
+
+    StrictMock<CrcMock> 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<CrcMock> 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<CrcMock> 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<CrcMock> crc;
+    size_t dataLen;
+    uint8_t request[MAX_IPMI_BUFFER] = {0};
+    uint8_t reply[MAX_IPMI_BUFFER] = {0};
+
+    auto req = reinterpret_cast<struct BmcBlobWriteTx*>(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<CrcMock> crc;
+    size_t dataLen;
+    uint8_t request[MAX_IPMI_BUFFER] = {0};
+    uint8_t reply[MAX_IPMI_BUFFER] = {0};
+
+    auto req = reinterpret_cast<struct BmcBlobWriteTx*>(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<CrcMock> crc;
+    StrictMock<ManagerMock> 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<CrcMock> crc;
+    StrictMock<ManagerMock> 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<CrcMock> crc;
+    StrictMock<ManagerMock> 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<CrcMock> crc;
+    StrictMock<ManagerMock> 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