LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - when_expr.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 93.4 % 211 197
Test Date: 2026-05-07 15:53:08 Functions: 100.0 % 16 16

            Line data    Source code
       1              : /**
       2              :  * @file when_expr.c
       3              :  * @brief Boolean condition expression parser, evaluator and builder (US-81).
       4              :  */
       5              : 
       6              : #include "when_expr.h"
       7              : #include <ctype.h>
       8              : #include <fnmatch.h>
       9              : #include <stdio.h>
      10              : #include <stdlib.h>
      11              : #include <string.h>
      12              : #include <time.h>
      13              : 
      14              : /* ── Internal types ────────────────────────────────────────────────── */
      15              : 
      16              : typedef enum {
      17              :     T_EOF, T_AND, T_OR, T_NOT, T_LPAREN, T_RPAREN, T_ATOM, T_ERR
      18              : } TokType;
      19              : 
      20              : typedef struct {
      21              :     TokType type;
      22              :     char    field[32];
      23              :     char    pat[1024];
      24              : } Tok;
      25              : 
      26              : typedef struct {
      27              :     const char *p;
      28              :     Tok         cur;
      29              :     int         error;
      30              : } Lex;
      31              : 
      32              : /* ── Lexer ─────────────────────────────────────────────────────────── */
      33              : 
      34          253 : static void lex_skip_ws(Lex *l) {
      35          287 :     while (*l->p == ' ' || *l->p == '\t') l->p++;
      36          253 : }
      37              : 
      38          253 : static Tok lex_read(Lex *l) {
      39              :     Tok t;
      40          253 :     memset(&t, 0, sizeof(t));
      41          253 :     lex_skip_ws(l);
      42              : 
      43          253 :     if (!*l->p)               { t.type = T_EOF;    return t; }
      44          148 :     if (*l->p == '(')         { t.type = T_LPAREN; l->p++; return t; }
      45          148 :     if (*l->p == ')')         { t.type = T_RPAREN; l->p++; return t; }
      46          148 :     if (*l->p == '!')         { t.type = T_NOT;    l->p++; return t; }
      47              : 
      48              :     /* Read word up to ':', space, paren, '!', or EOF */
      49          139 :     const char *ws = l->p;
      50          860 :     while (*l->p && *l->p != ':' && *l->p != '(' && *l->p != ')' &&
      51         1320 :            *l->p != '!' && *l->p != ' ' && *l->p != '\t')
      52          582 :         l->p++;
      53          139 :     size_t wlen = (size_t)(l->p - ws);
      54              : 
      55          139 :     if (wlen == 3 && strncasecmp(ws, "and", 3) == 0) { t.type = T_AND; return t; }
      56          130 :     if (wlen == 2 && strncasecmp(ws, "or",  2) == 0) { t.type = T_OR;  return t; }
      57              : 
      58          122 :     if (*l->p != ':') { t.type = T_ERR; return t; }
      59          122 :     l->p++; /* skip ':' */
      60              : 
      61          122 :     if (wlen >= sizeof(t.field)) wlen = sizeof(t.field) - 1;
      62          122 :     memcpy(t.field, ws, wlen);
      63          122 :     t.field[wlen] = '\0';
      64              : 
      65              :     /* Pattern: until whitespace, ')', '(', or EOF */
      66          122 :     const char *ps = l->p;
      67         1416 :     while (*l->p && *l->p != ' ' && *l->p != '\t' &&
      68         2676 :            *l->p != ')' && *l->p != '(')
      69         1277 :         l->p++;
      70          122 :     size_t plen = (size_t)(l->p - ps);
      71          122 :     if (plen >= sizeof(t.pat)) plen = sizeof(t.pat) - 1;
      72          122 :     memcpy(t.pat, ps, plen);
      73          122 :     t.pat[plen] = '\0';
      74              : 
      75          122 :     t.type = T_ATOM;
      76          122 :     return t;
      77              : }
      78              : 
      79          253 : static void lex_advance(Lex *l) { l->cur = lex_read(l); }
      80              : 
      81              : /* ── AST helpers ───────────────────────────────────────────────────── */
      82              : 
      83          148 : static WhenNode *node_new(WhenNodeType type) {
      84          148 :     WhenNode *n = calloc(1, sizeof(WhenNode));
      85          148 :     if (n) n->type = type;
      86          148 :     return n;
      87              : }
      88              : 
      89          401 : void when_node_free(WhenNode *n) {
      90          401 :     if (!n) return;
      91          148 :     when_node_free(n->left);
      92          148 :     when_node_free(n->right);
      93          148 :     free(n->pattern);
      94          148 :     free(n);
      95              : }
      96              : 
      97              : /* ── Parser (recursive descent) ────────────────────────────────────── */
      98              : 
      99              : static WhenNode *parse_or(Lex *l);
     100              : static WhenNode *parse_and(Lex *l);
     101              : static WhenNode *parse_not(Lex *l);
     102              : static WhenNode *parse_primary(Lex *l);
     103              : 
     104          105 : static WhenNode *parse_or(Lex *l) {
     105          105 :     WhenNode *left = parse_and(l);
     106          105 :     if (!left) return NULL;
     107          113 :     while (l->cur.type == T_OR) {
     108            8 :         lex_advance(l);
     109            8 :         WhenNode *right = parse_and(l);
     110            8 :         if (!right) { when_node_free(left); return NULL; }
     111            8 :         WhenNode *n = node_new(WN_OR);
     112            8 :         if (!n) { when_node_free(left); when_node_free(right); return NULL; }
     113            8 :         n->left = left; n->right = right;
     114            8 :         left = n;
     115              :     }
     116          105 :     return left;
     117              : }
     118              : 
     119          113 : static WhenNode *parse_and(Lex *l) {
     120          113 :     WhenNode *left = parse_not(l);
     121          113 :     if (!left) return NULL;
     122          122 :     while (l->cur.type == T_AND) {
     123            9 :         lex_advance(l);
     124            9 :         WhenNode *right = parse_not(l);
     125            9 :         if (!right) { when_node_free(left); return NULL; }
     126            9 :         WhenNode *n = node_new(WN_AND);
     127            9 :         if (!n) { when_node_free(left); when_node_free(right); return NULL; }
     128            9 :         n->left = left; n->right = right;
     129            9 :         left = n;
     130              :     }
     131          113 :     return left;
     132              : }
     133              : 
     134          131 : static WhenNode *parse_not(Lex *l) {
     135          131 :     if (l->cur.type == T_NOT) {
     136            9 :         lex_advance(l);
     137            9 :         WhenNode *child = parse_not(l);
     138            9 :         if (!child) return NULL;
     139            9 :         WhenNode *n = node_new(WN_NOT);
     140            9 :         if (!n) { when_node_free(child); return NULL; }
     141            9 :         n->left = child;
     142            9 :         return n;
     143              :     }
     144          122 :     return parse_primary(l);
     145              : }
     146              : 
     147          122 : static WhenNode *parse_primary(Lex *l) {
     148          122 :     if (l->cur.type == T_LPAREN) {
     149            0 :         lex_advance(l);
     150            0 :         WhenNode *inner = parse_or(l);
     151            0 :         if (!inner) return NULL;
     152            0 :         if (l->cur.type != T_RPAREN) { when_node_free(inner); return NULL; }
     153            0 :         lex_advance(l);
     154            0 :         return inner;
     155              :     }
     156          122 :     if (l->cur.type == T_ATOM) {
     157              :         WhenNodeType nt;
     158          122 :         if      (strcmp(l->cur.field, "from")    == 0) nt = WN_FROM;
     159           53 :         else if (strcmp(l->cur.field, "to")      == 0) nt = WN_TO;
     160           44 :         else if (strcmp(l->cur.field, "subject") == 0) nt = WN_SUBJECT;
     161           36 :         else if (strcmp(l->cur.field, "label")   == 0) nt = WN_LABEL;
     162           27 :         else if (strcmp(l->cur.field, "body")    == 0) nt = WN_BODY;
     163           18 :         else if (strcmp(l->cur.field, "age-gt")  == 0) nt = WN_AGE_GT;
     164            9 :         else if (strcmp(l->cur.field, "age-lt")  == 0) nt = WN_AGE_LT;
     165            0 :         else { l->error = 1; return NULL; }
     166              : 
     167          122 :         WhenNode *n = node_new(nt);
     168          122 :         if (!n) return NULL;
     169              : 
     170          122 :         if (nt == WN_AGE_GT || nt == WN_AGE_LT) {
     171           18 :             n->age_val = atoi(l->cur.pat);
     172              :         } else {
     173          104 :             n->pattern = strdup(l->cur.pat);
     174          104 :             if (!n->pattern) { free(n); return NULL; }
     175              :         }
     176          122 :         lex_advance(l);
     177          122 :         return n;
     178              :     }
     179            0 :     return NULL;
     180              : }
     181              : 
     182          105 : WhenNode *when_parse(const char *expr) {
     183          105 :     if (!expr || !*expr) return NULL;
     184              :     Lex l;
     185          105 :     memset(&l, 0, sizeof(l));
     186          105 :     l.p = expr;
     187          105 :     lex_advance(&l);
     188          105 :     if (l.cur.type == T_EOF) return NULL;
     189          105 :     WhenNode *tree = parse_or(&l);
     190          105 :     if (!tree || l.cur.type != T_EOF || l.error) {
     191            0 :         when_node_free(tree);
     192            0 :         return NULL;
     193              :     }
     194          105 :     return tree;
     195              : }
     196              : 
     197              : /* ── Evaluator ─────────────────────────────────────────────────────── */
     198              : 
     199           86 : static int when_glob(const char *pattern, const char *val) {
     200           86 :     if (!pattern || !pattern[0]) return 1;
     201           86 :     if (!val || !val[0]) return 0;
     202           77 :     return fnmatch(pattern, val, FNM_CASEFOLD) == 0;
     203              : }
     204              : 
     205            9 : static int when_label_match(const char *pattern, const char *csv) {
     206            9 :     if (!pattern || !pattern[0]) return 1;
     207            9 :     if (!csv || !csv[0]) return 0;
     208            8 :     char *copy = strdup(csv);
     209            8 :     if (!copy) return 0;
     210            8 :     int found = 0;
     211            8 :     char *tok = copy;
     212           16 :     while (tok && *tok) {
     213            8 :         char *sep = strchr(tok, ',');
     214            8 :         if (sep) *sep = '\0';
     215            8 :         if (fnmatch(pattern, tok, FNM_CASEFOLD) == 0) { found = 1; break; }
     216            8 :         tok = sep ? sep + 1 : NULL;
     217              :     }
     218            8 :     free(copy);
     219            8 :     return found;
     220              : }
     221              : 
     222          148 : int when_eval(const WhenNode *n,
     223              :               const char *from, const char *subject,
     224              :               const char *to, const char *labels_csv,
     225              :               const char *body, time_t message_date)
     226              : {
     227          148 :     if (!n) return 1;
     228          148 :     switch (n->type) {
     229            8 :     case WN_OR:
     230           16 :         return when_eval(n->left,  from, subject, to, labels_csv, body, message_date) ||
     231            8 :                when_eval(n->right, from, subject, to, labels_csv, body, message_date);
     232            9 :     case WN_AND:
     233           18 :         return when_eval(n->left,  from, subject, to, labels_csv, body, message_date) &&
     234            9 :                when_eval(n->right, from, subject, to, labels_csv, body, message_date);
     235            9 :     case WN_NOT:
     236            9 :         return !when_eval(n->left, from, subject, to, labels_csv, body, message_date);
     237           69 :     case WN_FROM:    return when_glob(n->pattern, from);
     238            9 :     case WN_TO:      return when_glob(n->pattern, to);
     239            8 :     case WN_SUBJECT: return when_glob(n->pattern, subject);
     240            9 :     case WN_LABEL:   return when_label_match(n->pattern, labels_csv);
     241            9 :     case WN_BODY:
     242            9 :         if (!body) return 0;
     243            0 :         return when_glob(n->pattern, body);
     244            9 :     case WN_AGE_GT:
     245            9 :         if (message_date <= 0) return 0;
     246            0 :         return (int)((time(NULL) - message_date) / 86400) > n->age_val;
     247            9 :     case WN_AGE_LT:
     248            9 :         if (message_date <= 0) return 0;
     249            0 :         return (int)((time(NULL) - message_date) / 86400) < n->age_val;
     250              :     }
     251            0 :     return 0;
     252              : }
     253              : 
     254              : /* ── Expression builders ───────────────────────────────────────────── */
     255              : 
     256              : /* Append an atom "field:pattern" (optionally negated) to a buffer. */
     257         1134 : static void buf_append_atom(char *buf, size_t cap,
     258              :                             const char *field, const char *pattern,
     259              :                             int negated, int *first)
     260              : {
     261         1134 :     if (!pattern || !pattern[0]) return;
     262          113 :     size_t used = strlen(buf);
     263          113 :     if (!*first && used + 5 < cap)
     264            7 :         strncat(buf, " and ", cap - used - 1);
     265          113 :     used = strlen(buf);
     266          113 :     if (negated && used + 2 < cap) {
     267            3 :         strncat(buf, "!", cap - used - 1);
     268            3 :         used++;
     269              :     }
     270          113 :     strncat(buf, field, cap - used - 1);
     271          113 :     used = strlen(buf);
     272          113 :     strncat(buf, ":", cap - used - 1);
     273          113 :     used = strlen(buf);
     274          113 :     strncat(buf, pattern, cap - used - 1);
     275          113 :     *first = 0;
     276              : }
     277              : 
     278          141 : char *when_from_flat(const char *if_from, const char *if_subject,
     279              :                      const char *if_to, const char *if_label,
     280              :                      const char *if_not_from, const char *if_not_subject,
     281              :                      const char *if_not_to, const char *if_body,
     282              :                      int if_age_gt, int if_age_lt)
     283              : {
     284          141 :     char buf[8192] = {0};
     285          141 :     int first = 1;
     286              :     char tmp[32];
     287              : 
     288          141 :     buf_append_atom(buf, sizeof(buf), "from",    if_from,        0, &first);
     289          141 :     buf_append_atom(buf, sizeof(buf), "subject", if_subject,     0, &first);
     290          141 :     buf_append_atom(buf, sizeof(buf), "to",      if_to,          0, &first);
     291          141 :     buf_append_atom(buf, sizeof(buf), "label",   if_label,       0, &first);
     292          141 :     buf_append_atom(buf, sizeof(buf), "from",    if_not_from,    1, &first);
     293          141 :     buf_append_atom(buf, sizeof(buf), "subject", if_not_subject, 1, &first);
     294          141 :     buf_append_atom(buf, sizeof(buf), "to",      if_not_to,      1, &first);
     295          141 :     buf_append_atom(buf, sizeof(buf), "body",    if_body,        0, &first);
     296              : 
     297          141 :     if (if_age_gt > 0) {
     298            3 :         snprintf(tmp, sizeof(tmp), "%d", if_age_gt);
     299            3 :         buf_append_atom(buf, sizeof(buf), "age-gt", tmp, 0, &first);
     300              :     }
     301          141 :     if (if_age_lt > 0) {
     302            3 :         snprintf(tmp, sizeof(tmp), "%d", if_age_lt);
     303            3 :         buf_append_atom(buf, sizeof(buf), "age-lt", tmp, 0, &first);
     304              :     }
     305              : 
     306          141 :     return first ? NULL : strdup(buf);
     307              : }
     308              : 
     309           41 : char *when_from_conds(const WhenCond *conds, int nconds, int is_or) {
     310           41 :     if (nconds == 0) return NULL;
     311           41 :     const char *sep = is_or ? " or " : " and ";
     312           41 :     char buf[8192] = {0};
     313           41 :     size_t cap = sizeof(buf);
     314              : 
     315           85 :     for (int i = 0; i < nconds; i++) {
     316           44 :         const WhenCond *c = &conds[i];
     317           44 :         if (!c->field || !c->pattern) continue;
     318              : 
     319           44 :         size_t used = strlen(buf);
     320           44 :         if (i > 0 && used + strlen(sep) + 1 < cap) {
     321            3 :             strncat(buf, sep, cap - used - 1);
     322            3 :             used = strlen(buf);
     323              :         }
     324           44 :         if (c->negated && used + 2 < cap) {
     325            4 :             strncat(buf, "!", cap - used - 1);
     326            4 :             used = strlen(buf);
     327              :         }
     328           44 :         strncat(buf, c->field, cap - used - 1);
     329           44 :         used = strlen(buf);
     330           44 :         strncat(buf, ":", cap - used - 1);
     331           44 :         used = strlen(buf);
     332           44 :         strncat(buf, c->pattern, cap - used - 1);
     333              :     }
     334              : 
     335           41 :     return buf[0] ? strdup(buf) : NULL;
     336              : }
        

Generated by: LCOV version 2.0-1