diff options
Diffstat (limited to 'src/io')
-rw-r--r-- | src/io/fwd.hpp | 32 | ||||
-rw-r--r-- | src/io/lock.cpp | 87 | ||||
-rw-r--r-- | src/io/lock.hpp | 40 | ||||
-rw-r--r-- | src/io/read.cpp | 122 | ||||
-rw-r--r-- | src/io/read.hpp | 51 | ||||
-rw-r--r-- | src/io/read_test.cpp | 59 | ||||
-rw-r--r-- | src/io/write.cpp | 174 | ||||
-rw-r--r-- | src/io/write.hpp | 66 | ||||
-rw-r--r-- | src/io/write_test.cpp | 79 |
9 files changed, 710 insertions, 0 deletions
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())); +} |