diff --git a/NEWS b/NEWS index 91a932e78..cece79a95 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,6 @@ ver 0.21.2 (not yet released) +* protocol + - operator "=~" matches a regular expression * decoder - ffmpeg: require FFmpeg 3.1 or later - ffmpeg: fix broken sound with certain codecs diff --git a/doc/protocol.rst b/doc/protocol.rst index b14f12f93..27364b821 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -157,6 +157,11 @@ of: and are case-sensitive; the `search` commands specify a sub string and ignore case. +- ``(TAG =~ 'VALUE')`` and ``(TAG !~ 'VALUE')`` use a Perl-compatible + regular expression instead of doing a simple string comparison. + (This feature is only available if :program:`MPD` was compiled with + :file:`libpcre`) + - ``(file == 'VALUE')``: match the full song URI (relative to the music directory). diff --git a/doc/user.rst b/doc/user.rst index 23ed5bcc9..d7773d358 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -67,6 +67,7 @@ For example, the following installs a fairly complete list of build dependencies .. code-block:: none apt install g++ \ + libpcre3-dev \ libmad0-dev libmpg123-dev libid3tag0-dev \ libflac-dev libvorbis-dev libopus-dev \ libadplug-dev libaudiofile-dev libsndfile1-dev libfaad-dev \ diff --git a/meson.build b/meson.build index f5457a1f2..47f4b8b39 100644 --- a/meson.build +++ b/meson.build @@ -315,6 +315,7 @@ subdir('src/lib/gcrypt') subdir('src/lib/wrap') subdir('src/lib/nfs') subdir('src/lib/oss') +subdir('src/lib/pcre') subdir('src/lib/pulse') subdir('src/lib/sndio') subdir('src/lib/sqlite') diff --git a/meson_options.txt b/meson_options.txt index a30f95858..9a31e548e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -176,6 +176,7 @@ option('expat', type: 'feature', description: 'Expat XML support') option('icu', type: 'feature', description: 'Use libicu for Unicode') option('iconv', type: 'feature', description: 'Use iconv() for character set conversion') option('libwrap', type: 'feature', description: 'libwrap support') +option('pcre', type: 'feature', description: 'Enable regular expression support (using libpcre)') option('sqlite', type: 'feature', description: 'SQLite database support (for stickers)') option('yajl', type: 'feature', description: 'libyajl for YAML support') option('zlib', type: 'feature', description: 'zlib support (for database compression)') diff --git a/src/lib/pcre/RegexPointer.hxx b/src/lib/pcre/RegexPointer.hxx new file mode 100644 index 000000000..7311d0407 --- /dev/null +++ b/src/lib/pcre/RegexPointer.hxx @@ -0,0 +1,66 @@ +/* + * Copyright 2007-2018 Content Management AG + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef REGEX_POINTER_HXX +#define REGEX_POINTER_HXX + +#include "util/StringView.hxx" +#include "util/Compiler.h" + +#include + +#include + +class RegexPointer { +protected: + pcre *re = nullptr; + pcre_extra *extra = nullptr; + + unsigned n_capture = 0; + +public: + constexpr bool IsDefined() const noexcept { + return re != nullptr; + } + + gcc_pure + bool Match(StringView s) const noexcept { + /* we don't need the data written to ovector, but PCRE can + omit internal allocations if we pass a buffer to + pcre_exec() */ + std::array ovector; + return pcre_exec(re, extra, s.data, s.size, + 0, 0, &ovector.front(), ovector.size()) >= 0; + } +}; + +#endif diff --git a/src/lib/pcre/UniqueRegex.cxx b/src/lib/pcre/UniqueRegex.cxx new file mode 100644 index 000000000..4028380ab --- /dev/null +++ b/src/lib/pcre/UniqueRegex.cxx @@ -0,0 +1,71 @@ +/* + * Copyright 2007-2018 Content Management AG + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "UniqueRegex.hxx" +#include "util/RuntimeError.hxx" + +void +UniqueRegex::Compile(const char *pattern, bool anchored, bool capture, + bool caseless) +{ + constexpr int default_options = PCRE_DOTALL|PCRE_NO_AUTO_CAPTURE|PCRE_UTF8; + + int options = default_options; + if (anchored) + options |= PCRE_ANCHORED; + if (capture) + options &= ~PCRE_NO_AUTO_CAPTURE; + if (caseless) + options |= PCRE_CASELESS; + + const char *error_string; + int error_offset; + re = pcre_compile(pattern, options, &error_string, &error_offset, nullptr); + if (re == nullptr) + throw FormatRuntimeError("Error in regex at offset %d: %s", + error_offset, error_string); + + int study_options = 0; +#ifdef PCRE_CONFIG_JIT + study_options |= PCRE_STUDY_JIT_COMPILE; +#endif + extra = pcre_study(re, study_options, &error_string); + if (extra == nullptr && error_string != nullptr) { + pcre_free(re); + re = nullptr; + throw FormatRuntimeError("Regex study error: %s", error_string); + } + + int n; + if (capture && pcre_fullinfo(re, extra, PCRE_INFO_CAPTURECOUNT, &n) == 0) + n_capture = n; +} diff --git a/src/lib/pcre/UniqueRegex.hxx b/src/lib/pcre/UniqueRegex.hxx new file mode 100644 index 000000000..3be166f18 --- /dev/null +++ b/src/lib/pcre/UniqueRegex.hxx @@ -0,0 +1,79 @@ +/* + * Copyright 2007-2018 Content Management AG + * All rights reserved. + * + * author: Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef UNIQUE_REGEX_HXX +#define UNIQUE_REGEX_HXX + +#include "RegexPointer.hxx" +#include "util/StringView.hxx" + +#include + +#include + +class UniqueRegex : public RegexPointer { +public: + UniqueRegex() = default; + + UniqueRegex(const char *pattern, bool anchored, bool capture, + bool caseless) { + Compile(pattern, anchored, capture, caseless); + } + + UniqueRegex(UniqueRegex &&src) noexcept:RegexPointer(src) { + src.re = nullptr; + src.extra = nullptr; + } + + ~UniqueRegex() noexcept { + pcre_free(re); +#ifdef PCRE_CONFIG_JIT + pcre_free_study(extra); +#else + pcre_free(extra); +#endif + } + + UniqueRegex &operator=(UniqueRegex &&src) { + using std::swap; + swap(*this, src); + return *this; + } + + /** + * Throws std::runtime_error on error. + */ + void Compile(const char *pattern, bool anchored, bool capture, + bool caseless); +}; + +#endif diff --git a/src/lib/pcre/meson.build b/src/lib/pcre/meson.build new file mode 100644 index 000000000..07e83a600 --- /dev/null +++ b/src/lib/pcre/meson.build @@ -0,0 +1,21 @@ +pcre_dep = dependency('libpcre', required: get_option('pcre')) +conf.set('HAVE_PCRE', pcre_dep.found()) +if not pcre_dep.found() + subdir_done() +endif + +pcre = static_library( + 'pcre', + 'UniqueRegex.cxx', + include_directories: inc, + dependencies: [ + pcre_dep, + ], +) + +pcre_dep = declare_dependency( + link_with: pcre, + dependencies: [ + pcre_dep, + ], +) diff --git a/src/song/Filter.cxx b/src/song/Filter.cxx index 3eaf29e5f..2c47ebb59 100644 --- a/src/song/Filter.cxx +++ b/src/song/Filter.cxx @@ -207,6 +207,20 @@ static StringFilter ParseStringFilter(const char *&s, bool fold_case) { bool negated = false; + +#ifdef HAVE_PCRE + if ((s[0] == '!' || s[0] == '=') && s[1] == '~') { + negated = s[0] == '!'; + s = StripLeft(s + 2); + auto value = ExpectQuoted(s); + StringFilter f(std::move(value), fold_case, false, negated); + f.SetRegex(std::make_shared(f.GetValue().c_str(), + false, false, + fold_case)); + return f; + } +#endif + if (s[0] == '!' && s[1] == '=') negated = true; else if (s[0] != '=' || s[1] != '=') diff --git a/src/song/StringFilter.cxx b/src/song/StringFilter.cxx index 1e57b638f..ea3107f68 100644 --- a/src/song/StringFilter.cxx +++ b/src/song/StringFilter.cxx @@ -31,6 +31,11 @@ StringFilter::MatchWithoutNegation(const char *s) const noexcept assert(s != nullptr); #endif +#ifdef HAVE_PCRE + if (regex) + return regex->Match(s); +#endif + if (fold_case) { return substring ? fold_case.IsIn(s) diff --git a/src/song/StringFilter.hxx b/src/song/StringFilter.hxx index 1cc3f8998..0c7c3f5e8 100644 --- a/src/song/StringFilter.hxx +++ b/src/song/StringFilter.hxx @@ -23,7 +23,12 @@ #include "lib/icu/Compare.hxx" #include "util/Compiler.h" +#ifdef HAVE_PCRE +#include "lib/pcre/UniqueRegex.hxx" +#endif + #include +#include class StringFilter { std::string value; @@ -33,6 +38,10 @@ class StringFilter { */ IcuCompare fold_case; +#ifdef HAVE_PCRE + std::shared_ptr regex; +#endif + /** * Search for substrings instead of matching the whole string? */ @@ -53,6 +62,21 @@ public: return value.empty(); } + bool IsRegex() const noexcept { +#ifdef HAVE_PCRE + return !!regex; +#else + return false; +#endif + } + +#ifdef HAVE_PCRE + template + void SetRegex(R &&_regex) noexcept { + regex = std::forward(_regex); + } +#endif + const auto &GetValue() const noexcept { return value; } @@ -70,7 +94,9 @@ public: } const char *GetOperator() const noexcept { - return negated ? "!=" : "=="; + return IsRegex() + ? (negated ? "!~" : "=~") + : (negated ? "!=" : "=="); } gcc_pure diff --git a/src/song/meson.build b/src/song/meson.build index 71eda8d65..a51c36538 100644 --- a/src/song/meson.build +++ b/src/song/meson.build @@ -19,6 +19,7 @@ song_dep = declare_dependency( link_with: song, dependencies: [ icu_dep, + pcre_dep, tag_dep, util_dep, ],