[daap/smartpl] Add new bison/flex parsers

This commit is contained in:
ejurgensen 2022-01-10 21:05:51 +01:00
parent 3a93dc5da8
commit efe5df5e12
14 changed files with 1560 additions and 13 deletions

1
.gitignore vendored
View File

@ -29,6 +29,7 @@ missing
stamp-h1
autotools-stamp
build-stamp
ylwrap
owntone.spec
owntone.conf
owntone.service

View File

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

65
m4/ax_prog_bison.m4 Normal file
View File

@ -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 <salvestrini@users.sourceforge.net>
# Copyright (c) 2010 Diego Elio Petteno` <flameeyes@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, see <https://www.gnu.org/licenses/>.
#
# 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]])
)
])

60
m4/ax_prog_flex.m4 Normal file
View File

@ -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 <salvestrini@users.sourceforge.net>
# Copyright (c) 2010 Diego Elio Petteno` <flameeyes@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, see <https://www.gnu.org/licenses/>.
#
# 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]])
)
])

3
src/.gitignore vendored
View File

@ -1,5 +1,8 @@
owntone
*_lexer.[ch]
*_parser.[ch]
daap_query_hash.h
rsp_query_hash.h
dacp_prop_hash.h

View File

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

74
src/daap_lexer.l Normal file
View File

@ -0,0 +1,74 @@
/*
* 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="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 <stdio.h>
#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; }
<IN_CRITERIA>{re_key}/{re_operator} { yylval->str = strdup(yytext); return DAAP_T_KEY; }
<IN_CRITERIA>{re_operator} { BEGIN IN_CRITERIA_VALUE; return (*yytext == '!' ? DAAP_T_NOT : DAAP_T_EQUAL); }
<IN_CRITERIA>. { return *yytext; }
<IN_CRITERIA_VALUE>\*{re_value}\*/{re_quote} { yylval->str = strdup(yytext); return DAAP_T_WILDCARD; }
<IN_CRITERIA_VALUE>{re_value}/{re_quote} { yylval->str = strdup(yytext); return DAAP_T_VALUE; }
<IN_CRITERIA_VALUE>{re_quote} { BEGIN INITIAL; return DAAP_T_QUOTE; }
<IN_CRITERIA_VALUE>. { return *yytext; }
"+"|" " { return DAAP_T_AND; }
"," { return DAAP_T_OR; }
"\r"?"\n" { return DAAP_T_NEWLINE; }
. { return *yytext; }
%%

508
src/daap_parser.y Normal file
View File

@ -0,0 +1,508 @@
/*
* 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 {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($$); } <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
}
/* 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<str> DAAP_T_KEY
%token<str> DAAP_T_VALUE
%token<str> 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 <ast> expr
%type <ival> 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; }
;
%%

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
* 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
@ -25,13 +25,22 @@
#include <string.h>
#include <errno.h>
#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);
}

View File

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

View File

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

192
src/smartpl_lexer.l Normal file
View File

@ -0,0 +1,192 @@
/*
* 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="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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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);
}

567
src/smartpl_parser.y Normal file
View File

@ -0,0 +1,567 @@
/*
* 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 {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($$); } <str>
%destructor { ast_free($$); } <ast>
/* ========================= NON-BOILERPLATE SECTION =========================*/
/* Includes required by the parser rules */
%code top {
#ifndef _GNU_SOURCE
#define _GNU_SOURCE // For asprintf
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h> // For vsnprintf
#include <string.h>
#include <time.h>
#include <assert.h>
#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 <str> SMARTPL_T_UNQUOTED
/* The semantic value holds the actual name of the field */
%token <str> SMARTPL_T_STRTAG
%token <str> SMARTPL_T_INTTAG
%token <str> SMARTPL_T_DATETAG
%token <str> SMARTPL_T_DATAKINDTAG
%token <str> SMARTPL_T_MEDIAKINDTAG
%token <str> 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 <str> SMARTPL_T_DATE
%token <ival> SMARTPL_T_DATE_TODAY
%token <ival> SMARTPL_T_DATE_YESTERDAY
%token <ival> SMARTPL_T_DATE_LASTWEEK
%token <ival> SMARTPL_T_DATE_LASTMONTH
%token <ival> SMARTPL_T_DATE_LASTYEAR
%token <ival> SMARTPL_T_NUM
%token <ival> SMARTPL_T_DATAKIND
%token <ival> 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 <ival> SMARTPL_T_EQUALS
%token <ival> SMARTPL_T_LESS
%token <ival> SMARTPL_T_LESSEQUAL
%token <ival> SMARTPL_T_GREATER
%token <ival> SMARTPL_T_GREATEREQUAL
%token <ival> SMARTPL_T_IS
%token <ival> SMARTPL_T_INCLUDES
%token <ival> SMARTPL_T_BEFORE
%token <ival> SMARTPL_T_AFTER
%token <ival> SMARTPL_T_AGO
%left SMARTPL_T_OR SMARTPL_T_AND
%type <ast> criteria
%type <ast> predicate
%type <ast> dateexpr
%type <ast> interval
%type <ast> having
%type <ast> order
%type <ast> limit
%type <str> time
%type <ival> daterelative
%type <ival> strbool
%type <ival> intbool
%type <ival> 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
;
%%

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 Christian Meffert <christian.meffert@googlemail.com>
* 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
@ -32,6 +32,7 @@
#include <errno.h>
#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;
}