From efe5df5e12e65d18720f0d266c3e57c83bda6fb3 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 10 Jan 2022 21:05:51 +0100 Subject: [PATCH] [daap/smartpl] Add new bison/flex parsers --- .gitignore | 1 + configure.ac | 9 + m4/ax_prog_bison.m4 | 65 +++++ m4/ax_prog_flex.m4 | 60 +++++ src/.gitignore | 3 + src/Makefile.am | 34 ++- src/daap_lexer.l | 74 ++++++ src/daap_parser.y | 508 ++++++++++++++++++++++++++++++++++++++ src/daap_query.c | 15 +- src/daap_query.gperf | 7 +- src/rsp_query.gperf | 11 +- src/smartpl_lexer.l | 192 +++++++++++++++ src/smartpl_parser.y | 567 +++++++++++++++++++++++++++++++++++++++++++ src/smartpl_query.c | 27 ++- 14 files changed, 1560 insertions(+), 13 deletions(-) create mode 100644 m4/ax_prog_bison.m4 create mode 100644 m4/ax_prog_flex.m4 create mode 100644 src/daap_lexer.l create mode 100644 src/daap_parser.y create mode 100644 src/smartpl_lexer.l create mode 100644 src/smartpl_parser.y diff --git a/.gitignore b/.gitignore index f7d3d622..1f5011af 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ missing stamp-h1 autotools-stamp build-stamp +ylwrap owntone.spec owntone.conf owntone.service diff --git a/configure.ac b/configure.ac index 9aca3b9f..ff68b4c6 100644 --- a/configure.ac +++ b/configure.ac @@ -31,6 +31,15 @@ If you modify any .gperf files, you will need to install it.]])], [AC_MSG_ERROR([[GNU gperf required, please install it.]])]) ]) +AX_PROG_FLEX([AC_DEFINE([LEX], [flex], [flex found])], + [AS_IF([test ! -f "$srcdir/src/smartpl_lexer.c"], + [AC_MSG_ERROR([flex required, please install it])]) + ]) +AX_PROG_BISON([AC_DEFINE([YACC], [bison], [GNU bison found])], + [AS_IF([test ! -f "$srcdir/src/smartpl_parser.c"], + [AC_MSG_ERROR([GNU bison required, please install it])]) + ]) + dnl Enable all warnings by default. AM_CPPFLAGS="-Wall" AC_SUBST([AM_CPPFLAGS]) diff --git a/m4/ax_prog_bison.m4 b/m4/ax_prog_bison.m4 new file mode 100644 index 00000000..c7dce00d --- /dev/null +++ b/m4/ax_prog_bison.m4 @@ -0,0 +1,65 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_prog_bison.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PROG_BISON(ACTION-IF-TRUE,ACTION-IF-FALSE) +# +# DESCRIPTION +# +# Check whether bison is the parser generator. Run ACTION-IF-TRUE if +# successful, ACTION-IF-FALSE otherwise +# +# LICENSE +# +# Copyright (c) 2009 Francesco Salvestrini +# Copyright (c) 2010 Diego Elio Petteno` +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +#serial 10 + +AC_DEFUN([AX_PROG_BISON], [ + AC_REQUIRE([AC_PROG_YACC]) + AC_REQUIRE([AC_PROG_EGREP]) + + AC_CACHE_CHECK([if bison is the parser generator],[ax_cv_prog_bison],[ + AS_IF([$YACC --version 2>/dev/null | $EGREP -q '^bison '], + [ax_cv_prog_bison=yes], [ax_cv_prog_bison=no]) + ]) + AS_IF([test "$ax_cv_prog_bison" = "yes"], [ + dnl replace the yacc-compatible compiler with the real bison, as + dnl otherwise autoconf limits us to the POSIX yacc. + dnl We also change the generated filename to the old one, so that + dnl automake's ylwrap can deal with it. + YACC="${YACC% -y} -o y.tab.c" + ] m4_ifnblank([$1], [[$1]]), + m4_ifnblank([$2], [[$2]]) + ) +]) + diff --git a/m4/ax_prog_flex.m4 b/m4/ax_prog_flex.m4 new file mode 100644 index 00000000..6d485f9d --- /dev/null +++ b/m4/ax_prog_flex.m4 @@ -0,0 +1,60 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_prog_flex.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PROG_FLEX(ACTION-IF-TRUE,ACTION-IF-FALSE) +# +# DESCRIPTION +# +# Check whether flex is the scanner generator. Run ACTION-IF-TRUE if +# successful, ACTION-IF-FALSE otherwise +# +# LICENSE +# +# Copyright (c) 2009 Francesco Salvestrini +# Copyright (c) 2010 Diego Elio Petteno` +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +#serial 13 + +AC_DEFUN([AX_PROG_FLEX], [ + AC_REQUIRE([AM_PROG_LEX]) + AC_REQUIRE([AC_PROG_EGREP]) + + AC_CACHE_CHECK([if flex is the lexer generator],[ax_cv_prog_flex],[ + AS_IF([$LEX --version 2>/dev/null | $EGREP -qw '^g?flex'], + [ax_cv_prog_flex=yes], [ax_cv_prog_flex=no]) + ]) + AS_IF([test "$ax_cv_prog_flex" = "yes"], + m4_ifnblank([$1], [[$1]]), + m4_ifnblank([$2], [[$2]]) + ) +]) + diff --git a/src/.gitignore b/src/.gitignore index 4580a8b5..931a0772 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,5 +1,8 @@ owntone +*_lexer.[ch] +*_parser.[ch] + daap_query_hash.h rsp_query_hash.h dacp_prop_hash.h diff --git a/src/Makefile.am b/src/Makefile.am index bc1962ef..591f7462 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -62,6 +62,21 @@ GPERF_FILES = \ GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h) +LEXER_SRC = daap_lexer.l smartpl_lexer.l +PARSER_SRC = daap_parser.y smartpl_parser.y + +# This flag is given to flex and tells it to produce headers. automake's ylwrap +# doesn't seem to rename the header like it does with the .c flex output, so +# here we give it the final name. The "$(@:.c=.h)" uses substitution reference +# and means 'change .c to .h in $@' (the target name, e.g. calc_lexer.c). +AM_LFLAGS = --header-file=$(@:.c=.h) + +# This flag is given to Bison and tells it to produce headers. Note that +# automake recognizes this flag too, and has special logic around it, so don't +# change it to compound arguments (so for instance no "-dv"). I'm also not sure +# --defines will work instead of -d. +AM_YFLAGS = -d + AM_CPPFLAGS += \ $(OWNTONE_CPPFLAGS) \ $(OWNTONE_OPTS_CPPFLAGS) \ @@ -130,16 +145,27 @@ owntone_SOURCES = main.c \ mxml-compat.h \ outputs/plist_wrap.h \ $(LIBWEBSOCKETS_SRC) \ - $(GPERF_SRC) + $(GPERF_SRC) \ + $(LEXER_SRC) $(PARSER_SRC) -# built by maintainers, and distributed. Clean with maintainer-clean +# This should ensure the headers are built first. automake knows how to make +# parser headers, but doesn't know how to do that for flex. So instead we set +# the C files as target, as the AM_LFLAGS will make sure headers are produced. BUILT_SOURCES = \ - $(GPERF_SRC) + $(GPERF_SRC) \ + $(LEXER_SRC:.l=.c) $(PARSER_SRC:.y=.h) +# automake doesn't know how to make lexer headers, nor does it automatically +# include them, so need to specify them as EXTRA_DIST. EXTRA_DIST = \ - $(GPERF_FILES) + $(GPERF_FILES) \ + $(LEXER_SRC:.l=.h) # gperf construction rules %_hash.h: %.gperf $(AM_V_GEN)$(GPERF) --output-file=$@ $< +# Anything built by make should be cleaned by make clean, but when it comes to +# flex/bison automake's support leaves something to be desired +clean-local: + rm -f $(LEXER_SRC:.l=.[ch]) $(PARSER_SRC:.y=.[ch]) diff --git a/src/daap_lexer.l b/src/daap_lexer.l new file mode 100644 index 00000000..a06ea202 --- /dev/null +++ b/src/daap_lexer.l @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021-2022 Espen Jürgensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* =========================== BOILERPLATE SECTION ===========================*/ + +/* This is to avoid compiler warnings about unused functions. More options are + noyyalloc noyyrealloc noyyfree. */ +%option noyywrap nounput noinput + +/* Thread safe scanner */ +%option reentrant + +/* To avoid symbol name conflicts with multiple lexers */ +%option prefix="daap_" + +/* Automake's ylwrap expexts the output to have this name */ +%option outfile="lex.yy.c" + +/* Makes a Bison-compatible yylex */ +%option bison-bridge + +%{ +#include +#include "daap_parser.h" + +/* Unknown why this is required despite using prefix */ +#define YYSTYPE DAAP_STYPE + +%} + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +re_quote ' +re_key [[:alnum:]\.\-]+ +re_value (\\.|[^'])+ +re_operator (!?[:@]) + +%x IN_CRITERIA IN_CRITERIA_VALUE + +%% + +{re_quote} { BEGIN IN_CRITERIA; return DAAP_T_QUOTE; } + +{re_key}/{re_operator} { yylval->str = strdup(yytext); return DAAP_T_KEY; } +{re_operator} { BEGIN IN_CRITERIA_VALUE; return (*yytext == '!' ? DAAP_T_NOT : DAAP_T_EQUAL); } +. { return *yytext; } + +\*{re_value}\*/{re_quote} { yylval->str = strdup(yytext); return DAAP_T_WILDCARD; } +{re_value}/{re_quote} { yylval->str = strdup(yytext); return DAAP_T_VALUE; } +{re_quote} { BEGIN INITIAL; return DAAP_T_QUOTE; } +. { return *yytext; } + +"+"|" " { return DAAP_T_AND; } +"," { return DAAP_T_OR; } +"\r"?"\n" { return DAAP_T_NEWLINE; } +. { return *yytext; } + +%% + diff --git a/src/daap_parser.y b/src/daap_parser.y new file mode 100644 index 00000000..1227cd88 --- /dev/null +++ b/src/daap_parser.y @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2021-2022 Espen Jürgensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* =========================== BOILERPLATE SECTION ===========================*/ + +/* No global variables and yylex has scanner as argument */ +%define api.pure true + +/* Change prefix of symbols from yy to avoid clashes with any other parsers we + may want to link */ +%define api.prefix {daap_} + +/* Gives better errors than "syntax error" */ +%define parse.error verbose + +/* Enables debug mode */ +%define parse.trace + +/* Adds output parameter to the parser */ +%parse-param {struct daap_result *result} + +/* Adds "scanner" as argument to the parses calls to yylex, which is required + when the lexer is in reentrant mode. The type is void because caller caller + shouldn't need to know about yyscan_t */ +%param {void *scanner} + +%code provides { +/* Convenience functions for caller to use instead of interfacing with lexer and + parser directly */ +int daap_lex_cb(char *input, void (*cb)(int, const char *)); +int daap_lex_parse(struct daap_result *result, const char *input); +} + +/* Implementation of the convenience function and the parsing error function + required by Bison */ +%code { + #include "daap_lexer.h" + + int daap_lex_cb(char *input, void (*cb)(int, const char *)) + { + int ret; + yyscan_t scanner; + YY_BUFFER_STATE buf; + YYSTYPE val; + + if ((ret = daap_lex_init(&scanner)) != 0) + return ret; + + buf = daap__scan_string(input, scanner); + + while ((ret = daap_lex(&val, scanner)) > 0) + cb(ret, daap_get_text(scanner)); + + daap__delete_buffer(buf, scanner); + daap_lex_destroy(scanner); + return 0; + } + + int daap_lex_parse(struct daap_result *result, const char *input) + { + YY_BUFFER_STATE buffer; + yyscan_t scanner; + int retval = -1; + int ret; + + result->errmsg[0] = '\0'; // For safety + + ret = daap_lex_init(&scanner); + if (ret != 0) + goto error_init; + + buffer = daap__scan_string(input, scanner); + if (!buffer) + goto error_buffer; + + ret = daap_parse(result, scanner); + if (ret != 0) + goto error_parse; + + retval = 0; + + error_parse: + daap__delete_buffer(buffer, scanner); + error_buffer: + daap_lex_destroy(scanner); + error_init: + return retval; + } + + void daap_error(struct daap_result *result, yyscan_t scanner, const char *msg) + { + snprintf(result->errmsg, sizeof(result->errmsg), "%s", msg); + } + +} + +/* ============ ABSTRACT SYNTAX TREE (AST) BOILERPLATE SECTION ===============*/ + +%code { + struct ast + { + int type; + struct ast *l; + struct ast *r; + void *data; + int ival; + }; + + __attribute__((unused)) static struct ast * ast_new(int type, struct ast *l, struct ast *r) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->l = l; + a->r = r; + return a; + } + + /* Note *data is expected to be freeable with regular free() */ + __attribute__((unused)) static struct ast * ast_data(int type, void *data) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->data = data; + return a; + } + + __attribute__((unused)) static struct ast * ast_int(int type, int ival) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->ival = ival; + return a; + } + + __attribute__((unused)) static void ast_free(struct ast *a) + { + if (!a) + return; + + ast_free(a->l); + ast_free(a->r); + free(a->data); + free(a); + } +} + +%destructor { free($$); } +%destructor { ast_free($$); } + + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +/* Includes required by the parser rules */ +%code top { +#include +#include +#include +#include +#include // For vsnprintf +} + +/* Dependencies, mocked or real */ +%code top { +#ifndef DEBUG_PARSER_MOCK +#include "daap_query_hash.h" +#include "db.h" +#else +struct dmap_query_field_map { + char *dmap_field; + char *db_col; + int as_int; +}; + +static struct dmap_query_field_map testdqfm_int = { "daap.testint", "f.testint", 1 }; +static struct dmap_query_field_map testdqfm_str = { "daap.teststr", "f.teststr", 0 }; + +static struct dmap_query_field_map * daap_query_field_lookup(char *tag, int len) +{ + if (strcmp(tag, testdqfm_str.dmap_field) == 0) + return &testdqfm_str; + else + return &testdqfm_int; +} + +static char * db_escape_string(const char *str) +{ + char *new = strdup(str); + char *ptr; + while ((ptr = strpbrk(new, "\\'"))) + *ptr = 'X'; + return new; +} +#endif +} + +/* Definition of struct that will hold the parsing result */ +%code requires { +struct daap_result { + char str[1024]; + int offset; + char escape_char; // Character used to escape _ and % in a LIKE result str + int err; + char errmsg[128]; +}; +} + +%code { +static int str_replace(char *s, size_t sz, const char *pattern, const char *replacement) +{ + char *ptr; + char *src; + char *dst; + size_t num; + + if (!s) + return -1; + + if (!pattern || !replacement) + return 0; + + size_t p_len = strlen(pattern); + size_t r_len = strlen(replacement); + size_t s_len = strlen(s) + 1; // Incl terminator + + ptr = s; + while ((ptr = strstr(ptr, pattern))) + { + // We will move the part of the string after the pattern from src to dst + src = ptr + p_len; + dst = ptr + r_len; + + num = s_len - (src - s); // Number of bytes w/terminator we need to move + if (dst + num > s + sz) + return -1; // Not enough room + + // Shift everything after the pattern to the right, use memmove since + // there might be an overlap + memmove(dst, src, num); + + // Write replacement, no null terminater + memcpy(ptr, replacement, r_len); + + // Advance ptr to avoid infinite looping + ptr = dst; + } + + return 0; +} + +static void sql_append(struct daap_result *result, const char *fmt, ...) +{ + va_list ap; + int remaining = sizeof(result->str) - result->offset; + int ret; + + if (remaining <= 0) + goto nospace; + + va_start(ap, fmt); + ret = vsnprintf(result->str + result->offset, remaining, fmt, ap); + va_end(ap); + if (ret < 0 || ret >= remaining) + goto nospace; + + result->offset += ret; + return; + + nospace: + snprintf(result->errmsg, sizeof(result->errmsg), "Parser output buffer too small (%lu bytes)", sizeof(result->str)); + result->err = -2; +} + +static bool clause_is_always_true(bool is_equal, const char *key, const char *val) +{ + // This rule is carried over from the old parser, not sure of the background + if (is_equal && (strcmp(key, "daap.songalbumid") == 0) && val && val[0] == '0') + return true; + + return false; +} + +static bool clause_is_always_false(bool is_equal, const char *key, const char *val) +{ + // The server makes sure there always is an artist/album, so something like + // 'daap.songartist:' is always false + if (strcmp(key, "daap.songalbumartist") == 0 || strcmp(key, "daap.songartist") == 0 || strcmp(key, "daap.songalbum") == 0) + return !val; + // The server never has any media type 32, so ignore to improve select query + if ((strcmp(key, "com.apple.itunes.mediakind") == 0 || strcmp(key, "com.apple.itunes.extended-media-kind") == 0) && val && (strcmp(val, "32") == 0)) + return true; + + return false; +} + +// Switches the daap '*' to '%', and escapes any '%' or '_' that might be in the +// string +static void sql_like_escape(char **value, char *escape_char) +{ + char *s = *value; + size_t len = strlen(s); + char *new; + + if (len < 2) + return; // Shouldn't ever happen since lexer should give strings w/wildcards + + // Fast path, nothing to escape + if (!strpbrk(s, "_%")) + { + s[0] = s[len - 1] = '%'; + return; + } + + len = 2 * len; // Enough for every char to be escaped + new = realloc(s, len); + str_replace(new, len, "%", "\\%"); + str_replace(new, len, "_", "\\_"); + new[0] = new[strlen(new) - 1] = '%'; + *escape_char = '\\'; + *value = new; +} + +static void sql_str_escape(char **value) +{ + char *old = *value; + *value = db_escape_string(old); + free(old); +} + +static void sql_append_dmap_clause(struct daap_result *result, struct ast *a) +{ + const struct dmap_query_field_map *dqfm; + struct ast *k = a->l; + struct ast *v = a->r; + bool is_equal = (a->type == DAAP_T_EQUAL); + char *key; + + if (!k || k->type != DAAP_T_KEY || !(key = (char *)k->data)) + { + snprintf(result->errmsg, sizeof(result->errmsg), "Missing key in dmap input"); + result->err = -3; + return; + } + else if (!v || (v->type != DAAP_T_VALUE && v->type != DAAP_T_WILDCARD)) // NULL is ok + { + snprintf(result->errmsg, sizeof(result->errmsg), "Missing value in dmap input"); + result->err = -3; + return; + } + + if (clause_is_always_true(is_equal, key, (char *)v->data)) + { + sql_append(result, is_equal ? "(1 = 1)" : "(1 = 0)"); + return; + } + else if (clause_is_always_false(is_equal, key, (char *)v->data)) + { + sql_append(result, is_equal ? "(1 = 0)" : "(1 = 1)"); + return; + } + + dqfm = daap_query_field_lookup(key, strlen(key)); + if (!dqfm) + { + snprintf(result->errmsg, sizeof(result->errmsg), "Could not map dmap input field '%s' to a db column", key); + result->err = -4; + return; + } + + if (!dqfm->as_int && !v->data) + { + // If it is a string and there is no value we select for '' OR NULL + sql_append(result, "(%s %s ''", dqfm->db_col, is_equal ? "=" : "<>"); + sql_append(result, is_equal ? " OR " : " AND "); + sql_append(result, "%s %s NULL)", dqfm->db_col, is_equal ? "IS" : "IS NOT"); + return; + } + else if (!dqfm->as_int && v->type == DAAP_T_WILDCARD) + { + sql_like_escape((char **)&v->data, &result->escape_char); + sql_str_escape((char **)&v->data); + sql_append(result, "%s", dqfm->db_col); + sql_append(result, is_equal ? " LIKE " : " NOT LIKE "); + sql_append(result, "'%s'", (char *)v->data); + return; + } + else if (!v->data) + { + snprintf(result->errmsg, sizeof(result->errmsg), "Missing value for int field '%s'", key); + result->err = -5; + return; + } + + sql_append(result, "%s", dqfm->db_col); + sql_append(result, is_equal ? " = " : " <> "); + if (!dqfm->as_int) + { + sql_str_escape((char **)&v->data); + sql_append(result, "'%s'", (char *)v->data); + return; + } + + sql_append(result, "%s", (char *)v->data); +} + +/* Creates the parsing result from the AST */ +static void sql_from_ast(struct daap_result *result, struct ast *a) { + if (!a || result->err < 0) + return; + + switch (a->type) + { + case DAAP_T_OR: + case DAAP_T_AND: + sql_from_ast(result, a->l); + sql_append(result, a->type == DAAP_T_OR ? " OR " : " AND "); + sql_from_ast(result, a->r); + break; + case DAAP_T_EQUAL: + case DAAP_T_NOT: + sql_append_dmap_clause(result, a); // Special handling due to many special rules + break; + case DAAP_T_PARENS: + sql_append(result, "("); + sql_from_ast(result, a->l); + sql_append(result, ")"); + break; + default: + snprintf(result->errmsg, sizeof(result->errmsg), "Parser produced unrecognized AST type %d", a->type); + result->err = -1; + } +} + +static int result_set(struct daap_result *result, struct ast *a) +{ + memset(result, 0, sizeof(struct daap_result)); + sql_from_ast(result, a); + ast_free(a); + if (result->escape_char) + sql_append(result, " ESCAPE '%c'", result->escape_char); + return result->err; +} +} + +%union { + char *str; + int ival; + struct ast *ast; +} + +%token DAAP_T_KEY +%token DAAP_T_VALUE +%token DAAP_T_WILDCARD + +%token DAAP_T_EQUAL +%token DAAP_T_NOT +%token DAAP_T_QUOTE +%token DAAP_T_PARENS +%token DAAP_T_NEWLINE + +%left DAAP_T_AND DAAP_T_OR + +%type expr +%type bool + +%% + +query: + expr { return result_set(result, $1); } +| expr DAAP_T_NEWLINE { return result_set(result, $1); } +; + +expr: + expr DAAP_T_AND expr { $$ = ast_new(DAAP_T_AND, $1, $3); } +| expr DAAP_T_OR expr { $$ = ast_new(DAAP_T_OR, $1, $3); } +| '(' expr ')' { $$ = ast_new(DAAP_T_PARENS, $2, NULL); } +; + +expr: + DAAP_T_QUOTE DAAP_T_KEY bool DAAP_T_VALUE DAAP_T_QUOTE { $$ = ast_new($3, ast_data(DAAP_T_KEY, $2), ast_data(DAAP_T_VALUE, $4)); } +| DAAP_T_QUOTE DAAP_T_KEY bool DAAP_T_QUOTE { $$ = ast_new($3, ast_data(DAAP_T_KEY, $2), ast_data(DAAP_T_VALUE, NULL)); } +| DAAP_T_QUOTE DAAP_T_KEY bool DAAP_T_WILDCARD DAAP_T_QUOTE { $$ = ast_new($3, ast_data(DAAP_T_KEY, $2), ast_data(DAAP_T_WILDCARD, $4)); } +; + +bool: + DAAP_T_EQUAL { $$ = DAAP_T_EQUAL; } +| DAAP_T_NOT { $$ = DAAP_T_NOT; } +; + +%% + diff --git a/src/daap_query.c b/src/daap_query.c index 79711363..9d1c2c87 100644 --- a/src/daap_query.c +++ b/src/daap_query.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2011 Julien BLACHE + * Copyright (C) 2021-2022 Espen Jürgensen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,13 +25,22 @@ #include #include +#include "daap_query.h" +#include "daap_parser.h" #include "logger.h" #include "misc.h" -#include "daap_query.h" char * daap_query_parse_sql(const char *daap_query) { - return NULL; + struct daap_result result; + + if (daap_lex_parse(&result, daap_query) != 0) + { + DPRINTF(E_LOG, L_DAAP, "Could not parse '%s': %s\n", daap_query, result.errmsg); + return NULL; + } + + return safe_strdup(result.str); } diff --git a/src/daap_query.gperf b/src/daap_query.gperf index 2bd87697..bc2c0d41 100644 --- a/src/daap_query.gperf +++ b/src/daap_query.gperf @@ -7,8 +7,11 @@ %define lookup-function-name daap_query_field_lookup %define slot-name dmap_field %struct-type -%omit-struct-type -struct dmap_query_field_map; +struct dmap_query_field_map { + char *dmap_field; + char *db_col; + int as_int; +}; %% "dmap.itemname", "f.title", 0 "dmap.itemid", "f.id", 1 diff --git a/src/rsp_query.gperf b/src/rsp_query.gperf index 8068adbf..aea99800 100644 --- a/src/rsp_query.gperf +++ b/src/rsp_query.gperf @@ -1,14 +1,21 @@ %language=ANSI-C %readonly-tables %enum +enum rsp_field_types { + RSP_TYPE_STRING, + RSP_TYPE_INT, + RSP_TYPE_DATE, +}; %switch=1 %compare-lengths %define hash-function-name rsp_query_field_hash %define lookup-function-name rsp_query_field_lookup %define slot-name rsp_field %struct-type -%omit-struct-type -struct rsp_query_field_map; +struct rsp_query_field_map { + char *rsp_field; + int field_type; +}; %% "id", RSP_TYPE_INT "path", RSP_TYPE_STRING diff --git a/src/smartpl_lexer.l b/src/smartpl_lexer.l new file mode 100644 index 00000000..59730f94 --- /dev/null +++ b/src/smartpl_lexer.l @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2021-2022 Espen Jürgensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* =========================== BOILERPLATE SECTION ===========================*/ + +/* This is to avoid compiler warnings about unused functions. More options are + noyyalloc noyyrealloc noyyfree. */ +%option noyywrap nounput noinput + +/* Thread safe scanner */ +%option reentrant + +/* To avoid symbol name conflicts with multiple lexers */ +%option prefix="smartpl_" + +/* Automake's ylwrap expexts the output to have this name */ +%option outfile="lex.yy.c" + +/* Makes a Bison-compatible yylex */ +%option bison-bridge + +%{ +#include +#include +#include +#include +#include "smartpl_parser.h" + +/* Unknown why this is required despite using prefix */ +#define YYSTYPE SMARTPL_STYPE +%} + + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +%{ +time_t l_converttime(int day, int month, int year); +time_t l_convertyyyymmdd(char *date); +%} + +%option case-insensitive + +quoted \"[^\"\n]*[\"\n] +yyyymmdd [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + +%% + +[\n\t ]+ /* Ignore whitespace */ +\#.*\n /* Ignore comments */ + +artist { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +album_artist { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +album { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +title { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +genre { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +composer { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +path { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +type { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +grouping { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +artist_id { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +songartistid { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } /* TODO isn't this an int? */ +songalbumid { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +codectype { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } +comment { yylval->str = strdup(yytext); return SMARTPL_T_STRTAG; } + +play_count { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +skip_count { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +rating { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +year { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +compilation { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +track { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +disc { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +bitrate { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +bits_per_sample { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +samplerate { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +song_length { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } +usermark { yylval->str = strdup(yytext); return SMARTPL_T_INTTAG; } + +time_added { yylval->str = strdup(yytext); return SMARTPL_T_DATETAG; } +time_modified { yylval->str = strdup(yytext); return SMARTPL_T_DATETAG; } +time_played { yylval->str = strdup(yytext); return SMARTPL_T_DATETAG; } +time_skipped { yylval->str = strdup(yytext); return SMARTPL_T_DATETAG; } +date_released { yylval->str = strdup(yytext); return SMARTPL_T_DATETAG; } + +track_count { yylval->str = strdup(yytext); return SMARTPL_T_GROUPTAG; } +album_count { yylval->str = strdup(yytext); return SMARTPL_T_GROUPTAG; } + +data_kind { yylval->str = strdup(yytext); return SMARTPL_T_DATAKINDTAG; } +media_kind { yylval->str = strdup(yytext); return SMARTPL_T_MEDIAKINDTAG; } + + /* TODO include db.h and use real values */ +file { yylval->ival = 0; return SMARTPL_T_DATAKIND; } +url { yylval->ival = 1; return SMARTPL_T_DATAKIND; } +spotify { yylval->ival = 2; return SMARTPL_T_DATAKIND; } +pipe { yylval->ival = 3; return SMARTPL_T_DATAKIND; } + +music { yylval->ival = 0; return SMARTPL_T_MEDIAKIND; } +movie { yylval->ival = 1; return SMARTPL_T_MEDIAKIND; } +podcast { yylval->ival = 2; return SMARTPL_T_MEDIAKIND; } +audiobook { yylval->ival = 3; return SMARTPL_T_MEDIAKIND; } +tvshow { yylval->ival = 4; return SMARTPL_T_MEDIAKIND; } + +having { return SMARTPL_T_HAVING; } +order\ by { return SMARTPL_T_ORDERBY; } +random { return SMARTPL_T_RANDOM; } +desc { return SMARTPL_T_ORDER_DESC; } +asc { return SMARTPL_T_ORDER_ASC; } +limit { return SMARTPL_T_LIMIT; } + +{yyyymmdd} { yylval->str = strdup(yytext); return SMARTPL_T_DATE; } +today { return (yylval->ival = SMARTPL_T_DATE_TODAY); } +yesterday { return (yylval->ival = SMARTPL_T_DATE_YESTERDAY); } +last\ week { return (yylval->ival = SMARTPL_T_DATE_LASTWEEK); } +last\ month { return (yylval->ival = SMARTPL_T_DATE_LASTMONTH); } +last\ year { return (yylval->ival = SMARTPL_T_DATE_LASTYEAR); } + +days? { return SMARTPL_T_DAYS; } +weeks? { return SMARTPL_T_WEEKS; } +months? { return SMARTPL_T_MONTHS; } +years? { return SMARTPL_T_YEARS; } + +ago { return (yylval->ival = SMARTPL_T_AGO); } +before { return (yylval->ival = SMARTPL_T_BEFORE); } +after { return (yylval->ival = SMARTPL_T_AFTER); } + +is { return (yylval->ival = SMARTPL_T_IS); } +includes { return (yylval->ival = SMARTPL_T_INCLUDES); } += { return (yylval->ival = SMARTPL_T_EQUALS); } + +\<= { return (yylval->ival = SMARTPL_T_LESSEQUAL); } +\< { return (yylval->ival = SMARTPL_T_LESS); } +\>= { return (yylval->ival = SMARTPL_T_GREATEREQUAL); } +\> { return (yylval->ival = SMARTPL_T_GREATER); } + +or { return SMARTPL_T_OR; } +and { return SMARTPL_T_AND; } +not { return SMARTPL_T_NOT; } + +{quoted} { yylval->str=strdup(yytext+1); + if(yylval->str[strlen(yylval->str)-1] == '"') + yylval->str[strlen(yylval->str)-1] = '\0'; + return SMARTPL_T_UNQUOTED; } + +[0-9]+ { yylval->ival=atoi(yytext); return SMARTPL_T_NUM; } + +. { return yytext[0]; } + +%% + +time_t l_convertyyyymmdd(char *date) +{ + char year[5]; + char month[3]; + char day[3]; + + memset(year, 0, sizeof(year)); + memset(month, 0, sizeof(month)); + memset(day, 0, sizeof(day)); + + strncpy(year, date, 4); + strncpy(month, date + 5, 2); + strncpy(day, date + 8, 2); + + return l_converttime(atoi(day), atoi(month), atoi(year)); +} + +time_t l_converttime(int day, int month, int year) +{ + struct tm tm; + + memset(&tm, 0, sizeof(tm)); + tm.tm_year = year - 1900; + tm.tm_mon = month-1; + tm.tm_mday = day; + + return mktime(&tm); +} diff --git a/src/smartpl_parser.y b/src/smartpl_parser.y new file mode 100644 index 00000000..7c5962ed --- /dev/null +++ b/src/smartpl_parser.y @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2021-2022 Espen Jürgensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* =========================== BOILERPLATE SECTION ===========================*/ + +/* No global variables and yylex has scanner as argument */ +%define api.pure true + +/* Change prefix of symbols from yy to avoid clashes with any other parsers we + may want to link */ +%define api.prefix {smartpl_} + +/* Gives better errors than "syntax error" */ +%define parse.error verbose + +/* Enables debug mode */ +%define parse.trace + +/* Adds output parameter to the parser */ +%parse-param {struct smartpl_result *result} + +/* Adds "scanner" as argument to the parses calls to yylex, which is required + when the lexer is in reentrant mode. The type is void because caller caller + shouldn't need to know about yyscan_t */ +%param {void *scanner} + +%code provides { +/* Convenience functions for caller to use instead of interfacing with lexer and + parser directly */ +int smartpl_lex_cb(char *input, void (*cb)(int, const char *)); +int smartpl_lex_parse(struct smartpl_result *result, const char *input); +} + +/* Implementation of the convenience function and the parsing error function + required by Bison */ +%code { + #include "smartpl_lexer.h" + + int smartpl_lex_cb(char *input, void (*cb)(int, const char *)) + { + int ret; + yyscan_t scanner; + YY_BUFFER_STATE buf; + YYSTYPE val; + + if ((ret = smartpl_lex_init(&scanner)) != 0) + return ret; + + buf = smartpl__scan_string(input, scanner); + + while ((ret = smartpl_lex(&val, scanner)) > 0) + cb(ret, smartpl_get_text(scanner)); + + smartpl__delete_buffer(buf, scanner); + smartpl_lex_destroy(scanner); + return 0; + } + + int smartpl_lex_parse(struct smartpl_result *result, const char *input) + { + YY_BUFFER_STATE buffer; + yyscan_t scanner; + int retval = -1; + int ret; + + result->errmsg[0] = '\0'; // For safety + + ret = smartpl_lex_init(&scanner); + if (ret != 0) + goto error_init; + + buffer = smartpl__scan_string(input, scanner); + if (!buffer) + goto error_buffer; + + ret = smartpl_parse(result, scanner); + if (ret != 0) + goto error_parse; + + retval = 0; + + error_parse: + smartpl__delete_buffer(buffer, scanner); + error_buffer: + smartpl_lex_destroy(scanner); + error_init: + return retval; + } + + void smartpl_error(struct smartpl_result *result, yyscan_t scanner, const char *msg) + { + snprintf(result->errmsg, sizeof(result->errmsg), "%s", msg); + } + +} + +/* ============ ABSTRACT SYNTAX TREE (AST) BOILERPLATE SECTION ===============*/ + +%code { + struct ast + { + int type; + struct ast *l; + struct ast *r; + void *data; + int ival; + }; + + __attribute__((unused)) static struct ast * ast_new(int type, struct ast *l, struct ast *r) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->l = l; + a->r = r; + return a; + } + + /* Note *data is expected to be freeable with regular free() */ + __attribute__((unused)) static struct ast * ast_data(int type, void *data) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->data = data; + return a; + } + + __attribute__((unused)) static struct ast * ast_int(int type, int ival) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->ival = ival; + return a; + } + + __attribute__((unused)) static void ast_free(struct ast *a) + { + if (!a) + return; + + ast_free(a->l); + ast_free(a->r); + free(a->data); + free(a); + } +} + +%destructor { free($$); } +%destructor { ast_free($$); } + + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +/* Includes required by the parser rules */ +%code top { +#ifndef _GNU_SOURCE +#define _GNU_SOURCE // For asprintf +#endif +#include +#include +#include +#include // For vsnprintf +#include +#include +#include + +#define INVERT_MASK 0x80000000 +} + +/* Definition of struct that will hold the parsing result */ +%code requires { +struct result_part { + char str[512]; + int offset; +}; + +struct smartpl_result { + struct result_part where_part; + struct result_part order_part; + struct result_part having_part; + char title[128]; + const char *where; // Points to where_part.str + const char *order; // Points to order_part.str + const char *having; // Points to having_part.str + int limit; + int err; + char errmsg[128]; +}; +} + +%code { +enum sql_append_type { + SQL_APPEND_OPERATOR, + SQL_APPEND_OPERATOR_STR, + SQL_APPEND_OPERATOR_LIKE, + SQL_APPEND_FIELD, + SQL_APPEND_STR, + SQL_APPEND_INT, + SQL_APPEND_ORDER, + SQL_APPEND_PARENS, + SQL_APPEND_DATE_STRFTIME, + SQL_APPEND_DATE_FIELD, +}; + +static void sql_from_ast(struct smartpl_result *, struct result_part *, struct ast *); + +static void sql_append(struct smartpl_result *result, struct result_part *part, const char *fmt, ...) +{ + va_list ap; + int remaining = sizeof(part->str) - part->offset; + int ret; + + if (remaining <= 0) + goto nospace; + + va_start(ap, fmt); + ret = vsnprintf(part->str + part->offset, remaining, fmt, ap); + va_end(ap); + if (ret < 0 || ret >= remaining) + goto nospace; + + part->offset += ret; + return; + + nospace: + snprintf(result->errmsg, sizeof(result->errmsg), "Parser output buffer too small (%lu bytes)", sizeof(part->str)); + result->err = -2; +} + +static void sql_append_recursive(struct smartpl_result *result, struct result_part *part, struct ast *a, const char *op, const char *op_not, bool is_not, enum sql_append_type append_type) +{ + switch (append_type) + { + case SQL_APPEND_OPERATOR: + sql_from_ast(result, part, a->l); + sql_append(result, part, " %s ", is_not ? op_not : op); + sql_from_ast(result, part, a->r); + break; + case SQL_APPEND_OPERATOR_STR: + sql_from_ast(result, part, a->l); + sql_append(result, part, " %s '", is_not ? op_not : op); + sql_from_ast(result, part, a->r); + sql_append(result, part, "'"); + break; + case SQL_APPEND_OPERATOR_LIKE: + sql_from_ast(result, part, a->l); + sql_append(result, part, " %s '%%", is_not ? op_not : op); + sql_from_ast(result, part, a->r); + sql_append(result, part, "%%'"); + break; + case SQL_APPEND_FIELD: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, part, "f.%s", (char *)a->data); + break; + case SQL_APPEND_STR: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, part, "%s", (char *)a->data); + break; + case SQL_APPEND_INT: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, part, "%d", a->ival); + break; + case SQL_APPEND_ORDER: + assert(a->l == NULL); + assert(a->r == NULL); + if (a->data) + sql_append(result, part, "f.%s ", (char *)a->data); + sql_append(result, part, "%s", is_not ? op_not : op); + break; + case SQL_APPEND_PARENS: + assert(a->r == NULL); + sql_append(result, part, "("); + sql_from_ast(result, part, a->l); + sql_append(result, part, ")"); + break; + case SQL_APPEND_DATE_STRFTIME: + sql_append(result, part, "strftime('%%s', datetime("); + sql_from_ast(result, part, a->l); // Appends the anchor date + sql_from_ast(result, part, a->r); // Appends interval if there is one + sql_append(result, part, "'utc'))"); + break; + case SQL_APPEND_DATE_FIELD: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, part, "'"); + if (is_not ? op_not : op) + sql_append(result, part, "%s", is_not ? op_not : op); + if (a->data) + sql_append(result, part, "%s", (char *)a->data); + sql_append(result, part, "', "); + break; + } +} + +/* Creates the parsing result from the AST. Errors are set via result->err. */ +static void sql_from_ast(struct smartpl_result *result, struct result_part *part, struct ast *a) { + if (!a || result->err < 0) + return; + + bool is_not = (a->type & INVERT_MASK); + a->type &= ~INVERT_MASK; + + switch (a->type) + { + case SMARTPL_T_EQUALS: + sql_append_recursive(result, part, a, "=", "!=", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_LESS: + sql_append_recursive(result, part, a, "<", ">=", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_LESSEQUAL: + sql_append_recursive(result, part, a, "<=", ">", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_GREATER: + sql_append_recursive(result, part, a, ">", ">=", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_GREATEREQUAL: + sql_append_recursive(result, part, a, ">=", "<", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_IS: + sql_append_recursive(result, part, a, "=", "!=", is_not, SQL_APPEND_OPERATOR_STR); break; + case SMARTPL_T_INCLUDES: + sql_append_recursive(result, part, a, "LIKE", "NOT LIKE", is_not, SQL_APPEND_OPERATOR_LIKE); break; + case SMARTPL_T_BEFORE: + sql_append_recursive(result, part, a, "<", ">=", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_AFTER: + sql_append_recursive(result, part, a, ">", "<=", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_AND: + sql_append_recursive(result, part, a, "AND", "AND NOT", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_OR: + sql_append_recursive(result, part, a, "OR", "OR NOT", is_not, SQL_APPEND_OPERATOR); break; + case SMARTPL_T_DATEEXPR: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_DATE_STRFTIME); break; + case SMARTPL_T_DATE: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_DATE_TODAY: + sql_append_recursive(result, part, a, "now', 'start of day", NULL, 0, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_DATE_YESTERDAY: + sql_append_recursive(result, part, a, "now', 'start of day', '-1 day", NULL, 0, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_DATE_LASTWEEK: + sql_append_recursive(result, part, a, "now', 'start of day', 'weekday 0', '-13 days", NULL, 0, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_DATE_LASTMONTH: + sql_append_recursive(result, part, a, "now', 'start of month', '-1 month", NULL, 0, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_DATE_LASTYEAR: + sql_append_recursive(result, part, a, "now', 'start of year', '-1 year", NULL, 0, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_INTERVAL: + sql_append_recursive(result, part, a, "-", "+", is_not, SQL_APPEND_DATE_FIELD); break; + case SMARTPL_T_UNQUOTED: + case SMARTPL_T_GROUPTAG: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_STR); break; + case SMARTPL_T_STRTAG: + case SMARTPL_T_INTTAG: + case SMARTPL_T_DATETAG: + case SMARTPL_T_DATAKINDTAG: + case SMARTPL_T_MEDIAKINDTAG: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_FIELD); break; + case SMARTPL_T_NUM: + case SMARTPL_T_DATAKIND: + case SMARTPL_T_MEDIAKIND: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_INT); break; + case SMARTPL_T_ORDERBY: + sql_append_recursive(result, part, a, "ASC", "DESC", is_not, SQL_APPEND_ORDER); break; + case SMARTPL_T_RANDOM: + sql_append_recursive(result, part, a, "random()", NULL, 0, SQL_APPEND_ORDER); break; + case SMARTPL_T_PARENS: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_PARENS); break; + default: + snprintf(result->errmsg, sizeof(result->errmsg), "Parser produced unrecognized AST type %d", a->type); + result->err = -1; + } +} + +static int result_set(struct smartpl_result *result, char *title, struct ast *criteria, struct ast *having, struct ast *order, struct ast *limit) +{ + memset(result, 0, sizeof(struct smartpl_result)); + snprintf(result->title, sizeof(result->title), "%s", title); // just silently truncated if too long + sql_from_ast(result, &result->where_part, criteria); + sql_from_ast(result, &result->having_part, having); + sql_from_ast(result, &result->order_part, order); + + result->where = result->where_part.offset ? result->where_part.str : NULL; + result->having = result->having_part.offset ? result->having_part.str : NULL; + result->order = result->order_part.offset ? result->order_part.str : NULL; + result->limit = limit ? limit->ival : 0; + + free(title); + ast_free(criteria); + ast_free(having); + ast_free(order); + ast_free(limit); + + return result->err; +} +} + +%union { + unsigned int ival; + char *str; + struct ast *ast; +} + +/* A string that was quoted. Quotes were stripped by lexer. */ +%token SMARTPL_T_UNQUOTED + +/* The semantic value holds the actual name of the field */ +%token SMARTPL_T_STRTAG +%token SMARTPL_T_INTTAG +%token SMARTPL_T_DATETAG +%token SMARTPL_T_DATAKINDTAG +%token SMARTPL_T_MEDIAKINDTAG +%token SMARTPL_T_GROUPTAG + +%token SMARTPL_T_DATEEXPR +%token SMARTPL_T_HAVING +%token SMARTPL_T_ORDERBY +%token SMARTPL_T_ORDER_ASC +%token SMARTPL_T_ORDER_DESC +%token SMARTPL_T_LIMIT +%token SMARTPL_T_RANDOM +%token SMARTPL_T_PARENS +%token SMARTPL_T_OR +%token SMARTPL_T_AND +%token SMARTPL_T_NOT + +%token SMARTPL_T_DAYS +%token SMARTPL_T_WEEKS +%token SMARTPL_T_MONTHS +%token SMARTPL_T_YEARS +%token SMARTPL_T_INTERVAL + +%token SMARTPL_T_DATE +%token SMARTPL_T_DATE_TODAY +%token SMARTPL_T_DATE_YESTERDAY +%token SMARTPL_T_DATE_LASTWEEK +%token SMARTPL_T_DATE_LASTMONTH +%token SMARTPL_T_DATE_LASTYEAR + +%token SMARTPL_T_NUM +%token SMARTPL_T_DATAKIND +%token SMARTPL_T_MEDIAKIND + +/* The below are only ival so we can set intbool, datebool and strbool via the + default rule for semantic values, i.e. $$ = $1. The semantic value (ival) is + set to the token value by the lexer. */ +%token SMARTPL_T_EQUALS +%token SMARTPL_T_LESS +%token SMARTPL_T_LESSEQUAL +%token SMARTPL_T_GREATER +%token SMARTPL_T_GREATEREQUAL +%token SMARTPL_T_IS +%token SMARTPL_T_INCLUDES +%token SMARTPL_T_BEFORE +%token SMARTPL_T_AFTER +%token SMARTPL_T_AGO + +%left SMARTPL_T_OR SMARTPL_T_AND + +%type criteria +%type predicate +%type dateexpr +%type interval +%type having +%type order +%type limit +%type time +%type daterelative +%type strbool +%type intbool +%type datebool + +%% + +playlist: + SMARTPL_T_UNQUOTED '{' criteria having order limit '}' { return result_set(result, $1, $3, $4, $5, $6); } +| SMARTPL_T_UNQUOTED '{' criteria having order '}' { return result_set(result, $1, $3, $4, $5, NULL); } +| SMARTPL_T_UNQUOTED '{' criteria having limit '}' { return result_set(result, $1, $3, $4, NULL, $5); } +| SMARTPL_T_UNQUOTED '{' criteria having '}' { return result_set(result, $1, $3, $4, NULL, NULL); } +| SMARTPL_T_UNQUOTED '{' criteria order limit '}' { return result_set(result, $1, $3, NULL, $4, $5); } +| SMARTPL_T_UNQUOTED '{' criteria order '}' { return result_set(result, $1, $3, NULL, $4, NULL); } +| SMARTPL_T_UNQUOTED '{' criteria limit '}' { return result_set(result, $1, $3, NULL, NULL, $4); } +| SMARTPL_T_UNQUOTED '{' criteria '}' { return result_set(result, $1, $3, NULL, NULL, NULL); } +; + +criteria: criteria SMARTPL_T_AND criteria { $$ = ast_new(SMARTPL_T_AND, $1, $3); } +| criteria SMARTPL_T_OR criteria { $$ = ast_new(SMARTPL_T_OR, $1, $3); } +| '(' criteria ')' { $$ = ast_new(SMARTPL_T_PARENS, $2, NULL); } +| predicate +; + +predicate: SMARTPL_T_STRTAG strbool SMARTPL_T_UNQUOTED { $$ = ast_new($2, ast_data(SMARTPL_T_STRTAG, $1), ast_data(SMARTPL_T_UNQUOTED, $3)); } +| SMARTPL_T_INTTAG intbool SMARTPL_T_NUM { $$ = ast_new($2, ast_data(SMARTPL_T_INTTAG, $1), ast_int(SMARTPL_T_NUM, $3)); } +| SMARTPL_T_DATETAG datebool dateexpr { $$ = ast_new($2, ast_data(SMARTPL_T_DATETAG, $1), $3); } +| SMARTPL_T_DATAKINDTAG SMARTPL_T_IS SMARTPL_T_DATAKIND { $$ = ast_new(SMARTPL_T_EQUALS, ast_data(SMARTPL_T_DATAKINDTAG, $1), ast_int(SMARTPL_T_DATAKIND, $3)); } +| SMARTPL_T_MEDIAKINDTAG SMARTPL_T_IS SMARTPL_T_MEDIAKIND { $$ = ast_new(SMARTPL_T_EQUALS, ast_data(SMARTPL_T_MEDIAKINDTAG, $1), ast_int(SMARTPL_T_MEDIAKIND, $3)); } +| SMARTPL_T_NOT predicate { struct ast *a = $2; a->type |= INVERT_MASK; $$ = $2; } +; + +dateexpr: SMARTPL_T_DATE { $$ = ast_new(SMARTPL_T_DATEEXPR, ast_data(SMARTPL_T_DATE, $1), NULL); } +| daterelative { $$ = ast_new(SMARTPL_T_DATEEXPR, ast_data($1, NULL), NULL); } +| interval SMARTPL_T_DATE { $$ = ast_new(SMARTPL_T_DATEEXPR, ast_data(SMARTPL_T_DATE, $2), $1); } +| interval daterelative { $$ = ast_new(SMARTPL_T_DATEEXPR, ast_data($2, NULL), $1); } +| time SMARTPL_T_AGO { $$ = ast_new(SMARTPL_T_DATEEXPR, ast_data(SMARTPL_T_DATE_TODAY, NULL), ast_data(SMARTPL_T_INTERVAL, $1)); } + +daterelative: SMARTPL_T_DATE_TODAY +| SMARTPL_T_DATE_YESTERDAY +| SMARTPL_T_DATE_LASTWEEK +| SMARTPL_T_DATE_LASTMONTH +| SMARTPL_T_DATE_LASTYEAR +; + +interval: time SMARTPL_T_BEFORE { $$ = ast_data(SMARTPL_T_INTERVAL, $1); } +| time SMARTPL_T_AFTER { $$ = ast_data(SMARTPL_T_INTERVAL | INVERT_MASK, $1); } +; + +time: SMARTPL_T_NUM SMARTPL_T_DAYS { if (asprintf(&($$), "%d days", $1) < 0) YYABORT; } +| SMARTPL_T_NUM SMARTPL_T_WEEKS { if (asprintf(&($$), "%d days", 7 * $1) < 0) YYABORT; } +| SMARTPL_T_NUM SMARTPL_T_MONTHS { if (asprintf(&($$), "%d months", $1) < 0) YYABORT; } +| SMARTPL_T_NUM SMARTPL_T_YEARS { if (asprintf(&($$), "%d years", $1) < 0) YYABORT; } +; + +having: SMARTPL_T_HAVING SMARTPL_T_GROUPTAG intbool SMARTPL_T_NUM { $$ = ast_new($3, ast_data(SMARTPL_T_GROUPTAG, $2), ast_int(SMARTPL_T_NUM, $4)); } + +order: SMARTPL_T_ORDERBY SMARTPL_T_STRTAG { $$ = ast_data(SMARTPL_T_ORDERBY, $2); } +| SMARTPL_T_ORDERBY SMARTPL_T_INTTAG { $$ = ast_data(SMARTPL_T_ORDERBY, $2); } +| SMARTPL_T_ORDERBY SMARTPL_T_DATETAG { $$ = ast_data(SMARTPL_T_ORDERBY, $2); } +| SMARTPL_T_ORDERBY SMARTPL_T_DATAKINDTAG { $$ = ast_data(SMARTPL_T_ORDERBY, $2); } +| SMARTPL_T_ORDERBY SMARTPL_T_MEDIAKINDTAG { $$ = ast_data(SMARTPL_T_ORDERBY, $2); } +| SMARTPL_T_ORDERBY SMARTPL_T_RANDOM { $$ = ast_data(SMARTPL_T_RANDOM, NULL); } +| order SMARTPL_T_ORDER_ASC { struct ast *a = $1; a->type = SMARTPL_T_ORDERBY; $$ = $1; } +| order SMARTPL_T_ORDER_DESC { struct ast *a = $1; a->type |= INVERT_MASK; $$ = $1; } +; + +limit: SMARTPL_T_LIMIT SMARTPL_T_NUM { $$ = ast_int(SMARTPL_T_LIMIT, $2); } +; + +strbool: SMARTPL_T_IS +| SMARTPL_T_INCLUDES +; + +intbool: SMARTPL_T_EQUALS +| SMARTPL_T_LESS +| SMARTPL_T_LESSEQUAL +| SMARTPL_T_GREATER +| SMARTPL_T_GREATEREQUAL +; + +datebool: SMARTPL_T_BEFORE +| SMARTPL_T_AFTER +; + +%% + diff --git a/src/smartpl_query.c b/src/smartpl_query.c index d563c1a7..a897c353 100644 --- a/src/smartpl_query.c +++ b/src/smartpl_query.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 Christian Meffert + * Copyright (C) 2021-2022 Espen Jürgensen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,6 +32,7 @@ #include #include "smartpl_query.h" +#include "smartpl_parser.h" #include "logger.h" #include "misc.h" @@ -45,7 +46,29 @@ smartpl_query_parse_file(struct smartpl *smartpl, const char *file) int smartpl_query_parse_string(struct smartpl *smartpl, const char *expression) { - return -1; + struct smartpl_result result; + + if (smartpl_lex_parse(&result, expression) != 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not parse '%s': %s\n", expression, result.errmsg); + return -1; + } + + if (!result.title || !result.where) + { + DPRINTF(E_LOG, L_SCAN, "Missing title or filter when parsing '%s'\n", expression); + return -1; + } + + free_smartpl(smartpl, 1); + + smartpl->title = strdup(result.title); + smartpl->query_where = strdup(result.where); + smartpl->having = safe_strdup(result.having); + smartpl->order = safe_strdup(result.order); + smartpl->limit = result.limit; + + return 0; }