diff --git a/src/Makefile.am b/src/Makefile.am index c96da89c..81a1d319 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -56,14 +56,13 @@ endif GPERF_FILES = \ daap_query.gperf \ - rsp_query.gperf \ dacp_prop.gperf \ dmap_fields.gperf GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h) -LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l -PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y +LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l +PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y # 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 @@ -116,7 +115,6 @@ owntone_SOURCES = main.c \ misc.c misc.h \ misc_json.c misc_json.h \ rng.c rng.h \ - rsp_query.c rsp_query.h \ daap_query.c daap_query.h \ smartpl_query.c smartpl_query.h \ player.c player.h \ diff --git a/src/httpd_rsp.c b/src/httpd_rsp.c index f0bcb961..16c4b730 100644 --- a/src/httpd_rsp.c +++ b/src/httpd_rsp.c @@ -39,7 +39,7 @@ #include "misc.h" #include "httpd.h" #include "transcode.h" -#include "rsp_query.h" +#include "parsers/rsp_parser.h" #define RSP_VERSION "1.0" #define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?" @@ -209,9 +209,9 @@ rsp_send_error(struct evhttp_request *req, char *errmsg) static int query_params_set(struct query_params *qp, struct httpd_request *hreq) { + struct rsp_result parse_result; const char *param; char query[1024]; - char *filter; int ret; qp->offset = 0; @@ -243,6 +243,7 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq) else qp->idx_type = I_NONE; + qp->filter = NULL; param = evhttp_find_header(hreq->query, "query"); if (param) { @@ -263,19 +264,15 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq) return -1; } - qp->filter = rsp_query_parse_sql(query); - if (!qp->filter) - DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query\n"); + if (rsp_lex_parse(&parse_result, query) != 0) + DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query: %s\n", query); + else + qp->filter = safe_asprintf("(%s) AND %s", parse_result.str, rsp_filter_files); } // Always filter to include only files (not streams and Spotify) - if (qp->filter) - filter = safe_asprintf("%s AND %s", qp->filter, rsp_filter_files); - else - filter = strdup(rsp_filter_files); - - free(qp->filter); - qp->filter = filter; + if (!qp->filter) + qp->filter = strdup(rsp_filter_files); return 0; } diff --git a/src/parsers/rsp_lexer.l b/src/parsers/rsp_lexer.l new file mode 100644 index 00000000..cee6d561 --- /dev/null +++ b/src/parsers/rsp_lexer.l @@ -0,0 +1,82 @@ +/* + * 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="rsp_" + +/* 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 "rsp_parser.h" + +/* Unknown why this is required despite using prefix */ +#define YYSTYPE RSP_STYPE + +%} + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +/* quoted \"(\\.|[^\\"])*\" */ +quoted \"(\\.|[^"])+\" +yyyymmdd [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + +%% + +[\n\t ]+ /* Ignore whitespace */ + +artist { yylval->str = strdup(yytext); return RSP_T_STRTAG; } +album_artist { yylval->str = strdup(yytext); return RSP_T_STRTAG; } +album { yylval->str = strdup(yytext); return RSP_T_STRTAG; } +title { yylval->str = strdup(yytext); return RSP_T_STRTAG; } +genre { yylval->str = strdup(yytext); return RSP_T_STRTAG; } +composer { yylval->str = strdup(yytext); return RSP_T_STRTAG; } + +id { yylval->str = strdup(yytext); return RSP_T_INTTAG; } + +includes { return RSP_T_INCLUDES; } += { return RSP_T_EQUAL; } + +or { return RSP_T_OR; } +and { return RSP_T_AND; } +not { return RSP_T_NOT; } + +{quoted} { yylval->str=strdup(yytext+1); + if(yylval->str[strlen(yylval->str)-1] == '"') + yylval->str[strlen(yylval->str)-1] = '\0'; + return RSP_T_STRING; } + +[0-9]+ { yylval->ival=atoi(yytext); return RSP_T_NUM; } + +. { return yytext[0]; } + +%% + diff --git a/src/parsers/rsp_parser.y b/src/parsers/rsp_parser.y new file mode 100644 index 00000000..a6c118c8 --- /dev/null +++ b/src/parsers/rsp_parser.y @@ -0,0 +1,416 @@ +/* + * 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 {rsp_} + +/* 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 rsp_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 rsp_lex_cb(char *input, void (*cb)(int, const char *)); +int rsp_lex_parse(struct rsp_result *result, const char *input); +} + +/* Implementation of the convenience function and the parsing error function + required by Bison */ +%code { + #include "rsp_lexer.h" + + int rsp_lex_cb(char *input, void (*cb)(int, const char *)) + { + int ret; + yyscan_t scanner; + YY_BUFFER_STATE buf; + YYSTYPE val; + + if ((ret = rsp_lex_init(&scanner)) != 0) + return ret; + + buf = rsp__scan_string(input, scanner); + + while ((ret = rsp_lex(&val, scanner)) > 0) + cb(ret, rsp_get_text(scanner)); + + rsp__delete_buffer(buf, scanner); + rsp_lex_destroy(scanner); + return 0; + } + + int rsp_lex_parse(struct rsp_result *result, const char *input) + { + YY_BUFFER_STATE buffer; + yyscan_t scanner; + int retval = -1; + int ret; + + result->errmsg[0] = '\0'; // For safety + + ret = rsp_lex_init(&scanner); + if (ret != 0) + goto error_init; + + buffer = rsp__scan_string(input, scanner); + if (!buffer) + goto error_buffer; + + ret = rsp_parse(result, scanner); + if (ret != 0) + goto error_parse; + + retval = 0; + + error_parse: + rsp__delete_buffer(buffer, scanner); + error_buffer: + rsp_lex_destroy(scanner); + error_init: + return retval; + } + + void rsp_error(struct rsp_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 +#include + +#define INVERT_MASK 0x80000000 +} + +/* Dependencies, mocked or real */ +%code top { +#ifndef DEBUG_PARSER_MOCK +#include "db.h" +#include "misc.h" +#else +#include "owntonefunctions.h" +#endif +} + +/* Definition of struct that will hold the parsing result */ +%code requires { +struct rsp_result { + char str[1024]; + int offset; + 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_PARENS, +}; + +static void sql_from_ast(struct rsp_result *, struct ast *); + +// 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; + + *escape_char = 0; + + // Fast path, nothing to escape + if (!strpbrk(s, "_%")) + return; + + len = 2 * len; // Enough for every char to be escaped + new = realloc(s, len); + safe_snreplace(new, len, "%", "\\%"); + safe_snreplace(new, len, "_", "\\_"); + *escape_char = '\\'; + *value = new; +} + +static void sql_str_escape(char **value) +{ + char *s = *value; + + if (strchr(s, '\"')) + safe_snreplace(s, strlen(s) + 1, "\\\"", "\""); // See test case 3 + + *value = db_escape_string(s); + free(s); +} + +static void sql_append(struct rsp_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 (%zu bytes)", sizeof(result->str)); + result->err = -2; +} + +static void sql_append_recursive(struct rsp_result *result, struct ast *a, const char *op, const char *op_not, bool is_not, enum sql_append_type append_type) +{ + char escape_char; + + switch (append_type) + { + case SQL_APPEND_OPERATOR: + sql_from_ast(result, a->l); + sql_append(result, " %s ", is_not ? op_not : op); + sql_from_ast(result, a->r); + break; + case SQL_APPEND_OPERATOR_STR: + sql_from_ast(result, a->l); + sql_append(result, " %s '", is_not ? op_not : op); + sql_from_ast(result, a->r); + sql_append(result, "'"); + break; + case SQL_APPEND_OPERATOR_LIKE: + sql_from_ast(result, a->l); + sql_append(result, " %s '%%", is_not ? op_not : op); + sql_like_escape((char **)(&a->r->data), &escape_char); + sql_from_ast(result, a->r); + sql_append(result, "%%'"); + if (escape_char) + sql_append(result, " ESCAPE '%c'", escape_char); + break; + case SQL_APPEND_FIELD: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, "f.%s", (char *)a->data); + break; + case SQL_APPEND_STR: + assert(a->l == NULL); + assert(a->r == NULL); + sql_str_escape((char **)&a->data); + sql_append(result, "%s", (char *)a->data); + break; + case SQL_APPEND_INT: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, "%d", a->ival); + break; + case SQL_APPEND_PARENS: + assert(a->r == NULL); + sql_append(result, "("); + sql_from_ast(result, a->l); + sql_append(result, ")"); + break; + } +} + +static void sql_from_ast(struct rsp_result *result, struct ast *a) +{ + if (!a || result->err < 0) + return; + + // Not currently used since grammar below doesn't ever set with INVERT_MASK + bool is_not = (a->type & INVERT_MASK); + a->type &= ~INVERT_MASK; + + switch (a->type) + { + case RSP_T_OR: + sql_append_recursive(result, a, "OR", "OR NOT", is_not, SQL_APPEND_OPERATOR); break; + case RSP_T_AND: + sql_append_recursive(result, a, "AND", "AND NOT", is_not, SQL_APPEND_OPERATOR); break; + case RSP_T_EQUAL: + sql_append_recursive(result, a, "=", "!=", is_not, SQL_APPEND_OPERATOR); break; + case RSP_T_IS: + sql_append_recursive(result, a, "=", "!=", is_not, SQL_APPEND_OPERATOR_STR); break; + case RSP_T_INCLUDES: + sql_append_recursive(result, a, "LIKE", "NOT LIKE", is_not, SQL_APPEND_OPERATOR_LIKE); break; + case RSP_T_STRTAG: + case RSP_T_INTTAG: + sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_FIELD); break; + case RSP_T_NUM: + sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_INT); break; + case RSP_T_STRING: + sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_STR); break; + break; + case RSP_T_PARENS: + sql_append_recursive(result, 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 rsp_result *result, struct ast *a) +{ + memset(result, 0, sizeof(struct rsp_result)); + sql_from_ast(result, a); + ast_free(a); + return result->err; +} +} + +%union { + unsigned int ival; + char *str; + struct ast *ast; +} + +/* A string that was quoted. Quotes were stripped by lexer. */ +%token RSP_T_STRING + +/* A number (integer) */ +%token RSP_T_NUM + +/* The semantic value holds the actual name of the field */ +%token RSP_T_STRTAG +%token RSP_T_INTTAG + +%token RSP_T_PARENS +%token RSP_T_OR +%token RSP_T_AND +%token RSP_T_NOT + +%token RSP_T_EQUAL +%token RSP_T_IS +%token RSP_T_INCLUDES + +%left RSP_T_OR RSP_T_AND + +%type criteria +%type predicate + +%% + +query: + criteria { return result_set(result, $1); } +; + +criteria: criteria RSP_T_AND criteria { $$ = ast_new(RSP_T_AND, $1, $3); } +| criteria RSP_T_OR criteria { $$ = ast_new(RSP_T_OR, $1, $3); } +| '(' criteria ')' { $$ = ast_new(RSP_T_PARENS, $2, NULL); } +| predicate +; + +predicate: RSP_T_STRTAG RSP_T_EQUAL RSP_T_STRING { $$ = ast_new(RSP_T_IS, ast_data(RSP_T_STRTAG, $1), ast_data(RSP_T_STRING, $3)); } +| RSP_T_STRTAG RSP_T_INCLUDES RSP_T_STRING { $$ = ast_new(RSP_T_INCLUDES, ast_data(RSP_T_STRTAG, $1), ast_data(RSP_T_STRING, $3)); } +| RSP_T_INTTAG RSP_T_EQUAL RSP_T_NUM { $$ = ast_new(RSP_T_EQUAL, ast_data(RSP_T_INTTAG, $1), ast_int(RSP_T_NUM, $3)); } +; + +%% + diff --git a/src/rsp_query.c b/src/rsp_query.c deleted file mode 100644 index 16ac76f8..00000000 --- a/src/rsp_query.c +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2009-2011 Julien BLACHE - * - * 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 - */ - -#ifdef HAVE_CONFIG_H -# include -#endif - -#include -#include -#include -#include - -#include "logger.h" -#include "misc.h" -#include "rsp_query.h" - -char * -rsp_query_parse_sql(const char *rsp_query) -{ - return NULL; -} diff --git a/src/rsp_query.gperf b/src/rsp_query.gperf deleted file mode 100644 index aea99800..00000000 --- a/src/rsp_query.gperf +++ /dev/null @@ -1,61 +0,0 @@ -%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 -struct rsp_query_field_map { - char *rsp_field; - int field_type; -}; -%% -"id", RSP_TYPE_INT -"path", RSP_TYPE_STRING -"fname", RSP_TYPE_STRING -"title", RSP_TYPE_STRING -"artist", RSP_TYPE_STRING -"album", RSP_TYPE_STRING -"genre", RSP_TYPE_STRING -"comment", RSP_TYPE_STRING -"type", RSP_TYPE_STRING -"composer", RSP_TYPE_STRING -"orchestra", RSP_TYPE_STRING -"grouping", RSP_TYPE_STRING -"url", RSP_TYPE_STRING -"bitrate", RSP_TYPE_INT -"samplerate", RSP_TYPE_INT -"song_length", RSP_TYPE_INT -"file_size", RSP_TYPE_INT -"year", RSP_TYPE_INT -"track", RSP_TYPE_INT -"total_tracks", RSP_TYPE_INT -"disc", RSP_TYPE_INT -"total_discs", RSP_TYPE_INT -"bpm", RSP_TYPE_INT -"compilation", RSP_TYPE_INT -"rating", RSP_TYPE_INT -"play_count", RSP_TYPE_INT -"skip_count", RSP_TYPE_INT -"data_kind", RSP_TYPE_INT -"item_kind", RSP_TYPE_INT -"description", RSP_TYPE_STRING -"time_added", RSP_TYPE_DATE -"time_modified", RSP_TYPE_DATE -"time_played", RSP_TYPE_DATE -"time_skipped", RSP_TYPE_DATE -"db_timestamp", RSP_TYPE_DATE -"sample_count", RSP_TYPE_INT -"codectype", RSP_TYPE_STRING -"idx", RSP_TYPE_INT -"has_video", RSP_TYPE_INT -"contentrating", RSP_TYPE_INT -"bits_per_sample", RSP_TYPE_INT -"album_artist", RSP_TYPE_STRING diff --git a/src/rsp_query.h b/src/rsp_query.h deleted file mode 100644 index a5283bbf..00000000 --- a/src/rsp_query.h +++ /dev/null @@ -1,9 +0,0 @@ - -#ifndef __RSP_QUERY_H__ -#define __RSP_QUERY_H__ - - -char * -rsp_query_parse_sql(const char *rsp_query); - -#endif /* !__RSP_QUERY_H__ */