/// /// \file Buffer.cc /// \author kyle /// \created 10/10/23 /// \brief Buffer implementation. /// /// \section COPYRIGHT /// Copyright 2023 K. Isom /// /// Permission to use, copy, modify, and/or distribute this software for /// any purpose with or without fee is hereby granted, provided that the /// above copyright notice and this permission notice appear in all copies. /// /// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL /// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED /// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR /// BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES /// OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, /// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, /// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS /// SOFTWARE. /// #include #include #include #include #include #include "Buffer.h" static constexpr auto statusOK = Buffer::FileStatus::FileStatusOK; static std::string anonymousName() { std::uniform_int_distribution<> dist(1000, 9999); std::random_device randomDevice; std::mt19937 rng(randomDevice()); std::stringstream ss; ss << "Buffer<" << dist(rng) << ">"; return ss.str(); } std::string Buffer::FileStatusToString(Buffer::FileStatus status) { switch (status) { case FileStatus::FileStatusOK: return std::string("OK"); case FileStatus::FileStatusReadOnly: return std::string("read-only file"); case FileStatus::FileStatusIOFailed: return std::string("I/O failure"); case FileStatus::FileStatusVirtual: return std::string("virtual buffer"); case FileStatus::FileStatusInvalidPermissions: return std::string("invalid permissions"); case FileStatus::FileStatusNonExistent: return std::string("file does not exist"); default: abort(); } } bool Buffer::StatusOK(FileStatus status) { return status == FileStatus::FileStatusOK; } Buffer::Buffer() : dirty(false), name(anonymousName()), status(statusOK) { } Buffer::Buffer(std::string fName) : dirty(false), name(std::move(fName)) { } Buffer::Buffer(std::filesystem::path fPath) : dirty(false), path(OptString(fPath.string())) { this->name = fPath.filename().string(); this->cursor = Cursor(); this->file = OptFile(fPath.string()); // N.B. I am leaving this in to show that I thought about it, but // it's the wrong choice. A Frame should call Refresh on a buffer // when it's ready to load it. // if (this->Exists()) { // /// \todo Should I signal an error here, or is it // /// okay for this to be a best-effort thing? // this->status = this->Refresh(); // } } Buffer::Buffer(std::string fName, std::string fPath) : dirty(false), name(std::move(fName)), path(fPath) { if (this->path) { this->file = OptFile(File(fPath)); } } void Buffer::Close() { this->clearContents(); this->dirty = false; this->file = std::nullopt; this->path = std::nullopt; this->name = std::string("deleted buffer (" + this->name + ")"); } const std::string Buffer::Name() { return this->name; } bool Buffer::IsVirtual() { return !this->file.has_value(); } Buffer::FileStatus Buffer::Flush(OptString altPath) { OptOutFileStream handle = std::nullopt; if (altPath) { auto altFile = File(altPath.value()); handle = altFile.Flush(); } else if (this->file) { handle = this->file.value().Flush(); } else { // At this point, we have no alternate path to write out // and this Buffer isn't file-backed. From this point // forward, we can operate under the assumption that // `this->file` is a valid file. return Buffer::FileStatus::FileStatusVirtual; } auto realFile = this->file.value(); if (!handle) { if (realFile.IsReadOnly()) { return Buffer::FileStatus::FileStatusReadOnly; } if (!realFile.IsWriteable()) { return Buffer::FileStatus::FileStatusInvalidPermissions; } realFile.MarkReadOnly(); return Buffer::FileStatus::FileStatusIOFailed; } /// At this point, we know we're working with a valid file handle. auto realHandle = handle.value(); if (!realHandle->good()) { return Buffer::FileStatus::FileStatusIOFailed; } for (auto line: this->contents) { std::copy(line.begin(), line.end(), std::ostream_iterator(*realHandle, "")); } realHandle->flush(); realHandle->close(); delete realHandle; this->dirty = false; return Buffer::FileStatus::FileStatusOK; } Buffer::FileStatus Buffer::Refresh() { /// We can't actually refresh a virtual buffer. Unlike flush, /// it doesn't make sense to Refresh from an alternate file. if (this->IsVirtual()) { return Buffer::FileStatus::FileStatusVirtual; } auto realFile = this->file.value(); auto handle = realFile.Refresh(); if (!handle) { return FileStatus::FileStatusNonExistent; } auto realHandle = handle.value(); this->clearContents(); this->dirty = false; // clean slate, kiddo /// \todo Moral quandary: if the file disappears from disk, we /// probably want to know that, but maybe clearing the /// contents first isn't the right move. Maybe mark the /// file as ReadOnly? if (!realHandle->good()) { realHandle->close(); delete realHandle; return FileStatus::FileStatusIOFailed; } while (realHandle->good()) { std::string temp; while (std::getline(*realHandle, temp)) { std::vector row(temp.begin(), temp.end()); row.push_back('\n'); this->contents.push_back(row); } } auto status = Buffer::FileStatus::FileStatusOK; if (!realHandle->eof()) { status = FileStatus::FileStatusIOFailed; } realHandle->close(); delete realHandle; return status; } void Buffer::Rename(std::string fName) { this->name = fName; } void Buffer::ChangePath(std::string newPath) { this->path = OptString(std::move(newPath)); } Cursor Buffer::Where() { return this->cursor; } void Buffer::MarkDirty() { if (this->dirty) { return; } this->dirty = true; } bool Buffer::IsDirty() { return this->dirty; } size_t Buffer::Size() { size_t size = 0; for (const auto &line: this->contents) { size += line.size(); } return size; } bool Buffer::Exists() { if (!this->file) { return false; } return this->file.value().Exists(); } void Buffer::PrintBufferStatus(std::ostream &os) { os << "buffer:<" << this->name << ">["; if (this->IsVirtual()) { os << "virt]"; } else { os << "file:"; if (this->Exists()) { os << "real"; } else { os << "new"; } os << ":"; auto &realFile = this->file.value(); if (realFile.IsReadWriteable()) { os << "rw"; } else if (realFile.IsReadable()) { os << "r-"; } else if (realFile.IsWriteable()) { os << "-w"; } else { os << " "; } if (realFile.IsReadOnly()) { os << "@"; } else { os << "-"; } if (this->IsDirty()) { os << "!"; } else { os << "-"; } os << ":" << this->Size() << "B"; } os << "\n"; return; } void Buffer::clearContents() { for (auto &line: this->contents) { line.clear(); } this->contents.clear(); }