LCOV - code coverage report
Current view: top level - libemail/src/infrastructure - when_expr.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 99.5 % 211 210
Test Date: 2026-05-07 15:53:07 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          365 : static void lex_skip_ws(Lex *l) {
      35          424 :     while (*l->p == ' ' || *l->p == '\t') l->p++;
      36          365 : }
      37              : 
      38          365 : static Tok lex_read(Lex *l) {
      39              :     Tok t;
      40          365 :     memset(&t, 0, sizeof(t));
      41          365 :     lex_skip_ws(l);
      42              : 
      43          365 :     if (!*l->p)               { t.type = T_EOF;    return t; }
      44          223 :     if (*l->p == '(')         { t.type = T_LPAREN; l->p++; return t; }
      45          220 :     if (*l->p == ')')         { t.type = T_RPAREN; l->p++; return t; }
      46          218 :     if (*l->p == '!')         { t.type = T_NOT;    l->p++; return t; }
      47              : 
      48              :     /* Read word up to ':', space, paren, '!', or EOF */
      49          199 :     const char *ws = l->p;
      50         1237 :     while (*l->p && *l->p != ':' && *l->p != '(' && *l->p != ')' &&
      51         1906 :            *l->p != '!' && *l->p != ' ' && *l->p != '\t')
      52          839 :         l->p++;
      53          199 :     size_t wlen = (size_t)(l->p - ws);
      54              : 
      55          199 :     if (wlen == 3 && strncasecmp(ws, "and", 3) == 0) { t.type = T_AND; return t; }
      56          185 :     if (wlen == 2 && strncasecmp(ws, "or",  2) == 0) { t.type = T_OR;  return t; }
      57              : 
      58          171 :     if (*l->p != ':') { t.type = T_ERR; return t; }
      59          170 :     l->p++; /* skip ':' */
      60              : 
      61          170 :     if (wlen >= sizeof(t.field)) wlen = sizeof(t.field) - 1;
      62          170 :     memcpy(t.field, ws, wlen);
      63          170 :     t.field[wlen] = '\0';
      64              : 
      65              :     /* Pattern: until whitespace, ')', '(', or EOF */
      66          170 :     const char *ps = l->p;
      67         1852 :     while (*l->p && *l->p != ' ' && *l->p != '\t' &&
      68         3480 :            *l->p != ')' && *l->p != '(')
      69         1654 :         l->p++;
      70          170 :     size_t plen = (size_t)(l->p - ps);
      71          170 :     if (plen >= sizeof(t.pat)) plen = sizeof(t.pat) - 1;
      72          170 :     memcpy(t.pat, ps, plen);
      73          170 :     t.pat[plen] = '\0';
      74              : 
      75          170 :     t.type = T_ATOM;
      76          170 :     return t;
      77              : }
      78              : 
      79          365 : static void lex_advance(Lex *l) { l->cur = lex_read(l); }
      80              : 
      81              : /* ── AST helpers ───────────────────────────────────────────────────── */
      82              : 
      83          213 : static WhenNode *node_new(WhenNodeType type) {
      84          213 :     WhenNode *n = calloc(1, sizeof(WhenNode));
      85          213 :     if (n) n->type = type;
      86          213 :     return n;
      87              : }
      88              : 
      89          570 : void when_node_free(WhenNode *n) {
      90          570 :     if (!n) return;
      91          213 :     when_node_free(n->left);
      92          213 :     when_node_free(n->right);
      93          213 :     free(n->pattern);
      94          213 :     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          146 : static WhenNode *parse_or(Lex *l) {
     105          146 :     WhenNode *left = parse_and(l);
     106          146 :     if (!left) return NULL;
     107          157 :     while (l->cur.type == T_OR) {
     108           14 :         lex_advance(l);
     109           14 :         WhenNode *right = parse_and(l);
     110           14 :         if (!right) { when_node_free(left); return NULL; }
     111           14 :         WhenNode *n = node_new(WN_OR);
     112           14 :         if (!n) { when_node_free(left); when_node_free(right); return NULL; }
     113           14 :         n->left = left; n->right = right;
     114           14 :         left = n;
     115              :     }
     116          143 :     return left;
     117              : }
     118              : 
     119          160 : static WhenNode *parse_and(Lex *l) {
     120          160 :     WhenNode *left = parse_not(l);
     121          160 :     if (!left) return NULL;
     122          171 :     while (l->cur.type == T_AND) {
     123           14 :         lex_advance(l);
     124           14 :         WhenNode *right = parse_not(l);
     125           14 :         if (!right) { when_node_free(left); return NULL; }
     126           14 :         WhenNode *n = node_new(WN_AND);
     127           14 :         if (!n) { when_node_free(left); when_node_free(right); return NULL; }
     128           14 :         n->left = left; n->right = right;
     129           14 :         left = n;
     130              :     }
     131          157 :     return left;
     132              : }
     133              : 
     134          193 : static WhenNode *parse_not(Lex *l) {
     135          193 :     if (l->cur.type == T_NOT) {
     136           19 :         lex_advance(l);
     137           19 :         WhenNode *child = parse_not(l);
     138           19 :         if (!child) return NULL;
     139           16 :         WhenNode *n = node_new(WN_NOT);
     140           16 :         if (!n) { when_node_free(child); return NULL; }
     141           16 :         n->left = child;
     142           16 :         return n;
     143              :     }
     144          174 :     return parse_primary(l);
     145              : }
     146              : 
     147          174 : static WhenNode *parse_primary(Lex *l) {
     148          174 :     if (l->cur.type == T_LPAREN) {
     149            3 :         lex_advance(l);
     150            3 :         WhenNode *inner = parse_or(l);
     151            3 :         if (!inner) return NULL;
     152            3 :         if (l->cur.type != T_RPAREN) { when_node_free(inner); return NULL; }
     153            2 :         lex_advance(l);
     154            2 :         return inner;
     155              :     }
     156          171 :     if (l->cur.type == T_ATOM) {
     157              :         WhenNodeType nt;
     158          170 :         if      (strcmp(l->cur.field, "from")    == 0) nt = WN_FROM;
     159           70 :         else if (strcmp(l->cur.field, "to")      == 0) nt = WN_TO;
     160           60 :         else if (strcmp(l->cur.field, "subject") == 0) nt = WN_SUBJECT;
     161           48 :         else if (strcmp(l->cur.field, "label")   == 0) nt = WN_LABEL;
     162           34 :         else if (strcmp(l->cur.field, "body")    == 0) nt = WN_BODY;
     163           23 :         else if (strcmp(l->cur.field, "age-gt")  == 0) nt = WN_AGE_GT;
     164           12 :         else if (strcmp(l->cur.field, "age-lt")  == 0) nt = WN_AGE_LT;
     165            1 :         else { l->error = 1; return NULL; }
     166              : 
     167          169 :         WhenNode *n = node_new(nt);
     168          169 :         if (!n) return NULL;
     169              : 
     170          169 :         if (nt == WN_AGE_GT || nt == WN_AGE_LT) {
     171           22 :             n->age_val = atoi(l->cur.pat);
     172              :         } else {
     173          147 :             n->pattern = strdup(l->cur.pat);
     174          147 :             if (!n->pattern) { free(n); return NULL; }
     175              :         }
     176          169 :         lex_advance(l);
     177          169 :         return n;
     178              :     }
     179            1 :     return NULL;
     180              : }
     181              : 
     182          146 : WhenNode *when_parse(const char *expr) {
     183          146 :     if (!expr || !*expr) return NULL;
     184              :     Lex l;
     185          144 :     memset(&l, 0, sizeof(l));
     186          144 :     l.p = expr;
     187          144 :     lex_advance(&l);
     188          144 :     if (l.cur.type == T_EOF) return NULL;
     189          143 :     WhenNode *tree = parse_or(&l);
     190          143 :     if (!tree || l.cur.type != T_EOF || l.error) {
     191            3 :         when_node_free(tree);
     192            3 :         return NULL;
     193              :     }
     194          140 :     return tree;
     195              : }
     196              : 
     197              : /* ── Evaluator ─────────────────────────────────────────────────────── */
     198              : 
     199          125 : static int when_glob(const char *pattern, const char *val) {
     200          125 :     if (!pattern || !pattern[0]) return 1;
     201          125 :     if (!val || !val[0]) return 0;
     202          116 :     return fnmatch(pattern, val, FNM_CASEFOLD) == 0;
     203              : }
     204              : 
     205           15 : static int when_label_match(const char *pattern, const char *csv) {
     206           15 :     if (!pattern || !pattern[0]) return 1;
     207           15 :     if (!csv || !csv[0]) return 0;
     208           13 :     char *copy = strdup(csv);
     209           13 :     if (!copy) return 0;
     210           13 :     int found = 0;
     211           13 :     char *tok = copy;
     212           27 :     while (tok && *tok) {
     213           16 :         char *sep = strchr(tok, ',');
     214           16 :         if (sep) *sep = '\0';
     215           16 :         if (fnmatch(pattern, tok, FNM_CASEFOLD) == 0) { found = 1; break; }
     216           14 :         tok = sep ? sep + 1 : NULL;
     217              :     }
     218           13 :     free(copy);
     219           13 :     return found;
     220              : }
     221              : 
     222          222 : 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          222 :     if (!n) return 1;
     228          221 :     switch (n->type) {
     229           18 :     case WN_OR:
     230           32 :         return when_eval(n->left,  from, subject, to, labels_csv, body, message_date) ||
     231           14 :                when_eval(n->right, from, subject, to, labels_csv, body, message_date);
     232           16 :     case WN_AND:
     233           30 :         return when_eval(n->left,  from, subject, to, labels_csv, body, message_date) &&
     234           14 :                when_eval(n->right, from, subject, to, labels_csv, body, message_date);
     235           14 :     case WN_NOT:
     236           14 :         return !when_eval(n->left, from, subject, to, labels_csv, body, message_date);
     237          103 :     case WN_FROM:    return when_glob(n->pattern, from);
     238            9 :     case WN_TO:      return when_glob(n->pattern, to);
     239           12 :     case WN_SUBJECT: return when_glob(n->pattern, subject);
     240           15 :     case WN_LABEL:   return when_label_match(n->pattern, labels_csv);
     241           11 :     case WN_BODY:
     242           11 :         if (!body) return 0;
     243            1 :         return when_glob(n->pattern, body);
     244           12 :     case WN_AGE_GT:
     245           12 :         if (message_date <= 0) return 0;
     246            2 :         return (int)((time(NULL) - message_date) / 86400) > n->age_val;
     247           11 :     case WN_AGE_LT:
     248           11 :         if (message_date <= 0) return 0;
     249            2 :         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         1191 : static void buf_append_atom(char *buf, size_t cap,
     258              :                             const char *field, const char *pattern,
     259              :                             int negated, int *first)
     260              : {
     261         1191 :     if (!pattern || !pattern[0]) return;
     262          120 :     size_t used = strlen(buf);
     263          120 :     if (!*first && used + 5 < cap)
     264            8 :         strncat(buf, " and ", cap - used - 1);
     265          120 :     used = strlen(buf);
     266          120 :     if (negated && used + 2 < cap) {
     267            4 :         strncat(buf, "!", cap - used - 1);
     268            4 :         used++;
     269              :     }
     270          120 :     strncat(buf, field, cap - used - 1);
     271          120 :     used = strlen(buf);
     272          120 :     strncat(buf, ":", cap - used - 1);
     273          120 :     used = strlen(buf);
     274          120 :     strncat(buf, pattern, cap - used - 1);
     275          120 :     *first = 0;
     276              : }
     277              : 
     278          148 : 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          148 :     char buf[8192] = {0};
     285          148 :     int first = 1;
     286              :     char tmp[32];
     287              : 
     288          148 :     buf_append_atom(buf, sizeof(buf), "from",    if_from,        0, &first);
     289          148 :     buf_append_atom(buf, sizeof(buf), "subject", if_subject,     0, &first);
     290          148 :     buf_append_atom(buf, sizeof(buf), "to",      if_to,          0, &first);
     291          148 :     buf_append_atom(buf, sizeof(buf), "label",   if_label,       0, &first);
     292          148 :     buf_append_atom(buf, sizeof(buf), "from",    if_not_from,    1, &first);
     293          148 :     buf_append_atom(buf, sizeof(buf), "subject", if_not_subject, 1, &first);
     294          148 :     buf_append_atom(buf, sizeof(buf), "to",      if_not_to,      1, &first);
     295          148 :     buf_append_atom(buf, sizeof(buf), "body",    if_body,        0, &first);
     296              : 
     297          148 :     if (if_age_gt > 0) {
     298            4 :         snprintf(tmp, sizeof(tmp), "%d", if_age_gt);
     299            4 :         buf_append_atom(buf, sizeof(buf), "age-gt", tmp, 0, &first);
     300              :     }
     301          148 :     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          148 :     return first ? NULL : strdup(buf);
     307              : }
     308              : 
     309           46 : char *when_from_conds(const WhenCond *conds, int nconds, int is_or) {
     310           46 :     if (nconds == 0) return NULL;
     311           45 :     const char *sep = is_or ? " or " : " and ";
     312           45 :     char buf[8192] = {0};
     313           45 :     size_t cap = sizeof(buf);
     314              : 
     315           97 :     for (int i = 0; i < nconds; i++) {
     316           52 :         const WhenCond *c = &conds[i];
     317           52 :         if (!c->field || !c->pattern) continue;
     318              : 
     319           52 :         size_t used = strlen(buf);
     320           52 :         if (i > 0 && used + strlen(sep) + 1 < cap) {
     321            7 :             strncat(buf, sep, cap - used - 1);
     322            7 :             used = strlen(buf);
     323              :         }
     324           52 :         if (c->negated && used + 2 < cap) {
     325            5 :             strncat(buf, "!", cap - used - 1);
     326            5 :             used = strlen(buf);
     327              :         }
     328           52 :         strncat(buf, c->field, cap - used - 1);
     329           52 :         used = strlen(buf);
     330           52 :         strncat(buf, ":", cap - used - 1);
     331           52 :         used = strlen(buf);
     332           52 :         strncat(buf, c->pattern, cap - used - 1);
     333              :     }
     334              : 
     335           45 :     return buf[0] ? strdup(buf) : NULL;
     336              : }
        

Generated by: LCOV version 2.0-1