Merge pull request #1386 from owntone/bison_parser1

Replace antlr3 parsers with bison/flex
This commit is contained in:
ejurgensen 2022-01-30 10:20:01 +01:00 committed by GitHub
commit bc1c3e7bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2133 additions and 2722 deletions

View File

@ -1,4 +1,4 @@
name: "forked-daapd CodeQL config"
name: "OwnTone CodeQL config"
paths-ignore:
- htdocs

View File

@ -40,10 +40,10 @@ jobs:
# uses a compiled language
- run: |
sudo apt-get update
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
autoreconf -vi
./configure --enable-lastfm --enable-chromecast
scan-build --status-bugs -disable-checker deadcode.DeadStores make
scan-build --status-bugs -disable-checker deadcode.DeadStores --exclude src/parsers make
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -17,6 +17,17 @@ jobs:
- name: Install build tools
run: brew install automake autoconf libtool pkg-config
- name: Install gperf, bison and flex
# MacOS comes with an ancient bison, we need a newer version. Homebrew's
# bison and flex are keg-only, which means they are not symlinked into
# /usr/local because macOS already provides this software. Homebrew tells
# you to adjust the $PATH, but I couldn't make that work, and I think
# symlinking is a better solution for simple binaries.
run: |
brew install gperf bison flex
sudo ln -s /usr/local/opt/bison/bin/bison /usr/local/bin/bison
sudo ln -s /usr/local/opt/flex/bin/flex /usr/local/bin/flex
- name: Install libinotify-kqueue
# brew does not have libinotify package
run: |
@ -40,12 +51,6 @@ jobs:
sudo make install
cd ..
- name: Install gperf and antlr3
run: |
brew install gperf
chmod +x ./scripts/antlr35_install.sh
./scripts/antlr35_install.sh -y -p /usr/local
- name: Install ffmpeg
# The libbluray ffmpeg dependency fails without the chown (source: stackoverflow)
run: |

View File

@ -16,7 +16,7 @@ jobs:
- name: install dependencies
run: |
sudo apt-get update
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
sudo apt-get install -yq build-essential clang clang-tools git autotools-dev autoconf libtool gettext gawk gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev libevent-dev libplist-dev libsodium-dev libcurl4-openssl-dev libjson-c-dev libprotobuf-c-dev libpulse-dev libwebsockets-dev libgnutls28-dev
- name: build and check
run: |
autoreconf -vi

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

@ -1,6 +1,9 @@
# Installation instructions for OwnTone
This document contains instructions for installing OwnTone from the git tree.
This document contains instructions for installing OwnTone from the git tree. If
you just want to install from a release tarball, you don't need the build tools
(git, autotools, autoconf, automake, gawk, gperf, gettext, bison and flex), and
you can skip the autoreconf step.
The source for this version of OwnTone can be found here:
[owntone/owntone-server](https://github.com/owntone/owntone-server.git)
@ -19,7 +22,7 @@ libraries:
```bash
sudo apt-get install \
build-essential git autotools-dev autoconf automake libtool gettext gawk \
gperf antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev \
gperf bison flex libconfuse-dev libunistring-dev libsqlite3-dev \
libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev \
libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev \
libevent-dev libplist-dev libsodium-dev libjson-c-dev libwebsockets-dev \
@ -79,11 +82,11 @@ will need ffmpeg. You can google how to do that. Then run:
```bash
sudo yum install \
git automake autoconf gettext-devel gperf gawk libtool \
git automake autoconf gettext-devel gperf gawk libtool bison flex \
sqlite-devel libconfuse-devel libunistring-devel mxml-devel libevent-devel \
avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel \
libplist-devel libsodium-devel json-c-devel libwebsockets-devel \
libcurl-devel protobuf-c-devel antlr3 antlr3-C-devel
libcurl-devel protobuf-c-devel
```
Clone the OwnTone repo:
@ -140,16 +143,9 @@ build) ports... you'll want a decent network connection and some patience!
Install macports (which requires Xcode):
https://www.macports.org/install.php
Install Apple's Java (this enables java command on OSX 10.7+):
https://support.apple.com/kb/DL1572?locale=en_US
Afterwards, you can optionally install Oracle's newer version, and then
choose it using the Java pref in the System Preferences:
http://www.oracle.com/technetwork/java/javase/downloads/index.html
```bash
sudo port install \
autoconf automake libtool pkgconfig git gperf libgcrypt \
autoconf automake libtool pkgconfig git gperf bison flex libgcrypt \
libunistring libconfuse ffmpeg libevent json-c libwebsockets curl \
libplist libsodium protobuf-c
```
@ -184,12 +180,6 @@ git clone https://github.com/owntone/owntone-server.git
cd owntone-server
```
Install antlr3 and library using the included script:
```bash
scripts/antlr35_install.sh -p /usr/local
```
Finally, configure, build and install, adding configure arguments for
optional features:
@ -229,21 +219,16 @@ dns-sd -B _daap._tcp
Required tools:
- ANTLR v3 is required to build OwnTone, along with its C runtime
(libantlr3c). Use a version between 3.1.3 and 3.5 of ANTLR v3 and the
matching C runtime version. Get it from <http://www.antlr3.org/>
- Java runtime: ANTLR is written in Java and as such a JRE is required to
run the tool. The JRE is enough, you don't need a full JDK.
- autotools: autoconf 2.63+, automake 1.10+, libtool 2.2. Run `autoreconf -i`
at the top of the source tree to generate the build system.
- gettext: libunistring requires iconv and gettext provides the autotools
macro definitions for iconv.
- gperf
- bison 3.0+ (yacc is not sufficient)
- flex (lex is not sufficient)
Libraries:
- libantlr3c (ANTLR3 C runtime, use the same version as antlr3)
from <https://github.com/antlr/website-antlr3/tree/gh-pages/download/C>
- Avahi client libraries (avahi-client), 0.6.24 minimum
from <http://avahi.org/>
- sqlite3 3.5.0+ with unlock notify API enabled (read below)
@ -297,22 +282,6 @@ documentation, look for `SQLITE_ENABLE_UNLOCK_NOTIFY`.
Start by generating the build system by running `autoreconf -i`. This will
generate the configure script and `Makefile.in`.
The configure script will look for a wrapper called antlr3 in the PATH to
invoke ANTLR3. If your installation of ANTLR3 does not come with such a
wrapper, create one as follows:
```bash
#!/bin/sh
CLASSPATH=...
exec /path/to/java -cp $CLASSPATH org.antlr.Tool "$@"
```
Adjust the `CLASSPATH` as needed so that Java will find all the jars needed
by ANTLR3.
The parsers will be generated during the build, no manual intervention is
needed.
To display the configure options `run ./configure --help`.
Support for Spotify is optional. Use `--disable-spotify` to disable this feature.
@ -371,25 +340,6 @@ Use `--disable-install-systemd` if you don't want that.
Using `--enable-install-user` means that `make install` will also add a system
user and group for owntone.
You may see two kinds of warnings during make.
First, `/usr/bin/antlr3` may generate a long series of warnings that
begin like this:
```log
warning(24): template error: context ...
```
Second, you may see compiler warnings that look like this:
```log
RSPLexer.c: In function `mESCAPED':
RSPLexer.c:2674:16: warning: unused variable `_type' [-Wunused-variable]
ANTLR3_UINT32 _type;
^~~~~
```
You can safely ignore all of these warnings.
After installation:
- edit the configuration file, `/etc/owntone.conf`

View File

@ -27,20 +27,17 @@ AS_IF([[test -z "$GPERF"]],
AC_MSG_NOTICE([[
GNU gperf not found, but it's output appears to be present.
If you modify any gperf or ANTLR grammar files (.g), you will need
to install it.]])],
If you modify any .gperf files, you will need to install it.]])],
[AC_MSG_ERROR([[GNU gperf required, please install it.]])])
])
AC_PATH_PROG([ANTLR], [[antlr3]])
AS_IF([[test -z "$ANTLR"]],
[AS_IF([[test -f "$srcdir/src/SMARTPLLexer.h"]],
[AM_MISSING_PROG([ANTLR], [[antlr3]])
AC_MSG_NOTICE([[
antlr3 not found, but it's output appears to be present.
If you modify any ANTLR grammar files (.g), you will need to install it.]])],
[AC_MSG_ERROR([[antlr3 wrapper required, please install it.]])])
AX_PROG_FLEX([AC_DEFINE([LEX], [flex], [flex found])],
[AS_IF([test ! -f "$srcdir/src/parsers/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/parsers/smartpl_parser.c"],
[AC_MSG_ERROR([GNU bison required, please install it])])
])
dnl Enable all warnings by default.
@ -174,16 +171,6 @@ PKG_CHECK_EXISTS([libplist],
[OWNTONE_MODULES_CHECK([OWNTONE], [LIBPLIST], [libplist-2.0],
[plist_dict_get_item], [plist/plist.h])])
OWNTONE_FUNC_REQUIRE([OWNTONE], [ANTLR3 C runtime], [ANTLR3C], [antlr3c],
[antlr3BaseRecognizerNew], [antlr3.h],
[AC_CHECK_FUNC([[antlr3NewAsciiStringInPlaceStream]],
[AC_DEFINE([ANTLR3C_NEW_INPUT], 0,
[define if antlr3 C runtime uses new input routines])],
[AC_DEFINE([ANTLR3C_NEW_INPUT], 1,
[define if antlr3 C runtime uses new input routines])])
])
AM_PATH_LIBGCRYPT([1:1.2.0])
OWNTONE_FUNC_REQUIRE([OWNTONE], [GNU Crypt Library], [LIBGCRYPT], [gcrypt],
[gcry_control], [gcrypt.h])

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

View File

@ -18,10 +18,10 @@ Group: Applications/Multimedia
Url: https://github.com/owntone/owntone-server
Source0: https://github.com/owntone/%{name}/archive/%{version}/%{name}-%{version}.tar.xz
%{?systemd_ordering}
BuildRequires: gcc, make, systemd, pkgconfig, libunistring-devel
BuildRequires: gcc, make, bison, flex, systemd, pkgconfig, libunistring-devel
BuildRequires: pkgconfig(zlib), pkgconfig(libconfuse), pkgconfig(mxml)
BuildRequires: pkgconfig(sqlite3) >= 3.5.0, pkgconfig(libevent) >= 2.0.0
BuildRequires: pkgconfig(json-c), antlr3-C-devel, libgcrypt-devel >= 1.2.0
BuildRequires: pkgconfig(json-c), libgcrypt-devel >= 1.2.0
BuildRequires: libgpg-error-devel >= 1.6
BuildRequires: pkgconfig(libavformat), pkgconfig(libavcodec)
BuildRequires: pkgconfig(libswscale), pkgconfig(libavutil)
@ -109,6 +109,10 @@ exit 0
%{_mandir}/man?/*
%changelog
* Mon Jan 17 2022 Espen Jürgensen <espen.jurgensen@gmail.com> - 28.3-1
- Remove antlr dependency
- Add bison/flex dependency
* Mon Nov 22 2021 Derek Atkins <derek@ihtfp.com> - 28.2-1
- Release tarball is a XZ not GZ file
- Configure always needs protobuf-c, not just for chromecast

View File

@ -1,255 +0,0 @@
#!/bin/sh
WORKDIR=~/antlr35.tmp
# programs
MAKE=${MAKE-make}
DOWNLOAD="wget --no-check-certificate"
ALTDOWNLOAD="curl -LO"
SUDO=sudo
# source
ANTLR_VERSION=3.5
ANTLR3_SOURCE="https://github.com/antlr/website-antlr3/raw/gh-pages/download"
ANTLR3_JAR="antlr-3.5.2-complete.jar"
ANTLR3_URL="$ANTLR3_SOURCE/$ANTLR3_JAR"
LIBANTLR3C="libantlr3c-3.4"
LIBANTLR3C_SOURCE="https://github.com/antlr/website-antlr3/raw/gh-pages/download/C"
LIBANTLR3C_TAR="${LIBANTLR3C}.tar.gz"
LIBANTLR3C_URL="$LIBANTLR3C_SOURCE/$LIBANTLR3C_TAR"
usage() {
echo
echo "This script will download, build and install antlr $ANTLR_VERSION"
echo " (and matching libantlrc) on your computer."
echo
echo "Usage: ${0##*/} -h | [ -p <prefix> ] [ -y ] [ <build-dir> ]"
echo
echo "Parameters:"
echo " -h Show this help"
echo " -p <prefix> Install to prefix (default: choose /usr or /usr/local)"
echo " -y Automatic yes to prompts (run non-interactively with -p)"
echo " <build-dir> Build directory (default: $WORKDIR)"
exit 0
}
GIVEN_PREFIX=
ALWAYS_YES=
while [ "$1" != "" ]; do
case $1 in
-p | --prefix )
shift
GIVEN_PREFIX=$1
;;
-y | --yes )
ALWAYS_YES=1
;;
-h | --help )
usage
exit
;;
* )
echo "Unrecognized option $1 (try -h for usage)"
exit 1
;;
esac
shift
done
# override build directory? (support ~ expansion)
[ -n "$1" ] && WORKDIR=$1
ORIG_DIR=`pwd`
err() {
echo "$*"
if [ -n "$FILES_EXIST" ]; then
echo "Files remain in $WORKDIR..."
else
cd "$ORIG_DIR"
rmdir "$WORKDIR"
fi
exit 1
}
is_yes() {
case "$1" in
[N]*|[n]*) return 1;;
*) ;;
esac
return 0
}
ask_yn() {
if [ "$ALWAYS_YES" = "1" ]; then
yn="y"
else
read -p "$1" yn
fi
}
prog_install() {
ask_yn "Would you like to install into $PREFIX now? [Y/n] "
if ! is_yes "$yn"; then
echo "Build left ready to install from $WORKDIR"
echo "You can re-run the script (eg. as root) to install into"
echo " $PREFIX later."
exit
fi
if [ `id -u` -ne 0 ]; then
ask_yn "Would you like to install with sudo? NOTE: You WILL be asked for your password! [Y/n] "
if ! is_yes "$yn"; then
SUDO=
ask_yn "Continue to install as non-root user? [Y/n] "
is_yes "$yn" || err "Install cancelled"
fi
else
SUDO=
fi
cd $LIBANTLR3C || err "Unable to cd to build libantlr3c build directory!"
echo "Installing libantlr3c to $PREFIX"
$SUDO $MAKE install || err "Install of libantlr3c to $PREFIX failed!"
cd "$ORIG_DIR"
cd $WORKDIR
echo "Installing antlr3 to $PREFIX"
$SUDO mkdir -p "$PREFIX_JAVA" || err "Unable to create $PREFIX_JAVA"
$SUDO install "$ANTLR3_JAR" "$PREFIX_JAVA" || \
err "Failed to install antlr3 jar to $PREFIX_JAVA"
$SUDO mkdir -p "$PREFIX/bin" || err "Unable to create $PREFIX/bin"
$SUDO install -m 755 antlr3 "$PREFIX/bin" || \
err "Failed to install antlr3 to $PREFIX/bin"
echo "Install complete (build remains in $WORKDIR)"
}
echo "This script will download, build and install antlr $ANTLR_VERSION"
echo " (and matching libantlrc) on your computer."
echo
# check if make works
ISGNU=`$MAKE --version 2>/dev/null | grep "GNU Make"`
if [ -z "$ISGNU" ]; then
MAKE=gmake
ISGNU=`$MAKE --version 2>/dev/null | grep "GNU Make"`
fi
[ -z "$ISGNU" ] && err "Unable to locate GNU Make, set \$MAKE to it's location and re-run"
if [ -f "$WORKDIR/install_env" ]; then
echo "Existing build found in $WORKDIR"
FILES_EXIST=1
cd $WORKDIR || err "Unable to cd to '$WORKDIR'"
. install_env
[ -n "$PREFIX" ] || err "PREFIX is missing in file 'install_env'"
if [ -n "$GIVEN_PREFIX" ] && [ "$GIVEN_PREFIX" != "$PREFIX" ]; then
echo "You must rebuild to install into $GIVEN_PREFIX (current build for $PREFIX)"
ask_yn "Would you like to rebuild for ${GIVEN_PREFIX}? [Y/n] "
if is_yes "$yn"; then
rm -f install_env
PREFIX=
else
ask_yn "Would you like to install to ${PREFIX}? [Y/n] "
! is_yes "$yn" && err "Install cancelled"
fi
fi
if [ -n "$PREFIX" ]; then
PREFIX_JAVA=$PREFIX/share/java
prog_install
exit 0
fi
fi
if [ ! -d "$WORKDIR" ]; then
ask_yn "Should the script create $WORKDIR and use it for building? [Y/n] "
is_yes "$yn" || exit
fi
if [ -n "$GIVEN_PREFIX" ]; then
PREFIX=$GIVEN_PREFIX
else
read -p "Should the script install with prefix /usr or /usr/local? [U/l] " yn
if [ "$yn" = "l" ]; then
PREFIX=/usr/local
else
PREFIX=/usr
fi
fi
PREFIX_JAVA=$PREFIX/share/java
MACHBITS=`getconf LONG_BIT 2>/dev/null`
[ "$MACHBITS" = "64" ] && DEF_AN="[Y/n]" || DEF_AN="[y/N]"
ask_yn "Should the script build libantlr3c for 64 bit? $DEF_AN "
[ -z "$yn" -a "$MACHBITS" != "64" ] && yn=n
is_yes "$yn" && ENABLE64BIT="--enable-64bit"
mkdir -p "$WORKDIR" || err "Error creating $WORKDIR"
# don't quote WORKDIR to catch a WORKDIR that will break the build (eg spaces)
cd $WORKDIR || err "Unable to cd to '$WORKDIR' (does it include spaces?)"
REMOVE_ON_CANCEL=
cancel_download() {
echo "removing $REMOVE_ON_CANCEL"
[ -n "$REMOVE_ON_CANCEL" ] && rm -f "$REMOVE_ON_CANCEL"
err "Cancelling download..."
}
antlr_download() {
trap cancel_download SIGINT
$DOWNLOAD --help >/dev/null 2>&1 || DOWNLOAD=$ALTDOWNLOAD
$DOWNLOAD --help >/dev/null 2>&1 || {
echo "Unable to find wget or curl commands to download source,"
echo " please install either one and re-try."
exit 1
}
[ "x$1" = "xreset" ] && rm "$ANTLR3_JAR" "$LIBANTLR3C_TAR"
if [ ! -f "$ANTLR3_JAR" ]; then
echo
echo "Downloading antlr from $ANTLR3_URL"
echo "Ctrl-C to abort..."
REMOVE_ON_CANCEL=$ANTLR3_JAR
$DOWNLOAD "$ANTLR3_URL" || err "Download of $ANTLR3_JAR failed!"
FILES_EXIST=1
fi
if [ ! -f "$LIBANTLR3C_TAR" ]; then
echo
echo "Downloading libantlr3c from $LIBANTLR3C_URL"
echo "Ctrl-C to abort..."
REMOVE_ON_CANCEL=$LIBANTLR3C_TAR
$DOWNLOAD "$LIBANTLR3C_URL" || err "Download of $LIBANTLR3C_TAR failed!"
FILES_EXIST=1
fi
trap - SIGINT
}
# retrieve the source
if [ -f "$ANTLR3_JAR" -a -f "$LIBANTLR3C_TAR" ]; then
FILES_EXIST=1
ask_yn "Files appear to already be downloaded, use them? [Y/n] "
! is_yes "$yn" && antlr_download reset
else
ask_yn "Should the script download and build antlr and libantlr3c? [Y/n] "
is_yes "$yn" || exit
antlr_download
fi
# build/install libantlr3c
[ -d "$LIBANTLR3C" ] && rm -rf "$LIBANTLR3C"
tar xzf "$LIBANTLR3C_TAR" || err "Uncompress of $LIBANTLR3C_TAR failed!"
cd $LIBANTLR3C || err "Unable to cd to build $LIBANTLR3C build directory!"
./configure $ENABLE64BIT --prefix=$PREFIX && $MAKE
[ $? -ne 0 ] && err "Build of libantlr3c failed!"
# install antlr3 jar and wrapper
cd "$ORIG_DIR"
cd $WORKDIR
printf "#!/bin/sh
export CLASSPATH
CLASSPATH=\$CLASSPATH:$PREFIX_JAVA/${ANTLR3_JAR}:$PREFIX_JAVA
/usr/bin/java org.antlr.Tool \$*
" > antlr3
# save for later install attempts
echo "PREFIX=$PREFIX" > install_env
echo
prog_install

View File

@ -12,7 +12,7 @@ fi
DEPS="gmake autoconf automake libtool gettext gperf glib pkgconf wget git \
ffmpeg libconfuse libevent mxml libgcrypt libunistring libiconv curl \
libplist libinotify avahi sqlite3 alsa-lib libsodium json-c libwebsockets
protobuf-c"
protobuf-c bison flex"
echo "The script can install the following dependency packages for you:"
echo $DEPS
read -p "Should the script install these packages? [y/N] " yn
@ -46,30 +46,6 @@ if [ ! -d $WORKDIR ]; then
fi
cd $WORKDIR
read -p "Should the script install antlr and libantlr3c? [y/N] " yn
if [ "$yn" = "y" ]; then
read -p "Should the script build libantlr3c for 64 bit? [Y/n] " yn
if [ "$yn" != "n" ]; then
ENABLE64BIT="--enable-64bit"
fi
wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/antlr-3.4-complete.jar
wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/C/libantlr3c-3.4.tar.gz
sudo install antlr-3.4-complete.jar /usr/local/share/java
printf "#!/bin/sh
export CLASSPATH
CLASSPATH=\$CLASSPATH:/usr/local/share/java/antlr-3.4-complete.jar:/usr/local/share/java
/usr/local/bin/java org.antlr.Tool \$*
" > antlr3
sudo install -m 755 antlr3 /usr/local/bin
tar xzf libantlr3c-3.4.tar.gz
cd libantlr3c-3.4
./configure $ENABLE64BIT && gmake && sudo gmake install
cd $WORKDIR
fi
read -p "Should the script build owntone? [y/N] " yn
if [ "$yn" = "y" ]; then
git clone https://github.com/owntone/owntone-server.git

6
src/.gitignore vendored
View File

@ -1,11 +1,5 @@
owntone
*.tokens
*Lexer.[ch]
*Parser.[ch]
*2SQL.[ch]
*.u
daap_query_hash.h
rsp_query_hash.h
dacp_prop_hash.h

View File

@ -1,63 +0,0 @@
/*
* Copyright (C) 2009-2010 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
grammar DAAP;
options {
output = AST;
ASTLabelType = pANTLR3_BASE_TREE;
language = C;
}
query : expr NEWLINE? EOF -> expr
;
expr : aexpr (OPOR^ aexpr)*
;
aexpr : crit (OPAND^ crit)*
;
crit : LPAR expr RPAR -> expr
| STR
;
QUOTE : '\'';
LPAR : '(';
RPAR : ')';
OPAND : '+' | ' ';
OPOR : ',';
NEWLINE : '\r'? '\n';
/*
Unescaping adapted from (ported to the C runtime)
<http://stackoverflow.com/questions/504402/how-to-handle-escape-sequences-in-string-literals-in-antlr-3>
*/
STR
@init{ pANTLR3_STRING unesc = GETTEXT()->factory->newRaw(GETTEXT()->factory); }
: QUOTE ( reg = ~('\\' | '\'') { unesc->addc(unesc, reg); }
| esc = ESCAPED { unesc->appendS(unesc, GETTEXT()); } )+ QUOTE { SETTEXT(unesc); };
fragment
ESCAPED : '\\'
( '\\' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\\")); }
| '\'' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\'")); }
)
;

View File

@ -1,383 +0,0 @@
/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
tree grammar DAAP2SQL;
options {
tokenVocab = DAAP;
ASTLabelType = pANTLR3_BASE_TREE;
language = C;
}
@header {
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include "logger.h"
#include "db.h"
#include "daap_query.h"
}
@members {
struct dmap_query_field_map {
char *dmap_field;
char *db_col;
int as_int;
};
/* gperf static hash, daap_query.gperf */
#include "daap_query_hash.h"
}
query returns [ pANTLR3_STRING result ]
@init { $result = NULL; }
: e = expr
{
if (!$e.valid)
{
$result = NULL;
}
else
{
$result = $e.result->factory->newRaw($e.result->factory);
$result->append8($result, "(");
$result->appendS($result, $e.result);
$result->append8($result, ")");
}
}
;
expr returns [ pANTLR3_STRING result, int valid ]
@init { $result = NULL; $valid = 1; }
: ^(OPAND a = expr b = expr)
{
if ($a.valid && $b.valid)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "(");
$result->appendS($result, $a.result);
$result->append8($result, " AND ");
$result->appendS($result, $b.result);
$result->append8($result, ")");
}
else if ($a.valid)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->appendS($result, $a.result);
}
else if ($b.valid)
{
$result = $b.result->factory->newRaw($b.result->factory);
$result->appendS($result, $b.result);
}
else
{
$valid = 0;
}
}
| ^(OPOR a = expr b = expr)
{
if ($a.valid && $b.valid)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "(");
$result->appendS($result, $a.result);
$result->append8($result, " OR ");
$result->appendS($result, $b.result);
$result->append8($result, ")");
}
else if ($a.valid)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->appendS($result, $a.result);
}
else if ($b.valid)
{
$result = $b.result->factory->newRaw($b.result->factory);
$result->appendS($result, $b.result);
}
else
{
$valid = 0;
}
}
| STR
{
pANTLR3_STRING str;
pANTLR3_UINT8 field;
pANTLR3_UINT8 val;
pANTLR3_UINT8 escaped;
ANTLR3_UINT8 op;
int neg_op;
const struct dmap_query_field_map *dqfm;
char *end;
long long llval;
escaped = NULL;
$result = $STR.text->factory->newRaw($STR.text->factory);
str = $STR.text->toUTF8($STR.text);
/* NOTE: the lexer delivers the string without quotes
which may not be obvious from the grammar due to embedded code
*/
/* Make daap.songalbumid:0 a no-op */
if (strcmp((char *)str->chars, "daap.songalbumid:0") == 0)
{
$result->append8($result, "1 = 1");
goto STR_out;
}
field = str->chars;
val = field;
while ((*val != '\0') && ((*val == '.')
|| (*val == '-')
|| ((*val >= 'a') && (*val <= 'z'))
|| ((*val >= 'A') && (*val <= 'Z'))
|| ((*val >= '0') && (*val <= '9'))))
{
val++;
}
if (*field == '\0')
{
DPRINTF(E_LOG, L_DAAP, "No field name found in clause '\%s'\n", field);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
if (*val == '\0')
{
DPRINTF(E_LOG, L_DAAP, "No operator found in clause '\%s'\n", field);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
op = *val;
*val = '\0';
val++;
if (op == '!')
{
if (*val == '\0')
{
DPRINTF(E_LOG, L_DAAP, "Negation found but operator missing in clause '\%s\%c'\n", field, op);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
neg_op = 1;
op = *val;
val++;
}
else
neg_op = 0;
/* Lookup DMAP field in the query field map */
dqfm = daap_query_field_lookup((char *)field, strlen((char *)field));
if (!dqfm)
{
DPRINTF(E_LOG, L_DAAP, "DMAP field '\%s' is not a valid field in queries\n", field);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
/* Empty values OK for string fields, NOK for integer */
if (*val == '\0')
{
if (dqfm->as_int)
{
DPRINTF(E_LOG, L_DAAP, "No value given in clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
/* No need to exclude empty artist and album, as the server makes sure there always exists an artist/album. */
if (neg_op && (op == ':' || op == '@')
&& (strcmp((char *)field, "daap.songalbumartist") == 0
|| strcmp((char *)field, "daap.songartist") == 0
|| strcmp((char *)field, "daap.songalbum") == 0))
{
DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op);
$valid = 0;
goto STR_result_valid_0;
}
/* Need to check against NULL too */
if (op == ':' || op == '@')
$result->append8($result, "(");
}
/* Int field: check integer conversion */
if (dqfm->as_int)
{
errno = 0;
llval = strtoll((const char *)val, &end, 10);
if (((errno == ERANGE) && ((llval == LLONG_MAX) || (llval == LLONG_MIN)))
|| ((errno != 0) && (llval == 0)))
{
DPRINTF(E_LOG, L_DAAP, "Value '\%s' in clause '\%s\%s\%c\%s' does not convert to an integer type\n",
val, field, (neg_op) ? "!" : "", op, val);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
if (end == (char *)val)
{
DPRINTF(E_LOG, L_DAAP, "Value '\%s' in clause '\%s\%s\%c\%s' does not represent an integer value\n",
val, field, (neg_op) ? "!" : "", op, val);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
*end = '\0'; /* Cut out potential garbage - we're being kind */
/* The server only has media_kind = 1 for music - so remove media_kind = 32 to imporve select query performance. */
if (llval == 32
&& (strcmp((char *)field, "com.apple.itunes.mediakind") == 0
|| strcmp((char *)field, "com.apple.itunes.extended-media-kind") == 0))
{
DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c\%s'\n", field, (neg_op) ? "!" : "", op, val);
if (neg_op)
$result->append8($result, "1 = 1");
else
$result->append8($result, "1 = 0");
goto STR_out;
}
}
/* String field: escape string, check for '*' */
else
{
/* With Apple Music 1.0.1.37, we observed some ':' queries using '@' instead, resulting in 'Unknown operator' errors */
/* Ex: '/databases/1/containers?query=('com.apple.itunes.extended-media-kind@1', ...) */
if (op != ':' && op != '@')
{
DPRINTF(E_LOG, L_DAAP, "Operation '\%c' not valid for string values\n", op);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
escaped = (pANTLR3_UINT8)db_escape_string((char *)val);
if (!escaped)
{
DPRINTF(E_LOG, L_DAAP, "Could not escape value\n");
$valid = 0;
goto STR_result_valid_0; /* ABORT */
}
val = escaped;
if (val[0] == '*')
{
op = '\%';
val[0] = '\%';
}
if (val[0] && val[1] && val[strlen((char *)val) - 1] == '*')
{
op = '\%';
val[strlen((char *)val) - 1] = '\%';
}
}
$result->append8($result, dqfm->db_col);
switch(op)
{
case '@':
case ':':
if (neg_op)
$result->append8($result, " <> ");
else
$result->append8($result, " = ");
break;
case '+':
if (neg_op)
$result->append8($result, " <= ");
else
$result->append8($result, " > ");
break;
case '-':
if (neg_op)
$result->append8($result, " >= ");
else
$result->append8($result, " < ");
break;
case '\%':
$result->append8($result, " LIKE ");
break;
default:
if (neg_op)
DPRINTF(E_LOG, L_DAAP, "Missing or unknown operator '\%c' in clause '\%s!\%c\%s'\n", op, field, op, val);
else
DPRINTF(E_LOG, L_DAAP, "Unknown operator '\%c' in clause '\%s\%c\%s'\n", op, field, op, val);
$valid = 0;
goto STR_result_valid_0; /* ABORT */
break;
}
if (!dqfm->as_int)
$result->append8($result, "'");
$result->append8($result, (const char *)val);
if (!dqfm->as_int)
$result->append8($result, "'");
/* For empty string value, we need to check against NULL too */
if ((*val == '\0') && (op == ':' || op == '@'))
{
if (neg_op)
$result->append8($result, " AND ");
else
$result->append8($result, " OR ");
$result->append8($result, dqfm->db_col);
if (neg_op)
$result->append8($result, " IS NOT NULL");
else
$result->append8($result, " IS NULL");
$result->append8($result, ")");
}
STR_result_valid_0: /* bail out label */
;
if (escaped)
free(escaped);
STR_out: /* get out of here */
;
}
;

View File

@ -56,33 +56,19 @@ endif
GPERF_FILES = \
daap_query.gperf \
rsp_query.gperf \
dacp_prop.gperf \
dmap_fields.gperf
GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h)
ANTLR_GRAMMARS = \
RSP.g RSP2SQL.g \
DAAP.g DAAP2SQL.g \
SMARTPL.g SMARTPL2SQL.g
LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l
PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y
ANTLR_TOKENS = $(ANTLR_GRAMMARS:.g=.tokens)
ANTLR_DEPS = $(ANTLR_GRAMMARS:%.g=$(srcdir)/%.u)
ANTLR_SRC = \
RSPLexer.c RSPLexer.h RSPParser.c RSPParser.h \
RSP2SQL.c RSP2SQL.h \
DAAPLexer.c DAAPLexer.h DAAPParser.c DAAPParser.h \
DAAP2SQL.c DAAP2SQL.h \
SMARTPLLexer.c SMARTPLLexer.h SMARTPLParser.c SMARTPLParser.h \
SMARTPL2SQL.c SMARTPL2SQL.h
ANTLR_OBJECTS = \
RSPLexer.$(OBJEXT) RSPParser.$(OBJEXT) RSP2SQL.$(OBJEXT) \
DAAPLexer.$(OBJEXT) DAAPParser.$(OBJEXT) DAAP2SQL.$(OBJEXT) \
SMARTPLLexer.$(OBJEXT) SMARTPLParser.$(OBJEXT) SMARTPL2SQL.$(OBJEXT)
# 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) \
@ -129,8 +115,6 @@ owntone_SOURCES = main.c \
misc.c misc.h \
misc_json.c misc_json.h \
rng.c rng.h \
rsp_query.c rsp_query.h \
daap_query.c daap_query.h \
smartpl_query.c smartpl_query.h \
player.c player.h \
worker.c worker.h \
@ -153,41 +137,31 @@ owntone_SOURCES = main.c \
outputs/plist_wrap.h \
$(LIBWEBSOCKETS_SRC) \
$(GPERF_SRC) \
$(ANTLR_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) \
$(ANTLR_SRC) \
$(ANTLR_TOKENS) \
$(ANTLR_DEPS)
$(LEXER_SRC:.l=.h) $(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) \
$(ANTLR_GRAMMARS) \
$(ANTLR_TOKENS) \
$(ANTLR_DEPS)
# silence unused warnings from antlr generated files
$(ANTLR_OBJECTS): AM_CPPFLAGS += -Wno-unused
$(LEXER_SRC:.l=.h)
# gperf construction rules
%_hash.h: %.gperf
$(AM_V_GEN)$(GPERF) --output-file=$@ $<
# silent rules for antlr
antlr_verbose = $(antlr_verbose_@AM_V@)
antlr_verbose_ = $(antlr_verbose_@AM_DEFAULT_V@)
antlr_verbose_0 = @echo " GEN " $< "products";
# Rule for generating lexer headers. $@ is an automatic variable that is equal
# to the particular target name, so a header file name.
$(LEXER_SRC:.l=.h): $(LEXER_SRC)
$(LEX) --header-file=$@ --stdout $(@:.h=.l) > /dev/null
# ANTLR grammar products
%.tokens %.c %Lexer.c %Parser.c %Lexer.h %Parser.h %.h: %.g
$(antlr_verbose)$(ANTLR) -Xconversiontimeout 30000 $(ANTLR_OPTIONS) -fo . $<
# ANTLR dependency files (bypass circular dependency of .g on .tokens)
%.u: %.g
$(AM_V_GEN)$(ANTLR) -depend -fo . $< > $@
$(AM_V_at)$(SED) -n -e '/^.*\.g[ ]*:\(.*\)/ { s//\1/;h;d; }' -e '/\.tokens.*:/ { p;d; }' -e '/:/ { G;s/\n/ /;p; }' $@ > $@-t
$(AM_V_at)mv $@-t $@
-include $(ANTLR_DEPS)
# 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])

148
src/RSP.g
View File

@ -1,148 +0,0 @@
/*
* Copyright (C) 2009-2010 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
grammar RSP;
options {
output = AST;
ASTLabelType = pANTLR3_BASE_TREE;
language = C;
}
query : expr NEWLINE? EOF -> expr
;
expr : aexpr (OR^ aexpr)*
;
aexpr : crit (AND^ crit)*
;
crit : LPAR expr RPAR -> expr
| strcrit
| intcrit
| datecrit
;
strcrit : FIELD strop STR -> ^(strop FIELD STR)
| FIELD NOT strop STR -> ^(NOT ^(strop FIELD STR))
;
strop : equal=EQUAL
| includes=INCLUDES
| startsw=STARTSW
| endsw=ENDSW
;
intcrit : FIELD intop INT -> ^(intop FIELD INT)
| FIELD NOT intop INT -> ^(NOT ^(intop FIELD INT))
;
intop : equal=EQUAL
| less=LESS
| greater=GREATER
| lte=LTE
| gte=GTE
;
datecrit: FIELD dateop datespec -> ^(dateop FIELD datespec)
;
dateop : before=BEFORE
| after=AFTER
;
datespec: dateref
| INT dateintval dateop dateref -> ^(dateop dateref INT dateintval)
;
dateref : date=DATE
| today=TODAY
;
dateintval
: day=DAY
| week=WEEK
| month=MONTH
| year=YEAR
;
QUOTE : '"';
LPAR : '(';
RPAR : ')';
AND : 'and';
OR : 'or';
NOT : '!';
/* Both string & int */
EQUAL : '=';
/* String */
INCLUDES: 'includes';
STARTSW : 'startswith';
ENDSW : 'endswith';
/* Int */
GREATER : '>';
LESS : '<';
GTE : '>=';
LTE : '<=';
/* Date */
BEFORE : 'before';
AFTER : 'after';
DAY : 'day' | 'days';
WEEK : 'week' | 'weeks';
MONTH : 'month' | 'months';
YEAR : 'year' | 'years';
TODAY : 'today';
NEWLINE : '\r'? '\n';
WS : (' ' | '\t') { $channel = HIDDEN; };
FIELD : 'a'..'z' ('a'..'z' | '_')* 'a'..'z';
INT : DIGIT19 DIGIT09*;
/* YYYY-MM-DD */
DATE : DIGIT19 DIGIT09 DIGIT09 DIGIT09 '-' ('0' DIGIT19 | '1' '0'..'2') '-' ('0' DIGIT19 | '1'..'2' DIGIT09 | '3' '0'..'1');
/*
Unescaping adapted from (ported to the C runtime)
<http://stackoverflow.com/questions/504402/how-to-handle-escape-sequences-in-string-literals-in-antlr-3>
*/
STR
@init{ pANTLR3_STRING unesc = GETTEXT()->factory->newRaw(GETTEXT()->factory); }
: QUOTE ( reg = ~('\\' | '"') { unesc->addc(unesc, reg); }
| esc = ESCAPED { unesc->appendS(unesc, GETTEXT()); } )+ QUOTE { SETTEXT(unesc); }
;
fragment
ESCAPED : '\\'
( '\\' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\\")); }
| '"' { SETTEXT(GETTEXT()->factory->newStr8(GETTEXT()->factory, (pANTLR3_UINT8)"\"")); }
)
;
fragment
DIGIT09 : '0'..'9';
fragment
DIGIT19 : '1'..'9';

View File

@ -1,474 +0,0 @@
/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
tree grammar RSP2SQL;
options {
tokenVocab = RSP;
ASTLabelType = pANTLR3_BASE_TREE;
language = C;
}
@header {
/* Needs #define _GNU_SOURCE for strptime() */
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include "logger.h"
#include "db.h"
#include "misc.h"
#include "rsp_query.h"
}
@members {
#define RSP_TYPE_STRING 0
#define RSP_TYPE_INT 1
#define RSP_TYPE_DATE 2
struct rsp_query_field_map {
char *rsp_field;
int field_type;
/* RSP fields are named after the DB columns - or vice versa */
};
/* gperf static hash, rsp_query.gperf */
#include "rsp_query_hash.h"
}
query returns [ pANTLR3_STRING result ]
@init { $result = NULL; }
: e = expr
{
if (!$e.valid)
{
$result = NULL;
}
else
{
$result = $e.result->factory->newRaw($e.result->factory);
$result->append8($result, "(");
$result->appendS($result, $e.result);
$result->append8($result, ")");
}
}
;
expr returns [ pANTLR3_STRING result, int valid ]
@init { $result = NULL; $valid = 1; }
: ^(AND a = expr b = expr)
{
if (!$a.valid || !$b.valid)
{
$valid = 0;
}
else
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "(");
$result->appendS($result, $a.result);
$result->append8($result, " AND ");
$result->appendS($result, $b.result);
$result->append8($result, ")");
}
}
| ^(OR a = expr b = expr)
{
if (!$a.valid || !$b.valid)
{
$valid = 0;
}
else
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "(");
$result->appendS($result, $a.result);
$result->append8($result, " OR ");
$result->appendS($result, $b.result);
$result->append8($result, ")");
}
}
| c = strcrit
{
$valid = $c.valid;
$result = $c.result;
}
| ^(NOT c = strcrit)
{
if (!$c.valid || !$c.result)
{
$valid = 0;
}
else
{
$result = $c.result->factory->newRaw($c.result->factory);
$result->append8($result, "(NOT ");
$result->appendS($result, $c.result);
$result->append8($result, ")");
}
}
| i = intcrit
{
$valid = $i.valid;
$result = $i.result;
}
| ^(NOT i = intcrit)
{
if (!$i.valid || !$i.result)
{
$valid = 0;
}
else
{
$result = $i.result->factory->newRaw($i.result->factory);
$result->append8($result, "(NOT ");
$result->appendS($result, $i.result);
$result->append8($result, ")");
}
}
| d = datecrit
{
$valid = $d.valid;
$result = $d.result;
}
;
strcrit returns [ pANTLR3_STRING result, int valid ]
@init { $result = NULL; $valid = 1; }
: ^(o = strop f = FIELD s = STR)
{
char *op;
const struct rsp_query_field_map *rqfp;
pANTLR3_STRING field;
char *escaped;
ANTLR3_UINT32 optok;
escaped = NULL;
op = NULL;
optok = $o.op->getType($o.op);
switch (optok)
{
case EQUAL:
op = " = ";
break;
case INCLUDES:
case STARTSW:
case ENDSW:
op = " LIKE ";
break;
}
field = $f->getText($f);
/* Field lookup */
rqfp = rsp_query_field_lookup((char *)field->chars, strlen((char *)field->chars));
if (!rqfp)
{
DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a valid field in queries\n", field->chars);
$valid = 0;
goto strcrit_valid_0; /* ABORT */
}
/* Check field type */
if (rqfp->field_type != RSP_TYPE_STRING)
{
DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a string field\n", field->chars);
$valid = 0;
goto strcrit_valid_0; /* ABORT */
}
escaped = db_escape_string((char *)$s->getText($s)->chars);
if (!escaped)
{
DPRINTF(E_LOG, L_RSP, "Could not escape value\n");
$valid = 0;
goto strcrit_valid_0; /* ABORT */
}
$result = field->factory->newRaw(field->factory);
$result->append8($result, "f.");
$result->appendS($result, field);
$result->append8($result, op);
$result->append8($result, "'");
if ((optok == INCLUDES) || (optok == STARTSW))
$result->append8($result, "\%");
$result->append8($result, escaped);
if ((optok == INCLUDES) || (optok == ENDSW))
$result->append8($result, "\%");
$result->append8($result, "'");
strcrit_valid_0:
;
if (escaped)
free(escaped);
}
;
strop returns [ pANTLR3_COMMON_TOKEN op ]
@init { $op = NULL; }
: n = EQUAL
{ $op = $n->getToken($n); }
| n = INCLUDES
{ $op = $n->getToken($n); }
| n = STARTSW
{ $op = $n->getToken($n); }
| n = ENDSW
{ $op = $n->getToken($n); }
;
intcrit returns [ pANTLR3_STRING result, int valid ]
@init { $result = NULL; $valid = 1; }
: ^(o = intop f = FIELD i = INT)
{
char *op;
const struct rsp_query_field_map *rqfp;
pANTLR3_STRING field;
op = NULL;
switch ($o.op->getType($o.op))
{
case EQUAL:
op = " = ";
break;
case LESS:
op = " < ";
break;
case GREATER:
op = " > ";
break;
case LTE:
op = " <= ";
break;
case GTE:
op = " >= ";
break;
}
field = $f->getText($f);
/* Field lookup */
rqfp = rsp_query_field_lookup((char *)field->chars, strlen((char *)field->chars));
if (!rqfp)
{
DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a valid field in queries\n", field->chars);
$valid = 0;
goto intcrit_valid_0; /* ABORT */
}
/* Check field type */
if (rqfp->field_type != RSP_TYPE_INT)
{
DPRINTF(E_LOG, L_RSP, "Field '\%s' is not an integer field\n", field->chars);
$valid = 0;
goto intcrit_valid_0; /* ABORT */
}
$result = field->factory->newRaw(field->factory);
$result->append8($result, "f.");
$result->appendS($result, field);
$result->append8($result, op);
$result->appendS($result, $i->getText($i));
intcrit_valid_0:
;
}
;
intop returns [ pANTLR3_COMMON_TOKEN op ]
@init { $op = NULL; }
: n = EQUAL
{ $op = $n->getToken($n); }
| n = LESS
{ $op = $n->getToken($n); }
| n = GREATER
{ $op = $n->getToken($n); }
| n = LTE
{ $op = $n->getToken($n); }
| n = GTE
{ $op = $n->getToken($n); }
;
datecrit returns [ pANTLR3_STRING result, int valid ]
@init { $result = NULL; $valid = 1; }
: ^(o = dateop f = FIELD d = datespec)
{
char *op;
const struct rsp_query_field_map *rqfp;
pANTLR3_STRING field;
char buf[32];
int ret;
op = NULL;
switch ($o.op->getType($o.op))
{
case BEFORE:
op = " < ";
break;
case AFTER:
op = " > ";
break;
}
field = $f->getText($f);
/* Field lookup */
rqfp = rsp_query_field_lookup((char *)field->chars, strlen((char *)field->chars));
if (!rqfp)
{
DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a valid field in queries\n", field->chars);
$valid = 0;
goto datecrit_valid_0; /* ABORT */
}
/* Check field type */
if (rqfp->field_type != RSP_TYPE_DATE)
{
DPRINTF(E_LOG, L_RSP, "Field '\%s' is not a date field\n", field->chars);
$valid = 0;
goto datecrit_valid_0; /* ABORT */
}
ret = snprintf(buf, sizeof(buf), "\%ld", $d.date);
if ((ret < 0) || (ret >= sizeof(buf)))
{
DPRINTF(E_LOG, L_RSP, "Date \%ld too large for buffer, oops!\n", $d.date);
$valid = 0;
goto datecrit_valid_0; /* ABORT */
}
$result = field->factory->newRaw(field->factory);
$result->append8($result, "f.");
$result->appendS($result, field);
$result->append8($result, op);
$result->append8($result, buf);
datecrit_valid_0:
;
}
;
dateop returns [ pANTLR3_COMMON_TOKEN op ]
@init { $op = NULL; }
: n = BEFORE
{ $op = $n->getToken($n); }
| n = AFTER
{ $op = $n->getToken($n); }
;
datespec returns [ time_t date, int valid ]
@init { $date = 0; $valid = 1; }
: r = dateref
{
if (!$r.valid)
$valid = 0;
else
$date = $r.date;
}
| ^(o = dateop r = dateref m = INT i = dateintval)
{
int32_t val;
int ret;
if (!$r.valid || !$i.valid)
{
$valid = 0;
goto datespec_valid_0; /* ABORT */
}
ret = safe_atoi32((char *)$m->getText($m)->chars, &val);
if (ret < 0)
{
DPRINTF(E_LOG, L_RSP, "Could not convert '\%s' to integer\n", (char *)$m->getText($m));
$valid = 0;
goto datespec_valid_0; /* ABORT */
}
switch ($o.op->getType($o.op))
{
case BEFORE:
$date = $r.date - (val * $i.period);
break;
case AFTER:
$date = $r.date + (val * $i.period);
break;
}
datespec_valid_0:
;
}
;
dateref returns [ time_t date, int valid ]
@init { $date = 0; $valid = 1; }
: n = DATE
{
struct tm tm;
char *ret;
ret = strptime((char *)$n->getText($n), "\%Y-\%m-\%d", &tm);
if (!ret)
{
DPRINTF(E_LOG, L_RSP, "Date '\%s' could not be interpreted\n", (char *)$n->getText($n));
$valid = 0;
goto dateref_valid_0; /* ABORT */
}
else
{
if (*ret != '\0')
DPRINTF(E_LOG, L_RSP, "Garbage at end of date '\%s' ?!\n", (char *)$n->getText($n));
$date = mktime(&tm);
if ($date == (time_t) -1)
{
DPRINTF(E_LOG, L_RSP, "Date '\%s' could not be converted to an epoch\n", (char *)$n->getText($n));
$valid = 0;
goto dateref_valid_0; /* ABORT */
}
}
dateref_valid_0:
;
}
| TODAY
{ $date = time(NULL); }
;
dateintval returns [ time_t period, int valid ]
@init { $period = 0; $valid = 1; }
: DAY
{ $period = 24 * 60 * 60; }
| WEEK
{ $period = 7 * 24 * 60 * 60; }
| MONTH
{ $period = 30 * 24 * 60 * 60; }
| YEAR
{ $period = 365 * 24 * 60 * 60; }
;

View File

@ -1,240 +0,0 @@
/*
* Copyright (C) 2015 Christian Meffert <christian.meffert@googlemail.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
*/
grammar SMARTPL;
options {
output = AST;
ASTLabelType = pANTLR3_BASE_TREE;
language = C;
}
playlist : STR '{' expression '}' EOF
;
expression : aexpr (OR^ aexpr)* (HAVING^ aexpr)? (ORDERBY^ orderexpr)? (LIMIT^ limitexpr)?
;
orderexpr : ordertag SORTDIR
;
ordertag : STRTAG
| INTTAG
| DATETAG
| ENUMTAG
| RANDOMTAG
| (XXX)? NeverUsedRule
;
NeverUsedRule: /* antlr3 seems to have a problem with ordertag, introducing the NeverUsedRule fixes it. See: https://stackoverflow.com/questions/20057063/follow-set-in-is-undefined-in-generated-parser */
;
XXX : 'XXX' /**/
;
limitexpr : INT
;
aexpr : nexpr (AND^ nexpr)*
;
nexpr : NOT^ crit
| crit
;
crit : LPAR expression RPAR -> expression
| STRTAG (INCLUDES|IS|STARTSWITH) STR
| INTTAG INTBOOL INT
| DATETAG (AFTER|BEFORE) dateval
| ENUMTAG IS ENUMVAL
| GROUPTAG INTBOOL INT
;
dateval : DATE
| interval BEFORE DATE
| interval AFTER DATE
| interval AGO
;
interval : INT DATINTERVAL
;
STRTAG : 'artist'
| 'album_artist'
| 'album'
| 'title'
| 'genre'
| 'composer'
| 'path'
| 'type'
| 'grouping'
| 'artist_id'
| 'album_id'
| 'songartistid'
| 'songalbumid'
| 'codectype'
| 'comment'
;
INTTAG : 'play_count'
| 'skip_count'
| 'rating'
| 'year'
| 'compilation'
| 'track'
| 'disc'
| 'bitrate'
| 'bits_per_sample'
| 'samplerate'
| 'song_length'
| 'usermark'
;
DATETAG : 'time_added'
| 'time_modified'
| 'time_played'
| 'time_skipped'
| 'date_released'
;
ENUMTAG : 'data_kind'
| 'media_kind'
| 'scan_kind'
;
GROUPTAG : 'track_count'
| 'album_count'
;
RANDOMTAG : 'random'
;
INCLUDES : 'includes'
;
IS : 'is'
;
STARTSWITH : 'starts with'
;
INTBOOL : (GREATER|GREATEREQUAL|LESS|LESSEQUAL|EQUAL)
;
fragment
GREATER : '>'
;
fragment
GREATEREQUAL: '>='
;
fragment
LESS : '<'
;
fragment
LESSEQUAL : '<='
;
fragment
EQUAL : '='
;
AFTER : 'after'
;
BEFORE : 'before'
;
AGO : 'ago'
;
AND : 'AND'
| 'and'
;
OR : 'OR'
| 'or'
;
NOT : 'NOT'
| 'not'
;
LPAR : '('
;
RPAR : ')'
;
DATE : ('0'..'9')('0'..'9')('0'..'9')('0'..'9')'-'('0'..'1')('0'..'9')'-'('0'..'3')('0'..'9')
| 'today'
| 'yesterday'
| 'last week'
| 'last month'
| 'last year'
;
DATINTERVAL : 'days'
| 'weeks'
| 'months'
| 'years'
;
ENUMVAL : 'music'
| 'movie'
| 'podcast'
| 'audiobook'
| 'tvshow'
| 'file'
| 'url'
| 'spotify'
| 'pipe'
| 'files'
| 'rss'
;
ORDERBY : 'order by'
| 'ORDER BY'
;
SORTDIR : 'asc'
| 'ASC'
| 'desc'
| 'DESC'
;
LIMIT : 'limit'
| 'LIMIT'
;
HAVING : 'having'
| 'HAVING'
;
STR : '"' ~('"')+ '"'
;
INT : ('0'..'9')+
;
WHITESPACE : ('\t'|' '|'\r'|'\n'|'\u000C') { $channel = HIDDEN; }
;

View File

@ -1,427 +0,0 @@
/*
* Copyright (C) 2015 Christian Meffert <christian.meffert@googlemail.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
*/
tree grammar SMARTPL2SQL;
options {
tokenVocab = SMARTPL;
ASTLabelType = pANTLR3_BASE_TREE;
language = C;
}
@header {
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <time.h>
#include <sqlite3.h>
#include "logger.h"
#include "db.h"
}
@members {
static void append_date(pANTLR3_STRING result, const char *datval, char beforeorafter, const char *interval)
{
if (strcmp((char *)datval, "today") == 0)
{
result->append8(result, "strftime('\%s', datetime('now', 'start of day'");
}
else if (strcmp((char *)datval, "yesterday") == 0)
{
result->append8(result, "strftime('\%s', datetime('now', 'start of day', '-1 day'");
}
else if (strcmp((char *)datval, "last week") == 0)
{
result->append8(result, "strftime('\%s', datetime('now', 'start of day', 'weekday 0', '-13 days'");
}
else if (strcmp((char *)datval, "last month") == 0)
{
result->append8(result, "strftime('\%s', datetime('now', 'start of month', '-1 month'");
}
else if (strcmp((char *)datval, "last year") == 0)
{
result->append8(result, "strftime('\%s', datetime('now', 'start of year', '-1 year'");
}
else
{
result->append8(result, "strftime('\%s', datetime(\'");
result->append8(result, datval);
result->append8(result, "\'");
}
if (beforeorafter)
{
result->append8(result, ", '");
result->addc(result, beforeorafter);
result->append8(result, interval);
result->addc(result, '\'');
}
result->append8(result, ", 'utc'))");
}
}
playlist returns [ pANTLR3_STRING title, pANTLR3_STRING query, pANTLR3_STRING orderby, pANTLR3_STRING having, int limit ]
@init { $title = NULL; $query = NULL; $orderby = NULL; $having = NULL; $limit = -1; }
: STR '{' e = expression '}'
{
pANTLR3_UINT8 val;
val = $STR.text->toUTF8($STR.text)->chars;
val++;
val[strlen((const char *)val) - 1] = '\0';
$title = $STR.text->factory->newRaw($STR.text->factory);
$title->append8($title, (const char *)val);
$query = $e.result->factory->newRaw($e.result->factory);
$query->append8($query, "(");
$query->appendS($query, $e.result);
$query->append8($query, ")");
$limit = $e.limit;
$orderby = $e.result->factory->newRaw($e.result->factory);
$orderby->appendS($orderby, $e.orderby);
$having = $e.result->factory->newRaw($e.result->factory);
$having->appendS($having, $e.having);
}
;
expression returns [ pANTLR3_STRING result, pANTLR3_STRING orderby, pANTLR3_STRING having, int limit ]
@init { $result = NULL; $orderby = NULL; $having = NULL; $limit = -1; }
: ^(LIMIT a = expression INT)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->appendS($result, $a.result);
$having = $a.result->factory->newRaw($a.result->factory);
$having->appendS($having, $a.having);
$orderby = $a.result->factory->newRaw($a.result->factory);
$orderby->appendS($orderby, $a.orderby);
$limit = atoi((const char *)$INT.text->chars);
}
| ^(ORDERBY a = expression o = ordertag SORTDIR)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->appendS($result, $a.result);
$having = $a.result->factory->newRaw($a.result->factory);
$having->appendS($having, $a.having);
$orderby = $o.result->factory->newRaw($o.result->factory);
$orderby->appendS($orderby, $o.result);
$orderby->append8($orderby, " ");
$orderby->appendS($orderby, $SORTDIR.text->toUTF8($SORTDIR.text));
}
| ^(HAVING a = expression b = expression)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->appendS($result, $a.result);
$having = $b.result->factory->newRaw($b.result->factory);
$having->appendS($having, $b.result);
}
| ^(NOT a = expression)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "NOT(");
$result->appendS($result, $a.result);
$result->append8($result, ")");
}
| ^(AND a = expression b = expression)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "(");
$result->appendS($result, $a.result);
$result->append8($result, " AND ");
$result->appendS($result, $b.result);
$result->append8($result, ")");
}
| ^(OR a = expression b = expression)
{
$result = $a.result->factory->newRaw($a.result->factory);
$result->append8($result, "(");
$result->appendS($result, $a.result);
$result->append8($result, " OR ");
$result->appendS($result, $b.result);
$result->append8($result, ")");
}
| STRTAG INCLUDES STR
{
pANTLR3_UINT8 val;
char *tmp;
val = $STR.text->toUTF8($STR.text)->chars;
val++;
val[strlen((const char *)val) - 1] = '\0';
tmp = sqlite3_mprintf("\%q", (const char *)val);
$result = $STR.text->factory->newRaw($STR.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $STRTAG.text->toUTF8($STRTAG.text));
$result->append8($result, " LIKE '\%");
$result->append8($result, tmp);
$result->append8($result, "\%'");
sqlite3_free(tmp);
}
| STRTAG IS STR
{
pANTLR3_UINT8 val;
char *tmp;
val = $STR.text->toUTF8($STR.text)->chars;
val++;
val[strlen((const char *)val) - 1] = '\0';
tmp = sqlite3_mprintf("\%q", (const char *)val);
$result = $STR.text->factory->newRaw($STR.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $STRTAG.text->toUTF8($STRTAG.text));
$result->append8($result, " LIKE '");
$result->append8($result, tmp);
$result->append8($result, "'");
sqlite3_free(tmp);
}
| STRTAG STARTSWITH STR
{
pANTLR3_UINT8 val;
char *tmp;
val = $STR.text->toUTF8($STR.text)->chars;
val++;
val[strlen((const char *)val) - 1] = '\0';
tmp = sqlite3_mprintf("\%q", (const char *)val);
$result = $STR.text->factory->newRaw($STR.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $STRTAG.text->toUTF8($STRTAG.text));
$result->append8($result, " LIKE '");
$result->append8($result, tmp);
$result->append8($result, "\%'");
sqlite3_free(tmp);
}
| INTTAG INTBOOL INT
{
$result = $INTTAG.text->factory->newRaw($INTTAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $INTTAG.text->toUTF8($INTTAG.text));
$result->append8($result, " ");
$result->appendS($result, $INTBOOL.text->toUTF8($INTBOOL.text));
$result->append8($result, " ");
$result->appendS($result, $INT.text->toUTF8($INT.text));
}
| DATETAG AFTER dateval
{
$result = $DATETAG.text->factory->newRaw($DATETAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $DATETAG.text->toUTF8($DATETAG.text));
$result->append8($result, " > ");
$result->append8($result, (const char*)$dateval.result->chars);
}
| DATETAG BEFORE dateval
{
$result = $DATETAG.text->factory->newRaw($DATETAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $DATETAG.text->toUTF8($DATETAG.text));
$result->append8($result, " < ");
$result->append8($result, (const char*)$dateval.result->chars);
}
| ENUMTAG IS ENUMVAL
{
pANTLR3_UINT8 tag;
pANTLR3_UINT8 val;
char str[20];
sprintf(str, "1=1");
tag = $ENUMTAG.text->chars;
val = $ENUMVAL.text->chars;
if (strcmp((char *)tag, "media_kind") == 0)
{
if (strcmp((char *)val, "music") == 0)
{
sprintf(str, "f.media_kind = \%d", MEDIA_KIND_MUSIC);
}
else if (strcmp((char *)val, "movie") == 0)
{
sprintf(str, "f.media_kind = \%d", MEDIA_KIND_MOVIE);
}
else if (strcmp((char *)val, "podcast") == 0)
{
sprintf(str, "f.media_kind = \%d", MEDIA_KIND_PODCAST);
}
else if (strcmp((char *)val, "audiobook") == 0)
{
sprintf(str, "f.media_kind = \%d", MEDIA_KIND_AUDIOBOOK);
}
else if (strcmp((char *)val, "tvshow") == 0)
{
sprintf(str, "f.media_kind = \%d", MEDIA_KIND_TVSHOW);
}
}
else if (strcmp((char *)tag, "data_kind") == 0)
{
if (strcmp((char *)val, "file") == 0)
{
sprintf(str, "f.data_kind = \%d", DATA_KIND_FILE);
}
else if (strcmp((char *)val, "url") == 0)
{
sprintf(str, "f.data_kind = \%d", DATA_KIND_HTTP);
}
else if (strcmp((char *)val, "spotify") == 0)
{
sprintf(str, "f.data_kind = \%d", DATA_KIND_SPOTIFY);
}
else if (strcmp((char *)val, "pipe") == 0)
{
sprintf(str, "f.data_kind = \%d", DATA_KIND_PIPE);
}
}
else if (strcmp((char *)tag, "scan_kind") == 0)
{
if (strcmp((char *)val, "files") == 0)
{
sprintf(str, "f.scan_kind = \%d", SCAN_KIND_FILES);
}
else if (strcmp((char *)val, "spotify") == 0)
{
sprintf(str, "f.scan_kind = \%d", SCAN_KIND_SPOTIFY);
}
else if (strcmp((char *)val, "rss") == 0)
{
sprintf(str, "f.scan_kind = \%d", SCAN_KIND_RSS);
}
}
$result = $ENUMTAG.text->factory->newRaw($ENUMTAG.text->factory);
$result->append8($result, str);
}
| GROUPTAG INTBOOL INT
{
$result = $GROUPTAG.text->factory->newRaw($GROUPTAG.text->factory);
$result->appendS($result, $GROUPTAG.text->toUTF8($GROUPTAG.text));
$result->append8($result, " ");
$result->appendS($result, $INTBOOL.text->toUTF8($INTBOOL.text));
$result->append8($result, " ");
$result->appendS($result, $INT.text->toUTF8($INT.text));
}
;
ordertag returns [ pANTLR3_STRING result ]
@init { $result = NULL; }
: STRTAG
{
$result = $STRTAG.text->factory->newRaw($STRTAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $STRTAG.text->toUTF8($STRTAG.text));
}
| INTTAG
{
$result = $INTTAG.text->factory->newRaw($INTTAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $INTTAG.text->toUTF8($INTTAG.text));
}
| DATETAG
{
$result = $DATETAG.text->factory->newRaw($DATETAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $DATETAG.text->toUTF8($DATETAG.text));
}
| ENUMTAG
{
$result = $ENUMTAG.text->factory->newRaw($ENUMTAG.text->factory);
$result->append8($result, "f.");
$result->appendS($result, $ENUMTAG.text->toUTF8($ENUMTAG.text));
}
| RANDOMTAG
{
$result = $RANDOMTAG.text->factory->newRaw($RANDOMTAG.text->factory);
$result->append8($result, "random()");
}
;
dateval returns [ pANTLR3_STRING result ]
@init { $result = NULL; }
: DATE
{
pANTLR3_UINT8 datval;
datval = $DATE.text->chars;
$result = $DATE.text->factory->newRaw($DATE.text->factory);
append_date($result, (const char *)datval, 0, NULL);
}
| interval BEFORE DATE
{
$result = $DATE.text->factory->newRaw($DATE.text->factory);
append_date($result, (const char *)$DATE.text->chars, '-', (const char *)$interval.result->chars);
}
| interval AFTER DATE
{
$result = $DATE.text->factory->newRaw($DATE.text->factory);
append_date($result, (const char *)$DATE.text->chars, '+', (const char *)$interval.result->chars);
}
| interval AGO
{
$result = $AGO.text->factory->newRaw($AGO.text->factory);
append_date($result, "today", '-', (const char *)$interval.result->chars);
}
;
interval returns [ pANTLR3_STRING result ]
@init { $result = NULL; }
: INT DATINTERVAL
{
pANTLR3_UINT8 interval;
int intval;
char buf[25];
$result = $DATINTERVAL.text->factory->newRaw($DATINTERVAL.text->factory);
// SQL doesnt have a modifer for 'week' but for day/hr/min/sec/month/yr
interval = $DATINTERVAL.text->chars;
if (strcmp((char *)interval, "weeks") == 0)
{
intval = atoi((const char *)$INT.text->chars) * 7;
snprintf(buf, sizeof(buf), "\%d days", intval);
$result->append8($result, buf);
}
else
{
$result->append8($result, (const char *)$INT.text->chars);
$result->append8($result, " ");
$result->append8($result, (const char *)$DATINTERVAL.text->chars);
}
return $result;
}
;

View File

@ -1,157 +0,0 @@
/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include "logger.h"
#include "misc.h"
#include "daap_query.h"
#include "DAAPLexer.h"
#include "DAAPParser.h"
#include "DAAP2SQL.h"
char *
daap_query_parse_sql(const char *daap_query)
{
/* Input DAAP query, fed to the lexer */
pANTLR3_INPUT_STREAM query;
/* Lexer and the resulting token stream, fed to the parser */
pDAAPLexer lxr;
pANTLR3_COMMON_TOKEN_STREAM tkstream;
/* Parser and the resulting AST, fed to the tree parser */
pDAAPParser psr;
DAAPParser_query_return qtree;
pANTLR3_COMMON_TREE_NODE_STREAM nodes;
/* Tree parser and the resulting SQL query string */
pDAAP2SQL sqlconv;
pANTLR3_STRING sql;
char *ret = NULL;
if (!daap_query)
{
DPRINTF(E_LOG, L_DAAP, "DAAP query is null\n");
return NULL;
}
DPRINTF(E_DBG, L_DAAP, "Trying DAAP query -%s-\n", daap_query);
#if ANTLR3C_NEW_INPUT
query = antlr3StringStreamNew ((pANTLR3_UINT8)daap_query, ANTLR3_ENC_8BIT, (ANTLR3_UINT64)strlen(daap_query), (pANTLR3_UINT8)"DAAP query");
#else
query = antlr3NewAsciiStringInPlaceStream ((pANTLR3_UINT8)daap_query, (ANTLR3_UINT64)strlen(daap_query), (pANTLR3_UINT8)"DAAP query");
#endif
if (!query)
{
DPRINTF(E_DBG, L_DAAP, "Could not create input stream\n");
return NULL;
}
lxr = DAAPLexerNew(query);
if (!lxr)
{
DPRINTF(E_DBG, L_DAAP, "Could not create DAAP lexer\n");
goto lxr_fail;
}
tkstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr));
if (!tkstream)
{
DPRINTF(E_DBG, L_DAAP, "Could not create DAAP token stream\n");
goto tkstream_fail;
}
psr = DAAPParserNew(tkstream);
if (!psr)
{
DPRINTF(E_DBG, L_DAAP, "Could not create DAAP parser\n");
goto psr_fail;
}
qtree = psr->query(psr);
/* Check for parser errors */
if (psr->pParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_DAAP, "DAAP query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount);
goto psr_error;
}
DPRINTF(E_SPAM, L_DAAP, "DAAP query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars);
nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT);
if (!nodes)
{
DPRINTF(E_DBG, L_DAAP, "Could not create node stream\n");
goto psr_error;
}
sqlconv = DAAP2SQLNew(nodes);
if (!sqlconv)
{
DPRINTF(E_DBG, L_DAAP, "Could not create SQL converter\n");
goto sql_fail;
}
sql = sqlconv->query(sqlconv);
/* Check for tree parser errors */
if (sqlconv->pTreeParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_DAAP, "DAAP query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount);
goto sql_error;
}
if (sql)
{
DPRINTF(E_DBG, L_DAAP, "DAAP SQL query: -%s-\n", sql->chars);
ret = strdup((char *)sql->chars);
}
else
{
DPRINTF(E_LOG, L_DAAP, "Invalid DAAP query -%s-\n", daap_query);
ret = NULL;
}
sql_error:
sqlconv->free(sqlconv);
sql_fail:
nodes->free(nodes);
psr_error:
psr->free(psr);
psr_fail:
tkstream->free(tkstream);
tkstream_fail:
lxr->free(lxr);
lxr_fail:
query->close(query);
return ret;
}

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,12 +0,0 @@
#ifndef __DAAP_QUERY_H__
#define __DAAP_QUERY_H__
#include "logger.h"
#include "misc.h"
char *
daap_query_parse_sql(const char *daap_query);
#endif /* !__DAAP_QUERY_H__ */

View File

@ -28,7 +28,7 @@
#include "httpd.h"
#include "logger.h"
#include "dmap_common.h"
#include "parsers/daap_parser.h"
/* gperf static hash, dmap_fields.gperf */
#include "dmap_fields_hash.h"
@ -636,3 +636,21 @@ dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, str
return 0;
}
char *
dmap_query_parse_sql(const char *dmap_query)
{
struct daap_result result;
DPRINTF(E_SPAM, L_DAAP, "Parse DMAP query input '%s'\n", dmap_query);
if (daap_lex_parse(&result, dmap_query) != 0)
{
DPRINTF(E_LOG, L_DAAP, "Could not parse '%s': %s\n", dmap_query, result.errmsg);
return NULL;
}
DPRINTF(E_SPAM, L_DAAP, "Parse DMAP query output '%s'\n", result.str);
return safe_strdup(result.str);
}

View File

@ -88,4 +88,7 @@ dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, stru
int
dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_queue_item *queue_item);
char *
dmap_query_parse_sql(const char *dmap_query);
#endif /* !__DMAP_HELPERS_H__ */

View File

@ -51,7 +51,6 @@
#include "misc.h"
#include "transcode.h"
#include "artwork.h"
#include "daap_query.h"
#include "dmap_common.h"
#include "cache.h"
@ -597,7 +596,7 @@ query_params_set(struct query_params *qp, int *sort_headers, struct httpd_reques
{
DPRINTF(E_DBG, L_DAAP, "DAAP browse query filter: %s\n", param);
qp->filter = daap_query_parse_sql(param);
qp->filter = dmap_query_parse_sql(param);
if (!qp->filter)
DPRINTF(E_LOG, L_DAAP, "Ignoring improper DAAP query: %s\n", param);

View File

@ -46,7 +46,6 @@
#include "artwork.h"
#include "dmap_common.h"
#include "db.h"
#include "daap_query.h"
#include "player.h"
#include "listener.h"
@ -237,7 +236,7 @@ find_first_song_id(const char *query)
qp.sort = S_NONE;
qp.offset = 0;
qp.limit = 1;
qp.filter = daap_query_parse_sql(query);
qp.filter = dmap_query_parse_sql(query);
if (!qp.filter)
{
DPRINTF(E_LOG, L_DACP, "Improper DAAP query!\n");
@ -378,7 +377,7 @@ dacp_queueitem_add(const char *query, const char *queuefilter, const char *sort,
return -1;
}
qp.filter = daap_query_parse_sql(buf);
qp.filter = dmap_query_parse_sql(buf);
}
else
{
@ -387,14 +386,14 @@ dacp_queueitem_add(const char *query, const char *queuefilter, const char *sort,
// If the queuefilter is unkown, ignore it and use the query parameter instead to build the sql query
id = 0;
qp.type = Q_ITEMS;
qp.filter = daap_query_parse_sql(query);
qp.filter = dmap_query_parse_sql(query);
}
}
else
{
id = 0;
qp.type = Q_ITEMS;
qp.filter = daap_query_parse_sql(query);
qp.filter = dmap_query_parse_sql(query);
}
if (sort)

View File

@ -39,7 +39,7 @@
#include "misc.h"
#include "httpd.h"
#include "transcode.h"
#include "rsp_query.h"
#include "parsers/rsp_parser.h"
#define RSP_VERSION "1.0"
#define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?"
@ -209,9 +209,9 @@ rsp_send_error(struct evhttp_request *req, char *errmsg)
static int
query_params_set(struct query_params *qp, struct httpd_request *hreq)
{
struct rsp_result parse_result;
const char *param;
char query[1024];
char *filter;
int ret;
qp->offset = 0;
@ -243,6 +243,7 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq)
else
qp->idx_type = I_NONE;
qp->filter = NULL;
param = evhttp_find_header(hreq->query, "query");
if (param)
{
@ -263,19 +264,15 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq)
return -1;
}
qp->filter = rsp_query_parse_sql(query);
if (!qp->filter)
DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query\n");
if (rsp_lex_parse(&parse_result, query) != 0)
DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query: %s\n", query);
else
qp->filter = safe_asprintf("(%s) AND %s", parse_result.str, rsp_filter_files);
}
// Always filter to include only files (not streams and Spotify)
if (qp->filter)
filter = safe_asprintf("%s AND %s", qp->filter, rsp_filter_files);
else
filter = strdup(rsp_filter_files);
free(qp->filter);
qp->filter = filter;
if (!qp->filter)
qp->filter = strdup(rsp_filter_files);
return 0;
}

2
src/parsers/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*_lexer.[ch]
*_parser.[ch]

74
src/parsers/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; }
%%

450
src/parsers/daap_parser.y Normal file
View File

@ -0,0 +1,450 @@
/*
* 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"
#include "misc.h"
#else
#include "owntonefunctions.h"
#endif
}
/* Definition of struct that will hold the parsing result */
%code requires {
struct daap_result {
char str[1024];
int offset;
int err;
char errmsg[128];
};
}
%code {
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 (%zu 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;
*escape_char = 0;
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);
safe_snreplace(new, len, "%", "\\%");
safe_snreplace(new, len, "_", "\\_");
new[0] = new[strlen(new) - 1] = '%';
*escape_char = '\\';
*value = new;
}
static void sql_str_escape(char **value)
{
char *s = *value;
if (strchr(s, '\''))
safe_snreplace(s, strlen(s) + 1, "\\'", "'"); // See Kid's audiobooks test case
*value = db_escape_string(s);
free(s);
}
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 escape_char;
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, &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);
if (escape_char)
sql_append(result, " ESCAPE '%c'", escape_char);
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);
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; }
;
%%

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

@ -0,0 +1,99 @@
/*
* Copyright (C) 2021-2022 Espen Jürgensen <espenjurgensen@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/* =========================== BOILERPLATE SECTION ===========================*/
/* This is to avoid compiler warnings about unused functions. More options are
noyyalloc noyyrealloc noyyfree. */
%option noyywrap nounput noinput
/* Thread safe scanner */
%option reentrant
/* To avoid symbol name conflicts with multiple lexers */
%option prefix="rsp_"
/* Automake's ylwrap expexts the output to have this name */
%option outfile="lex.yy.c"
/* Makes a Bison-compatible yylex */
%option bison-bridge
%{
#include <stdio.h>
#include "rsp_parser.h"
/* Unknown why this is required despite using prefix */
#define YYSTYPE RSP_STYPE
%}
/* ========================= NON-BOILERPLATE SECTION =========================*/
/* quoted \"(\\.|[^\\"])*\" */
quoted \"(\\.|[^"])+\"
yyyymmdd [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
%%
[\n\t ]+ /* Ignore whitespace */
/* This selection of tags is based on what is actually seen from
current Soundsbridges and then some extra that seem plausible
it might use. Add more if needed.
*/
artist { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
album_artist { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
album { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
title { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
genre { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
composer { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
path { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
fname { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
type { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
orchestra { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
grouping { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
url { yylval->str = strdup(yytext); return RSP_T_STRTAG; }
id { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
bitrate { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
samplerate { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
song_length { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
track { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
disc { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
compilation { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
rating { yylval->str = strdup(yytext); return RSP_T_INTTAG; }
includes { return RSP_T_INCLUDES; }
= { return RSP_T_EQUAL; }
or { return RSP_T_OR; }
and { return RSP_T_AND; }
not { return RSP_T_NOT; }
{quoted} { yylval->str=strdup(yytext+1);
if(yylval->str[strlen(yylval->str)-1] == '"')
yylval->str[strlen(yylval->str)-1] = '\0';
return RSP_T_STRING; }
[0-9]+ { yylval->ival=atoi(yytext); return RSP_T_NUM; }
. { return yytext[0]; }
%%

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

@ -0,0 +1,416 @@
/*
* Copyright (C) 2021-2022 Espen Jürgensen <espenjurgensen@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/* =========================== BOILERPLATE SECTION ===========================*/
/* No global variables and yylex has scanner as argument */
%define api.pure true
/* Change prefix of symbols from yy to avoid clashes with any other parsers we
may want to link */
%define api.prefix {rsp_}
/* Gives better errors than "syntax error" */
%define parse.error verbose
/* Enables debug mode */
%define parse.trace
/* Adds output parameter to the parser */
%parse-param {struct rsp_result *result}
/* Adds "scanner" as argument to the parses calls to yylex, which is required
when the lexer is in reentrant mode. The type is void because caller caller
shouldn't need to know about yyscan_t */
%param {void *scanner}
%code provides {
/* Convenience functions for caller to use instead of interfacing with lexer and
parser directly */
int rsp_lex_cb(char *input, void (*cb)(int, const char *));
int rsp_lex_parse(struct rsp_result *result, const char *input);
}
/* Implementation of the convenience function and the parsing error function
required by Bison */
%code {
#include "rsp_lexer.h"
int rsp_lex_cb(char *input, void (*cb)(int, const char *))
{
int ret;
yyscan_t scanner;
YY_BUFFER_STATE buf;
YYSTYPE val;
if ((ret = rsp_lex_init(&scanner)) != 0)
return ret;
buf = rsp__scan_string(input, scanner);
while ((ret = rsp_lex(&val, scanner)) > 0)
cb(ret, rsp_get_text(scanner));
rsp__delete_buffer(buf, scanner);
rsp_lex_destroy(scanner);
return 0;
}
int rsp_lex_parse(struct rsp_result *result, const char *input)
{
YY_BUFFER_STATE buffer;
yyscan_t scanner;
int retval = -1;
int ret;
result->errmsg[0] = '\0'; // For safety
ret = rsp_lex_init(&scanner);
if (ret != 0)
goto error_init;
buffer = rsp__scan_string(input, scanner);
if (!buffer)
goto error_buffer;
ret = rsp_parse(result, scanner);
if (ret != 0)
goto error_parse;
retval = 0;
error_parse:
rsp__delete_buffer(buffer, scanner);
error_buffer:
rsp_lex_destroy(scanner);
error_init:
return retval;
}
void rsp_error(struct rsp_result *result, yyscan_t scanner, const char *msg)
{
snprintf(result->errmsg, sizeof(result->errmsg), "%s", msg);
}
}
/* ============ ABSTRACT SYNTAX TREE (AST) BOILERPLATE SECTION ===============*/
%code {
struct ast
{
int type;
struct ast *l;
struct ast *r;
void *data;
int ival;
};
__attribute__((unused)) static struct ast * ast_new(int type, struct ast *l, struct ast *r)
{
struct ast *a = calloc(1, sizeof(struct ast));
a->type = type;
a->l = l;
a->r = r;
return a;
}
/* Note *data is expected to be freeable with regular free() */
__attribute__((unused)) static struct ast * ast_data(int type, void *data)
{
struct ast *a = calloc(1, sizeof(struct ast));
a->type = type;
a->data = data;
return a;
}
__attribute__((unused)) static struct ast * ast_int(int type, int ival)
{
struct ast *a = calloc(1, sizeof(struct ast));
a->type = type;
a->ival = ival;
return a;
}
__attribute__((unused)) static void ast_free(struct ast *a)
{
if (!a)
return;
ast_free(a->l);
ast_free(a->r);
free(a->data);
free(a);
}
}
%destructor { free($$); } <str>
%destructor { ast_free($$); } <ast>
/* ========================= NON-BOILERPLATE SECTION =========================*/
/* Includes required by the parser rules */
%code top {
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdarg.h> // For vsnprintf
#include <assert.h>
#define INVERT_MASK 0x80000000
}
/* Dependencies, mocked or real */
%code top {
#ifndef DEBUG_PARSER_MOCK
#include "db.h"
#include "misc.h"
#else
#include "owntonefunctions.h"
#endif
}
/* Definition of struct that will hold the parsing result */
%code requires {
struct rsp_result {
char str[1024];
int offset;
int err;
char errmsg[128];
};
}
%code {
enum sql_append_type {
SQL_APPEND_OPERATOR,
SQL_APPEND_OPERATOR_STR,
SQL_APPEND_OPERATOR_LIKE,
SQL_APPEND_FIELD,
SQL_APPEND_STR,
SQL_APPEND_INT,
SQL_APPEND_PARENS,
};
static void sql_from_ast(struct rsp_result *, struct ast *);
// Escapes any '%' or '_' that might be in the string
static void sql_like_escape(char **value, char *escape_char)
{
char *s = *value;
size_t len = strlen(s);
char *new;
*escape_char = 0;
// Fast path, nothing to escape
if (!strpbrk(s, "_%"))
return;
len = 2 * len; // Enough for every char to be escaped
new = realloc(s, len);
safe_snreplace(new, len, "%", "\\%");
safe_snreplace(new, len, "_", "\\_");
*escape_char = '\\';
*value = new;
}
static void sql_str_escape(char **value)
{
char *s = *value;
if (strchr(s, '\"'))
safe_snreplace(s, strlen(s) + 1, "\\\"", "\""); // See test case 3
*value = db_escape_string(s);
free(s);
}
static void sql_append(struct rsp_result *result, const char *fmt, ...)
{
va_list ap;
int remaining = sizeof(result->str) - result->offset;
int ret;
if (remaining <= 0)
goto nospace;
va_start(ap, fmt);
ret = vsnprintf(result->str + result->offset, remaining, fmt, ap);
va_end(ap);
if (ret < 0 || ret >= remaining)
goto nospace;
result->offset += ret;
return;
nospace:
snprintf(result->errmsg, sizeof(result->errmsg), "Parser output buffer too small (%zu bytes)", sizeof(result->str));
result->err = -2;
}
static void sql_append_recursive(struct rsp_result *result, struct ast *a, const char *op, const char *op_not, bool is_not, enum sql_append_type append_type)
{
char escape_char;
switch (append_type)
{
case SQL_APPEND_OPERATOR:
sql_from_ast(result, a->l);
sql_append(result, " %s ", is_not ? op_not : op);
sql_from_ast(result, a->r);
break;
case SQL_APPEND_OPERATOR_STR:
sql_from_ast(result, a->l);
sql_append(result, " %s '", is_not ? op_not : op);
sql_from_ast(result, a->r);
sql_append(result, "'");
break;
case SQL_APPEND_OPERATOR_LIKE:
sql_from_ast(result, a->l);
sql_append(result, " %s '%%", is_not ? op_not : op);
sql_like_escape((char **)(&a->r->data), &escape_char);
sql_from_ast(result, a->r);
sql_append(result, "%%'");
if (escape_char)
sql_append(result, " ESCAPE '%c'", escape_char);
break;
case SQL_APPEND_FIELD:
assert(a->l == NULL);
assert(a->r == NULL);
sql_append(result, "f.%s", (char *)a->data);
break;
case SQL_APPEND_STR:
assert(a->l == NULL);
assert(a->r == NULL);
sql_str_escape((char **)&a->data);
sql_append(result, "%s", (char *)a->data);
break;
case SQL_APPEND_INT:
assert(a->l == NULL);
assert(a->r == NULL);
sql_append(result, "%d", a->ival);
break;
case SQL_APPEND_PARENS:
assert(a->r == NULL);
sql_append(result, "(");
sql_from_ast(result, a->l);
sql_append(result, ")");
break;
}
}
static void sql_from_ast(struct rsp_result *result, struct ast *a)
{
if (!a || result->err < 0)
return;
// Not currently used since grammar below doesn't ever set with INVERT_MASK
bool is_not = (a->type & INVERT_MASK);
a->type &= ~INVERT_MASK;
switch (a->type)
{
case RSP_T_OR:
sql_append_recursive(result, a, "OR", "OR NOT", is_not, SQL_APPEND_OPERATOR); break;
case RSP_T_AND:
sql_append_recursive(result, a, "AND", "AND NOT", is_not, SQL_APPEND_OPERATOR); break;
case RSP_T_EQUAL:
sql_append_recursive(result, a, "=", "!=", is_not, SQL_APPEND_OPERATOR); break;
case RSP_T_IS:
sql_append_recursive(result, a, "=", "!=", is_not, SQL_APPEND_OPERATOR_STR); break;
case RSP_T_INCLUDES:
sql_append_recursive(result, a, "LIKE", "NOT LIKE", is_not, SQL_APPEND_OPERATOR_LIKE); break;
case RSP_T_STRTAG:
case RSP_T_INTTAG:
sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_FIELD); break;
case RSP_T_NUM:
sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_INT); break;
case RSP_T_STRING:
sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_STR); break;
break;
case RSP_T_PARENS:
sql_append_recursive(result, a, NULL, NULL, 0, SQL_APPEND_PARENS); break;
default:
snprintf(result->errmsg, sizeof(result->errmsg), "Parser produced unrecognized AST type %d", a->type);
result->err = -1;
}
}
static int result_set(struct rsp_result *result, struct ast *a)
{
memset(result, 0, sizeof(struct rsp_result));
sql_from_ast(result, a);
ast_free(a);
return result->err;
}
}
%union {
unsigned int ival;
char *str;
struct ast *ast;
}
/* A string that was quoted. Quotes were stripped by lexer. */
%token <str> RSP_T_STRING
/* A number (integer) */
%token <ival> RSP_T_NUM
/* The semantic value holds the actual name of the field */
%token <str> RSP_T_STRTAG
%token <str> RSP_T_INTTAG
%token RSP_T_PARENS
%token RSP_T_OR
%token RSP_T_AND
%token RSP_T_NOT
%token RSP_T_EQUAL
%token RSP_T_IS
%token RSP_T_INCLUDES
%left RSP_T_OR RSP_T_AND
%type <ast> criteria
%type <ast> predicate
%%
query:
criteria { return result_set(result, $1); }
;
criteria: criteria RSP_T_AND criteria { $$ = ast_new(RSP_T_AND, $1, $3); }
| criteria RSP_T_OR criteria { $$ = ast_new(RSP_T_OR, $1, $3); }
| '(' criteria ')' { $$ = ast_new(RSP_T_PARENS, $2, NULL); }
| predicate
;
predicate: RSP_T_STRTAG RSP_T_EQUAL RSP_T_STRING { $$ = ast_new(RSP_T_IS, ast_data(RSP_T_STRTAG, $1), ast_data(RSP_T_STRING, $3)); }
| RSP_T_STRTAG RSP_T_INCLUDES RSP_T_STRING { $$ = ast_new(RSP_T_INCLUDES, ast_data(RSP_T_STRTAG, $1), ast_data(RSP_T_STRING, $3)); }
| RSP_T_INTTAG RSP_T_EQUAL RSP_T_NUM { $$ = ast_new(RSP_T_EQUAL, ast_data(RSP_T_INTTAG, $1), ast_int(RSP_T_NUM, $3)); }
;
%%

161
src/parsers/smartpl_lexer.l Normal file
View File

@ -0,0 +1,161 @@
/*
* 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 =========================*/
%option case-insensitive
/* quoted \"(\\(.|\n)|[^\\"\n])*\" */
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; }
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_ENUMTAG_DATAKIND; }
media_kind { yylval->str = strdup(yytext); return SMARTPL_T_ENUMTAG_MEDIAKIND; }
scan_kind { yylval->str = strdup(yytext); return SMARTPL_T_ENUMTAG_SCANKIND; }
file { return SMARTPL_T_FILE; }
files { return SMARTPL_T_FILE; }
url { return SMARTPL_T_URL; }
spotify { return SMARTPL_T_SPOTIFY; }
pipe { return SMARTPL_T_PIPE; }
rss { return SMARTPL_T_RSS; }
music { return SMARTPL_T_MUSIC; }
movie { return SMARTPL_T_MOVIE; }
podcast { return SMARTPL_T_PODCAST; }
audiobook { return SMARTPL_T_AUDIOBOOK; }
tvshow { return SMARTPL_T_TVSHOW; }
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_EQUAL); }
\<= { 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_STRING; }
[0-9]+ { yylval->ival=atoi(yytext); return SMARTPL_T_NUM; }
. { return yytext[0]; }
%%

View File

@ -0,0 +1,642 @@
/*
* 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
}
/* Dependencies, mocked or real */
%code top {
#ifndef DEBUG_PARSER_MOCK
#include "db.h"
#include "misc.h"
#else
#include "owntonefunctions.h"
#endif
}
/* Definition of struct that will hold the parsing result */
%code requires {
struct 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 *);
// Escapes any '%' or '_' that might be in the string
static void sql_like_escape(char **value, char *escape_char)
{
char *s = *value;
size_t len = strlen(s);
char *new;
*escape_char = 0;
// Fast path, nothing to escape
if (!strpbrk(s, "_%"))
return;
len = 2 * len; // Enough for every char to be escaped
new = realloc(s, len);
safe_snreplace(new, len, "%", "\\%");
safe_snreplace(new, len, "_", "\\_");
*escape_char = '\\';
*value = new;
}
static void sql_str_escape(char **value)
{
char *old = *value;
*value = db_escape_string(old);
free(old);
}
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 (%zu 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)
{
char escape_char;
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_like_escape((char **)(&a->r->data), &escape_char);
sql_from_ast(result, part, a->r);
sql_append(result, part, "%%'");
if (escape_char)
sql_append(result, part, " ESCAPE '%c'", escape_char);
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_str_escape((char **)&a->data);
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_EQUAL:
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_STRING:
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_ENUMTAG:
sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_FIELD); break;
case SMARTPL_T_NUM:
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_STRING
/* Numbers (integers) */
%token <ival> SMARTPL_T_NUM
/* 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_GROUPTAG
%token SMARTPL_T_ENUMTAG
%token <str> SMARTPL_T_ENUMTAG_DATAKIND
%token <str> SMARTPL_T_ENUMTAG_MEDIAKIND
%token <str> SMARTPL_T_ENUMTAG_SCANKIND
%token SMARTPL_T_FILE;
%token SMARTPL_T_URL;
%token SMARTPL_T_SPOTIFY;
%token SMARTPL_T_PIPE;
%token SMARTPL_T_RSS;
%token SMARTPL_T_MUSIC;
%token SMARTPL_T_MOVIE;
%token SMARTPL_T_PODCAST;
%token SMARTPL_T_AUDIOBOOK;
%token SMARTPL_T_TVSHOW;
%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
/* 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_IS
%token <ival> SMARTPL_T_INCLUDES
%token <ival> SMARTPL_T_EQUAL
%token <ival> SMARTPL_T_LESS
%token <ival> SMARTPL_T_LESSEQUAL
%token <ival> SMARTPL_T_GREATER
%token <ival> SMARTPL_T_GREATEREQUAL
%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> enumexpr
%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_STRING '{' criteria having order limit '}' { return result_set(result, $1, $3, $4, $5, $6); }
| SMARTPL_T_STRING '{' criteria having order '}' { return result_set(result, $1, $3, $4, $5, NULL); }
| SMARTPL_T_STRING '{' criteria having limit '}' { return result_set(result, $1, $3, $4, NULL, $5); }
| SMARTPL_T_STRING '{' criteria having '}' { return result_set(result, $1, $3, $4, NULL, NULL); }
| SMARTPL_T_STRING '{' criteria order limit '}' { return result_set(result, $1, $3, NULL, $4, $5); }
| SMARTPL_T_STRING '{' criteria order '}' { return result_set(result, $1, $3, NULL, $4, NULL); }
| SMARTPL_T_STRING '{' criteria limit '}' { return result_set(result, $1, $3, NULL, NULL, $4); }
| SMARTPL_T_STRING '{' 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_STRING { $$ = ast_new($2, ast_data(SMARTPL_T_STRTAG, $1), ast_data(SMARTPL_T_STRING, $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_NOT predicate { struct ast *a = $2; a->type |= INVERT_MASK; $$ = $2; }
| enumexpr
;
enumexpr:
/* DATA_KIND */
SMARTPL_T_ENUMTAG_DATAKIND SMARTPL_T_IS SMARTPL_T_FILE { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, DATA_KIND_FILE)); }
| SMARTPL_T_ENUMTAG_DATAKIND SMARTPL_T_IS SMARTPL_T_URL { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, DATA_KIND_HTTP)); }
| SMARTPL_T_ENUMTAG_DATAKIND SMARTPL_T_IS SMARTPL_T_SPOTIFY { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, DATA_KIND_SPOTIFY)); }
| SMARTPL_T_ENUMTAG_DATAKIND SMARTPL_T_IS SMARTPL_T_PIPE { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, DATA_KIND_PIPE)); }
/* MEDIA_KIND */
| SMARTPL_T_ENUMTAG_MEDIAKIND SMARTPL_T_IS SMARTPL_T_MUSIC { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, MEDIA_KIND_MUSIC)); }
| SMARTPL_T_ENUMTAG_MEDIAKIND SMARTPL_T_IS SMARTPL_T_MOVIE { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, MEDIA_KIND_MOVIE)); }
| SMARTPL_T_ENUMTAG_MEDIAKIND SMARTPL_T_IS SMARTPL_T_PODCAST { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, MEDIA_KIND_PODCAST)); }
| SMARTPL_T_ENUMTAG_MEDIAKIND SMARTPL_T_IS SMARTPL_T_AUDIOBOOK { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, MEDIA_KIND_AUDIOBOOK)); }
| SMARTPL_T_ENUMTAG_MEDIAKIND SMARTPL_T_IS SMARTPL_T_TVSHOW { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, MEDIA_KIND_TVSHOW)); }
/* SCAN_KIND */
| SMARTPL_T_ENUMTAG_SCANKIND SMARTPL_T_IS SMARTPL_T_FILE { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, SCAN_KIND_FILES)); }
| SMARTPL_T_ENUMTAG_SCANKIND SMARTPL_T_IS SMARTPL_T_SPOTIFY { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, SCAN_KIND_SPOTIFY)); }
| SMARTPL_T_ENUMTAG_SCANKIND SMARTPL_T_IS SMARTPL_T_RSS { $$ = ast_new(SMARTPL_T_EQUAL, ast_data(SMARTPL_T_ENUMTAG, $1), ast_int(SMARTPL_T_NUM, SCAN_KIND_RSS)); }
;
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_ENUMTAG_DATAKIND { $$ = ast_data(SMARTPL_T_ORDERBY, $2); }
| SMARTPL_T_ORDERBY SMARTPL_T_ENUMTAG_MEDIAKIND { $$ = ast_data(SMARTPL_T_ORDERBY, $2); }
| SMARTPL_T_ORDERBY SMARTPL_T_ENUMTAG_SCANKIND { $$ = 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_EQUAL
| SMARTPL_T_LESS
| SMARTPL_T_LESSEQUAL
| SMARTPL_T_GREATER
| SMARTPL_T_GREATEREQUAL
;
datebool: SMARTPL_T_BEFORE
| SMARTPL_T_AFTER
;
%%

View File

@ -1,150 +0,0 @@
/*
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include "logger.h"
#include "misc.h"
#include "rsp_query.h"
#include "RSPLexer.h"
#include "RSPParser.h"
#include "RSP2SQL.h"
char *
rsp_query_parse_sql(const char *rsp_query)
{
/* Input RSP query, fed to the lexer */
pANTLR3_INPUT_STREAM query;
/* Lexer and the resulting token stream, fed to the parser */
pRSPLexer lxr;
pANTLR3_COMMON_TOKEN_STREAM tkstream;
/* Parser and the resulting AST, fed to the tree parser */
pRSPParser psr;
RSPParser_query_return qtree;
pANTLR3_COMMON_TREE_NODE_STREAM nodes;
/* Tree parser and the resulting SQL query string */
pRSP2SQL sqlconv;
pANTLR3_STRING sql;
char *ret = NULL;
DPRINTF(E_DBG, L_RSP, "Trying RSP query -%s-\n", rsp_query);
#if ANTLR3C_NEW_INPUT
query = antlr3StringStreamNew ((pANTLR3_UINT8)rsp_query, ANTLR3_ENC_8BIT, (ANTLR3_UINT64)strlen(rsp_query), (pANTLR3_UINT8)"RSP query");
#else
query = antlr3NewAsciiStringInPlaceStream ((pANTLR3_UINT8)rsp_query, (ANTLR3_UINT64)strlen(rsp_query), (pANTLR3_UINT8)"RSP query");
#endif
if (!query)
{
DPRINTF(E_DBG, L_RSP, "Could not create input stream\n");
return NULL;
}
lxr = RSPLexerNew(query);
if (!lxr)
{
DPRINTF(E_DBG, L_RSP, "Could not create RSP lexer\n");
goto lxr_fail;
}
tkstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr));
if (!tkstream)
{
DPRINTF(E_DBG, L_RSP, "Could not create RSP token stream\n");
goto tkstream_fail;
}
psr = RSPParserNew(tkstream);
if (!psr)
{
DPRINTF(E_DBG, L_RSP, "Could not create RSP parser\n");
goto psr_fail;
}
qtree = psr->query(psr);
/* Check for parser errors */
if (psr->pParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_RSP, "RSP query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount);
goto psr_error;
}
DPRINTF(E_SPAM, L_RSP, "RSP query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars);
nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT);
if (!nodes)
{
DPRINTF(E_DBG, L_RSP, "Could not create node stream\n");
goto psr_error;
}
sqlconv = RSP2SQLNew(nodes);
if (!sqlconv)
{
DPRINTF(E_DBG, L_RSP, "Could not create SQL converter\n");
goto sql_fail;
}
sql = sqlconv->query(sqlconv);
/* Check for tree parser errors */
if (sqlconv->pTreeParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_RSP, "RSP query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount);
goto sql_error;
}
if (sql)
{
ret = strdup((char *)sql->chars);
}
else
{
DPRINTF(E_LOG, L_RSP, "Invalid RSP query\n");
ret = NULL;
}
sql_error:
sqlconv->free(sqlconv);
sql_fail:
nodes->free(nodes);
psr_error:
psr->free(psr);
psr_fail:
tkstream->free(tkstream);
tkstream_fail:
lxr->free(lxr);
lxr_fail:
query->close(query);
return ret;
}

View File

@ -1,54 +0,0 @@
%language=ANSI-C
%readonly-tables
%enum
%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;
%%
"id", RSP_TYPE_INT
"path", RSP_TYPE_STRING
"fname", RSP_TYPE_STRING
"title", RSP_TYPE_STRING
"artist", RSP_TYPE_STRING
"album", RSP_TYPE_STRING
"genre", RSP_TYPE_STRING
"comment", RSP_TYPE_STRING
"type", RSP_TYPE_STRING
"composer", RSP_TYPE_STRING
"orchestra", RSP_TYPE_STRING
"grouping", RSP_TYPE_STRING
"url", RSP_TYPE_STRING
"bitrate", RSP_TYPE_INT
"samplerate", RSP_TYPE_INT
"song_length", RSP_TYPE_INT
"file_size", RSP_TYPE_INT
"year", RSP_TYPE_INT
"track", RSP_TYPE_INT
"total_tracks", RSP_TYPE_INT
"disc", RSP_TYPE_INT
"total_discs", RSP_TYPE_INT
"bpm", RSP_TYPE_INT
"compilation", RSP_TYPE_INT
"rating", RSP_TYPE_INT
"play_count", RSP_TYPE_INT
"skip_count", RSP_TYPE_INT
"data_kind", RSP_TYPE_INT
"item_kind", RSP_TYPE_INT
"description", RSP_TYPE_STRING
"time_added", RSP_TYPE_DATE
"time_modified", RSP_TYPE_DATE
"time_played", RSP_TYPE_DATE
"time_skipped", RSP_TYPE_DATE
"db_timestamp", RSP_TYPE_DATE
"sample_count", RSP_TYPE_INT
"codectype", RSP_TYPE_STRING
"idx", RSP_TYPE_INT
"has_video", RSP_TYPE_INT
"contentrating", RSP_TYPE_INT
"bits_per_sample", RSP_TYPE_INT
"album_artist", RSP_TYPE_STRING

View File

@ -1,9 +0,0 @@
#ifndef __RSP_QUERY_H__
#define __RSP_QUERY_H__
char *
rsp_query_parse_sql(const char *rsp_query);
#endif /* !__RSP_QUERY_H__ */

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,186 +32,90 @@
#include <errno.h>
#include "smartpl_query.h"
#include "parsers/smartpl_parser.h"
#include "logger.h"
#include "misc.h"
#include "SMARTPLLexer.h"
#include "SMARTPLParser.h"
#include "SMARTPL2SQL.h"
static int
parse_input(struct smartpl *smartpl, pANTLR3_INPUT_STREAM input)
{
pSMARTPLLexer lxr;
pANTLR3_COMMON_TOKEN_STREAM tstream;
pSMARTPLParser psr;
SMARTPLParser_playlist_return qtree;
pANTLR3_COMMON_TREE_NODE_STREAM nodes;
pSMARTPL2SQL sqlconv;
SMARTPL2SQL_playlist_return plreturn;
int ret;
lxr = SMARTPLLexerNew(input);
// Need to check for errors
if (lxr == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL lexer\n");
ret = -1;
goto lxr_fail;
}
tstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr));
if (tstream == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL token stream\n");
ret = -1;
goto tkstream_fail;
}
// Finally, now that we have our lexer constructed, we can create the parser
psr = SMARTPLParserNew(tstream); // CParserNew is generated by ANTLR3
if (psr == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL parser\n");
ret = -1;
goto psr_fail;
}
qtree = psr->playlist(psr);
/* Check for parser errors */
if (psr->pParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_SCAN, "SMARTPL query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount);
ret = -1;
goto psr_error;
}
DPRINTF(E_DBG, L_SCAN, "SMARTPL query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars);
nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT);
if (!nodes)
{
DPRINTF(E_LOG, L_SCAN, "Could not create node stream\n");
ret = -1;
goto psr_error;
}
sqlconv = SMARTPL2SQLNew(nodes);
if (!sqlconv)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SQL converter\n");
ret = -1;
goto sql_fail;
}
plreturn = sqlconv->playlist(sqlconv);
/* Check for tree parser errors */
if (sqlconv->pTreeParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_SCAN, "SMARTPL query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount);
ret = -1;
goto sql_error;
}
if (plreturn.title && plreturn.query)
{
DPRINTF(E_DBG, L_SCAN, "SMARTPL SQL title '%s', query: '%s', having: '%s', order by: '%s', limit: %d \n", plreturn.title->chars, plreturn.query->chars, plreturn.having->chars, plreturn.orderby->chars, plreturn.limit);
if (smartpl->title)
free(smartpl->title);
smartpl->title = strdup((char *)plreturn.title->chars);
if (smartpl->query_where)
free(smartpl->query_where);
smartpl->query_where = strdup((char *)plreturn.query->chars);
if (smartpl->having)
free(smartpl->having);
smartpl->having = safe_strdup((char *)plreturn.having->chars);
if (smartpl->order)
free(smartpl->order);
smartpl->order = safe_strdup((char *)plreturn.orderby->chars);
smartpl->limit = plreturn.limit;
ret = 0;
}
else
{
DPRINTF(E_LOG, L_SCAN, "Invalid SMARTPL query\n");
ret = -1;
}
sql_error:
sqlconv->free(sqlconv);
sql_fail:
nodes->free(nodes);
psr_error:
psr->free(psr);
psr_fail:
tstream->free(tstream);
tkstream_fail:
lxr->free(lxr);
lxr_fail:
return ret;
}
#define SMARTPL_SIZE_MAX 8192
int
smartpl_query_parse_file(struct smartpl *smartpl, const char *file)
{
pANTLR3_INPUT_STREAM input;
char *expression = NULL;
size_t size;
size_t got;
int ret;
FILE *f;
#if ANTLR3C_NEW_INPUT
input = antlr3FileStreamNew((pANTLR3_UINT8) file, ANTLR3_ENC_8BIT);
#else
input = antlr3AsciiFileStreamNew((pANTLR3_UINT8) file);
#endif
// The input will be created successfully, providing that there is enough memory and the file exists etc
if (input == NULL)
f = fopen(file, "rb");
if (!f)
{
DPRINTF(E_LOG, L_SCAN, "Unable to open smart playlist file %s\n", file);
return -1;
DPRINTF(E_LOG, L_SCAN, "Could not open smart playlist '%s'\n", file);
goto error;
}
ret = parse_input(smartpl, input);
input->close(input);
fseek(f, 0, SEEK_END);
size = ftell(f);
if (size <= 0 || size > SMARTPL_SIZE_MAX)
{
DPRINTF(E_LOG, L_SCAN, "Smart playlist '%s' is zero bytes or too large (max size is %d)\n", file, SMARTPL_SIZE_MAX);
goto error;
}
fseek(f, 0, SEEK_SET);
expression = calloc(1, size + 1);
got = fread(expression, 1, size, f);
if (got != size)
{
DPRINTF(E_LOG, L_SCAN, "Unknown error reading smart playlist '%s'\n", file);
goto error;
}
fclose(f);
ret = smartpl_query_parse_string(smartpl, expression);
free(expression);
return ret;
error:
free(expression);
if (f)
fclose(f);
return -1;
}
int
smartpl_query_parse_string(struct smartpl *smartpl, const char *expression)
{
pANTLR3_INPUT_STREAM input;
int ret;
struct smartpl_result result;
#if ANTLR3C_NEW_INPUT
input = antlr3StringStreamNew ((pANTLR3_UINT8)expression, ANTLR3_ENC_8BIT, (ANTLR3_UINT64)strlen(expression), (pANTLR3_UINT8)"SMARTPL expression");
#else
input = antlr3NewAsciiStringInPlaceStream ((pANTLR3_UINT8)expression, (ANTLR3_UINT64)strlen(expression), (pANTLR3_UINT8)"SMARTPL expression");
#endif
DPRINTF(E_SPAM, L_SCAN, "Parse smartpl query input '%s'\n", expression);
// The input will be created successfully, providing that there is enough memory and the file exists etc
if (input == NULL)
if (smartpl_lex_parse(&result, expression) != 0)
{
DPRINTF(E_LOG, L_SCAN, "Unable to pars smart pl expression %s\n", expression);
DPRINTF(E_LOG, L_SCAN, "Could not parse '%s': %s\n", expression, result.errmsg);
return -1;
}
ret = parse_input(smartpl, input);
input->close(input);
if (!result.title || !result.where)
{
DPRINTF(E_LOG, L_SCAN, "Missing title or filter when parsing '%s'\n", expression);
return -1;
}
return ret;
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;
DPRINTF(E_SPAM, L_SCAN, "Parse smartpl query output '%s': WHERE %s HAVING %s ORDER BY %s LIMIT %d\n",
smartpl->title, smartpl->query_where, smartpl->having, smartpl->order, smartpl->limit);
return 0;
}