From da67260c955ebe1bc590b461e5ee8c9665f26f5b Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Mon, 20 Jan 2014 17:20:57 +0100
Subject: [PATCH 1/3] new developer mailing list

---
 configure.ac      | 2 +-
 doc/developer.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/configure.ac b/configure.ac
index b176aeedd..fc94f4c1e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,6 +1,6 @@
 AC_PREREQ(2.60)
 
-AC_INIT(mpd, 0.18.8, musicpd-dev-team@lists.sourceforge.net)
+AC_INIT(mpd, 0.18.8, mpd-devel@musicpd.org)
 
 VERSION_MAJOR=0
 VERSION_MINOR=18
diff --git a/doc/developer.xml b/doc/developer.xml
index e9022172c..729e6a513 100644
--- a/doc/developer.xml
+++ b/doc/developer.xml
@@ -155,7 +155,7 @@ foo(const char *abc, int xyz)
 
     <para>
       Send your patches to the mailing list:
-      musicpd-dev-team@lists.sourceforge.net
+      mpd-devel@musicpd.org
     </para>
   </chapter>
 </book>

From f7eb2b697ef6ac523632ad27b43b185f5901438c Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Mon, 27 Jan 2014 09:51:31 +0100
Subject: [PATCH 2/3] test/test_icy_parser: unit test for IcyMetaDataParser.cxx

---
 Makefile.am              | 11 +++++++
 test/test_icy_parser.cxx | 70 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+)
 create mode 100644 test/test_icy_parser.cxx

diff --git a/Makefile.am b/Makefile.am
index 7240cb3f1..c7cf631bb 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1061,6 +1061,7 @@ C_TESTS = \
 	test/test_util \
 	test/test_byte_reverse \
 	test/test_mixramp \
+	test/test_icy_parser \
 	test/test_pcm \
 	test/test_queue_priority
 
@@ -1496,6 +1497,16 @@ test_test_mixramp_LDADD = \
 	$(GLIB_LIBS) \
 	$(CPPUNIT_LIBS)
 
+test_test_icy_parser_SOURCES = \
+	src/Log.cxx \
+	test/test_icy_parser.cxx
+test_test_icy_parser_CPPFLAGS = $(AM_CPPFLAGS) $(CPPUNIT_CFLAGS) -DCPPUNIT_HAVE_RTTI=0
+test_test_icy_parser_CXXFLAGS = $(AM_CXXFLAGS) -Wno-error=deprecated-declarations
+test_test_icy_parser_LDADD = \
+	libtag.a \
+	$(GLIB_LIBS) \
+	$(CPPUNIT_LIBS)
+
 test_test_pcm_SOURCES = \
 	test/test_pcm_util.hxx \
 	test/test_pcm_dither.cxx \
diff --git a/test/test_icy_parser.cxx b/test/test_icy_parser.cxx
new file mode 100644
index 000000000..83925cc99
--- /dev/null
+++ b/test/test_icy_parser.cxx
@@ -0,0 +1,70 @@
+/*
+ * Unit tests for class IcyMetaDataParser.
+ */
+
+#include "config.h"
+
+/* include the .cxx file to get access to internal functions */
+#include "IcyMetaDataParser.cxx"
+
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+#include <cppunit/ui/text/TestRunner.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <string>
+
+static void
+CompareTagTitle(const Tag &tag, const std::string &title)
+{
+	CPPUNIT_ASSERT_EQUAL(1u, tag.num_items);
+
+	const TagItem &item = *tag.items[0];
+	CPPUNIT_ASSERT_EQUAL(TAG_TITLE, item.type);
+	CPPUNIT_ASSERT_EQUAL(title, std::string(item.value));
+}
+
+static void
+TestIcyParserTitle(const char *input, const char *title)
+{
+	Tag *tag = icy_parse_tag(input);
+	CompareTagTitle(*tag, title);
+	delete tag;
+}
+
+static void
+TestIcyParserEmpty(const char *input)
+{
+	Tag *tag = icy_parse_tag(input);
+	CPPUNIT_ASSERT_EQUAL(0u, tag->num_items);
+	delete tag;
+}
+
+class IcyTest : public CppUnit::TestFixture {
+	CPPUNIT_TEST_SUITE(IcyTest);
+	CPPUNIT_TEST(TestIcyMetadataParser);
+	CPPUNIT_TEST_SUITE_END();
+
+public:
+	void TestIcyMetadataParser() {
+		TestIcyParserEmpty("foo=bar;");
+		TestIcyParserTitle("StreamTitle='foo bar'", "foo bar");
+		TestIcyParserTitle("StreamTitle='foo bar';", "foo bar");
+		TestIcyParserTitle("StreamTitle='foo\"bar';", "foo\"bar");
+		TestIcyParserTitle("a=b;StreamTitle='foo';", "foo");
+		TestIcyParserTitle("a=;StreamTitle='foo';", "foo");
+		TestIcyParserTitle("a=b;StreamTitle='foo';c=d", "foo");
+		TestIcyParserTitle("a=b;StreamTitle='foo'", "foo");
+	}
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(IcyTest);
+
+int
+main(gcc_unused int argc, gcc_unused char **argv)
+{
+	CppUnit::TextUi::TestRunner runner;
+	auto &registry = CppUnit::TestFactoryRegistry::getRegistry();
+	runner.addTest(registry.makeTest());
+	return runner.run() ? EXIT_SUCCESS : EXIT_FAILURE;
+}

From 2b10ecfa37e273c752c3f87e2491e2a1a5f0ae58 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Tue, 21 Jan 2014 14:51:35 +0100
Subject: [PATCH 3/3] IcyMetadataParser: more robust tag parser

Allow semicolons and single quotes in the stream title.  This is not
part of any specification, but found in real life.
---
 NEWS                      |  1 +
 src/IcyMetaDataParser.cxx | 90 ++++++++++++++++++++++++++++++---------
 test/test_icy_parser.cxx  | 15 +++++++
 3 files changed, 86 insertions(+), 20 deletions(-)

diff --git a/NEWS b/NEWS
index 68efb8a85..1a0c5c415 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,7 @@
 ver 0.18.8 (not yet released)
 * decoder
   - ffmpeg: support libav v10_alpha1
+* more robust Icy-Metadata parser
 * fix Solaris build failure
 
 ver 0.18.7 (2013/01/13)
diff --git a/src/IcyMetaDataParser.cxx b/src/IcyMetaDataParser.cxx
index 8861efb2e..bfa2e8558 100644
--- a/src/IcyMetaDataParser.cxx
+++ b/src/IcyMetaDataParser.cxx
@@ -81,31 +81,85 @@ icy_add_item(Tag &tag, TagType type, const char *value)
 }
 
 static void
-icy_parse_tag_item(Tag &tag, const char *item)
+icy_parse_tag_item(Tag &tag, const char *name, const char *value)
 {
-	gchar **p = g_strsplit(item, "=", 0);
+	if (strcmp(name, "StreamTitle") == 0)
+		icy_add_item(tag, TAG_TITLE, value);
+	else
+		FormatDebug(icy_metadata_domain,
+			    "unknown icy-tag: '%s'", name);
+}
 
-	if (p[0] != nullptr && p[1] != nullptr) {
-		if (strcmp(p[0], "StreamTitle") == 0)
-			icy_add_item(tag, TAG_TITLE, p[1]);
-		else
-			FormatDebug(icy_metadata_domain,
-				    "unknown icy-tag: '%s'", p[0]);
+/**
+ * Find a single quote that is followed by a semicolon (or by the end
+ * of the string).  If that fails, return the first single quote.  If
+ * that also fails, return #end.
+ */
+static char *
+find_end_quote(char *p, char *const end)
+{
+	char *fallback = std::find(p, end, '\'');
+	if (fallback >= end - 1 || fallback[1] == ';')
+		return fallback;
+
+	p = fallback + 1;
+	while (true) {
+		p = std::find(p, end, '\'');
+		if (p == end)
+			return fallback;
+
+		if (p == end - 1 || p[1] == ';')
+			return p;
+
+		++p;
 	}
-
-	g_strfreev(p);
 }
 
 static Tag *
-icy_parse_tag(const char *p)
+icy_parse_tag(char *p, char *const end)
 {
+	assert(p != nullptr);
+	assert(end != nullptr);
+	assert(p <= end);
+
 	Tag *tag = new Tag();
-	gchar **items = g_strsplit(p, ";", 0);
 
-	for (unsigned i = 0; items[i] != nullptr; ++i)
-		icy_parse_tag_item(*tag, items[i]);
+	while (p != end) {
+		const char *const name = p;
+		char *eq = std::find(p, end, '=');
+		if (eq == end)
+			break;
 
-	g_strfreev(items);
+		*eq = 0;
+		p = eq + 1;
+
+		if (*p != '\'') {
+			/* syntax error; skip to the next semicolon,
+			   try to recover */
+			char *semicolon = std::find(p, end, ';');
+			if (semicolon == end)
+				break;
+			p = semicolon + 1;
+			continue;
+		}
+
+		++p;
+
+		const char *const value = p;
+		char *quote = find_end_quote(p, end);
+		if (quote == end)
+			break;
+
+		*quote = 0;
+		p = quote + 1;
+
+		icy_parse_tag_item(*tag, name, value);
+
+		char *semicolon = std::find(p, end, ';');
+		if (semicolon == end)
+			break;
+		p = semicolon + 1;
+	}
 
 	return tag;
 }
@@ -152,15 +206,11 @@ IcyMetaDataParser::Meta(const void *data, size_t length)
 		++length;
 
 	if (meta_position == meta_size) {
-		/* null-terminate the string */
-
-		meta_data[meta_size] = 0;
-
 		/* parse */
 
 		delete tag;
 
-		tag = icy_parse_tag(meta_data);
+		tag = icy_parse_tag(meta_data, meta_data + meta_size);
 		g_free(meta_data);
 
 		/* change back to normal data mode */
diff --git a/test/test_icy_parser.cxx b/test/test_icy_parser.cxx
index 83925cc99..2abf60f9e 100644
--- a/test/test_icy_parser.cxx
+++ b/test/test_icy_parser.cxx
@@ -14,6 +14,17 @@
 
 #include <string>
 
+#include <string.h>
+
+static Tag *
+icy_parse_tag(const char *p)
+{
+	char *q = strdup(p);
+	Tag *tag = icy_parse_tag(q, q + strlen(q));
+	free(q);
+	return tag;
+}
+
 static void
 CompareTagTitle(const Tag &tag, const std::string &title)
 {
@@ -51,10 +62,14 @@ public:
 		TestIcyParserTitle("StreamTitle='foo bar'", "foo bar");
 		TestIcyParserTitle("StreamTitle='foo bar';", "foo bar");
 		TestIcyParserTitle("StreamTitle='foo\"bar';", "foo\"bar");
+		TestIcyParserTitle("StreamTitle='foo=bar';", "foo=bar");
 		TestIcyParserTitle("a=b;StreamTitle='foo';", "foo");
 		TestIcyParserTitle("a=;StreamTitle='foo';", "foo");
 		TestIcyParserTitle("a=b;StreamTitle='foo';c=d", "foo");
 		TestIcyParserTitle("a=b;StreamTitle='foo'", "foo");
+		TestIcyParserTitle("a='b;c';StreamTitle='foo;bar'", "foo;bar");
+		TestIcyParserTitle("a='b'c';StreamTitle='foo'bar'", "foo'bar");
+		TestIcyParserTitle("StreamTitle='fo'o'b'ar';a='b'c'd'", "fo'o'b'ar");
 	}
 };