diff --git a/src/DecoderThread.cxx b/src/DecoderThread.cxx
index 21653830b..9ca478fae 100644
--- a/src/DecoderThread.cxx
+++ b/src/DecoderThread.cxx
@@ -26,6 +26,7 @@
 #include "song.h"
 #include "mpd_error.h"
 #include "Mapper.hxx"
+#include "Path.hxx"
 #include "decoder_api.h"
 #include "tag.h"
 #include "input_stream.h"
@@ -431,7 +432,7 @@ decoder_run(struct decoder_control *dc)
 	assert(song != NULL);
 
 	if (song_is_file(song))
-		uri = map_song_fs(song);
+		uri = map_song_fs(song).Steal();
 	else
 		uri = song_get_uri(song);
 
diff --git a/src/ExcludeList.cxx b/src/ExcludeList.cxx
index 2c2dc952f..3f929b93c 100644
--- a/src/ExcludeList.cxx
+++ b/src/ExcludeList.cxx
@@ -32,14 +32,12 @@
 #include <errno.h>
 
 bool
-ExcludeList::LoadFile(const char *path_fs)
+ExcludeList::LoadFile(const Path &path_fs)
 {
-	assert(path_fs != NULL);
-
-	FILE *file = fopen(path_fs, "r");
+	FILE *file = fopen(path_fs.c_str(), "r");
 	if (file == NULL) {
 		if (errno != ENOENT) {
-			char *path_utf8 = fs_charset_to_utf8(path_fs);
+			char *path_utf8 = path_fs.ToUTF8();
 			g_debug("Failed to open %s: %s",
 				path_utf8, g_strerror(errno));
 			g_free(path_utf8);
diff --git a/src/ExcludeList.hxx b/src/ExcludeList.hxx
index 4d678b085..f3dc1f057 100644
--- a/src/ExcludeList.hxx
+++ b/src/ExcludeList.hxx
@@ -31,6 +31,8 @@
 
 #include <glib.h>
 
+class Path;
+
 class ExcludeList {
 	class Pattern {
 		GPatternSpec *pattern;
@@ -65,7 +67,7 @@ public:
 	/**
 	 * Loads and parses a .mpdignore file.
 	 */
-	bool LoadFile(const char *path_fs);
+	bool LoadFile(const Path &path_fs);
 
 	/**
 	 * Checks whether one of the patterns in the .mpdignore file matches
diff --git a/src/Main.cxx b/src/Main.cxx
index dd269354d..ef78c92b2 100644
--- a/src/Main.cxx
+++ b/src/Main.cxx
@@ -238,7 +238,8 @@ glue_state_file_init(GError **error_r)
 		return true;
 	}
 
-	state_file = new StateFile(path, *global_partition, *main_loop);
+	state_file = new StateFile(Path::FromUTF8(path),
+				   *global_partition, *main_loop);
 	g_free(path);
 	state_file->Read();
 	return true;
diff --git a/src/Mapper.cxx b/src/Mapper.cxx
index 1a5ebe111..09fa190f3 100644
--- a/src/Mapper.cxx
+++ b/src/Mapper.cxx
@@ -156,67 +156,54 @@ map_to_relative_path(const char *path_utf8)
 		: path_utf8;
 }
 
-char *
+Path
 map_uri_fs(const char *uri)
 {
-	char *uri_fs, *path_fs;
-
 	assert(uri != NULL);
 	assert(*uri != '/');
 
 	if (music_dir_fs == NULL)
-		return NULL;
+		return Path::Null();
 
-	uri_fs = utf8_to_fs_charset(uri);
-	if (uri_fs == NULL)
-		return NULL;
+	const Path uri_fs = Path::FromUTF8(uri);
+	if (uri_fs.IsNull())
+		return Path::Null();
 
-	path_fs = g_build_filename(music_dir_fs, uri_fs, NULL);
-	g_free(uri_fs);
-
-	return path_fs;
+	return Path::Build(music_dir_fs, uri_fs);
 }
 
-char *
+Path
 map_directory_fs(const Directory *directory)
 {
 	assert(music_dir_utf8 != NULL);
 	assert(music_dir_fs != NULL);
 
 	if (directory->IsRoot())
-		return g_strdup(music_dir_fs);
+		return Path::FromFS(music_dir_fs);
 
 	return map_uri_fs(directory->GetPath());
 }
 
-char *
+Path
 map_directory_child_fs(const Directory *directory, const char *name)
 {
 	assert(music_dir_utf8 != NULL);
 	assert(music_dir_fs != NULL);
 
-	char *name_fs, *parent_fs, *path;
-
 	/* check for invalid or unauthorized base names */
 	if (*name == 0 || strchr(name, '/') != NULL ||
 	    strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
-		return NULL;
+		return Path::Null();
 
-	parent_fs = map_directory_fs(directory);
-	if (parent_fs == NULL)
-		return NULL;
+	const Path parent_fs = map_directory_fs(directory);
+	if (parent_fs.IsNull())
+		return Path::Null();
 
-	name_fs = utf8_to_fs_charset(name);
-	if (name_fs == NULL) {
-		g_free(parent_fs);
-		return NULL;
-	}
+	const Path name_fs = Path::FromUTF8(name);
+	if (name_fs.IsNull())
+		return Path::Null();
 
-	path = g_build_filename(parent_fs, name_fs, NULL);
-	g_free(parent_fs);
-	g_free(name_fs);
-
-	return path;
+	return Path::Build(parent_fs, name_fs);
 }
 
 /**
@@ -224,19 +211,17 @@ map_directory_child_fs(const Directory *directory, const char *name)
  * not have a real parent directory, only the dummy object
  * #detached_root.
  */
-static char *
+static Path
 map_detached_song_fs(const char *uri_utf8)
 {
-	char *uri_fs = utf8_to_fs_charset(uri_utf8);
-	if (uri_fs == NULL)
-		return NULL;
+	Path uri_fs = Path::FromUTF8(uri_utf8);
+	if (uri_fs.IsNull())
+		return Path::Null();
 
-	char *path = g_build_filename(music_dir_fs, uri_fs, NULL);
-	g_free(uri_fs);
-	return path;
+	return Path::Build(music_dir_fs, uri_fs);
 }
 
-char *
+Path
 map_song_fs(const struct song *song)
 {
 	assert(song_is_file(song));
@@ -246,7 +231,7 @@ map_song_fs(const struct song *song)
 			? map_detached_song_fs(song->uri)
 			: map_directory_child_fs(song->parent, song->uri);
 	else
-		return utf8_to_fs_charset(song->uri);
+		return Path::FromUTF8(song->uri);
 }
 
 char *
@@ -273,22 +258,17 @@ map_spl_path(void)
 	return playlist_dir_fs;
 }
 
-char *
+Path
 map_spl_utf8_to_fs(const char *name)
 {
-	char *filename_utf8, *filename_fs, *path;
-
 	if (playlist_dir_fs == NULL)
-		return NULL;
+		return Path::Null();
 
-	filename_utf8 = g_strconcat(name, PLAYLIST_FILE_SUFFIX, NULL);
-	filename_fs = utf8_to_fs_charset(filename_utf8);
+	char *filename_utf8 = g_strconcat(name, PLAYLIST_FILE_SUFFIX, NULL);
+	const Path filename_fs = Path::FromUTF8(filename_utf8);
 	g_free(filename_utf8);
-	if (filename_fs == NULL)
-		return NULL;
+	if (filename_fs.IsNull())
+		return Path::Null();
 
-	path = g_build_filename(playlist_dir_fs, filename_fs, NULL);
-	g_free(filename_fs);
-
-	return path;
+	return Path::Build(playlist_dir_fs, filename_fs);
 }
diff --git a/src/Mapper.hxx b/src/Mapper.hxx
index 2ced38a10..01c947b5a 100644
--- a/src/Mapper.hxx
+++ b/src/Mapper.hxx
@@ -29,6 +29,7 @@
 
 #define PLAYLIST_FILE_SUFFIX ".m3u"
 
+class Path;
 struct Directory;
 struct song;
 
@@ -75,8 +76,8 @@ map_to_relative_path(const char *path_utf8);
  * is basically done by converting the URI to the file system charset
  * and prepending the music directory.
  */
-gcc_malloc
-char *
+gcc_pure
+Path
 map_uri_fs(const char *uri);
 
 /**
@@ -85,8 +86,8 @@ map_uri_fs(const char *uri);
  * @param directory the directory object
  * @return the path in file system encoding, or nullptr if mapping failed
  */
-gcc_malloc
-char *
+gcc_pure
+Path
 map_directory_fs(const Directory *directory);
 
 /**
@@ -97,8 +98,8 @@ map_directory_fs(const Directory *directory);
  * @param name the child's name in UTF-8
  * @return the path in file system encoding, or nullptr if mapping failed
  */
-gcc_malloc
-char *
+gcc_pure
+Path
 map_directory_child_fs(const Directory *directory, const char *name);
 
 /**
@@ -108,8 +109,8 @@ map_directory_child_fs(const Directory *directory, const char *name);
  * @param song the song object
  * @return the path in file system encoding, or nullptr if mapping failed
  */
-gcc_malloc
-char *
+gcc_pure
+Path
 map_song_fs(const struct song *song);
 
 /**
@@ -138,7 +139,7 @@ map_spl_path(void);
  * @return the path in file system encoding, or nullptr if mapping failed
  */
 gcc_pure
-char *
+Path
 map_spl_utf8_to_fs(const char *name);
 
 #endif
diff --git a/src/Path.hxx b/src/Path.hxx
index db3f95961..5c76e4b87 100644
--- a/src/Path.hxx
+++ b/src/Path.hxx
@@ -21,7 +21,14 @@
 #define MPD_PATH_HXX
 
 #include "check.h"
+#include "gcc.h"
 
+#include <glib.h>
+
+#include <algorithm>
+
+#include <assert.h>
+#include <string.h>
 #include <limits.h>
 
 #if !defined(MPD_PATH_MAX)
@@ -54,4 +61,204 @@ utf8_to_fs_charset(const char *path_utf8);
 
 const char *path_get_fs_charset();
 
+/**
+ * A path name in the native file system character set.
+ */
+class Path {
+public:
+	typedef char value_type;
+	typedef value_type *pointer;
+	typedef const value_type *const_pointer;
+
+private:
+	pointer value;
+
+	struct Donate {};
+
+	/**
+	 * Donate the allocated pointer to a new #Path object.
+	 */
+	constexpr Path(Donate, pointer _value):value(_value) {}
+
+	/**
+	 * Release memory allocated by the value, but do not clear the
+	 * value pointer.
+	 */
+	void Free() {
+		/* free() can be optimized by gcc, while g_free() can
+		   not: when the compiler knows that the value is
+		   nullptr, it will not emit a free() call in the
+		   inlined destructor; however on Windows, we need to
+		   call g_free(), because the value has been allocated
+		   by GLib, and on Windows, this matters */
+#ifdef WIN32
+		g_free(value);
+#else
+		free(value);
+#endif
+	}
+
+public:
+	/**
+	 * Copy a #Path object.
+	 */
+	Path(const Path &other)
+		:value(g_strdup(other.value)) {}
+
+	/**
+	 * Move a #Path object.
+	 */
+	Path(Path &&other):value(other.value) {
+		other.value = nullptr;
+	}
+
+	~Path() {
+		Free();
+	}
+
+	/**
+	 * Return a "nulled" instance.  Its IsNull() method will
+	 * return true.  Such an object must not be used.
+	 *
+	 * @see IsNull()
+	 */
+	gcc_const
+	static Path Null() {
+		return Path(Donate(), nullptr);
+	}
+
+	/**
+	 * Join two path components with the path separator.
+	 */
+	gcc_pure gcc_nonnull_all
+	static Path Build(const_pointer a, const_pointer b) {
+		return Path(Donate(), g_build_filename(a, b, nullptr));
+	}
+
+	gcc_pure gcc_nonnull_all
+	static Path Build(const_pointer a, const Path &b) {
+		return Build(a, b.c_str());
+	}
+
+	gcc_pure gcc_nonnull_all
+	static Path Build(const Path &a, const_pointer b) {
+		return Build(a.c_str(), b);
+	}
+
+	gcc_pure
+	static Path Build(const Path &a, const Path &b) {
+		return Build(a.c_str(), b.c_str());
+	}
+
+	/**
+	 * Convert a C string that is already in the filesystem
+	 * character set to a #Path instance.
+	 */
+	gcc_pure
+	static Path FromFS(const_pointer fs) {
+		return Path(Donate(), g_strdup(fs));
+	}
+
+	/**
+	 * Convert a UTF-8 C string to a #Path instance.
+	 *
+	 * TODO: return a "nulled" instance on error and add checks to
+	 * all callers
+	 */
+	gcc_pure
+	static Path FromUTF8(const char *utf8) {
+		return Path(Donate(), utf8_to_fs_charset(utf8));
+	}
+
+	/**
+	 * Copy a #Path object.
+	 */
+	Path &operator=(const Path &other) {
+		if (this != &other) {
+			Free();
+			value = g_strdup(other.value);
+		}
+
+		return *this;
+	}
+
+	/**
+	 * Move a #Path object.
+	 */
+	Path &operator=(Path &&other) {
+		std::swap(value, other.value);
+		return *this;
+	}
+
+	/**
+	 * Steal the allocated value.  This object has an undefined
+	 * value, and the caller is response for freeing this method's
+	 * return value.
+	 */
+	pointer Steal() {
+		pointer result = value;
+		value = nullptr;
+		return result;
+	}
+
+	/**
+	 * Check if this is a "nulled" instance.  A "nulled" instance
+	 * must not be used.
+	 */
+	bool IsNull() const {
+		return value == nullptr;
+	}
+
+	/**
+	 * Clear this object's value, make it "nulled".
+	 *
+	 * @see IsNull()
+	 */
+	void SetNull() {
+		Free();
+		value = nullptr;
+	}
+
+	gcc_pure
+	bool empty() const {
+		assert(value != nullptr);
+
+		return *value == 0;
+	}
+
+	/**
+	 * @return the length of this string in number of "value_type"
+	 * elements (which may not be the number of characters).
+	 */
+	gcc_pure
+	size_t length() const {
+		assert(value != nullptr);
+
+		return strlen(value);
+	}
+
+	/**
+	 * Returns the value as a const C string.  The returned
+	 * pointer is invalidated whenever the value of life of this
+	 * instance ends.
+	 */
+	gcc_pure
+	const_pointer c_str() const {
+		assert(value != nullptr);
+
+		return value;
+	}
+
+	/**
+	 * Convert the path to UTF-8.  The caller is responsible for
+	 * freeing the return value with g_free().  Returns nullptr on
+	 * error.
+	 */
+	char *ToUTF8() const {
+		return value != nullptr
+			? fs_charset_to_utf8(value)
+			: nullptr;
+	}
+};
+
 #endif
diff --git a/src/PlaylistFile.cxx b/src/PlaylistFile.cxx
index 486c93994..ea81540cf 100644
--- a/src/PlaylistFile.cxx
+++ b/src/PlaylistFile.cxx
@@ -106,15 +106,15 @@ spl_check_name(const char *name_utf8, GError **error_r)
 	return true;
 }
 
-static char *
+static Path
 spl_map_to_fs(const char *name_utf8, GError **error_r)
 {
 	if (spl_map(error_r) == NULL ||
 	    !spl_check_name(name_utf8, error_r))
-		return NULL;
+		return Path::Null();
 
-	char *path_fs = map_spl_utf8_to_fs(name_utf8);
-	if (path_fs == NULL)
+	Path path_fs = map_spl_utf8_to_fs(name_utf8);
+	if (path_fs.IsNull())
 		g_set_error_literal(error_r, playlist_quark(),
 				    PLAYLIST_RESULT_BAD_NAME,
 				    "Bad playlist name");
@@ -209,12 +209,11 @@ SavePlaylistFile(const PlaylistFileContents &contents, const char *utf8path,
 	if (spl_map(error_r) == NULL)
 		return false;
 
-	char *path_fs = spl_map_to_fs(utf8path, error_r);
-	if (path_fs == NULL)
+	const Path path_fs = spl_map_to_fs(utf8path, error_r);
+	if (path_fs.IsNull())
 		return false;
 
-	FILE *file = fopen(path_fs, "w");
-	g_free(path_fs);
+	FILE *file = fopen(path_fs.c_str(), "w");
 	if (file == NULL) {
 		playlist_errno(error_r);
 		return false;
@@ -235,8 +234,8 @@ LoadPlaylistFile(const char *utf8path, GError **error_r)
 	if (spl_map(error_r) == NULL)
 		return contents;
 
-	char *path_fs = spl_map_to_fs(utf8path, error_r);
-	if (path_fs == NULL)
+	const Path path_fs = spl_map_to_fs(utf8path, error_r);
+	if (path_fs.IsNull())
 		return contents;
 
 	TextFile file(path_fs);
@@ -308,17 +307,14 @@ spl_move_index(const char *utf8path, unsigned src, unsigned dest,
 bool
 spl_clear(const char *utf8path, GError **error_r)
 {
-	FILE *file;
-
 	if (spl_map(error_r) == NULL)
 		return false;
 
-	char *path_fs = spl_map_to_fs(utf8path, error_r);
-	if (path_fs == NULL)
+	const Path path_fs = spl_map_to_fs(utf8path, error_r);
+	if (path_fs.IsNull())
 		return false;
 
-	file = fopen(path_fs, "w");
-	g_free(path_fs);
+	FILE *file = fopen(path_fs.c_str(), "w");
 	if (file == NULL) {
 		playlist_errno(error_r);
 		return false;
@@ -333,12 +329,11 @@ spl_clear(const char *utf8path, GError **error_r)
 bool
 spl_delete(const char *name_utf8, GError **error_r)
 {
-	char *path_fs = spl_map_to_fs(name_utf8, error_r);
-	if (path_fs == NULL)
+	const Path path_fs = spl_map_to_fs(name_utf8, error_r);
+	if (path_fs.IsNull())
 		return false;
 
-	int ret = unlink(path_fs);
-	g_free(path_fs);
+	int ret = unlink(path_fs.c_str());
 	if (ret < 0) {
 		playlist_errno(error_r);
 		return false;
@@ -376,17 +371,14 @@ spl_remove_index(const char *utf8path, unsigned pos, GError **error_r)
 bool
 spl_append_song(const char *utf8path, struct song *song, GError **error_r)
 {
-	FILE *file;
-
 	if (spl_map(error_r) == NULL)
 		return false;
 
-	char *path_fs = spl_map_to_fs(utf8path, error_r);
-	if (path_fs == NULL)
+	const Path path_fs = spl_map_to_fs(utf8path, error_r);
+	if (path_fs.IsNull())
 		return false;
 
-	file = fopen(path_fs, "a");
-	g_free(path_fs);
+	FILE *file = fopen(path_fs.c_str(), "a");
 	if (file == NULL) {
 		playlist_errno(error_r);
 		return false;
@@ -439,24 +431,24 @@ spl_append_uri(const char *url, const char *utf8file, GError **error_r)
 }
 
 static bool
-spl_rename_internal(const char *from_path_fs, const char *to_path_fs,
+spl_rename_internal(const Path &from_path_fs, const Path &to_path_fs,
 		    GError **error_r)
 {
-	if (!g_file_test(from_path_fs, G_FILE_TEST_IS_REGULAR)) {
+	if (!g_file_test(from_path_fs.c_str(), G_FILE_TEST_IS_REGULAR)) {
 		g_set_error_literal(error_r, playlist_quark(),
 				    PLAYLIST_RESULT_NO_SUCH_LIST,
 				    "No such playlist");
 		return false;
 	}
 
-	if (g_file_test(to_path_fs, G_FILE_TEST_EXISTS)) {
+	if (g_file_test(to_path_fs.c_str(), G_FILE_TEST_EXISTS)) {
 		g_set_error_literal(error_r, playlist_quark(),
 				    PLAYLIST_RESULT_LIST_EXISTS,
 				    "Playlist exists already");
 		return false;
 	}
 
-	if (rename(from_path_fs, to_path_fs) < 0) {
+	if (rename(from_path_fs.c_str(), to_path_fs.c_str()) < 0) {
 		playlist_errno(error_r);
 		return false;
 	}
@@ -471,20 +463,13 @@ spl_rename(const char *utf8from, const char *utf8to, GError **error_r)
 	if (spl_map(error_r) == NULL)
 		return false;
 
-	char *from_path_fs = spl_map_to_fs(utf8from, error_r);
-	if (from_path_fs == NULL)
+	Path from_path_fs = spl_map_to_fs(utf8from, error_r);
+	if (from_path_fs.IsNull())
 		return false;
 
-	char *to_path_fs = spl_map_to_fs(utf8to, error_r);
-	if (to_path_fs == NULL) {
-		g_free(from_path_fs);
+	Path to_path_fs = spl_map_to_fs(utf8to, error_r);
+	if (to_path_fs.IsNull())
 		return false;
-	}
 
-	bool success = spl_rename_internal(from_path_fs, to_path_fs, error_r);
-
-	g_free(from_path_fs);
-	g_free(to_path_fs);
-
-	return success;
+	return spl_rename_internal(from_path_fs, to_path_fs, error_r);
 }
diff --git a/src/PlaylistMapper.cxx b/src/PlaylistMapper.cxx
index 01b8f7dd8..e6b8ee439 100644
--- a/src/PlaylistMapper.cxx
+++ b/src/PlaylistMapper.cxx
@@ -21,6 +21,7 @@
 #include "PlaylistMapper.hxx"
 #include "PlaylistFile.hxx"
 #include "Mapper.hxx"
+#include "Path.hxx"
 
 extern "C" {
 #include "playlist_list.h"
@@ -75,19 +76,13 @@ static struct playlist_provider *
 playlist_open_in_music_dir(const char *uri, GMutex *mutex, GCond *cond,
 			   struct input_stream **is_r)
 {
-	char *path_fs;
-
 	assert(uri_safe_local(uri));
 
-	path_fs = map_uri_fs(uri);
-	if (path_fs == NULL)
+	Path path = map_uri_fs(uri);
+	if (path.IsNull())
 		return NULL;
 
-	struct playlist_provider *playlist =
-		playlist_open_path(path_fs, mutex, cond, is_r);
-	g_free(path_fs);
-
-	return playlist;
+	return playlist_open_path(path.c_str(), mutex, cond, is_r);
 }
 
 struct playlist_provider *
diff --git a/src/PlaylistSave.cxx b/src/PlaylistSave.cxx
index 5f47d73c0..89feebbde 100644
--- a/src/PlaylistSave.cxx
+++ b/src/PlaylistSave.cxx
@@ -38,62 +38,47 @@ void
 playlist_print_song(FILE *file, const struct song *song)
 {
 	if (playlist_saveAbsolutePaths && song_in_database(song)) {
-		char *path = map_song_fs(song);
-		if (path != NULL) {
-			fprintf(file, "%s\n", path);
-			g_free(path);
-		}
+		const Path path = map_song_fs(song);
+		if (!path.IsNull())
+			fprintf(file, "%s\n", path.c_str());
 	} else {
-		char *uri = song_get_uri(song), *uri_fs;
-
-		uri_fs = utf8_to_fs_charset(uri);
+		char *uri = song_get_uri(song);
+		const Path uri_fs = Path::FromUTF8(uri);
 		g_free(uri);
 
-		fprintf(file, "%s\n", uri_fs);
-		g_free(uri_fs);
+		fprintf(file, "%s\n", uri_fs.c_str());
 	}
 }
 
 void
 playlist_print_uri(FILE *file, const char *uri)
 {
-	char *s;
+	Path path = playlist_saveAbsolutePaths && !uri_has_scheme(uri) &&
+		!g_path_is_absolute(uri)
+		? map_uri_fs(uri)
+		: Path::FromUTF8(uri);
 
-	if (playlist_saveAbsolutePaths && !uri_has_scheme(uri) &&
-	    !g_path_is_absolute(uri))
-		s = map_uri_fs(uri);
-	else
-		s = utf8_to_fs_charset(uri);
-
-	if (s != NULL) {
-		fprintf(file, "%s\n", s);
-		g_free(s);
-	}
+	if (!path.IsNull())
+		fprintf(file, "%s\n", path.c_str());
 }
 
 enum playlist_result
 spl_save_queue(const char *name_utf8, const struct queue *queue)
 {
-	char *path_fs;
-	FILE *file;
-
 	if (map_spl_path() == NULL)
 		return PLAYLIST_RESULT_DISABLED;
 
 	if (!spl_valid_name(name_utf8))
 		return PLAYLIST_RESULT_BAD_NAME;
 
-	path_fs = map_spl_utf8_to_fs(name_utf8);
-	if (path_fs == NULL)
+	const Path path_fs = map_spl_utf8_to_fs(name_utf8);
+	if (path_fs.IsNull())
 		return PLAYLIST_RESULT_BAD_NAME;
 
-	if (g_file_test(path_fs, G_FILE_TEST_EXISTS)) {
-		g_free(path_fs);
+	if (g_file_test(path_fs.c_str(), G_FILE_TEST_EXISTS))
 		return PLAYLIST_RESULT_LIST_EXISTS;
-	}
 
-	file = fopen(path_fs, "w");
-	g_free(path_fs);
+	FILE *file = fopen(path_fs.c_str(), "w");
 
 	if (file == NULL)
 		return PLAYLIST_RESULT_ERRNO;
diff --git a/src/PlaylistSong.cxx b/src/PlaylistSong.cxx
index ec27656f2..0d60c1413 100644
--- a/src/PlaylistSong.cxx
+++ b/src/PlaylistSong.cxx
@@ -65,8 +65,8 @@ apply_song_metadata(struct song *dest, const struct song *src)
 		return dest;
 
 	if (song_in_database(dest)) {
-		char *path_fs = map_song_fs(dest);
-		if (path_fs == NULL)
+		char *path_fs = map_song_fs(dest).Steal();
+		if (path_fs == nullptr)
 			return dest;
 
 		char *path_utf8 = fs_charset_to_utf8(path_fs);
diff --git a/src/SongUpdate.cxx b/src/SongUpdate.cxx
index f84a37cea..ed7293f14 100644
--- a/src/SongUpdate.cxx
+++ b/src/SongUpdate.cxx
@@ -26,6 +26,7 @@ extern "C" {
 
 #include "Directory.hxx"
 #include "Mapper.hxx"
+#include "Path.hxx"
 #include "tag.h"
 #include "input_stream.h"
 
@@ -85,7 +86,6 @@ bool
 song_file_update(struct song *song)
 {
 	const char *suffix;
-	char *path_fs;
 	const struct decoder_plugin *plugin;
 	struct stat st;
 	struct input_stream *is = NULL;
@@ -102,8 +102,8 @@ song_file_update(struct song *song)
 	if (plugin == NULL)
 		return false;
 
-	path_fs = map_song_fs(song);
-	if (path_fs == NULL)
+	const Path path_fs = map_song_fs(song);
+	if (path_fs.IsNull())
 		return false;
 
 	if (song->tag != NULL) {
@@ -111,8 +111,7 @@ song_file_update(struct song *song)
 		song->tag = NULL;
 	}
 
-	if (stat(path_fs, &st) < 0 || !S_ISREG(st.st_mode)) {
-		g_free(path_fs);
+	if (stat(path_fs.c_str(), &st) < 0 || !S_ISREG(st.st_mode)) {
 		return false;
 	}
 
@@ -129,7 +128,7 @@ song_file_update(struct song *song)
 	do {
 		/* load file tag */
 		song->tag = tag_new();
-		if (decoder_plugin_scan_file(plugin, path_fs,
+		if (decoder_plugin_scan_file(plugin, path_fs.c_str(),
 					     &full_tag_handler, song->tag))
 			break;
 
@@ -143,7 +142,8 @@ song_file_update(struct song *song)
 			if (is == NULL) {
 				mutex = g_mutex_new();
 				cond = g_cond_new();
-				is = input_stream_open(path_fs, mutex, cond,
+				is = input_stream_open(path_fs.c_str(),
+						       mutex, cond,
 						       NULL);
 			}
 
@@ -174,9 +174,9 @@ song_file_update(struct song *song)
 	}
 
 	if (song->tag != NULL && tag_is_empty(song->tag))
-		tag_scan_fallback(path_fs, &full_tag_handler, song->tag);
+		tag_scan_fallback(path_fs.c_str(), &full_tag_handler,
+				  song->tag);
 
-	g_free(path_fs);
 	return song->tag != NULL;
 }
 
diff --git a/src/StateFile.cxx b/src/StateFile.cxx
index b78056520..6f93137a7 100644
--- a/src/StateFile.cxx
+++ b/src/StateFile.cxx
@@ -34,8 +34,8 @@
 #undef G_LOG_DOMAIN
 #define G_LOG_DOMAIN "state_file"
 
-StateFile::StateFile(const char *_path, Partition &_partition, EventLoop &_loop)
-	:TimeoutMonitor(_loop), path(_path), partition(_partition),
+StateFile::StateFile(Path &&_path, Partition &_partition, EventLoop &_loop)
+	:TimeoutMonitor(_loop), path(std::move(_path)), partition(_partition),
 	 prev_volume_version(0), prev_output_version(0),
 	 prev_playlist_version(0)
 {
@@ -73,7 +73,7 @@ StateFile::Read()
 
 	g_debug("Loading state file %s", path.c_str());
 
-	TextFile file(path.c_str());
+	TextFile file(path);
 	if (file.HasFailed()) {
 		g_warning("failed to open %s: %s",
 			  path.c_str(), g_strerror(errno));
diff --git a/src/StateFile.hxx b/src/StateFile.hxx
index 39c3fcdf6..72d71e105 100644
--- a/src/StateFile.hxx
+++ b/src/StateFile.hxx
@@ -21,6 +21,7 @@
 #define MPD_STATE_FILE_HXX
 
 #include "event/TimeoutMonitor.hxx"
+#include "Path.hxx"
 #include "gcc.h"
 
 #include <string>
@@ -28,7 +29,7 @@
 struct Partition;
 
 class StateFile final : private TimeoutMonitor {
-	std::string path;
+	Path path;
 
 	Partition &partition;
 
@@ -40,7 +41,7 @@ class StateFile final : private TimeoutMonitor {
 		prev_playlist_version;
 
 public:
-	StateFile(const char *path, Partition &partition, EventLoop &loop);
+	StateFile(Path &&path, Partition &partition, EventLoop &loop);
 
 	void Read();
 	void Write();
diff --git a/src/TextFile.hxx b/src/TextFile.hxx
index 25f6ea7a8..b24889f61 100644
--- a/src/TextFile.hxx
+++ b/src/TextFile.hxx
@@ -21,6 +21,7 @@
 #define MPD_TEXT_FILE_HXX
 
 #include "gcc.h"
+#include "Path.hxx"
 
 #include <glib.h>
 
@@ -35,8 +36,9 @@ class TextFile {
 	GString *const buffer;
 
 public:
-	TextFile(const char *path_fs)
-		:file(fopen(path_fs, "r")), buffer(g_string_sized_new(step)) {}
+	TextFile(const Path &path_fs)
+		:file(fopen(path_fs.c_str(), "r")),
+		 buffer(g_string_sized_new(step)) {}
 
 	TextFile(const TextFile &other) = delete;
 
diff --git a/src/UpdateArchive.cxx b/src/UpdateArchive.cxx
index 72f7aaf19..c45e1b733 100644
--- a/src/UpdateArchive.cxx
+++ b/src/UpdateArchive.cxx
@@ -24,6 +24,7 @@
 #include "Directory.hxx"
 #include "song.h"
 #include "Mapper.hxx"
+#include "Path.hxx"
 
 extern "C" {
 #include "archive_list.h"
@@ -96,20 +97,19 @@ update_archive_file2(Directory *parent, const char *name,
 		   changed since - don't consider updating it */
 		return;
 
-	char *path_fs = map_directory_child_fs(parent, name);
+	const Path path_fs = map_directory_child_fs(parent, name);
 
 	/* open archive */
 	GError *error = NULL;
-	struct archive_file *file = archive_file_open(plugin, path_fs, &error);
+	struct archive_file *file = archive_file_open(plugin, path_fs.c_str(),
+						      &error);
 	if (file == NULL) {
-		g_free(path_fs);
 		g_warning("%s", error->message);
 		g_error_free(error);
 		return;
 	}
 
-	g_debug("archive %s opened", path_fs);
-	g_free(path_fs);
+	g_debug("archive %s opened", path_fs.c_str());
 
 	if (directory == NULL) {
 		g_debug("creating archive directory: %s", name);
diff --git a/src/UpdateContainer.cxx b/src/UpdateContainer.cxx
index daa7f1ec4..d59fa96c0 100644
--- a/src/UpdateContainer.cxx
+++ b/src/UpdateContainer.cxx
@@ -26,6 +26,7 @@
 #include "song.h"
 #include "decoder_plugin.h"
 #include "Mapper.hxx"
+#include "Path.hxx"
 
 extern "C" {
 #include "tag_handler.h"
@@ -84,22 +85,22 @@ update_container_file(Directory *directory,
 	contdir->device = DEVICE_CONTAINER;
 	db_unlock();
 
-	char *const pathname = map_directory_child_fs(directory, name);
+	const Path pathname = map_directory_child_fs(directory, name);
 
 	char *vtrack;
 	unsigned int tnum = 0;
-	while ((vtrack = plugin->container_scan(pathname, ++tnum)) != NULL) {
+	while ((vtrack = plugin->container_scan(pathname.c_str(), ++tnum)) != NULL) {
 		struct song *song = song_file_new(vtrack, contdir);
 
 		// shouldn't be necessary but it's there..
 		song->mtime = st->st_mtime;
 
-		char *child_path_fs = map_directory_child_fs(contdir, vtrack);
+		const Path child_path_fs =
+			map_directory_child_fs(contdir, vtrack);
 
 		song->tag = tag_new();
-		decoder_plugin_scan_file(plugin, child_path_fs,
+		decoder_plugin_scan_file(plugin, child_path_fs.c_str(),
 					 &add_tag_handler, song->tag);
-		g_free(child_path_fs);
 
 		db_lock();
 		contdir->AddSong(song);
@@ -111,8 +112,6 @@ update_container_file(Directory *directory,
 		g_free(vtrack);
 	}
 
-	g_free(pathname);
-
 	if (tnum == 1) {
 		db_lock();
 		delete_directory(contdir);
diff --git a/src/UpdateIO.cxx b/src/UpdateIO.cxx
index 2aee56514..cbf05b6fc 100644
--- a/src/UpdateIO.cxx
+++ b/src/UpdateIO.cxx
@@ -21,6 +21,7 @@
 #include "UpdateIO.hxx"
 #include "Directory.hxx"
 #include "Mapper.hxx"
+#include "Path.hxx"
 #include "glib_compat.h"
 
 #include <glib.h>
@@ -31,15 +32,15 @@
 int
 stat_directory(const Directory *directory, struct stat *st)
 {
-	char *path_fs = map_directory_fs(directory);
-	if (path_fs == NULL)
+	const Path path_fs = map_directory_fs(directory);
+	if (path_fs.IsNull())
 		return -1;
 
-	int ret = stat(path_fs, st);
+	int ret = stat(path_fs.c_str(), st);
 	if (ret < 0)
-		g_warning("Failed to stat %s: %s", path_fs, g_strerror(errno));
+		g_warning("Failed to stat %s: %s",
+			  path_fs.c_str(), g_strerror(errno));
 
-	g_free(path_fs);
 	return ret;
 }
 
@@ -47,23 +48,23 @@ int
 stat_directory_child(const Directory *parent, const char *name,
 		     struct stat *st)
 {
-	char *path_fs = map_directory_child_fs(parent, name);
-	if (path_fs == NULL)
+	const Path path_fs = map_directory_child_fs(parent, name);
+	if (path_fs.IsNull())
 		return -1;
 
-	int ret = stat(path_fs, st);
+	int ret = stat(path_fs.c_str(), st);
 	if (ret < 0)
-		g_warning("Failed to stat %s: %s", path_fs, g_strerror(errno));
+		g_warning("Failed to stat %s: %s",
+			  path_fs.c_str(), g_strerror(errno));
 
-	g_free(path_fs);
 	return ret;
 }
 
 bool
 directory_exists(const Directory *directory)
 {
-	char *path_fs = map_directory_fs(directory);
-	if (path_fs == NULL)
+	const Path path_fs = map_directory_fs(directory);
+	if (path_fs.IsNull())
 		/* invalid path: cannot exist */
 		return false;
 
@@ -72,25 +73,19 @@ directory_exists(const Directory *directory)
 		? G_FILE_TEST_IS_REGULAR
 		: G_FILE_TEST_IS_DIR;
 
-	bool exists = g_file_test(path_fs, test);
-	g_free(path_fs);
-
-	return exists;
+	return g_file_test(path_fs.c_str(), test);
 }
 
 bool
 directory_child_is_regular(const Directory *directory,
 			   const char *name_utf8)
 {
-	char *path_fs = map_directory_child_fs(directory, name_utf8);
-	if (path_fs == NULL)
+	const Path path_fs = map_directory_child_fs(directory, name_utf8);
+	if (path_fs.IsNull())
 		return false;
 
 	struct stat st;
-	bool is_regular = stat(path_fs, &st) == 0 && S_ISREG(st.st_mode);
-	g_free(path_fs);
-
-	return is_regular;
+	return stat(path_fs.c_str(), &st) == 0 && S_ISREG(st.st_mode);
 }
 
 bool
@@ -104,14 +99,12 @@ directory_child_access(const Directory *directory,
 	(void)mode;
 	return true;
 #else
-	char *path = map_directory_child_fs(directory, name);
-	if (path == NULL)
+	const Path path = map_directory_child_fs(directory, name);
+	if (path.IsNull())
 		/* something went wrong, but that isn't a permission
 		   problem */
 		return true;
 
-	bool success = access(path, mode) == 0 || errno != EACCES;
-	g_free(path);
-	return success;
+	return access(path.c_str(), mode) == 0 || errno != EACCES;
 #endif
 }
diff --git a/src/UpdateWalk.cxx b/src/UpdateWalk.cxx
index 1716862b5..d0e9281a7 100644
--- a/src/UpdateWalk.cxx
+++ b/src/UpdateWalk.cxx
@@ -102,27 +102,23 @@ remove_excluded_from_directory(Directory *directory,
 
 	Directory *child, *n;
 	directory_for_each_child_safe(child, n, directory) {
-		char *name_fs = utf8_to_fs_charset(child->GetName());
+		const Path name_fs = Path::FromUTF8(child->GetName());
 
-		if (exclude_list.Check(name_fs)) {
+		if (exclude_list.Check(name_fs.c_str())) {
 			delete_directory(child);
 			modified = true;
 		}
-
-		g_free(name_fs);
 	}
 
 	struct song *song, *ns;
 	directory_for_each_song_safe(song, ns, directory) {
 		assert(song->parent == directory);
 
-		char *name_fs = utf8_to_fs_charset(song->uri);
-		if (exclude_list.Check(name_fs)) {
+		const Path name_fs = Path::FromUTF8(song->uri);
+		if (exclude_list.Check(name_fs.c_str())) {
 			delete_song(directory, song);
 			modified = true;
 		}
-
-		g_free(name_fs);
 	}
 
 	db_unlock();
@@ -145,18 +141,16 @@ purge_deleted_from_directory(Directory *directory)
 
 	struct song *song, *ns;
 	directory_for_each_song_safe(song, ns, directory) {
-		char *path;
 		struct stat st;
-		if ((path = map_song_fs(song)) == NULL ||
-		    stat(path, &st) < 0 || !S_ISREG(st.st_mode)) {
+		const Path path = map_song_fs(song);
+		if (path.IsNull() ||
+		    stat(path.c_str(), &st) < 0 || !S_ISREG(st.st_mode)) {
 			db_lock();
 			delete_song(directory, song);
 			db_unlock();
 
 			modified = true;
 		}
-
-		g_free(path);
 	}
 
 	for (auto i = directory->playlists.begin(),
@@ -283,13 +277,12 @@ static bool
 skip_symlink(const Directory *directory, const char *utf8_name)
 {
 #ifndef WIN32
-	char *path_fs = map_directory_child_fs(directory, utf8_name);
-	if (path_fs == NULL)
+	const Path path_fs = map_directory_child_fs(directory, utf8_name);
+	if (path_fs.IsNull())
 		return true;
 
 	char buffer[MPD_PATH_MAX];
-	ssize_t length = readlink(path_fs, buffer, sizeof(buffer));
-	g_free(path_fs);
+	ssize_t length = readlink(path_fs.c_str(), buffer, sizeof(buffer));
 	if (length < 0)
 		/* don't skip if this is not a symlink */
 		return errno != EINVAL;
@@ -359,24 +352,19 @@ update_directory(Directory *directory, const struct stat *st)
 
 	directory_set_stat(directory, st);
 
-	char *path_fs = map_directory_fs(directory);
-	if (path_fs == NULL)
+	const Path path_fs = map_directory_fs(directory);
+	if (path_fs.IsNull())
 		return false;
 
-	DIR *dir = opendir(path_fs);
+	DIR *dir = opendir(path_fs.c_str());
 	if (!dir) {
 		g_warning("Failed to open directory %s: %s",
-			  path_fs, g_strerror(errno));
-		g_free(path_fs);
+			  path_fs.c_str(), g_strerror(errno));
 		return false;
 	}
 
-	char *exclude_path_fs  = g_build_filename(path_fs, ".mpdignore", NULL);
 	ExcludeList exclude_list;
-	exclude_list.LoadFile(exclude_path_fs);
-	g_free(exclude_path_fs);
-
-	g_free(path_fs);
+	exclude_list.LoadFile(Path::Build(path_fs, ".mpdignore"));
 
 	if (!exclude_list.IsEmpty())
 		remove_excluded_from_directory(directory, exclude_list);
diff --git a/src/db/SimpleDatabasePlugin.cxx b/src/db/SimpleDatabasePlugin.cxx
index 8eea81e30..b7c60d9d8 100644
--- a/src/db/SimpleDatabasePlugin.cxx
+++ b/src/db/SimpleDatabasePlugin.cxx
@@ -68,7 +68,7 @@ SimpleDatabase::Configure(const struct config_param *param, GError **error_r)
 		return false;
 	}
 
-	path = _path;
+	path = Path::FromUTF8(_path);
 	free(_path);
 
 	return true;
@@ -77,6 +77,7 @@ SimpleDatabase::Configure(const struct config_param *param, GError **error_r)
 bool
 SimpleDatabase::Check(GError **error_r) const
 {
+	assert(!path.IsNull());
 	assert(!path.empty());
 
 	/* Check if the file exists */
@@ -153,7 +154,7 @@ SimpleDatabase::Load(GError **error_r)
 	assert(!path.empty());
 	assert(root != NULL);
 
-	TextFile file(path.c_str());
+	TextFile file(path);
 	if (file.HasFailed()) {
 		g_set_error(error_r, simple_db_quark(), errno,
 			    "Failed to open database file \"%s\": %s",
diff --git a/src/db/SimpleDatabasePlugin.hxx b/src/db/SimpleDatabasePlugin.hxx
index 789dcdae9..3549aa98c 100644
--- a/src/db/SimpleDatabasePlugin.hxx
+++ b/src/db/SimpleDatabasePlugin.hxx
@@ -21,17 +21,17 @@
 #define MPD_SIMPLE_DATABASE_PLUGIN_HXX
 
 #include "DatabasePlugin.hxx"
+#include "Path.hxx"
 #include "gcc.h"
 
 #include <cassert>
-#include <string>
 
 #include <time.h>
 
 struct Directory;
 
 class SimpleDatabase : public Database {
-	std::string path;
+	Path path;
 
 	Directory *root;
 
@@ -41,6 +41,9 @@ class SimpleDatabase : public Database {
 	unsigned borrowed_song_count;
 #endif
 
+	SimpleDatabase()
+		:path(Path::Null()) {}
+
 public:
 	gcc_pure
 	Directory *GetRoot() {