From c39d8e5813ed3730c24ef910d77f34175a90f124 Mon Sep 17 00:00:00 2001
From: gd <gd@iotide.com>
Date: Sat, 29 Jun 2024 11:18:18 +0300
Subject: [PATCH] Added explicitly case sensitive/insensitive filter operators.

The default case sensitivity is hard coded for each command.
These operators allow to override the this default case sensitivity.
---
 doc/protocol.rst    | 37 +++++++++++++++++++++++++++++++++++++
 src/song/Filter.cxx | 44 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+)

diff --git a/doc/protocol.rst b/doc/protocol.rst
index f0b477bd1..e2425e250 100644
--- a/doc/protocol.rst
+++ b/doc/protocol.rst
@@ -243,6 +243,43 @@ applies `Unicode normalization <https://unicode.org/reports/tr15/>`__
 and converts all punctuation to ASCII equivalents
 if MPD was compiled with `ICU <https://icu.unicode.org/>`__ support.
 
+Explicit case-sensitivity [#since_0_24]_
+----------------------------------------
+
+.. note:: The following variants of filter operators override the default case sensitivity
+   that is command dependant with explicit case sensitivity.
+
+.. list-table:: Explicitly case-sensitive operators
+   :widths: 33 33 33
+
+   * - Explicitly case-sensitive
+     - Explicitly case-insensitive
+     - Equivalent command dependant
+
+   * - ``eq_cs``
+     - ``eq_ci``
+     - ``==``
+
+   * - ``!eq_cs``
+     - ``!eq_ci``
+     - ``!=``
+
+   * - ``contains_cs``
+     - ``contains_ci``
+     - ``contains``
+
+   * - ``!contains_cs``
+     - ``!contains_ci``
+     - ``!contains``
+
+   * - ``starts_with_cs``
+     - ``starts_with_ci``
+     - ``starts_with``
+
+   * - ``!starts_with_cs``
+     - ``!starts_with_ci``
+     - ``!starts_with``
+
 Prior to MPD 0.21, the syntax looked like this::
 
  find TYPE VALUE
diff --git a/src/song/Filter.cxx b/src/song/Filter.cxx
index 047389da9..8f7dbb2c6 100644
--- a/src/song/Filter.cxx
+++ b/src/song/Filter.cxx
@@ -190,6 +190,39 @@ ExpectQuoted(const char *&s)
 	return {buffer, length};
 }
 
+/**
+ * Operator definition used to parse the operator
+ * from the command and create the StringFilter
+ * if it matched the operator prefix.
+ */
+struct OperatorDef {
+	const char *prefix;
+	bool fold_case;
+	bool negated;
+	StringFilter::Position position;
+};
+
+/**
+ * Pre-defined operators with explicit case-sensitivity.
+ */
+static constexpr std::array<OperatorDef, 12> operators = {
+	//            operator prefix     fold case  negated     position
+	OperatorDef { "contains_cs ",     false,     false,      StringFilter::Position::ANYWHERE },
+	OperatorDef { "!contains_cs ",    false,     true,       StringFilter::Position::ANYWHERE },
+	OperatorDef { "contains_ci ",     true,      false,      StringFilter::Position::ANYWHERE },
+	OperatorDef { "!contains_ci ",    true,      true,       StringFilter::Position::ANYWHERE },
+
+	OperatorDef { "starts_with_cs ",  false,     false,      StringFilter::Position::PREFIX },
+	OperatorDef { "!starts_with_cs ", false,     true,       StringFilter::Position::PREFIX },
+	OperatorDef { "starts_with_ci ",  true,      false,      StringFilter::Position::PREFIX },
+	OperatorDef { "!starts_with_ci ", true,      true,       StringFilter::Position::PREFIX },
+
+	OperatorDef { "eq_cs ",           false,     false,      StringFilter::Position::FULL },
+	OperatorDef { "!eq_cs ",          false,     true,       StringFilter::Position::FULL },
+	OperatorDef { "eq_ci ",           true,      false,      StringFilter::Position::FULL },
+	OperatorDef { "!eq_ci ",          true,      true,       StringFilter::Position::FULL },
+};
+
 /**
  * Parse a string operator and its second operand and convert it to a
  * #StringFilter.
@@ -199,6 +232,17 @@ ExpectQuoted(const char *&s)
 static StringFilter
 ParseStringFilter(const char *&s, bool fold_case)
 {
+	for (auto& op: operators) {
+		if (auto after_prefix = StringAfterPrefixIgnoreCase(s, op.prefix)) {
+			s = StripLeft(after_prefix);
+			return StringFilter(
+				ExpectQuoted(s),
+				op.fold_case,
+				op.position,
+				op.negated);
+		}
+	}
+
 	if (auto after_contains = StringAfterPrefixIgnoreCase(s, "contains ")) {
 		s = StripLeft(after_contains);
 		auto value = ExpectQuoted(s);