// =====================================================================================================================
// fennec, a free and open source game engine
// Copyright © 2025 - 2026 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
#ifdef FENNEC_COMPILER_MSVC
#else
#include
#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() {
if (is_open()) {
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"");
}
void file::print(const cstring& str) {
write(str.data(), str.length());
}
void file::print(const string& str) {
write(str.data(), str.length());
}
void file::println(const cstring& str) {
write(str.data(), str.length());
putc('\n');
}
void file::println(const string& str) {
write(str.data(), str.length());
putc('\n');
}
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 ((char)(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 ((wchar_t)(res = fputwc(c, _handle)) != c && res != EOF) {
_error = strerror(errno);
return true;
}
return false;
}
}