diff --git a/src/base_media_func.h b/src/base_media_func.h
index 01e184f8ca..0ec644aaa5 100644
--- a/src/base_media_func.h
+++ b/src/base_media_func.h
@@ -21,7 +21,7 @@
  */
 #define fetch_metadata(name) \
 	item = metadata->GetItem(name, false); \
-	if (item == nullptr || StrEmpty(item->value)) { \
+	if (item == nullptr || !item->value.has_value() || item->value->empty()) { \
 		DEBUG(grf, 0, "Base " SET_TYPE "set detail loading: %s field missing.", name); \
 		DEBUG(grf, 0, "  Is %s readable for the user running OpenTTD?", full_filename); \
 		return false; \
@@ -42,28 +42,28 @@ bool BaseSet<T, Tnum_files, Tsearch_in_tars>::FillSetDetails(IniFile *ini, const
 	IniItem *item;
 
 	fetch_metadata("name");
-	this->name = stredup(item->value);
+	this->name = stredup(item->value->c_str());
 
 	fetch_metadata("description");
-	this->description[stredup("")] = stredup(item->value);
+	this->description[stredup("")] = stredup(item->value->c_str());
 
 	/* Add the translations of the descriptions too. */
 	for (const IniItem *item = metadata->item; item != nullptr; item = item->next) {
-		if (strncmp("description.", item->name, 12) != 0) continue;
+		if (item->name.compare(0, 12, "description.") != 0) continue;
 
-		this->description[stredup(item->name + 12)] = stredup(item->value);
+		this->description[stredup(item->name.c_str() + 12)] = stredup(item->value.value_or("").c_str());
 	}
 
 	fetch_metadata("shortname");
-	for (uint i = 0; item->value[i] != '\0' && i < 4; i++) {
-		this->shortname |= ((uint8)item->value[i]) << (i * 8);
+	for (uint i = 0; item->value.value()[i] != '\0' && i < 4; i++) {
+		this->shortname |= ((uint8)item->value.value()[i]) << (i * 8);
 	}
 
 	fetch_metadata("version");
-	this->version = atoi(item->value);
+	this->version = atoi(item->value->c_str());
 
 	item = metadata->GetItem("fallback", false);
-	this->fallback = (item != nullptr && strcmp(item->value, "0") != 0 && strcmp(item->value, "false") != 0);
+	this->fallback = (item != nullptr && item->value && item->value.value() != "0" && item->value.value() != "false");
 
 	/* For each of the file types we want to find the file, MD5 checksums and warning messages. */
 	IniGroup *files  = ini->GetGroup("files");
@@ -73,13 +73,12 @@ bool BaseSet<T, Tnum_files, Tsearch_in_tars>::FillSetDetails(IniFile *ini, const
 		MD5File *file = &this->files[i];
 		/* Find the filename first. */
 		item = files->GetItem(BaseSet<T, Tnum_files, Tsearch_in_tars>::file_names[i], false);
-		if (item == nullptr || (item->value == nullptr && !allow_empty_filename)) {
+		if (item == nullptr || (!item->value.has_value() && !allow_empty_filename)) {
 			DEBUG(grf, 0, "No " SET_TYPE " file for: %s (in %s)", BaseSet<T, Tnum_files, Tsearch_in_tars>::file_names[i], full_filename);
 			return false;
 		}
 
-		const char *filename = item->value;
-		if (filename == nullptr) {
+		if (!item->value.has_value()) {
 			file->filename = nullptr;
 			/* If we list no file, that file must be valid */
 			this->valid_files++;
@@ -87,15 +86,16 @@ bool BaseSet<T, Tnum_files, Tsearch_in_tars>::FillSetDetails(IniFile *ini, const
 			continue;
 		}
 
+		const char *filename = item->value->c_str();
 		file->filename = str_fmt("%s%s", path, filename);
 
 		/* Then find the MD5 checksum */
 		item = md5s->GetItem(filename, false);
-		if (item == nullptr || item->value == nullptr) {
+		if (item == nullptr || !item->value.has_value()) {
 			DEBUG(grf, 0, "No MD5 checksum specified for: %s (in %s)", filename, full_filename);
 			return false;
 		}
-		char *c = item->value;
+		const char *c = item->value->c_str();
 		for (uint i = 0; i < sizeof(file->hash) * 2; i++, c++) {
 			uint j;
 			if ('0' <= *c && *c <= '9') {
@@ -118,11 +118,11 @@ bool BaseSet<T, Tnum_files, Tsearch_in_tars>::FillSetDetails(IniFile *ini, const
 		/* Then find the warning message when the file's missing */
 		item = origin->GetItem(filename, false);
 		if (item == nullptr) item = origin->GetItem("default", false);
-		if (item == nullptr) {
+		if (item == nullptr || !item->value.has_value()) {
 			DEBUG(grf, 1, "No origin warning message specified for: %s", filename);
 			file->missing_warning = stredup("");
 		} else {
-			file->missing_warning = stredup(item->value);
+			file->missing_warning = stredup(item->value->c_str());
 		}
 
 		file->check_result = T::CheckMD5(file, BASESET_DIR);
@@ -170,7 +170,7 @@ bool BaseMedia<Tbase_set>::AddFile(const char *filename, size_t basepath_length,
 	if (set->FillSetDetails(ini, path, filename)) {
 		Tbase_set *duplicate = nullptr;
 		for (Tbase_set *c = BaseMedia<Tbase_set>::available_sets; c != nullptr; c = c->next) {
-			if (strcmp(c->name, set->name) == 0 || c->shortname == set->shortname) {
+			if (c->name == set->name || c->shortname == set->shortname) {
 				duplicate = c;
 				break;
 			}
diff --git a/src/gfxinit.cpp b/src/gfxinit.cpp
index 86dc868e6b..5fe913fb9a 100644
--- a/src/gfxinit.cpp
+++ b/src/gfxinit.cpp
@@ -356,11 +356,11 @@ bool GraphicsSet::FillSetDetails(IniFile *ini, const char *path, const char *ful
 		IniItem *item;
 
 		fetch_metadata("palette");
-		this->palette = (*item->value == 'D' || *item->value == 'd') ? PAL_DOS : PAL_WINDOWS;
+		this->palette = (item->value.value()[0] == 'D' || item->value.value()[0] == 'd') ? PAL_DOS : PAL_WINDOWS;
 
 		/* Get optional blitter information. */
 		item = metadata->GetItem("blitter", false);
-		this->blitter = (item != nullptr && *item->value == '3') ? BLT_32BPP : BLT_8BPP;
+		this->blitter = (item != nullptr && item->value.value()[0] == '3') ? BLT_32BPP : BLT_8BPP;
 	}
 	return ret;
 }
diff --git a/src/hotkeys.cpp b/src/hotkeys.cpp
index d5cd90b3c5..0bf350f8f0 100644
--- a/src/hotkeys.cpp
+++ b/src/hotkeys.cpp
@@ -290,7 +290,7 @@ void HotkeyList::Load(IniFile *ini)
 		IniItem *item = group->GetItem(hotkey->name, false);
 		if (item != nullptr) {
 			hotkey->keycodes.clear();
-			if (item->value != nullptr) ParseHotkeys(hotkey, item->value);
+			if (item->value.has_value()) ParseHotkeys(hotkey, item->value->c_str());
 		}
 	}
 }
diff --git a/src/ini.cpp b/src/ini.cpp
index a02d4c14da..f81d109ff2 100644
--- a/src/ini.cpp
+++ b/src/ini.cpp
@@ -12,9 +12,11 @@
 #include "ini_type.h"
 #include "string_func.h"
 #include "fileio_func.h"
+#include <fstream>
 
 #if (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309L) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 500)
 # include <unistd.h>
+# include <fcntl.h>
 #endif
 
 #ifdef _WIN32
@@ -45,31 +47,33 @@ bool IniFile::SaveToDisk(const char *filename)
 	 * that file. This to prevent that when OpenTTD crashes during the save
 	 * you end up with a truncated configuration file.
 	 */
-	char file_new[MAX_PATH];
+	std::string file_new{ filename };
+	file_new.append(".new");
 
-	strecpy(file_new, filename, lastof(file_new));
-	strecat(file_new, ".new", lastof(file_new));
-	FILE *f = fopen(file_new, "w");
-	if (f == nullptr) return false;
+	std::ofstream os(OTTD2FS(file_new.c_str()));
+	if (os.fail()) return false;
 
 	for (const IniGroup *group = this->group; group != nullptr; group = group->next) {
-		if (group->comment) fputs(group->comment, f);
-		fprintf(f, "[%s]\n", group->name);
+		os << group->comment << "[" << group->name << "]\n";
 		for (const IniItem *item = group->item; item != nullptr; item = item->next) {
-			if (item->comment != nullptr) fputs(item->comment, f);
+			os << item->comment;
 
 			/* protect item->name with quotes if needed */
-			if (strchr(item->name, ' ') != nullptr ||
-					item->name[0] == '[') {
-				fprintf(f, "\"%s\"", item->name);
+			if (item->name.find(' ') != std::string::npos ||
+				item->name[0] == '[') {
+				os << "\"" << item->name << "\"";
 			} else {
-				fprintf(f, "%s", item->name);
+				os << item->name;
 			}
 
-			fprintf(f, " = %s\n", item->value == nullptr ? "" : item->value);
+			os << " = " << item->value.value_or("") << "\n";
 		}
 	}
-	if (this->comment) fputs(this->comment, f);
+	os << this->comment;
+
+	os.flush();
+	os.close();
+	if (os.fail()) return false;
 
 /*
  * POSIX (and friends) do not guarantee that when a file is closed it is
@@ -78,11 +82,10 @@ bool IniFile::SaveToDisk(const char *filename)
  * (modification date etc.) is not important to us; only the real data is.
  */
 #if defined(_POSIX_SYNCHRONIZED_IO) && _POSIX_SYNCHRONIZED_IO > 0
-	int ret = fdatasync(fileno(f));
-	fclose(f);
+	int f = open(file_new.c_str(), O_RDWR);
+	int ret = fdatasync(f);
+	close(f);
 	if (ret != 0) return false;
-#else
-	fclose(f);
 #endif
 
 #if defined(_WIN32)
@@ -91,7 +94,7 @@ bool IniFile::SaveToDisk(const char *filename)
 	/* Allocate space for one more \0 character. */
 	TCHAR tfilename[MAX_PATH + 1], tfile_new[MAX_PATH + 1];
 	_tcsncpy(tfilename, OTTD2FS(filename), MAX_PATH);
-	_tcsncpy(tfile_new, OTTD2FS(file_new), MAX_PATH);
+	_tcsncpy(tfile_new, OTTD2FS(file_new.c_str()), MAX_PATH);
 	/* SHFileOperation wants a double '\0' terminated string. */
 	tfilename[MAX_PATH - 1] = '\0';
 	tfile_new[MAX_PATH - 1] = '\0';
@@ -107,8 +110,8 @@ bool IniFile::SaveToDisk(const char *filename)
 	shfopt.pTo    = tfilename;
 	SHFileOperation(&shfopt);
 #else
-	if (rename(file_new, filename) < 0) {
-		DEBUG(misc, 0, "Renaming %s to %s failed; configuration not saved", file_new, filename);
+	if (rename(file_new.c_str(), filename) < 0) {
+		DEBUG(misc, 0, "Renaming %s to %s failed; configuration not saved", file_new.c_str(), filename);
 	}
 #endif
 
diff --git a/src/ini_load.cpp b/src/ini_load.cpp
index dd72831308..93b6bdf72d 100644
--- a/src/ini_load.cpp
+++ b/src/ini_load.cpp
@@ -19,12 +19,10 @@
  * Construct a new in-memory item of an Ini file.
  * @param parent the group we belong to
  * @param name   the name of the item
- * @param last   the last element of the name of the item
  */
-IniItem::IniItem(IniGroup *parent, const char *name, const char *last) : next(nullptr), value(nullptr), comment(nullptr)
+IniItem::IniItem(IniGroup *parent, const std::string &name) : next(nullptr)
 {
-	this->name = stredup(name, last);
-	str_validate(this->name, this->name + strlen(this->name));
+	this->name = str_validate(name);
 
 	*parent->last_item = this;
 	parent->last_item = &this->next;
@@ -33,10 +31,6 @@ IniItem::IniItem(IniGroup *parent, const char *name, const char *last) : next(nu
 /** Free everything we loaded. */
 IniItem::~IniItem()
 {
-	free(this->name);
-	free(this->value);
-	free(this->comment);
-
 	delete this->next;
 }
 
@@ -46,20 +40,21 @@ IniItem::~IniItem()
  */
 void IniItem::SetValue(const char *value)
 {
-	free(this->value);
-	this->value = stredup(value);
+	if (value == nullptr) {
+		this->value.reset();
+	} else {
+		this->value.emplace(value);
+	}
 }
 
 /**
  * Construct a new in-memory group of an Ini file.
  * @param parent the file we belong to
  * @param name   the name of the group
- * @param last   the last element of the name of the group
  */
-IniGroup::IniGroup(IniLoadFile *parent, const char *name, const char *last) : next(nullptr), type(IGT_VARIABLES), item(nullptr), comment(nullptr)
+IniGroup::IniGroup(IniLoadFile *parent, const std::string &name) : next(nullptr), type(IGT_VARIABLES), item(nullptr)
 {
-	this->name = stredup(name, last);
-	str_validate(this->name, this->name + strlen(this->name));
+	this->name = str_validate(name);
 
 	this->last_item = &this->item;
 	*parent->last_group = this;
@@ -67,7 +62,7 @@ IniGroup::IniGroup(IniLoadFile *parent, const char *name, const char *last) : ne
 
 	if (parent->list_group_names != nullptr) {
 		for (uint i = 0; parent->list_group_names[i] != nullptr; i++) {
-			if (strcmp(this->name, parent->list_group_names[i]) == 0) {
+			if (this->name == parent->list_group_names[i]) {
 				this->type = IGT_LIST;
 				return;
 			}
@@ -75,7 +70,7 @@ IniGroup::IniGroup(IniLoadFile *parent, const char *name, const char *last) : ne
 	}
 	if (parent->seq_group_names != nullptr) {
 		for (uint i = 0; parent->seq_group_names[i] != nullptr; i++) {
-			if (strcmp(this->name, parent->seq_group_names[i]) == 0) {
+			if (this->name == parent->seq_group_names[i]) {
 				this->type = IGT_SEQUENCE;
 				return;
 			}
@@ -86,9 +81,6 @@ IniGroup::IniGroup(IniLoadFile *parent, const char *name, const char *last) : ne
 /** Free everything we loaded. */
 IniGroup::~IniGroup()
 {
-	free(this->name);
-	free(this->comment);
-
 	delete this->item;
 	delete this->next;
 }
@@ -100,16 +92,16 @@ IniGroup::~IniGroup()
  * @param create whether to create an item when not found or not.
  * @return the requested item or nullptr if not found.
  */
-IniItem *IniGroup::GetItem(const char *name, bool create)
+IniItem *IniGroup::GetItem(const std::string &name, bool create)
 {
 	for (IniItem *item = this->item; item != nullptr; item = item->next) {
-		if (strcmp(item->name, name) == 0) return item;
+		if (item->name == name) return item;
 	}
 
 	if (!create) return nullptr;
 
 	/* otherwise make a new one */
-	return new IniItem(this, name, nullptr);
+	return new IniItem(this, name);
 }
 
 /**
@@ -129,7 +121,6 @@ void IniGroup::Clear()
  */
 IniLoadFile::IniLoadFile(const char * const *list_group_names, const char * const *seq_group_names) :
 		group(nullptr),
-		comment(nullptr),
 		list_group_names(list_group_names),
 		seq_group_names(seq_group_names)
 {
@@ -139,7 +130,6 @@ IniLoadFile::IniLoadFile(const char * const *list_group_names, const char * cons
 /** Free everything we loaded. */
 IniLoadFile::~IniLoadFile()
 {
-	free(this->comment);
 	delete this->group;
 }
 
@@ -147,26 +137,21 @@ IniLoadFile::~IniLoadFile()
  * Get the group with the given name. If it doesn't exist
  * and \a create_new is \c true create a new group.
  * @param name name of the group to find.
- * @param len  the maximum length of said name (\c 0 means length of the string).
  * @param create_new Allow creation of group if it does not exist.
  * @return The requested group if it exists or was created, else \c nullptr.
  */
-IniGroup *IniLoadFile::GetGroup(const char *name, size_t len, bool create_new)
+IniGroup *IniLoadFile::GetGroup(const std::string &name, bool create_new)
 {
-	if (len == 0) len = strlen(name);
-
 	/* does it exist already? */
 	for (IniGroup *group = this->group; group != nullptr; group = group->next) {
-		if (!strncmp(group->name, name, len) && group->name[len] == 0) {
-			return group;
-		}
+		if (group->name == name) return group;
 	}
 
 	if (!create_new) return nullptr;
 
 	/* otherwise make a new one */
-	IniGroup *group = new IniGroup(this, name, name + len - 1);
-	group->comment = stredup("\n");
+	IniGroup *group = new IniGroup(this, name);
+	group->comment = "\n";
 	return group;
 }
 
@@ -182,7 +167,7 @@ void IniLoadFile::RemoveGroup(const char *name)
 
 	/* does it exist already? */
 	for (group = this->group; group != nullptr; prev = group, group = group->next) {
-		if (strncmp(group->name, name, len) == 0) {
+		if (group->name.compare(0, len, name) == 0) {
 			break;
 		}
 	}
@@ -260,17 +245,17 @@ void IniLoadFile::LoadFromDisk(const char *filename, Subdirectory subdir)
 				e--;
 			}
 			s++; // skip [
-			group = new IniGroup(this, s, e - 1);
+			group = new IniGroup(this, std::string(s, e - s));
 			if (comment_size != 0) {
-				group->comment = stredup(comment, comment + comment_size - 1);
+				group->comment.assign(comment, comment_size);
 				comment_size = 0;
 			}
 		} else if (group != nullptr) {
 			if (group->type == IGT_SEQUENCE) {
 				/* A sequence group, use the line as item name without further interpretation. */
-				IniItem *item = new IniItem(group, buffer, e - 1);
+				IniItem *item = new IniItem(group, std::string(buffer, e - buffer));
 				if (comment_size) {
-					item->comment = stredup(comment, comment + comment_size - 1);
+					item->comment.assign(comment, comment_size);
 					comment_size = 0;
 				}
 				continue;
@@ -286,9 +271,9 @@ void IniLoadFile::LoadFromDisk(const char *filename, Subdirectory subdir)
 			}
 
 			/* it's an item in an existing group */
-			IniItem *item = new IniItem(group, s, t - 1);
+			IniItem *item = new IniItem(group, std::string(s, t - s));
 			if (comment_size != 0) {
-				item->comment = stredup(comment, comment + comment_size - 1);
+				item->comment.assign(comment, comment_size);
 				comment_size = 0;
 			}
 
@@ -304,8 +289,11 @@ void IniLoadFile::LoadFromDisk(const char *filename, Subdirectory subdir)
 			*e = '\0';
 
 			/* If the value was not quoted and empty, it must be nullptr */
-			item->value = (!quoted && e == t) ? nullptr : stredup(t);
-			if (item->value != nullptr) str_validate(item->value, item->value + strlen(item->value));
+			if (!quoted && e == t) {
+				item->value.reset();
+			} else {
+				item->value = str_validate(std::string(t));
+			}
 		} else {
 			/* it's an orphan item */
 			this->ReportFileError("ini: '", buffer, "' outside of group");
@@ -313,7 +301,7 @@ void IniLoadFile::LoadFromDisk(const char *filename, Subdirectory subdir)
 	}
 
 	if (comment_size > 0) {
-		this->comment = stredup(comment, comment + comment_size - 1);
+		this->comment.assign(comment, comment_size);
 		comment_size = 0;
 	}
 
diff --git a/src/ini_type.h b/src/ini_type.h
index 679969efda..f98b6395b9 100644
--- a/src/ini_type.h
+++ b/src/ini_type.h
@@ -11,6 +11,8 @@
 #define INI_TYPE_H
 
 #include "fileio_type.h"
+#include <string>
+#include "3rdparty/optional/ottd_optional.h"
 
 /** Types of groups */
 enum IniGroupType {
@@ -21,12 +23,12 @@ enum IniGroupType {
 
 /** A single "line" in an ini file. */
 struct IniItem {
-	IniItem *next; ///< The next item in this group
-	char *name;    ///< The name of this item
-	char *value;   ///< The value of this item
-	char *comment; ///< The comment associated with this item
+	IniItem *next;                    ///< The next item in this group
+	std::string name;                 ///< The name of this item
+	opt::optional<std::string> value; ///< The value of this item
+	std::string comment;              ///< The comment associated with this item
 
-	IniItem(struct IniGroup *parent, const char *name, const char *last = nullptr);
+	IniItem(struct IniGroup *parent, const std::string &name);
 	~IniItem();
 
 	void SetValue(const char *value);
@@ -38,13 +40,13 @@ struct IniGroup {
 	IniGroupType type;   ///< type of group
 	IniItem *item;       ///< the first item in the group
 	IniItem **last_item; ///< the last item in the group
-	char *name;          ///< name of group
-	char *comment;       ///< comment for group
+	std::string name;    ///< name of group
+	std::string comment; ///< comment for group
 
-	IniGroup(struct IniLoadFile *parent, const char *name, const char *last = nullptr);
+	IniGroup(struct IniLoadFile *parent, const std::string &name);
 	~IniGroup();
 
-	IniItem *GetItem(const char *name, bool create);
+	IniItem *GetItem(const std::string &name, bool create);
 	void Clear();
 };
 
@@ -52,14 +54,14 @@ struct IniGroup {
 struct IniLoadFile {
 	IniGroup *group;                      ///< the first group in the ini
 	IniGroup **last_group;                ///< the last group in the ini
-	char *comment;                        ///< last comment in file
+	std::string comment;                  ///< last comment in file
 	const char * const *list_group_names; ///< nullptr terminated list with group names that are lists
 	const char * const *seq_group_names;  ///< nullptr terminated list with group names that are sequences.
 
 	IniLoadFile(const char * const *list_group_names = nullptr, const char * const *seq_group_names = nullptr);
 	virtual ~IniLoadFile();
 
-	IniGroup *GetGroup(const char *name, size_t len = 0, bool create_new = true);
+	IniGroup *GetGroup(const std::string &name, bool create_new = true);
 	void RemoveGroup(const char *name);
 
 	void LoadFromDisk(const char *filename, Subdirectory subdir);
diff --git a/src/music.cpp b/src/music.cpp
index a95a9d348d..131651e55c 100644
--- a/src/music.cpp
+++ b/src/music.cpp
@@ -135,10 +135,10 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f
 			this->songinfo[i].filename = filename; // non-owned pointer
 
 			IniItem *item = catindex->GetItem(_music_file_names[i], false);
-			if (item != nullptr && !StrEmpty(item->value)) {
+			if (item != nullptr && item->value.has_value() && !item->value->empty()) {
 				/* Song has a CAT file index, assume it's MPS MIDI format */
 				this->songinfo[i].filetype = MTT_MPSMIDI;
-				this->songinfo[i].cat_index = atoi(item->value);
+				this->songinfo[i].cat_index = atoi(item->value->c_str());
 				char *songname = GetMusicCatEntryName(filename, this->songinfo[i].cat_index);
 				if (songname == nullptr) {
 					DEBUG(grf, 0, "Base music set song missing from CAT file: %s/%d", filename, this->songinfo[i].cat_index);
@@ -161,12 +161,12 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f
 				while (*trimmed_filename == PATHSEPCHAR) trimmed_filename++;
 
 				item = names->GetItem(trimmed_filename, false);
-				if (item != nullptr && !StrEmpty(item->value)) break;
+				if (item != nullptr && item->value.has_value() && !item->value->empty()) break;
 			}
 
 			if (this->songinfo[i].filetype == MTT_STANDARDMIDI) {
-				if (item != nullptr && !StrEmpty(item->value)) {
-					strecpy(this->songinfo[i].songname, item->value, lastof(this->songinfo[i].songname));
+				if (item != nullptr && item->value.has_value() && !item->value->empty()) {
+					strecpy(this->songinfo[i].songname, item->value->c_str(), lastof(this->songinfo[i].songname));
 				} else {
 					DEBUG(grf, 0, "Base music set song name missing: %s", filename);
 					return false;
@@ -181,12 +181,12 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f
 				this->songinfo[i].tracknr = tracknr++;
 			}
 
-			item = timingtrim->GetItem(trimmed_filename, false);
-			if (item != nullptr && !StrEmpty(item->value)) {
-				const char *endpos = strchr(item->value, ':');
-				if (endpos != nullptr) {
-					this->songinfo[i].override_start = atoi(item->value);
-					this->songinfo[i].override_end = atoi(endpos + 1);
+			item = trimmed_filename != nullptr ? timingtrim->GetItem(trimmed_filename, false) : nullptr;
+			if (item != nullptr && item->value.has_value() && !item->value->empty()) {
+				auto endpos = item->value->find(':');
+				if (endpos != std::string::npos) {
+					this->songinfo[i].override_start = atoi(item->value->c_str());
+					this->songinfo[i].override_end = atoi(item->value->c_str() + endpos + 1);
 				}
 			}
 		}
diff --git a/src/settings.cpp b/src/settings.cpp
index 7fb2e72491..ef1aaa1b6c 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -505,10 +505,6 @@ static void IniLoadSettings(IniFile *ini, const SettingDesc *sd, const char *grp
 {
 	IniGroup *group;
 	IniGroup *group_def = ini->GetGroup(grpname);
-	IniItem *item;
-	const void *p;
-	void *ptr;
-	const char *s;
 
 	for (; sd->save.cmd != SL_END; sd++) {
 		const SettingDescBase *sdb = &sd->desc;
@@ -517,30 +513,30 @@ static void IniLoadSettings(IniFile *ini, const SettingDesc *sd, const char *grp
 		if (!SlIsObjectCurrentlyValid(sld->version_from, sld->version_to)) continue;
 
 		/* For settings.xx.yy load the settings from [xx] yy = ? */
-		s = strchr(sdb->name, '.');
-		if (s != nullptr) {
-			group = ini->GetGroup(sdb->name, s - sdb->name);
-			s++;
+		std::string s{ sdb->name };
+		auto sc = s.find('.');
+		if (sc != std::string::npos) {
+			group = ini->GetGroup(s.substr(0, sc));
+			s = s.substr(sc + 1);
 		} else {
-			s = sdb->name;
 			group = group_def;
 		}
 
-		item = group->GetItem(s, false);
+		IniItem *item = group->GetItem(s, false);
 		if (item == nullptr && group != group_def) {
-			/* For settings.xx.yy load the settings from [settingss] yy = ? in case the previous
+			/* For settings.xx.yy load the settings from [settings] yy = ? in case the previous
 			 * did not exist (e.g. loading old config files with a [settings] section */
 			item = group_def->GetItem(s, false);
 		}
 		if (item == nullptr) {
 			/* For settings.xx.zz.yy load the settings from [zz] yy = ? in case the previous
 			 * did not exist (e.g. loading old config files with a [yapf] section */
-			const char *sc = strchr(s, '.');
-			if (sc != nullptr) item = ini->GetGroup(s, sc - s)->GetItem(sc + 1, false);
+			sc = s.find('.');
+			if (sc != std::string::npos) item = ini->GetGroup(s.substr(0, sc))->GetItem(s.substr(sc + 1), false);
 		}
 
-		p = (item == nullptr) ? sdb->def : StringToVal(sdb, item->value);
-		ptr = GetVariableAddress(object, sld);
+		const void *p = (item == nullptr) ? sdb->def : StringToVal(sdb, item->value.has_value() ? item->value->c_str() : nullptr);
+		void *ptr = GetVariableAddress(object, sld);
 
 		switch (sdb->cmd) {
 			case SDT_BOOLX: // All four are various types of (integer) numbers
@@ -604,7 +600,6 @@ static void IniSaveSettings(IniFile *ini, const SettingDesc *sd, const char *grp
 	IniGroup *group_def = nullptr, *group;
 	IniItem *item;
 	char buf[512];
-	const char *s;
 	void *ptr;
 
 	for (; sd->save.cmd != SL_END; sd++) {
@@ -617,22 +612,22 @@ static void IniSaveSettings(IniFile *ini, const SettingDesc *sd, const char *grp
 		if (sld->conv & SLF_NOT_IN_CONFIG) continue;
 
 		/* XXX - wtf is this?? (group override?) */
-		s = strchr(sdb->name, '.');
-		if (s != nullptr) {
-			group = ini->GetGroup(sdb->name, s - sdb->name);
-			s++;
+		std::string s{ sdb->name };
+		auto sc = s.find('.');
+		if (sc != std::string::npos) {
+			group = ini->GetGroup(s.substr(0, sc));
+			s = s.substr(sc + 1);
 		} else {
 			if (group_def == nullptr) group_def = ini->GetGroup(grpname);
-			s = sdb->name;
 			group = group_def;
 		}
 
 		item = group->GetItem(s, true);
 		ptr = GetVariableAddress(object, sld);
 
-		if (item->value != nullptr) {
+		if (item->value.has_value()) {
 			/* check if the value is the same as the old value */
-			const void *p = StringToVal(sdb, item->value);
+			const void *p = StringToVal(sdb, item->value->c_str());
 
 			/* The main type of a variable/setting is in bytes 8-15
 			 * The subtype (what kind of numbers do we have there) is in 0-7 */
@@ -714,8 +709,7 @@ static void IniSaveSettings(IniFile *ini, const SettingDesc *sd, const char *grp
 		}
 
 		/* The value is different, that means we have to write it to the ini */
-		free(item->value);
-		item->value = stredup(buf);
+		item->value.emplace(buf);
 	}
 }
 
@@ -737,7 +731,7 @@ static void IniLoadSettingList(IniFile *ini, const char *grpname, StringList &li
 	list.clear();
 
 	for (const IniItem *item = group->item; item != nullptr; item = item->next) {
-		if (item->name != nullptr) list.emplace_back(item->name);
+		if (!item->name.empty()) list.push_back(item->name);
 	}
 }
 
@@ -1427,14 +1421,14 @@ static void AILoadConfig(IniFile *ini, const char *grpname)
 	for (item = group->item; c < MAX_COMPANIES && item != nullptr; c++, item = item->next) {
 		AIConfig *config = AIConfig::GetConfig(c, AIConfig::SSS_FORCE_NEWGAME);
 
-		config->Change(item->name);
+		config->Change(item->name.c_str());
 		if (!config->HasScript()) {
-			if (strcmp(item->name, "none") != 0) {
-				DEBUG(script, 0, "The AI by the name '%s' was no longer found, and removed from the list.", item->name);
+			if (item->name != "none") {
+				DEBUG(script, 0, "The AI by the name '%s' was no longer found, and removed from the list.", item->name.c_str());
 				continue;
 			}
 		}
-		if (item->value != nullptr) config->StringToSettings(item->value);
+		if (item->value.has_value()) config->StringToSettings(item->value->c_str());
 	}
 }
 
@@ -1454,14 +1448,14 @@ static void GameLoadConfig(IniFile *ini, const char *grpname)
 
 	GameConfig *config = GameConfig::GetConfig(AIConfig::SSS_FORCE_NEWGAME);
 
-	config->Change(item->name);
+	config->Change(item->name.c_str());
 	if (!config->HasScript()) {
-		if (strcmp(item->name, "none") != 0) {
-			DEBUG(script, 0, "The GameScript by the name '%s' was no longer found, and removed from the list.", item->name);
+		if (item->name != "none") {
+			DEBUG(script, 0, "The GameScript by the name '%s' was no longer found, and removed from the list.", item->name.c_str());
 			return;
 		}
 	}
-	if (item->value != nullptr) config->StringToSettings(item->value);
+	if (item->value.has_value()) config->StringToSettings(item->value->c_str());
 }
 
 /**
@@ -1485,7 +1479,7 @@ static int DecodeHexNibble(char c)
  * @param dest_size Number of bytes in \a dest.
  * @return Whether reading was successful.
  */
-static bool DecodeHexText(char *pos, uint8 *dest, size_t dest_size)
+static bool DecodeHexText(const char *pos, uint8 *dest, size_t dest_size)
 {
 	while (dest_size > 0) {
 		int hi = DecodeHexNibble(pos[0]);
@@ -1517,7 +1511,7 @@ static GRFConfig *GRFLoadConfig(IniFile *ini, const char *grpname, bool is_stati
 		GRFConfig *c = nullptr;
 
 		uint8 grfid_buf[4], md5sum[16];
-		char *filename = item->name;
+		const char *filename = item->name.c_str();
 		bool has_grfid = false;
 		bool has_md5sum = false;
 
@@ -1541,8 +1535,8 @@ static GRFConfig *GRFLoadConfig(IniFile *ini, const char *grpname, bool is_stati
 		if (c == nullptr) c = new GRFConfig(filename);
 
 		/* Parse parameters */
-		if (!StrEmpty(item->value)) {
-			int count = ParseIntList(item->value, c->param, lengthof(c->param));
+		if (item->value.has_value() && !item->value->empty()) {
+			int count = ParseIntList(item->value->c_str(), c->param, lengthof(c->param));
 			if (count < 0) {
 				SetDParamStr(0, filename);
 				ShowErrorMessage(STR_CONFIG_ERROR, STR_CONFIG_ERROR_ARRAY, WL_CRITICAL);
@@ -1565,7 +1559,7 @@ static GRFConfig *GRFLoadConfig(IniFile *ini, const char *grpname, bool is_stati
 				SetDParam(1, STR_CONFIG_ERROR_INVALID_GRF_UNKNOWN);
 			}
 
-			SetDParamStr(0, StrEmpty(filename) ? item->name : filename);
+			SetDParamStr(0, StrEmpty(filename) ? item->name.c_str() : filename);
 			ShowErrorMessage(STR_CONFIG_ERROR, STR_CONFIG_ERROR_INVALID_GRF, WL_CRITICAL);
 			delete c;
 			continue;
@@ -1776,8 +1770,8 @@ StringList GetGRFPresetList()
 
 	std::unique_ptr<IniFile> ini(IniLoadConfig());
 	for (IniGroup *group = ini->group; group != nullptr; group = group->next) {
-		if (strncmp(group->name, "preset-", 7) == 0) {
-			list.emplace_back(group->name + 7);
+		if (group->name.compare(0, 7, "preset-") == 0) {
+			list.push_back(group->name.substr(7));
 		}
 	}
 
diff --git a/src/settingsgen/settingsgen.cpp b/src/settingsgen/settingsgen.cpp
index 9f272dabbe..764f370bde 100644
--- a/src/settingsgen/settingsgen.cpp
+++ b/src/settingsgen/settingsgen.cpp
@@ -214,11 +214,11 @@ static IniLoadFile *LoadIniFile(const char *filename)
  */
 static void DumpGroup(IniLoadFile *ifile, const char * const group_name)
 {
-	IniGroup *grp = ifile->GetGroup(group_name, 0, false);
+	IniGroup *grp = ifile->GetGroup(group_name, false);
 	if (grp != nullptr && grp->type == IGT_SEQUENCE) {
 		for (IniItem *item = grp->item; item != nullptr; item = item->next) {
-			if (item->name) {
-				_stored_output.Add(item->name);
+			if (!item->name.empty()) {
+				_stored_output.Add(item->name.c_str());
 				_stored_output.Add("\n", 1);
 			}
 		}
@@ -236,8 +236,8 @@ static const char *FindItemValue(const char *name, IniGroup *grp, IniGroup *defa
 {
 	IniItem *item = grp->GetItem(name, false);
 	if (item == nullptr && defaults != nullptr) item = defaults->GetItem(name, false);
-	if (item == nullptr || item->value == nullptr) return nullptr;
-	return item->value;
+	if (item == nullptr || !item->value.has_value()) return nullptr;
+	return item->value->c_str();
 }
 
 /**
@@ -249,19 +249,19 @@ static void DumpSections(IniLoadFile *ifile)
 	static const int MAX_VAR_LENGTH = 64;
 	static const char * const special_group_names[] = {PREAMBLE_GROUP_NAME, POSTAMBLE_GROUP_NAME, DEFAULTS_GROUP_NAME, TEMPLATES_GROUP_NAME, nullptr};
 
-	IniGroup *default_grp = ifile->GetGroup(DEFAULTS_GROUP_NAME, 0, false);
-	IniGroup *templates_grp  = ifile->GetGroup(TEMPLATES_GROUP_NAME, 0, false);
+	IniGroup *default_grp = ifile->GetGroup(DEFAULTS_GROUP_NAME, false);
+	IniGroup *templates_grp  = ifile->GetGroup(TEMPLATES_GROUP_NAME, false);
 	if (templates_grp == nullptr) return;
 
 	/* Output every group, using its name as template name. */
 	for (IniGroup *grp = ifile->group; grp != nullptr; grp = grp->next) {
 		const char * const *sgn;
-		for (sgn = special_group_names; *sgn != nullptr; sgn++) if (strcmp(grp->name, *sgn) == 0) break;
+		for (sgn = special_group_names; *sgn != nullptr; sgn++) if (grp->name == *sgn) break;
 		if (*sgn != nullptr) continue;
 
 		IniItem *template_item = templates_grp->GetItem(grp->name, false); // Find template value.
-		if (template_item == nullptr || template_item->value == nullptr) {
-			fprintf(stderr, "settingsgen: Warning: Cannot find template %s\n", grp->name);
+		if (template_item == nullptr || !template_item->value.has_value()) {
+			fprintf(stderr, "settingsgen: Warning: Cannot find template %s\n", grp->name.c_str());
 			continue;
 		}
 
@@ -281,7 +281,7 @@ static void DumpSections(IniLoadFile *ifile)
 		}
 
 		/* Output text of the template, except template variables of the form '$[_a-z0-9]+' which get replaced by their value. */
-		const char *txt = template_item->value;
+		const char *txt = template_item->value->c_str();
 		while (*txt != '\0') {
 			if (*txt != '$') {
 				_stored_output.Add(txt, 1);
diff --git a/src/string.cpp b/src/string.cpp
index 90d287c9ef..bec7c8e926 100644
--- a/src/string.cpp
+++ b/src/string.cpp
@@ -185,18 +185,11 @@ void str_fix_scc_encoded(char *str, const char *last)
 }
 
 
-/**
- * Scans the string for valid characters and if it finds invalid ones,
- * replaces them with a question mark '?' (if not ignored)
- * @param str the string to validate
- * @param last the last valid character of str
- * @param settings the settings for the string validation.
- */
-void str_validate(char *str, const char *last, StringValidationSettings settings)
+template <class T>
+static void str_validate(T &dst, const char *str, const char *last, StringValidationSettings settings)
 {
 	/* Assume the ABSOLUTE WORST to be in str as it comes from the outside. */
 
-	char *dst = str;
 	while (str <= last && *str != '\0') {
 		size_t len = Utf8EncodedCharLen(*str);
 		/* If the character is unknown, i.e. encoded length is 0
@@ -220,7 +213,7 @@ void str_validate(char *str, const char *last, StringValidationSettings settings
 			do {
 				*dst++ = *str++;
 			} while (--len != 0);
-		} else if ((settings & SVS_ALLOW_NEWLINE) != 0  && c == '\n') {
+		} else if ((settings & SVS_ALLOW_NEWLINE) != 0 && c == '\n') {
 			*dst++ = *str++;
 		} else {
 			if ((settings & SVS_ALLOW_NEWLINE) != 0 && c == '\r' && str[1] == '\n') {
@@ -232,10 +225,40 @@ void str_validate(char *str, const char *last, StringValidationSettings settings
 			if ((settings & SVS_REPLACE_WITH_QUESTION_MARK) != 0) *dst++ = '?';
 		}
 	}
+}
 
+/**
+ * Scans the string for valid characters and if it finds invalid ones,
+ * replaces them with a question mark '?' (if not ignored)
+ * @param str the string to validate
+ * @param last the last valid character of str
+ * @param settings the settings for the string validation.
+ */
+void str_validate(char *str, const char *last, StringValidationSettings settings)
+{
+	char *dst = str;
+	str_validate(dst, str, last, settings);
 	*dst = '\0';
 }
 
+/**
+ * Scans the string for valid characters and if it finds invalid ones,
+ * replaces them with a question mark '?' (if not ignored)
+ * @param str the string to validate
+ * @param settings the settings for the string validation.
+ */
+std::string str_validate(const std::string &str, StringValidationSettings settings)
+{
+	auto buf = str.data();
+	auto last = buf + str.size();
+
+	std::ostringstream dst;
+	std::ostreambuf_iterator<char> dst_iter(dst);
+	str_validate(dst_iter, buf, last, settings);
+
+	return dst.str();
+}
+
 /**
  * Scans the string for valid characters and if it finds invalid ones,
  * replaces them with a question mark '?'.
diff --git a/src/string_func.h b/src/string_func.h
index 002b8f2177..febdf7c2a4 100644
--- a/src/string_func.h
+++ b/src/string_func.h
@@ -40,6 +40,7 @@ int CDECL vseprintf(char *str, const char *last, const char *format, va_list ap)
 char *CDECL str_fmt(const char *str, ...) WARN_FORMAT(1, 2);
 
 void str_validate(char *str, const char *last, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK);
+std::string str_validate(const std::string &str, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK);
 void ValidateString(const char *str);
 
 void str_fix_scc_encoded(char *str, const char *last);