// ===================================================================================================================== // fennec, a free and open source game engine // Copyright © 2025 Medusa Slockbower // // 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 . // ===================================================================================================================== #include #include #include #include #include #include #ifdef FENNEC_COMPILER_MSVC #else #include #endif namespace fennec { constexpr const cstring fmode_translate(uint8_t mode) { if (not file::is_valid(mode)) { return ""; } mode &= ~fmode_wide; // Ignore wide bit switch (mode) { case fmode_read: return "r"; case fmode_write: return "a"; // I chose r+ because for most read/write assets in the context of the game engine will be setting files, save files, // and editor assets, which require a complete read of the file before any writing is done case fmode_read | fmode_write: return "r+"; case fmode_write | fmode_trunc: return "w"; case fmode_read | fmode_write | fmode_trunc: return "w+"; case fmode_write | fmode_trunc | fmode_exclusive: return "wx"; case fmode_read | fmode_write | fmode_trunc | fmode_exclusive: return "wx+"; // Binary Files case fmode_binary | fmode_read: return "rb"; case fmode_binary | fmode_write: return "ab"; case fmode_binary | fmode_read | fmode_write: return "rb+"; case fmode_binary | fmode_write | fmode_trunc: return "wb"; case fmode_binary | fmode_read | fmode_write | fmode_trunc: return "wb+"; case fmode_binary | fmode_write | fmode_trunc | fmode_exclusive: return "wxb"; case fmode_binary | fmode_read | fmode_write | fmode_trunc | fmode_exclusive: return "wxb+"; default: return ""; } } file& file::cout() { static constexpr char path[] = "stdout"; static file out = []() -> file { file res; res._mode = fmode_write; res._handle = stdout; res._path = string(path); return res; }(); return out; } file& file::cin() { static constexpr char path[] = "stdin"; static file out = []() -> file { file res; res._mode = fmode_read; res._handle = stdin; res._path = string(path); return res; }(); return out; } file& file::cerr() { static constexpr char path[] = "stderr"; static file out = []() -> file { file res; res._mode = fmode_write; res._handle = stderr; res._path = string(path); return res; }(); return out; } file::file() : _handle(nullptr) , _path("") , _mode(0) , _error(nullptr) { } file::~file() { close(); } file::file(file&& file) noexcept : _handle(file._handle), _path(file._path), _mode(file._mode), _error(file._error) { file._handle = nullptr; file._error = nullptr; } file& file::operator=(file&& file) noexcept { assert(_error == nullptr, "Attempted Operation on a File in an Errored State"); close(); _handle = file._handle; _path = file._path; _mode = file._mode; _error = file._error; file._handle = nullptr; file._path = ""; file._error = nullptr; return *this; } bool file::open(const cstring& p, uint8_t mode) { assert(_error == nullptr, "Attempted Operation on a File in an Errored State"); // Ensure validity of the mode if (not is_valid(mode)) { return true; } // Close the file if already open if (_handle) { close(); } // Attempt to open the file _handle = fopen(p, fmode_translate(mode)); if (_handle == nullptr) { _error = strerror(errno); return true; } // Attempt to lock the file if (flock(fileno(_handle), LOCK_EX)) { _error = strerror(errno); fclose(_handle); _handle = nullptr; return true; } _mode = mode; _path = p; _path = _path.absolute(); return false; } bool file::open(const string& p, uint8_t mode) { assert(_error == nullptr, "Attempted Operation on a File in an Errored State"); // Ensure validity of the mode if (not is_valid(mode)) { return true; } // Close the file if already open if (_handle) { close(); } // Attempt to open the file _handle = fopen(p.cstr(), fmode_translate(mode)); // Validate the file if (_handle == nullptr) { _error = strerror(errno); return true; } // Attempt to lock the file if (flock(fileno(_handle), LOCK_EX)) { _error = strerror(errno); fclose(_handle); _handle = nullptr; return true; } // Set the orientation fwide(_handle, mode & fmode_wide ? 1 : -1); _mode = mode; _path = p; _path = _path.absolute(); return false; } bool file::open(const path& p, uint8_t mode) { assert(_error == nullptr, "Attempted Operation on a File in an Errored State"); // Ensure validity of the mode if (not is_valid(mode)) { return true; } // Close the file if already open if (_handle) { close(); } // Attempt to open the file _handle = fopen(p.cstr(), fmode_translate(mode)); // Validate the file if (_handle == nullptr) { _error = strerror(errno); return true; } // Attempt to lock the file if (flock(fileno(_handle), LOCK_EX)) { _error = strerror(errno); fclose(_handle); _handle = nullptr; return true; } // Set the orientation fwide(_handle, mode & fmode_wide ? 1 : -1); _mode = mode; _path = p.absolute(); return false; } bool file::close() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } // Attempt to unlock the file if (flock(fileno(_handle), LOCK_UN)) { _error = strerror(errno); return true; } // Close the file and reset variables fclose(_handle); _mode = 0; _handle = nullptr; _path = ""; return false; } bool file::commit() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } // Attempt to flush if (fflush(_handle)) { _error = strerror(errno); return true; } return false; } bool file::erase() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } // Close the file const path path = move(_path); if (close()) { return true; } // Erase the file remove(path.cstr()); return false; } bool file::rename(const cstring& str) { static const size_t page_size = pagesize(); assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } // Validate path path fpath = str; fpath = fpath.absolute(); if (_path == fpath) { return false; } // Attempt to open the new file FILE* fnew = fopen(fpath.cstr(), "wx"); // Check for open failure if (fnew == nullptr) { _error = strerror(errno); return true; } // Reopen this file as read _handle = freopen(nullptr, "r", _handle); // Check if it failed to reopen if (_handle == nullptr) { _error = strerror(errno); return true; } // Initialize buffer void* buffer = operator new(page_size, static_cast(page_size)); // Copy contents size_t read = 0; while ((read = fread(buffer, 1, page_size, _handle)) == page_size) { if (fwrite(buffer, 1, page_size, fnew) != page_size) { break; } } // Handle eof if (feof(_handle)) { fwrite(buffer, 1, read, fnew); } // Check new file for errors if (ferror(fnew)) { _error = strerror(errno); fclose(fnew); _mode = fmode_read; return true; } // Check the original file for errors if (ferror(_handle)) { _error = strerror(errno); fclose(fnew); fclose(_handle); _handle = nullptr; _mode = 0; _path = ""; return true; } // Cleanup the buffer operator delete(buffer, page_size, static_cast(page_size)); // Close old file fclose(_handle); // Reopen the new file _handle = freopen(fpath.cstr(), fmode_translate(_mode), fnew); // Check for open failure if (_handle == nullptr) { _error = strerror(errno); return true; } // Erase the old file remove(_path.cstr()); // Set the new path _path = fpath; return false; } bool file::rename(const string& str) { static const size_t page_size = pagesize(); assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } // Validate path path fpath = str; fpath = fpath.absolute(); if (_path == fpath) { return false; } // Attempt to open the new file FILE* fnew = fopen(fpath.cstr(), "wx"); // Check for open failure if (fnew == nullptr) { _error = strerror(errno); return true; } // Reopen this file as read _handle = freopen(nullptr, "r", _handle); // Check if it failed to reopen if (_handle == nullptr) { _error = strerror(errno); return true; } // Initialize buffer void* buffer = operator new(page_size, static_cast(page_size)); // Copy contents size_t read = 0; while ((read = fread(buffer, 1, page_size, _handle)) == page_size) { if (fwrite(buffer, 1, page_size, fnew) != page_size) { break; } } // Handle eof if (feof(_handle)) { fwrite(buffer, 1, read, fnew); } // Check new file for errors if (ferror(fnew)) { _error = strerror(errno); fclose(fnew); _mode = fmode_read; return true; } // Check the original file for errors if (ferror(_handle)) { _error = strerror(errno); fclose(fnew); fclose(_handle); _handle = nullptr; _mode = 0; _path = ""; return true; } // Cleanup the buffer operator delete(buffer, page_size, static_cast(page_size)); // Close old file fclose(_handle); // Reopen the new file _handle = freopen(_path.cstr(), fmode_translate(_mode), fnew); // Check for open failure if (_handle == nullptr) { _error = strerror(errno); return true; } // Set the new path _path = fpath; return false; } bool file::rename(const path& p) { static const size_t page_size = pagesize(); assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } // Validate path const path fpath = p.absolute(); if (_path == fpath) { return false; } // Attempt to open the new file FILE* fnew = fopen(fpath.cstr(), "wx"); // Check for open failure if (fnew == nullptr) { _error = strerror(errno); return true; } // Reopen this file as read _handle = freopen(nullptr, "r", _handle); // Check if it failed to reopen if (_handle == nullptr) { _error = strerror(errno); return true; } // Initialize buffer void* buffer = operator new(page_size, static_cast(page_size)); // Copy contents size_t read = 0; while ((read = fread(buffer, 1, page_size, _handle)) == page_size) { if (fwrite(buffer, 1, page_size, fnew) != page_size) { break; } } // Handle eof if (feof(_handle)) { fwrite(buffer, 1, read, fnew); } // Check new file for errors if (ferror(fnew)) { _error = strerror(errno); fclose(fnew); _mode = fmode_read; return true; } // Check the original file for errors if (ferror(_handle)) { _error = strerror(errno); fclose(fnew); fclose(_handle); _handle = nullptr; _mode = 0; _path = ""; return true; } // Cleanup the buffer operator delete(buffer, page_size, static_cast(page_size)); // Close old file fclose(_handle); // Reopen the new file _handle = freopen(_path.cstr(), fmode_translate(_mode), fnew); // Check for open failure if (_handle == nullptr) { _error = strerror(errno); return true; } // Set the new path _path = fpath; return false; } file file::copy(const cstring& str) { static const size_t page_size = pagesize(); assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return file(); } // Validate path path fpath = str; fpath = fpath.absolute(); if (_path == fpath) { return file(); } // Attempt to open the new file FILE* fnew = fopen(fpath.cstr(), "wx"); // Check for open failure if (fnew == nullptr) { _error = strerror(errno); return file(); } // Reopen this file as read _handle = freopen(nullptr, "r", _handle); // Check if it failed to reopen if (_handle == nullptr) { _error = strerror(errno); return file(); } // Initialize buffer void* buffer = operator new(page_size, static_cast(page_size)); // Copy contents size_t read = 0; while ((read = fread(buffer, 1, page_size, _handle)) == page_size) { if (fwrite(buffer, 1, page_size, fnew) != page_size) { break; } } // Handle eof if (feof(_handle)) { fwrite(buffer, 1, read, fnew); } // Check new file for errors if (ferror(fnew)) { _error = strerror(errno); fclose(fnew); _mode = fmode_read; return file(); } // Check the original file for errors if (ferror(_handle)) { _error = strerror(errno); fclose(fnew); fclose(_handle); _handle = nullptr; _mode = 0; _path = ""; return file(); } // Cleanup the buffer operator delete(buffer, page_size, static_cast(page_size)); // Reopen the new file _handle = freopen(nullptr, fmode_translate(_mode), _handle); // Check for open failure if (_handle == nullptr) { _error = strerror(errno); return file(); } // Set the new path file res; res._handle = fnew; res._path = fpath; res._mode = fmode_write; return res; } file file::copy(const string& str) { static const size_t page_size = pagesize(); assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return file(); } // Validate path path fpath = str; fpath = fpath.absolute(); if (_path == fpath) { return file(); } // Attempt to open the new file FILE* fnew = fopen(fpath.cstr(), "wx"); // Check for open failure if (fnew == nullptr) { _error = strerror(errno); return file(); } // Reopen this file as read _handle = freopen(nullptr, "r", _handle); // Check if it failed to reopen if (_handle == nullptr) { _error = strerror(errno); return file(); } // Initialize buffer void* buffer = operator new(page_size, static_cast(page_size)); // Copy contents size_t read = 0; while ((read = fread(buffer, 1, page_size, _handle)) == page_size) { if (fwrite(buffer, 1, page_size, fnew) != page_size) { break; } } // Handle eof if (feof(_handle)) { fwrite(buffer, 1, read, fnew); } // Check new file for errors if (ferror(fnew)) { operator delete(buffer, page_size, static_cast(page_size)); _error = strerror(errno); fclose(fnew); _mode = fmode_read; return file(); } // Check the original file for errors if (ferror(_handle)) { operator delete(buffer, page_size, static_cast(page_size)); _error = strerror(errno); fclose(fnew); fclose(_handle); _handle = nullptr; _mode = 0; _path = ""; return file(); } // Cleanup the buffer operator delete(buffer, page_size, static_cast(page_size)); // Reopen the new file _handle = freopen(nullptr, fmode_translate(_mode), _handle); // Check for open failure if (_handle == nullptr) { _error = strerror(errno); return file(); } // Set the new path file res; res._handle = fnew; res._path = fpath; res._mode = fmode_write; return res; } file file::copy(const path& p) { static const size_t page_size = pagesize(); assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return file(); } // Validate path const path fpath = p.absolute(); if (_path == fpath) { return file(); } // Attempt to open the new file FILE* fnew = fopen(fpath.cstr(), "wx"); // Check for open failure if (fnew == nullptr) { _error = strerror(errno); return file(); } // Reopen this file as read _handle = freopen(nullptr, "r", _handle); // Check if it failed to reopen if (_handle == nullptr) { _error = strerror(errno); return file(); } // Initialize buffer void* buffer = operator new(page_size, static_cast(page_size)); // Copy contents size_t read = 0; while ((read = fread(buffer, 1, page_size, _handle)) == page_size) { if (fwrite(buffer, 1, page_size, fnew) != page_size) { break; } } // Handle eof if (feof(_handle)) { fwrite(buffer, 1, read, fnew); } // Check new file for errors if (ferror(fnew)) { operator delete(buffer, page_size, static_cast(page_size)); _error = strerror(errno); fclose(fnew); _mode = fmode_read; return file(); } // Check the original file for errors if (ferror(_handle)) { operator delete(buffer, page_size, static_cast(page_size)); _error = strerror(errno); fclose(fnew); fclose(_handle); _handle = nullptr; _mode = 0; _path = ""; return file(); } // Cleanup the buffer operator delete(buffer, page_size, static_cast(page_size)); // Reopen the new file _handle = freopen(nullptr, fmode_translate(_mode), _handle); // Check for open failure if (_handle == nullptr) { _error = strerror(errno); return file(); } // Set the new path file res; res._handle = fnew; res._path = fpath; res._mode = fmode_write; return res; } size_t file::get_pos() const { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return npos; } return ftell(_handle); } bool file::set_pos(size_t i) { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } if (fseek(_handle, i, SEEK_SET)) { _error = strerror(errno); return true; } return false; } bool file::rewind() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } ::rewind(_handle); if (ferror(_handle)) { _error = strerror(errno); return true; } return false; } bool file::eof() const { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return false; } return feof(_handle); } char file::getc() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); assert(not(_mode & fmode_wide), "Attempted Wide Operation on Byte File"); // Check if there is a file if (_handle == nullptr) { return 0; } return fgetc(_handle); } wchar_t file::getwc() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); assert(_mode & fmode_wide, "Attempted Byte Operation on Wide File"); // Check if there is a file if (_handle == nullptr) { return 0; } return fgetwc(_handle); } size_t file::read(void* data, size_t size, size_t n) { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return 0; } const size_t read = fread(data, size, n, _handle); if (read != size && ferror(_handle)) { _error = strerror(errno); } return read; } size_t file::write(const void* data, size_t size, size_t n) { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); // Check if there is a file if (_handle == nullptr) { return 0; } const size_t r = fwrite(data, size, n, _handle); if (r != size && ferror(_handle)) { _error = strerror(errno); return 0; } return r; } string file::getline() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); assert(not(_mode & fmode_wide), "Attempted Wide Operation on Byte File"); // Check if there is a file if (_handle == nullptr) { return string{ "" }; } // Read the next line; char arr[LINE_MAX + 2] = { L'\0' }; cstring buff = arr; string res{""}; // read until first newline or end of file while (fgets(arr, LINE_MAX + 2, _handle)) { res += cstring(arr, buff.length()); if (buff.length() < LINE_MAX || buff[LINE_MAX] == L'\n') { return res; } } _error = strerror(errno); return string(""); } wstring file::getwline() { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); assert(_mode & fmode_wide, "Attempted Byte Operation on Wide File"); // Check if there is a file if (_handle == nullptr) { return _wstring{ L"" }; } // Read the next line; wchar_t arr[LINE_MAX + 2] = { L'\0' }; wcstring buff = arr; wstring res{L""}; // read until first newline or end of file while (fgetws(arr, LINE_MAX + 2, _handle)) { res += wcstring(arr, buff.length()); if (buff.length() < LINE_MAX || buff[LINE_MAX] == L'\n') { return res; } } _error = strerror(errno); return wstring(L""); } bool file::putc(char c) { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); assert(not(_mode & fmode_wide), "Attempted Wide Operation on Byte File"); // Check if there is a file if (_handle == nullptr) { return false; } int res; if ((res = fputc(c, _handle)) != c && res != EOF) { _error = strerror(errno); return true; } return false; } bool file::putwc(wchar_t c) { assert(_error == nullptr, "Attempted an Operation on a File in an Errored State"); assert(_mode & fmode_wide, "Attempted Byte Operation on Wide File"); // Check if there is a file if (_handle == nullptr) { return false; } int res; if ((res = fputc(c, _handle)) != c && res != EOF) { _error = strerror(errno); return true; } return false; } }