Add implementation of a system file interface

Implement simple read/write file operation semantics for the commit
operation. Add unit tests using the mock sys interface to verify the
behaviors.

Signed-off-by: Kun Yi <kunyi@google.com>
Change-Id: I75f8005561ab342138375bccb46c21e2841aa5e1
diff --git a/sys_file.cpp b/sys_file.cpp
new file mode 100644
index 0000000..2336af2
--- /dev/null
+++ b/sys_file.cpp
@@ -0,0 +1,124 @@
+#include "sys_file.hpp"
+
+#include <system_error>
+
+using namespace std::string_literals;
+
+static constexpr size_t rwBlockSize = 8192;
+
+namespace binstore
+{
+
+namespace
+{
+
+std::system_error errnoException(const std::string& message)
+{
+    return std::system_error(errno, std::generic_category(), message);
+}
+
+} // namespace
+
+SysFileImpl::SysFileImpl(const std::string& path, size_t offset,
+                         const internal::Sys* sys) :
+    sys(sys)
+{
+    fd_ = sys->open(path.c_str(), O_RDWR);
+    offset_ = offset;
+
+    if (fd_ < 0)
+    {
+        throw errnoException("Error opening file "s + path);
+    }
+}
+
+SysFileImpl::~SysFileImpl()
+{
+    sys->close(fd_);
+}
+
+void SysFileImpl::lseek(size_t pos) const
+{
+    if (sys->lseek(fd_, offset_ + pos, SEEK_SET) < 0)
+    {
+        throw errnoException("Cannot lseek to pos "s + std::to_string(pos));
+    }
+}
+
+size_t SysFileImpl::readToBuf(size_t pos, size_t count, char* buf) const
+{
+
+    lseek(pos);
+
+    size_t bytesRead = 0;
+
+    do
+    {
+        auto ret = sys->read(fd_, &buf[bytesRead], count - bytesRead);
+        if (ret < 0)
+        {
+            if (errno == EINTR)
+            {
+                continue;
+            }
+
+            throw errnoException("Error reading from file"s);
+        }
+        else if (ret > 0)
+        {
+            bytesRead += ret;
+        }
+        else // ret == 0
+        {
+            break;
+        }
+    } while (bytesRead < count);
+
+    return bytesRead;
+}
+
+std::string SysFileImpl::readAsStr(size_t pos, size_t count) const
+{
+    std::string result;
+    result.resize(count);
+    size_t bytesRead = readToBuf(pos, count, result.data());
+    result.resize(bytesRead);
+    return result;
+}
+
+std::string SysFileImpl::readRemainingAsStr(size_t pos) const
+{
+    std::string result;
+    size_t bytesRead, size = 0;
+
+    /* Since we don't know how much to read, read 'rwBlockSize' at a time
+     * until there is nothing to read anymore. */
+    do
+    {
+        result.resize(size + rwBlockSize);
+        bytesRead = readToBuf(pos + size, rwBlockSize, result.data() + size);
+        size += bytesRead;
+    } while (bytesRead == rwBlockSize);
+
+    result.resize(size);
+    return result;
+}
+
+void SysFileImpl::writeStr(const std::string& data, size_t pos)
+{
+    lseek(pos);
+    ssize_t ret;
+    ret = sys->write(fd_, data.data(), data.size());
+    if (ret < 0)
+    {
+        throw errnoException("Error writing to file"s);
+    }
+    if (static_cast<size_t>(ret) != data.size())
+    {
+        throw std::runtime_error(
+            "Tried to send data size "s + std::to_string(data.size()) +
+            " but could only send "s + std::to_string(ret));
+    }
+}
+
+} // namespace binstore
diff --git a/sys_file.hpp b/sys_file.hpp
new file mode 100644
index 0000000..5a2bb85
--- /dev/null
+++ b/sys_file.hpp
@@ -0,0 +1,90 @@
+#pragma once
+
+#include "sys.hpp"
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <string>
+
+namespace binstore
+{
+
+/**
+ * @brief Represents a file that supports read/write semantics
+ * TODO: leverage stdplus's support for smart file descriptors when it's ready.
+ */
+class SysFile
+{
+  public:
+    virtual ~SysFile() = default;
+
+    /**
+     * @brief Reads content at pos to char* buffer
+     * @param pos The byte pos into the file to read from
+     * @param count How many bytes to read
+     * @param buf Output data
+     * @returns The size of data read
+     * @throws std::system_error if read operation cannot be completed
+     */
+    virtual size_t readToBuf(size_t pos, size_t count, char* buf) const = 0;
+
+    /**
+     * @brief Reads content at pos
+     * @param pos The byte pos into the file to read from
+     * @param count How many bytes to read
+     * @returns The data read in a vector, whose size might be smaller than
+     *          count if there is not enough to read.
+     * @throws std::system_error if read operation cannot be completed
+     */
+    virtual std::string readAsStr(size_t pos, size_t count) const = 0;
+
+    /**
+     * @brief Reads all the content in file after pos
+     * @param pos The byte pos to read from
+     * @returns The data read in a vector, whose size might be smaller than
+     *          count if there is not enough to read.
+     * @throws std::system_error if read operation cannot be completed
+     */
+    virtual std::string readRemainingAsStr(size_t pos) const = 0;
+
+    /**
+     * @brief Writes all of data into file at pos
+     * @param pos The byte pos to write
+     * @returns void
+     * @throws std::system_error if write operation cannot be completed or
+     *         not all of the bytes can be written
+     */
+    virtual void writeStr(const std::string& data, size_t pos) = 0;
+};
+
+class SysFileImpl : public SysFile
+{
+  public:
+    /**
+     * @brief Constructs sysFile specified by path and offset
+     * @param path The file path
+     * @param offset The byte offset relatively. Reading a sysfile at position 0
+     *     actually reads underlying file at 'offset'
+     * @param sys Syscall operation interface
+     */
+    explicit SysFileImpl(const std::string& path, size_t offset = 0,
+                         const internal::Sys* sys = &internal::sys_impl);
+    ~SysFileImpl();
+    SysFileImpl() = delete;
+    SysFileImpl(const SysFileImpl&) = delete;
+    SysFileImpl& operator=(SysFileImpl) = delete;
+
+    size_t readToBuf(size_t pos, size_t count, char* buf) const override;
+    std::string readAsStr(size_t pos, size_t count) const override;
+    std::string readRemainingAsStr(size_t pos) const override;
+    void writeStr(const std::string& data, size_t pos) override;
+
+  private:
+    int fd_;
+    size_t offset_;
+    void lseek(size_t pos) const;
+    const internal::Sys* sys;
+};
+
+} // namespace binstore
diff --git a/test/Makefile.am b/test/Makefile.am
index 9f41737..b2fa580 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -11,11 +11,15 @@
 
 # Run all 'check' test programs
 check_PROGRAMS = \
+	sys_file_unittest \
 	handler_open_unittest \
 	handler_readwrite_unittest \
 	handler_unittest
 TESTS = $(check_PROGRAMS)
 
+sys_file_unittest_SOURCES = sys_file_unittest.cpp
+sys_file_unittest_LDADD = $(top_builddir)/sys_file.o
+
 handler_unittest_SOURCES = handler_unittest.cpp
 handler_unittest_LDADD = $(PHOSPHOR_LOGGING_LIBS) \
 	$(top_builddir)/handler.o \
diff --git a/test/sys_file_unittest.cpp b/test/sys_file_unittest.cpp
new file mode 100644
index 0000000..f4035cf
--- /dev/null
+++ b/test/sys_file_unittest.cpp
@@ -0,0 +1,131 @@
+#include "sys_file.hpp"
+#include "sys_mock.hpp"
+
+#include <fcntl.h>
+
+#include <cstring>
+#include <memory>
+
+#include <gmock/gmock.h>
+
+using namespace binstore;
+using namespace std::string_literals;
+
+using ::testing::_;
+using ::testing::Args;
+using ::testing::Ge;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+using ::testing::Return;
+using ::testing::SetErrnoAndReturn;
+using ::testing::StrEq;
+using ::testing::WithArgs;
+
+static constexpr int sysFileTestFd = 42;
+static inline std::string sysFileTestPath = "/test/path"s;
+static constexpr size_t sysFileTestOffset = 0;
+auto static const sysFileTestStr = "Hello, \0+.world!\0"s;
+std::vector<uint8_t> static const sysFileTestBuf(sysFileTestStr.begin(),
+                                                 sysFileTestStr.end());
+
+class SysFileTest : public ::testing::Test
+{
+  protected:
+    SysFileTest()
+    {
+        EXPECT_CALL(sys, open(StrEq(sysFileTestPath), O_RDWR))
+            .WillOnce(Return(sysFileTestFd));
+        EXPECT_CALL(sys, close(sysFileTestFd));
+
+        file = std::make_unique<SysFileImpl>(sysFileTestPath, sysFileTestOffset,
+                                             &sys);
+    }
+
+    const internal::SysMock sys;
+
+    std::unique_ptr<SysFile> file;
+};
+
+ACTION_P(BufSet, buf)
+{
+    size_t count = std::min(arg1, buf.size());
+    std::memcpy(arg0, buf.data(), count);
+
+    return count;
+}
+
+ACTION_P2(BufSetAndGetStartAddr, buf, addrPtr)
+{
+    size_t count = std::min(arg1, buf.size());
+    std::memcpy(arg0, buf.data(), count);
+    *addrPtr = arg0;
+
+    return count;
+}
+
+ACTION_P2(BufSetTruncated, buf, offset)
+{
+    size_t count = std::min(arg1, buf.size() - offset);
+    std::memcpy(arg0, buf.data() + offset, count);
+
+    return count;
+}
+
+TEST_F(SysFileTest, ReadSucceeds)
+{
+    EXPECT_CALL(sys, lseek(sysFileTestFd, 0, SEEK_SET));
+    EXPECT_CALL(sys, read(sysFileTestFd, NotNull(), _))
+        .WillOnce(WithArgs<1, 2>(BufSet(sysFileTestBuf)));
+
+    EXPECT_EQ(sysFileTestStr, file->readAsStr(0, sysFileTestBuf.size()));
+}
+
+TEST_F(SysFileTest, ReadMoreThanAvailable)
+{
+    EXPECT_CALL(sys, lseek(sysFileTestFd, 0, SEEK_SET));
+    EXPECT_CALL(sys, read(sysFileTestFd, NotNull(), _))
+        .WillOnce(WithArgs<1, 2>(BufSet(sysFileTestBuf)))
+        .WillOnce(Return(0));
+
+    EXPECT_EQ(sysFileTestStr, file->readAsStr(0, sysFileTestBuf.size() + 1024));
+}
+
+TEST_F(SysFileTest, ReadAtOffset)
+{
+    const size_t testOffset = 2;
+    std::string truncBuf = sysFileTestStr.substr(testOffset);
+
+    EXPECT_CALL(sys, lseek(sysFileTestFd, testOffset, SEEK_SET));
+    EXPECT_CALL(sys, read(sysFileTestFd, NotNull(), _))
+        .WillOnce(WithArgs<1, 2>(BufSetTruncated(sysFileTestBuf, testOffset)));
+
+    EXPECT_EQ(truncBuf, file->readAsStr(testOffset, truncBuf.size()));
+}
+
+TEST_F(SysFileTest, ReadRemainingFail)
+{
+    EXPECT_CALL(sys, lseek(sysFileTestFd, 0, SEEK_SET));
+    EXPECT_CALL(sys, read(sysFileTestFd, NotNull(), _))
+        .WillOnce(SetErrnoAndReturn(EIO, -1));
+
+    EXPECT_THROW(file->readRemainingAsStr(0), std::exception);
+}
+
+TEST_F(SysFileTest, ReadRemainingSucceeds)
+{
+    EXPECT_CALL(sys, lseek(sysFileTestFd, 0, SEEK_SET));
+    EXPECT_CALL(sys, read(sysFileTestFd, NotNull(), _))
+        .WillOnce(WithArgs<1, 2>(BufSet(sysFileTestBuf)))
+        .WillOnce(Return(0)); // EOF
+
+    EXPECT_EQ(sysFileTestStr, file->readRemainingAsStr(0));
+}
+
+TEST_F(SysFileTest, ReadRemainingBeyondEndReturnsEmpty)
+{
+    const size_t largeOffset = 9000;
+    EXPECT_CALL(sys, lseek(sysFileTestFd, largeOffset, SEEK_SET));
+    EXPECT_CALL(sys, read(sysFileTestFd, NotNull(), _)).WillOnce(Return(0));
+
+    EXPECT_THAT(file->readRemainingAsStr(largeOffset), IsEmpty());
+}