Merge pull request #1386 from owntone/bison_parser1
Replace antlr3 parsers with bison/flex
This commit is contained in:
commit
bc1c3e7bd3
|
@ -1,4 +1,4 @@
|
||||||
name: "forked-daapd CodeQL config"
|
name: "OwnTone CodeQL config"
|
||||||
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- htdocs
|
- htdocs
|
||||||
|
|
|
@ -40,10 +40,10 @@ jobs:
|
||||||
# uses a compiled language
|
# uses a compiled language
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update
|
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
|
autoreconf -vi
|
||||||
./configure --enable-lastfm --enable-chromecast
|
./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
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|
|
@ -17,6 +17,17 @@ jobs:
|
||||||
- name: Install build tools
|
- name: Install build tools
|
||||||
run: brew install automake autoconf libtool pkg-config
|
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
|
- name: Install libinotify-kqueue
|
||||||
# brew does not have libinotify package
|
# brew does not have libinotify package
|
||||||
run: |
|
run: |
|
||||||
|
@ -40,12 +51,6 @@ jobs:
|
||||||
sudo make install
|
sudo make install
|
||||||
cd ..
|
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
|
- name: Install ffmpeg
|
||||||
# The libbluray ffmpeg dependency fails without the chown (source: stackoverflow)
|
# The libbluray ffmpeg dependency fails without the chown (source: stackoverflow)
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: build and check
|
||||||
run: |
|
run: |
|
||||||
autoreconf -vi
|
autoreconf -vi
|
||||||
|
|
|
@ -29,6 +29,7 @@ missing
|
||||||
stamp-h1
|
stamp-h1
|
||||||
autotools-stamp
|
autotools-stamp
|
||||||
build-stamp
|
build-stamp
|
||||||
|
ylwrap
|
||||||
owntone.spec
|
owntone.spec
|
||||||
owntone.conf
|
owntone.conf
|
||||||
owntone.service
|
owntone.service
|
||||||
|
|
70
INSTALL.md
70
INSTALL.md
|
@ -1,6 +1,9 @@
|
||||||
# Installation instructions for OwnTone
|
# 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:
|
The source for this version of OwnTone can be found here:
|
||||||
[owntone/owntone-server](https://github.com/owntone/owntone-server.git)
|
[owntone/owntone-server](https://github.com/owntone/owntone-server.git)
|
||||||
|
@ -19,7 +22,7 @@ libraries:
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install \
|
sudo apt-get install \
|
||||||
build-essential git autotools-dev autoconf automake libtool gettext gawk \
|
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 \
|
libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev \
|
||||||
libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev \
|
libasound2-dev libmxml-dev libgcrypt20-dev libavahi-client-dev zlib1g-dev \
|
||||||
libevent-dev libplist-dev libsodium-dev libjson-c-dev libwebsockets-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
|
```bash
|
||||||
sudo yum install \
|
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 \
|
sqlite-devel libconfuse-devel libunistring-devel mxml-devel libevent-devel \
|
||||||
avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel \
|
avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel \
|
||||||
libplist-devel libsodium-devel json-c-devel libwebsockets-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:
|
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):
|
Install macports (which requires Xcode):
|
||||||
https://www.macports.org/install.php
|
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
|
```bash
|
||||||
sudo port install \
|
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 \
|
libunistring libconfuse ffmpeg libevent json-c libwebsockets curl \
|
||||||
libplist libsodium protobuf-c
|
libplist libsodium protobuf-c
|
||||||
```
|
```
|
||||||
|
@ -184,12 +180,6 @@ git clone https://github.com/owntone/owntone-server.git
|
||||||
cd owntone-server
|
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
|
Finally, configure, build and install, adding configure arguments for
|
||||||
optional features:
|
optional features:
|
||||||
|
|
||||||
|
@ -229,21 +219,16 @@ dns-sd -B _daap._tcp
|
||||||
|
|
||||||
Required tools:
|
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`
|
- 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.
|
at the top of the source tree to generate the build system.
|
||||||
- gettext: libunistring requires iconv and gettext provides the autotools
|
- gettext: libunistring requires iconv and gettext provides the autotools
|
||||||
macro definitions for iconv.
|
macro definitions for iconv.
|
||||||
- gperf
|
- gperf
|
||||||
|
- bison 3.0+ (yacc is not sufficient)
|
||||||
|
- flex (lex is not sufficient)
|
||||||
|
|
||||||
Libraries:
|
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
|
- Avahi client libraries (avahi-client), 0.6.24 minimum
|
||||||
from <http://avahi.org/>
|
from <http://avahi.org/>
|
||||||
- sqlite3 3.5.0+ with unlock notify API enabled (read below)
|
- 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
|
Start by generating the build system by running `autoreconf -i`. This will
|
||||||
generate the configure script and `Makefile.in`.
|
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`.
|
To display the configure options `run ./configure --help`.
|
||||||
|
|
||||||
Support for Spotify is optional. Use `--disable-spotify` to disable this feature.
|
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
|
Using `--enable-install-user` means that `make install` will also add a system
|
||||||
user and group for owntone.
|
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:
|
After installation:
|
||||||
|
|
||||||
- edit the configuration file, `/etc/owntone.conf`
|
- edit the configuration file, `/etc/owntone.conf`
|
||||||
|
|
29
configure.ac
29
configure.ac
|
@ -27,20 +27,17 @@ AS_IF([[test -z "$GPERF"]],
|
||||||
AC_MSG_NOTICE([[
|
AC_MSG_NOTICE([[
|
||||||
|
|
||||||
GNU gperf not found, but it's output appears to be present.
|
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
|
If you modify any .gperf files, you will need to install it.]])],
|
||||||
to install it.]])],
|
|
||||||
[AC_MSG_ERROR([[GNU gperf required, please install it.]])])
|
[AC_MSG_ERROR([[GNU gperf required, please install it.]])])
|
||||||
])
|
])
|
||||||
|
|
||||||
AC_PATH_PROG([ANTLR], [[antlr3]])
|
AX_PROG_FLEX([AC_DEFINE([LEX], [flex], [flex found])],
|
||||||
AS_IF([[test -z "$ANTLR"]],
|
[AS_IF([test ! -f "$srcdir/src/parsers/smartpl_lexer.c"],
|
||||||
[AS_IF([[test -f "$srcdir/src/SMARTPLLexer.h"]],
|
[AC_MSG_ERROR([flex required, please install it])])
|
||||||
[AM_MISSING_PROG([ANTLR], [[antlr3]])
|
])
|
||||||
AC_MSG_NOTICE([[
|
AX_PROG_BISON([AC_DEFINE([YACC], [bison], [GNU bison found])],
|
||||||
|
[AS_IF([test ! -f "$srcdir/src/parsers/smartpl_parser.c"],
|
||||||
antlr3 not found, but it's output appears to be present.
|
[AC_MSG_ERROR([GNU bison required, please install it])])
|
||||||
If you modify any ANTLR grammar files (.g), you will need to install it.]])],
|
|
||||||
[AC_MSG_ERROR([[antlr3 wrapper required, please install it.]])])
|
|
||||||
])
|
])
|
||||||
|
|
||||||
dnl Enable all warnings by default.
|
dnl Enable all warnings by default.
|
||||||
|
@ -174,16 +171,6 @@ PKG_CHECK_EXISTS([libplist],
|
||||||
[OWNTONE_MODULES_CHECK([OWNTONE], [LIBPLIST], [libplist-2.0],
|
[OWNTONE_MODULES_CHECK([OWNTONE], [LIBPLIST], [libplist-2.0],
|
||||||
[plist_dict_get_item], [plist/plist.h])])
|
[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])
|
AM_PATH_LIBGCRYPT([1:1.2.0])
|
||||||
OWNTONE_FUNC_REQUIRE([OWNTONE], [GNU Crypt Library], [LIBGCRYPT], [gcrypt],
|
OWNTONE_FUNC_REQUIRE([OWNTONE], [GNU Crypt Library], [LIBGCRYPT], [gcrypt],
|
||||||
[gcry_control], [gcrypt.h])
|
[gcry_control], [gcrypt.h])
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# ===========================================================================
|
||||||
|
# https://www.gnu.org/software/autoconf-archive/ax_prog_bison.html
|
||||||
|
# ===========================================================================
|
||||||
|
#
|
||||||
|
# SYNOPSIS
|
||||||
|
#
|
||||||
|
# AX_PROG_BISON(ACTION-IF-TRUE,ACTION-IF-FALSE)
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
#
|
||||||
|
# Check whether bison is the parser generator. Run ACTION-IF-TRUE if
|
||||||
|
# successful, ACTION-IF-FALSE otherwise
|
||||||
|
#
|
||||||
|
# LICENSE
|
||||||
|
#
|
||||||
|
# Copyright (c) 2009 Francesco Salvestrini <salvestrini@users.sourceforge.net>
|
||||||
|
# Copyright (c) 2010 Diego Elio Petteno` <flameeyes@gmail.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 2 of the License, or (at your
|
||||||
|
# option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
# Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# As a special exception, the respective Autoconf Macro's copyright owner
|
||||||
|
# gives unlimited permission to copy, distribute and modify the configure
|
||||||
|
# scripts that are the output of Autoconf when processing the Macro. You
|
||||||
|
# need not follow the terms of the GNU General Public License when using
|
||||||
|
# or distributing such scripts, even though portions of the text of the
|
||||||
|
# Macro appear in them. The GNU General Public License (GPL) does govern
|
||||||
|
# all other use of the material that constitutes the Autoconf Macro.
|
||||||
|
#
|
||||||
|
# This special exception to the GPL applies to versions of the Autoconf
|
||||||
|
# Macro released by the Autoconf Archive. When you make and distribute a
|
||||||
|
# modified version of the Autoconf Macro, you may extend this special
|
||||||
|
# exception to the GPL to apply to your modified version as well.
|
||||||
|
|
||||||
|
#serial 10
|
||||||
|
|
||||||
|
AC_DEFUN([AX_PROG_BISON], [
|
||||||
|
AC_REQUIRE([AC_PROG_YACC])
|
||||||
|
AC_REQUIRE([AC_PROG_EGREP])
|
||||||
|
|
||||||
|
AC_CACHE_CHECK([if bison is the parser generator],[ax_cv_prog_bison],[
|
||||||
|
AS_IF([$YACC --version 2>/dev/null | $EGREP -q '^bison '],
|
||||||
|
[ax_cv_prog_bison=yes], [ax_cv_prog_bison=no])
|
||||||
|
])
|
||||||
|
AS_IF([test "$ax_cv_prog_bison" = "yes"], [
|
||||||
|
dnl replace the yacc-compatible compiler with the real bison, as
|
||||||
|
dnl otherwise autoconf limits us to the POSIX yacc.
|
||||||
|
dnl We also change the generated filename to the old one, so that
|
||||||
|
dnl automake's ylwrap can deal with it.
|
||||||
|
YACC="${YACC% -y} -o y.tab.c"
|
||||||
|
] m4_ifnblank([$1], [[$1]]),
|
||||||
|
m4_ifnblank([$2], [[$2]])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
# ===========================================================================
|
||||||
|
# https://www.gnu.org/software/autoconf-archive/ax_prog_flex.html
|
||||||
|
# ===========================================================================
|
||||||
|
#
|
||||||
|
# SYNOPSIS
|
||||||
|
#
|
||||||
|
# AX_PROG_FLEX(ACTION-IF-TRUE,ACTION-IF-FALSE)
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
#
|
||||||
|
# Check whether flex is the scanner generator. Run ACTION-IF-TRUE if
|
||||||
|
# successful, ACTION-IF-FALSE otherwise
|
||||||
|
#
|
||||||
|
# LICENSE
|
||||||
|
#
|
||||||
|
# Copyright (c) 2009 Francesco Salvestrini <salvestrini@users.sourceforge.net>
|
||||||
|
# Copyright (c) 2010 Diego Elio Petteno` <flameeyes@gmail.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU General Public License as published by the
|
||||||
|
# Free Software Foundation; either version 2 of the License, or (at your
|
||||||
|
# option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
# Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# As a special exception, the respective Autoconf Macro's copyright owner
|
||||||
|
# gives unlimited permission to copy, distribute and modify the configure
|
||||||
|
# scripts that are the output of Autoconf when processing the Macro. You
|
||||||
|
# need not follow the terms of the GNU General Public License when using
|
||||||
|
# or distributing such scripts, even though portions of the text of the
|
||||||
|
# Macro appear in them. The GNU General Public License (GPL) does govern
|
||||||
|
# all other use of the material that constitutes the Autoconf Macro.
|
||||||
|
#
|
||||||
|
# This special exception to the GPL applies to versions of the Autoconf
|
||||||
|
# Macro released by the Autoconf Archive. When you make and distribute a
|
||||||
|
# modified version of the Autoconf Macro, you may extend this special
|
||||||
|
# exception to the GPL to apply to your modified version as well.
|
||||||
|
|
||||||
|
#serial 13
|
||||||
|
|
||||||
|
AC_DEFUN([AX_PROG_FLEX], [
|
||||||
|
AC_REQUIRE([AM_PROG_LEX])
|
||||||
|
AC_REQUIRE([AC_PROG_EGREP])
|
||||||
|
|
||||||
|
AC_CACHE_CHECK([if flex is the lexer generator],[ax_cv_prog_flex],[
|
||||||
|
AS_IF([$LEX --version 2>/dev/null | $EGREP -qw '^g?flex'],
|
||||||
|
[ax_cv_prog_flex=yes], [ax_cv_prog_flex=no])
|
||||||
|
])
|
||||||
|
AS_IF([test "$ax_cv_prog_flex" = "yes"],
|
||||||
|
m4_ifnblank([$1], [[$1]]),
|
||||||
|
m4_ifnblank([$2], [[$2]])
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
|
@ -18,10 +18,10 @@ Group: Applications/Multimedia
|
||||||
Url: https://github.com/owntone/owntone-server
|
Url: https://github.com/owntone/owntone-server
|
||||||
Source0: https://github.com/owntone/%{name}/archive/%{version}/%{name}-%{version}.tar.xz
|
Source0: https://github.com/owntone/%{name}/archive/%{version}/%{name}-%{version}.tar.xz
|
||||||
%{?systemd_ordering}
|
%{?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(zlib), pkgconfig(libconfuse), pkgconfig(mxml)
|
||||||
BuildRequires: pkgconfig(sqlite3) >= 3.5.0, pkgconfig(libevent) >= 2.0.0
|
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: libgpg-error-devel >= 1.6
|
||||||
BuildRequires: pkgconfig(libavformat), pkgconfig(libavcodec)
|
BuildRequires: pkgconfig(libavformat), pkgconfig(libavcodec)
|
||||||
BuildRequires: pkgconfig(libswscale), pkgconfig(libavutil)
|
BuildRequires: pkgconfig(libswscale), pkgconfig(libavutil)
|
||||||
|
@ -109,6 +109,10 @@ exit 0
|
||||||
%{_mandir}/man?/*
|
%{_mandir}/man?/*
|
||||||
|
|
||||||
%changelog
|
%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
|
* Mon Nov 22 2021 Derek Atkins <derek@ihtfp.com> - 28.2-1
|
||||||
- Release tarball is a XZ not GZ file
|
- Release tarball is a XZ not GZ file
|
||||||
- Configure always needs protobuf-c, not just for chromecast
|
- Configure always needs protobuf-c, not just for chromecast
|
||||||
|
|
|
@ -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
|
|
|
@ -12,7 +12,7 @@ fi
|
||||||
DEPS="gmake autoconf automake libtool gettext gperf glib pkgconf wget git \
|
DEPS="gmake autoconf automake libtool gettext gperf glib pkgconf wget git \
|
||||||
ffmpeg libconfuse libevent mxml libgcrypt libunistring libiconv curl \
|
ffmpeg libconfuse libevent mxml libgcrypt libunistring libiconv curl \
|
||||||
libplist libinotify avahi sqlite3 alsa-lib libsodium json-c libwebsockets
|
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 "The script can install the following dependency packages for you:"
|
||||||
echo $DEPS
|
echo $DEPS
|
||||||
read -p "Should the script install these packages? [y/N] " yn
|
read -p "Should the script install these packages? [y/N] " yn
|
||||||
|
@ -46,30 +46,6 @@ if [ ! -d $WORKDIR ]; then
|
||||||
fi
|
fi
|
||||||
cd $WORKDIR
|
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
|
read -p "Should the script build owntone? [y/N] " yn
|
||||||
if [ "$yn" = "y" ]; then
|
if [ "$yn" = "y" ]; then
|
||||||
git clone https://github.com/owntone/owntone-server.git
|
git clone https://github.com/owntone/owntone-server.git
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
owntone
|
owntone
|
||||||
|
|
||||||
*.tokens
|
|
||||||
*Lexer.[ch]
|
|
||||||
*Parser.[ch]
|
|
||||||
*2SQL.[ch]
|
|
||||||
*.u
|
|
||||||
|
|
||||||
daap_query_hash.h
|
daap_query_hash.h
|
||||||
rsp_query_hash.h
|
rsp_query_hash.h
|
||||||
dacp_prop_hash.h
|
dacp_prop_hash.h
|
||||||
|
|
63
src/DAAP.g
63
src/DAAP.g
|
@ -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)"\'")); }
|
|
||||||
)
|
|
||||||
;
|
|
383
src/DAAP2SQL.g
383
src/DAAP2SQL.g
|
@ -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 */
|
|
||||||
;
|
|
||||||
}
|
|
||||||
;
|
|
|
@ -56,33 +56,19 @@ endif
|
||||||
|
|
||||||
GPERF_FILES = \
|
GPERF_FILES = \
|
||||||
daap_query.gperf \
|
daap_query.gperf \
|
||||||
rsp_query.gperf \
|
|
||||||
dacp_prop.gperf \
|
dacp_prop.gperf \
|
||||||
dmap_fields.gperf
|
dmap_fields.gperf
|
||||||
|
|
||||||
GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h)
|
GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h)
|
||||||
|
|
||||||
ANTLR_GRAMMARS = \
|
LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l
|
||||||
RSP.g RSP2SQL.g \
|
PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y
|
||||||
DAAP.g DAAP2SQL.g \
|
|
||||||
SMARTPL.g SMARTPL2SQL.g
|
|
||||||
|
|
||||||
ANTLR_TOKENS = $(ANTLR_GRAMMARS:.g=.tokens)
|
# 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
|
||||||
ANTLR_DEPS = $(ANTLR_GRAMMARS:%.g=$(srcdir)/%.u)
|
# change it to compound arguments (so for instance no "-dv"). I'm also not sure
|
||||||
|
# --defines will work instead of -d.
|
||||||
ANTLR_SRC = \
|
AM_YFLAGS = -d
|
||||||
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)
|
|
||||||
|
|
||||||
AM_CPPFLAGS += \
|
AM_CPPFLAGS += \
|
||||||
$(OWNTONE_CPPFLAGS) \
|
$(OWNTONE_CPPFLAGS) \
|
||||||
|
@ -129,8 +115,6 @@ owntone_SOURCES = main.c \
|
||||||
misc.c misc.h \
|
misc.c misc.h \
|
||||||
misc_json.c misc_json.h \
|
misc_json.c misc_json.h \
|
||||||
rng.c rng.h \
|
rng.c rng.h \
|
||||||
rsp_query.c rsp_query.h \
|
|
||||||
daap_query.c daap_query.h \
|
|
||||||
smartpl_query.c smartpl_query.h \
|
smartpl_query.c smartpl_query.h \
|
||||||
player.c player.h \
|
player.c player.h \
|
||||||
worker.c worker.h \
|
worker.c worker.h \
|
||||||
|
@ -153,41 +137,31 @@ owntone_SOURCES = main.c \
|
||||||
outputs/plist_wrap.h \
|
outputs/plist_wrap.h \
|
||||||
$(LIBWEBSOCKETS_SRC) \
|
$(LIBWEBSOCKETS_SRC) \
|
||||||
$(GPERF_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 = \
|
BUILT_SOURCES = \
|
||||||
$(GPERF_SRC) \
|
$(GPERF_SRC) \
|
||||||
$(ANTLR_SRC) \
|
$(LEXER_SRC:.l=.h) $(PARSER_SRC:.y=.h)
|
||||||
$(ANTLR_TOKENS) \
|
|
||||||
$(ANTLR_DEPS)
|
|
||||||
|
|
||||||
|
# automake doesn't know how to make lexer headers, nor does it automatically
|
||||||
|
# include them, so need to specify them as EXTRA_DIST.
|
||||||
EXTRA_DIST = \
|
EXTRA_DIST = \
|
||||||
$(GPERF_FILES) \
|
$(GPERF_FILES) \
|
||||||
$(ANTLR_GRAMMARS) \
|
$(LEXER_SRC:.l=.h)
|
||||||
$(ANTLR_TOKENS) \
|
|
||||||
$(ANTLR_DEPS)
|
|
||||||
|
|
||||||
# silence unused warnings from antlr generated files
|
|
||||||
$(ANTLR_OBJECTS): AM_CPPFLAGS += -Wno-unused
|
|
||||||
|
|
||||||
# gperf construction rules
|
# gperf construction rules
|
||||||
%_hash.h: %.gperf
|
%_hash.h: %.gperf
|
||||||
$(AM_V_GEN)$(GPERF) --output-file=$@ $<
|
$(AM_V_GEN)$(GPERF) --output-file=$@ $<
|
||||||
|
|
||||||
# silent rules for antlr
|
# Rule for generating lexer headers. $@ is an automatic variable that is equal
|
||||||
antlr_verbose = $(antlr_verbose_@AM_V@)
|
# to the particular target name, so a header file name.
|
||||||
antlr_verbose_ = $(antlr_verbose_@AM_DEFAULT_V@)
|
$(LEXER_SRC:.l=.h): $(LEXER_SRC)
|
||||||
antlr_verbose_0 = @echo " GEN " $< "products";
|
$(LEX) --header-file=$@ --stdout $(@:.h=.l) > /dev/null
|
||||||
|
|
||||||
# ANTLR grammar products
|
# Anything built by make should be cleaned by make clean, but when it comes to
|
||||||
%.tokens %.c %Lexer.c %Parser.c %Lexer.h %Parser.h %.h: %.g
|
# flex/bison automake's support leaves something to be desired
|
||||||
$(antlr_verbose)$(ANTLR) -Xconversiontimeout 30000 $(ANTLR_OPTIONS) -fo . $<
|
clean-local:
|
||||||
|
rm -f $(LEXER_SRC:.l=.[ch]) $(PARSER_SRC:.y=.[ch])
|
||||||
# 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)
|
|
||||||
|
|
148
src/RSP.g
148
src/RSP.g
|
@ -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';
|
|
474
src/RSP2SQL.g
474
src/RSP2SQL.g
|
@ -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; }
|
|
||||||
;
|
|
240
src/SMARTPL.g
240
src/SMARTPL.g
|
@ -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; }
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
157
src/daap_query.c
157
src/daap_query.c
|
@ -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;
|
|
||||||
}
|
|
|
@ -7,8 +7,11 @@
|
||||||
%define lookup-function-name daap_query_field_lookup
|
%define lookup-function-name daap_query_field_lookup
|
||||||
%define slot-name dmap_field
|
%define slot-name dmap_field
|
||||||
%struct-type
|
%struct-type
|
||||||
%omit-struct-type
|
struct dmap_query_field_map {
|
||||||
struct dmap_query_field_map;
|
char *dmap_field;
|
||||||
|
char *db_col;
|
||||||
|
int as_int;
|
||||||
|
};
|
||||||
%%
|
%%
|
||||||
"dmap.itemname", "f.title", 0
|
"dmap.itemname", "f.title", 0
|
||||||
"dmap.itemid", "f.id", 1
|
"dmap.itemid", "f.id", 1
|
||||||
|
|
|
@ -1,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__ */
|
|
|
@ -28,7 +28,7 @@
|
||||||
#include "httpd.h"
|
#include "httpd.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "dmap_common.h"
|
#include "dmap_common.h"
|
||||||
|
#include "parsers/daap_parser.h"
|
||||||
|
|
||||||
/* gperf static hash, dmap_fields.gperf */
|
/* gperf static hash, dmap_fields.gperf */
|
||||||
#include "dmap_fields_hash.h"
|
#include "dmap_fields_hash.h"
|
||||||
|
@ -636,3 +636,21 @@ dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, str
|
||||||
|
|
||||||
return 0;
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -88,4 +88,7 @@ dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, stru
|
||||||
int
|
int
|
||||||
dmap_encode_queue_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_queue_item *queue_item);
|
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__ */
|
#endif /* !__DMAP_HELPERS_H__ */
|
||||||
|
|
|
@ -51,7 +51,6 @@
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
#include "transcode.h"
|
#include "transcode.h"
|
||||||
#include "artwork.h"
|
#include "artwork.h"
|
||||||
#include "daap_query.h"
|
|
||||||
#include "dmap_common.h"
|
#include "dmap_common.h"
|
||||||
#include "cache.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);
|
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)
|
if (!qp->filter)
|
||||||
DPRINTF(E_LOG, L_DAAP, "Ignoring improper DAAP query: %s\n", param);
|
DPRINTF(E_LOG, L_DAAP, "Ignoring improper DAAP query: %s\n", param);
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,6 @@
|
||||||
#include "artwork.h"
|
#include "artwork.h"
|
||||||
#include "dmap_common.h"
|
#include "dmap_common.h"
|
||||||
#include "db.h"
|
#include "db.h"
|
||||||
#include "daap_query.h"
|
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "listener.h"
|
#include "listener.h"
|
||||||
|
|
||||||
|
@ -237,7 +236,7 @@ find_first_song_id(const char *query)
|
||||||
qp.sort = S_NONE;
|
qp.sort = S_NONE;
|
||||||
qp.offset = 0;
|
qp.offset = 0;
|
||||||
qp.limit = 1;
|
qp.limit = 1;
|
||||||
qp.filter = daap_query_parse_sql(query);
|
qp.filter = dmap_query_parse_sql(query);
|
||||||
if (!qp.filter)
|
if (!qp.filter)
|
||||||
{
|
{
|
||||||
DPRINTF(E_LOG, L_DACP, "Improper DAAP query!\n");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
qp.filter = daap_query_parse_sql(buf);
|
qp.filter = dmap_query_parse_sql(buf);
|
||||||
}
|
}
|
||||||
else
|
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
|
// If the queuefilter is unkown, ignore it and use the query parameter instead to build the sql query
|
||||||
id = 0;
|
id = 0;
|
||||||
qp.type = Q_ITEMS;
|
qp.type = Q_ITEMS;
|
||||||
qp.filter = daap_query_parse_sql(query);
|
qp.filter = dmap_query_parse_sql(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
id = 0;
|
id = 0;
|
||||||
qp.type = Q_ITEMS;
|
qp.type = Q_ITEMS;
|
||||||
qp.filter = daap_query_parse_sql(query);
|
qp.filter = dmap_query_parse_sql(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sort)
|
if (sort)
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
#include "httpd.h"
|
#include "httpd.h"
|
||||||
#include "transcode.h"
|
#include "transcode.h"
|
||||||
#include "rsp_query.h"
|
#include "parsers/rsp_parser.h"
|
||||||
|
|
||||||
#define RSP_VERSION "1.0"
|
#define RSP_VERSION "1.0"
|
||||||
#define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?"
|
#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
|
static int
|
||||||
query_params_set(struct query_params *qp, struct httpd_request *hreq)
|
query_params_set(struct query_params *qp, struct httpd_request *hreq)
|
||||||
{
|
{
|
||||||
|
struct rsp_result parse_result;
|
||||||
const char *param;
|
const char *param;
|
||||||
char query[1024];
|
char query[1024];
|
||||||
char *filter;
|
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
qp->offset = 0;
|
qp->offset = 0;
|
||||||
|
@ -243,6 +243,7 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq)
|
||||||
else
|
else
|
||||||
qp->idx_type = I_NONE;
|
qp->idx_type = I_NONE;
|
||||||
|
|
||||||
|
qp->filter = NULL;
|
||||||
param = evhttp_find_header(hreq->query, "query");
|
param = evhttp_find_header(hreq->query, "query");
|
||||||
if (param)
|
if (param)
|
||||||
{
|
{
|
||||||
|
@ -263,19 +264,15 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
qp->filter = rsp_query_parse_sql(query);
|
if (rsp_lex_parse(&parse_result, query) != 0)
|
||||||
if (!qp->filter)
|
DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query: %s\n", query);
|
||||||
DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query\n");
|
else
|
||||||
|
qp->filter = safe_asprintf("(%s) AND %s", parse_result.str, rsp_filter_files);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always filter to include only files (not streams and Spotify)
|
// Always filter to include only files (not streams and Spotify)
|
||||||
if (qp->filter)
|
if (!qp->filter)
|
||||||
filter = safe_asprintf("%s AND %s", qp->filter, rsp_filter_files);
|
qp->filter = strdup(rsp_filter_files);
|
||||||
else
|
|
||||||
filter = strdup(rsp_filter_files);
|
|
||||||
|
|
||||||
free(qp->filter);
|
|
||||||
qp->filter = filter;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
*_lexer.[ch]
|
||||||
|
*_parser.[ch]
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021-2022 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* =========================== BOILERPLATE SECTION ===========================*/
|
||||||
|
|
||||||
|
/* This is to avoid compiler warnings about unused functions. More options are
|
||||||
|
noyyalloc noyyrealloc noyyfree. */
|
||||||
|
%option noyywrap nounput noinput
|
||||||
|
|
||||||
|
/* Thread safe scanner */
|
||||||
|
%option reentrant
|
||||||
|
|
||||||
|
/* To avoid symbol name conflicts with multiple lexers */
|
||||||
|
%option prefix="daap_"
|
||||||
|
|
||||||
|
/* Automake's ylwrap expexts the output to have this name */
|
||||||
|
%option outfile="lex.yy.c"
|
||||||
|
|
||||||
|
/* Makes a Bison-compatible yylex */
|
||||||
|
%option bison-bridge
|
||||||
|
|
||||||
|
%{
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "daap_parser.h"
|
||||||
|
|
||||||
|
/* Unknown why this is required despite using prefix */
|
||||||
|
#define YYSTYPE DAAP_STYPE
|
||||||
|
|
||||||
|
%}
|
||||||
|
|
||||||
|
/* ========================= NON-BOILERPLATE SECTION =========================*/
|
||||||
|
|
||||||
|
re_quote '
|
||||||
|
re_key [[:alnum:]\.\-]+
|
||||||
|
re_value (\\.|[^'])+
|
||||||
|
re_operator (!?[:@])
|
||||||
|
|
||||||
|
%x IN_CRITERIA IN_CRITERIA_VALUE
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
||||||
|
{re_quote} { BEGIN IN_CRITERIA; return DAAP_T_QUOTE; }
|
||||||
|
|
||||||
|
<IN_CRITERIA>{re_key}/{re_operator} { yylval->str = strdup(yytext); return DAAP_T_KEY; }
|
||||||
|
<IN_CRITERIA>{re_operator} { BEGIN IN_CRITERIA_VALUE; return (*yytext == '!' ? DAAP_T_NOT : DAAP_T_EQUAL); }
|
||||||
|
<IN_CRITERIA>. { return *yytext; }
|
||||||
|
|
||||||
|
<IN_CRITERIA_VALUE>\*{re_value}\*/{re_quote} { yylval->str = strdup(yytext); return DAAP_T_WILDCARD; }
|
||||||
|
<IN_CRITERIA_VALUE>{re_value}/{re_quote} { yylval->str = strdup(yytext); return DAAP_T_VALUE; }
|
||||||
|
<IN_CRITERIA_VALUE>{re_quote} { BEGIN INITIAL; return DAAP_T_QUOTE; }
|
||||||
|
<IN_CRITERIA_VALUE>. { return *yytext; }
|
||||||
|
|
||||||
|
"+"|" " { return DAAP_T_AND; }
|
||||||
|
"," { return DAAP_T_OR; }
|
||||||
|
"\r"?"\n" { return DAAP_T_NEWLINE; }
|
||||||
|
. { return *yytext; }
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -0,0 +1,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; }
|
||||||
|
;
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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]; }
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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)); }
|
||||||
|
;
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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]; }
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
|
@ -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
|
||||||
|
;
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
150
src/rsp_query.c
150
src/rsp_query.c
|
@ -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;
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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__ */
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2018 Christian Meffert <christian.meffert@googlemail.com>
|
* Copyright (C) 2021-2022 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or modify
|
* This program is free software; you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -32,186 +32,90 @@
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
#include "smartpl_query.h"
|
#include "smartpl_query.h"
|
||||||
|
#include "parsers/smartpl_parser.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
|
||||||
#include "SMARTPLLexer.h"
|
#define SMARTPL_SIZE_MAX 8192
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
int
|
||||||
smartpl_query_parse_file(struct smartpl *smartpl, const char *file)
|
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;
|
int ret;
|
||||||
|
FILE *f;
|
||||||
|
|
||||||
#if ANTLR3C_NEW_INPUT
|
f = fopen(file, "rb");
|
||||||
input = antlr3FileStreamNew((pANTLR3_UINT8) file, ANTLR3_ENC_8BIT);
|
if (!f)
|
||||||
#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)
|
|
||||||
{
|
{
|
||||||
DPRINTF(E_LOG, L_SCAN, "Unable to open smart playlist file %s\n", file);
|
DPRINTF(E_LOG, L_SCAN, "Could not open smart playlist '%s'\n", file);
|
||||||
return -1;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = parse_input(smartpl, input);
|
fseek(f, 0, SEEK_END);
|
||||||
input->close(input);
|
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;
|
return ret;
|
||||||
|
|
||||||
|
error:
|
||||||
|
free(expression);
|
||||||
|
if (f)
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
smartpl_query_parse_string(struct smartpl *smartpl, const char *expression)
|
smartpl_query_parse_string(struct smartpl *smartpl, const char *expression)
|
||||||
{
|
{
|
||||||
pANTLR3_INPUT_STREAM input;
|
struct smartpl_result result;
|
||||||
int ret;
|
|
||||||
|
|
||||||
#if ANTLR3C_NEW_INPUT
|
DPRINTF(E_SPAM, L_SCAN, "Parse smartpl query input '%s'\n", expression);
|
||||||
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
|
|
||||||
|
|
||||||
// The input will be created successfully, providing that there is enough memory and the file exists etc
|
if (smartpl_lex_parse(&result, expression) != 0)
|
||||||
if (input == NULL)
|
|
||||||
{
|
{
|
||||||
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = parse_input(smartpl, input);
|
if (!result.title || !result.where)
|
||||||
input->close(input);
|
{
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue