1055 lines
21 KiB
C
1055 lines
21 KiB
C
/*
|
|
* Copyright 2008-2013 Various Authors
|
|
* Copyright 2005 Timo Hirvonen
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "expr.h"
|
|
#include "glob.h"
|
|
#include "uchar.h"
|
|
#include "track_info.h"
|
|
#include "comment.h"
|
|
#include "xmalloc.h"
|
|
#include "utils.h"
|
|
#include "debug.h"
|
|
#include "list.h"
|
|
#include "ui_curses.h" /* using_utf8, charset */
|
|
#include "convert.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <ctype.h>
|
|
#include <stdarg.h>
|
|
#include <limits.h>
|
|
|
|
enum token_type {
|
|
/* special chars */
|
|
TOK_NOT,
|
|
TOK_LT,
|
|
TOK_GT,
|
|
|
|
#define NR_COMBINATIONS TOK_EQ
|
|
|
|
/* special chars */
|
|
TOK_EQ,
|
|
TOK_AND,
|
|
TOK_OR,
|
|
TOK_LPAREN,
|
|
TOK_RPAREN,
|
|
|
|
#define NR_SPECIALS TOK_NE
|
|
#define COMB_BASE TOK_NE
|
|
|
|
/* same as the first 3 + '=' */
|
|
TOK_NE,
|
|
TOK_LE,
|
|
TOK_GE,
|
|
|
|
TOK_KEY,
|
|
TOK_INT_OR_KEY,
|
|
TOK_STR
|
|
};
|
|
#define NR_TOKS (TOK_STR + 1)
|
|
|
|
struct token {
|
|
struct list_head node;
|
|
enum token_type type;
|
|
/* for TOK_KEY, TOK_INT_OR_KEY and TOK_STR */
|
|
char str[];
|
|
};
|
|
|
|
/* same order as TOK_* */
|
|
static const char specials[NR_SPECIALS] CMUS_NONSTRING = "!<>=&|()";
|
|
|
|
static const int tok_to_op[NR_TOKS] = {
|
|
-1, OP_LT, OP_GT, OP_EQ, -1, -1, -1, -1, OP_NE, OP_LE, OP_GE, -1, -1, -1
|
|
};
|
|
|
|
static const char * const op_names[NR_OPS] = { "<", "<=", "=", ">=", ">", "!=" };
|
|
static const char * const expr_names[NR_EXPRS] = {
|
|
"&", "|", "!", "a string", "an integer", "a boolean"
|
|
};
|
|
|
|
static char error_buf[64] = { 0, };
|
|
|
|
|
|
static void set_error(const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start(ap, format);
|
|
vsnprintf(error_buf, sizeof(error_buf), format, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
static struct token *get_str(const char *str, int *idxp)
|
|
{
|
|
struct token *tok;
|
|
int s = *idxp + 1;
|
|
int e = s;
|
|
|
|
/* can't remove all backslashes here => don't remove any */
|
|
while (str[e] != '"') {
|
|
int c = str[e];
|
|
|
|
if (c == 0)
|
|
goto err;
|
|
if (c == '\\') {
|
|
if (str[e + 1] == 0)
|
|
goto err;
|
|
e += 2;
|
|
continue;
|
|
}
|
|
e++;
|
|
}
|
|
|
|
tok = xmalloc(sizeof(struct token) + e - s + 1);
|
|
memcpy(tok->str, str + s, e - s);
|
|
tok->str[e - s] = 0;
|
|
tok->type = TOK_STR;
|
|
*idxp = e + 1;
|
|
return tok;
|
|
err:
|
|
set_error("end of expression at middle of string");
|
|
return NULL;
|
|
}
|
|
|
|
static struct token *get_int_or_key(const char *str, int *idxp)
|
|
{
|
|
int s = *idxp;
|
|
int e = s;
|
|
int digits_only = 1;
|
|
struct token *tok;
|
|
|
|
if (str[e] == '-')
|
|
e++;
|
|
while (str[e]) {
|
|
int i, c = str[e];
|
|
|
|
if (isspace(c))
|
|
goto out;
|
|
for (i = 0; i < NR_SPECIALS; i++) {
|
|
if (c == specials[i])
|
|
goto out;
|
|
}
|
|
if (c < '0' || c > '9') {
|
|
digits_only = 0;
|
|
if (!isalpha(c) && c != '_' && c != '-') {
|
|
set_error("unexpected '%c'", c);
|
|
return NULL;
|
|
}
|
|
}
|
|
e++;
|
|
}
|
|
out:
|
|
tok = xmalloc(sizeof(struct token) + e - s + 1);
|
|
memcpy(tok->str, str + s, e - s);
|
|
tok->str[e - s] = 0;
|
|
tok->type = TOK_KEY;
|
|
if (digits_only)
|
|
tok->type = TOK_INT_OR_KEY;
|
|
*idxp = e;
|
|
return tok;
|
|
}
|
|
|
|
static struct token *get_token(const char *str, int *idxp)
|
|
{
|
|
int idx = *idxp;
|
|
int c, i;
|
|
|
|
c = str[idx];
|
|
for (i = 0; i < NR_SPECIALS; i++) {
|
|
struct token *tok;
|
|
|
|
if (c != specials[i])
|
|
continue;
|
|
|
|
idx++;
|
|
tok = xnew(struct token, 1);
|
|
tok->type = i;
|
|
if (i < NR_COMBINATIONS && str[idx] == '=') {
|
|
tok->type = COMB_BASE + i;
|
|
idx++;
|
|
}
|
|
*idxp = idx;
|
|
return tok;
|
|
}
|
|
if (c == '"')
|
|
return get_str(str, idxp);
|
|
return get_int_or_key(str, idxp);
|
|
}
|
|
|
|
static void free_tokens(struct list_head *head)
|
|
{
|
|
struct list_head *item = head->next;
|
|
|
|
while (item != head) {
|
|
struct list_head *next = item->next;
|
|
struct token *tok = container_of(item, struct token, node);
|
|
|
|
free(tok);
|
|
item = next;
|
|
}
|
|
}
|
|
|
|
static int tokenize(struct list_head *head, const char *str)
|
|
{
|
|
struct token *tok;
|
|
int idx = 0;
|
|
|
|
while (1) {
|
|
while (isspace(str[idx]))
|
|
++idx;
|
|
if (str[idx] == 0)
|
|
break;
|
|
tok = get_token(str, &idx);
|
|
if (tok == NULL) {
|
|
free_tokens(head);
|
|
return -1;
|
|
}
|
|
list_add_tail(&tok->node, head);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static struct expr *expr_new(int type)
|
|
{
|
|
struct expr *new = xnew0(struct expr, 1);
|
|
|
|
new->type = type;
|
|
|
|
return new;
|
|
}
|
|
|
|
static int parse(struct expr **rootp, struct list_head *head, struct list_head **itemp, int level);
|
|
|
|
static int parse_one(struct expr **exprp, struct list_head *head, struct list_head **itemp)
|
|
{
|
|
struct list_head *item = *itemp;
|
|
struct token *tok;
|
|
enum token_type type;
|
|
int rc;
|
|
|
|
*exprp = NULL;
|
|
if (item == head) {
|
|
set_error("expression expected");
|
|
return -1;
|
|
}
|
|
|
|
tok = container_of(item, struct token, node);
|
|
type = tok->type;
|
|
if (type == TOK_NOT) {
|
|
struct expr *new, *tmp;
|
|
|
|
*itemp = item->next;
|
|
rc = parse_one(&tmp, head, itemp);
|
|
if (rc)
|
|
return rc;
|
|
new = expr_new(EXPR_NOT);
|
|
new->left = tmp;
|
|
*exprp = new;
|
|
return 0;
|
|
} else if (type == TOK_LPAREN) {
|
|
*itemp = item->next;
|
|
*exprp = NULL;
|
|
return parse(exprp, head, itemp, 1);
|
|
/* ')' already eaten */
|
|
} else if (type == TOK_KEY || type == TOK_INT_OR_KEY) {
|
|
const char *key = tok->str;
|
|
struct expr *new;
|
|
int op = -1;
|
|
|
|
item = item->next;
|
|
if (item != head) {
|
|
tok = container_of(item, struct token, node);
|
|
op = tok_to_op[tok->type];
|
|
}
|
|
if (item == head || op == -1) {
|
|
/* must be a bool */
|
|
new = expr_new(EXPR_BOOL);
|
|
new->key = xstrdup(key);
|
|
*itemp = item;
|
|
*exprp = new;
|
|
return 0;
|
|
}
|
|
item = item->next;
|
|
if (item == head) {
|
|
set_error("right side of expression expected");
|
|
return -1;
|
|
}
|
|
tok = container_of(item, struct token, node);
|
|
type = tok->type;
|
|
*itemp = item->next;
|
|
if (type == TOK_STR) {
|
|
if (op != OP_EQ && op != OP_NE) {
|
|
set_error("invalid string operator '%s'", op_names[op]);
|
|
return -1;
|
|
}
|
|
new = expr_new(EXPR_STR);
|
|
new->key = xstrdup(key);
|
|
glob_compile(&new->estr.glob_head, tok->str);
|
|
new->estr.op = op;
|
|
*exprp = new;
|
|
return 0;
|
|
} else if (type == TOK_INT_OR_KEY) {
|
|
long int val = 0;
|
|
|
|
if (str_to_int(tok->str, &val)) {
|
|
}
|
|
new = expr_new(EXPR_INT);
|
|
new->key = xstrdup(key);
|
|
new->eint.val = val;
|
|
new->eint.op = op;
|
|
*exprp = new;
|
|
return 0;
|
|
} else if (type == TOK_KEY) {
|
|
new = expr_new(EXPR_ID);
|
|
new->key = xstrdup(key);
|
|
new->eid.key = xstrdup(tok->str);
|
|
new->eid.op = op;
|
|
*exprp = new;
|
|
return 0;
|
|
}
|
|
if (op == OP_EQ || op == OP_NE) {
|
|
set_error("integer or string expected");
|
|
} else {
|
|
set_error("integer expected");
|
|
}
|
|
return -1;
|
|
}
|
|
set_error("key expected");
|
|
return -1;
|
|
}
|
|
|
|
static void add(struct expr **rootp, struct expr *expr)
|
|
{
|
|
struct expr *tmp, *root = *rootp;
|
|
|
|
if (root == NULL) {
|
|
*rootp = expr;
|
|
return;
|
|
}
|
|
|
|
tmp = root;
|
|
while (tmp->right)
|
|
tmp = tmp->right;
|
|
if (tmp->type <= EXPR_OR) {
|
|
/* tmp is binary, tree is incomplete */
|
|
tmp->right = expr;
|
|
expr->parent = tmp;
|
|
return;
|
|
}
|
|
|
|
/* tmp is unary, tree is complete
|
|
* expr must be a binary operator */
|
|
BUG_ON(expr->type > EXPR_OR);
|
|
|
|
expr->left = root;
|
|
root->parent = expr;
|
|
*rootp = expr;
|
|
}
|
|
|
|
static int parse(struct expr **rootp, struct list_head *head, struct list_head **itemp, int level)
|
|
{
|
|
struct list_head *item = *itemp;
|
|
|
|
while (1) {
|
|
struct token *tok;
|
|
struct expr *expr;
|
|
int rc, type;
|
|
|
|
rc = parse_one(&expr, head, &item);
|
|
if (rc)
|
|
return rc;
|
|
add(rootp, expr);
|
|
if (item == head) {
|
|
if (level > 0) {
|
|
set_error("')' expected");
|
|
return -1;
|
|
}
|
|
*itemp = item;
|
|
return 0;
|
|
}
|
|
tok = container_of(item, struct token, node);
|
|
if (tok->type == TOK_RPAREN) {
|
|
if (level == 0) {
|
|
set_error("unexpected ')'");
|
|
return -1;
|
|
}
|
|
*itemp = item->next;
|
|
return 0;
|
|
}
|
|
|
|
if (tok->type == TOK_AND) {
|
|
type = EXPR_AND;
|
|
} else if (tok->type == TOK_OR) {
|
|
type = EXPR_OR;
|
|
} else {
|
|
set_error("'&' or '|' expected");
|
|
return -1;
|
|
}
|
|
expr = expr_new(type);
|
|
add(rootp, expr);
|
|
item = item->next;
|
|
}
|
|
}
|
|
|
|
static const struct {
|
|
char short_key;
|
|
const char *long_key;
|
|
} map_short2long[] = {
|
|
{ 'A', "albumartist" },
|
|
{ 'D', "discnumber" },
|
|
{ 'T', "tag", },
|
|
{ 'a', "artist" },
|
|
{ 'c', "comment" },
|
|
{ 'd', "duration" },
|
|
{ 'f', "filename" },
|
|
{ 'g', "genre" },
|
|
{ 'l', "album" },
|
|
{ 'n', "tracknumber" },
|
|
{ 'X', "play_count" },
|
|
{ 's', "stream" },
|
|
{ 't', "title" },
|
|
{ 'y', "date" },
|
|
{ '\0', NULL },
|
|
};
|
|
|
|
static const struct {
|
|
const char *key;
|
|
enum expr_type type;
|
|
} builtin[] = {
|
|
{ "album", EXPR_STR },
|
|
{ "albumartist",EXPR_STR },
|
|
{ "artist", EXPR_STR },
|
|
{ "bitrate", EXPR_INT },
|
|
{ "bpm", EXPR_INT },
|
|
{ "codec", EXPR_STR },
|
|
{ "codec_profile",EXPR_STR },
|
|
{ "comment", EXPR_STR },
|
|
{ "date", EXPR_INT },
|
|
{ "discnumber", EXPR_INT },
|
|
{ "duration", EXPR_INT },
|
|
{ "filename", EXPR_STR },
|
|
{ "genre", EXPR_STR },
|
|
{ "media", EXPR_STR },
|
|
{ "originaldate",EXPR_INT },
|
|
{ "play_count", EXPR_INT },
|
|
{ "stream", EXPR_BOOL },
|
|
{ "tag", EXPR_BOOL },
|
|
{ "title", EXPR_STR },
|
|
{ "tracknumber",EXPR_INT },
|
|
{ NULL, -1 },
|
|
};
|
|
|
|
static const char *lookup_long_key(char c)
|
|
{
|
|
int i;
|
|
for (i = 0; map_short2long[i].short_key; i++) {
|
|
if (map_short2long[i].short_key == c)
|
|
return map_short2long[i].long_key;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static enum expr_type lookup_key_type(const char *key)
|
|
{
|
|
int i;
|
|
for (i = 0; builtin[i].key; i++) {
|
|
int cmp = strcmp(key, builtin[i].key);
|
|
if (cmp == 0)
|
|
return builtin[i].type;
|
|
if (cmp < 0)
|
|
break;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static unsigned long stack4_new(void)
|
|
{
|
|
return 0;
|
|
}
|
|
static void stack4_push(unsigned long *s, unsigned long e)
|
|
{
|
|
*s = (*s << 4) | e;
|
|
}
|
|
static void stack4_pop(unsigned long *s)
|
|
{
|
|
*s = *s >> 4;
|
|
}
|
|
static unsigned long stack4_top(unsigned long s)
|
|
{
|
|
return s & 0xf;
|
|
}
|
|
static void stack4_replace_top(unsigned long *s, unsigned long e)
|
|
{
|
|
*s = (*s & ~0xf) | e;
|
|
}
|
|
|
|
static char *expand_short_expr(const char *expr_short)
|
|
{
|
|
/* state space, can contain maximal 15 states */
|
|
enum state_type {
|
|
ST_SKIP_SPACE = 1,
|
|
ST_TOP,
|
|
ST_EXPECT_KEY,
|
|
ST_EXPECT_OP,
|
|
ST_EXPECT_INT,
|
|
ST_IN_INT,
|
|
ST_MEM_INT,
|
|
ST_IN_2ND_INT,
|
|
ST_EXPECT_STR,
|
|
ST_IN_QUOTE_STR,
|
|
ST_IN_STR,
|
|
};
|
|
|
|
size_t len_expr_short = strlen(expr_short);
|
|
/* worst case blowup of expr_short is 31/5 (e.g. ~n1-2), so take x7:
|
|
* strlen("~n1-2") == 5
|
|
* strlen("(tracknumber>=1&tracknumber<=2)") == 31
|
|
*/
|
|
char *out = xnew(char, len_expr_short * 7);
|
|
char *num = NULL;
|
|
size_t i, i_num = 0, k = 0;
|
|
const char *key = NULL;
|
|
int level = 0;
|
|
enum expr_type etype;
|
|
/* used as state-stack, can contain at least 32/4 = 8 states */
|
|
unsigned long state_stack = stack4_new();
|
|
stack4_push(&state_stack, ST_TOP);
|
|
stack4_push(&state_stack, ST_SKIP_SPACE);
|
|
|
|
/* include terminal '\0' to recognize end of string */
|
|
for (i = 0; i <= len_expr_short; i++) {
|
|
unsigned char c = expr_short[i];
|
|
switch (stack4_top(state_stack)) {
|
|
case ST_SKIP_SPACE:
|
|
if (c != ' ') {
|
|
stack4_pop(&state_stack);
|
|
i--;
|
|
}
|
|
break;
|
|
case ST_TOP:
|
|
switch (c) {
|
|
case '~':
|
|
stack4_push(&state_stack, ST_EXPECT_OP);
|
|
stack4_push(&state_stack, ST_SKIP_SPACE);
|
|
stack4_push(&state_stack, ST_EXPECT_KEY);
|
|
break;
|
|
case '(':
|
|
level++;
|
|
/* Fall through */
|
|
case '!':
|
|
case '|':
|
|
out[k++] = c;
|
|
stack4_push(&state_stack, ST_SKIP_SPACE);
|
|
break;
|
|
case ')':
|
|
level--;
|
|
out[k++] = c;
|
|
stack4_push(&state_stack, ST_EXPECT_OP);
|
|
stack4_push(&state_stack, ST_SKIP_SPACE);
|
|
break;
|
|
case '\0':
|
|
if (level > 0) {
|
|
set_error("')' expected");
|
|
goto error_exit;
|
|
}
|
|
out[k++] = c;
|
|
break;
|
|
default:
|
|
set_error("unexpected '%c'", c);
|
|
goto error_exit;
|
|
}
|
|
break;
|
|
case ST_EXPECT_KEY:
|
|
stack4_pop(&state_stack);
|
|
key = lookup_long_key(c);
|
|
if (!key) {
|
|
set_error("unknown short key %c", c);
|
|
goto error_exit;
|
|
}
|
|
etype = lookup_key_type(key);
|
|
if (etype == EXPR_INT) {
|
|
stack4_push(&state_stack, ST_EXPECT_INT);
|
|
out[k++] = '(';
|
|
} else if (etype == EXPR_STR) {
|
|
stack4_push(&state_stack, ST_EXPECT_STR);
|
|
} else if (etype != EXPR_BOOL) {
|
|
BUG("wrong etype: %d\n", etype);
|
|
}
|
|
strcpy(out+k, key);
|
|
k += strlen(key);
|
|
stack4_push(&state_stack, ST_SKIP_SPACE);
|
|
break;
|
|
case ST_EXPECT_OP:
|
|
if (c == '~' || c == '(' || c == '!')
|
|
out[k++] = '&';
|
|
i--;
|
|
stack4_replace_top(&state_stack, ST_SKIP_SPACE);
|
|
break;
|
|
case ST_EXPECT_INT:
|
|
if (c == '<' || c == '>') {
|
|
out[k++] = c;
|
|
stack4_replace_top(&state_stack, ST_IN_INT);
|
|
} else if (c == '-') {
|
|
out[k++] = '<';
|
|
out[k++] = '=';
|
|
stack4_replace_top(&state_stack, ST_IN_INT);
|
|
} else if (isdigit(c)) {
|
|
if (!num)
|
|
num = xnew(char, len_expr_short);
|
|
num[i_num++] = c;
|
|
stack4_replace_top(&state_stack, ST_MEM_INT);
|
|
} else {
|
|
set_error("integer expected", expr_short);
|
|
goto error_exit;
|
|
}
|
|
break;
|
|
case ST_IN_INT:
|
|
if (isdigit(c)) {
|
|
out[k++] = c;
|
|
} else {
|
|
i -= 1;
|
|
stack4_pop(&state_stack);
|
|
out[k++] = ')';
|
|
}
|
|
break;
|
|
case ST_MEM_INT:
|
|
if (isdigit(c)) {
|
|
num[i_num++] = c;
|
|
} else {
|
|
if (c == '-') {
|
|
out[k++] = '>';
|
|
out[k++] = '=';
|
|
stack4_replace_top(&state_stack, ST_IN_2ND_INT);
|
|
} else {
|
|
out[k++] = '=';
|
|
i--;
|
|
stack4_pop(&state_stack);
|
|
}
|
|
strncpy(out+k, num, i_num);
|
|
k += i_num;
|
|
i_num = 0;
|
|
if (c != '-')
|
|
out[k++] = ')';
|
|
}
|
|
break;
|
|
case ST_IN_2ND_INT:
|
|
if (isdigit(c)) {
|
|
num[i_num++] = c;
|
|
} else {
|
|
i--;
|
|
stack4_pop(&state_stack);
|
|
if (i_num > 0) {
|
|
out[k++] = '&';
|
|
strcpy(out+k, key);
|
|
k += strlen(key);
|
|
out[k++] = '<';
|
|
out[k++] = '=';
|
|
strncpy(out+k, num, i_num);
|
|
k += i_num;
|
|
}
|
|
out[k++] = ')';
|
|
}
|
|
break;
|
|
case ST_EXPECT_STR:
|
|
out[k++] = '=';
|
|
if (c == '"') {
|
|
stack4_replace_top(&state_stack, ST_IN_QUOTE_STR);
|
|
out[k++] = c;
|
|
} else {
|
|
stack4_replace_top(&state_stack, ST_IN_STR);
|
|
out[k++] = '"';
|
|
out[k++] = '*';
|
|
out[k++] = c;
|
|
}
|
|
break;
|
|
case ST_IN_QUOTE_STR:
|
|
if (c == '"' && expr_short[i-1] != '\\') {
|
|
stack4_pop(&state_stack);
|
|
}
|
|
out[k++] = c;
|
|
break;
|
|
case ST_IN_STR:
|
|
/* isalnum() doesn't work for multi-byte characters */
|
|
if (c != '~' && c != '!' && c != '|' &&
|
|
c != '(' && c != ')' && c != '\0') {
|
|
out[k++] = c;
|
|
} else {
|
|
while (k > 0 && out[k-1] == ' ')
|
|
k--;
|
|
out[k++] = '*';
|
|
out[k++] = '"';
|
|
i--;
|
|
stack4_pop(&state_stack);
|
|
}
|
|
break;
|
|
default:
|
|
BUG("state %ld not covered", stack4_top(state_stack));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (num)
|
|
free(num);
|
|
|
|
d_print("expanded \"%s\" to \"%s\"\n", expr_short, out);
|
|
|
|
return out;
|
|
|
|
error_exit:
|
|
if (num)
|
|
free(num);
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
int expr_is_short(const char *str)
|
|
{
|
|
int i;
|
|
for (i = 0; str[i]; i++) {
|
|
if (str[i] == '~')
|
|
return 1;
|
|
if (str[i] != '!' && str[i] != '(' && str[i] != ' ')
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
struct expr *expr_parse(const char *str)
|
|
{
|
|
return expr_parse_i(str, "filter contains control characters", 1);
|
|
}
|
|
|
|
struct expr *expr_parse_i(const char *str, const char *err_msg, int check_short)
|
|
{
|
|
LIST_HEAD(head);
|
|
struct expr *root = NULL;
|
|
struct list_head *item;
|
|
char *long_str = NULL, *u_str = NULL;
|
|
int i;
|
|
|
|
for (i = 0; str[i]; i++) {
|
|
unsigned char c = str[i];
|
|
if (c < 0x20) {
|
|
set_error(err_msg);
|
|
goto out;
|
|
}
|
|
}
|
|
if (!using_utf8 && utf8_encode(str, charset, &u_str) == 0) {
|
|
str = u_str;
|
|
}
|
|
if (!u_is_valid(str)) {
|
|
set_error("invalid UTF-8");
|
|
goto out;
|
|
}
|
|
|
|
if (check_short && expr_is_short(str)) {
|
|
str = long_str = expand_short_expr(str);
|
|
if (!str)
|
|
goto out;
|
|
}
|
|
|
|
if (tokenize(&head, str))
|
|
goto out;
|
|
|
|
item = head.next;
|
|
if (parse(&root, &head, &item, 0))
|
|
root = NULL;
|
|
free_tokens(&head);
|
|
|
|
out:
|
|
free(u_str);
|
|
free(long_str);
|
|
return root;
|
|
}
|
|
|
|
int expr_check_leaves(struct expr **exprp, const char *(*get_filter)(const char *name))
|
|
{
|
|
struct expr *expr = *exprp;
|
|
struct expr *e;
|
|
const char *filter;
|
|
int i, rc;
|
|
|
|
if (expr->left) {
|
|
if (expr_check_leaves(&expr->left, get_filter))
|
|
return -1;
|
|
if (expr->right)
|
|
return expr_check_leaves(&expr->right, get_filter);
|
|
return 0;
|
|
}
|
|
|
|
for (i = 0; builtin[i].key; i++) {
|
|
int cmp = strcmp(expr->key, builtin[i].key);
|
|
|
|
if (cmp > 0)
|
|
continue;
|
|
if (cmp < 0)
|
|
break;
|
|
|
|
if (builtin[i].type != expr->type) {
|
|
/* type mismatch */
|
|
set_error("%s is %s", builtin[i].key, expr_names[builtin[i].type]);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (expr->type != EXPR_BOOL) {
|
|
/* unknown key */
|
|
set_error("unknown key %s", expr->key);
|
|
return -1;
|
|
}
|
|
|
|
/* user defined filter */
|
|
filter = get_filter(expr->key);
|
|
if (filter == NULL) {
|
|
set_error("unknown filter or boolean %s", expr->key);
|
|
return -1;
|
|
}
|
|
e = expr_parse(filter);
|
|
if (e == NULL) {
|
|
return -1;
|
|
}
|
|
rc = expr_check_leaves(&e, get_filter);
|
|
if (rc) {
|
|
expr_free(e);
|
|
return rc;
|
|
}
|
|
|
|
/* replace */
|
|
e->parent = expr->parent;
|
|
expr_free(expr);
|
|
|
|
/* this sets parents left pointer */
|
|
*exprp = e;
|
|
return 0;
|
|
}
|
|
|
|
unsigned int expr_get_match_type(struct expr *expr)
|
|
{
|
|
const char *key;
|
|
|
|
if (expr->left) {
|
|
unsigned int left = expr_get_match_type(expr->left);
|
|
if (expr->type == EXPR_AND || expr->type == EXPR_OR)
|
|
return left | expr_get_match_type(expr->right);
|
|
return left;
|
|
}
|
|
|
|
key = expr->key;
|
|
if (strcmp(key, "artist") == 0 || strcmp(key, "albumartist") == 0)
|
|
return TI_MATCH_ARTIST;
|
|
if (strcmp(key, "album") == 0 || strcmp(key, "discnumber") == 0)
|
|
return TI_MATCH_ALBUM;
|
|
if (strcmp(key, "title") == 0 || strcmp(key, "tracknumber") == 0)
|
|
return TI_MATCH_TITLE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int expr_is_harmless(const struct expr *expr)
|
|
{
|
|
switch (expr->type) {
|
|
case EXPR_OR:
|
|
case EXPR_NOT:
|
|
return 0;
|
|
case EXPR_AND:
|
|
expr = expr->right;
|
|
default:
|
|
break;
|
|
}
|
|
if (expr->type == EXPR_INT) {
|
|
switch (expr->eint.op) {
|
|
case IOP_LT:
|
|
case IOP_EQ:
|
|
case IOP_LE:
|
|
return 0;
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
if (expr->type == EXPR_ID)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
static const char *str_val(const char *key, struct track_info *ti, char **need_free)
|
|
{
|
|
const char *val;
|
|
*need_free = NULL;
|
|
if (strcmp(key, "filename") == 0) {
|
|
val = ti->filename;
|
|
if (!using_utf8 && utf8_encode(val, charset, need_free) == 0) {
|
|
val = *need_free;
|
|
}
|
|
} else if (strcmp(key, "codec") == 0) {
|
|
val = ti->codec;
|
|
} else if (strcmp(key, "codec_profile") == 0) {
|
|
val = ti->codec_profile;
|
|
} else {
|
|
val = keyvals_get_val(ti->comments, key);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
static int int_val(const char *key, struct track_info *ti)
|
|
{
|
|
int val;
|
|
if (strcmp(key, "duration") == 0) {
|
|
val = ti->duration;
|
|
/* duration of a stream is infinite (well, almost) */
|
|
if (is_http_url(ti->filename))
|
|
val = INT_MAX;
|
|
} else if (strcmp(key, "date") == 0) {
|
|
val = (ti->date >= 0) ? (ti->date / 10000) : -1;
|
|
} else if (strcmp(key, "originaldate") == 0) {
|
|
val = (ti->originaldate >= 0) ? (ti->originaldate / 10000) : -1;
|
|
} else if (strcmp(key, "bitrate") == 0) {
|
|
val = (ti->bitrate >= 0) ? (int) (ti->bitrate / 1000. + 0.5) : -1;
|
|
} else if (strcmp(key, "play_count") == 0) {
|
|
val = ti->play_count;
|
|
} else if (strcmp(key, "bpm") == 0) {
|
|
val = ti->bpm;
|
|
} else {
|
|
val = comments_get_int(ti->comments, key);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
int expr_op_to_bool(int res, int op)
|
|
{
|
|
switch (op) {
|
|
case OP_LT:
|
|
return res < 0;
|
|
case OP_LE:
|
|
return res <= 0;
|
|
case OP_EQ:
|
|
return res == 0;
|
|
case OP_GE:
|
|
return res >= 0;
|
|
case OP_GT:
|
|
return res > 0;
|
|
case OP_NE:
|
|
return res != 0;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int expr_eval(struct expr *expr, struct track_info *ti)
|
|
{
|
|
enum expr_type type = expr->type;
|
|
const char *key;
|
|
|
|
if (expr->left) {
|
|
int left = expr_eval(expr->left, ti);
|
|
|
|
if (type == EXPR_AND)
|
|
return left && expr_eval(expr->right, ti);
|
|
if (type == EXPR_OR)
|
|
return left || expr_eval(expr->right, ti);
|
|
/* EXPR_NOT */
|
|
return !left;
|
|
}
|
|
|
|
key = expr->key;
|
|
if (type == EXPR_STR) {
|
|
int res;
|
|
char *need_free;
|
|
const char *val = str_val(key, ti, &need_free);
|
|
if (!val)
|
|
val = "";
|
|
res = glob_match(&expr->estr.glob_head, val);
|
|
free(need_free);
|
|
if (expr->estr.op == SOP_EQ)
|
|
return res;
|
|
return !res;
|
|
} else if (type == EXPR_INT) {
|
|
int val = int_val(key, ti);
|
|
int res;
|
|
if (expr->eint.val == -1) {
|
|
/* -1 is "not set"
|
|
* doesn't make sense to do 123 < "not set"
|
|
* but it makes sense to do date=-1 (date is not set)
|
|
*/
|
|
if (expr->eint.op == IOP_EQ)
|
|
return val == -1;
|
|
if (expr->eint.op == IOP_NE)
|
|
return val != -1;
|
|
}
|
|
if (val == -1) {
|
|
/* tag not set, can't compare */
|
|
return 0;
|
|
}
|
|
res = val - expr->eint.val;
|
|
return expr_op_to_bool(res, expr->eint.op);
|
|
} else if (type == EXPR_ID) {
|
|
int a = 0, b = 0;
|
|
const char *sa, *sb;
|
|
char *fa, *fb;
|
|
int res = 0;
|
|
if ((sa = str_val(key, ti, &fa))) {
|
|
if ((sb = str_val(expr->eid.key, ti, &fb))) {
|
|
res = strcmp(sa, sb);
|
|
free(fa);
|
|
free(fb);
|
|
return expr_op_to_bool(res, expr->eid.op);
|
|
}
|
|
free(fa);
|
|
} else {
|
|
a = int_val(key, ti);
|
|
b = int_val(expr->eid.key, ti);
|
|
res = a - b;
|
|
if (a == -1 || b == -1) {
|
|
switch (expr->eid.op) {
|
|
case KOP_EQ:
|
|
return res == 0;
|
|
case KOP_NE:
|
|
return res != 0;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
return expr_op_to_bool(res, expr->eid.op);
|
|
}
|
|
return res;
|
|
}
|
|
if (strcmp(key, "stream") == 0)
|
|
return is_http_url(ti->filename);
|
|
return track_info_has_tag(ti);
|
|
}
|
|
|
|
void expr_free(struct expr *expr)
|
|
{
|
|
if (expr->left) {
|
|
expr_free(expr->left);
|
|
if (expr->right)
|
|
expr_free(expr->right);
|
|
}
|
|
free(expr->key);
|
|
if (expr->type == EXPR_STR)
|
|
glob_free(&expr->estr.glob_head);
|
|
else if (expr->type == EXPR_ID)
|
|
free(expr->eid.key);
|
|
free(expr);
|
|
}
|
|
|
|
const char *expr_error(void)
|
|
{
|
|
return error_buf;
|
|
}
|