[rsp] Add basic bison/flex RSP parser

This commit is contained in:
ejurgensen 2022-01-14 22:29:08 +01:00
parent 9c1f7dd380
commit 71b4444118
7 changed files with 509 additions and 122 deletions

View File

@ -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 \

View File

@ -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;
}

82
src/parsers/rsp_lexer.l Normal file
View File

@ -0,0 +1,82 @@
/*
* Copyright (C) 2021-2022 Espen Jürgensen <espenjurgensen@gmail.com>
*
* 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 <stdio.h>
#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]; }
%%

416
src/parsers/rsp_parser.y Normal file
View File

@ -0,0 +1,416 @@
/*
* Copyright (C) 2021-2022 Espen Jürgensen <espenjurgensen@gmail.com>
*
* 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($$); } <str>
%destructor { ast_free($$); } <ast>
/* ========================= NON-BOILERPLATE SECTION =========================*/
/* Includes required by the parser rules */
%code top {
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdarg.h> // For vsnprintf
#include <assert.h>
#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 <str> RSP_T_STRING
/* A number (integer) */
%token <ival> RSP_T_NUM
/* The semantic value holds the actual name of the field */
%token <str> RSP_T_STRTAG
%token <str> 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 <ast> criteria
%type <ast> 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)); }
;
%%

View File

@ -1,36 +0,0 @@
/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
*
* 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 <config.h>
#endif
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include "logger.h"
#include "misc.h"
#include "rsp_query.h"
char *
rsp_query_parse_sql(const char *rsp_query)
{
return NULL;
}

View File

@ -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

View File

@ -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__ */