[daap/smartpl] Add new bison/flex parsers
This commit is contained in:
parent
3a93dc5da8
commit
efe5df5e12
|
@ -29,6 +29,7 @@ missing
|
||||||
stamp-h1
|
stamp-h1
|
||||||
autotools-stamp
|
autotools-stamp
|
||||||
build-stamp
|
build-stamp
|
||||||
|
ylwrap
|
||||||
owntone.spec
|
owntone.spec
|
||||||
owntone.conf
|
owntone.conf
|
||||||
owntone.service
|
owntone.service
|
||||||
|
|
|
@ -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.]])])
|
[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.
|
dnl Enable all warnings by default.
|
||||||
AM_CPPFLAGS="-Wall"
|
AM_CPPFLAGS="-Wall"
|
||||||
AC_SUBST([AM_CPPFLAGS])
|
AC_SUBST([AM_CPPFLAGS])
|
||||||
|
|
|
@ -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]])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
|
@ -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]])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
owntone
|
owntone
|
||||||
|
|
||||||
|
*_lexer.[ch]
|
||||||
|
*_parser.[ch]
|
||||||
|
|
||||||
daap_query_hash.h
|
daap_query_hash.h
|
||||||
rsp_query_hash.h
|
rsp_query_hash.h
|
||||||
dacp_prop_hash.h
|
dacp_prop_hash.h
|
||||||
|
|
|
@ -62,6 +62,21 @@ GPERF_FILES = \
|
||||||
|
|
||||||
GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h)
|
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 += \
|
AM_CPPFLAGS += \
|
||||||
$(OWNTONE_CPPFLAGS) \
|
$(OWNTONE_CPPFLAGS) \
|
||||||
$(OWNTONE_OPTS_CPPFLAGS) \
|
$(OWNTONE_OPTS_CPPFLAGS) \
|
||||||
|
@ -130,16 +145,27 @@ owntone_SOURCES = main.c \
|
||||||
mxml-compat.h \
|
mxml-compat.h \
|
||||||
outputs/plist_wrap.h \
|
outputs/plist_wrap.h \
|
||||||
$(LIBWEBSOCKETS_SRC) \
|
$(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 = \
|
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 = \
|
EXTRA_DIST = \
|
||||||
$(GPERF_FILES)
|
$(GPERF_FILES) \
|
||||||
|
$(LEXER_SRC:.l=.h)
|
||||||
|
|
||||||
# gperf construction rules
|
# gperf construction rules
|
||||||
%_hash.h: %.gperf
|
%_hash.h: %.gperf
|
||||||
$(AM_V_GEN)$(GPERF) --output-file=$@ $<
|
$(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])
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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; }
|
||||||
|
;
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,13 +25,22 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "daap_query.h"
|
||||||
|
#include "daap_parser.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
#include "daap_query.h"
|
|
||||||
|
|
||||||
|
|
||||||
char *
|
char *
|
||||||
daap_query_parse_sql(const char *daap_query)
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
%define lookup-function-name daap_query_field_lookup
|
%define lookup-function-name daap_query_field_lookup
|
||||||
%define slot-name dmap_field
|
%define slot-name dmap_field
|
||||||
%struct-type
|
%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.itemname", "f.title", 0
|
||||||
"dmap.itemid", "f.id", 1
|
"dmap.itemid", "f.id", 1
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
%language=ANSI-C
|
%language=ANSI-C
|
||||||
%readonly-tables
|
%readonly-tables
|
||||||
%enum
|
%enum
|
||||||
|
enum rsp_field_types {
|
||||||
|
RSP_TYPE_STRING,
|
||||||
|
RSP_TYPE_INT,
|
||||||
|
RSP_TYPE_DATE,
|
||||||
|
};
|
||||||
%switch=1
|
%switch=1
|
||||||
%compare-lengths
|
%compare-lengths
|
||||||
%define hash-function-name rsp_query_field_hash
|
%define hash-function-name rsp_query_field_hash
|
||||||
%define lookup-function-name rsp_query_field_lookup
|
%define lookup-function-name rsp_query_field_lookup
|
||||||
%define slot-name rsp_field
|
%define slot-name rsp_field
|
||||||
%struct-type
|
%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
|
"id", RSP_TYPE_INT
|
||||||
"path", RSP_TYPE_STRING
|
"path", RSP_TYPE_STRING
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
;
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
#include "smartpl_query.h"
|
#include "smartpl_query.h"
|
||||||
|
#include "smartpl_parser.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
|
||||||
|
@ -45,7 +46,29 @@ smartpl_query_parse_file(struct smartpl *smartpl, const char *file)
|
||||||
int
|
int
|
||||||
smartpl_query_parse_string(struct smartpl *smartpl, const char *expression)
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue