SongFilter: new extensible filter syntax

Will allow more complex fitler expression, such as negation (#89).
This commit is contained in:
Max Kellermann
2018-07-24 19:21:51 +02:00
parent a174159496
commit 5271e81ebe
5 changed files with 164 additions and 44 deletions

View File

@@ -22,10 +22,13 @@
#include "db/LightSong.hxx"
#include "DetachedSong.hxx"
#include "tag/ParseName.hxx"
#include "util/CharUtil.hxx"
#include "util/ChronoUtil.hxx"
#include "util/ConstBuffer.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringAPI.hxx"
#include "util/StringCompare.hxx"
#include "util/StringStrip.hxx"
#include "util/StringView.hxx"
#include "util/ASCII.hxx"
#include "util/TimeParser.hxx"
@@ -234,6 +237,99 @@ ParseTimeStamp(const char *s)
return ParseTimePoint(s, "%FT%TZ");
}
static constexpr bool
IsTagNameChar(char ch) noexcept
{
return IsAlphaASCII(ch) || ch == '_';
}
static const char *
FirstNonTagNameChar(const char *s) noexcept
{
while (IsTagNameChar(*s))
++s;
return s;
}
static auto
ExpectFilterType(const char *&s)
{
const char *end = FirstNonTagNameChar(s);
if (end == s)
throw std::runtime_error("Tag name expected");
const std::string name(s, end);
s = StripLeft(end);
const auto type = locate_parse_type(name.c_str());
if (type == TAG_NUM_OF_ITEM_TYPES)
throw FormatRuntimeError("Unknown filter type: %s",
name.c_str());
return type;
}
static constexpr bool
IsQuote(char ch) noexcept
{
return ch == '"' || ch == '\'';
}
static std::string
ExpectQuoted(const char *&s)
{
const char quote = *s++;
if (!IsQuote(quote))
throw std::runtime_error("Quoted string expected");
const char *begin = s;
const char *end = strchr(s, quote);
if (end == nullptr)
throw std::runtime_error("Closing quote not found");
s = StripLeft(end + 1);
return {begin, end};
}
const char *
SongFilter::ParseExpression(const char *s, bool fold_case)
{
assert(*s == '(');
s = StripLeft(s + 1);
if (*s == '(')
throw std::runtime_error("Nested expressions not yet implemented");
const auto type = ExpectFilterType(s);
if (type == LOCATE_TAG_MODIFIED_SINCE) {
const auto value_s = ExpectQuoted(s);
if (*s != ')')
throw std::runtime_error("')' expected");
items.emplace_back(type, ParseTimeStamp(value_s.c_str()));
return StripLeft(s + 1);
} else if (type == LOCATE_TAG_BASE_TYPE) {
auto value = ExpectQuoted(s);
if (*s != ')')
throw std::runtime_error("')' expected");
items.emplace_back(type, std::move(value), fold_case);
return StripLeft(s + 1);
} else {
if (s[0] != '=' || s[1] != '=')
throw std::runtime_error("'==' expected");
s = StripLeft(s + 2);
auto value = ExpectQuoted(s);
if (*s != ')')
throw std::runtime_error("')' expected");
items.emplace_back(type, std::move(value), fold_case);
return StripLeft(s + 1);
}
}
void
SongFilter::Parse(const char *tag_string, const char *value, bool fold_case)
{
@@ -262,6 +358,15 @@ SongFilter::Parse(ConstBuffer<const char *> args, bool fold_case)
throw std::runtime_error("Incorrect number of filter arguments");
do {
if (*args.front() == '(') {
const char *s = args.shift();
const char *end = ParseExpression(s, fold_case);
if (*end != 0)
throw std::runtime_error("Unparsed garbage after expression");
continue;
}
if (args.size < 2)
throw std::runtime_error("Incorrect number of filter arguments");

View File

@@ -142,6 +142,8 @@ public:
std::string ToExpression() const noexcept;
private:
const char *ParseExpression(const char *s, bool fold_case=false);
gcc_nonnull(2,3)
void Parse(const char *tag, const char *value, bool fold_case=false);

View File

@@ -94,7 +94,7 @@ static constexpr struct command commands[] = {
{ "config", PERMISSION_ADMIN, 0, 0, handle_config },
{ "consume", PERMISSION_CONTROL, 1, 1, handle_consume },
#ifdef ENABLE_DATABASE
{ "count", PERMISSION_READ, 2, -1, handle_count },
{ "count", PERMISSION_READ, 1, -1, handle_count },
#endif
{ "crossfade", PERMISSION_CONTROL, 1, 1, handle_crossfade },
{ "currentsong", PERMISSION_READ, 0, 0, handle_currentsong },
@@ -104,8 +104,8 @@ static constexpr struct command commands[] = {
{ "disableoutput", PERMISSION_ADMIN, 1, 1, handle_disableoutput },
{ "enableoutput", PERMISSION_ADMIN, 1, 1, handle_enableoutput },
#ifdef ENABLE_DATABASE
{ "find", PERMISSION_READ, 2, -1, handle_find },
{ "findadd", PERMISSION_ADD, 2, -1, handle_findadd},
{ "find", PERMISSION_READ, 1, -1, handle_find },
{ "findadd", PERMISSION_ADD, 1, -1, handle_findadd},
#endif
{ "idle", PERMISSION_READ, 0, -1, handle_idle },
{ "kill", PERMISSION_ADMIN, -1, -1, handle_kill },
@@ -149,11 +149,11 @@ static constexpr struct command commands[] = {
{ "playlistadd", PERMISSION_CONTROL, 2, 2, handle_playlistadd },
{ "playlistclear", PERMISSION_CONTROL, 1, 1, handle_playlistclear },
{ "playlistdelete", PERMISSION_CONTROL, 2, 2, handle_playlistdelete },
{ "playlistfind", PERMISSION_READ, 2, -1, handle_playlistfind },
{ "playlistfind", PERMISSION_READ, 1, -1, handle_playlistfind },
{ "playlistid", PERMISSION_READ, 0, 1, handle_playlistid },
{ "playlistinfo", PERMISSION_READ, 0, 1, handle_playlistinfo },
{ "playlistmove", PERMISSION_CONTROL, 3, 3, handle_playlistmove },
{ "playlistsearch", PERMISSION_READ, 2, -1, handle_playlistsearch },
{ "playlistsearch", PERMISSION_READ, 1, -1, handle_playlistsearch },
{ "plchanges", PERMISSION_READ, 1, 2, handle_plchanges },
{ "plchangesposid", PERMISSION_READ, 1, 2, handle_plchangesposid },
{ "previous", PERMISSION_CONTROL, 0, 0, handle_previous },
@@ -173,9 +173,9 @@ static constexpr struct command commands[] = {
{ "rm", PERMISSION_CONTROL, 1, 1, handle_rm },
{ "save", PERMISSION_CONTROL, 1, 1, handle_save },
#ifdef ENABLE_DATABASE
{ "search", PERMISSION_READ, 2, -1, handle_search },
{ "searchadd", PERMISSION_ADD, 2, -1, handle_searchadd },
{ "searchaddpl", PERMISSION_CONTROL, 3, -1, handle_searchaddpl },
{ "search", PERMISSION_READ, 1, -1, handle_search },
{ "searchadd", PERMISSION_ADD, 1, -1, handle_searchadd },
{ "searchaddpl", PERMISSION_CONTROL, 2, -1, handle_searchaddpl },
#endif
{ "seek", PERMISSION_CONTROL, 2, 2, handle_seek },
{ "seekcur", PERMISSION_CONTROL, 1, 1, handle_seekcur },