/* recsel.c - Record selection * Copyright (C) 2014, 2016 Werner Koch * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - 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. * * or both in parallel, as here. * * This file 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 . */ #include #include #include #include #include #include #include "util.h" #include "recsel.h" /* Select operators. */ typedef enum { SELECT_SAME, SELECT_SUB, SELECT_NONEMPTY, SELECT_ISTRUE, SELECT_EQ, /* Numerically equal. */ SELECT_LE, SELECT_GE, SELECT_LT, SELECT_GT, SELECT_STRLE, /* String is less or equal. */ SELECT_STRGE, SELECT_STRLT, SELECT_STRGT } select_op_t; /* Definition for a select expression. */ struct recsel_expr_s { recsel_expr_t next; select_op_t op; /* Operation code. */ unsigned int not:1; /* Negate operators. */ unsigned int disjun:1;/* Start of a disjunction. */ unsigned int xcase:1; /* String match is case sensitive. */ const char *value; /* (Points into NAME.) */ long numvalue; /* strtol of VALUE. */ char name[1]; /* Name of the property. */ }; /* Helper */ static inline gpg_error_t my_error_from_syserror (void) { return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } /* Helper */ static inline gpg_error_t my_error (gpg_err_code_t ec) { return gpg_err_make (default_errsource, ec); } /* Return a pointer to the next logical connection operator or NULL if * none. */ static char * find_next_lc (char *string) { char *p1, *p2; p1 = strchr (string, '&'); if (p1 && p1[1] != '&') p1 = NULL; p2 = strchr (string, '|'); if (p2 && p2[1] != '|') p2 = NULL; if (p1 && !p2) return p1; if (!p1) return p2; return p1 < p2 ? p1 : p2; } /* Parse an expression. The expression syntax is: * * [] {{} PROPNAME VALUE []} * * A [] indicates an optional part, a {} a repetition. PROPNAME and * VALUE may not be the empty string. White space between the * elements is ignored. Numerical values are computed as long int; * standard C notation applies. is the logical connection * operator; either "&&" for a conjunction or "||" for a disjunction. * A conjunction is assumed at the begin of an expression and * conjunctions have higher precedence than disjunctions. If VALUE * starts with one of the characters used in any a space after * the is required. A VALUE is terminated by an unless the * "--" is used in which case the VALUE spans to the end of the * expression. may be any of * * =~ Substring must match * !~ Substring must not match * = The full string must match * <> The full string must not match * == The numerical value must match * != The numerical value must not match * <= The numerical value of the field must be LE than the value. * < The numerical value of the field must be LT than the value. * >= The numerical value of the field must be GT than the value. * >= The numerical value of the field must be GE than the value. * -n True if value is not empty (no VALUE parameter allowed). * -z True if value is empty (no VALUE parameter allowed). * -t Alias for "PROPNAME != 0" (no VALUE parameter allowed). * -f Alias for "PROPNAME == 0" (no VALUE parameter allowed). * * Values for must be space separated and any of: * * -- VALUE spans to the end of the expression. * -c The string match in this part is done case-sensitive. * -t Do not trim leading and trailing spaces from VALUE. * Note that a space after is here required. * -r RFU: String match uses a regular expression * * For example four calls to recsel_parse_expr() with these values for * EXPR * * "uid =~ Alfa" * "&& uid !~ Test" * "|| uid =~ Alpha" * "uid !~ Test" * * or the equivalent expression * * "uid =~ Alfa" && uid !~ Test" || uid =~ Alpha" && "uid !~ Test" * * are making a selector for records where the "uid" property contains * the strings "Alfa" or "Alpha" but not the String "test". * * The caller must pass the address of a selector variable to this * function and initialize the value of the function to NULL before * the first call. recsel_release needs to be called to free the * selector. */ gpg_error_t recsel_parse_expr (recsel_expr_t *selector, const char *expression) { recsel_expr_t se_head = NULL; recsel_expr_t se, se2; char *expr_buffer; char *expr; char *s0, *s; int toend = 0; int xcase = 0; int notrim = 0; int disjun = 0; char *next_lc = NULL; while (*expression == ' ' || *expression == '\t') expression++; expr_buffer = xtrystrdup (expression); if (!expr_buffer) return my_error_from_syserror (); expr = expr_buffer; if (*expr == '|' && expr[1] == '|') { disjun = 1; expr += 2; } else if (*expr == '&' && expr[1] == '&') expr += 2; next_term: while (*expr == ' ' || *expr == '\t') expr++; while (*expr == '-') { switch (*++expr) { case '-': toend = 1; break; case 'c': xcase = 1; break; case 't': notrim = 1; break; default: log_error ("invalid flag '-%c' in expression\n", *expr); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_INV_FLAG); } expr++; while (*expr == ' ' || *expr == '\t') expr++; } next_lc = toend? NULL : find_next_lc (expr); if (next_lc) *next_lc = 0; /* Terminate this term. */ se = xtrymalloc (sizeof *se + strlen (expr)); if (!se) { gpg_error_t err = my_error_from_syserror (); recsel_release (se_head); xfree (expr_buffer); return err; } strcpy (se->name, expr); se->next = NULL; se->not = 0; se->disjun = disjun; se->xcase = xcase; if (!se_head) se_head = se; else { for (se2 = se_head; se2->next; se2 = se2->next) ; se2->next = se; } s = strpbrk (expr, "=<>!~-"); if (!s || s == expr ) { log_error ("no field name given in expression\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_NO_NAME); } s0 = s; if (!strncmp (s, "=~", 2)) { se->op = SELECT_SUB; s += 2; } else if (!strncmp (s, "!~", 2)) { se->op = SELECT_SUB; se->not = 1; s += 2; } else if (!strncmp (s, "<>", 2)) { se->op = SELECT_SAME; se->not = 1; s += 2; } else if (!strncmp (s, "==", 2)) { se->op = SELECT_EQ; s += 2; } else if (!strncmp (s, "!=", 2)) { se->op = SELECT_EQ; se->not = 1; s += 2; } else if (!strncmp (s, "<=", 2)) { se->op = SELECT_LE; s += 2; } else if (!strncmp (s, ">=", 2)) { se->op = SELECT_GE; s += 2; } else if (!strncmp (s, "<", 1)) { se->op = SELECT_LT; s += 1; } else if (!strncmp (s, ">", 1)) { se->op = SELECT_GT; s += 1; } else if (!strncmp (s, "=", 1)) { se->op = SELECT_SAME; s += 1; } else if (!strncmp (s, "-z", 2)) { se->op = SELECT_NONEMPTY; se->not = 1; s += 2; } else if (!strncmp (s, "-n", 2)) { se->op = SELECT_NONEMPTY; s += 2; } else if (!strncmp (s, "-f", 2)) { se->op = SELECT_ISTRUE; se->not = 1; s += 2; } else if (!strncmp (s, "-t", 2)) { se->op = SELECT_ISTRUE; s += 2; } else if (!strncmp (s, "-le", 3)) { se->op = SELECT_STRLE; s += 3; } else if (!strncmp (s, "-ge", 3)) { se->op = SELECT_STRGE; s += 3; } else if (!strncmp (s, "-lt", 3)) { se->op = SELECT_STRLT; s += 3; } else if (!strncmp (s, "-gt", 3)) { se->op = SELECT_STRGT; s += 3; } else { log_error ("invalid operator in expression\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_INV_OP); } /* We require that a space is used if the value starts with any of the operator characters. */ if (se->op == SELECT_NONEMPTY || se->op == SELECT_ISTRUE) ; else if (strchr ("=<>!~", *s)) { log_error ("invalid operator in expression\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_INV_OP); } if (*s == ' ' || *s == '\t') s++; if (!notrim) while (*s == ' ' || *s == '\t') s++; if (se->op == SELECT_NONEMPTY || se->op == SELECT_ISTRUE) { if (*s) { log_error ("value given for -n or -z\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_SYNTAX); } } else { if (!*s) { log_error ("no value given in expression\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_MISSING_VALUE); } } se->name[s0 - expr] = 0; trim_spaces (se->name); if (!se->name[0]) { log_error ("no field name given in expression\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_NO_NAME); } if (!notrim) trim_spaces (se->name + (s - expr)); se->value = se->name + (s - expr); if (!se->value[0] && !(se->op == SELECT_NONEMPTY || se->op == SELECT_ISTRUE)) { log_error ("no value given in expression\n"); recsel_release (se_head); xfree (expr_buffer); return my_error (GPG_ERR_MISSING_VALUE); } se->numvalue = strtol (se->value, NULL, 0); if (next_lc) { disjun = next_lc[1] == '|'; expr = next_lc + 2; goto next_term; } /* Read:y Append to passes last selector. */ if (!*selector) *selector = se_head; else { for (se2 = *selector; se2->next; se2 = se2->next) ; se2->next = se_head; } xfree (expr_buffer); return 0; } void recsel_release (recsel_expr_t a) { while (a) { recsel_expr_t tmp = a->next; xfree (a); a = tmp; } } void recsel_dump (recsel_expr_t selector) { recsel_expr_t se; log_debug ("--- Begin selectors ---\n"); for (se = selector; se; se = se->next) { log_debug ("%s %s %s %s '%s'\n", se==selector? " ": (se->disjun? "||":"&&"), se->xcase? "-c":" ", se->name, se->op == SELECT_SAME? (se->not? "<>":"= "): se->op == SELECT_SUB? (se->not? "!~":"=~"): se->op == SELECT_NONEMPTY?(se->not? "-z":"-n"): se->op == SELECT_ISTRUE? (se->not? "-f":"-t"): se->op == SELECT_EQ? (se->not? "!=":"=="): se->op == SELECT_LT? "< ": se->op == SELECT_LE? "<=": se->op == SELECT_GT? "> ": se->op == SELECT_GE? ">=": se->op == SELECT_STRLT? "-lt": se->op == SELECT_STRLE? "-le": se->op == SELECT_STRGT? "-gt": se->op == SELECT_STRGE? "-ge": /**/ "[oops]", se->value); } log_debug ("--- End selectors ---\n"); } /* Return true if the record RECORD has been selected. The GETVAL * function is called with COOKIE and the NAME of a property used in * the expression. */ int recsel_select (recsel_expr_t selector, const char *(*getval)(void *cookie, const char *propname), void *cookie) { recsel_expr_t se; const char *value; size_t selen, valuelen; long numvalue; int result = 1; se = selector; while (se) { value = getval? getval (cookie, se->name) : NULL; if (!value) value = ""; if (!*value) { /* Field is empty. */ result = 0; } else /* Field has a value. */ { valuelen = strlen (value); numvalue = strtol (value, NULL, 0); selen = strlen (se->value); switch (se->op) { case SELECT_SAME: if (se->xcase) result = (valuelen==selen && !memcmp (value,se->value,selen)); else result = (valuelen==selen && !memicmp (value,se->value,selen)); break; case SELECT_SUB: if (se->xcase) result = !!gnupg_memstr (value, valuelen, se->value); else result = !!memistr (value, valuelen, se->value); break; case SELECT_NONEMPTY: result = !!valuelen; break; case SELECT_ISTRUE: result = !!numvalue; break; case SELECT_EQ: result = (numvalue == se->numvalue); break; case SELECT_GT: result = (numvalue > se->numvalue); break; case SELECT_GE: result = (numvalue >= se->numvalue); break; case SELECT_LT: result = (numvalue < se->numvalue); break; case SELECT_LE: result = (numvalue <= se->numvalue); break; case SELECT_STRGT: if (se->xcase) result = strcmp (value, se->value) > 0; else result = strcasecmp (value, se->value) > 0; break; case SELECT_STRGE: if (se->xcase) result = strcmp (value, se->value) >= 0; else result = strcasecmp (value, se->value) >= 0; break; case SELECT_STRLT: if (se->xcase) result = strcmp (value, se->value) < 0; else result = strcasecmp (value, se->value) < 0; break; case SELECT_STRLE: if (se->xcase) result = strcmp (value, se->value) <= 0; else result = strcasecmp (value, se->value) <= 0; break; } } if (se->not) result = !result; if (result) { /* This expression evaluated to true. See whether there are remaining expressions in this conjunction. */ if (!se->next || se->next->disjun) break; /* All expressions are true. Return True. */ se = se->next; /* Test the next. */ } else { /* This expression evaluated to false and thus the * conjunction evaluates to false. We skip over the * remaining expressions of this conjunction and continue * with the next disjunction if any. */ do se = se->next; while (se && !se->disjun); } } return result; }