song/StringFilter: support regular expressions with "=~" and "!~"

This feature requires `libpcre`.
This commit is contained in:
Max Kellermann 2018-11-07 00:28:15 +01:00
parent fee75dc766
commit 72184dccfc
13 changed files with 294 additions and 1 deletions

2
NEWS
View File

@ -1,4 +1,6 @@
ver 0.21.2 (not yet released) ver 0.21.2 (not yet released)
* protocol
- operator "=~" matches a regular expression
* decoder * decoder
- ffmpeg: require FFmpeg 3.1 or later - ffmpeg: require FFmpeg 3.1 or later
- ffmpeg: fix broken sound with certain codecs - ffmpeg: fix broken sound with certain codecs

View File

@ -157,6 +157,11 @@ of:
and are case-sensitive; the `search` and are case-sensitive; the `search`
commands specify a sub string and ignore case. 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 - ``(file == 'VALUE')``: match the full song URI
(relative to the music directory). (relative to the music directory).

View File

@ -67,6 +67,7 @@ For example, the following installs a fairly complete list of build dependencies
.. code-block:: none .. code-block:: none
apt install g++ \ apt install g++ \
libpcre3-dev \
libmad0-dev libmpg123-dev libid3tag0-dev \ libmad0-dev libmpg123-dev libid3tag0-dev \
libflac-dev libvorbis-dev libopus-dev \ libflac-dev libvorbis-dev libopus-dev \
libadplug-dev libaudiofile-dev libsndfile1-dev libfaad-dev \ libadplug-dev libaudiofile-dev libsndfile1-dev libfaad-dev \

View File

@ -315,6 +315,7 @@ subdir('src/lib/gcrypt')
subdir('src/lib/wrap') subdir('src/lib/wrap')
subdir('src/lib/nfs') subdir('src/lib/nfs')
subdir('src/lib/oss') subdir('src/lib/oss')
subdir('src/lib/pcre')
subdir('src/lib/pulse') subdir('src/lib/pulse')
subdir('src/lib/sndio') subdir('src/lib/sndio')
subdir('src/lib/sqlite') subdir('src/lib/sqlite')

View File

@ -176,6 +176,7 @@ option('expat', type: 'feature', description: 'Expat XML support')
option('icu', type: 'feature', description: 'Use libicu for Unicode') option('icu', type: 'feature', description: 'Use libicu for Unicode')
option('iconv', type: 'feature', description: 'Use iconv() for character set conversion') option('iconv', type: 'feature', description: 'Use iconv() for character set conversion')
option('libwrap', type: 'feature', description: 'libwrap support') 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('sqlite', type: 'feature', description: 'SQLite database support (for stickers)')
option('yajl', type: 'feature', description: 'libyajl for YAML support') option('yajl', type: 'feature', description: 'libyajl for YAML support')
option('zlib', type: 'feature', description: 'zlib support (for database compression)') option('zlib', type: 'feature', description: 'zlib support (for database compression)')

View File

@ -0,0 +1,66 @@
/*
* Copyright 2007-2018 Content Management AG
* All rights reserved.
*
* author: Max Kellermann <mk@cm4all.com>
*
* 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 <pcre.h>
#include <array>
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<int, 16> ovector;
return pcre_exec(re, extra, s.data, s.size,
0, 0, &ovector.front(), ovector.size()) >= 0;
}
};
#endif

View File

@ -0,0 +1,71 @@
/*
* Copyright 2007-2018 Content Management AG
* All rights reserved.
*
* author: Max Kellermann <mk@cm4all.com>
*
* 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;
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2007-2018 Content Management AG
* All rights reserved.
*
* author: Max Kellermann <mk@cm4all.com>
*
* 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 <algorithm>
#include <pcre.h>
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<RegexPointer>(*this, src);
return *this;
}
/**
* Throws std::runtime_error on error.
*/
void Compile(const char *pattern, bool anchored, bool capture,
bool caseless);
};
#endif

21
src/lib/pcre/meson.build Normal file
View File

@ -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,
],
)

View File

@ -207,6 +207,20 @@ static StringFilter
ParseStringFilter(const char *&s, bool fold_case) ParseStringFilter(const char *&s, bool fold_case)
{ {
bool negated = false; 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<UniqueRegex>(f.GetValue().c_str(),
false, false,
fold_case));
return f;
}
#endif
if (s[0] == '!' && s[1] == '=') if (s[0] == '!' && s[1] == '=')
negated = true; negated = true;
else if (s[0] != '=' || s[1] != '=') else if (s[0] != '=' || s[1] != '=')

View File

@ -31,6 +31,11 @@ StringFilter::MatchWithoutNegation(const char *s) const noexcept
assert(s != nullptr); assert(s != nullptr);
#endif #endif
#ifdef HAVE_PCRE
if (regex)
return regex->Match(s);
#endif
if (fold_case) { if (fold_case) {
return substring return substring
? fold_case.IsIn(s) ? fold_case.IsIn(s)

View File

@ -23,7 +23,12 @@
#include "lib/icu/Compare.hxx" #include "lib/icu/Compare.hxx"
#include "util/Compiler.h" #include "util/Compiler.h"
#ifdef HAVE_PCRE
#include "lib/pcre/UniqueRegex.hxx"
#endif
#include <string> #include <string>
#include <memory>
class StringFilter { class StringFilter {
std::string value; std::string value;
@ -33,6 +38,10 @@ class StringFilter {
*/ */
IcuCompare fold_case; IcuCompare fold_case;
#ifdef HAVE_PCRE
std::shared_ptr<UniqueRegex> regex;
#endif
/** /**
* Search for substrings instead of matching the whole string? * Search for substrings instead of matching the whole string?
*/ */
@ -53,6 +62,21 @@ public:
return value.empty(); return value.empty();
} }
bool IsRegex() const noexcept {
#ifdef HAVE_PCRE
return !!regex;
#else
return false;
#endif
}
#ifdef HAVE_PCRE
template<typename R>
void SetRegex(R &&_regex) noexcept {
regex = std::forward<R>(_regex);
}
#endif
const auto &GetValue() const noexcept { const auto &GetValue() const noexcept {
return value; return value;
} }
@ -70,7 +94,9 @@ public:
} }
const char *GetOperator() const noexcept { const char *GetOperator() const noexcept {
return negated ? "!=" : "=="; return IsRegex()
? (negated ? "!~" : "=~")
: (negated ? "!=" : "==");
} }
gcc_pure gcc_pure

View File

@ -19,6 +19,7 @@ song_dep = declare_dependency(
link_with: song, link_with: song,
dependencies: [ dependencies: [
icu_dep, icu_dep,
pcre_dep,
tag_dep, tag_dep,
util_dep, util_dep,
], ],