PACKAGE_TARNAME := @PACKAGE_TARNAME@
PACKAGE_VERSION := @PACKAGE_VERSION@
+FLEX := @FLEX@
+BISON := @BISON@
M4 := @M4@
LOPSUBGEN := @LOPSUBGEN@
lls_suite_dir := $(build_dir)/lls
lls_m4_dir := m4/lls
test_dir := t
+yy_src_dir = yy
+yy_build_dir = $(build_dir)/yy
# sort removes duplicate words, which is all we need here
all_objs := $(sort $(recv_objs) $(filter_objs) $(client_objs) $(gui_objs) \
$(audiod_objs) $(audioc_objs) $(mixer_objs) $(server_objs) \
$(write_objs) $(afh_objs) $(play_objs))
deps := $(addprefix $(dep_dir)/, $(all_objs:.o=.d))
+deps += $(addprefix $(dep_dir)/, mp.bison.d mp.flex.d)
afh_objs += afh.lsg.o
audioc_objs += audioc.lsg.o
m4_lls_deps := $(addsuffix .m4d, $(suites))
lsg_h := $(addsuffix .lsg.h, $(suites))
+# flex/bison objects and headers are only needed if para_server is built
+ifeq ("$(findstring server, $(executables))", "server")
+ server_objs += mp.flex.o mp.bison.o
+ yy_h := $(yy_build_dir)/mp.bison.h
+endif
+
# now prefix all objects with object dir
recv_objs := $(addprefix $(object_dir)/, $(recv_objs))
filter_objs := $(addprefix $(object_dir)/, $(filter_objs))
include $(lls_m4_dir)/makefile
include $(test_dir)/makefile.test
+include $(yy_src_dir)/makefile
ifeq ($(findstring clean, $(MAKECMDGOALS)),)
-include $(deps)
-include $(m4_lls_deps)
endif
-$(object_dir) $(man_dir) $(dep_dir) $(m4depdir) $(lls_suite_dir):
+$(object_dir) $(man_dir) $(dep_dir) $(m4depdir) $(lls_suite_dir) \
+ $(yy_build_dir):
$(Q) $(MKDIR_P) $@
CPPFLAGS += -DBINDIR='"$(bindir)"'
CPPFLAGS += -DCC_VERSION='"$(cc_version)"'
CPPFLAGS += -I/usr/local/include
CPPFLAGS += -I$(lls_suite_dir)
+CPPFLAGS += -I$(yy_build_dir)
CPPFLAGS += $(lopsub_cppflags)
STRICT_CFLAGS += -fno-strict-aliasing
$(object_dir)/compress_filter.o: CFLAGS += -O3
-$(object_dir)/%.o: %.c | $(object_dir) $(dep_dir) $(lsg_h)
+$(object_dir)/%.o: %.c | $(object_dir) $(dep_dir) $(lsg_h) $(yy_h)
@[ -z "$(Q)" ] || echo 'CC $<'
$(Q) $(CC) -c -o $@ -MMD -MF $(dep_dir)/$(*F).d -MT $@ $(CPPFLAGS) \
$(STRICT_CFLAGS) $(CFLAGS) $<
}
/* Never fails if arg == NULL */
-static int activate_mood_or_playlist(const char *arg, int *num_admissible)
+static int activate_mood_or_playlist(const char *arg, int *num_admissible,
+ char **errmsg)
{
enum play_mode mode;
int ret;
if (!arg) {
- ret = change_current_mood(NULL); /* always successful */
+ ret = change_current_mood(NULL, NULL); /* always successful */
mode = PLAY_MODE_MOOD;
} else {
if (!strncmp(arg, "p/", 2)) {
ret = playlist_open(arg + 2);
+ if (ret < 0 && errmsg)
+ *errmsg = make_message( "could not open %s",
+ arg);
mode = PLAY_MODE_PLAYLIST;
} else if (!strncmp(arg, "m/", 2)) {
- ret = change_current_mood(arg + 2);
+ ret = change_current_mood(arg + 2, errmsg);
mode = PLAY_MODE_MOOD;
- } else
+ } else {
+ if (errmsg)
+ *errmsg = make_message("%s: parse error", arg);
return -ERRNO_TO_PARA_ERROR(EINVAL);
+ }
if (ret < 0)
return ret;
}
const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SELECT);
const char *arg;
int num_admissible, ret;
+ char *errmsg;
ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
assert(ret >= 0);
close_current_mood();
else
playlist_close();
- ret = activate_mood_or_playlist(arg, &num_admissible);
+ ret = activate_mood_or_playlist(arg, &num_admissible, &errmsg);
if (ret >= 0)
goto out;
/* ignore subsequent errors (but log them) */
+ para_printf(&aca->pbout, "%s\n", errmsg);
+ free(errmsg);
para_printf(&aca->pbout, "could not activate %s\n", arg);
if (current_mop && strcmp(current_mop, arg) != 0) {
int ret2;
para_printf(&aca->pbout, "switching back to %s\n", current_mop);
- ret2 = activate_mood_or_playlist(current_mop, &num_admissible);
+ ret2 = activate_mood_or_playlist(current_mop, &num_admissible,
+ &errmsg);
if (ret2 >= 0)
goto out;
+ para_printf(&aca->pbout, "%s\n", errmsg);
+ free(errmsg);
para_printf(&aca->pbout, "could not reactivate %s: %s\n",
current_mop, para_strerror(-ret2));
}
para_printf(&aca->pbout, "activating dummy mood\n");
- activate_mood_or_playlist(NULL, &num_admissible);
+ activate_mood_or_playlist(NULL, &num_admissible, NULL);
out:
para_printf(&aca->pbout, "activated %s (%d admissible files)\n",
current_mop? current_mop : "dummy mood", num_admissible);
static void init_admissible_files(const char *arg)
{
- int ret = activate_mood_or_playlist(arg, NULL);
+ int ret = activate_mood_or_playlist(arg, NULL, NULL);
if (ret < 0) {
assert(arg);
PARA_WARNING_LOG("could not activate %s: %s\n", arg,
para_strerror(-ret));
- activate_mood_or_playlist(NULL, NULL); /* always successful */
+ activate_mood_or_playlist(NULL, NULL, NULL);
}
}
AC_USE_SYSTEM_EXTENSIONS
AC_C_BIGENDIAN()
+AC_PATH_PROG([BISON], [bison])
+AC_PATH_PROG([FLEX], [flex])
+
AC_PATH_PROG([M4], [m4])
test -z "$M4" && AC_MSG_ERROR(
[The m4 macro processor is required to build this package])
#include <sys/types.h>
#include <sys/socket.h>
])
+################################################################### FNM_EXTMATCH
+AC_MSG_CHECKING(for extended wildcard pattern matching)
+AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
+ #include <fnmatch.h>
+]], [[
+ unsigned n = FNM_EXTMATCH;
+]])], [have_fnm_extmatch=yes], [have_fnm_extmatch=no])
+AC_MSG_RESULT($have_fnm_extmatch)
+if test $have_fnm_extmatch = yes; then
+ AC_DEFINE(HAVE_FNM_EXTMATCH, 1, define to 1 if FNM_EXTMATCH is defined)
+fi
########################################################################### curses
STASH_FLAGS
LIB_ARG_WITH([curses], [])
LIB_SUBST_FLAGS(samplerate)
UNSTASH_FLAGS
######################################################################### server
-if test -n "$CRYPTOLIB" && test $HAVE_OSL = yes; then
+if test -n "$CRYPTOLIB" && test $HAVE_OSL = yes && test -n "$BISON" && \
+ test -n "$FLEX"; then
build_server="yes"
executables="$executables server"
server_errlist_objs="
afs
aft
mood
+ mp
score
attribute
blob
PARA_ERROR(MAX_CLIENTS, "maximal number of clients exceeded"), \
PARA_ERROR(MISSING_COLON, "syntax error: missing colon"), \
PARA_ERROR(MOOD_SYNTAX, "mood syntax error"), \
+ PARA_ERROR(MOOD_PARSE, "mood parse error"), \
PARA_ERROR(MP3DEC_CORRUPT, "too many corrupt frames"), \
PARA_ERROR(MP3DEC_EOF, "mp3dec: end of file"), \
PARA_ERROR(MP3_INFO, "could not read mp3 info"), \
#include "mm.h"
#include "mood.h"
+/*
+ * Mood parser API. It's overkill to have an own header file for
+ * these declarations as they are only needed in this .c file.
+ */
+struct mp_context;
+int mp_init(const char *definition, int nbytes, struct mp_context **result,
+ char **errmsg);
+bool mp_eval_row(const struct osl_row *aft_row, struct mp_context *ctx);
+void mp_shutdown(struct mp_context *ctx);
+
/**
* Contains statistical data of the currently admissible audio files.
*
struct list_head deny_list;
/** The list of mood items of type \p score. */
struct list_head score_list;
+ /* Only used for version 2 moods. */
+ struct mp_context *parser_context;
};
/*
if (!m)
return -E_NO_MOOD;
+ if (m->parser_context) {
+ *scorep = 0;
+ return mp_eval_row(aft_row, m->parser_context);
+ }
ret = get_afsi_of_row(aft_row, &afsi);
if (ret < 0)
return ret;
list_for_each_entry_safe(item, tmp, &m->score_list, mood_item_node)
cleanup_list_entry(item);
free(m->name);
+ mp_shutdown(m->parser_context);
free(m);
}
return ret;
}
-static int load_mood(const struct osl_row *mood_row, struct mood **m)
+static int load_mood(const struct osl_row *mood_row, struct mood **m,
+ char **errmsg)
{
char *mood_name;
struct osl_object mood_def;
mlpd.m = alloc_new_mood(mood_name);
ret = for_each_line(FELF_READ_ONLY, mood_def.data, mood_def.size,
parse_mood_line, &mlpd);
- osl_close_disk_object(&mood_def);
if (ret < 0) {
- PARA_ERROR_LOG("unable to load mood %s: %s\n", mlpd.m->name,
- para_strerror(-ret));
- destroy_mood(mlpd.m);
- return ret;
+ PARA_INFO_LOG("opening version 2 mood %s\n", mlpd.m->name);
+ ret = mp_init(mood_def.data, mood_def.size, &mlpd.m->parser_context,
+ errmsg);
+ if (ret < 0)
+ destroy_mood(mlpd.m);
+ } else {
+ PARA_WARNING_LOG("loaded version 1 mood %s\n", mlpd.m->name);
+ PARA_WARNING_LOG("please convert to version 2\n");
+ ret = 1;
}
- *m = mlpd.m;
- return 1;
+ osl_close_disk_object(&mood_def);
+ if (ret >= 0)
+ *m = mlpd.m;
+ return ret;
}
static int check_mood(struct osl_row *mood_row, void *data)
}
if (!*mood_name) /* ignore dummy row */
goto out;
- para_printf(pb, "checking mood %s...\n", mood_name);
ret = for_each_line(FELF_READ_ONLY, mood_def.data, mood_def.size,
parse_mood_line, &mlpd);
- if (ret < 0)
- para_printf(pb, "mood %s: error in line %u: %s\n", mood_name,
- mlpd.line_num, para_strerror(-ret));
+ if (ret < 0) {
+ char *errmsg;
+ struct mood *m = alloc_new_mood("check");
+ ret = mp_init(mood_def.data, mood_def.size, &m->parser_context,
+ &errmsg);
+ if (ret < 0) {
+ para_printf(pb, "%s: %s\n", mood_name, errmsg);
+ free(errmsg);
+ para_printf(pb, "%s\n", para_strerror(-ret));
+ } else
+ destroy_mood(m);
+ } else {
+ para_printf(pb, "%s: v1 mood, please convert to v2\n",
+ mood_name);
+
+ }
ret = 1; /* don't fail the loop on invalid mood definitions */
out:
osl_close_disk_object(&mood_def);
* Change the current mood.
*
* \param mood_name The name of the mood to open.
+ * \param errmsg Error description is returned here.
*
* If \a mood_name is \a NULL, load the dummy mood that accepts every audio file
* and uses a scoring method based only on the \a last_played information.
*
+ * The errmsg pointer may be NULL, in which case no error message will be
+ * returned. If a non-NULL pointer is given, the caller must free *errmsg.
+ *
* If there is already an open mood, it will be closed first.
*
* \return Positive on success, negative on errors. Loading the dummy mood
* always succeeds.
*
- * \sa struct \ref afs_info::last_played.
+ * \sa struct \ref afs_info::last_played, \ref mp_eval_row().
*/
-int change_current_mood(const char *mood_name)
+int change_current_mood(const char *mood_name, char **errmsg)
{
int i, ret;
struct admissible_array aa = {
PARA_NOTICE_LOG("no such mood: %s\n", mood_name);
return ret;
}
- ret = load_mood(row, &m);
+ ret = load_mood(row, &m, errmsg);
if (ret < 0)
return ret;
close_current_mood();
if (current_mood->name)
mood_name = para_strdup(current_mood->name);
close_current_mood();
- ret = change_current_mood(mood_name);
+ ret = change_current_mood(mood_name, NULL);
free(mood_name);
return ret;
}
/** \file mood.h Public functions of mood.c. */
-int change_current_mood(const char *mood_name);
+int change_current_mood(const char *mood_name, char **errmsg);
void close_current_mood(void);
int mood_check_callback(struct afs_callback_arg *aca);
--- /dev/null
+/*
+ * Copyright (C) 2017 Andre Noll <maan@tuebingen.mpg.de>
+ *
+ * Licensed under the GPL v2. For licencing details see COPYING.
+ */
+
+/**
+ * \file mp.c Mood parser helper functions.
+ *
+ * This file contains the public and the private API of the flex/bison based
+ * mood parser.
+ *
+ * The public API (at the bottom of the file) allows to parse the same mood
+ * definition many times in an efficient manner.
+ *
+ * The first function to all is \ref mp_init(), which analyzes the given mood
+ * definition syntactically. It returns the abstract syntax tree of the mood
+ * definition and pre-compiles all regular expression patterns to make later
+ * pattern matching efficient.
+ *
+ * Semantic analysis is performed in \ref mp_eval_row(). This function is
+ * called from \ref mood.c once for each file in the audio file table. It
+ * utilizes the abstract syntax tree and the pre-compiled regular expressions
+ * to determine the set of admissible audio files.
+ *
+ * If the mood is no longer needed, \ref mp_shutdown() should be called to free
+ * the resources.
+ *
+ * The internal API is described in \ref mp.h.
+ */
+
+#include "para.h"
+
+#include <regex.h>
+#include <fnmatch.h>
+#include <osl.h>
+#include <lopsub.h>
+
+#include "string.h"
+#include "error.h"
+#include "afh.h"
+#include "afs.h"
+#include "mp.h"
+#include "mp.bison.h"
+
+struct mp_context {
+ /* global context */
+ char *errmsg;
+ struct mp_ast_node *ast;
+ /* per audio file context */
+ const struct osl_row *aft_row;
+ char *path;
+ bool have_afsi;
+ struct afs_info afsi;
+ bool have_afhi;
+ struct afh_info afhi;
+};
+
+/**
+ * Parse a (generalized) string literal.
+ *
+ * \param src The string to parse.
+ * \param quote_chars Opening and closing quote characters.
+ * \param result The corresponding C string is returned here.
+ *
+ * This function turns a generalized C99 string literal like "xyz\n" into a C
+ * string (containing the three characters 'x', 'y' and 'z', followed by a
+ * newline character and the terminating zero byte). The function allows to
+ * specify different quote characters so that, for example, regular expression
+ * patterns enclosed in '/' can be parsed as well. To parse a proper string
+ * literal, one has to pass two double quotes as the second argument.
+ *
+ * The function strips off the opening and leading quote characters, replaces
+ * double backslashes by single backslashes and handles the usual escapes like
+ * \n and \".
+ *
+ * The caller must make sure that the input is well-formed. The function simply
+ * aborts if the input is not a valid C99 string literal (modulo the quote
+ * characters).
+ *
+ * \return Offset of the first character after the closing quote. For proper
+ * string literals this will be the terminating zero byte of the input string,
+ * for regular expression patterns it is the beginning of the flags which
+ * modify the matching behaviour.
+ *
+ * \sa \ref mp_parse_regex_pattern(), \ref mp_parse_wildcard_pattern().
+ */
+unsigned parse_quoted_string(const char *src, const char quote_chars[2],
+ char **result)
+{
+ size_t n, len = strlen(src);
+ char *dst, *p;
+ bool backslash;
+
+ assert(len >= 2);
+ assert(src[0] == quote_chars[0]);
+ p = dst = para_malloc(len - 1);
+ backslash = false;
+ for (n = 1;; n++) {
+ char c;
+ assert(n < len);
+ c = src[n];
+ if (!backslash) {
+ if (c == '\\') {
+ backslash = true;
+ continue;
+ }
+ if (c == quote_chars[1])
+ break;
+ *p++ = c;
+ continue;
+ }
+ if (c == quote_chars[1])
+ *p++ = quote_chars[1];
+ else switch (c) {
+ case '\\': *p++ = '\\'; break;
+ case 'a': *p++ = '\a'; break;
+ case 'b': *p++ = '\b'; break;
+ case 'f': *p++ = '\f'; break;
+ case 'n': *p++ = '\n'; break;
+ case 'r': *p++ = '\r'; break;
+ case 't': *p++ = '\t'; break;
+ case 'v': *p++ = '\v'; break;
+ default: assert(false);
+ }
+ backslash = false;
+ }
+ assert(src[n] == quote_chars[1]);
+ *p = '\0';
+ *result = dst;
+ return n + 1;
+}
+
+/**
+ * Parse and compile an extended regular expression pattern, including flags.
+ *
+ * \param src The pattern to parse.
+ * \param result C-string and flags are returned here.
+ *
+ * A regex pattern is identical to a C99 string literal except (a) it is
+ * enclosed in '/' characters rather than double quotes, (b) double quote
+ * characters which are part of the pattern do not need to be quoted with
+ * backslashes, but slashes must be quoted in this way, and (c) the closing
+ * slash may be followed by one or more flag characters which modify the
+ * matching behaviour.
+ *
+ * The only flags which are currently supported are 'i' to ignore case in match
+ * (REG_ICASE) and 'n' to change the handling of newline characters
+ * (REG_NEWLINE).
+ *
+ * \return Standard. This function calls \ref parse_quoted_string(), hence it
+ * aborts if the input string is malformed. However, errors from \ref
+ * para_regcomp are returned without aborting the process. The rationale behind
+ * this difference is that passing a malformed string must be considered an
+ * implementation bug because malformed strings should be rejected earlier by
+ * the lexer.
+ *
+ * \sa \ref mp_parse_wildcard_pattern(), \ref parse_quoted_string(),
+ * \ref para_regcomp(), regex(3).
+ */
+int mp_parse_regex_pattern(const char *src, struct mp_re_pattern *result)
+{
+ int ret;
+ char *pat;
+ unsigned n = parse_quoted_string(src, "//", &pat);
+
+ result->flags = 0;
+ for (; src[n]; n++) {
+ switch (src[n]) {
+ case 'i': result->flags |= REG_ICASE; break;
+ case 'n': result->flags |= REG_NEWLINE; break;
+ default: assert(false);
+ }
+ }
+ ret = para_regcomp(&result->preg, pat, result->flags);
+ free(pat);
+ return ret;
+}
+
+/**
+ * Parse a wildcard pattern, including flags.
+ *
+ * \param src The pattern to parse.
+ * \param result C-string and flags are returned here.
+ *
+ * This function parses a shell wildcard pattern. It is similar to \ref
+ * mp_parse_regex_pattern(), so the remarks mentioned there apply to this
+ * function as well.
+ *
+ * Wildcard patterns differ from regular expression patterns in that (a) they
+ * must be enclosed in '|' characters, (b) they support different flags for
+ * modifying matching behaviour, and (c) there is no cache for them.
+ *
+ * The following flags, whose meaning is explained in fnmatch(3), are currently
+ * supported: 'n' (FNM_NOESCAPE), 'p' (FNM_PATHNAME), 'P' (FNM_PERIOD), 'l'
+ * (FNM_LEADING_DIR), 'i' (FNM_CASEFOLD), 'e' (FNM_EXTMATCH). The last flag is
+ * a GNU extension. It is silently ignored on non GNU systems.
+ *
+ * \sa \ref parse_quoted_string(), \ref mp_parse_regex_pattern(), fnmatch(3).
+ */
+void mp_parse_wildcard_pattern(const char *src, struct mp_wc_pattern *result)
+{
+ unsigned n = parse_quoted_string(src, "||", &result->pat);
+
+ result->flags = 0;
+ for (; src[n]; n++) {
+ switch (src[n]) {
+ case 'n': result->flags |= FNM_NOESCAPE; break;
+ case 'p': result->flags |= FNM_PATHNAME; break;
+ case 'P': result->flags |= FNM_PERIOD; break;
+ /* not POSIX, but both FreeBSD and NetBSD have it */
+ case 'l': result->flags |= FNM_LEADING_DIR; break;
+ case 'i': result->flags |= FNM_CASEFOLD; break;
+ /* GNU only */
+#ifdef HAVE_FNM_EXTMATCH
+ case 'e': result->flags |= FNM_EXTMATCH; break;
+#else /* silently ignore extglob flag */
+ case 'e': break;
+#endif
+ default: assert(false);
+ }
+ }
+}
+
+/**
+ * Set the error bit in the parser context and log a message.
+ *
+ * \param line The number of the input line which caused the error.
+ * \param ctx Contains the error bit.
+ * \param fmt Usual format string.
+ *
+ * This is called if the lexer or the parser detect an error in the mood
+ * definition. Only the first error is logged (with a severity of "warn").
+ */
+__printf_3_4 void mp_parse_error(int line, struct mp_context *ctx,
+ const char *fmt, ...)
+{
+ va_list ap;
+ char *tmp;
+
+ if (ctx->errmsg) /* we already printed an error message */
+ return;
+ va_start(ap, fmt);
+ xvasprintf(&tmp, fmt, ap);
+ va_end(ap);
+ xasprintf(&ctx->errmsg, "line %d: %s", line, tmp);
+ free(tmp);
+ PARA_WARNING_LOG("%s\n", ctx->errmsg);
+}
+
+static int get_afsi(struct mp_context *ctx)
+{
+ int ret;
+
+ if (ctx->have_afsi)
+ return 0;
+ ret = get_afsi_of_row(ctx->aft_row, &ctx->afsi);
+ if (ret < 0)
+ return ret;
+ ctx->have_afsi = true;
+ return 1;
+}
+
+static int get_afhi(struct mp_context *ctx)
+{
+ int ret;
+
+ if (ctx->have_afhi)
+ return 0;
+ ret = get_afhi_of_row(ctx->aft_row, &ctx->afhi);
+ if (ret < 0)
+ return ret;
+ ctx->have_afhi = true;
+ return 1;
+}
+
+/**
+ * Return the full path to the audio file.
+ *
+ * \param ctx Contains a reference to the row of the audio file table which
+ * corresponds to the current audio file. The path of the audio file, the
+ * afs_info and the afh_info structures (which contain the tag information) can
+ * be retrieved through this reference.
+ *
+ * \return A reference to the path. Must not be freed by the caller.
+ *
+ * \sa \ref get_audio_file_path_of_row().
+ */
+char *mp_path(struct mp_context *ctx)
+{
+ if (!ctx->path)
+ get_audio_file_path_of_row(ctx->aft_row, &ctx->path);
+ return ctx->path;
+}
+
+/**
+ * Check whether the given attribute is set for the current audio file.
+ *
+ * \param attr The string to look up in the attribute table.
+ * \param ctx See \ref mp_path().
+ *
+ * First, determine the bit number which corresponds to the attribute, then
+ * check if this bit is set in the ->attributes field of the afs_info structure
+ * of the audio file.
+ *
+ * \return True if the attribute is set, false if it is not. On errors, for
+ * example if the given string is no attribute, the function returns false.
+ *
+ * \sa \ref get_attribute_bitnum_by_name().
+ */
+bool mp_is_set(const char *attr, struct mp_context *ctx)
+{
+ int ret;
+ unsigned char bitnum;
+ const uint64_t one = 1;
+
+ ret = get_attribute_bitnum_by_name(attr, &bitnum);
+ if (ret < 0) /* treat invalid attributes as not set */
+ return false;
+ ret = get_afsi(ctx);
+ if (ret < 0)
+ return false;
+ return (one << bitnum) & ctx->afsi.attributes;
+}
+
+/**
+ * Count the number of attributes set.
+ *
+ * \param ctx See \ref mp_path().
+ *
+ * \return The number of bits which are set in the ->attributes field of the
+ * afs_info structure of the current audio file.
+ */
+int64_t mp_num_attributes_set(struct mp_context *ctx)
+{
+ const uint64_t m = ~(uint64_t)0;
+ int ret;
+ uint64_t v;
+
+ ret = get_afsi(ctx);
+ if (ret < 0)
+ return 0;
+
+ v = ctx->afsi.attributes;
+ /* taken from https://graphics.stanford.edu/~seander/bithacks.html */
+ v = v - ((v >> 1) & m / 3);
+ v = (v & m / 15 * 3) + ((v >> 2) & m / 15 * 3);
+ v = (v + (v >> 4)) & m / 255 * 15;
+ v = (v * (m / 255)) >> 56;
+ assert(v <= 64);
+ return v;
+}
+
+/**
+ * Define a function which returns a field of the afs_info structure.
+ *
+ * \param _name The name of the field.
+ *
+ * The defined function casts the value to int64_t. On errors, zero is returned.
+ */
+#define MP_AFSI(_name) \
+ int64_t mp_ ## _name(struct mp_context *ctx) \
+ { \
+ int ret = get_afsi(ctx); \
+ if (ret < 0) \
+ return 0; \
+ return ctx->afsi._name; \
+ }
+/** \cond MP_AFSI */
+MP_AFSI(num_played)
+MP_AFSI(image_id)
+MP_AFSI(lyrics_id)
+/** \endcond */
+
+/**
+ * Define a function which returns a field of the afh_info structure.
+ *
+ * \param _name The name of the field.
+ *
+ * The defined function casts the value to int64_t. On errors, zero is returned.
+ */
+#define MP_AFHI(_name) \
+ int64_t mp_ ## _name(struct mp_context *ctx) \
+ { \
+ int ret = get_afhi(ctx); \
+ if (ret < 0) \
+ return 0; \
+ return ctx->afhi._name; \
+ }
+/** \cond MP_AFHI */
+MP_AFHI(bitrate)
+MP_AFHI(frequency)
+MP_AFHI(channels)
+/** \endcond */
+
+/**
+ * Define a function which extracts and returns the value of a meta tag.
+ *
+ * \param _name The name of the tag (artist, title, ...).
+ *
+ * The function will return a pointer to memory owned by the audio file
+ * selector. On errors, or if the current audio file has no tag of the given
+ * name, the function returns the empty string. The caller must not attempt to
+ * free the returned string.
+ */
+#define MP_TAG(_name) \
+ char *mp_ ## _name (struct mp_context *ctx) \
+ { \
+ int ret = get_afhi(ctx); \
+ if (ret < 0) \
+ return ""; \
+ return ctx->afhi.tags._name; \
+ }
+/** \cond MP_TAG */
+MP_TAG(artist)
+MP_TAG(title)
+MP_TAG(album)
+MP_TAG(comment)
+/** \endcond */
+
+/**
+ * Parse and return the value of the year tag.
+ *
+ * \param ctx See \ref mp_path().
+ *
+ * \return If the year tag is not present, can not be parsed, or its value is
+ * less than zero, the function returns 0. If the value is less than 100, we
+ * add 1900.
+ */
+int64_t mp_year(struct mp_context *ctx)
+{
+ int64_t year;
+ int ret = get_afhi(ctx);
+
+ if (ret < 0)
+ return 0;
+ assert(ctx->afhi.tags.year);
+ ret = para_atoi64(ctx->afhi.tags.year, &year);
+ if (ret < 0)
+ return 0;
+ if (year < 0)
+ return 0;
+ if (year < 100)
+ year += 1900;
+ return year;
+}
+
+/*
+ * Ideally, these functions should be declared in a header file which is
+ * created by flex with the --header-file option. However, for flex-2.6.x
+ * (2017) this option is borken: if --reentrant is also given, the generated
+ * header file contains syntax errors. As a workaround we declare the functions
+ * here.
+ */
+/** \cond flex_workaround */
+int mp_yylex_init(mp_yyscan_t *yyscanner);
+struct yy_buffer_state *mp_yy_scan_bytes(const char *buf, int len,
+ mp_yyscan_t yyscanner);
+void mp_yy_delete_buffer(struct yy_buffer_state *bs, mp_yyscan_t yyscanner);
+int mp_yylex_destroy(mp_yyscan_t yyscanner);
+void mp_yyset_lineno(int lineno, mp_yyscan_t scanner);
+/** \endcond */
+
+/* Public API */
+
+/**
+ * Initialize the mood parser.
+ *
+ * This allocates and sets up the internal structures of the mood parser
+ * and creates an abstract syntax tree from the given mood definition.
+ * It must be called before \ref mp_eval_row() can be called.
+ *
+ * The context pointer returned by this function may be passed to \ref
+ * mp_eval_row() to determine whether an audio file is admissible.
+ *
+ * \param definition A reference to the mood definition.
+ * \param nbytes The size of the mood definition.
+ * \param result Opaque context pointer is returned here.
+ * \param errmsg Optional error message is returned here.
+ *
+ * It's OK to pass a NULL pointer or a zero sized buffer as the mood
+ * definition. This corresponds to the "dummy" mood for which all audio files
+ * are admissible.
+ *
+ * The error message pointer may also be NULL in which case no error message
+ * is returned. Otherwise, the caller must free the returned string.
+ *
+ * \return Standard. On success *errmsg is set to NULL.
+ */
+int mp_init(const char *definition, int nbytes, struct mp_context **result,
+ char **errmsg)
+{
+ int ret;
+ mp_yyscan_t scanner;
+ struct mp_context *ctx;
+ struct yy_buffer_state *buffer_state;
+
+ if (!definition || nbytes == 0) { /* dummy mood */
+ if (errmsg)
+ *errmsg = NULL;
+ *result = NULL;
+ return 0;
+ }
+ ctx = para_calloc(sizeof(*ctx));
+ ctx->errmsg = NULL;
+ ctx->ast = NULL;
+
+ ret = mp_yylex_init(&scanner);
+ assert(ret == 0);
+ buffer_state = mp_yy_scan_bytes(definition, nbytes, scanner);
+ mp_yyset_lineno(1, scanner);
+ PARA_NOTICE_LOG("creating abstract syntax tree\n");
+ ret = mp_yyparse(ctx, &ctx->ast, scanner);
+ mp_yy_delete_buffer(buffer_state, scanner);
+ mp_yylex_destroy(scanner);
+ if (ctx->errmsg) { /* parse error */
+ if (errmsg)
+ *errmsg = ctx->errmsg;
+ else
+ free(ctx->errmsg);
+ free(ctx);
+ return -E_MOOD_PARSE;
+ }
+ if (errmsg)
+ *errmsg = NULL;
+ *result = ctx;
+ return 1;
+}
+
+/**
+ * Determine whether the given audio file is admissible.
+ *
+ * \param aft_row The audio file to check for admissibility.
+ * \param ctx As returned from \ref mp_init().
+ *
+ * \return Whether the audio file is admissible.
+ *
+ * If the mood parser was set up without an input buffer (dummy mood), this
+ * function returns true (without looking at the audio file metadata) to
+ * indicate that the given audio file should be considered admissible.
+ *
+ * \sa \ref change_current_mood(), \ref mp_eval_ast().
+ */
+bool mp_eval_row(const struct osl_row *aft_row, struct mp_context *ctx)
+{
+ if (!ctx) /* dummy mood */
+ return true;
+ assert(aft_row);
+ ctx->aft_row = aft_row;
+ ctx->have_afsi = false;
+ ctx->have_afhi = false;
+ ctx->path = NULL;
+ return mp_eval_ast(ctx->ast, ctx);
+}
+
+/**
+ * Deallocate the resources of a mood parser.
+ *
+ * This function frees the abstract syntax tree which was created by \ref
+ * mp_init().
+ *
+ * \param ctx As returned from \ref mp_init().
+ *
+ * It's OK to pass a NULL pointer, in which case the function does nothing.
+ */
+void mp_shutdown(struct mp_context *ctx)
+{
+ if (!ctx)
+ return;
+ mp_free_ast(ctx->ast);
+ free(ctx);
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 Andre Noll <maan@tuebingen.mpg.de>
+ *
+ * Licensed under the GPL v2. For licencing details see COPYING.
+ */
+
+/**
+ * \file mp.h Internal mood parser API (backend).
+ *
+ * This header is included from the lexer, the parser, and from \ref mp.c, but
+ * not from \ref mood.c, the only user of the mood parser front end. It
+ * contains structures and function prototypes which are considered
+ * implementation details.
+ *
+ * There is one function for each keyword in the context-free grammar of the
+ * parser. These functions return the semantic value of the keyword.
+ *
+ * The functions declared here are defined either in mp.c or in mp.y.
+ */
+
+/** Opaque, only known to mp.c. Passed to the generated mp_yyparse(). */
+struct mp_context;
+
+/**
+ * Since we use a reentrant lexer, all functions generated by flex(1)
+ * receive an additional argument of this type.
+ */
+typedef void *mp_yyscan_t;
+
+/** Parsed regex pattern. */
+struct mp_re_pattern {
+ regex_t preg; /**< Pre-compiled regex. **/
+ unsigned flags; /**< Subset of the cflags described in regex(3). */
+};
+
+/** Parsed wildcard pattern. */
+struct mp_wc_pattern {
+ char *pat; /**< Unescaped C string (without quotes and flags). */
+ unsigned flags; /**< For modifying matching behaviour. */
+};
+
+/**
+ * The possible values of a node in the abstract syntax tree (AST).
+ *
+ * Constant semantic values (string literals, numeric constants, wildcard and
+ * regex patterns which are part of the mood definition) are determined during
+ * \ref mp_init() while values which depend on the audio file (path, bitrate,
+ * etc.) are determined during mp_eval_row().
+ *
+ * This union, and the \ref mp_ast_node structure below are used extensively in
+ * mp.y. However, both need to be public because the lexer must be able to
+ * create AST nodes for the constant semantic values.
+ */
+union mp_semantic_value {
+ bool boolval; /**< Comparators, =~ and =|. */
+ char *strval; /**< String literals, tags, path. */
+ int64_t intval; /**< Constants, bitrate, frequency, etc. */
+ struct mp_wc_pattern wc_pattern; /**< Right-hand side operand of =|. */
+ struct mp_re_pattern re_pattern; /**< Right-hand side operand of =~. */
+};
+
+/**
+ * Describes one node of the abstract syntax tree.
+ *
+ * A node is either interior or a leaf node. Interior nodes have at least one
+ * child while leaf nodes have a semantic value and no children.
+ *
+ * Examples: (a) STRING_LITERAL has a semantic value (the unescaped string
+ * literal) and no children, (b) NEG (unary minus) has no semantic value but
+ * one child (the numeric expression that is to be negated), (c) LESS_OR_EQUAL
+ * has no semantic value and two children (the two numeric expressions being
+ * compared).
+ */
+struct mp_ast_node {
+ /** Corresponds to a token type, for example LESS_OR_EQUAL. */
+ int id;
+ union {
+ /** Pointers to the child nodes (interior nodes only). */
+ struct mp_ast_node **children;
+ /** Leaf nodes only. */
+ union mp_semantic_value sv;
+ };
+ /**
+ * The number of children is implicitly given by the id, but we include
+ * it here to avoid having to maintain a lookup table. The AST is
+ * usually small, so we can afford to waste a byte per node.
+ */
+ uint8_t num_children;
+};
+
+/* Called from both the lexer and the parser. */
+__printf_3_4 void mp_parse_error(int line, struct mp_context *ctx,
+ const char *fmt, ...);
+
+/* Helper functions for the lexer. */
+unsigned parse_quoted_string(const char *src, const char quote_chars[2],
+ char **result);
+int mp_parse_regex_pattern(const char *src, struct mp_re_pattern *result);
+void mp_parse_wildcard_pattern(const char *src, struct mp_wc_pattern *result);
+
+/*
+ * The functions below are implemented in mp.y. They are documented here
+ * because mp.y is not doxyfied.
+ */
+
+/**
+ * Allocate a new leaf node for the abstract syntax tree.
+ *
+ * \param id Initial value for the ->id field of the new node
+ *
+ * \return Pointer to a node whose ->num_children field is initialized to zero.
+ * The caller is expected to initialize the ->sv field.
+ */
+struct mp_ast_node *mp_new_ast_leaf_node(int id);
+
+/**
+ * Evaluate an abstract syntax tree, starting at the root node.
+ *
+ * \param root As returned from \ref mp_init() via the context pointer.
+ * \param ctx Contains the aft row to evaluate.
+ *
+ * \return True if the AST evaluates to true, a non-empty string, or a
+ * non-zero number. False otherwise.
+ *
+ * \sa mp_eval_row().
+ */
+bool mp_eval_ast(struct mp_ast_node *root, struct mp_context *ctx);
+
+/**
+ * Deallocate an abstract syntax tree.
+ *
+ * This frees the memory occupied by the nodes of the AST, the child pointers
+ * of the internal nodes and the (constant) semantic values of the leaf nodes
+ * (string literals, unescaped wildcard patterns and pre-compiled regular
+ * expressions).
+ *
+ * \param root It's OK to pass NULL here.
+ */
+void mp_free_ast(struct mp_ast_node *root);
+
+/* Helper functions for the parser. */
+bool mp_is_set(const char *attr, struct mp_context *ctx);
+char *mp_path(struct mp_context *ctx);
+int64_t mp_year(struct mp_context *ctx);
+int64_t mp_num_attributes_set(struct mp_context *ctx);
+
+/* Generated with MP_AFSI() */
+/** \cond MP_AFSI */
+int64_t mp_num_played(struct mp_context *ctx);
+int64_t mp_image_id(struct mp_context *ctx);
+int64_t mp_lyrics_id(struct mp_context *ctx);
+/** \endcond */
+
+/* Generated with MP_AFHI() */
+/** \cond MP_AFHI */
+int64_t mp_bitrate(struct mp_context *ctx);
+int64_t mp_frequency(struct mp_context *ctx);
+int64_t mp_channels(struct mp_context *ctx);
+/** \endcond */
+
+/* Generated with MP_TAG() */
+/** \cond MP_TAG */
+char *mp_artist(struct mp_context *ctx);
+char *mp_title(struct mp_context *ctx);
+char *mp_album(struct mp_context *ctx);
+char *mp_comment(struct mp_context *ctx);
+/** \endcond */
to install the development package (`libssl-dev` or `libgcrypt-dev`
on debian systems) as well.
+- [flex](https://github.com/westes/flex) and
+[bison](https://www.gnu.org/software/bison) are needed to build the
+mood parser of para_server. The build system will skip para_server
+if these tools are not installed.
+
- [libmad](http://www.underbit.com/products/mad/). To compile in MP3
support for paraslash, the development package must be installed. It
is called `libmad0-dev` on debian-based systems. Note that libmad is
<h3> Moods </h3>
-A mood consists of a unique name and its *mood definition*, which is
-a set of *mood lines* containing expressions in terms of attributes
-and other data contained in the database.
-
-At any time at most one mood can be *active* which means that
-para_server is going to select only files from that subset of
-admissible files.
-
-So in order to create a mood definition one has to write a set of
-mood lines. Mood lines come in three flavours: Accept lines, deny
-lines and score lines.
-
-The general syntax of the three types of mood lines is
-
-
- accept [with score <score>] [if] [not] <mood_method> [options]
- deny [with score <score>] [if] [not] <mood_method> [options]
- score <score> [if] [not] <mood_method> [options]
-
-
-Here <score> is either an integer or the string "random" which assigns
-a random score to all matching files. The score value changes the
-order in which admissible files are going to be selected, but is of
-minor importance for this introduction.
-
-So we concentrate on the first two forms, i.e. accept and deny
-lines. As usual, everything in square brackets is optional, i.e.
-accept/deny lines take the following form when ignoring scores:
-
- accept [if] [not] <mood_method> [options]
-
-and analogously for the deny case. The "if" keyword is only syntactic
-sugar and has no function. The "not" keyword just inverts the result,
-so the essence of a mood line is the mood method part and the options
-following thereafter.
-
-A *mood method* is realized as a function which takes an audio file
-and computes a number from the data contained in the database.
-If this number is non-negative, we say the file *matches* the mood
-method. The file matches the full mood line if it either
-
- - matches the mood method and the "not" keyword is not given,
-or
- - does not match the mood method, but the "not" keyword is given.
-
-The set of admissible files for the whole mood is now defined as those
-files which match at least one accept mood line, but no deny mood line.
-More formally, an audio file F is admissible if and only if
-
- (F ~ AL1 or F ~ AL2...) and not (F ~ DL1 or F ~ DN2 ...)
-
-where AL1, AL2... are the accept lines, DL1, DL2... are the deny
-lines and "~" means "matches".
-
-The cases where no mood lines of accept/deny type are defined need
-special treatment:
-
- - Neither accept nor deny lines: This treats all files as
- admissible (in fact, that is the definition of the dummy mood
- which is activated automatically if no moods are available).
-
- - Only accept lines: A file is admissible iff it matches at
- least one accept line:
-
- F ~ AL1 or F ~ AL2 or ...
-
- - Only deny lines: A file is admissible iff it matches no
- deny line:
-
- not (F ~ DL1 or F ~ DN2 ...)
-
-
-
-<h3> List of mood_methods </h3>
-
- no_attributes_set
-
-Takes no arguments and matches an audio file if and only if no
-attributes are set.
-
- is_set <attribute_name>
-
-Takes the name of an attribute and matches iff that attribute is set.
-
- path_matches <pattern>
-
-Takes a filename pattern and matches iff the path of the audio file
-matches the pattern.
-
- artist_matches <pattern>
- album_matches <pattern>
- title_matches <pattern>
- comment_matches <pattern>
-
-Takes an extended regular expression and matches iff the text of the
-corresponding tag of the audio file matches the pattern. If the tag
-is not set, the empty string is matched against the pattern.
-
- year ~ <num>
- bitrate ~ <num>
- frequency ~ <num>
- channels ~ <num>
- num_played ~ <num>
- image_id ~ <num>
- lyrics_id ~ <num>
-
-Takes a comparator ~ of the set {<, =, <=, >, >=, !=} and a number
-<num>. Matches an audio file iff the condition <val> ~ <num> is
-satisfied where val is the corresponding value of the audio file
-(value of the year tag, bitrate in kbit/s, etc.).
-
-The year tag is special as its value is undefined if the audio file
-has no year tag or the content of the year tag is not a number. Such
-audio files never match. Another difference is the special treatment
-if the year tag is a two-digit number. In this case either 1900 or
-2000 is added to the tag value, depending on whether the number is
-greater than 2000 plus the current year.
-
+A mood consists of a unique name and a definition. The definition
+is an expression which describes which audio files are considered
+admissible. At any time at most one mood can be active, meaning
+that para_server will only stream files which are admissible for the
+active mood.
+
+The expression may refer to attributes and other metadata stored in
+the database. Expressions may be combined by means of logical and
+arithmetical operators in a natural way. Moreover, string matching
+based on regular expression or wildcard patterns is supported.
+
+The set of admissible files is determined by applying the expression
+to each audio file in turn. For a mood definition to be valid, its
+expression must evaluate to a number, a string or a boolean value
+("true" or "false"). For numbers, any value other than zero means the
+file is admissible. For strings, any non-empty string indicates an
+admissible file. For boolean values, true means admissible and false
+means not admissible. As a special case, the empty expression treats
+all files as admissible.
+
+<h3> Mood grammar </h3>
+
+Expressions are based on a context-free grammar which distinguishes
+between several types for syntactic units or groupings. The grammar
+defines a set of keywords which have a type and a corresponding
+semantic value, as shown in the following table.
+
+Keyword | Type | Semantic value
+:--------------------|--------:|:----------------------------------
+`path` | string | Full path of the current audio file
+`artist` | string | Content of the artist meta tag
+`title` | string | Content of the title meta tag
+`album` | string | Content of the album meta tag
+`comment` | string | Content of the somment meta tag
+`num_attributes_set` | integer | Number of attributes which are set
+`year` | integer | Content of the year meta tag [\*]
+`num_played` | integer | How many times the file has been streamed
+`image_id` | integer | The identifier of the (cover art) image
+`lyrics_id` | integer | The identifier of the lyrics blob
+`bitrate` | integer | The average bitrate
+`frequency` | integer | The output sample rate
+`channels` | integer | The number of channels
+`is_set("foo")` | boolean | True if attribute "foo" is set.
+
+[\*] For most audio formats, the year tag is stored as a string. It
+is converted to an integer by the mood parser. If the audio file
+has no year tag or the content of the year tag is not a number, the
+semantic value is zero. A special convention applies if the year tag
+is a one-digit or a two-digit number. In this case 1900 is added to
+the tag value.
+
+Expressions may be grouped using parentheses, logical and
+arithmetical operators or string matching operators. The following
+table lists the available operators.
+
+Token | Meaning
+:------|:-------
+`\|\|` | Logical Or
+`&&` | Logical And
+`!` | Logical Not
+`==` | Equal (can be applied to all types)
+`!=` | Not equal. Likewise
+`<` | Less than
+`<=` | Less or equal
+`>=` | Greater or equal
+`+` | Arithmetical minus
+`-` | Binary/unary minus
+`*` | Multiplication
+`/` | Division
+`=~` | Regular expression match
+`=\|` | Filename match
+
+Besides integers, strings and booleans there is an additional type
+which describes regular expression or wildcard patterns. Patterns
+are not just strings because they also include a list of flags which
+modify matching behaviour.
+
+Regular expression patterns are of the form `/pattern/[flags]`. That
+is, the pattern is delimited by slashes, and is followed by zero or
+more characters, each specifying a flag according to the following
+table
+
+Flag | POSIX name | Meaning
+:----|--------------:|--------
+`i` | `REG_ICASE` | Ignore case in match
+`n` | `REG_NEWLINE` | Treat newline as an ordinary character
+
+Note that only extended regular expression patterns are supported. See
+regex(3) for details.
+
+Wildcard patterns are similar, but the pattern must be delimited by
+`'|'` characters rather than slashes. For wildcard patterns different
+flags exist, as shown below.
+
+Flag | POSIX name | Meaning
+:----|-----------------------:|--------
+`n` | `FNM_NOESCAPE` | Treat backslash as an ordinary character
+`p` | `FNM_PATHNAME` | Match a slash only with a slash in pattern
+`P` | `FNM_PERIOD` | Leading period has to be matched exactly
+`l` | `FNM_LEADING_DIR` [\*] | Ignore "/\*" rest after successful matching
+`i` | `FNM_CASEFOLD` [\*] | Ignore case in match
+`e` | `FNM_EXTMATCH` [\*\*] | Enable extended pattern matching
+
+[\*] Not in POSIX, but both FreeBSD and NetBSD have it.
+
+[\*\*] GNU extension, silently ignored on non GNU systems.
+
+See fnmatch(3) for details.
+
+Mood definitions may contain arbitrary whitespace and comments.
+A comment is a word beginning with #. This word and all remaining
+characters of the line are ignored.
+
+<h3> Example moods </h3>
+
+* Files with no/invalid year tag: `year == 0`
+
+* Only oldies: `year != 0 && year < 1980`
+
+* Only 80's Rock or Metal: `(year >= 1980 && year < 1990) &&
+ (is_set("rock") || is_set("metal"))`
+
+* Files with incomplete tags: `artist == "" || title == "" || album =
+"" || comment == "" || year == 0`
+
+* Files with no attributes defined so far: `num_attributes_set == 0`
+
+* Only newly added files: `num_played == 0`
+
+* Only poor quality files: `bitrate < 96`
+
+* Cope with different spellings of Motörhead: `artist =~ /mot(ö|oe{0,1})rhead/i`
+
+* The same with extended wildcard patterns: `artist =| |mot+(o\|oe\|ö)rhead|ie`
<h3> Mood usage </h3>
para ls -a
-
-<h3> Example mood definition </h3>
-
-Suppose you have defined attributes "punk" and "rock" and want to define
-a mood containing only Punk-Rock songs. That is, an audio file should be
-admissible if and only if both attributes are set. Since
-
- punk and rock
-
-is obviously the same as
-
- not (not punk or not rock)
-
-(de Morgan's rule), a mood definition that selects only Punk-Rock
-songs is
-
- deny if not is_set punk
- deny if not is_set rock
-
-
-
File renames and content changes
--------------------------------
--- /dev/null
+.PRECIOUS: $(yy_build_dir)/%.flex.c $(yy_build_dir)/%.bison.c \
+ $(yy_build_dir)/%.bison.h
+
+$(yy_build_dir)/%.flex.c: $(yy_src_dir)/%.lex | $(yy_build_dir)
+ @[ -z "$(Q)" ] || echo 'FLEX $<'
+ @$(FLEX) -o $@ $<
+
+$(yy_build_dir)/%.bison.c $(yy_build_dir)/%.bison.h: $(yy_src_dir)/%.y \
+ | $(yy_build_dir)
+ @[ -z "$(Q)" ] || echo 'BISON $<'
+ @$(BISON) --defines=$(yy_build_dir)/$(notdir $(<:.y=.bison.h)) \
+ --output=$(yy_build_dir)/$(notdir $(<:.y=.bison.c)) $<
+
+$(object_dir)/%.o: $(yy_build_dir)/%.c | $(object_dir)
+ @[ -z "$(Q)" ] || echo 'CC $<'
+ @$(Q) $(CC) -g -c -o $@ $(CPPFLAGS) -MMD -MF $(dep_dir)/$(*F).d \
+ -MT $@ -iquote . -Wno-unused-macros $<
--- /dev/null
+/*
+ * Copyright (C) 2017 Andre Noll <maan@tuebingen.mpg.de>
+ *
+ * Licensed under the GPL v2. For licencing details see COPYING.
+ */
+
+ /*
+ * Since we do not supply yywrap(), we use noyywrap to instruct the scanner to
+ * behave as though yywrap() returned 1.
+ */
+%option noyywrap
+
+ /*
+ * We don't want symbols to clash with those of other flex users, particularly
+ * lopsub.
+ */
+%option prefix="mp_yy"
+
+ /*
+ * Generate a scanner that maintains the number of the current line read from
+ * its input in the yylineno variable.
+ */
+%option yylineno
+
+ /* Generate a bison-compatible scanner. */
+%option bison-bridge bison-locations
+
+ /*
+ * Warn (in particular) if the default rule can be matched but no default rule
+ * has been given.
+ */
+%option warn
+
+ /*
+ * Generate a scanner which is portable and safe to use in one or more threads
+ * of control.
+ */
+%option reentrant
+
+ /*
+ * Generate a scanner which always looks one extra character ahead. This is a
+ * bit faster than an interactive scanner for which look ahead happens only
+ * when necessary.
+ */
+%option never-interactive
+
+%{
+#include <regex.h>
+#include "para.h"
+#include "string.h"
+#include "mp.h"
+#include "error.h"
+
+#define YYSTYPE MP_YYSTYPE
+#define YYLTYPE MP_YYLTYPE
+#define YY_DECL int mp_yylex(MP_YYSTYPE *yylval_param, MP_YYLTYPE *yylloc_param, \
+ struct mp_context *ctx, struct mp_ast_node **ast, mp_yyscan_t yyscanner)
+#include "mp.bison.h"
+#define MP_YY_USER_ACTION do {mp_yylloc->first_line = mp_yylineno;} while (0);
+%}
+DECIMAL_CONSTANT (0|([[:digit:]]{-}[0])[[:digit:]]*)
+STRING_LITERAL \"([^\"\\\n]|(\\[\"\\abfnrtv]))*\"
+REGEX_PATTERN \/([^\/\\\n]|(\\[\/\\abfnrtv]))*\/([in])*
+WILDCARD_PATTERN \|([^\|\\\n]|(\\[\|\\abfnrtv]))*\|([npPlie])*
+%%
+
+is_set {return IS_SET;}
+num_attributes_set {return NUM_ATTRIBUTES_SET;}
+path {return PATH;}
+artist {return ARTIST;}
+title {return TITLE;}
+album {return ALBUM;}
+comment {return COMMENT;}
+year {return YEAR;}
+num_played {return NUM_PLAYED;}
+image_id {return IMAGE_ID;}
+lyrics_id {return LYRICS_ID;}
+bitrate {return BITRATE;}
+frequency {return FREQUENCY;}
+channels {return CHANNELS;}
+true {return TRUE;}
+false {return FALSE;}
+
+[[:space:]]+|#.*\n /* skip comments and whitespace */
+
+"("|")"|","|"+"|"-"|"*"|"/"|"<"|">" {return yytext[0];}
+
+"||" {return OR;}
+"&&" {return AND;}
+"!" {return NOT;}
+"==" {return EQUAL;}
+"!=" {return NOT_EQUAL;}
+"<=" {return LESS_OR_EQUAL;}
+">=" {return GREATER_OR_EQUAL;}
+"=~" {return REGEX_MATCH;}
+"=|" {return FILENAME_MATCH;}
+
+{DECIMAL_CONSTANT} {
+ int ret;
+ yylval->node = mp_new_ast_leaf_node(NUM);
+ ret = para_atoi64(yytext, &yylval->node->sv.intval);
+ if (ret < 0) {
+ free(yylval->node);
+ mp_parse_error(yylloc->first_line, ctx, "%s: %s", yytext,
+ para_strerror(-ret));
+ return -E_MOOD_PARSE;
+ }
+ return NUM;
+}
+
+{STRING_LITERAL} {
+ yylval->node = mp_new_ast_leaf_node(STRING_LITERAL);
+ parse_quoted_string(yytext, "\"\"", &yylval->node->sv.strval);
+ //PARA_CRIT_LOG("strval: %s\n", yylval->node->sv.strval);
+ //PARA_CRIT_LOG("node: %p\n", yylval->node);
+ return STRING_LITERAL;
+}
+
+{REGEX_PATTERN} {
+ int ret;
+ yylval->node = mp_new_ast_leaf_node(REGEX_PATTERN);
+ ret = mp_parse_regex_pattern(yytext, &yylval->node->sv.re_pattern);
+ if (ret < 0) {
+ mp_parse_error(yylloc->first_line, ctx, "%s: %s", yytext,
+ para_strerror(-ret));
+ return -E_MOOD_PARSE;
+ }
+ return REGEX_PATTERN;
+}
+
+{WILDCARD_PATTERN} {
+ yylval->node = mp_new_ast_leaf_node(WILDCARD_PATTERN);
+ mp_parse_wildcard_pattern(yytext, &yylval->node->sv.wc_pattern);
+ return WILDCARD_PATTERN;
+}
+
+. {
+ mp_parse_error(yylloc->first_line, ctx, "unrecognized text: %s",
+ yytext);
+ return -E_MOOD_PARSE;
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 Andre Noll <maan@tuebingen.mpg.de>
+ *
+ * Licensed under the GPL v2. For licencing details see COPYING.
+ */
+
+/*
+ * Provide more verbose and specific error messages instead of just "syntax
+ * error".
+ */
+%define parse.error verbose
+
+/*
+ * Verbose error messages may contain incorrect information if LAC (Lookahead
+ * Correction) is not enabled.
+ */
+%define parse.lac full
+
+/* Avoid symbol clashes (lopsub might also expose yy* symbols). */
+%define api.prefix {mp_yy}
+
+/*
+ * Although locations are automatically enabled as soon as the grammar uses the
+ * special @N tokens, specifying %locations explicitly allows for more accurate
+ * syntax error messages.
+ */
+%locations
+
+/*
+ * Generate a pure (reentrant) parser. With this option enabled, yylval and
+ * yylloc become local variables in yyparse(), and a different calling
+ * convention is used for yylex().
+ */
+%define api.pure full
+
+/* Additional arguments to yylex(), yyparse() and yyerror() */
+%param {struct mp_context *ctx}
+%param {struct mp_ast_node **ast}
+%param {mp_yyscan_t yyscanner} /* reentrant lexers */
+
+%{
+#include <fnmatch.h>
+#include <regex.h>
+
+#include "para.h"
+#include "string.h"
+#include "mp.h"
+#include "mp.bison.h"
+#include "error.h"
+
+int yylex(MP_YYSTYPE *lvalp, MP_YYLTYPE *llocp, struct mp_context *ctx,
+ struct mp_ast_node **ast, mp_yyscan_t yyscanner);
+static void yyerror(YYLTYPE *llocp, struct mp_context *ctx,
+ struct mp_ast_node **ast, mp_yyscan_t yyscanner, const char *msg);
+
+enum semantic_types {
+ ST_STRVAL,
+ ST_INTVAL,
+ ST_BOOLVAL,
+ ST_REGEX_PATTERN,
+ ST_WC_PATTERN
+};
+
+static struct mp_ast_node *ast_node_raw(int id)
+{
+ struct mp_ast_node *node = para_malloc(sizeof(struct mp_ast_node));
+ node->id = id;
+ return node;
+}
+
+/* This is non-static because it is also called from the lexer. */
+struct mp_ast_node *mp_new_ast_leaf_node(int id)
+{
+ struct mp_ast_node *node = ast_node_raw(id);
+ node->num_children = 0;
+ return node;
+}
+
+static struct mp_ast_node *ast_node_new_unary(int id, struct mp_ast_node *child)
+{
+ struct mp_ast_node *node = ast_node_raw(id);
+ node->num_children = 1;
+ node->children = para_malloc(sizeof(struct mp_ast_node *));
+ node->children[0] = child;
+ return node;
+}
+
+static struct mp_ast_node *ast_node_new_binary(int id, struct mp_ast_node *left,
+ struct mp_ast_node *right)
+{
+ struct mp_ast_node *node = ast_node_raw(id);
+ node->num_children = 2;
+ node->children = para_malloc(2 * sizeof(struct mp_ast_node *));
+ node->children[0] = left;
+ node->children[1] = right;
+ return node;
+}
+
+void mp_free_ast(struct mp_ast_node *root)
+{
+ if (!root)
+ return;
+ if (root->num_children > 0) {
+ int i;
+ for (i = 0; i < root->num_children; i++)
+ mp_free_ast(root->children[i]);
+ free(root->children);
+ } else {
+ union mp_semantic_value *sv = &root->sv;
+ switch (root->id) {
+ case STRING_LITERAL:
+ free(sv->strval);
+ break;
+ case REGEX_PATTERN:
+ regfree(&sv->re_pattern.preg);
+ break;
+ case WILDCARD_PATTERN:
+ free(sv->wc_pattern.pat);
+ break;
+ }
+ }
+ free(root);
+}
+
+static int eval_node(struct mp_ast_node *node, struct mp_context *ctx,
+ union mp_semantic_value *result);
+
+static void eval_binary_op(struct mp_ast_node *node, struct mp_context *ctx,
+ union mp_semantic_value *v1, union mp_semantic_value *v2)
+{
+ eval_node(node->children[0], ctx, v1);
+ eval_node(node->children[1], ctx, v2);
+}
+
+static int eval_node(struct mp_ast_node *node, struct mp_context *ctx,
+ union mp_semantic_value *result)
+{
+ int ret;
+ char *arg;
+ union mp_semantic_value v1, v2;
+
+ switch (node->id) {
+ /* strings */
+ case STRING_LITERAL:
+ result->strval = node->sv.strval;
+ return ST_STRVAL;
+ case PATH:
+ result->strval = mp_path(ctx);
+ return ST_STRVAL;
+ case ARTIST:
+ result->strval = mp_artist(ctx);
+ return ST_STRVAL;
+ case TITLE:
+ result->strval = mp_title(ctx);
+ return ST_STRVAL;
+ case ALBUM:
+ result->strval = mp_album(ctx);
+ return ST_STRVAL;
+ case COMMENT:
+ result->strval = mp_comment(ctx);
+ return ST_STRVAL;
+ /* integers */
+ case NUM:
+ result->intval = node->sv.intval;
+ return ST_INTVAL;
+ case '+':
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->intval = v1.intval + v2.intval;
+ return ST_INTVAL;
+ case '-':
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->intval = v1.intval - v2.intval;
+ return ST_INTVAL;
+ case '*':
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->intval = v1.intval * v2.intval;
+ return ST_INTVAL;
+ case '/':
+ eval_binary_op(node, ctx, &v1, &v2);
+ if (v2.intval == 0) {
+ static bool warned;
+ if (!warned)
+ PARA_ERROR_LOG("division by zero\n");
+ warned = true;
+ result->intval = 0;
+ } else
+ result->intval = v1.intval / v2.intval;
+ return ST_INTVAL;
+ case NEG:
+ eval_node(node->children[0], ctx, &v1);
+ result->intval = -v1.intval;
+ return ST_INTVAL;
+ case YEAR:
+ result->intval = mp_year(ctx);
+ return ST_INTVAL;
+ case NUM_ATTRIBUTES_SET:
+ result->intval = mp_num_attributes_set(ctx);
+ return ST_INTVAL;
+ case NUM_PLAYED:
+ result->intval = mp_num_played(ctx);
+ return ST_INTVAL;
+ case IMAGE_ID:
+ result->intval = mp_image_id(ctx);
+ return ST_INTVAL;
+ case LYRICS_ID:
+ result->intval = mp_lyrics_id(ctx);
+ return ST_INTVAL;
+ case BITRATE:
+ result->intval = mp_bitrate(ctx);
+ return ST_INTVAL;
+ case FREQUENCY:
+ result->intval = mp_frequency(ctx);
+ return ST_INTVAL;
+ case CHANNELS:
+ result->intval= mp_channels(ctx);
+ return ST_INTVAL;
+ /* bools */
+ case IS_SET:
+ arg = node->children[0]->sv.strval;
+ result->boolval = mp_is_set(arg, ctx);
+ return ST_BOOLVAL;
+ case TRUE:
+ result->boolval = true;
+ return ST_BOOLVAL;
+ case FALSE:
+ result->boolval = false;
+ return ST_BOOLVAL;
+ case OR:
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = v1.boolval || v2.boolval;
+ return ST_BOOLVAL;
+ case AND:
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = v1.boolval && v2.boolval;
+ return ST_BOOLVAL;
+ case NOT:
+ eval_node(node->children[0], ctx, &v1);
+ result->boolval = !v1.boolval;
+ return ST_BOOLVAL;
+ case EQUAL:
+ ret = eval_node(node->children[0], ctx, &v1);
+ eval_node(node->children[1], ctx, &v2);
+ if (ret == ST_STRVAL)
+ result->boolval = !strcmp(v1.strval, v2.strval);
+ else
+ result->boolval = v1.intval == v2.intval;
+ return ST_BOOLVAL;
+ case NOT_EQUAL:
+ ret = eval_node(node->children[0], ctx, &v1);
+ eval_node(node->children[1], ctx, &v2);
+ if (ret == ST_STRVAL)
+ result->boolval = strcmp(v1.strval, v2.strval);
+ else
+ result->boolval = v1.intval != v2.intval;
+ return ST_BOOLVAL;
+ case '<':
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = v1.intval < v2.intval;
+ return ST_BOOLVAL;
+ case '>':
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = v1.intval > v2.intval;
+ return ST_BOOLVAL;
+ case LESS_OR_EQUAL:
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = v1.intval <= v2.intval;
+ return ST_BOOLVAL;
+ case GREATER_OR_EQUAL:
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = v1.intval >= v2.intval;
+ return ST_BOOLVAL;
+ case FILENAME_MATCH:
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = fnmatch(v2.wc_pattern.pat, v1.strval,
+ v2.wc_pattern.flags) == 0;
+ return ST_BOOLVAL;
+ case REGEX_MATCH:
+ eval_binary_op(node, ctx, &v1, &v2);
+ result->boolval = regexec(&v2.re_pattern.preg, v1.strval,
+ 0, NULL, 0) == 0;
+ return ST_BOOLVAL;
+ case REGEX_PATTERN:
+ result->re_pattern = node->sv.re_pattern;
+ return ST_REGEX_PATTERN;
+ case WILDCARD_PATTERN:
+ result->wc_pattern = node->sv.wc_pattern;
+ return ST_WC_PATTERN;
+ default:
+ PARA_EMERG_LOG("bug: invalid node id %d\n", node->id);
+ exit(EXIT_FAILURE);
+ }
+}
+
+bool mp_eval_ast(struct mp_ast_node *root, struct mp_context *ctx)
+{
+ union mp_semantic_value v;
+ int ret = eval_node(root, ctx, &v);
+
+ if (ret == ST_INTVAL)
+ return v.intval != 0;
+ if (ret == ST_STRVAL)
+ return v.strval[0] != 0;
+ if (ret == ST_BOOLVAL)
+ return v.boolval;
+ assert(false);
+}
+
+%}
+
+%union {
+ struct mp_ast_node *node;
+}
+
+/* terminals */
+%token <node> NUM
+%token <node> STRING_LITERAL
+%token <node> REGEX_PATTERN
+%token <node> WILDCARD_PATTERN
+
+/* keywords with semantic value */
+%token <node> PATH
+%token <node> ARTIST
+%token <node> TITLE
+%token <node> ALBUM
+%token <node> COMMENT
+%token <node> YEAR
+%token <node> NUM_ATTRIBUTES_SET
+%token <node> NUM_PLAYED
+%token <node> IMAGE_ID
+%token <node> LYRICS_ID
+%token <node> BITRATE
+%token <node> FREQUENCY
+%token <node> CHANNELS
+%token <node> FALSE TRUE
+
+/* keywords without semantic value */
+%token IS_SET
+
+/* operators, ordered by precendence */
+%left OR
+%left AND
+%left EQUAL NOT_EQUAL
+%left LESS_THAN LESS_OR_EQUAL GREATER_OR_EQUAL REGEX_MATCH FILENAME_MATCH
+%left '-' '+'
+%left '*' '/'
+%right NOT NEG /* negation (unary minus) */
+
+/* nonterminals */
+%type <node> string
+%type <node> exp
+%type <node> boolexp
+
+%%
+
+program:
+ /* empty */ {*ast = NULL; return 0;}
+ | string {*ast = $1; return 0;}
+ | exp {*ast = $1; return 0;}
+ | boolexp {*ast = $1; return 0;}
+
+string: STRING_LITERAL {$$ = $1;}
+ | PATH {$$ = mp_new_ast_leaf_node(PATH);}
+ | ARTIST {$$ = mp_new_ast_leaf_node(ARTIST);}
+ | TITLE {$$ = mp_new_ast_leaf_node(TITLE);}
+ | ALBUM {$$ = mp_new_ast_leaf_node(ALBUM);}
+ | COMMENT {$$ = mp_new_ast_leaf_node(COMMENT);}
+;
+
+exp: NUM {$$ = $1;}
+ | exp '+' exp {$$ = ast_node_new_binary('+', $1, $3);}
+ | exp '-' exp {$$ = ast_node_new_binary('-', $1, $3);}
+ | exp '*' exp {$$ = ast_node_new_binary('*', $1, $3);}
+ | exp '/' exp {$$ = ast_node_new_binary('/', $1, $3);}
+ | '-' exp %prec NEG {$$ = ast_node_new_unary(NEG, $2);}
+ | '(' exp ')' {$$ = $2;}
+ | YEAR {$$ = mp_new_ast_leaf_node(YEAR);}
+ | NUM_ATTRIBUTES_SET {$$ = mp_new_ast_leaf_node(NUM_ATTRIBUTES_SET);}
+ | NUM_PLAYED {$$ = mp_new_ast_leaf_node(NUM_PLAYED);}
+ | IMAGE_ID {$$ = mp_new_ast_leaf_node(IMAGE_ID);}
+ | LYRICS_ID {$$ = mp_new_ast_leaf_node(LYRICS_ID);}
+ | BITRATE {$$ = mp_new_ast_leaf_node(BITRATE);}
+ | FREQUENCY {$$ = mp_new_ast_leaf_node(FREQUENCY);}
+ | CHANNELS {$$ = mp_new_ast_leaf_node(CHANNELS);}
+;
+
+boolexp: IS_SET '(' STRING_LITERAL ')' {$$ = ast_node_new_unary(IS_SET, $3);}
+ | TRUE {$$ = mp_new_ast_leaf_node(TRUE);}
+ | FALSE {$$ = mp_new_ast_leaf_node(FALSE);}
+ | '(' boolexp ')' {$$ = $2;}
+ | boolexp OR boolexp {$$ = ast_node_new_binary(OR, $1, $3);}
+ | boolexp AND boolexp {$$ = ast_node_new_binary(AND, $1, $3);}
+ | NOT boolexp {$$ = ast_node_new_unary(NOT, $2);}
+ | exp EQUAL exp {$$ = ast_node_new_binary(EQUAL, $1, $3);}
+ | exp NOT_EQUAL exp {$$ = ast_node_new_binary(NOT_EQUAL, $1, $3);}
+ | exp '<' exp {$$ = ast_node_new_binary('<', $1, $3);}
+ | exp '>' exp {$$ = ast_node_new_binary('>', $1, $3);}
+ | exp LESS_OR_EQUAL exp {
+ $$ = ast_node_new_binary(LESS_OR_EQUAL, $1, $3);
+ }
+ | exp GREATER_OR_EQUAL exp {
+ $$ = ast_node_new_binary(GREATER_OR_EQUAL, $1, $3);
+ }
+ | string REGEX_MATCH REGEX_PATTERN {
+ $$ = ast_node_new_binary(REGEX_MATCH, $1, $3);
+ }
+ | string FILENAME_MATCH WILDCARD_PATTERN {
+ $$ = ast_node_new_binary(FILENAME_MATCH, $1, $3);
+ }
+ | string EQUAL string {$$ = ast_node_new_binary(EQUAL, $1, $3);}
+ | string NOT_EQUAL string {$$ = ast_node_new_binary(NOT_EQUAL, $1, $3);}
+;
+%%
+
+/* Called by yyparse() on error */
+static void yyerror(YYLTYPE *llocp, struct mp_context *ctx,
+ struct mp_ast_node **ast, mp_yyscan_t yyscanner, const char *msg)
+{
+ mp_parse_error(llocp->first_line, ctx, "%s", msg);
+}