diff --git a/Makefile.am b/Makefile.am
index 3491f2e6b..3ab492aae 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -508,6 +508,7 @@ FS_LIBS = libfs.a
 libfs_a_SOURCES = \
 	src/fs/io/Reader.hxx \
 	src/fs/io/FileReader.cxx src/fs/io/FileReader.hxx \
+	src/fs/io/BufferedReader.cxx src/fs/io/BufferedReader.hxx \
 	src/fs/io/TextFile.cxx src/fs/io/TextFile.hxx \
 	src/fs/io/OutputStream.hxx \
 	src/fs/io/StdoutOutputStream.hxx \
diff --git a/src/PlaylistFile.cxx b/src/PlaylistFile.cxx
index 2fb28aadf..f0aa2d2d7 100644
--- a/src/PlaylistFile.cxx
+++ b/src/PlaylistFile.cxx
@@ -116,6 +116,29 @@ spl_map_to_fs(const char *name_utf8, Error &error)
 	return path_fs;
 }
 
+gcc_pure
+static bool
+IsNotFoundError(const Error &error)
+{
+#ifdef WIN32
+	return error.IsDomain(win32_domain) &&
+		error.GetCode() == ERROR_FILE_NOT_FOUND;
+#else
+	return error.IsDomain(errno_domain) &&
+		error.GetCode() == ENOENT;
+#endif
+}
+
+static void
+TranslatePlaylistError(Error &error)
+{
+	if (IsNotFoundError(error)) {
+		error.Clear();
+		error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_LIST),
+			  "No such playlist");
+	}
+}
+
 /**
  * Create an #Error for the current errno.
  */
@@ -228,9 +251,9 @@ LoadPlaylistFile(const char *utf8path, Error &error)
 	if (path_fs.IsNull())
 		return contents;
 
-	TextFile file(path_fs);
+	TextFile file(path_fs, error);
 	if (file.HasFailed()) {
-		playlist_errno(error);
+		TranslatePlaylistError(error);
 		return contents;
 	}
 
diff --git a/src/StateFile.cxx b/src/StateFile.cxx
index 408b19426..e0f0cedb1 100644
--- a/src/StateFile.cxx
+++ b/src/StateFile.cxx
@@ -103,10 +103,10 @@ StateFile::Read()
 
 	FormatDebug(state_file_domain, "Loading state file %s", path_utf8.c_str());
 
-	TextFile file(path);
+	Error error;
+	TextFile file(path, error);
 	if (file.HasFailed()) {
-		FormatErrno(state_file_domain, "failed to open %s",
-			    path_utf8.c_str());
+		LogError(error);
 		return;
 	}
 
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
index ae34a523f..9e750996c 100644
--- a/src/db/plugins/simple/SimpleDatabasePlugin.cxx
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -168,12 +168,9 @@ SimpleDatabase::Load(Error &error)
 	assert(!path.IsNull());
 	assert(root != nullptr);
 
-	TextFile file(path);
-	if (file.HasFailed()) {
-		error.FormatErrno("Failed to open database file \"%s\"",
-				  path_utf8.c_str());
+	TextFile file(path, error);
+	if (file.HasFailed())
 		return false;
-	}
 
 	if (!db_load_internal(file, *root, error))
 		return false;
diff --git a/src/fs/StandardDirectory.cxx b/src/fs/StandardDirectory.cxx
index 7a8666501..7a836f906 100644
--- a/src/fs/StandardDirectory.cxx
+++ b/src/fs/StandardDirectory.cxx
@@ -40,6 +40,7 @@
 #endif
 
 #ifdef USE_XDG
+#include "util/Error.hxx"
 #include "util/StringUtil.hxx"
 #include "io/TextFile.hxx"
 #include <string.h>
@@ -204,7 +205,7 @@ static AllocatedPath GetUserDir(const char *name)
 	if (config_dir.IsNull())
 		return result;
 	auto dirs_file = AllocatedPath::Build(config_dir, "user-dirs.dirs");
-	TextFile input(dirs_file);
+	TextFile input(dirs_file, IgnoreError());
 	if (input.HasFailed())
 		return result;
 	char *line;
diff --git a/src/fs/io/BufferedReader.cxx b/src/fs/io/BufferedReader.cxx
new file mode 100644
index 000000000..ba2f17dcf
--- /dev/null
+++ b/src/fs/io/BufferedReader.cxx
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "BufferedReader.hxx"
+#include "Reader.hxx"
+#include "util/TextFile.hxx"
+
+bool
+BufferedReader::Fill(bool need_more)
+{
+	if (gcc_unlikely(last_error.IsDefined()))
+		return false;
+
+	if (eof)
+		return !need_more;
+
+	auto w = buffer.Write();
+	if (w.IsEmpty()) {
+		if (buffer.GetCapacity() >= MAX_SIZE)
+			return !need_more;
+
+		buffer.Grow(buffer.GetCapacity() * 2);
+		w = buffer.Write();
+		assert(!w.IsEmpty());
+	}
+
+	size_t nbytes = reader.Read(w.data, w.size, last_error);
+	if (nbytes == 0) {
+		if (gcc_unlikely(last_error.IsDefined()))
+			return false;
+
+		eof = true;
+		return !need_more;
+	}
+
+	buffer.Append(nbytes);
+	return true;
+}
+
+char *
+BufferedReader::ReadLine()
+{
+	do {
+		char *line = ReadBufferedLine(buffer);
+		if (line != nullptr)
+			return line;
+	} while (Fill(true));
+
+	if (last_error.IsDefined() || !eof || buffer.IsEmpty())
+		return nullptr;
+
+	auto w = buffer.Write();
+	if (w.IsEmpty()) {
+		buffer.Grow(buffer.GetCapacity() + 1);
+		w = buffer.Write();
+		assert(!w.IsEmpty());
+	}
+
+	/* terminate the last line */
+	w[0] = 0;
+
+	char *line = buffer.Read().data;
+	buffer.Clear();
+	return line;
+}
diff --git a/src/fs/io/BufferedReader.hxx b/src/fs/io/BufferedReader.hxx
new file mode 100644
index 000000000..61cc8df83
--- /dev/null
+++ b/src/fs/io/BufferedReader.hxx
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_BUFFERED_READER_HXX
+#define MPD_BUFFERED_READER_HXX
+
+#include "check.h"
+#include "Compiler.h"
+#include "util/DynamicFifoBuffer.hxx"
+#include "util/Error.hxx"
+
+#include <stddef.h>
+
+class Reader;
+class Error;
+
+class BufferedReader {
+	static constexpr size_t MAX_SIZE = 512 * 1024;
+
+	Reader &reader;
+
+	DynamicFifoBuffer<char> buffer;
+
+	Error last_error;
+
+	bool eof;
+
+public:
+	BufferedReader(Reader &_reader)
+		:reader(_reader), buffer(4096), eof(false) {}
+
+	gcc_pure
+	bool Check() const {
+		return !last_error.IsDefined();
+	}
+
+	bool Check(Error &error) const {
+		if (last_error.IsDefined()) {
+			error.Set(last_error);
+			return false;
+		} else
+			return true;
+	}
+
+	bool Fill(bool need_more);
+
+	gcc_pure
+	WritableBuffer<void> Read() const {
+		return buffer.Read().ToVoid();
+	}
+
+	void Consume(size_t n) {
+		buffer.Consume(n);
+	}
+
+	char *ReadLine();
+};
+
+#endif
diff --git a/src/fs/io/TextFile.cxx b/src/fs/io/TextFile.cxx
index b1a92b9cc..396d0f9cd 100644
--- a/src/fs/io/TextFile.cxx
+++ b/src/fs/io/TextFile.cxx
@@ -19,63 +19,30 @@
 
 #include "config.h"
 #include "TextFile.hxx"
-#include "util/Alloc.hxx"
+#include "FileReader.hxx"
+#include "BufferedReader.hxx"
 #include "fs/Path.hxx"
-#include "fs/FileSystem.hxx"
 
 #include <assert.h>
-#include <string.h>
-#include <stdlib.h>
 
-TextFile::TextFile(Path path_fs)
-	:file(FOpen(path_fs, FOpenMode::ReadText)),
-	 buffer((char *)xalloc(step)), capacity(step), length(0) {}
+TextFile::TextFile(Path path_fs, Error &error)
+	:file_reader(new FileReader(path_fs, error)),
+	 buffered_reader(file_reader->IsDefined()
+			 ? new BufferedReader(*file_reader)
+			 : nullptr)
+{
+}
 
 TextFile::~TextFile()
 {
-	free(buffer);
-
-	if (file != nullptr)
-		fclose(file);
+	delete buffered_reader;
+	delete file_reader;
 }
 
 char *
 TextFile::ReadLine()
 {
-	assert(file != nullptr);
+	assert(buffered_reader != nullptr);
 
-	while (true) {
-		if (length >= capacity) {
-			if (capacity >= max_length)
-				/* too large already - bail out */
-				return nullptr;
-
-			capacity <<= 1;
-			char *new_buffer = (char *)realloc(buffer, capacity);
-			if (new_buffer == nullptr)
-				/* out of memory - bail out */
-				return nullptr;
-		}
-
-		char *p = fgets(buffer + length, capacity - length, file);
-		if (p == nullptr) {
-			if (length == 0 || ferror(file))
-				return nullptr;
-			break;
-		}
-
-		length += strlen(buffer + length);
-		if (buffer[length - 1] == '\n')
-			break;
-	}
-
-	/* remove the newline characters */
-	if (buffer[length - 1] == '\n')
-		--length;
-	if (buffer[length - 1] == '\r')
-		--length;
-
-	buffer[length] = 0;
-	length = 0;
-	return buffer;
+	return buffered_reader->ReadLine();
 }
diff --git a/src/fs/io/TextFile.hxx b/src/fs/io/TextFile.hxx
index e3a712a88..33a1b8060 100644
--- a/src/fs/io/TextFile.hxx
+++ b/src/fs/io/TextFile.hxx
@@ -22,29 +22,26 @@
 
 #include "Compiler.h"
 
-#include <stdio.h>
 #include <stddef.h>
 
 class Path;
+class Error;
+class FileReader;
+class BufferedReader;
 
 class TextFile {
-	static constexpr size_t max_length = 512 * 1024;
-	static constexpr size_t step = 1024;
-
-	FILE *const file;
-
-	char *buffer;
-	size_t capacity, length;
+	FileReader *const file_reader;
+	BufferedReader *const buffered_reader;
 
 public:
-	TextFile(Path path_fs);
+	TextFile(Path path_fs, Error &error);
 
 	TextFile(const TextFile &other) = delete;
 
 	~TextFile();
 
 	bool HasFailed() const {
-		return gcc_unlikely(file == nullptr);
+		return gcc_unlikely(buffered_reader == nullptr);
 	}
 
 	/**
@@ -53,7 +50,6 @@ public:
 	 * 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 nullptr on end-of-file or error
 	 */
 	char *ReadLine();