fd/line: Add line buffered reader

This gives us similar functionality to std::getline but on stdplus::Fd
instances. Also more predictable behavior around newlines at the end of
a file.

Change-Id: I19039c15fa02019e4ad767ca8a1996a8417fdd86
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/include-fd/stdplus/fd/line.hpp b/include-fd/stdplus/fd/line.hpp
new file mode 100644
index 0000000..6a82c7a
--- /dev/null
+++ b/include-fd/stdplus/fd/line.hpp
@@ -0,0 +1,35 @@
+#pragma once
+#include <array>
+#include <functional>
+#include <span>
+#include <stdplus/fd/intf.hpp>
+#include <string>
+
+namespace stdplus
+{
+namespace fd
+{
+
+class LineReader
+{
+  public:
+    static constexpr size_t buf_size = 4096;
+
+    LineReader(stdplus::Fd& fd);
+    LineReader(const LineReader&) = delete;
+    LineReader(LineReader&&) = default;
+    LineReader& operator=(const LineReader&) = delete;
+    LineReader& operator=(LineReader&&) = default;
+
+    const std::string* readLine();
+
+  private:
+    std::reference_wrapper<stdplus::Fd> fd;
+    std::array<char, buf_size> buf;
+    std::span<char> buf_data;
+    std::string line;
+    bool line_complete = false, hit_eof = false;
+};
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/fd/line.cpp b/src/fd/line.cpp
new file mode 100644
index 0000000..36c1e6f
--- /dev/null
+++ b/src/fd/line.cpp
@@ -0,0 +1,59 @@
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/line.hpp>
+#include <stdplus/fd/ops.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+LineReader::LineReader(Fd& fd) : fd(fd)
+{
+}
+
+const std::string* LineReader::readLine()
+{
+    if (hit_eof)
+    {
+        throw exception::Eof("readLine");
+    }
+    if (line_complete)
+    {
+        line.clear();
+    }
+    while (true)
+    {
+        if (buf_data.empty())
+        {
+            try
+            {
+                buf_data = read(fd, buf);
+                if (buf_data.empty())
+                {
+                    return nullptr;
+                }
+            }
+            catch (const exception::Eof&)
+            {
+                hit_eof = true;
+                return &line;
+            }
+        }
+        line_complete = false;
+        for (size_t i = 0; i < buf_data.size(); ++i)
+        {
+            if (buf_data[i] == '\n')
+            {
+                line.insert(line.end(), buf_data.begin(), buf_data.begin() + i);
+                buf_data = buf_data.subspan(i + 1);
+                line_complete = true;
+                return &line;
+            }
+        }
+        line.insert(line.end(), buf_data.begin(), buf_data.end());
+        buf_data = {};
+    }
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/meson.build b/src/meson.build
index 34c3674..4364395 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -37,6 +37,7 @@
     'fd/create.cpp',
     'fd/dupable.cpp',
     'fd/impl.cpp',
+    'fd/line.cpp',
     'fd/managed.cpp',
     'fd/mmap.cpp',
     'fd/ops.cpp',
diff --git a/test/fd/line.cpp b/test/fd/line.cpp
new file mode 100644
index 0000000..8d9eb32
--- /dev/null
+++ b/test/fd/line.cpp
@@ -0,0 +1,98 @@
+#include <gtest/gtest.h>
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/gmock.hpp>
+#include <stdplus/fd/line.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/raw.hpp>
+#include <stdplus/util/cexec.hpp>
+#include <string_view>
+#include <sys/mman.h>
+
+namespace stdplus
+{
+namespace fd
+{
+
+ManagedFd makeMemfd(std::string_view contents)
+{
+    auto fd = ManagedFd(CHECK_ERRNO(memfd_create("test", 0), "memfd_create"));
+    write(fd, contents);
+    lseek(fd, 0, Whence::Set);
+    return fd;
+}
+
+TEST(LineReader, Empty)
+{
+    auto fd = makeMemfd("");
+    LineReader reader(fd);
+    EXPECT_EQ("", *reader.readLine());
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+}
+
+TEST(LineReader, SingleLine)
+{
+    auto fd = makeMemfd("\n");
+    LineReader reader(fd);
+    EXPECT_EQ("", *reader.readLine());
+    EXPECT_EQ("", *reader.readLine());
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+}
+
+TEST(LineReader, SomeData)
+{
+    auto fd = makeMemfd("A\nbcd\n\ne");
+    LineReader reader(fd);
+    EXPECT_EQ("A", *reader.readLine());
+    EXPECT_EQ("bcd", *reader.readLine());
+    EXPECT_EQ("", *reader.readLine());
+    EXPECT_EQ("e", *reader.readLine());
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+}
+
+TEST(LineReader, LargerThanBuf)
+{
+    std::string big(LineReader::buf_size + 10, 'a');
+    auto fd = makeMemfd(std::string("alpha\n") + big + "\ndef");
+    LineReader reader(fd);
+    EXPECT_EQ("alpha", *reader.readLine());
+    EXPECT_EQ(big, *reader.readLine());
+    EXPECT_EQ("def", *reader.readLine());
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+}
+
+using testing::_;
+
+inline auto readSv(std::string_view s)
+{
+    return [s](std::span<std::byte> buf) {
+        memcpy(buf.data(), s.data(), s.size());
+        return buf.subspan(0, s.size());
+    };
+}
+
+TEST(LineReader, Nonblock)
+{
+    FdMock fd;
+    {
+        testing::InSequence seq;
+        EXPECT_CALL(fd, read(_)).WillOnce(readSv("alph"));
+        EXPECT_CALL(fd, read(_)).WillOnce(readSv({}));
+        EXPECT_CALL(fd, read(_)).WillOnce(readSv("a"));
+        EXPECT_CALL(fd, read(_))
+            .WillRepeatedly(testing::Throw(stdplus::exception::Eof("test")));
+    }
+
+    LineReader reader(fd);
+    EXPECT_EQ(nullptr, reader.readLine());
+    EXPECT_EQ("alpha", *reader.readLine());
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+    EXPECT_THROW(reader.readLine(), exception::Eof);
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/test/meson.build b/test/meson.build
index 4ebf17f..a0be742 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -33,6 +33,7 @@
     'fd/managed',
     'fd/intf',
     'fd/impl',
+    'fd/line',
     'fd/mmap',
     'fd/mock',
     'fd/ops',