diff --git a/src/DatabaseSave.cxx b/src/DatabaseSave.cxx
index 5bd50f55f..78a2c4939 100644
--- a/src/DatabaseSave.cxx
+++ b/src/DatabaseSave.cxx
@@ -77,9 +77,8 @@ db_save_internal(FILE *fp, const Directory *music_root)
 }
 
 bool
-db_load_internal(FILE *fp, Directory *music_root, GError **error)
+db_load_internal(TextFile &file, Directory *music_root, GError **error)
 {
-	GString *buffer = g_string_sized_new(1024);
 	char *line;
 	int format = 0;
 	bool found_charset = false, found_version = false;
@@ -89,16 +88,15 @@ db_load_internal(FILE *fp, Directory *music_root, GError **error)
 	assert(music_root != NULL);
 
 	/* get initial info */
-	line = read_text_line(fp, buffer);
+	line = file.ReadLine();
 	if (line == NULL || strcmp(DIRECTORY_INFO_BEGIN, line) != 0) {
 		g_set_error(error, db_quark(), 0, "Database corrupted");
-		g_string_free(buffer, true);
 		return false;
 	}
 
 	memset(tags, false, sizeof(tags));
 
-	while ((line = read_text_line(fp, buffer)) != NULL &&
+	while ((line = file.ReadLine()) != NULL &&
 	       strcmp(line, DIRECTORY_INFO_END) != 0) {
 		if (g_str_has_prefix(line, DB_FORMAT_PREFIX)) {
 			format = atoi(line + sizeof(DB_FORMAT_PREFIX) - 1);
@@ -106,7 +104,6 @@ db_load_internal(FILE *fp, Directory *music_root, GError **error)
 			if (found_version) {
 				g_set_error(error, db_quark(), 0,
 					    "Duplicate version line");
-				g_string_free(buffer, true);
 				return false;
 			}
 
@@ -117,7 +114,6 @@ db_load_internal(FILE *fp, Directory *music_root, GError **error)
 			if (found_charset) {
 				g_set_error(error, db_quark(), 0,
 					    "Duplicate charset line");
-				g_string_free(buffer, true);
 				return false;
 			}
 
@@ -132,7 +128,6 @@ db_load_internal(FILE *fp, Directory *music_root, GError **error)
 					    "\"%s\" instead of \"%s\"; "
 					    "discarding database file",
 					    new_charset, old_charset);
-				g_string_free(buffer, true);
 				return false;
 			}
 		} else if (g_str_has_prefix(line, DB_TAG_PREFIX)) {
@@ -150,7 +145,6 @@ db_load_internal(FILE *fp, Directory *music_root, GError **error)
 		} else {
 			g_set_error(error, db_quark(), 0,
 				    "Malformed line: %s", line);
-			g_string_free(buffer, true);
 			return false;
 		}
 	}
@@ -174,9 +168,8 @@ db_load_internal(FILE *fp, Directory *music_root, GError **error)
 	g_debug("reading DB");
 
 	db_lock();
-	success = directory_load(fp, music_root, buffer, error);
+	success = directory_load(file, music_root, error);
 	db_unlock();
-	g_string_free(buffer, true);
 
 	return success;
 }
diff --git a/src/DatabaseSave.hxx b/src/DatabaseSave.hxx
index 5133961fe..40048f261 100644
--- a/src/DatabaseSave.hxx
+++ b/src/DatabaseSave.hxx
@@ -25,11 +25,12 @@
 #include <stdio.h>
 
 struct Directory;
+class TextFile;
 
 void
 db_save_internal(FILE *file, const Directory *root);
 
 bool
-db_load_internal(FILE *file, Directory *root, GError **error);
+db_load_internal(TextFile &file, Directory *root, GError **error);
 
 #endif
diff --git a/src/DirectorySave.cxx b/src/DirectorySave.cxx
index 9c5df685f..6a5efb058 100644
--- a/src/DirectorySave.cxx
+++ b/src/DirectorySave.cxx
@@ -76,10 +76,9 @@ directory_save(FILE *fp, const Directory *directory)
 }
 
 static Directory *
-directory_load_subdir(FILE *fp, Directory *parent, const char *name,
-		      GString *buffer, GError **error_r)
+directory_load_subdir(TextFile &file, Directory *parent, const char *name,
+		      GError **error_r)
 {
-	const char *line;
 	bool success;
 
 	if (parent->FindChild(name) != nullptr) {
@@ -90,7 +89,7 @@ directory_load_subdir(FILE *fp, Directory *parent, const char *name,
 
 	Directory *directory = parent->CreateChild(name);
 
-	line = read_text_line(fp, buffer);
+	const char *line = file.ReadLine();
 	if (line == NULL) {
 		g_set_error(error_r, directory_quark(), 0,
 			    "Unexpected end of file");
@@ -103,7 +102,7 @@ directory_load_subdir(FILE *fp, Directory *parent, const char *name,
 			g_ascii_strtoull(line + sizeof(DIRECTORY_MTIME) - 1,
 					 NULL, 10);
 
-		line = read_text_line(fp, buffer);
+		line = file.ReadLine();
 		if (line == NULL) {
 			g_set_error(error_r, directory_quark(), 0,
 				    "Unexpected end of file");
@@ -119,7 +118,7 @@ directory_load_subdir(FILE *fp, Directory *parent, const char *name,
 		return NULL;
 	}
 
-	success = directory_load(fp, directory, buffer, error_r);
+	success = directory_load(file, directory, error_r);
 	if (!success) {
 		directory->Delete();
 		return NULL;
@@ -129,18 +128,17 @@ directory_load_subdir(FILE *fp, Directory *parent, const char *name,
 }
 
 bool
-directory_load(FILE *fp, Directory *directory,
-	       GString *buffer, GError **error)
+directory_load(TextFile &file, Directory *directory, GError **error)
 {
 	const char *line;
 
-	while ((line = read_text_line(fp, buffer)) != NULL &&
+	while ((line = file.ReadLine()) != NULL &&
 	       !g_str_has_prefix(line, DIRECTORY_END)) {
 		if (g_str_has_prefix(line, DIRECTORY_DIR)) {
 			Directory *subdir =
-				directory_load_subdir(fp, directory,
+				directory_load_subdir(file, directory,
 						      line + sizeof(DIRECTORY_DIR) - 1,
-						      buffer, error);
+						      error);
 			if (subdir == NULL)
 				return false;
 		} else if (g_str_has_prefix(line, SONG_BEGIN)) {
@@ -153,8 +151,7 @@ directory_load(FILE *fp, Directory *directory,
 				return false;
 			}
 
-			song = song_load(fp, directory, name,
-					 buffer, error);
+			song = song_load(file, directory, name, error);
 			if (song == NULL)
 				return false;
 
@@ -165,8 +162,8 @@ directory_load(FILE *fp, Directory *directory,
 			   buffer */
 			char *name = g_strdup(line + sizeof(PLAYLIST_META_BEGIN) - 1);
 
-			if (!playlist_metadata_load(fp, directory->playlists,
-						    name, buffer, error)) {
+			if (!playlist_metadata_load(file, directory->playlists,
+						    name, error)) {
 				g_free(name);
 				return false;
 			}
diff --git a/src/DirectorySave.hxx b/src/DirectorySave.hxx
index 83b820618..a7f3034a7 100644
--- a/src/DirectorySave.hxx
+++ b/src/DirectorySave.hxx
@@ -25,12 +25,12 @@
 #include <stdio.h>
 
 struct Directory;
+class TextFile;
 
 void
 directory_save(FILE *fp, const Directory *directory);
 
 bool
-directory_load(FILE *fp, Directory *directory,
-	       GString *buffer, GError **error);
+directory_load(TextFile &file, Directory *directory, GError **error);
 
 #endif
diff --git a/src/PlaylistDatabase.cxx b/src/PlaylistDatabase.cxx
index 984af4adc..edc6a2815 100644
--- a/src/PlaylistDatabase.cxx
+++ b/src/PlaylistDatabase.cxx
@@ -46,15 +46,15 @@ playlist_vector_save(FILE *fp, const PlaylistVector &pv)
 }
 
 bool
-playlist_metadata_load(FILE *fp, PlaylistVector &pv, const char *name,
-		       GString *buffer, GError **error_r)
+playlist_metadata_load(TextFile &file, PlaylistVector &pv, const char *name,
+		       GError **error_r)
 {
 	PlaylistInfo pm(name, 0);
 
 	char *line, *colon;
 	const char *value;
 
-	while ((line = read_text_line(fp, buffer)) != NULL &&
+	while ((line = file.ReadLine()) != NULL &&
 	       strcmp(line, "playlist_end") != 0) {
 		colon = strchr(line, ':');
 		if (colon == NULL || colon == line) {
diff --git a/src/PlaylistDatabase.hxx b/src/PlaylistDatabase.hxx
index da2bb6b97..f5f039267 100644
--- a/src/PlaylistDatabase.hxx
+++ b/src/PlaylistDatabase.hxx
@@ -28,12 +28,13 @@
 #define PLAYLIST_META_BEGIN "playlist_begin: "
 
 class PlaylistVector;
+class TextFile;
 
 void
 playlist_vector_save(FILE *fp, const PlaylistVector &pv);
 
 bool
-playlist_metadata_load(FILE *fp, PlaylistVector &pv, const char *name,
-		       GString *buffer, GError **error_r);
+playlist_metadata_load(TextFile &file, PlaylistVector &pv, const char *name,
+		       GError **error_r);
 
 #endif
diff --git a/src/PlaylistFile.cxx b/src/PlaylistFile.cxx
index e03cc5e54..67ffdff4f 100644
--- a/src/PlaylistFile.cxx
+++ b/src/PlaylistFile.cxx
@@ -236,16 +236,14 @@ LoadPlaylistFile(const char *utf8path, GError **error_r)
 	if (path_fs == NULL)
 		return contents;
 
-	FILE *file = fopen(path_fs, "r");
-	g_free(path_fs);
-	if (file == NULL) {
+	TextFile file(path_fs);
+	if (file.HasFailed()) {
 		playlist_errno(error_r);
 		return contents;
 	}
 
-	GString *buffer = g_string_sized_new(1024);
 	char *s;
-	while ((s = read_text_line(file, buffer)) != NULL) {
+	while ((s = file.ReadLine()) != NULL) {
 		if (*s == 0 || *s == PLAYLIST_COMMENT)
 			continue;
 
@@ -265,7 +263,6 @@ LoadPlaylistFile(const char *utf8path, GError **error_r)
 			break;
 	}
 
-	fclose(file);
 	return contents;
 }
 
diff --git a/src/PlaylistState.cxx b/src/PlaylistState.cxx
index c45c2a834..1bb18250f 100644
--- a/src/PlaylistState.cxx
+++ b/src/PlaylistState.cxx
@@ -104,18 +104,18 @@ playlist_state_save(FILE *fp, const struct playlist *playlist,
 }
 
 static void
-playlist_state_load(FILE *fp, GString *buffer, struct playlist *playlist)
+playlist_state_load(TextFile &file, struct playlist *playlist)
 {
-	const char *line = read_text_line(fp, buffer);
+	const char *line = file.ReadLine();
 	if (line == NULL) {
 		g_warning("No playlist in state file");
 		return;
 	}
 
 	while (!g_str_has_prefix(line, PLAYLIST_STATE_FILE_PLAYLIST_END)) {
-		queue_load_song(fp, buffer, line, &playlist->queue);
+		queue_load_song(file, line, &playlist->queue);
 
-		line = read_text_line(fp, buffer);
+		line = file.ReadLine();
 		if (line == NULL) {
 			g_warning("'" PLAYLIST_STATE_FILE_PLAYLIST_END
 				  "' not found in state file");
@@ -127,7 +127,7 @@ playlist_state_load(FILE *fp, GString *buffer, struct playlist *playlist)
 }
 
 bool
-playlist_state_restore(const char *line, FILE *fp, GString *buffer,
+playlist_state_restore(const char *line, TextFile &file,
 		       struct playlist *playlist, struct player_control *pc)
 {
 	int current = -1;
@@ -145,7 +145,7 @@ playlist_state_restore(const char *line, FILE *fp, GString *buffer,
 	else if (strcmp(line, PLAYLIST_STATE_FILE_STATE_PAUSE) == 0)
 		state = PLAYER_STATE_PAUSE;
 
-	while ((line = read_text_line(fp, buffer)) != NULL) {
+	while ((line = file.ReadLine()) != NULL) {
 		if (g_str_has_prefix(line, PLAYLIST_STATE_FILE_TIME)) {
 			seek_time =
 			    atoi(&(line[strlen(PLAYLIST_STATE_FILE_TIME)]));
@@ -189,7 +189,7 @@ playlist_state_restore(const char *line, FILE *fp, GString *buffer,
 					  (PLAYLIST_STATE_FILE_CURRENT)]));
 		} else if (g_str_has_prefix(line,
 					    PLAYLIST_STATE_FILE_PLAYLIST_BEGIN)) {
-			playlist_state_load(fp, buffer, playlist);
+			playlist_state_load(file, playlist);
 		}
 	}
 
diff --git a/src/PlaylistState.hxx b/src/PlaylistState.hxx
index d38fe9d12..572f6fb4a 100644
--- a/src/PlaylistState.hxx
+++ b/src/PlaylistState.hxx
@@ -30,13 +30,14 @@
 
 struct playlist;
 struct player_control;
+class TextFile;
 
 void
 playlist_state_save(FILE *fp, const struct playlist *playlist,
 		    struct player_control *pc);
 
 bool
-playlist_state_restore(const char *line, FILE *fp, GString *buffer,
+playlist_state_restore(const char *line, TextFile &file,
 		       struct playlist *playlist, struct player_control *pc);
 
 /**
diff --git a/src/QueueSave.cxx b/src/QueueSave.cxx
index a468013f4..6ba48f336 100644
--- a/src/QueueSave.cxx
+++ b/src/QueueSave.cxx
@@ -71,8 +71,7 @@ queue_save(FILE *fp, const struct queue *queue)
 }
 
 void
-queue_load_song(FILE *fp, GString *buffer, const char *line,
-		struct queue *queue)
+queue_load_song(TextFile &file, const char *line, queue *queue)
 {
 	if (queue_is_full(queue))
 		return;
@@ -81,7 +80,7 @@ queue_load_song(FILE *fp, GString *buffer, const char *line,
 	if (g_str_has_prefix(line, PRIO_LABEL)) {
 		priority = strtoul(line + sizeof(PRIO_LABEL) - 1, NULL, 10);
 
-		line = read_text_line(fp, buffer);
+		line = file.ReadLine();
 		if (line == NULL)
 			return;
 	}
@@ -95,7 +94,7 @@ queue_load_song(FILE *fp, GString *buffer, const char *line,
 			return;
 
 		GError *error = NULL;
-		song = song_load(fp, NULL, uri, buffer, &error);
+		song = song_load(file, NULL, uri, &error);
 		if (song == NULL) {
 			g_warning("%s", error->message);
 			g_error_free(error);
diff --git a/src/QueueSave.hxx b/src/QueueSave.hxx
index d6b09329b..dc4a764a9 100644
--- a/src/QueueSave.hxx
+++ b/src/QueueSave.hxx
@@ -29,6 +29,7 @@
 #include <stdio.h>
 
 struct queue;
+class TextFile;
 
 void
 queue_save(FILE *fp, const struct queue *queue);
@@ -37,7 +38,6 @@ queue_save(FILE *fp, const struct queue *queue);
  * Loads one song from the state file and appends it to the queue.
  */
 void
-queue_load_song(FILE *fp, GString *buffer, const char *line,
-		struct queue *queue);
+queue_load_song(TextFile &file, const char *line, queue *queue);
 
 #endif
diff --git a/src/SongSave.cxx b/src/SongSave.cxx
index cbc2536b1..a32b7fecc 100644
--- a/src/SongSave.cxx
+++ b/src/SongSave.cxx
@@ -63,8 +63,8 @@ song_save(FILE *fp, const struct song *song)
 }
 
 struct song *
-song_load(FILE *fp, Directory *parent, const char *uri,
-	  GString *buffer, GError **error_r)
+song_load(TextFile &file, Directory *parent, const char *uri,
+	  GError **error_r)
 {
 	struct song *song = parent != NULL
 		? song_file_new(uri, parent)
@@ -73,7 +73,7 @@ song_load(FILE *fp, Directory *parent, const char *uri,
 	enum tag_type type;
 	const char *value;
 
-	while ((line = read_text_line(fp, buffer)) != NULL &&
+	while ((line = file.ReadLine()) != NULL &&
 	       strcmp(line, SONG_END) != 0) {
 		colon = strchr(line, ':');
 		if (colon == NULL || colon == line) {
diff --git a/src/SongSave.hxx b/src/SongSave.hxx
index 81838a3ee..3214c545f 100644
--- a/src/SongSave.hxx
+++ b/src/SongSave.hxx
@@ -28,6 +28,7 @@
 
 struct song;
 struct Directory;
+class TextFile;
 
 void
 song_save(FILE *fp, const struct song *song);
@@ -41,7 +42,7 @@ song_save(FILE *fp, const struct song *song);
  * @return true on success, false on error
  */
 struct song *
-song_load(FILE *fp, Directory *parent, const char *uri,
-	  GString *buffer, GError **error_r);
+song_load(TextFile &file, Directory *parent, const char *uri,
+	  GError **error_r);
 
 #endif
diff --git a/src/StateFile.cxx b/src/StateFile.cxx
index 5af60ddba..cd344eb0e 100644
--- a/src/StateFile.cxx
+++ b/src/StateFile.cxx
@@ -78,39 +78,31 @@ state_file_write(struct player_control *pc)
 static void
 state_file_read(struct player_control *pc)
 {
-	FILE *fp;
 	bool success;
 
 	assert(state_file_path != NULL);
 
 	g_debug("Loading state file %s", state_file_path);
 
-	fp = fopen(state_file_path, "r");
-	if (G_UNLIKELY(!fp)) {
+	TextFile file(state_file_path);
+	if (file.HasFailed()) {
 		g_warning("failed to open %s: %s",
 			  state_file_path, g_strerror(errno));
 		return;
 	}
 
-	GString *buffer = g_string_sized_new(1024);
 	const char *line;
-	while ((line = read_text_line(fp, buffer)) != NULL) {
+	while ((line = file.ReadLine()) != NULL) {
 		success = read_sw_volume_state(line) ||
 			audio_output_state_read(line) ||
-			playlist_state_restore(line, fp, buffer,
-					       &g_playlist, pc);
+			playlist_state_restore(line, file, &g_playlist, pc);
 		if (!success)
 			g_warning("Unrecognized line in state file: %s", line);
 	}
 
-	fclose(fp);
-
 	prev_volume_version = sw_volume_state_get_hash();
 	prev_output_version = audio_output_state_get_version();
 	prev_playlist_version = playlist_state_get_hash(&g_playlist, pc);
-
-
-	g_string_free(buffer, true);
 }
 
 /**
diff --git a/src/TextFile.cxx b/src/TextFile.cxx
index ec9c5d442..4ad59ee4a 100644
--- a/src/TextFile.cxx
+++ b/src/TextFile.cxx
@@ -24,21 +24,14 @@
 #include <string.h>
 
 char *
-read_text_line(FILE *file, GString *buffer)
+TextFile::ReadLine()
 {
-	enum {
-		max_length = 512 * 1024,
-		step = 1024,
-	};
-
 	gsize length = 0, i;
 	char *p;
 
 	assert(file != NULL);
 	assert(buffer != NULL);
-
-	if (buffer->allocated_len < step)
-		g_string_set_size(buffer, step);
+	assert(buffer->allocated_len >= step);
 
 	while (buffer->len < max_length) {
 		p = fgets(buffer->str + length,
diff --git a/src/TextFile.hxx b/src/TextFile.hxx
index 5bd6dbd3c..25f6ea7a8 100644
--- a/src/TextFile.hxx
+++ b/src/TextFile.hxx
@@ -20,20 +20,47 @@
 #ifndef MPD_TEXT_FILE_HXX
 #define MPD_TEXT_FILE_HXX
 
+#include "gcc.h"
+
 #include <glib.h>
 
 #include <stdio.h>
 
-/**
- * Reads a line from the input file, and strips trailing space.  There
- * is a reasonable maximum line length, only to prevent denial of
- * service.
- *
- * @param file the source file, opened in text mode
- * @param buffer an allocator for the buffer
- * @return a pointer to the line, or NULL on end-of-file or error
- */
-char *
-read_text_line(FILE *file, GString *buffer);
+class TextFile {
+	static constexpr size_t max_length = 512 * 1024;
+	static constexpr size_t step = 1024;
+
+	FILE *const file;
+
+	GString *const buffer;
+
+public:
+	TextFile(const char *path_fs)
+		:file(fopen(path_fs, "r")), buffer(g_string_sized_new(step)) {}
+
+	TextFile(const TextFile &other) = delete;
+
+	~TextFile() {
+		if (file != nullptr)
+			fclose(file);
+
+		g_string_free(buffer, true);
+	}
+
+	bool HasFailed() const {
+		return gcc_unlikely(file == nullptr);
+	}
+
+	/**
+	 * Reads a line from the input file, and strips trailing
+	 * space.  There is a reasonable maximum line length, only to
+	 * prevent denial of service.
+	 *
+	 * @param file the source file, opened in text mode
+	 * @param buffer an allocator for the buffer
+	 * @return a pointer to the line, or NULL on end-of-file or error
+	 */
+	char *ReadLine();
+};
 
 #endif
diff --git a/src/db/SimpleDatabasePlugin.cxx b/src/db/SimpleDatabasePlugin.cxx
index 04e319498..84e4e7cee 100644
--- a/src/db/SimpleDatabasePlugin.cxx
+++ b/src/db/SimpleDatabasePlugin.cxx
@@ -26,6 +26,7 @@
 #include "DatabaseSave.hxx"
 #include "DatabaseLock.hxx"
 #include "db_error.h"
+#include "TextFile.hxx"
 
 extern "C" {
 #include "conf.h"
@@ -155,20 +156,16 @@ SimpleDatabase::Load(GError **error_r)
 	assert(!path.empty());
 	assert(root != NULL);
 
-	FILE *fp = fopen(path.c_str(), "r");
-	if (fp == NULL) {
+	TextFile file(path.c_str());
+	if (file.HasFailed()) {
 		g_set_error(error_r, simple_db_quark(), errno,
 			    "Failed to open database file \"%s\": %s",
 			    path.c_str(), g_strerror(errno));
 		return false;
 	}
 
-	if (!db_load_internal(fp, root, error_r)) {
-		fclose(fp);
+	if (!db_load_internal(file, root, error_r))
 		return false;
-	}
-
-	fclose(fp);
 
 	struct stat st;
 	if (stat(path.c_str(), &st) == 0)