fd: Implement managed file descriptor support

Change-Id: I0c5c438aa2c31ae52e115951b3fb1e85df182fd1
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/meson_options.txt b/meson_options.txt
index 86bc7fc..2914f5c 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,2 +1,3 @@
+option('fd', type: 'feature', description: 'Managed file descriptor support')
 option('tests', type: 'feature', description: 'Build tests')
 option('examples', type: 'boolean', value: true, description: 'Build examples')
diff --git a/src/meson.build b/src/meson.build
index 09d8b0e..dca95ce 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -36,10 +36,36 @@
   span_dep,
 ]
 
+has_fd = false
+if not get_option('fd').disabled() and has_span
+  has_fd = true
+
+  fd_srcs = [
+    'stdplus/fd/create.cpp',
+    'stdplus/fd/dupable.cpp',
+    'stdplus/fd/impl.cpp',
+    'stdplus/fd/managed.cpp',
+    'stdplus/fd/ops.cpp',
+  ]
+
+  install_headers(
+    'stdplus/fd/create.hpp',
+    'stdplus/fd/dupable.hpp',
+    'stdplus/fd/gmock.hpp',
+    'stdplus/fd/impl.hpp',
+    'stdplus/fd/intf.hpp',
+    'stdplus/fd/managed.hpp',
+    'stdplus/fd/ops.hpp',
+    subdir: 'stdplus/fd')
+elif get_option('fd').enabled()
+  error('File descriptor support required')
+endif
+
 stdplus_lib = library(
   'stdplus',
   'stdplus/exception.cpp',
   'stdplus/signal.cpp',
+  fd_srcs,
   include_directories: stdplus_headers,
   implicit_include_directories: false,
   dependencies: stdplus_deps,
@@ -66,6 +92,7 @@
 
 install_headers(
   'stdplus/exception.hpp',
+  'stdplus/flags.hpp',
   'stdplus/raw.hpp',
   'stdplus/signal.hpp',
   'stdplus/types.hpp',
diff --git a/src/stdplus/fd/create.cpp b/src/stdplus/fd/create.cpp
new file mode 100644
index 0000000..2240a98
--- /dev/null
+++ b/src/stdplus/fd/create.cpp
@@ -0,0 +1,28 @@
+#include <fcntl.h>
+#include <fmt/format.h>
+#include <stdplus/fd/create.hpp>
+#include <stdplus/util/cexec.hpp>
+#include <sys/socket.h>
+
+namespace stdplus
+{
+namespace fd
+{
+
+DupableFd open(const char* pathname, OpenFlags flags, mode_t mode)
+{
+    return DupableFd(
+        CHECK_ERRNO(::open(pathname, static_cast<int>(flags), mode),
+                    fmt::format("open `{}`", pathname)));
+}
+
+DupableFd socket(SocketDomain domain, SocketType type, SocketProto protocol)
+{
+    return DupableFd(
+        CHECK_ERRNO(::socket(static_cast<int>(domain), static_cast<int>(type),
+                             static_cast<int>(protocol)),
+                    "socket"));
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/fd/create.hpp b/src/stdplus/fd/create.hpp
new file mode 100644
index 0000000..574b4f4
--- /dev/null
+++ b/src/stdplus/fd/create.hpp
@@ -0,0 +1,76 @@
+#pragma once
+#include <fcntl.h>
+#include <stdplus/fd/dupable.hpp>
+#include <stdplus/flags.hpp>
+#include <string>
+
+namespace stdplus
+{
+namespace fd
+{
+
+enum class OpenAccess : int
+{
+    ReadOnly = O_RDONLY,
+    WriteOnly = O_WRONLY,
+    ReadWrite = O_RDWR,
+};
+
+enum class OpenFlag : int
+{
+    Append = O_APPEND,
+    Async = O_ASYNC,
+    CloseOnExec = O_CLOEXEC,
+    Create = O_CREAT,
+    Direct = O_DIRECT,
+    Directory = O_DIRECTORY,
+    Dsync = O_DSYNC,
+    EnsureCreate = O_EXCL,
+    LargeFile = O_LARGEFILE,
+    NoAtime = O_NOATIME,
+    NoCtty = O_NOCTTY,
+    NoFollow = O_NOFOLLOW,
+    NonBlock = O_NONBLOCK,
+    Path = O_PATH,
+    Sync = O_SYNC,
+    TmpFile = O_TMPFILE,
+    Trunc = O_TRUNC,
+};
+
+class OpenFlags : public BitFlags<int, OpenFlag>
+{
+  public:
+    inline OpenFlags(OpenAccess access) :
+        BitFlags<int, OpenFlag>(static_cast<int>(access))
+    {
+    }
+
+    inline OpenFlags(BitFlags<int, OpenFlag> flags) :
+        BitFlags<int, OpenFlag>(flags)
+    {
+    }
+};
+
+DupableFd open(const char* pathname, OpenFlags flags, mode_t mode = 0);
+inline DupableFd open(const std::string& pathname, OpenFlags flags,
+                      mode_t mode = 0)
+{
+    return open(pathname.c_str(), flags, mode);
+}
+
+enum class SocketDomain : int
+{
+};
+
+enum class SocketType : int
+{
+};
+
+enum class SocketProto : int
+{
+};
+
+DupableFd socket(SocketDomain domain, SocketType type, SocketProto protocol);
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/fd/dupable.cpp b/src/stdplus/fd/dupable.cpp
new file mode 100644
index 0000000..96e347f
--- /dev/null
+++ b/src/stdplus/fd/dupable.cpp
@@ -0,0 +1,41 @@
+#include <fcntl.h>
+#include <stdplus/fd/dupable.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/util/cexec.hpp>
+#include <utility>
+
+namespace stdplus
+{
+namespace fd
+{
+namespace detail
+{
+
+int ref(const int& fd)
+{
+    return CHECK_ERRNO(fcntl(fd, F_DUPFD_CLOEXEC, fd), "fcntl dupfd_cloexec");
+}
+
+} // namespace detail
+
+DupableFd::DupableFd(const int& fd) : handle(fd)
+{
+}
+
+DupableFd::DupableFd(int&& fd) : handle(std::move(fd))
+{
+    fd::setFdFlags(*this, fd::getFdFlags(*this).set(fd::FdFlag::CloseOnExec));
+}
+
+int DupableFd::release()
+{
+    return handle.release();
+}
+
+int DupableFd::get() const
+{
+    return handle.value();
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/fd/dupable.hpp b/src/stdplus/fd/dupable.hpp
new file mode 100644
index 0000000..b008ce0
--- /dev/null
+++ b/src/stdplus/fd/dupable.hpp
@@ -0,0 +1,59 @@
+#pragma once
+#include <stdplus/fd/impl.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/handle/copyable.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+namespace detail
+{
+
+int ref(const int& fd);
+
+using DupableFdHandle = Copyable<int>::Handle<drop, ref>;
+
+} // namespace detail
+
+/** @class DupableFd
+ *  @brief Holds references to file descriptors which can be dup'd
+ *  @details Provides RAII semantics for file descriptors
+ */
+class DupableFd : public FdImpl
+{
+  public:
+    /** @brief Duplicates and holds a file descriptor
+     *         Does not automatically close the input descriptor
+     *
+     *  @param[in] fd  - File descriptor being duplicated
+     *  @throws std::system_error for underlying syscall failures
+     */
+    explicit DupableFd(const int& fd);
+
+    /** @brief Holds the input file descriptor
+     *         Becomes the sole owner of the file descriptor
+     *
+     *  @param[in] fd  - File descriptor to hold
+     */
+    explicit DupableFd(int&& fd);
+
+    /** @brief Unmanages the file descriptor and returns the value with
+     *         ownership to the caller.
+     *
+     *  @return The file descriptor number
+     */
+    [[nodiscard]] int release();
+
+    int get() const override;
+
+  private:
+    friend class ManagedFd;
+    detail::DupableFdHandle handle;
+};
+
+} // namespace fd
+
+using fd::DupableFd;
+
+} // namespace stdplus
diff --git a/src/stdplus/fd/gmock.hpp b/src/stdplus/fd/gmock.hpp
new file mode 100644
index 0000000..bd57b51
--- /dev/null
+++ b/src/stdplus/fd/gmock.hpp
@@ -0,0 +1,39 @@
+#pragma once
+#include <gmock/gmock.h>
+#include <stdplus/fd/intf.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+class FdMock : public Fd
+{
+  public:
+    MOCK_METHOD(span<std::byte>, read, (span<std::byte> buf), (override));
+    MOCK_METHOD(span<std::byte>, recv, (span<std::byte> buf, RecvFlags flags),
+                (override));
+    MOCK_METHOD(span<const std::byte>, write, (span<const std::byte> data),
+                (override));
+    MOCK_METHOD(span<const std::byte>, send,
+                (span<const std::byte> data, SendFlags flags), (override));
+    MOCK_METHOD(size_t, lseek, (off_t offset, Whence whence), (override));
+    MOCK_METHOD(void, truncate, (off_t size), (override));
+    MOCK_METHOD(void, bind, (span<const std::byte> sockaddr), (override));
+    MOCK_METHOD(void, setsockopt,
+                (SockLevel level, SockOpt optname, span<const std::byte> opt),
+                (override));
+    MOCK_METHOD(int, ioctl, (unsigned long id, void* data), (override));
+    MOCK_METHOD(int, constIoctl, (unsigned long id, void* data),
+                (const, override));
+    MOCK_METHOD(void, fcntlSetfd, (FdFlags flags), (override));
+    MOCK_METHOD(FdFlags, fcntlGetfd, (), (const, override));
+    MOCK_METHOD(void, fcntlSetfl, (FileFlags flags), (override));
+    MOCK_METHOD(FileFlags, fcntlGetfl, (), (const, override));
+};
+
+} // namespace fd
+
+using fd::FdMock;
+
+} // namespace stdplus
diff --git a/src/stdplus/fd/impl.cpp b/src/stdplus/fd/impl.cpp
new file mode 100644
index 0000000..511bda4
--- /dev/null
+++ b/src/stdplus/fd/impl.cpp
@@ -0,0 +1,160 @@
+#include <fcntl.h>
+#include <fmt/format.h>
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/impl.hpp>
+#include <stdplus/util/cexec.hpp>
+#include <string_view>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+namespace stdplus
+{
+namespace fd
+{
+
+using namespace std::literals::string_view_literals;
+
+span<std::byte> FdImpl::read(span<std::byte> buf)
+{
+    ssize_t amt = ::read(get(), buf.data(), buf.size());
+    if (amt == -1)
+    {
+        if (errno == EAGAIN || errno == EWOULDBLOCK)
+        {
+            return {};
+        }
+        throw util::makeSystemError(errno, "read");
+    }
+    else if (amt == 0)
+    {
+        throw exception::Eof("read");
+    }
+    return buf.subspan(0, amt);
+}
+
+span<std::byte> FdImpl::recv(span<std::byte> buf, RecvFlags flags)
+{
+    ssize_t amt =
+        ::recv(get(), buf.data(), buf.size(), static_cast<int>(flags));
+    if (amt == -1)
+    {
+        if (errno == EAGAIN || errno == EWOULDBLOCK)
+        {
+            return {};
+        }
+        throw util::makeSystemError(errno, "recv");
+    }
+    else if (amt == 0)
+    {
+        throw exception::Eof("recv");
+    }
+    return buf.subspan(0, amt);
+}
+
+span<const std::byte> FdImpl::write(span<const std::byte> data)
+{
+    ssize_t amt = ::write(get(), data.data(), data.size());
+    if (amt == -1)
+    {
+        if (errno == EAGAIN || errno == EWOULDBLOCK)
+        {
+            return {};
+        }
+        throw util::makeSystemError(errno, "write");
+    }
+    return data.subspan(0, amt);
+}
+
+span<const std::byte> FdImpl::send(span<const std::byte> data, SendFlags flags)
+{
+    ssize_t amt =
+        ::send(get(), data.data(), data.size(), static_cast<int>(flags));
+    if (amt == -1)
+    {
+        if (errno == EAGAIN || errno == EWOULDBLOCK)
+        {
+            return {};
+        }
+        throw util::makeSystemError(errno, "send");
+    }
+    return data.subspan(0, amt);
+}
+
+static std::string_view whenceStr(Whence whence)
+{
+    switch (whence)
+    {
+        case Whence::Set:
+            return "set"sv;
+        case Whence::Cur:
+            return "cur"sv;
+        case Whence::End:
+            return "end"sv;
+        default:
+            return "Unknown whence"sv;
+    }
+}
+
+size_t FdImpl::lseek(off_t offset, Whence whence)
+{
+    return CHECK_ERRNO(::lseek(get(), offset, static_cast<int>(whence)),
+                       fmt::format("lseek {}B {}", offset, whenceStr(whence)));
+}
+
+void FdImpl::truncate(off_t size)
+{
+    CHECK_ERRNO(::ftruncate(get(), size), fmt::format("ftruncate {}B", size));
+}
+
+void FdImpl::bind(span<const std::byte> sockaddr)
+{
+    CHECK_ERRNO(
+        ::bind(get(), reinterpret_cast<const struct sockaddr*>(sockaddr.data()),
+               sockaddr.size()),
+        "bind");
+}
+
+void FdImpl::setsockopt(SockLevel level, SockOpt optname,
+                        span<const std::byte> opt)
+{
+    CHECK_ERRNO(::setsockopt(get(), static_cast<int>(level),
+                             static_cast<int>(optname), opt.data(), opt.size()),
+                "setsockopt");
+}
+
+int FdImpl::ioctl(unsigned long id, void* data)
+{
+    return constIoctl(id, data);
+}
+
+int FdImpl::constIoctl(unsigned long id, void* data) const
+{
+    return CHECK_ERRNO(::ioctl(get(), id, data),
+                       fmt::format("ioctl {:#x}", id));
+}
+
+void FdImpl::fcntlSetfd(FdFlags flags)
+{
+    CHECK_ERRNO(::fcntl(get(), F_SETFD, static_cast<int>(flags)),
+                "fcntl setfd");
+}
+
+FdFlags FdImpl::fcntlGetfd() const
+{
+    return FdFlags(CHECK_ERRNO(::fcntl(get(), F_GETFD), "fcntl getfd"));
+}
+
+void FdImpl::fcntlSetfl(FileFlags flags)
+{
+    CHECK_ERRNO(::fcntl(get(), F_SETFL, static_cast<int>(flags)),
+                "fcntl setfl");
+}
+
+FileFlags FdImpl::fcntlGetfl() const
+{
+    return FileFlags(CHECK_ERRNO(::fcntl(get(), F_GETFL), "fcntl getfl"));
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/fd/impl.hpp b/src/stdplus/fd/impl.hpp
new file mode 100644
index 0000000..b321dfb
--- /dev/null
+++ b/src/stdplus/fd/impl.hpp
@@ -0,0 +1,36 @@
+#pragma once
+#include <stdplus/fd/intf.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+class FdImpl : public Fd
+{
+  public:
+    virtual int get() const = 0;
+
+    span<std::byte> read(span<std::byte> buf) override;
+    span<std::byte> recv(span<std::byte> buf, RecvFlags flags) override;
+    span<const std::byte> write(span<const std::byte> data) override;
+    span<const std::byte> send(span<const std::byte> data,
+                               SendFlags flags) override;
+    size_t lseek(off_t offset, Whence whence) override;
+    void truncate(off_t size) override;
+    void bind(span<const std::byte> sockaddr) override;
+    void setsockopt(SockLevel level, SockOpt optname,
+                    span<const std::byte> opt) override;
+    int ioctl(unsigned long id, void* data) override;
+    int constIoctl(unsigned long id, void* data) const override;
+    void fcntlSetfd(FdFlags flags) override;
+    FdFlags fcntlGetfd() const override;
+    void fcntlSetfl(FileFlags flags) override;
+    FileFlags fcntlGetfl() const override;
+};
+
+} // namespace fd
+
+using fd::FdImpl;
+
+} // namespace stdplus
diff --git a/src/stdplus/fd/intf.hpp b/src/stdplus/fd/intf.hpp
new file mode 100644
index 0000000..aac85a2
--- /dev/null
+++ b/src/stdplus/fd/intf.hpp
@@ -0,0 +1,108 @@
+#pragma once
+#include <cstddef>
+#include <fcntl.h>
+#include <stdplus/flags.hpp>
+#include <stdplus/types.hpp>
+#include <sys/socket.h>
+
+namespace stdplus
+{
+namespace fd
+{
+
+enum class RecvFlag : int
+{
+    DontWait = MSG_DONTWAIT,
+    ErrQueue = MSG_ERRQUEUE,
+    OutOfBounds = MSG_OOB,
+    Peek = MSG_PEEK,
+    Trunc = MSG_TRUNC,
+    WaitAll = MSG_WAITALL,
+};
+using RecvFlags = BitFlags<int, RecvFlag>;
+
+enum class SendFlag : int
+{
+    Confirm = MSG_CONFIRM,
+    DontRoute = MSG_DONTROUTE,
+    DontWait = MSG_DONTWAIT,
+    EndOfRecord = MSG_EOR,
+    More = MSG_MORE,
+    NoSignal = MSG_NOSIGNAL,
+    OutOfBounds = MSG_OOB,
+};
+using SendFlags = BitFlags<int, SendFlag>;
+
+enum class Whence : int
+{
+    Set = SEEK_SET,
+    Cur = SEEK_CUR,
+    End = SEEK_END,
+};
+
+enum class SockLevel : int
+{
+    Socket = SOL_SOCKET,
+};
+
+enum class SockOpt : int
+{
+    Debug = SO_DEBUG,
+    Broadcast = SO_BROADCAST,
+    ReuseAddr = SO_REUSEADDR,
+    KeepAlive = SO_KEEPALIVE,
+    Linger = SO_LINGER,
+    OOBInline = SO_OOBINLINE,
+    SendBuf = SO_SNDBUF,
+    RecvBuf = SO_RCVBUF,
+    DontRoute = SO_DONTROUTE,
+    RecvLowWait = SO_RCVLOWAT,
+    RecvTimeout = SO_RCVTIMEO,
+    SendLowWait = SO_SNDLOWAT,
+    SendTimeout = SO_SNDTIMEO,
+};
+
+enum class FdFlag : int
+{
+    CloseOnExec = FD_CLOEXEC,
+};
+using FdFlags = BitFlags<int, FdFlag>;
+
+enum class FileFlag : int
+{
+    Append = O_APPEND,
+    Async = O_ASYNC,
+    Direct = O_DIRECT,
+    NoAtime = O_NOATIME,
+    NonBlock = O_NONBLOCK,
+};
+using FileFlags = BitFlags<int, FileFlag>;
+
+class Fd
+{
+  public:
+    virtual ~Fd() = default;
+
+    virtual span<std::byte> read(span<std::byte> buf) = 0;
+    virtual span<std::byte> recv(span<std::byte> buf, RecvFlags flags) = 0;
+    virtual span<const std::byte> write(span<const std::byte> data) = 0;
+    virtual span<const std::byte> send(span<const std::byte> data,
+                                       SendFlags flags) = 0;
+    virtual size_t lseek(off_t offset, Whence whence) = 0;
+    virtual void truncate(off_t size) = 0;
+    virtual void bind(span<const std::byte> sockaddr) = 0;
+    virtual void setsockopt(SockLevel level, SockOpt optname,
+                            span<const std::byte> opt) = 0;
+    virtual int ioctl(unsigned long id, void* data) = 0;
+    virtual int constIoctl(unsigned long id, void* data) const = 0;
+    virtual void fcntlSetfd(FdFlags flags) = 0;
+    virtual FdFlags fcntlGetfd() const = 0;
+    virtual void fcntlSetfl(FileFlags flags) = 0;
+    virtual FileFlags fcntlGetfl() const = 0;
+};
+
+} // namespace fd
+
+using fd::Fd;
+
+} // namespace stdplus
diff --git a/src/stdplus/fd/managed.cpp b/src/stdplus/fd/managed.cpp
new file mode 100644
index 0000000..4611143
--- /dev/null
+++ b/src/stdplus/fd/managed.cpp
@@ -0,0 +1,59 @@
+#include <fmt/format.h>
+#include <stdplus/fd/dupable.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/util/cexec.hpp>
+#include <unistd.h>
+#include <utility>
+
+namespace stdplus
+{
+namespace fd
+{
+namespace detail
+{
+
+void drop(int&& fd)
+{
+    CHECK_ERRNO(close(fd), "close");
+}
+
+} // namespace detail
+
+ManagedFd::ManagedFd(int&& fd) : handle(std::move(fd))
+{
+    fd::setFdFlags(*this, fd::getFdFlags(*this).set(fd::FdFlag::CloseOnExec));
+}
+
+ManagedFd::ManagedFd(DupableFd&& other) noexcept :
+    handle(static_cast<detail::ManagedFdHandle&&>(other.handle))
+{
+}
+
+ManagedFd::ManagedFd(const DupableFd& other) : ManagedFd(DupableFd(other))
+{
+}
+
+ManagedFd& ManagedFd::operator=(DupableFd&& other) noexcept
+{
+    handle = static_cast<detail::ManagedFdHandle&&>(other.handle);
+    return *this;
+}
+
+ManagedFd& ManagedFd::operator=(const DupableFd& other)
+{
+    return *this = DupableFd(other);
+}
+
+int ManagedFd::release()
+{
+    return handle.release();
+}
+
+int ManagedFd::get() const
+{
+    return handle.value();
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/fd/managed.hpp b/src/stdplus/fd/managed.hpp
new file mode 100644
index 0000000..3b9dde3
--- /dev/null
+++ b/src/stdplus/fd/managed.hpp
@@ -0,0 +1,60 @@
+#pragma once
+#include <stdplus/fd/impl.hpp>
+#include <stdplus/handle/managed.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+namespace detail
+{
+
+/** @brief Closes the file descriptor when dropping an Fd handle
+ *
+ *  @param[in] fd - File descriptor to close
+ */
+void drop(int&& fd);
+
+using ManagedFdHandle = Managed<int>::Handle<drop>;
+
+} // namespace detail
+
+class DupableFd;
+
+/** @class ManagedFd
+ *  @brief Holds references to unique, non-dupable file descriptors
+ *  @details Provides RAII semantics for file descriptors
+ */
+class ManagedFd : public FdImpl
+{
+  public:
+    /** @brief Holds the input file descriptor
+     *         Becomes the sole owner of the file descriptor
+     *
+     *  @param[in] fd  - File descriptor to hold
+     */
+    explicit ManagedFd(int&& fd);
+
+    ManagedFd(DupableFd&& other) noexcept;
+    ManagedFd(const DupableFd& other);
+    ManagedFd& operator=(DupableFd&& other) noexcept;
+    ManagedFd& operator=(const DupableFd& other);
+
+    /** @brief Unmanages the file descriptor and returns the value with
+     *         ownership to the caller.
+     *
+     *  @return The file descriptor number
+     */
+    [[nodiscard]] int release();
+
+    int get() const override;
+
+  private:
+    detail::ManagedFdHandle handle;
+};
+
+} // namespace fd
+
+using fd::ManagedFd;
+
+} // namespace stdplus
diff --git a/src/stdplus/fd/ops.cpp b/src/stdplus/fd/ops.cpp
new file mode 100644
index 0000000..d96dbb5
--- /dev/null
+++ b/src/stdplus/fd/ops.cpp
@@ -0,0 +1,93 @@
+#include <fmt/format.h>
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <utility>
+
+namespace stdplus
+{
+namespace fd
+{
+namespace detail
+{
+
+template <typename Fun, typename Byte, typename... Args>
+static void opExact(const char* name, Fun&& fun, Fd& fd, span<Byte> data,
+                    Args&&... args)
+{
+    while (data.size() > 0)
+    {
+        auto ret = (fd.*fun)(data, std::forward<Args>(args)...);
+        if (ret.size() == 0)
+        {
+            throw exception::WouldBlock(
+                fmt::format("{} missing {}B", name, data.size()));
+        }
+        data = data.subspan(ret.size());
+    }
+}
+
+void readExact(Fd& fd, span<std::byte> data)
+{
+    opExact("readExact", &Fd::read, fd, data);
+}
+
+void recvExact(Fd& fd, span<std::byte> data, RecvFlags flags)
+{
+    opExact("recvExact", &Fd::recv, fd, data, flags);
+}
+
+void writeExact(Fd& fd, span<const std::byte> data)
+{
+    opExact("writeExact", &Fd::write, fd, data);
+}
+
+void sendExact(Fd& fd, span<const std::byte> data, SendFlags flags)
+{
+    opExact("sendExact", &Fd::send, fd, data, flags);
+}
+
+template <typename Fun, typename Byte, typename... Args>
+static span<Byte> opAligned(const char* name, Fun&& fun, Fd& fd, size_t align,
+                            span<Byte> data, Args&&... args)
+{
+    span<Byte> ret;
+    do
+    {
+        auto r =
+            (fd.*fun)(data.subspan(ret.size()), std::forward<Args>(args)...);
+        if (ret.size() != 0 && r.size() == 0)
+        {
+            throw exception::WouldBlock(
+                fmt::format("{} is {}B/{}B", name, ret.size() % align, align));
+        }
+        ret = data.subspan(0, ret.size() + r.size());
+    } while (ret.size() % align != 0);
+    return ret;
+}
+
+span<std::byte> readAligned(Fd& fd, size_t align, span<std::byte> buf)
+{
+    return opAligned("readAligned", &Fd::read, fd, align, buf);
+}
+
+span<std::byte> recvAligned(Fd& fd, size_t align, span<std::byte> buf,
+                            RecvFlags flags)
+{
+    return opAligned("recvAligned", &Fd::recv, fd, align, buf, flags);
+}
+
+span<const std::byte> writeAligned(Fd& fd, size_t align,
+                                   span<const std::byte> data)
+{
+    return opAligned("writeAligned", &Fd::write, fd, align, data);
+}
+
+span<const std::byte> sendAligned(Fd& fd, size_t align,
+                                  span<const std::byte> data, SendFlags flags)
+{
+    return opAligned("sendAligned", &Fd::send, fd, align, data, flags);
+}
+
+} // namespace detail
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/fd/ops.hpp b/src/stdplus/fd/ops.hpp
new file mode 100644
index 0000000..317eb30
--- /dev/null
+++ b/src/stdplus/fd/ops.hpp
@@ -0,0 +1,145 @@
+#pragma once
+#include <stdplus/fd/intf.hpp>
+#include <stdplus/raw.hpp>
+#include <stdplus/types.hpp>
+#include <utility>
+
+namespace stdplus
+{
+namespace fd
+{
+namespace detail
+{
+
+void readExact(Fd& fd, span<std::byte> data);
+void recvExact(Fd& fd, span<std::byte> data, RecvFlags flags);
+void writeExact(Fd& fd, span<const std::byte> data);
+void sendExact(Fd& fd, span<const std::byte> data, SendFlags flags);
+
+span<std::byte> readAligned(Fd& fd, size_t align, span<std::byte> buf);
+span<std::byte> recvAligned(Fd& fd, size_t align, span<std::byte> buf,
+                            RecvFlags flags);
+span<const std::byte> writeAligned(Fd& fd, size_t align,
+                                   span<const std::byte> data);
+span<const std::byte> sendAligned(Fd& fd, size_t align,
+                                  span<const std::byte> data, SendFlags flags);
+
+template <typename Fun, typename Container, typename... Args>
+auto alignedOp(Fun&& fun, Fd& fd, Container&& c, Args&&... args)
+{
+    using Data = raw::detail::dataType<Container>;
+    auto ret = fun(fd, sizeof(Data), raw::asSpan<std::byte>(c),
+                   std::forward<Args>(args)...);
+    return span<Data>(std::begin(c), ret.size() / sizeof(Data));
+}
+
+} // namespace detail
+
+template <typename Container>
+inline auto read(Fd& fd, Container&& c)
+{
+    return detail::alignedOp(detail::readAligned, fd,
+                             std::forward<Container>(c));
+}
+
+template <typename Container>
+inline auto recv(Fd& fd, Container&& c, RecvFlags flags)
+{
+    return detail::alignedOp(detail::recvAligned, fd,
+                             std::forward<Container>(c), flags);
+}
+
+template <typename Container>
+inline auto write(Fd& fd, Container&& c)
+{
+    return detail::alignedOp(detail::writeAligned, fd,
+                             std::forward<Container>(c));
+}
+
+template <typename Container>
+inline auto send(Fd& fd, Container&& c, SendFlags flags)
+{
+    return detail::alignedOp(detail::sendAligned, fd,
+                             std::forward<Container>(c), flags);
+}
+
+template <typename T>
+inline void readExact(Fd& fd, T&& t)
+{
+    detail::readExact(fd, raw::asSpan<std::byte>(t));
+}
+
+template <typename T>
+inline void recvExact(Fd& fd, T&& t, RecvFlags flags)
+{
+    detail::recvExact(fd, raw::asSpan<std::byte>(t), flags);
+}
+
+template <typename T>
+inline void writeExact(Fd& fd, T&& t)
+{
+    detail::writeExact(fd, raw::asSpan<std::byte>(t));
+}
+
+template <typename T>
+inline void sendExact(Fd& fd, T&& t, SendFlags flags)
+{
+    detail::sendExact(fd, raw::asSpan<std::byte>(t), flags);
+}
+
+inline size_t lseek(Fd& fd, off_t offset, Whence whence)
+{
+    return fd.lseek(offset, whence);
+}
+
+inline void truncate(Fd& fd, off_t size)
+{
+    return fd.truncate(size);
+}
+
+template <typename SockAddr>
+inline void bind(Fd& fd, SockAddr&& sockaddr)
+{
+    return fd.bind(raw::asSpan<std::byte>(sockaddr));
+}
+
+template <typename Opt>
+inline void setsockopt(Fd& fd, SockLevel level, SockOpt optname, Opt&& opt)
+{
+    return fd.setsockopt(level, optname, raw::asSpan<std::byte>(opt));
+}
+
+template <typename Data>
+inline int constIoctl(const Fd& fd, unsigned long id, Data&& data)
+{
+    return fd.constIoctl(id, raw::asSpan<std::byte>(data).data());
+}
+
+template <typename Data>
+inline int ioctl(Fd& fd, unsigned long id, Data&& data)
+{
+    return fd.ioctl(id, raw::asSpan<std::byte>(data).data());
+}
+
+inline FdFlags getFdFlags(const Fd& fd)
+{
+    return fd.fcntlGetfd();
+}
+
+inline void setFdFlags(Fd& fd, FdFlags flags)
+{
+    return fd.fcntlSetfd(flags);
+}
+
+inline FileFlags getFileFlags(const Fd& fd)
+{
+    return fd.fcntlGetfl();
+}
+
+inline void setFileFlags(Fd& fd, FileFlags flags)
+{
+    return fd.fcntlSetfl(flags);
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/src/stdplus/flags.hpp b/src/stdplus/flags.hpp
new file mode 100644
index 0000000..047c776
--- /dev/null
+++ b/src/stdplus/flags.hpp
@@ -0,0 +1,46 @@
+#pragma once
+#include <utility>
+
+namespace stdplus
+{
+
+template <typename Int, typename Flag = Int>
+class BitFlags
+{
+  public:
+    inline explicit BitFlags(Int val = 0) noexcept : val(val)
+    {
+    }
+
+    inline BitFlags& set(Flag flag) & noexcept
+    {
+        val |= static_cast<Int>(flag);
+        return *this;
+    }
+    inline BitFlags&& set(Flag flag) && noexcept
+    {
+        val |= static_cast<Int>(flag);
+        return std::move(*this);
+    }
+
+    inline BitFlags& unset(Flag flag) & noexcept
+    {
+        val &= ~static_cast<Int>(flag);
+        return *this;
+    }
+    inline BitFlags&& unset(Flag flag) && noexcept
+    {
+        val &= ~static_cast<Int>(flag);
+        return std::move(*this);
+    }
+
+    explicit inline operator Int() const noexcept
+    {
+        return val;
+    }
+
+  private:
+    Int val;
+};
+
+} // namespace stdplus
diff --git a/test/fd/dupable.cpp b/test/fd/dupable.cpp
new file mode 100644
index 0000000..2b34f9f
--- /dev/null
+++ b/test/fd/dupable.cpp
@@ -0,0 +1,14 @@
+#include <gtest/gtest.h>
+#include <stdplus/fd/dupable.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+TEST(DupableFd, Noop)
+{
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/test/fd/impl.cpp b/test/fd/impl.cpp
new file mode 100644
index 0000000..f2336f5
--- /dev/null
+++ b/test/fd/impl.cpp
@@ -0,0 +1,14 @@
+#include <gtest/gtest.h>
+#include <stdplus/fd/impl.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+TEST(DupableFd, Noop)
+{
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/test/fd/intf.cpp b/test/fd/intf.cpp
new file mode 100644
index 0000000..d4cc8c2
--- /dev/null
+++ b/test/fd/intf.cpp
@@ -0,0 +1,4 @@
+#include <stdplus/fd/intf.hpp>
+int main(int, char*[])
+{
+}
diff --git a/test/fd/managed.cpp b/test/fd/managed.cpp
new file mode 100644
index 0000000..003c4e3
--- /dev/null
+++ b/test/fd/managed.cpp
@@ -0,0 +1,14 @@
+#include <gtest/gtest.h>
+#include <stdplus/fd/managed.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+TEST(DupableFd, Noop)
+{
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/test/fd/mock.cpp b/test/fd/mock.cpp
new file mode 100644
index 0000000..0cc6dfd
--- /dev/null
+++ b/test/fd/mock.cpp
@@ -0,0 +1,12 @@
+#include <gtest/gtest.h>
+#include <stdplus/fd/gmock.hpp>
+
+namespace stdplus
+{
+
+TEST(Mock, Instantiate)
+{
+    FdMock fd;
+}
+
+} // namespace stdplus
diff --git a/test/fd/ops.cpp b/test/fd/ops.cpp
new file mode 100644
index 0000000..2d52101
--- /dev/null
+++ b/test/fd/ops.cpp
@@ -0,0 +1,17 @@
+#include <gtest/gtest.h>
+#include <stdplus/fd/ops.hpp>
+
+namespace stdplus
+{
+namespace fd
+{
+
+TEST(Flags, Flags)
+{
+    FdFlags f = FdFlags(0).set(FdFlag::CloseOnExec).unset(FdFlag::CloseOnExec);
+    f.set(FdFlag::CloseOnExec).unset(FdFlag::CloseOnExec);
+    EXPECT_EQ(0, static_cast<int>(f));
+}
+
+} // namespace fd
+} // namespace stdplus
diff --git a/test/meson.build b/test/meson.build
index 0de7168..9a9b060 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -27,6 +27,19 @@
   'util/string',
 ]
 
+if has_fd
+  gtests += [
+    'fd/dupable',
+    'fd/managed',
+    'fd/intf',
+    'fd/impl',
+    'fd/mock',
+    'fd/ops',
+  ]
+else
+  warning('Not testing file descriptor feature')
+endif
+
 if gtest.found() and gmock.found()
   foreach t : gtests
     test(t, executable(t.underscorify(), t + '.cpp',