diff --git a/NEWS b/NEWS index 6ea828dfc..b7b96af42 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,7 @@ ver 0.24 (not yet released) - limit "player" idle events to the current partition - operator "starts_with" - show PCRE support in "config" response + - apply Unicode normalization to case-insensitive filter expressions * archive - add option to disable archive plugins in mpd.conf * input diff --git a/doc/protocol.rst b/doc/protocol.rst index 9d8125698..8fde63888 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -235,7 +235,9 @@ of: (album == 'BAR'))` The :command:`find` commands are case sensitive, while -:command:`search` and related commands ignore case. +:command:`search` and related commands ignore case. The latter also +applies `Unicode normalization `__ +if MPD was compiled with `ICU `__ support. Prior to MPD 0.21, the syntax looked like this:: diff --git a/src/lib/icu/Canonicalize.cxx b/src/lib/icu/Canonicalize.cxx index 8f97cb9f5..08528c592 100644 --- a/src/lib/icu/Canonicalize.cxx +++ b/src/lib/icu/Canonicalize.cxx @@ -25,7 +25,7 @@ #include "util/AllocatedString.hxx" #ifdef HAVE_ICU -#include "FoldCase.hxx" +#include "Normalize.hxx" #include "Util.hxx" #include "util/AllocatedArray.hxx" #include "util/SpanCast.hxx" @@ -39,10 +39,11 @@ try { if (u.data() == nullptr) return {src}; - if (fold_case) - if (auto folded = IcuFoldCase(ToStringView(std::span{u})); - folded != nullptr) - u = std::move(folded); + if (auto n = fold_case + ? IcuNormalizeCaseFold(ToStringView(std::span{u})) + : IcuNormalize(ToStringView(std::span{u})); + n != nullptr) + u = std::move(n); return UCharToUTF8(ToStringView(std::span{u})); #else diff --git a/test/TestStringFilter.cxx b/test/TestStringFilter.cxx index 80ac38122..4b526759b 100644 --- a/test/TestStringFilter.cxx +++ b/test/TestStringFilter.cxx @@ -97,6 +97,10 @@ TEST_F(StringFilterTest, Latin) const StringFilter f{"nëedlé", false, false, false, false}; EXPECT_TRUE(f.Match("nëedlé")); +#if defined(HAVE_ICU) || defined(_WIN32) + EXPECT_TRUE(f.Match("nëedl\u00e9")); + // TODO EXPECT_TRUE(f.Match("nëedl\u0065\u0301")); +#endif EXPECT_FALSE(f.Match("NËEDLÉ")); EXPECT_FALSE(f.Match("needlé")); EXPECT_FALSE(f.Match("néedlé")); @@ -109,13 +113,47 @@ TEST_F(StringFilterTest, Latin) EXPECT_FALSE(f.Match("FOOnëedleBAR")); } +#if defined(HAVE_ICU) || defined(_WIN32) + +TEST_F(StringFilterTest, Normalize) +{ + const StringFilter f{"1①H", true, false, false, false}; + + EXPECT_TRUE(f.Match("1①H")); + EXPECT_TRUE(f.Match("¹₁H")); + EXPECT_TRUE(f.Match("①1ℌ")); + EXPECT_TRUE(f.Match("①1ℍ")); + EXPECT_FALSE(f.Match("21H")); + +#ifndef _WIN32 + // fails with Windows CompareStringEx() + EXPECT_TRUE(StringFilter("dž", true, false, false, false).Match("dž")); +#endif + + EXPECT_TRUE(StringFilter("\u212b", true, false, false, false).Match("\u0041\u030a")); + EXPECT_TRUE(StringFilter("\u212b", true, false, false, false).Match("\u00c5")); + + EXPECT_TRUE(StringFilter("\u1e69", true, false, false, false).Match("\u0073\u0323\u0307")); + +#ifndef _WIN32 + // fails with Windows CompareStringEx() + EXPECT_TRUE(StringFilter("\u1e69", true, false, false, false).Match("\u0073\u0307\u0323")); +#endif +} + +#endif + TEST_F(StringFilterTest, FoldCase) { const StringFilter f{"nëedlé", true, false, false, false}; EXPECT_TRUE(f.Match("nëedlé")); #if defined(HAVE_ICU) || defined(_WIN32) + EXPECT_TRUE(f.Match("nëedl\u00e9")); + EXPECT_TRUE(f.Match("nëedl\u0065\u0301")); EXPECT_TRUE(f.Match("NËEDLÉ")); + EXPECT_TRUE(f.Match("NËEDL\u00c9")); + EXPECT_TRUE(f.Match("NËEDL\u0045\u0301")); #endif EXPECT_FALSE(f.Match("needlé")); EXPECT_FALSE(f.Match("néedlé"));