From 1fb7ce5a604db78c4d02f719053827269705ce13 Mon Sep 17 00:00:00 2001
From: Ben Longbons <b.r.longbons@gmail.com>
Date: Tue, 29 Oct 2013 15:58:21 -0700
Subject: Add classes for reading and writing files

---
 src/io/fwd.hpp        |  32 ++++++++++
 src/io/lock.cpp       |  87 +++++++++++++++++++++++++
 src/io/lock.hpp       |  40 ++++++++++++
 src/io/read.cpp       | 122 +++++++++++++++++++++++++++++++++++
 src/io/read.hpp       |  51 +++++++++++++++
 src/io/read_test.cpp  |  59 +++++++++++++++++
 src/io/write.cpp      | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/io/write.hpp      |  66 +++++++++++++++++++
 src/io/write_test.cpp |  79 +++++++++++++++++++++++
 9 files changed, 710 insertions(+)
 create mode 100644 src/io/fwd.hpp
 create mode 100644 src/io/lock.cpp
 create mode 100644 src/io/lock.hpp
 create mode 100644 src/io/read.cpp
 create mode 100644 src/io/read.hpp
 create mode 100644 src/io/read_test.cpp
 create mode 100644 src/io/write.cpp
 create mode 100644 src/io/write.hpp
 create mode 100644 src/io/write_test.cpp

(limited to 'src/io')

diff --git a/src/io/fwd.hpp b/src/io/fwd.hpp
new file mode 100644
index 0000000..c68c437
--- /dev/null
+++ b/src/io/fwd.hpp
@@ -0,0 +1,32 @@
+#ifndef TMWA_IO_FWD_HPP
+#define TMWA_IO_FWD_HPP
+//    io/fwd.hpp - Forward declarations of I/O classes
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "../common/sanity.hpp"
+
+
+namespace io
+{
+    class ReadFile;
+    class WriteFile;
+    class AppendFile;
+} // namespace io
+
+#endif //TMWA_IO_FWD_HPP
diff --git a/src/io/lock.cpp b/src/io/lock.cpp
new file mode 100644
index 0000000..7ee4fbe
--- /dev/null
+++ b/src/io/lock.cpp
@@ -0,0 +1,87 @@
+#include "lock.hpp"
+//    io/lock.hpp - Output to files with atomic replacement and backups.
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "../strings/zstring.hpp"
+
+// TODO remove this violation of include order
+#include "../common/cxxstdio.hpp"
+
+#include "../poison.hpp"
+
+
+/// number of backups to keep
+static
+const int backup_count = 10;
+
+/// Protected file writing
+/// (Until the file is closed, it keeps the old file)
+
+// Start writing a tmpfile
+static
+int get_lock_open(ZString filename, int *info)
+{
+    int fd;
+    int no = getpid();
+
+    // Get a filename that doesn't already exist
+    FString newfile;
+    do
+    {
+        newfile = STRPRINTF("%s_%d.tmp", filename, no++);
+        fd = open(newfile.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0666);
+    }
+    while (fd == -1 && errno == EEXIST);
+    if (fd == -1)
+        abort();
+    *info = --no;
+    return fd;
+}
+
+namespace io
+{
+    WriteLock::WriteLock(FString fn, bool linebuffered)
+    : WriteFile(get_lock_open(fn, &tmp_suffix), linebuffered), filename(fn)
+    {}
+    WriteLock::~WriteLock()
+    {
+        if (!WriteFile::close())
+        {
+            // leave partial file
+            FPRINTF(stderr, "Warning: failed to write replacement for %s\n", filename);
+            abort();
+        }
+
+        int n = backup_count;
+        FString old_filename = STRPRINTF("%s.%d", filename, n);
+        while (--n)
+        {
+            FString newer_filename = STRPRINTF("%s.%d", filename, n);
+            rename(newer_filename.c_str(), old_filename.c_str());
+            old_filename = std::move(newer_filename);
+        }
+        rename(filename.c_str(), old_filename.c_str());
+
+        FString tmpfile = STRPRINTF("%s_%d.tmp", filename, tmp_suffix);
+        rename(tmpfile.c_str(), filename.c_str());
+    }
+} // namespace io
diff --git a/src/io/lock.hpp b/src/io/lock.hpp
new file mode 100644
index 0000000..49576a5
--- /dev/null
+++ b/src/io/lock.hpp
@@ -0,0 +1,40 @@
+#ifndef TMWA_IO_LOCK_HPP
+#define TMWA_IO_LOCK_HPP
+//    io/lock.hpp - Output to files with atomic replacement and backups.
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "write.hpp"
+
+#include "../strings/fstring.hpp"
+
+
+namespace io
+{
+    class WriteLock : public WriteFile
+    {
+        FString filename;
+        int tmp_suffix;
+    public:
+        WriteLock(FString filename, bool linebuffered=false);
+        ~WriteLock();
+        bool close() = delete;
+    };
+} // namespace io
+
+#endif // TMWA_IO_LOCK_HPP
diff --git a/src/io/read.cpp b/src/io/read.cpp
new file mode 100644
index 0000000..fad40bb
--- /dev/null
+++ b/src/io/read.cpp
@@ -0,0 +1,122 @@
+#include "read.hpp"
+//    io/read.cpp - Input from files
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "../strings/fstring.hpp"
+#include "../strings/mstring.hpp"
+#include "../strings/zstring.hpp"
+
+#include "../common/cxxstdio.hpp"
+
+#include "src/poison.hpp"
+
+
+namespace io
+{
+    ReadFile::ReadFile(int f)
+    : fd(f), start(0), end(0)
+    {
+    }
+    ReadFile::ReadFile(ZString name)
+    : fd(open(name.c_str(), O_RDONLY | O_CLOEXEC)), start(0), end(0)
+    {
+    }
+    ReadFile::~ReadFile()
+    {
+        close(fd);
+        fd = -1;
+    }
+
+    bool ReadFile::get(char& c)
+    {
+        if (start == end)
+        {
+            if (fd == -1)
+                return false;
+            ssize_t rv = read(fd, &buf, sizeof(buf));
+            if (rv == 0 || rv == -1)
+            {
+                close(fd);
+                fd = -1;
+                return false;
+            }
+            start = 0;
+            end = rv;
+        }
+        c = buf[start++];
+        return true;
+    }
+    size_t ReadFile::get(char *out, size_t len)
+    {
+        for (size_t i = 0; i < len; ++i)
+        {
+            if (!get(out[i]))
+                return i;
+        }
+        return len;
+    }
+    bool ReadFile::getline(FString& line)
+    {
+        MString tmp;
+        char c;
+        bool anything = false;
+        bool happy = false;
+        bool unhappy = false;
+        while (get(c))
+        {
+            anything = true;
+            if (c == '\r')
+            {
+                unhappy = true;
+                if (!get(c))
+                    break;
+                if (c != '\n')
+                    --start;
+                else
+                    happy = true;
+                break;
+            }
+            if (c == '\n')
+            {
+                happy = true;
+                break;
+            }
+            tmp += c;
+        }
+        if (unhappy)
+        {
+            if (happy)
+                PRINTF("warning: file contains CR\n");
+            else
+                PRINTF("warning: file contains bare CR\n");
+        }
+        else if (!happy && anything)
+            PRINTF("warning: file does not contain a trailing newline\n");
+        line = FString(tmp);
+        return anything;
+    }
+
+    bool ReadFile::is_open()
+    {
+        return fd != -1;
+    }
+} // namespace io
diff --git a/src/io/read.hpp b/src/io/read.hpp
new file mode 100644
index 0000000..d5b8c23
--- /dev/null
+++ b/src/io/read.hpp
@@ -0,0 +1,51 @@
+#ifndef TMWA_IO_READ_HPP
+#define TMWA_IO_READ_HPP
+//    io/read.hpp - Input from files.
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "../common/sanity.hpp"
+
+#include "../strings/fwd.hpp"
+
+
+namespace io
+{
+    class ReadFile
+    {
+    private:
+        int fd;
+        unsigned short start, end;
+        char buf[4096];
+    public:
+        explicit
+        ReadFile(int fd);
+        explicit
+        ReadFile(ZString name);
+        ReadFile(ReadFile&&) = delete;
+        ~ReadFile();
+
+        bool get(char&);
+        size_t get(char *buf, size_t len);
+        bool getline(FString&);
+
+        bool is_open();
+    };
+} // namespace io
+
+#endif //TMWA_IO_READ_HPP
diff --git a/src/io/read_test.cpp b/src/io/read_test.cpp
new file mode 100644
index 0000000..193777f
--- /dev/null
+++ b/src/io/read_test.cpp
@@ -0,0 +1,59 @@
+#include "read.hpp"
+
+#include <gtest/gtest.h>
+
+#include "../strings/zstring.hpp"
+
+static
+int string_pipe(ZString sz)
+{
+    // if this doesn't work we'll get test failures anyway
+    int pfd[2];
+    pipe(pfd);
+    write(pfd[1], sz.c_str(), sz.size());
+    close(pfd[1]);
+    return pfd[0];
+}
+
+TEST(io, read1)
+{
+    io::ReadFile rf(string_pipe("Hello"));
+    FString hi;
+    EXPECT_TRUE(rf.getline(hi));
+    EXPECT_EQ(hi, "Hello");
+    EXPECT_FALSE(rf.getline(hi));
+}
+TEST(io, read2)
+{
+    io::ReadFile rf(string_pipe("Hello\n"));
+    FString hi;
+    EXPECT_TRUE(rf.getline(hi));
+    EXPECT_EQ(hi, "Hello");
+    EXPECT_FALSE(rf.getline(hi));
+}
+TEST(io, read3)
+{
+    io::ReadFile rf(string_pipe("Hello\r"));
+    FString hi;
+    EXPECT_TRUE(rf.getline(hi));
+    EXPECT_EQ(hi, "Hello");
+    EXPECT_FALSE(rf.getline(hi));
+}
+TEST(io, read4)
+{
+    io::ReadFile rf(string_pipe("Hello\r\n"));
+    FString hi;
+    EXPECT_TRUE(rf.getline(hi));
+    EXPECT_EQ(hi, "Hello");
+    EXPECT_FALSE(rf.getline(hi));
+}
+TEST(io, read5)
+{
+    io::ReadFile rf(string_pipe("Hello\n\r"));
+    FString hi;
+    EXPECT_TRUE(rf.getline(hi));
+    EXPECT_EQ(hi, "Hello");
+    EXPECT_TRUE(rf.getline(hi));
+    EXPECT_FALSE(hi);
+    EXPECT_FALSE(rf.getline(hi));
+}
diff --git a/src/io/write.cpp b/src/io/write.cpp
new file mode 100644
index 0000000..5f8a975
--- /dev/null
+++ b/src/io/write.cpp
@@ -0,0 +1,174 @@
+#include "write.hpp"
+//    io/write.cpp - Output to files
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <sys/uio.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "../strings/xstring.hpp"
+
+#include "src/poison.hpp"
+
+
+namespace io
+{
+    WriteFile::WriteFile(int f, bool linebuffered)
+    : fd(f), lb(linebuffered), buflen(0)
+    {}
+    WriteFile::WriteFile(ZString name, bool linebuffered)
+    : fd(open(name.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666)), lb(linebuffered), buflen(0)
+    {}
+    WriteFile::~WriteFile()
+    {
+        if (fd != -1)
+        {
+            if (!close())
+                abort();
+        }
+    }
+
+    void WriteFile::put(char c)
+    {
+        really_put(&c, 1);
+    }
+    void WriteFile::really_put(const char *dat, size_t len)
+    {
+        if (len + buflen <= sizeof(buf))
+        {
+            std::copy(dat, dat + len, buf + buflen);
+            buflen += len;
+            goto maybe_linebuffered;
+        }
+
+        {
+            size_t offset = 0;
+            while (offset < buflen)
+            {
+                // iovec is used for both reading and writing,
+                // so it declares the pointer to non-const.
+                iovec iov[2] =
+                {
+                    {buf + offset, buflen - offset},
+                    {const_cast<char *>(dat), len},
+                };
+                ssize_t rv = writev(fd, iov, 2);
+                if (rv <= 0)
+                    goto write_fail;
+                offset += rv;
+            }
+            offset -= buflen;
+            dat += offset;
+            len -= offset;
+        }
+
+        while (len > sizeof(buf))
+        {
+            ssize_t rv = write(fd, dat, len);
+            if (rv <= 0)
+                goto write_fail;
+            dat += rv;
+            len -= rv;
+        }
+        buflen = len;
+
+    maybe_linebuffered:
+        if (lb)
+        {
+            auto rbegin = std::reverse_iterator<char *>(buf + buflen);
+            auto rend = std::reverse_iterator<char *>(buf);
+            auto last_nl = std::find(rbegin, rend, '\n');
+
+            if (last_nl != rend)
+            {
+                size_t offset = 0;
+                size_t remaining = rend - last_nl;
+                while (remaining)
+                {
+                    ssize_t rv = write(fd, buf + offset, remaining);
+                    if (rv <= 0)
+                        goto write_fail;
+                    offset += rv;
+                    remaining -= rv;
+                }
+                std::copy(buf + offset, buf + buflen, buf);
+                buflen -= offset;
+            }
+        }
+        return;
+
+    write_fail:
+        ::close(fd);
+        fd = -1;
+        return;
+    }
+
+    void WriteFile::put_line(XString xs)
+    {
+        really_put(xs.data(), xs.size());
+        if (!xs.endswith('\n'))
+            put('\n');
+    }
+
+    bool WriteFile::close()
+    {
+        size_t off = 0;
+        while (off < buflen)
+        {
+            ssize_t rv = write(fd, buf + off, buflen - off);
+            if (rv <= 0)
+            {
+                ::close(fd);
+                fd = -1;
+                return false;
+            }
+            off += rv;
+        }
+
+        int f = fd;
+        fd = -1;
+        return ::close(f) == 0;
+    }
+
+    bool WriteFile::is_open()
+    {
+        return fd != -1;
+    }
+
+    AppendFile::AppendFile(ZString name, bool linebuffered)
+    : WriteFile(open(name.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666), linebuffered)
+    {}
+
+    int do_vprint(WriteFile& out, const char *fmt, va_list ap)
+    {
+        int len;
+        {
+            va_list ap2;
+            va_copy(ap2, ap);
+            len = vsnprintf(nullptr, 0, fmt, ap2);
+            va_end(ap2);
+        }
+        char buffer[len + 1];
+        vsnprintf(buffer, len + 1, fmt, ap);
+
+        out.really_put(buffer, len);
+        return len;
+    }
+} // namespace io
diff --git a/src/io/write.hpp b/src/io/write.hpp
new file mode 100644
index 0000000..f0aa584
--- /dev/null
+++ b/src/io/write.hpp
@@ -0,0 +1,66 @@
+#ifndef TMWA_IO_WRITE_HPP
+#define TMWA_IO_WRITE_HPP
+//    io/write.hpp - Output to files.
+//
+//    Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+//    This file is part of The Mana World (Athena server)
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 3 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "../common/sanity.hpp"
+
+#include <cstdarg>
+
+#include "../strings/fwd.hpp"
+
+namespace io
+{
+    class WriteFile
+    {
+    private:
+        int fd;
+        bool lb;
+        struct {} _unused;
+        unsigned short buflen;
+        char buf[4096];
+    public:
+        explicit
+        WriteFile(int fd, bool linebuffered=false);
+        explicit
+        WriteFile(ZString name, bool linebuffered=false);
+        WriteFile(WriteFile&&) = delete;
+        ~WriteFile();
+
+        void put(char);
+        void really_put(const char *dat, size_t len);
+        void put_line(XString);
+
+        __attribute__((warn_unused_result))
+        bool close();
+        bool is_open();
+    };
+
+    class AppendFile : public WriteFile
+    {
+    public:
+        explicit
+        AppendFile(ZString name, bool linebuffered=false);
+    };
+
+    __attribute__((format(printf, 2, 0)))
+    int do_vprint(WriteFile& out, const char *fmt, va_list ap);
+} // namespace io
+
+#endif //TMWA_IO_WRITE_HPP
diff --git a/src/io/write_test.cpp b/src/io/write_test.cpp
new file mode 100644
index 0000000..36ab556
--- /dev/null
+++ b/src/io/write_test.cpp
@@ -0,0 +1,79 @@
+#include "write.hpp"
+
+#include <gtest/gtest.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "../strings/fstring.hpp"
+#include "../strings/mstring.hpp"
+#include "../strings/xstring.hpp"
+
+static
+int pipew(int& rfd)
+{
+    int pfd[2];
+    pipe2(pfd, O_NONBLOCK);
+    rfd = pfd[0];
+    return pfd[1];
+}
+
+class PipeWriter
+{
+private:
+    int rfd;
+public:
+    io::WriteFile wf;
+public:
+    PipeWriter(bool lb)
+    : wf(pipew(rfd), lb)
+    {}
+    ~PipeWriter()
+    {
+        close(rfd);
+    }
+    FString slurp()
+    {
+        MString tmp;
+        char buf[4096];
+        while (true)
+        {
+            ssize_t rv = read(rfd, buf, sizeof(buf));
+            if (rv == -1)
+            {
+                if (errno != EAGAIN)
+                    return {"Error, read failed :("};
+                rv = 0;
+            }
+            if (rv == 0)
+                break;
+            tmp += XString(buf + 0, buf + rv, nullptr);
+        }
+        return FString(tmp);
+    }
+};
+
+TEST(io, write1)
+{
+    PipeWriter pw(false);
+    io::WriteFile& wf = pw.wf;
+    wf.really_put("Hello, ", 7);
+    EXPECT_EQ("", pw.slurp());
+    wf.put_line("World!\n");
+    EXPECT_EQ("", pw.slurp());
+    EXPECT_TRUE(wf.close());
+    EXPECT_EQ("Hello, World!\n", pw.slurp());
+}
+
+TEST(io, write2)
+{
+    PipeWriter pw(true);
+    io::WriteFile& wf = pw.wf;
+    wf.really_put("Hello, ", 7);
+    EXPECT_EQ("", std::string(pw.slurp().c_str()));
+    wf.put_line("World!");
+    wf.really_put("XXX", 3);
+    EXPECT_EQ("Hello, World!\n", std::string(pw.slurp().c_str()));
+    EXPECT_TRUE(wf.close());
+    EXPECT_EQ("XXX", std::string(pw.slurp().c_str()));
+}
-- 
cgit v1.2.3-70-g09d2