kge/Buffer.cc

364 lines
7.0 KiB
C++

///
/// \file Buffer.cc
/// \author kyle
/// \created 10/10/23
/// \brief Buffer implementation.
///
/// \section COPYRIGHT
/// Copyright 2023 K. Isom <kyle@imap.cc>
///
/// 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 <filesystem>
#include <iterator>
#include <optional>
#include <random>
#include <sstream>
#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<uint8_t>(*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<uint8_t> 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();
}