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 : }
|