From 9abb686eebfa0a512a45586d3ff3d46b4da8ecff Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sun, 16 Feb 2020 20:48:46 +0100
Subject: [PATCH 01/16] increment version number to 0.21.21

---
 NEWS                        | 2 ++
 android/AndroidManifest.xml | 4 ++--
 doc/conf.py                 | 2 +-
 meson.build                 | 2 +-
 4 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/NEWS b/NEWS
index b6e6dff7b..4e35fd90a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,5 @@
+ver 0.21.21 (not yet released)
+
 ver 0.21.20 (2020/02/16)
 * decoder
   - audiofile, ffmpeg, sndfile: handle MIME type "audio/wav"
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index a7ea8a550..83286b0b7 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -2,8 +2,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="org.musicpd"
           android:installLocation="auto"
-          android:versionCode="43"
-          android:versionName="0.21.20">
+          android:versionCode="44"
+          android:versionName="0.21.21">
 
   <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28"/>
 
diff --git a/doc/conf.py b/doc/conf.py
index 836c2b58c..a0d4b393f 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -38,7 +38,7 @@ author = 'Max Kellermann'
 # built documents.
 #
 # The short X.Y version.
-version = '0.21.20'
+version = '0.21.21'
 # The full version, including alpha/beta/rc tags.
 release = version
 
diff --git a/meson.build b/meson.build
index 49bb534c9..d6d0412ec 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,7 @@
 project(
   'mpd',
   ['c', 'cpp'],
-  version: '0.21.20',
+  version: '0.21.21',
   meson_version: '>= 0.49.0',
   default_options: [
     'c_std=c99',

From 976372ff6334db52e2e2beef24794cd4549de592 Mon Sep 17 00:00:00 2001
From: John Regan <john@jrjrtech.com>
Date: Tue, 18 Feb 2020 15:06:52 -0500
Subject: [PATCH 02/16] gme: check for empty metadata strings instead of
 nullptr

Using libgme 0.6.2 on macOS, it appears that gme_info_t strings can be
empty, which creates weird track titles: (001/050)

This adds an additional check for an empty string.
---
 NEWS                                     |  2 ++
 src/decoder/plugins/GmeDecoderPlugin.cxx | 11 ++++++-----
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/NEWS b/NEWS
index 4e35fd90a..4648d09a0 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.21.21 (not yet released)
+* decoder
+  - gme: ignore empty tags
 
 ver 0.21.20 (2020/02/16)
 * decoder
diff --git a/src/decoder/plugins/GmeDecoderPlugin.cxx b/src/decoder/plugins/GmeDecoderPlugin.cxx
index 64f0a1ad4..fcea526af 100644
--- a/src/decoder/plugins/GmeDecoderPlugin.cxx
+++ b/src/decoder/plugins/GmeDecoderPlugin.cxx
@@ -28,6 +28,7 @@
 #include "fs/AllocatedPath.hxx"
 #include "fs/FileSystem.hxx"
 #include "util/ScopeExit.hxx"
+#include "util/StringCompare.hxx"
 #include "util/StringFormat.hxx"
 #include "util/UriUtil.hxx"
 #include "util/Domain.hxx"
@@ -222,7 +223,7 @@ ScanGmeInfo(const gme_info_t &info, unsigned song_num, int track_count,
 	if (track_count > 1)
 		handler.OnTag(TAG_TRACK, StringFormat<16>("%u", song_num + 1));
 
-	if (info.song != nullptr) {
+	if (!StringIsEmpty(info.song)) {
 		if (track_count > 1) {
 			/* start numbering subtunes from 1 */
 			const auto tag_title =
@@ -234,16 +235,16 @@ ScanGmeInfo(const gme_info_t &info, unsigned song_num, int track_count,
 			handler.OnTag(TAG_TITLE, info.song);
 	}
 
-	if (info.author != nullptr)
+	if (!StringIsEmpty(info.author))
 		handler.OnTag(TAG_ARTIST, info.author);
 
-	if (info.game != nullptr)
+	if (!StringIsEmpty(info.game))
 		handler.OnTag(TAG_ALBUM, info.game);
 
-	if (info.comment != nullptr)
+	if (!StringIsEmpty(info.comment))
 		handler.OnTag(TAG_COMMENT, info.comment);
 
-	if (info.copyright != nullptr)
+	if (!StringIsEmpty(info.copyright))
 		handler.OnTag(TAG_DATE, info.copyright);
 }
 

From d5468dfe89acb85885d2f8017a66bdecdfcbb758 Mon Sep 17 00:00:00 2001
From: Thomas Klausner <tk@giga.or.at>
Date: Sat, 29 Feb 2020 09:55:13 +0100
Subject: [PATCH 03/16] Add missing header.

Fixes
../src/time/ISO8601.cxx:67:24: error: use of undeclared identifier 'strtoul'
        unsigned long value = strtoul(s, &endptr, 10);
                              ^
../src/time/ISO8601.cxx:77:14: error: use of undeclared identifier 'strtoul'
                        minutes = strtoul(s, &endptr, 10);
                                  ^

on NetBSD with clang 9.0.0.
---
 src/time/ISO8601.cxx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/time/ISO8601.cxx b/src/time/ISO8601.cxx
index 725215ea6..336ddcd5b 100644
--- a/src/time/ISO8601.cxx
+++ b/src/time/ISO8601.cxx
@@ -37,6 +37,7 @@
 #include <stdexcept>
 
 #include <assert.h>
+#include <stdlib.h>
 
 StringBuffer<64>
 FormatISO8601(const struct tm &tm) noexcept

From 7a68b1e71fd2f29478b95a5116ab0792f218f9ff Mon Sep 17 00:00:00 2001
From: Thomas Klausner <tk@giga.or.at>
Date: Sat, 29 Feb 2020 09:56:10 +0100
Subject: [PATCH 04/16] Adapt SolarisOutputPlugin.cxx to be usable on NetBSD.

---
 NEWS                                       |  2 ++
 src/output/plugins/SolarisOutputPlugin.cxx | 11 ++++++++---
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index 4648d09a0..741938b37 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,8 @@
 ver 0.21.21 (not yet released)
 * decoder
   - gme: ignore empty tags
+* output
+  - solaris: port to NetBSD
 
 ver 0.21.20 (2020/02/16)
 * decoder
diff --git a/src/output/plugins/SolarisOutputPlugin.cxx b/src/output/plugins/SolarisOutputPlugin.cxx
index d6323d5aa..5176f7ac4 100644
--- a/src/output/plugins/SolarisOutputPlugin.cxx
+++ b/src/output/plugins/SolarisOutputPlugin.cxx
@@ -22,22 +22,23 @@
 #include "system/FileDescriptor.hxx"
 #include "system/Error.hxx"
 
+#include <sys/ioctl.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <errno.h>
 
-#ifdef __sun
+#if defined(__sun)
 #include <sys/audio.h>
 #include <sys/stropts.h>
+#elif defined(__NetBSD__)
+#include <sys/audioio.h>
 #else
 
 /* some fake declarations that allow build this plugin on systems
    other than Solaris, just to see if it compiles */
 
-#include <sys/ioctl.h>
-
 #ifndef I_FLUSH
 #define I_FLUSH 0
 #endif
@@ -147,7 +148,11 @@ SolarisOutput::Play(const void *chunk, size_t size)
 void
 SolarisOutput::Cancel() noexcept
 {
+#if defined(AUDIO_FLUSH)
+	ioctl(fd.Get(), AUDIO_FLUSH);
+#elif defined(I_FLUSH)
 	ioctl(fd.Get(), I_FLUSH);
+#endif
 }
 
 const struct AudioOutputPlugin solaris_output_plugin = {

From 5fe70a3417267910317b7ecf5d5439497e8262af Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 7 Mar 2020 09:30:33 +0100
Subject: [PATCH 05/16] .travis.yml: rename "matrix" to "jobs"

Travis has changed the canonical name for this a while ago.

From 5faf76051d3a99c637653c76d0fc762e75b87bd5 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 7 Mar 2020 09:29:45 +0100
Subject: [PATCH 06/16] .travis.yml: force updating homebrew on OSX

Workaround for Travis failures as described in
 https://travis-ci.community/t/macos-build-fails-because-of-homebrew-bundle-unknown-command/7296/18
---
 .travis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.travis.yml b/.travis.yml
index c62d7e244..f780cb785 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -126,6 +126,7 @@ matrix:
           packages:
             - ccache
             - meson
+          update: true
       env:
         - MATRIX_EVAL="export PATH=/usr/local/opt/ccache/libexec:$PATH HOMEBREW_NO_ANALYTICS=1"
 

From b7ce4523083d5c98b4d944045a318b6a3e590a46 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 7 Mar 2020 09:02:02 +0100
Subject: [PATCH 07/16] fs/Traits: add IsSpecialFilename()

Merge some duplicate code in a central library.
---
 src/db/update/InotifyUpdate.cxx      |  4 ++--
 src/db/update/Walk.cxx               |  2 +-
 src/fs/Traits.hxx                    | 12 ++++++++++++
 src/storage/plugins/LocalStorage.cxx | 11 +----------
 4 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/src/db/update/InotifyUpdate.cxx b/src/db/update/InotifyUpdate.cxx
index 374a1ea04..0885aa435 100644
--- a/src/db/update/InotifyUpdate.cxx
+++ b/src/db/update/InotifyUpdate.cxx
@@ -24,6 +24,7 @@
 #include "storage/StorageInterface.hxx"
 #include "fs/AllocatedPath.hxx"
 #include "fs/FileInfo.hxx"
+#include "fs/Traits.hxx"
 #include "Log.hxx"
 
 #include <string>
@@ -146,8 +147,7 @@ WatchDirectory::GetUriFS() const noexcept
 /* we don't look at "." / ".." nor files with newlines in their name */
 static bool skip_path(const char *path)
 {
-	return (path[0] == '.' && path[1] == 0) ||
-		(path[0] == '.' && path[1] == '.' && path[2] == 0) ||
+	return PathTraitsFS::IsSpecialFilename(path) ||
 		strchr(path, '\n') != nullptr;
 }
 
diff --git a/src/db/update/Walk.cxx b/src/db/update/Walk.cxx
index e2c9b7a8c..eb853fcb8 100644
--- a/src/db/update/Walk.cxx
+++ b/src/db/update/Walk.cxx
@@ -237,7 +237,7 @@ try {
 	LogError(std::current_exception());
 }
 
-/* we don't look at "." / ".." nor files with newlines in their name */
+/* we don't look at files with newlines in their name */
 gcc_pure
 static bool
 skip_path(const char *name_utf8) noexcept
diff --git a/src/fs/Traits.hxx b/src/fs/Traits.hxx
index 6a7e31f0c..735ec6c9e 100644
--- a/src/fs/Traits.hxx
+++ b/src/fs/Traits.hxx
@@ -108,6 +108,12 @@ struct PathTraitsFS {
 		return IsSeparator(*p);
 	}
 
+	gcc_pure gcc_nonnull_all
+	static bool IsSpecialFilename(const_pointer_type name) noexcept {
+		return (name[0] == '.' && name[1] == 0) ||
+			(name[0] == '.' && name[1] == '.' && name[2] == 0);
+	}
+
 	gcc_pure gcc_nonnull_all
 	static size_t GetLength(const_pointer_type p) noexcept {
 		return StringLength(p);
@@ -216,6 +222,12 @@ struct PathTraitsUTF8 {
 		return IsSeparator(*p);
 	}
 
+	gcc_pure gcc_nonnull_all
+	static bool IsSpecialFilename(const_pointer_type name) noexcept {
+		return (name[0] == '.' && name[1] == 0) ||
+			(name[0] == '.' && name[1] == '.' && name[2] == 0);
+	}
+
 	gcc_pure gcc_nonnull_all
 	static size_t GetLength(const_pointer_type p) noexcept {
 		return StringLength(p);
diff --git a/src/storage/plugins/LocalStorage.cxx b/src/storage/plugins/LocalStorage.cxx
index 562e0937a..662f8801e 100644
--- a/src/storage/plugins/LocalStorage.cxx
+++ b/src/storage/plugins/LocalStorage.cxx
@@ -144,21 +144,12 @@ LocalStorage::OpenDirectory(const char *uri_utf8)
 	return std::make_unique<LocalDirectoryReader>(MapFSOrThrow(uri_utf8));
 }
 
-gcc_pure
-static bool
-SkipNameFS(PathTraitsFS::const_pointer_type name_fs) noexcept
-{
-	return name_fs[0] == '.' &&
-		(name_fs[1] == 0 ||
-		 (name_fs[1] == '.' && name_fs[2] == 0));
-}
-
 const char *
 LocalDirectoryReader::Read() noexcept
 {
 	while (reader.ReadEntry()) {
 		const Path name_fs = reader.GetEntry();
-		if (SkipNameFS(name_fs.c_str()))
+		if (PathTraitsFS::IsSpecialFilename(name_fs.c_str()))
 			continue;
 
 		try {

From 73a1f078a6bb0a2228f7d9e7a48a53382707ec35 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 7 Mar 2020 09:15:54 +0100
Subject: [PATCH 08/16] archive/iso9660: use IsSpecialFilename()

---
 src/archive/plugins/Iso9660ArchivePlugin.cxx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/archive/plugins/Iso9660ArchivePlugin.cxx b/src/archive/plugins/Iso9660ArchivePlugin.cxx
index a27230728..50cea2aef 100644
--- a/src/archive/plugins/Iso9660ArchivePlugin.cxx
+++ b/src/archive/plugins/Iso9660ArchivePlugin.cxx
@@ -93,7 +93,8 @@ Iso9660ArchiveFile::Visit(char *path, size_t length, size_t capacity,
 		auto *statbuf = (iso9660_stat_t *)
 			_cdio_list_node_data(entnode);
 		const char *filename = statbuf->filename;
-		if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0)
+		if (PathTraitsUTF8::IsSpecialFilename(filename))
+			/* skip special names like "." and ".." */
 			continue;
 
 		size_t filename_length = strlen(filename);

From 8d34a1cfc6e5e6e1a5f958160edc78c141854b18 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 7 Mar 2020 09:16:37 +0100
Subject: [PATCH 09/16] archive/iso9660: skip empty filenames

Aparently, libcdio sometimes returns empty filenames, causing MPD
crashes.  This shouldn't really happen, and I consider this a libcdio
bug - but if it happens, people blame MPD, so let's add a check.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/776
---
 NEWS                                         | 2 ++
 src/archive/plugins/Iso9660ArchivePlugin.cxx | 5 ++++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 741938b37..c8742f395 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.21.21 (not yet released)
+* archive
+  - iso9660: skip empty file names to work around libcdio bug
 * decoder
   - gme: ignore empty tags
 * output
diff --git a/src/archive/plugins/Iso9660ArchivePlugin.cxx b/src/archive/plugins/Iso9660ArchivePlugin.cxx
index 50cea2aef..6cb307bd4 100644
--- a/src/archive/plugins/Iso9660ArchivePlugin.cxx
+++ b/src/archive/plugins/Iso9660ArchivePlugin.cxx
@@ -28,6 +28,7 @@
 #include "input/InputStream.hxx"
 #include "fs/Path.hxx"
 #include "util/RuntimeError.hxx"
+#include "util/StringCompare.hxx"
 
 #include <cdio/iso9660.h>
 
@@ -93,7 +94,9 @@ Iso9660ArchiveFile::Visit(char *path, size_t length, size_t capacity,
 		auto *statbuf = (iso9660_stat_t *)
 			_cdio_list_node_data(entnode);
 		const char *filename = statbuf->filename;
-		if (PathTraitsUTF8::IsSpecialFilename(filename))
+		if (StringIsEmpty(filename) ||
+		    PathTraitsUTF8::IsSpecialFilename(filename))
+			/* skip empty names (libcdio bug?) */
 			/* skip special names like "." and ".." */
 			continue;
 

From cd364023ae2e8cc824692289bc6c169e6c67594c Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Sat, 7 Mar 2020 09:31:46 +0100
Subject: [PATCH 10/16] .travis.yml: rename "matrix" to "jobs"

Travis has changed the canonical name for this a while ago.

(Now really.  The last commit for this was empty.)
---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index f780cb785..38b72d547 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
 language: cpp
 
-matrix:
+jobs:
   include:
     # Ubuntu Bionic (18.04) with GCC 7
     - os: linux

From acb29f792f937f41f82be0949457513ec46aed4e Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 11 Mar 2020 20:34:02 +0100
Subject: [PATCH 11/16] tag/Mask: fix yet another typo, this time in Unset()

Similar to commits e8f2f980488318fb0e37c1fe6bc1300e97327b6e and
ff1ff1e54a25ed80abdca9e7e63b36db150ec766

Closes https://github.com/MusicPlayerDaemon/MPD/issues/783
---
 NEWS             | 2 ++
 src/tag/Mask.hxx | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index c8742f395..7f9d25bc3 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.21.21 (not yet released)
+* configuration
+  - fix bug in "metadata_to_use" setting
 * archive
   - iso9660: skip empty file names to work around libcdio bug
 * decoder
diff --git a/src/tag/Mask.hxx b/src/tag/Mask.hxx
index 4b6126bc5..e7de5dd54 100644
--- a/src/tag/Mask.hxx
+++ b/src/tag/Mask.hxx
@@ -89,7 +89,7 @@ public:
 	}
 
 	void Unset(TagType tag) {
-		*this |= ~TagMask(tag);
+		*this &= ~TagMask(tag);
 	}
 };
 

From 98a7d8da6cd88ddb335fbc375ef546a09ae8ea16 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 11 Mar 2020 20:51:10 +0100
Subject: [PATCH 12/16] playlist/xspf: use C++11 initializer

---
 src/playlist/plugins/XspfPlaylistPlugin.cxx | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/playlist/plugins/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx
index b266830f9..4a4b240bf 100644
--- a/src/playlist/plugins/XspfPlaylistPlugin.cxx
+++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx
@@ -45,7 +45,7 @@ struct XspfParser {
 	enum {
 		ROOT, PLAYLIST, TRACKLIST, TRACK,
 		LOCATION,
-	} state;
+	} state = ROOT;
 
 	/**
 	 * The current tag within the "track" element.  This is only
@@ -60,9 +60,6 @@ struct XspfParser {
 	std::string location;
 
 	TagBuilder tag_builder;
-
-	XspfParser()
-		:state(ROOT) {}
 };
 
 static void XMLCALL

From 51b1dd867207fdc69d83556fa32742198847b31a Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 11 Mar 2020 20:51:47 +0100
Subject: [PATCH 13/16] playlist/xspf: use tag_table to convert element name to
 TagType

---
 src/playlist/plugins/XspfPlaylistPlugin.cxx | 29 ++++++++++++---------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/playlist/plugins/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx
index 4a4b240bf..b084725fe 100644
--- a/src/playlist/plugins/XspfPlaylistPlugin.cxx
+++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx
@@ -23,6 +23,7 @@
 #include "song/DetachedSong.hxx"
 #include "input/InputStream.hxx"
 #include "tag/Builder.hxx"
+#include "tag/Table.hxx"
 #include "util/StringView.hxx"
 #include "lib/expat/ExpatParser.hxx"
 #include "Log.hxx"
@@ -62,6 +63,19 @@ struct XspfParser {
 	TagBuilder tag_builder;
 };
 
+static constexpr struct tag_table xspf_tag_elements[] = {
+	{ "title", TAG_TITLE },
+
+	/* TAG_COMPOSER would be more correct according to the XSPF
+	   spec */
+	{ "creator", TAG_ARTIST },
+
+	{ "annotation", TAG_COMMENT },
+	{ "album", TAG_ALBUM },
+	{ "trackNum", TAG_TRACK },
+	{ nullptr, TAG_NUM_OF_ITEM_TYPES }
+};
+
 static void XMLCALL
 xspf_start_element(void *user_data, const XML_Char *element_name,
 		   gcc_unused const XML_Char **atts)
@@ -93,18 +107,9 @@ xspf_start_element(void *user_data, const XML_Char *element_name,
 	case XspfParser::TRACK:
 		if (strcmp(element_name, "location") == 0)
 			parser->state = XspfParser::LOCATION;
-		else if (strcmp(element_name, "title") == 0)
-			parser->tag_type = TAG_TITLE;
-		else if (strcmp(element_name, "creator") == 0)
-			/* TAG_COMPOSER would be more correct
-			   according to the XSPF spec */
-			parser->tag_type = TAG_ARTIST;
-		else if (strcmp(element_name, "annotation") == 0)
-			parser->tag_type = TAG_COMMENT;
-		else if (strcmp(element_name, "album") == 0)
-			parser->tag_type = TAG_ALBUM;
-		else if (strcmp(element_name, "trackNum") == 0)
-			parser->tag_type = TAG_TRACK;
+		else
+			parser->tag_type = tag_table_lookup(xspf_tag_elements,
+							    element_name);
 
 		break;
 

From 3e9705815108dd25b2f9f77fbd77bbcbb3340d69 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 11 Mar 2020 20:54:53 +0100
Subject: [PATCH 14/16] playlist/xspf: move location.empty() check to
 _start_element()

---
 src/playlist/plugins/XspfPlaylistPlugin.cxx | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/playlist/plugins/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx
index b084725fe..f32dc35d4 100644
--- a/src/playlist/plugins/XspfPlaylistPlugin.cxx
+++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx
@@ -107,7 +107,7 @@ xspf_start_element(void *user_data, const XML_Char *element_name,
 	case XspfParser::TRACK:
 		if (strcmp(element_name, "location") == 0)
 			parser->state = XspfParser::LOCATION;
-		else
+		else if (!parser->location.empty())
 			parser->tag_type = tag_table_lookup(xspf_tag_elements,
 							    element_name);
 
@@ -169,8 +169,7 @@ xspf_char_data(void *user_data, const XML_Char *s, int len)
 		break;
 
 	case XspfParser::TRACK:
-		if (!parser->location.empty() &&
-		    parser->tag_type != TAG_NUM_OF_ITEM_TYPES)
+		if (parser->tag_type != TAG_NUM_OF_ITEM_TYPES)
 			parser->tag_builder.AddItem(parser->tag_type,
 						    StringView(s, len));
 

From e0a8fd398c7ee1479fd5c3eed9828c098f87666b Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Thu, 12 Mar 2020 08:00:54 +0100
Subject: [PATCH 15/16] playlist/xspf: add State::TAG

---
 src/playlist/plugins/XspfPlaylistPlugin.cxx | 22 +++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/playlist/plugins/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx
index f32dc35d4..9ed5a8544 100644
--- a/src/playlist/plugins/XspfPlaylistPlugin.cxx
+++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx
@@ -45,7 +45,7 @@ struct XspfParser {
 	 */
 	enum {
 		ROOT, PLAYLIST, TRACKLIST, TRACK,
-		LOCATION,
+		TAG, LOCATION,
 	} state = ROOT;
 
 	/**
@@ -99,7 +99,6 @@ xspf_start_element(void *user_data, const XML_Char *element_name,
 		if (strcmp(element_name, "track") == 0) {
 			parser->state = XspfParser::TRACK;
 			parser->location.clear();
-			parser->tag_type = TAG_NUM_OF_ITEM_TYPES;
 		}
 
 		break;
@@ -107,12 +106,16 @@ xspf_start_element(void *user_data, const XML_Char *element_name,
 	case XspfParser::TRACK:
 		if (strcmp(element_name, "location") == 0)
 			parser->state = XspfParser::LOCATION;
-		else if (!parser->location.empty())
+		else if (!parser->location.empty()) {
 			parser->tag_type = tag_table_lookup(xspf_tag_elements,
 							    element_name);
+			if (parser->tag_type != TAG_NUM_OF_ITEM_TYPES)
+				parser->state = XspfParser::TAG;
+		}
 
 		break;
 
+	case XspfParser::TAG:
 	case XspfParser::LOCATION:
 		break;
 	}
@@ -146,11 +149,11 @@ xspf_end_element(void *user_data, const XML_Char *element_name)
 							    parser->tag_builder.Commit());
 
 			parser->state = XspfParser::TRACKLIST;
-		} else
-			parser->tag_type = TAG_NUM_OF_ITEM_TYPES;
+		}
 
 		break;
 
+	case XspfParser::TAG:
 	case XspfParser::LOCATION:
 		parser->state = XspfParser::TRACK;
 		break;
@@ -166,13 +169,12 @@ xspf_char_data(void *user_data, const XML_Char *s, int len)
 	case XspfParser::ROOT:
 	case XspfParser::PLAYLIST:
 	case XspfParser::TRACKLIST:
+	case XspfParser::TRACK:
 		break;
 
-	case XspfParser::TRACK:
-		if (parser->tag_type != TAG_NUM_OF_ITEM_TYPES)
-			parser->tag_builder.AddItem(parser->tag_type,
-						    StringView(s, len));
-
+	case XspfParser::TAG:
+		parser->tag_builder.AddItem(parser->tag_type,
+					    StringView(s, len));
 		break;
 
 	case XspfParser::LOCATION:

From c45f1138560b526649bf6e6433fa7d9ba8a6564b Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Wed, 11 Mar 2020 20:44:16 +0100
Subject: [PATCH 16/16] playlist/xspf: concatenate multiple CharacterData
 fragments

Closes https://github.com/MusicPlayerDaemon/MPD/issues/781
---
 NEWS                                        |  2 ++
 src/playlist/plugins/XspfPlaylistPlugin.cxx | 21 +++++++++++++++------
 2 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/NEWS b/NEWS
index 7f9d25bc3..7cf1248d3 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,8 @@
 ver 0.21.21 (not yet released)
 * configuration
   - fix bug in "metadata_to_use" setting
+* playlist
+  - xspf: fix corrupt tags in the presence of XML entities
 * archive
   - iso9660: skip empty file names to work around libcdio bug
 * decoder
diff --git a/src/playlist/plugins/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx
index 9ed5a8544..851c7ba83 100644
--- a/src/playlist/plugins/XspfPlaylistPlugin.cxx
+++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx
@@ -61,6 +61,8 @@ struct XspfParser {
 	std::string location;
 
 	TagBuilder tag_builder;
+
+	std::string value;
 };
 
 static constexpr struct tag_table xspf_tag_elements[] = {
@@ -81,6 +83,7 @@ xspf_start_element(void *user_data, const XML_Char *element_name,
 		   gcc_unused const XML_Char **atts)
 {
 	XspfParser *parser = (XspfParser *)user_data;
+	parser->value.clear();
 
 	switch (parser->state) {
 	case XspfParser::ROOT:
@@ -154,10 +157,21 @@ xspf_end_element(void *user_data, const XML_Char *element_name)
 		break;
 
 	case XspfParser::TAG:
+		if (!parser->value.empty())
+			parser->tag_builder.AddItem(parser->tag_type,
+						    StringView(parser->value.data(),
+							       parser->value.length()));
+
+		parser->state = XspfParser::TRACK;
+		break;
+
 	case XspfParser::LOCATION:
+		parser->location = std::move(parser->value);
 		parser->state = XspfParser::TRACK;
 		break;
 	}
+
+	parser->value.clear();
 }
 
 static void XMLCALL
@@ -173,13 +187,8 @@ xspf_char_data(void *user_data, const XML_Char *s, int len)
 		break;
 
 	case XspfParser::TAG:
-		parser->tag_builder.AddItem(parser->tag_type,
-					    StringView(s, len));
-		break;
-
 	case XspfParser::LOCATION:
-		parser->location.assign(s, len);
-
+		parser->value.append(s, len);
 		break;
 	}
 }