Implement stat

Implement both session based and path based stat, and properly
set commit state to dirty when new content has not been committed.

Resolves: openbmc/phosphor-ipmi-blobs-binarystore#3
Tested: unit tests pass

Signed-off-by: Kun Yi <kunyi731@gmail.com>
Change-Id: I7c833cf4c7dcb0089db778caa60fc92edceb984e
diff --git a/binarystore.cpp b/binarystore.cpp
index b7c88a3..5b0e299 100644
--- a/binarystore.cpp
+++ b/binarystore.cpp
@@ -171,6 +171,7 @@
     currentBlob_ = blob_.add_blobs();
     currentBlob_->set_blob_id(blobId);
 
+    commitState_ = CommitState::Dirty;
     log<level::NOTICE>("Created new blob", entry("BLOB_ID=%s", blobId.c_str()));
     return true;
 }
@@ -231,6 +232,7 @@
         return false;
     }
 
+    commitState_ = CommitState::Dirty;
     /* Copy (overwrite) the data */
     if (offset + data.size() > dataPtr->size())
     {
@@ -273,9 +275,58 @@
     return true;
 }
 
-bool BinaryStore::stat()
+/*
+ * Sets |meta| with size and state of the blob. Returns |blobState| with
+ * standard definition from phosphor-ipmi-blobs header blob.hpp, plus OEM
+ * flag bits BinaryStore::CommitState.
+
+enum StateFlags
 {
-    return false;
+    open_read = (1 << 0),
+    open_write = (1 << 1),
+    committing = (1 << 2),
+    committed = (1 << 3),
+    commit_error = (1 << 4),
+};
+
+enum CommitState
+{
+    Dirty = (1 << 8), // In-memory data might not match persisted data
+    Clean = (1 << 9), // In-memory data matches persisted data
+    Uninitialized = (1 << 10), // Cannot find persisted data
+    CommitError = (1 << 11)    // Error happened during committing
+};
+
+*/
+bool BinaryStore::stat(blobs::BlobMeta* meta)
+{
+    uint16_t blobState = blobs::StateFlags::open_read;
+    if (writable_)
+    {
+        blobState |= blobs::StateFlags::open_write;
+    }
+
+    if (commitState_ == CommitState::Clean)
+    {
+        blobState |= blobs::StateFlags::committed;
+    }
+    else if (commitState_ == CommitState::CommitError)
+    {
+        blobState |= blobs::StateFlags::commit_error;
+    }
+    blobState |= commitState_;
+
+    if (currentBlob_)
+    {
+        meta->size = currentBlob_->data().size();
+    }
+    else
+    {
+        meta->size = 0;
+    }
+    meta->blobState = blobState;
+
+    return true;
 }
 
 } // namespace binstore
diff --git a/binarystore.hpp b/binarystore.hpp
index 3a25581..9f95539 100644
--- a/binarystore.hpp
+++ b/binarystore.hpp
@@ -4,6 +4,7 @@
 
 #include <unistd.h>
 
+#include <blobs-ipmid/blobs.hpp>
 #include <cstdint>
 #include <memory>
 #include <string>
@@ -87,9 +88,11 @@
     virtual bool close() = 0;
 
     /**
-     * TODO
+     * Returns blob stat flags.
+     * @param meta: output stat flags.
+     * @returns True if able to get the stat flags and write to *meta
      */
-    virtual bool stat() = 0;
+    virtual bool stat(blobs::BlobMeta* meta) = 0;
 };
 
 /**
@@ -100,6 +103,17 @@
 class BinaryStore : public BinaryStoreInterface
 {
   public:
+    /* |CommitState| differs slightly with |StateFlags| in blob.hpp,
+     * and thus is defined in the OEM space (bit 8 - 15). User can call stat()
+     * to query the |CommitState| of the blob path. */
+    enum CommitState
+    {
+        Dirty = (1 << 8), // In-memory data might not match persisted data
+        Clean = (1 << 9), // In-memory data matches persisted data
+        Uninitialized = (1 << 10), // Cannot find persisted data
+        CommitError = (1 << 11)    // Error happened during committing
+    };
+
     BinaryStore() = delete;
     BinaryStore(const std::string& baseBlobId, std::unique_ptr<SysFile> file,
                 uint32_t maxSize) :
@@ -124,7 +138,7 @@
     bool write(uint32_t offset, const std::vector<uint8_t>& data) override;
     bool commit() override;
     bool close() override;
-    bool stat() override;
+    bool stat(blobs::BlobMeta* meta) override;
 
     /**
      * Helper factory method to create a BinaryStore instance
@@ -140,14 +154,6 @@
                          std::unique_ptr<SysFile> file, uint32_t maxSize);
 
   private:
-    enum class CommitState
-    {
-        Dirty,         // In-memory data might not match persisted data
-        Clean,         // In-memory data matches persisted data
-        Uninitialized, // Cannot find persisted data
-        CommitError    // Error happened during committing
-    };
-
     /* Load the serialized data from sysfile if commit state is dirty.
      * Returns False if encountered error when loading */
     bool loadSerializedData();
diff --git a/binarystore_mock.hpp b/binarystore_mock.hpp
index 3c7e6c8..9fa83e6 100644
--- a/binarystore_mock.hpp
+++ b/binarystore_mock.hpp
@@ -33,6 +33,8 @@
             .WillByDefault(Invoke(&real_store_, &BinaryStore::write));
         ON_CALL(*this, commit)
             .WillByDefault(Invoke(&real_store_, &BinaryStore::commit));
+        ON_CALL(*this, stat)
+            .WillByDefault(Invoke(&real_store_, &BinaryStore::stat));
     }
     MOCK_CONST_METHOD0(getBaseBlobId, std::string());
     MOCK_CONST_METHOD0(getBlobIds, std::vector<std::string>());
@@ -42,7 +44,7 @@
     MOCK_METHOD2(write, bool(uint32_t, const std::vector<uint8_t>&));
     MOCK_METHOD0(commit, bool());
     MOCK_METHOD0(close, bool());
-    MOCK_METHOD0(stat, bool());
+    MOCK_METHOD1(stat, bool(blobs::BlobMeta* meta));
 
   private:
     BinaryStore real_store_;
diff --git a/handler.cpp b/handler.cpp
index 0556633..66500f4 100644
--- a/handler.cpp
+++ b/handler.cpp
@@ -81,8 +81,13 @@
 bool BinaryStoreBlobHandler::stat(const std::string& path,
                                   struct BlobMeta* meta)
 {
-    // TODO: implement
-    return false;
+    auto it = stores_.find(internal::getBaseFromId(path));
+    if (it == stores_.end())
+    {
+        return false;
+    }
+
+    return it->second->stat(meta);
 }
 
 bool BinaryStoreBlobHandler::open(uint16_t session, uint16_t flags,
@@ -179,8 +184,13 @@
 
 bool BinaryStoreBlobHandler::stat(uint16_t session, struct BlobMeta* meta)
 {
-    // TODO: implement
-    return false;
+    auto it = sessions_.find(session);
+    if (it == sessions_.end())
+    {
+        return false;
+    }
+
+    return it->second->stat(meta);
 }
 
 bool BinaryStoreBlobHandler::expire(uint16_t session)
diff --git a/test/Makefile.am b/test/Makefile.am
index 3d53916..08ef779 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -16,6 +16,7 @@
 	parse_config_unittest \
 	sys_file_unittest \
 	handler_commit_unittest \
+	handler_stat_unittest \
 	handler_open_unittest \
 	handler_readwrite_unittest \
 	handler_unittest
@@ -44,6 +45,15 @@
 	-lprotobuf
 handler_commit_unittest_CXXFLAGS = $(PHOSPHOR_LOGGING_CFLAGS)
 
+handler_stat_unittest_SOURCES = handler_stat_unittest.cpp
+handler_stat_unittest_LDADD = $(PHOSPHOR_LOGGING_LIBS) \
+	$(top_builddir)/handler.o \
+	$(top_builddir)/binarystore.o \
+	$(top_builddir)/sys_file.o \
+	$(top_builddir)/libbinarystore_la-binaryblob.pb.o \
+	-lprotobuf
+handler_stat_unittest_CXXFLAGS = $(PHOSPHOR_LOGGING_CFLAGS)
+
 handler_open_unittest_SOURCES = handler_open_unittest.cpp
 handler_open_unittest_LDADD = $(PHOSPHOR_LOGGING_LIBS) \
 	$(top_builddir)/handler.o \
diff --git a/test/handler_stat_unittest.cpp b/test/handler_stat_unittest.cpp
new file mode 100644
index 0000000..253d02c
--- /dev/null
+++ b/test/handler_stat_unittest.cpp
@@ -0,0 +1,120 @@
+#include "handler_unittest.hpp"
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::StartsWith;
+
+using namespace std::string_literals;
+
+namespace blobs
+{
+
+class BinaryStoreBlobHandlerStatTest : public BinaryStoreBlobHandlerTest
+{
+  protected:
+    BinaryStoreBlobHandlerStatTest()
+    {
+        addDefaultStore(statTestBaseId);
+    }
+
+    static inline std::string statTestBaseId = "/test/"s;
+    static inline std::string statTestBlobId = "/test/blob0"s;
+    static inline std::vector<uint8_t> statTestData = {0, 1, 2, 3};
+    static inline std::vector<uint8_t> statTestDataToOverwrite = {
+        4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14};
+    static inline std::vector<uint8_t> commitMetaUnused;
+
+    static inline uint16_t statTestSessionId = 0;
+    static inline uint16_t statTestNewSessionId = 1;
+    static inline uint32_t statTestOffset = 0;
+
+    void openAndWriteTestData(
+        const std::vector<uint8_t>& testData = statTestData)
+    {
+        uint16_t flags = OpenFlags::read | OpenFlags::write;
+        EXPECT_TRUE(handler.open(statTestSessionId, flags, statTestBlobId));
+        EXPECT_TRUE(handler.write(statTestSessionId, statTestOffset, testData));
+    }
+
+    void commitData()
+    {
+        EXPECT_TRUE(handler.commit(statTestSessionId, commitMetaUnused));
+    }
+};
+
+TEST_F(BinaryStoreBlobHandlerStatTest, InitialStatIsValidQueriedWithBlobId)
+{
+    BlobMeta meta;
+
+    /* Querying stat fails if there is no open session */
+    EXPECT_FALSE(handler.stat(statTestSessionId, &meta));
+    /* However stat completes if queried using blobId. Returning default. */
+    EXPECT_TRUE(handler.stat(statTestBlobId, &meta));
+    EXPECT_EQ(meta.size, 0);
+};
+
+TEST_F(BinaryStoreBlobHandlerStatTest, StatShowsCommittedState)
+{
+    BlobMeta meta;
+    const int testIter = 2;
+
+    openAndWriteTestData();
+    for (int i = 0; i < testIter; ++i)
+    {
+        EXPECT_TRUE(handler.stat(statTestSessionId, &meta));
+        EXPECT_EQ(meta.size, statTestData.size());
+        EXPECT_TRUE(meta.blobState & OpenFlags::read);
+        EXPECT_TRUE(meta.blobState & OpenFlags::write);
+        EXPECT_FALSE(meta.blobState & StateFlags::committed);
+        EXPECT_TRUE(meta.blobState & BinaryStore::CommitState::Dirty);
+    }
+
+    commitData();
+    for (int i = 0; i < testIter; ++i)
+    {
+        EXPECT_TRUE(handler.stat(statTestSessionId, &meta));
+        EXPECT_EQ(meta.size, statTestData.size());
+        EXPECT_TRUE(meta.blobState & OpenFlags::read);
+        EXPECT_TRUE(meta.blobState & OpenFlags::write);
+        EXPECT_TRUE(meta.blobState & StateFlags::committed);
+        EXPECT_TRUE(meta.blobState & BinaryStore::CommitState::Clean);
+    }
+}
+
+TEST_F(BinaryStoreBlobHandlerStatTest, StatChangedWhenOverwriting)
+{
+    BlobMeta meta;
+    const int testIter = 2;
+
+    openAndWriteTestData();
+    commitData();
+    // Overwrite with different data.
+    EXPECT_TRUE(handler.write(statTestSessionId, statTestOffset,
+                              statTestDataToOverwrite));
+    for (int i = 0; i < testIter; ++i)
+    {
+        EXPECT_TRUE(handler.stat(statTestSessionId, &meta));
+        EXPECT_EQ(meta.size, statTestDataToOverwrite.size());
+        EXPECT_TRUE(meta.blobState & OpenFlags::read);
+        EXPECT_TRUE(meta.blobState & OpenFlags::write);
+        EXPECT_FALSE(meta.blobState & StateFlags::committed);
+        EXPECT_TRUE(meta.blobState & BinaryStore::CommitState::Dirty);
+    }
+
+    commitData();
+    for (int i = 0; i < testIter; ++i)
+    {
+        EXPECT_TRUE(handler.stat(statTestSessionId, &meta));
+        EXPECT_EQ(meta.size, statTestDataToOverwrite.size());
+        EXPECT_TRUE(meta.blobState & OpenFlags::read);
+        EXPECT_TRUE(meta.blobState & OpenFlags::write);
+        EXPECT_TRUE(meta.blobState & StateFlags::committed);
+        EXPECT_TRUE(meta.blobState & BinaryStore::CommitState::Clean);
+    }
+}
+
+} // namespace blobs