Updated file and exception code from global core.
M core/CMakeLists.txt +1 -1
@@ 22,6 22,6 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_F
 
 add_library(core STATIC ${core_src})
 
-set_property(TARGET core PROPERTY CXX_STANDARD 20)
+set_property(TARGET core PROPERTY CXX_STANDARD 23)
 
 target_include_directories(core PUBLIC "${CMAKE_CURRENT_LIST_DIR}")

          
A => core/core/exceptions/error_linux.cpp +24 -0
@@ 0,0 1,24 @@ 
+#if defined(__linux) || defined(__APPLE__)
+
+#include "pch.h"
+
+#include <core/exceptions/error_linux.h>
+#include <string.h>
+
+namespace core
+{
+
+std::string translate_system_error(int error_code)
+{
+	char error_buffer[256];
+	return strerror_r(error_code, error_buffer, sizeof(error_buffer));
+}
+
+std::string translate_system_error()
+{
+	return translate_system_error(errno);
+}
+
+
+}
+#endif

          
A => core/core/exceptions/error_linux.h +13 -0
@@ 0,0 1,13 @@ 
+#pragma once
+
+#if defined(__linux) || defined(__APPLE__)
+
+namespace core
+{
+
+std::string translate_system_error(int error_code);
+std::string translate_system_error();
+
+}
+
+#endif

          
A => core/core/exceptions/error_win.cpp +41 -0
@@ 0,0 1,41 @@ 
+#if defined(_WIN32)
+
+#include "pch.h"
+
+#include <core/exceptions/error_win.h>
+#include <string>
+
+namespace core
+{
+
+std::string translate_system_error(DWORD error_code)
+{
+	LPTSTR string = nullptr;
+	
+	DWORD result = FormatMessage(
+		FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS,
+		NULL, // unused with FORMAT_MESSAGE_FROM_SYSTEM
+		error_code,
+		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+		reinterpret_cast<LPTSTR>(&string),
+		0, // minimum size for output buffer
+		NULL
+	);
+	
+	if (result == 0) {
+		return std::to_string(static_cast<unsigned>(error_code));
+	}
+
+	std::string error_string = string;
+	LocalFree(string);
+
+	return error_string;
+}
+
+std::string translate_system_error()
+{
+	return translate_system_error(GetLastError());
+}
+
+}
+#endif

          
A => core/core/exceptions/error_win.h +20 -0
@@ 0,0 1,20 @@ 
+#pragma once
+
+#if defined(_WIN32)
+
+#include <windows.h>
+
+namespace core
+{
+
+std::string translate_system_error(DWORD error_code);
+std::string translate_system_error();
+
+inline std::string translate_system_error(int error_code)
+{
+	return translate_system_error(static_cast<DWORD>(error_code));
+}
+
+}
+
+#endif

          
A => core/core/exceptions/exception.cpp +12 -0
@@ 0,0 1,12 @@ 
+#include "pch.h"
+
+#include <core/exceptions/exception.h>
+
+namespace core
+{
+
+Exception::~Exception()
+{
+}
+
+}

          
M core/core/exceptions/exception.h +5 -5
@@ 1,5 1,7 @@ 
 #pragma once
 
+#include <stdexcept>
+
 namespace core
 {
 

          
@@ 7,12 9,10 @@ namespace core
 /// @{
 
 /// Base class for exceptions.
-/// TODO: this type isn't noexcept copy constructible and thus may cause program termination if that happens while rethrown.
-/// It is more tricky since the std::runtime_error isn't wide string compatible so that can't be the base class.
-struct Exception
+struct Exception : std::runtime_error
 {
-	Exception(const std::string &msg) : message(msg) {}
-	std::string message;
+	Exception(const std::string &msg) : runtime_error(msg) {}
+	virtual ~Exception() override;
 };
 
 /// @}

          
M core/core/io/file_helpers.cpp +135 -19
@@ 2,14 2,19 @@ 
 
 #include <algorithm>
 #include <core/io/file_helpers.h>
+#include <core/io/file_reader.h>
+#include <core/io/file_writer.h>
+#include <core/io/path.h>
+#include <fstream>
 #include <sstream>
 
 namespace core
 {
 
-bool match_include_dir_and_file(const std::string &file, const std::vector<std::string> &include_dirs, std::string &result)
+bool match_include_dir_and_file(const std::string &filename, const std::vector<std::string> &include_dirs, std::string &result)
 {
 	std::vector<char> temp_string;
+	std::string error;
 	for (auto &dir : include_dirs) {
 		temp_string.clear();
 		// add dir

          
@@ 20,13 25,15 @@ bool match_include_dir_and_file(const st
 			temp_string.push_back('/');
 
 		// add file
-		temp_string.insert(temp_string.end(), file.begin(), file.end());
+		temp_string.insert(temp_string.end(), filename.begin(), filename.end());
 
 		// null terminate
 		temp_string.push_back(0);
 
 		// check if file exists
-		if (file_exists(temp_string.data())) {
+		bool exists = false;
+		bool success = file_exists(temp_string.data(), exists, error);
+		if (success && exists) {
 			result = temp_string.data();
 			return true;
 		}

          
@@ 34,31 41,140 @@ bool match_include_dir_and_file(const st
 	return false;
 }
 
-std::string base_name(const std::string &filename)
+bool make_directories(const std::string &dir, std::string &error)
+{
+	if (dir.empty()) {
+		return true;
+	}
+
+	// check if already there
+	bool exists = false;
+	bool success = dir_exists(dir, exists, error);
+	if (success && exists) {
+		return true;
+	}
+
+	// make the parent directories first
+	success = make_directories(path_pop(dir), error);
+	if (!success) {
+		return false;
+	}
+
+	// try to make this one
+	success = make_directory(dir, error);
+	return success;
+}
+
+bool file_size(const std::string &filename, size_t &size, std::string &error)
 {
-	size_t pos = filename.rfind(".");
-	// early out if no punctual character was found
-	if (pos == std::string::npos)
-		return filename;
+	std::ifstream file;
+	// open file and place read offset at end to measure size
+	#if defined(_MSC_VER)
+		std::wstring wide_filename;
+		bool success = utf8_to_wide(filename, wide_filename);
+		if (!success) {
+			std::stringstream ss;
+			ss << "Filename '" << filename << "' can't be converted from utf8 to wide string";
+			error = ss.str();
+			return false;
+		}
+		file.open(wide_filename, std::ios::in | std::ios::ate | std::ios::binary);
+	#elif defined(__GNUC__)
+		file.open(filename, std::ios::in | std::ios::ate | std::ios::binary);
+	#else
+		#error "Platform not supported"
+	#endif
+	if (!file.is_open()) {
+		std::stringstream ss;
+		ss << "Failed to open '" << filename << '\'';
+		error = ss.str();
+		return false;
+	}
 
-	return filename.substr(0, pos);
+	// get file size
+	size = static_cast<size_t>(file.tellg());
+	file.close();
+
+	return true;
 }
 
-std::string file_extension(const std::string &filename)
+bool load_file(const std::string &filename, std::vector<uint8_t> &contents, std::string &error)
 {
-	size_t pos = filename.rfind(".");
-	// early out if no punctual character was found
-	if (pos == std::string::npos)
-		return std::string();
+	FileReader reader;
+	if (!reader.open(filename, error)) {
+		return false;
+	}
+	size_t size = reader.size();
+	contents.resize(size);
+	if (!reader.read(contents.data(), size, error)) {
+		return false;
+	}
+	return reader.close(error);
+}
 
-	return filename.substr(pos, std::string::npos);
+bool save_file(const std::string &filename, std::span<uint8_t> contents, std::string &error)
+{
+	return FileWriter::write_contents(filename, contents.data(), contents.size(), error);
+}
+
+bool save_file(const std::string &filename, const std::string &contents, std::string &error)
+{
+	return FileWriter::write_contents(filename, contents, error);
 }
 
-std::string to_front_slashes(const std::string &path)
+bool open_input(const std::string &filename, std::ifstream &stream, bool binary, std::string &error)
 {
-	std::string front_slash_path(path);
-	std::replace(front_slash_path.begin(), front_slash_path.end(), '\\', '/');
-	return front_slash_path;
+	std::ios_base::openmode mode = binary ? (std::ios::in | std::ios::binary) : (std::ios::in);
+	#if defined(_MSC_VER)
+		std::wstring wide_filename;
+		bool success = utf8_to_wide(filename, wide_filename);
+		if (!success) {
+			std::stringstream ss;
+			ss << "Filename '" << filename << "' can't be converted from utf8 to wide string";
+			error = ss.str();
+			return false;
+		}
+		stream.open(wide_filename, mode);
+	#elif defined(__GNUC__)
+		stream.open(filename, mode);
+	#else
+		#error "Platform not supported"
+	#endif
+	if (!stream.is_open()) {
+		std::stringstream ss;
+		ss << "Failed to open '" << filename << '\'';
+		error = ss.str();
+		return false;
+	}
+	return true;
 }
 
+bool open_output(const std::string &filename, std::ofstream &stream, bool binary, std::string &error)
+{
+	std::ios_base::openmode mode = binary ? (std::ios::out | std::ios::binary | std::ios::trunc) : (std::ios::out | std::ios::trunc);
+	#if defined(_MSC_VER)
+		std::wstring wide_filename;
+		bool success = utf8_to_wide(filename, wide_filename);
+		if (!success) {
+			std::stringstream ss;
+			ss << "Filename '" << filename << "' can't be converted from utf8 to wide string";
+			error = ss.str();
+			return false;
+		}
+		stream.open(wide_filename, mode);
+	#elif defined(__GNUC__)
+		stream.open(filename, mode);
+	#else
+		#error "Platform not supported"
+	#endif
+	if (!stream.is_open()) {
+		std::stringstream ss;
+		ss << "Failed to open '" << filename << '\'';
+		error = ss.str();
+		return false;
+	}
+	return true;
+}
+
+
 } // namespace core

          
M core/core/io/file_helpers.h +37 -9
@@ 1,5 1,7 @@ 
 #pragma once
 
+#include <span>
+
 namespace core
 {
 

          
@@ 8,21 10,47 @@ namespace core
 
 /// Determines if there is an include directory that matches a file part.
 /// @return True if a file part matches an include directory. @a result is updated in that case.
-bool match_include_dir_and_file(const std::string &file, const std::vector<std::string> &include_dirs, std::string &result);
+bool match_include_dir_and_file(const std::string &filename, const std::vector<std::string> &include_dirs, std::string &result);
+
+/// Check if a file exists.
+/// @return True if successful.
+[[nodiscard]] bool file_exists(const std::string &filename, bool &exists, std::string &error);
 
 /// Check if a file exists.
-/// @return True if the file exists.
-bool file_exists(const char *file);
+/// @return True if successful.
+[[nodiscard]] bool dir_exists(const std::string &filename, bool &exists, std::string &error);
+
+/// Make a single directory.
+/// @return True if successful.
+[[nodiscard]] bool make_directory(const std::string &dir, std::string &error);
 
-/// Returns the filename without extension.
-std::string base_name(const std::string &filename);
+/// Make all directories in a path.
+/// @return True if successful.
+[[nodiscard]] bool make_directories(const std::string &dir, std::string &error);
+
+/// Determines the size of the file.
+/// @return True if successful.
+[[nodiscard]] bool file_size(const std::string &filename, size_t &size, std::string &error);
 
-/// Returns the file extension including the punctual character.
-std::string file_extension(const std::string &filename);
+/// Load an entire file.
+/// @return True if successful.
+[[nodiscard]] bool load_file(const std::string &filename, std::vector<uint8_t> &contents, std::string &error);
+
+/// Save an entire file.
+/// @return True if successful.
+[[nodiscard]] bool save_file(const std::string &filename, std::span<uint8_t> contents, std::string &error);
+[[nodiscard]] bool save_file(const std::string &filename, const std::string &contents, std::string &error);
 
-/// Change all back-slashes to front-slashes.
-std::string to_front_slashes(const std::string &path);
+/// @return True if successful.
+[[nodiscard]] bool list_dir(const std::string &path, std::vector<std::string> &result, std::string &error);
 
+/// Open a file for input and create an input stream.
+/// @return True if successful.
+[[nodiscard]] bool open_input(const std::string &filename, std::ifstream &stream, bool binary, std::string &error);
+
+/// Open a file for output and create an output stream.
+/// @return True if successful.
+[[nodiscard]] bool open_output(const std::string &filename, std::ofstream &stream, bool binary, std::string &error);
 
 /// @}
 

          
M core/core/io/file_helpers_linux.cpp +72 -5
@@ 2,8 2,13 @@ 
 
 #if defined(__linux) || defined(__APPLE__)
 
+#include <algorithm>
+#include <core/exceptions/error_linux.h>
 #include <core/io/file_helpers.h>
+#include <core/strings/utf8.h>
 #include <cstring>
+#include <dirent.h>
+#include <sstream>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>

          
@@ 11,16 16,78 @@ 
 namespace core
 {
 
-bool file_exists(const char *file)
+bool file_exists(const std::string &filename, bool &exists, std::string &error)
+{
+	struct stat s;
+	memset(&s, 0, sizeof(s));
+	int success = stat(filename.c_str(), &s);
+	if (success != 0) {
+		error = translate_system_error();
+		exists = false;
+		return errno == ENOENT;
+	}
+
+	error.clear();
+	bool is_regular_file = S_ISREG(s.st_mode);
+	exists = is_regular_file;
+	return true;
+}
+
+bool dir_exists(const std::string &filename, bool &exists, std::string &error)
 {
 	struct stat s;
 	memset(&s, 0, sizeof(s));
-	int success = stat(file, &s);
-	if (success != 0)
+	int success = stat(filename.c_str(), &s);
+	if (success != 0) {
+		error = translate_system_error();
+		exists = false;
+		return errno == ENOENT;
+	}
+
+	error.clear();
+	bool is_dir = S_ISDIR(s.st_mode);
+	exists = is_dir;
+	return true;
+}
+
+bool make_directory(const std::string &dir, std::string &error)
+{
+	mode_t mode = S_IRWXU; // user can do anything, everyone else nothing
+	int result = mkdir(dir.c_str(), mode);
+	bool success = result == 0 || errno == EEXIST; // check for exist error in case of a race condition
+	if (!success) {
+		error = translate_system_error();
 		return false;
+	}
+	error.clear();
+	return true;
+}
 
-	bool is_regular_file = S_ISREG(s.st_mode);
-	return is_regular_file;
+bool list_dir(const std::string &path, std::vector<std::string> &result, std::string &error)
+{
+	DIR *dir = opendir(path.c_str());
+	if (dir == nullptr) {
+		std::stringstream ss;
+		ss << "Can't list directory '" << path << "': " << translate_system_error();
+		error = ss.str();
+		return false;
+	}
+
+	struct dirent *ent;
+	while (true) {
+		ent = readdir(dir);
+		if (ent == nullptr)
+			break;
+		if (ent->d_name[0] == '.' && ent->d_name[1] == '\0')
+			continue;
+		if (ent->d_name[0] == '.' && ent->d_name[1] == '.' && ent->d_name[2] == '\0')
+			continue;
+		result.push_back(ent->d_name);
+	}
+	closedir(dir);
+
+	std::sort(result.begin(), result.end());
+	return true;
 }
 
 } // namespace core

          
M core/core/io/file_helpers_win.cpp +124 -4
@@ 2,17 2,137 @@ 
 
 #if defined(_WIN32)
 
+#include <algorithm>
+#include <core/exceptions/error_win.h>
 #include <core/io/file_helpers.h>
+#include <core/io/path.h>
 #include <core/strings/utf8.h>
+#include <sstream>
 
 namespace core
 {
 
-bool file_exists(const char *file)
+bool file_exists(const std::string &filename, bool &exists, std::string &error)
+{
+	std::wstring wide_filename;
+	bool success = utf8_to_wide(filename, wide_filename);
+	if (!success) {
+		error = "Filename can't be converted from utf8 to wide string";
+		exists = false;
+		return false;
+	}
+	DWORD attributes = GetFileAttributesW(wide_filename.c_str());
+	if (attributes == INVALID_FILE_ATTRIBUTES) {
+		std::stringstream ss;
+		ss << "Failed to check if file '" << filename << "' exists: " << translate_system_error();
+		error = ss.str();
+		exists = false;
+		return false;
+	}
+	error.clear();
+	exists = !(attributes & FILE_ATTRIBUTE_DIRECTORY);
+	return true;
+}
+
+bool dir_exists(const std::string &filename, bool &exists, std::string &error)
+{
+	std::wstring wide_filename;
+	bool success = utf8_to_wide(filename, wide_filename);
+	if (!success) {
+		error = "Path can't be converted from utf8 to wide string";
+		exists = false;
+		return false;
+	}
+	DWORD attributes = GetFileAttributesW(wide_filename.c_str());
+	if (attributes == INVALID_FILE_ATTRIBUTES) {
+		std::stringstream ss;
+		ss << "Failed to check if directory '" << filename << "' exists: " << translate_system_error();
+		error = ss.str();
+		exists = false;
+		return false;
+	}
+	error.clear();
+	exists = attributes & FILE_ATTRIBUTE_DIRECTORY;
+	return true;
+}
+
+bool make_directory(const std::string &dir, std::string &error)
 {
-	std::wstring wide_file = utf8_to_wide(file);
-	DWORD attributes = GetFileAttributesW(wide_file.c_str());
-	return (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY));
+	std::wstring wide_dir;
+	{
+		bool success = utf8_to_wide(dir, wide_dir);
+		if (!success) {
+			error = "Path can't be converted from utf8 to wide string";
+			return false;
+		}
+	}
+	
+	BOOL success = CreateDirectoryW(wide_dir.c_str(), NULL);
+	if (!success) {
+		std::stringstream ss;
+		ss << "Failed to create directory '" << dir << "': " << translate_system_error();
+		error = ss.str();
+		return false;
+	}
+
+	error.clear();
+	return true;
+}
+
+bool list_dir(const std::string &path, std::vector<std::string> &result, std::string &error)
+{
+	std::string search_path = path_concat(path, "*");
+	std::wstring wide_search_path;
+	if (!utf8_to_wide(search_path, wide_search_path)) {
+		error = "Path can't be converted from utf8 to wide string";
+		return false;
+	}
+	
+	if (wide_search_path.size() + 1 > MAX_PATH) { // +1 for termination character
+		std::stringstream ss;
+		ss << "Path '" << path << "' is too long to list directory";
+		error = ss.str();
+		return false;
+	}
+
+	WIN32_FIND_DATAW find_data;
+	HANDLE h = FindFirstFileW(wide_search_path.c_str(), &find_data);
+	
+	if (h == INVALID_HANDLE_VALUE) {
+		std::stringstream ss;
+		ss << "Can't list directory '" << path << "': " << translate_system_error();
+		error = ss.str();
+		return false;
+	}
+	
+	do {
+		std::wstring wide_filename = find_data.cFileName;
+		std::string narrow_filename;
+		if (!wide_to_utf8(wide_filename, narrow_filename)) {
+			// it wouldn't be nice to fail everything for one broken file so just skip it
+			continue;
+		}
+		
+		if (narrow_filename == "." || narrow_filename == "..") {
+			continue;
+		}
+		
+		result.push_back(narrow_filename);
+	} while (FindNextFileW(h, &find_data));
+
+	std::sort(result.begin(), result.end());
+
+	DWORD last_error = GetLastError();
+	FindClose(h);
+	if (last_error != ERROR_NO_MORE_FILES) {
+		std::stringstream ss;
+		ss << "Can't list directory '" << path << "': " << translate_system_error(last_error);
+		error = ss.str();
+		return false;
+	}
+
+	error.clear();
+	return true;
 }
 
 } // namespace core

          
M core/core/io/file_id.h +1 -1
@@ 17,7 17,7 @@ namespace core
 /// Get the file id from a path. This can be used to determine if
 /// two files are the exact same.
 /// @return False if the file can't be opened.
-bool file_id(const std::string &file, FileId &id);
+[[nodiscard]] bool file_id(const std::string &file, FileId &id, std::string &error);
 
 /// @}
 

          
M core/core/io/file_id_linux.cpp +14 -3
@@ 2,28 2,39 @@ 
 
 #if defined(__linux) || defined(__APPLE__)
 
+#include <core/exceptions/error_linux.h>
 #include <core/io/file_id.h>
 #include <cstring>
+#include <sstream>
 #include <sys/stat.h>
 #include <unistd.h>
 
 namespace core
 {
 
-bool file_id(const std::string &file, FileId &id)
+bool file_id(const std::string &filename, FileId &id, std::string &error)
 {
 	struct stat s;
 	memset(&s, 0, sizeof(s));
-	int success = stat(file.c_str(), &s);
-	if (success != 0)
+	int result = stat(filename.c_str(), &s);
+	bool success = result == 0;
+	if (!success) {
+		std::stringstream ss;
+		ss << "Failed to get file status for '" << filename << "': " << translate_system_error();
+		error = ss.str();
 		return false;
+	}
 
 	if (S_ISREG(s.st_mode)) {
+		error.clear();
 		id.device = s.st_dev;
 		id.inode = s.st_ino;
 		return true;
 	}
 
+	std::stringstream ss;
+	ss << "The path '" << filename << "' isn't a regular file";
+	error = ss.str();
 	return false;
 }
 

          
M core/core/io/file_id_win.cpp +20 -3
@@ 2,20 2,36 @@ 
 
 #if defined(_WIN32)
 
+#include <core/exceptions/error_win.h>
 #include <core/io/file_id.h>
 #include <core/strings/utf8.h>
+#include <sstream>
 
 namespace core
 {
 
-bool file_id(const std::string &file, FileId &id)
+bool file_id(const std::string &filename, FileId &id, std::string &error)
 {
-	HANDLE h = CreateFileW(utf8_to_wide(file).c_str(), 0, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
-	if (h == INVALID_HANDLE_VALUE)
+	std::wstring wide_filename;
+	bool success = utf8_to_wide(filename, wide_filename);
+	if (!success) {
+		error = "Path can't be converted from utf8 to wide string";
 		return false;
+	}
+
+	HANDLE h = CreateFileW(wide_filename.c_str(), 0, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+	if (h == INVALID_HANDLE_VALUE) {
+		std::stringstream ss;
+		ss << "Failed to open '" << filename << "': " << translate_system_error();
+		error = ss.str();
+		return false;
+	}
 	BY_HANDLE_FILE_INFORMATION hfi;
 	BOOL result = GetFileInformationByHandle(h, &hfi);
 	if (!result) {
+		std::stringstream ss;
+		ss << "Failed to get file information for '" << filename << "': " << translate_system_error();
+		error = ss.str();
 		CloseHandle(h);
 		return false;
 	}

          
@@ 25,6 41,7 @@ bool file_id(const std::string &file, Fi
 	id.file_index_lo = hfi.nFileIndexLow;
 
 	CloseHandle(h);
+	error.clear();
 	return true;
 }
 

          
M core/core/io/file_id_win.h +2 -0
@@ 1,5 1,7 @@ 
 #pragma once
 
+#include <windows.h>
+
 namespace core
 {
 

          
A => core/core/io/file_reader.cpp +89 -0
@@ 0,0 1,89 @@ 
+#include "pch.h"
+
+#include <core/math/sign.h>
+#include <core/io/file_reader.h>
+#include <core/strings/utf8.h>
+#include <sstream>
+
+namespace core {
+
+bool FileReader::open(const std::string &filename, std::string &error)
+{
+	_filename = filename;
+	#if defined(_MSC_VER)
+		_file.open(convert_utf8_to_wide(filename), std::ios::in | std::ios::ate | std::ios::binary);
+	#elif defined(__GNUC__)
+		_file.open(filename, std::ios::in | std::ios::ate | std::ios::binary);
+	#else
+		#error "Platform not supported"
+	#endif
+	
+	if (!_file.is_open()) {
+		std::stringstream ss;
+		ss << "Failed to open '" << filename << "' for reading";
+		error = ss.str();
+		return false;
+	}
+	
+	_size = static_cast<size_t>(_file.tellg());
+	_file.seekg(0);
+
+	if (_file.fail()) {
+		std::stringstream ss;
+		ss << "Failed to seek in '" << filename << "' when reading";
+		error = ss.str();
+		return false;
+	}
+	
+	return true;
+}
+
+size_t FileReader::size() const
+{
+	return _size;
+}
+
+bool FileReader::seek(size_t pos, std::string &error)
+{
+	_file.seekg(static_cast<std::streampos>(core::sign_cast(pos)));
+	
+	if (_file.fail()) {
+		std::stringstream ss;
+		ss << "Failed to seek in '" << _filename << "'";
+		error = ss.str();
+		return false;
+	}
+	return true;
+}
+
+bool FileReader::read(uint8_t *data, size_t &size, std::string &error)
+{
+	static_assert(sizeof(uint8_t) == sizeof(char), "unexpected size of char");
+	assert(_file.is_open());
+
+	_file.read(reinterpret_cast<char *>(data), core::sign_cast(size));
+	if (_file.fail()) {
+		std::stringstream ss;
+		ss << "Error when reading file '" << _filename << '\'';
+		error = ss.str();
+		return false;
+	}
+	
+	size = static_cast<size_t>(_file.gcount());
+	return true;
+}
+
+bool FileReader::close(std::string &error)
+{
+	_file.close();
+	if (_file.fail()) {
+		std::stringstream ss;
+		ss << "Error when closing file '" << _filename << "'";
+		error = ss.str();
+		return false;
+	}
+	
+	return true;
+}
+
+} // namespace core

          
A => core/core/io/file_reader.h +47 -0
@@ 0,0 1,47 @@ 
+#pragma once
+
+#include <fstream>
+
+namespace core {
+
+/// @addtogroup io
+/// @{
+
+/// This handles reading binary files.
+class FileReader
+{
+public:
+	/// Open a file for reading.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful.
+	[[nodiscard]] bool open(const std::string &filename, std::string &error);
+
+	/// @return Size of file in bytes.
+	[[nodiscard]] size_t size() const;
+
+	/// Seek to an absolute position in the file.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful.
+	[[nodiscard]] bool seek(size_t pos, std::string &error);
+	
+	/// Read data from the opened file.
+	/// @param size As in parameter it tells the size of the buffer and as out
+	///             parameter it returns the actual size loaded.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful.
+	[[nodiscard]] bool read(uint8_t *data, size_t &size, std::string &error);
+
+	/// Close the file.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful.
+	[[nodiscard]] bool close(std::string &error);
+
+private:
+	std::string _filename;
+	std::ifstream _file;
+	size_t _size;
+};
+
+/// @}
+
+} // namespace core

          
M core/core/io/file_writer.cpp +72 -15
@@ 1,39 1,96 @@ 
 #include "pch.h"
 
-#include <core/exceptions/file_exception.h>
+#include <core/math/sign.h>
 #include <core/io/file_writer.h>
 #include <core/strings/utf8.h>
+#include <sstream>
 
 namespace core {
 
-void FileWriter::open(const std::string &filename)
+bool FileWriter::open(const std::string &filename, std::string &error)
 {
+	_filename = filename;
 	#if defined(_MSC_VER)
-		std::wstring wide_filename;
-		try {
-			wide_filename = convert_utf8_to_wide(filename);
-		} catch (Exception &e) {
-			throw FileException("Path cannot be converted to wide byte format: " + filename);
-		}
-		_file.open(wide_filename, std::ios::out | std::ios::trunc | std::ios::binary);
+		_file.open(convert_utf8_to_wide(filename), std::ios::out | std::ios::trunc | std::ios::binary);
 	#elif defined(__GNUC__)
 		_file.open(filename, std::ios::out | std::ios::trunc | std::ios::binary);
 	#else
 		#error "Platform not supported"
 	#endif
 
-	if (!_file.is_open())
-		throw FileException("Failed to open " + filename);
+	if (!_file.is_open()) {
+		std::stringstream ss;
+		ss << "Failed to open '" << filename << "' for writing";
+		error = ss.str();
+		return false;
+	}
+	
+	return true;
 }
 
-void FileWriter::write(const uint8_t *data, uint32_t size)
+bool FileWriter::write(const uint8_t *data, size_t size, std::string &error)
 {
 	static_assert(sizeof(uint8_t) == sizeof(char), "unexpected size of char");
 	assert(_file.is_open());
 
-	_file.write(reinterpret_cast<const char *>(data), size);
-	if (_file.fail())
-		throw FileException("Error when writing file");
+	_file.write(reinterpret_cast<const char *>(data), core::sign_cast(size));
+	if (_file.fail()) {
+		std::stringstream ss;
+		ss << "Error when writing file '" << _filename << '\'';
+		error = ss.str();
+		return false;
+	}
+	
+	return true;
+}
+
+bool FileWriter::close(std::string &error)
+{
+	_file.close();
+	if (_file.fail()) {
+		std::stringstream ss;
+		ss << "Error when closing file '" << _filename << "'";
+		error = ss.str();
+		return false;
+	}
+	
+	return true;
+}
+
+bool FileWriter::write_contents(const std::string &filename, const uint8_t *data, size_t size, std::string &error)
+{
+	core::FileWriter writer;
+	if (!writer.open(filename, error)) {
+		return false;
+	}
+	if (size != 0) {
+		if (!writer.write(data, size, error)) {
+			return false;
+		}
+	}
+	
+	return writer.close(error);
+}
+
+bool FileWriter::write_contents(const std::string &filename, const std::vector<uint8_t> &data, std::string &error)
+{
+	return write_contents(filename, data.data(), data.size(), error);
+}
+
+bool FileWriter::write_contents(const std::string &filename, const std::vector<char> &data, std::string &error)
+{
+	return write_contents(filename, reinterpret_cast<const uint8_t *>(data.data()), data.size(), error);
+}
+
+bool FileWriter::write_contents(const std::string &filename, const std::stringstream &data, std::string &error)
+{
+	std::string contents = data.str();
+	return write_contents(filename, reinterpret_cast<const uint8_t *>(contents.data()), contents.size(), error);
+}
+
+bool FileWriter::write_contents(const std::string &filename, const std::string &data, std::string &error)
+{
+	return write_contents(filename, reinterpret_cast<const uint8_t *>(data.data()), data.size(), error);
 }
 
 } // namespace core

          
M core/core/io/file_writer.h +25 -2
@@ 11,13 11,36 @@ namespace core {
 class FileWriter
 {
 public:
-	void open(const std::string &filename);
-	void write(const uint8_t *data, uint32_t size);
+	/// Open a file for writing.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful
+	[[nodiscard]] bool open(const std::string &filename, std::string &error);
+	
+	/// Write data to the opened file.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful
+	[[nodiscard]] bool write(const uint8_t *data, size_t size, std::string &error);
+
+	/// Close the file.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful
+	[[nodiscard]] bool close(std::string &error);
+
+	/// Write binary data in a single function call.
+	/// @param error Is filled with the error reason in case of an error.
+	/// @return True if successful
+	[[nodiscard]] static bool write_contents(const std::string &filename, const uint8_t *data, size_t size, std::string &error);
+	[[nodiscard]] static bool write_contents(const std::string &filename, const std::vector<uint8_t> &data, std::string &error);
+	[[nodiscard]] static bool write_contents(const std::string &filename, const std::vector<char> &data, std::string &error);
+	[[nodiscard]] static bool write_contents(const std::string &filename, const std::stringstream &data, std::string &error);
+	[[nodiscard]] static bool write_contents(const std::string &filename, const std::string &data, std::string &error);
 
 private:
+	std::string _filename;
 	std::ofstream _file;
 };
 
+
 /// @}
 
 } // namespace core

          
A => core/core/io/path.cpp +88 -0
@@ 0,0 1,88 @@ 
+#include "pch.h"
+
+#include <algorithm>
+#include <core/io/path.h>
+#include <core/math/sign.h>
+#include <cstring>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+namespace core
+{
+
+#if defined(_WIN32)
+	const char path_separators[2] = {'\\', '/'};
+#endif
+
+#if defined(__linux) || defined(__APPLE__)
+	const char path_separators[1] = {'/'};
+#endif
+	
+std::string path_concat(const std::string &dir, const std::string &path)
+{
+	if (dir.empty()) {
+		return path;
+	}
+	if (path.empty()) {
+		return dir;
+	}
+
+	if (std::find(std::begin(path_separators), std::end(path_separators), dir.back()) != std::end(path_separators)) {
+		return dir + path;
+	}
+	std::string result;
+	result.reserve(dir.size() + 1 + path.size());
+	result.append(dir);
+	result.push_back(path_separator());
+	result.append(path);
+	return result;
+}
+
+std::string path_pop(const std::string &path)
+{
+	auto it = std::find_end(std::begin(path), std::end(path), std::begin(path_separators), std::end(path_separators));
+	if (it == std::end(path)) {
+		return std::string();
+	}
+	return path.substr(0, unsign_cast(it - std::begin(path)));
+}
+
+std::string base_name(const std::string &filename)
+{
+	size_t pos = filename.rfind(".");
+
+	if (pos == std::string::npos)
+		return filename;
+
+	return filename.substr(0, pos);
+}
+
+std::string filename(const std::string &path)
+{
+	auto it = std::find_end(std::begin(path), std::end(path), std::begin(path_separators), std::end(path_separators));
+	if (it == std::end(path)) {
+		return path;
+	}
+	
+	return path.substr(unsign_cast(it - std::begin(path) + 1));
+}
+
+std::string file_extension(const std::string &filename)
+{
+	size_t pos = filename.rfind(".");
+
+	if (pos == std::string::npos)
+		return std::string();
+
+	return filename.substr(pos, std::string::npos);
+}
+
+std::string to_front_slashes(const std::string &path)
+{
+	std::string front_slash_path(path);
+	std::replace(front_slash_path.begin(), front_slash_path.end(), '\\', '/');
+	return front_slash_path;
+}
+
+}

          
A => core/core/io/path.h +46 -0
@@ 0,0 1,46 @@ 
+#pragma once
+
+#include <string>
+
+namespace core
+{
+
+#if defined(_WIN32)
+	/// Returns the character to separate directories in a path.
+	constexpr char path_separator()
+	{
+		return '\\';
+	}
+
+	extern const char path_separators[2];
+#endif
+
+#if defined(__linux) || defined(__APPLE__)
+	/// Returns the character to separate directories in a path.
+	constexpr char path_separator()
+	{
+		return '/';
+	}
+
+	extern const char path_separators[1];
+#endif
+
+/// Concatenate a directory with a file or directory, adding the directory separator inbetween if necessary.
+std::string path_concat(const std::string &dir, const std::string &path);
+
+/// Returns the path with last component removed.
+std::string path_pop(const std::string &path);
+
+/// Get the filename from a path.
+std::string filename(const std::string &path);
+
+/// Returns the filename without extension.
+std::string base_name(const std::string &filename);
+
+/// Returns the file extension including the punctual character.
+std::string file_extension(const std::string &filename);
+
+/// Change all back-slashes to front-slashes.
+std::string to_front_slashes(const std::string &path);
+
+}

          
M core/core/io/text_reader.cpp +12 -32
@@ 1,47 1,27 @@ 
 #include "pch.h"
 
-#include <core/exceptions/file_exception.h>
+#include <core/io/file_reader.h>
 #include <core/io/text_reader.h>
 #include <core/strings/utf8.h>
-#include <fstream>
-#include <locale>
 #include <vector>
 
 namespace core
 {
 
-std::string load_file(const std::string &filename)
+bool load_file(const std::string &filename, std::string &contents, std::string &error)
 {
-	// load the file contents
-	std::ifstream file;
-	// open file and place read offset at end to measure size
-	#if defined(_MSC_VER)
-		std::wstring wide_filename;
-		try {
-			wide_filename = convert_utf8_to_wide(filename);
-		} catch (Exception &e) {
-			throw FileException("Path cannot be converted to wide byte format: " + filename);
-		}
-		file.open(wide_filename, std::ios::in | std::ios::ate | std::ios::binary);
-	#elif defined(__GNUC__)
-		file.open(filename, std::ios::in | std::ios::ate | std::ios::binary);
-	#else
-		#error "Platform not supported"
-	#endif
-	if (!file.is_open())
-		throw FileException("Failed to open " + filename);
-
-	// get file size
-	uint64_t size = static_cast<uint64_t>(file.tellg());
+	FileReader reader;
+	if (!reader.open(filename, error)) {
+		return false;
+	}
+	size_t size = reader.size();
 	std::vector<char> data;
 	data.resize(size);
-
-	// read contents
-	file.seekg(0, std::ios::beg);
-	file.read(data.data(), static_cast<std::streamsize>(size));
-	file.close();
-
-	return std::string(data.data(), data.size());
+	if (!reader.read(reinterpret_cast<uint8_t *>(data.data()), size, error)) {
+		return false;
+	}
+	contents = std::string(data.data(), size);
+	return reader.close(error);
 }
 
 } // namespace core

          
M core/core/io/text_reader.h +1 -1
@@ 7,7 7,7 @@ namespace core
 /// @{
 
 /// Read an utf8 encoded file and return the contents as an utf8 encoded character array.
-std::string load_file(const std::string &filename);
+[[nodiscard]] bool load_file(const std::string &filename, std::string &contents, std::string &error);
 
 /// @}
 

          
M jasm/CMakeLists.txt +1 -1
@@ 32,7 32,7 @@ if (${MINGW})
 	)
 endif()
 
-set_property(TARGET jasm PROPERTY CXX_STANDARD 20)
+set_property(TARGET jasm PROPERTY CXX_STANDARD 23)
 
 target_link_libraries(jasm core)
 

          
M jasm/assemble/assembler_impl/assembler_impl.cpp +15 -10
@@ 7,6 7,7 @@ 
 #include <core/io/file_helpers.h>
 #include <core/io/file_id.h>
 #include <core/io/file_writer.h>
+#include <core/io/path.h>
 #include <core/math/sign.h>
 #include <core/strings/utf8.h>
 #include <exceptions/assembly_exception.h>

          
@@ 729,7 730,8 @@ size_t Assembler::syntax_analyze(const s
 	core::FileId fid;
 	std::string file_path = filename;
 	match_include_dir_and_file(filename, _include_dirs, file_path);
-	if (!core::file_id(file_path, fid)) {
+	std::string error;
+	if (!core::file_id(file_path, fid, error)) {
 		size_t file_index = _used_files.size();
 		// make sure that the file has front slashes to get the output from linux and pc unit tests match
 		_used_files.emplace_back(core::to_front_slashes(filename));

          
@@ 1071,9 1073,10 @@ void Assembler::dump_symbols(const std::
 	std::string utf8 = ss.str();
 
 	// write to disk
-	FileWriter wr;
-	wr.open(filename);
-	wr.write(reinterpret_cast<const uint8_t *>(utf8.data()), static_cast<uint32_t>(utf8.size()));
+	std::string error;
+	if (!core::save_file(filename, utf8, error)) {
+		throw AssemblyException(error);
+	}
 }
 
 struct SimpleSymbolInformation

          
@@ 1211,9 1214,10 @@ void Assembler::dump_vice_symbols(const 
 	std::string utf8 = ss.str();
 
 	// write to disk
-	FileWriter wr;
-	wr.open(filename);
-	wr.write(reinterpret_cast<const uint8_t *>(utf8.c_str()), static_cast<uint32_t>(utf8.size()));
+	std::string error;
+	if (!core::save_file(filename, utf8, error)) {
+		throw AssemblyException(error);
+	}
 }
 
 void Assembler::dump_gba_symbols(const std::string &filename)

          
@@ 1288,9 1292,10 @@ void Assembler::dump_gba_symbols(const s
 	std::string utf8 = ss.str();
 
 	// write to disk
-	FileWriter wr;
-	wr.open(filename);
-	wr.write(reinterpret_cast<const uint8_t *>(utf8.c_str()), static_cast<uint32_t>(utf8.size()));
+	std::string error;
+	if (!core::save_file(filename, utf8, error)) {
+		throw AssemblyException(error);
+	}
 }
 
 void Assembler::recurse_print_sections(const Section &section, int indent)

          
M jasm/io/data_reader.cpp +4 -4
@@ 79,15 79,15 @@ bool DataReader::size(uint64_t handle, s
 				try {
 					info.size = info.size_future.get();
 				} catch (Exception &e) {
-					info.file_size_error = e.message;
+					info.file_size_error = e.what();
 				}
 			}
 		#else
 			try {
 				load(&info);
 			} catch (Exception &e) {
-				info.file_size_error = e.message;
-				info.file_data_error = e.message;
+				info.file_size_error = e.what();
+				info.file_data_error = e.what();
 			}
 			info.loaded = true;
 		#endif

          
@@ 116,7 116,7 @@ bool DataReader::data(uint64_t handle, c
 				load(&info);
 			#endif
 		} catch (Exception &e) {
-			info.file_data_error = e.message;
+			info.file_data_error = e.what();
 		}
 		info.loaded = true;
 	}

          
M jasm/main.cpp +15 -4
@@ 34,7 34,10 @@ static const char *revision_hash =
 void write_file(int32_t start_address, bool load_addr_header, bool multi_bank_mode, const Section &first_section, const std::string &output_file, const std::vector<uint8_t> &data, const StringRepository &strings, const std::vector<std::string> &used_files)
 {
 	FileWriter file;
-	file.open(output_file);
+	std::string error;
+	if (!file.open(output_file, error)) {
+		throw AssemblyException(error);
+	}
 	if (load_addr_header) {
 		if (!multi_bank_mode) {
 			if (start_address < 0 || start_address > 0xffff) {

          
@@ 47,9 50,17 @@ void write_file(int32_t start_address, b
 		std::array<uint8_t, 2> header;
 		header[0] = static_cast<uint8_t>(start_address & 0xff);
 		header[1] = static_cast<uint8_t>(start_address >> 8);
-		file.write(header.data(), static_cast<uint32_t>(header.size()));
+		if (!file.write(header.data(), static_cast<uint32_t>(header.size()), error)) {
+			throw AssemblyException(error);
+		}
 	}
-	file.write(&data[0], static_cast<uint32_t>(data.size()));
+	if (!file.write(&data[0], static_cast<uint32_t>(data.size()), error)) {
+		throw AssemblyException(error);
+	}
+
+	if (!file.close(error)) {
+		throw AssemblyException(error);
+	}
 }
 
 void merge_sections_and_write(CommandLineArgs &args, const std::vector<Section> &sections, const StringRepository &strings, const std::vector<std::string> &used_files)

          
@@ 179,7 190,7 @@ int safe_main(int argc, const char * con
 		assemble(args);
 	}
 	catch (Exception &e) {
-		error() << e.message << '\n';
+		error() << e.what() << '\n';
 		return_code = 10;
 	}
 	return return_code;

          
M jasm/tokenize/tokenizer.cpp +5 -7
@@ 42,13 42,11 @@ void Tokenizer::tokenize(uint32_t file_i
 	_processor_stack.push_back(_processor);
 	
 	std::string contents;
-	try {
-		contents = core::load_file(file_path);
-	} catch (core::Exception &e) {
+	std::string error;
+	if (!core::load_file(file_path, contents, error)) {
 		std::stringstream ss;
-		ss << e.message << "\nwhile loading '" << _filename << "'";
-		e.message = ss.str();
-		throw e;
+		ss << error << "\nwhile loading '" << _filename << "'";
+		throw AssemblyException(ss.str());
 	}
 
 	// convert to wide strings to simplify the character classification

          
@@ 56,7 54,7 @@ void Tokenizer::tokenize(uint32_t file_i
 	try {
 		wide_contents = core::utf8_to_wide(contents);
 	} catch (core::Exception &) {
-		throw core::FileException("File contents isn't utf8 encoded: " + _filename);
+		throw AssemblyException("File contents isn't utf8 encoded: " + _filename);
 	}
 	
 	// classify all characters