• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 17366932035

31 Aug 2025 12:06PM UTC coverage: 50.099% (+0.05%) from 50.049%
17366932035

push

github

web-flow
tweak observer event types (#4023)

9132 of 18228 relevant lines covered (50.1%)

272.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.63
/pattern/compile.c
1
/**
2
 * @file
3
 * Compile a Pattern
4
 *
5
 * @authors
6
 * Copyright (C) 2020 R Primus <rprimus@gmail.com>
7
 * Copyright (C) 2020-2022 Pietro Cerutti <gahr@gahr.ch>
8
 * Copyright (C) 2020-2023 Richard Russon <rich@flatcap.org>
9
 *
10
 * @copyright
11
 * This program is free software: you can redistribute it and/or modify it under
12
 * the terms of the GNU General Public License as published by the Free Software
13
 * Foundation, either version 2 of the License, or (at your option) any later
14
 * version.
15
 *
16
 * This program is distributed in the hope that it will be useful, but WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19
 * details.
20
 *
21
 * You should have received a copy of the GNU General Public License along with
22
 * this program.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24

25
/**
26
 * @page pattern_compile Compile a Pattern
27
 *
28
 * Compile a Pattern
29
 */
30

31
#include "config.h"
32
#include <stdbool.h>
33
#include <stdint.h>
34
#include <stdio.h>
35
#include <stdlib.h>
36
#include <string.h>
37
#include <sys/types.h>
38
#include <time.h>
39
#include "private.h"
40
#include "mutt/lib.h"
41
#include "address/lib.h"
42
#include "config/lib.h"
43
#include "core/lib.h"
44
#include "lib.h"
45
#include "parse/lib.h"
46
#include "mview.h"
47

48
struct Menu;
49

50
// clang-format off
51
typedef uint16_t ParseDateRangeFlags; ///< Flags for parse_date_range(), e.g. #MUTT_PDR_MINUS
52
#define MUTT_PDR_NO_FLAGS       0  ///< No flags are set
53
#define MUTT_PDR_MINUS    (1 << 0) ///< Pattern contains a range
54
#define MUTT_PDR_PLUS     (1 << 1) ///< Extend the range using '+'
55
#define MUTT_PDR_WINDOW   (1 << 2) ///< Extend the range in both directions using '*'
56
#define MUTT_PDR_ABSOLUTE (1 << 3) ///< Absolute pattern range
57
#define MUTT_PDR_DONE     (1 << 4) ///< Pattern parse successfully
58
#define MUTT_PDR_ERROR    (1 << 8) ///< Invalid pattern
59
// clang-format on
60

61
#define MUTT_PDR_ERRORDONE (MUTT_PDR_ERROR | MUTT_PDR_DONE)
62

63
/**
64
 * eat_regex - Parse a regex - Implements ::eat_arg_t - @ingroup eat_arg_api
65
 */
66
static bool eat_regex(struct Pattern *pat, PatternCompFlags flags,
26✔
67
                      struct Buffer *s, struct Buffer *err)
68
{
69
  struct Buffer *buf = buf_pool_get();
26✔
70
  bool rc = false;
71
  char *pexpr = s->dptr;
26✔
72
  if ((parse_extract_token(buf, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) || !buf->data)
26✔
73
  {
74
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
75
    goto out;
×
76
  }
77
  if (buf_is_empty(buf))
26✔
78
  {
79
    buf_addstr(err, _("Empty expression"));
×
80
    goto out;
×
81
  }
82

83
  if (pat->string_match)
26✔
84
  {
85
    pat->p.str = mutt_str_dup(buf->data);
14✔
86
    pat->ign_case = mutt_mb_is_lower(buf->data);
14✔
87
  }
88
  else if (pat->group_match)
12✔
89
  {
90
    pat->p.group = mutt_pattern_group(buf->data);
×
91
  }
92
  else
93
  {
94
    pat->p.regex = MUTT_MEM_CALLOC(1, regex_t);
12✔
95
#ifdef USE_DEBUG_GRAPHVIZ
96
    pat->raw_pattern = mutt_str_dup(buf->data);
97
#endif
98
    uint16_t case_flags = mutt_mb_is_lower(buf->data) ? REG_ICASE : 0;
12✔
99
    int rc2 = REG_COMP(pat->p.regex, buf->data, REG_NEWLINE | REG_NOSUB | case_flags);
12✔
100
    if (rc2 != 0)
12✔
101
    {
102
      char errmsg[256] = { 0 };
×
103
      regerror(rc2, pat->p.regex, errmsg, sizeof(errmsg));
×
104
      buf_printf(err, "'%s': %s", buf->data, errmsg);
×
105
      FREE(&pat->p.regex);
×
106
      goto out;
×
107
    }
108
  }
109

110
  rc = true;
111

112
out:
26✔
113
  buf_pool_release(&buf);
26✔
114
  return rc;
26✔
115
}
116

117
/**
118
 * add_query_msgid - Parse a Message-Id and add it to a list - Implements ::mutt_file_map_t - @ingroup mutt_file_map_api
119
 * @retval true Always
120
 */
121
static bool add_query_msgid(char *line, int line_num, void *user_data)
×
122
{
123
  struct ListHead *msgid_list = (struct ListHead *) (user_data);
124
  char *nows = mutt_str_skip_whitespace(line);
×
125
  if (*nows == '\0')
×
126
    return true;
127
  mutt_str_remove_trailing_ws(nows);
×
128
  mutt_list_insert_tail(msgid_list, mutt_str_dup(nows));
×
129
  return true;
×
130
}
131

132
/**
133
 * eat_query - Parse a query for an external search program - Implements ::eat_arg_t - @ingroup eat_arg_api
134
 * @param pat   Pattern to store the results in
135
 * @param flags Flags, e.g. #MUTT_PC_PATTERN_DYNAMIC
136
 * @param s     String to parse
137
 * @param err   Buffer for error messages
138
 * @param m     Mailbox
139
 * @retval true The pattern was read successfully
140
 */
141
static bool eat_query(struct Pattern *pat, PatternCompFlags flags,
1✔
142
                      struct Buffer *s, struct Buffer *err, struct Mailbox *m)
143
{
144
  struct Buffer *cmd_buf = buf_pool_get();
1✔
145
  struct Buffer *tok_buf = buf_pool_get();
1✔
146
  bool rc = false;
147

148
  FILE *fp = NULL;
1✔
149

150
  const char *const c_external_search_command = cs_subset_string(NeoMutt->sub, "external_search_command");
1✔
151
  if (!c_external_search_command)
1✔
152
  {
153
    buf_addstr(err, _("No search command defined"));
×
154
    goto out;
×
155
  }
156

157
  char *pexpr = s->dptr;
1✔
158
  if ((parse_extract_token(tok_buf, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) ||
1✔
159
      !tok_buf->data)
1✔
160
  {
161
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
162
    goto out;
×
163
  }
164
  if (*tok_buf->data == '\0')
1✔
165
  {
166
    buf_addstr(err, _("Empty expression"));
×
167
    goto out;
×
168
  }
169

170
  buf_addstr(cmd_buf, c_external_search_command);
1✔
171
  buf_addch(cmd_buf, ' ');
1✔
172

173
  if (m)
1✔
174
  {
175
    char *escaped_folder = mutt_path_escape(mailbox_path(m));
×
176
    mutt_debug(LL_DEBUG2, "escaped folder path: %s\n", escaped_folder);
×
177
    buf_addch(cmd_buf, '\'');
×
178
    buf_addstr(cmd_buf, escaped_folder);
×
179
    buf_addch(cmd_buf, '\'');
×
180
  }
181
  else
182
  {
183
    buf_addch(cmd_buf, '/');
1✔
184
  }
185
  buf_addch(cmd_buf, ' ');
1✔
186
  buf_addstr(cmd_buf, tok_buf->data);
1✔
187

188
  mutt_message(_("Running search command: %s ..."), cmd_buf->data);
1✔
189
  pat->is_multi = true;
1✔
190
  mutt_list_clear(&pat->p.multi_cases);
1✔
191
  pid_t pid = filter_create(cmd_buf->data, NULL, &fp, NULL, NeoMutt->env);
1✔
192
  if (pid < 0)
1✔
193
  {
194
    buf_printf(err, "unable to fork command: %s\n", cmd_buf->data);
×
195
    goto out;
×
196
  }
197

198
  mutt_file_map_lines(add_query_msgid, &pat->p.multi_cases, fp, MUTT_RL_NO_FLAGS);
1✔
199
  mutt_file_fclose(&fp);
1✔
200
  filter_wait(pid);
1✔
201

202
  rc = true;
203

204
out:
1✔
205
  buf_pool_release(&cmd_buf);
1✔
206
  buf_pool_release(&tok_buf);
1✔
207
  return rc;
1✔
208
}
209

210
/**
211
 * get_offset - Calculate a symbolic offset
212
 * @param tm   Store the time here
213
 * @param s    string to parse
214
 * @param sign Sign of range, 1 for positive, -1 for negative
215
 * @retval ptr Next char after parsed offset
216
 *
217
 * - Ny years
218
 * - Nm months
219
 * - Nw weeks
220
 * - Nd days
221
 */
222
static const char *get_offset(struct tm *tm, const char *s, int sign)
13✔
223
{
224
  char *ps = NULL;
13✔
225
  int offset = strtol(s, &ps, 0);
13✔
226
  if (((sign < 0) && (offset > 0)) || ((sign > 0) && (offset < 0)))
13✔
227
    offset = -offset;
11✔
228

229
  switch (*ps)
13✔
230
  {
231
    case 'y':
1✔
232
      tm->tm_year += offset;
1✔
233
      break;
1✔
234
    case 'm':
1✔
235
      tm->tm_mon += offset;
1✔
236
      break;
1✔
237
    case 'w':
1✔
238
      tm->tm_mday += 7 * offset;
1✔
239
      break;
1✔
240
    case 'd':
5✔
241
      tm->tm_mday += offset;
5✔
242
      break;
5✔
243
    case 'H':
1✔
244
      tm->tm_hour += offset;
1✔
245
      break;
1✔
246
    case 'M':
1✔
247
      tm->tm_min += offset;
1✔
248
      break;
1✔
249
    case 'S':
1✔
250
      tm->tm_sec += offset;
1✔
251
      break;
1✔
252
    default:
253
      return s;
254
  }
255
  mutt_date_normalize_time(tm);
11✔
256
  return ps + 1;
11✔
257
}
258

259
/**
260
 * get_date - Parse a (partial) date in dd/mm/yyyy format
261
 * @param s   String to parse
262
 * @param t   Store the time here
263
 * @param err Buffer for error messages
264
 * @retval ptr First character after the date
265
 *
266
 * This function parses a (partial) date separated by '/'.  The month and year
267
 * are optional and if the year is less than 70 it's assumed to be after 2000.
268
 *
269
 * Examples:
270
 * - "10"         = 10 of this month, this year
271
 * - "10/12"      = 10 of December,   this year
272
 * - "10/12/04"   = 10 of December,   2004
273
 * - "10/12/2008" = 10 of December,   2008
274
 * - "20081210"   = 10 of December,   2008
275
 */
276
static const char *get_date(const char *s, struct tm *t, struct Buffer *err)
11✔
277
{
278
  char *p = NULL;
11✔
279
  struct tm tm = mutt_date_localtime(mutt_date_now());
11✔
280
  bool iso8601 = true;
281

282
  for (int v = 0; v < 8; v++)
51✔
283
  {
284
    if (s[v] && (s[v] >= '0') && (s[v] <= '9'))
48✔
285
      continue;
286

287
    iso8601 = false;
288
    break;
289
  }
290

291
  if (iso8601)
11✔
292
  {
293
    int year = 0;
3✔
294
    int month = 0;
3✔
295
    int mday = 0;
3✔
296
    sscanf(s, "%4d%2d%2d", &year, &month, &mday);
3✔
297

298
    t->tm_year = year;
3✔
299
    if (t->tm_year > 1900)
3✔
300
      t->tm_year -= 1900;
3✔
301
    t->tm_mon = month - 1;
3✔
302
    t->tm_mday = mday;
3✔
303

304
    if ((t->tm_mday < 1) || (t->tm_mday > 31))
3✔
305
    {
306
      buf_printf(err, _("Invalid day of month: %s"), s);
1✔
307
      return NULL;
1✔
308
    }
309
    if ((t->tm_mon < 0) || (t->tm_mon > 11))
2✔
310
    {
311
      buf_printf(err, _("Invalid month: %s"), s);
1✔
312
      return NULL;
1✔
313
    }
314

315
    return (s + 8);
1✔
316
  }
317

318
  t->tm_mday = strtol(s, &p, 10);
8✔
319
  if ((t->tm_mday < 1) || (t->tm_mday > 31))
8✔
320
  {
321
    buf_printf(err, _("Invalid day of month: %s"), s);
1✔
322
    return NULL;
1✔
323
  }
324
  if (*p != '/')
7✔
325
  {
326
    /* fill in today's month and year */
327
    t->tm_mon = tm.tm_mon;
×
328
    t->tm_year = tm.tm_year;
×
329
    return p;
×
330
  }
331
  p++;
7✔
332
  t->tm_mon = strtol(p, &p, 10) - 1;
7✔
333
  if ((t->tm_mon < 0) || (t->tm_mon > 11))
7✔
334
  {
335
    buf_printf(err, _("Invalid month: %s"), p);
1✔
336
    return NULL;
1✔
337
  }
338
  if (*p != '/')
6✔
339
  {
340
    t->tm_year = tm.tm_year;
×
341
    return p;
×
342
  }
343
  p++;
6✔
344
  t->tm_year = strtol(p, &p, 10);
6✔
345
  if (t->tm_year < 70) /* year 2000+ */
6✔
346
    t->tm_year += 100;
×
347
  else if (t->tm_year > 1900)
6✔
348
    t->tm_year -= 1900;
6✔
349
  return p;
6✔
350
}
351

352
/**
353
 * parse_date_range - Parse a date range
354
 * @param pc       String to parse
355
 * @param min      Earlier date
356
 * @param max      Later date
357
 * @param have_min Do we have a base minimum date?
358
 * @param base_min Base minimum date
359
 * @param err      Buffer for error messages
360
 * @retval ptr First character after the date
361
 */
362
static const char *parse_date_range(const char *pc, struct tm *min, struct tm *max,
5✔
363
                                    bool have_min, struct tm *base_min, struct Buffer *err)
364
{
365
  ParseDateRangeFlags flags = MUTT_PDR_NO_FLAGS;
366
  while (*pc && ((flags & MUTT_PDR_DONE) == 0))
9✔
367
  {
368
    const char *pt = NULL;
369
    char ch = *pc++;
4✔
370
    SKIPWS(pc);
4✔
371
    switch (ch)
4✔
372
    {
373
      case '-':
2✔
374
      {
375
        /* try a range of absolute date minus offset of Ndwmy */
376
        pt = get_offset(min, pc, -1);
2✔
377
        if (pc == pt)
2✔
378
        {
379
          if (flags == MUTT_PDR_NO_FLAGS)
2✔
380
          { /* nothing yet and no offset parsed => absolute date? */
381
            if (!get_date(pc, max, err))
2✔
382
            {
383
              flags |= (MUTT_PDR_ABSOLUTE | MUTT_PDR_ERRORDONE); /* done bad */
384
            }
385
            else
386
            {
387
              /* reestablish initial base minimum if not specified */
388
              if (!have_min)
2✔
389
                memcpy(min, base_min, sizeof(struct tm));
390
              flags |= (MUTT_PDR_ABSOLUTE | MUTT_PDR_DONE); /* done good */
391
            }
392
          }
393
          else
394
          {
395
            flags |= MUTT_PDR_ERRORDONE;
×
396
          }
397
        }
398
        else
399
        {
400
          pc = pt;
401
          if ((flags == MUTT_PDR_NO_FLAGS) && !have_min)
×
402
          { /* the very first "-3d" without a previous absolute date */
403
            max->tm_year = min->tm_year;
×
404
            max->tm_mon = min->tm_mon;
×
405
            max->tm_mday = min->tm_mday;
×
406
          }
407
          flags |= MUTT_PDR_MINUS;
×
408
        }
409
        break;
410
      }
411
      case '+':
1✔
412
      { /* enlarge plus range */
413
        pt = get_offset(max, pc, 1);
1✔
414
        if (pc == pt)
1✔
415
        {
416
          flags |= MUTT_PDR_ERRORDONE;
×
417
        }
418
        else
419
        {
420
          pc = pt;
421
          flags |= MUTT_PDR_PLUS;
1✔
422
        }
423
        break;
424
      }
425
      case '*':
1✔
426
      { /* enlarge window in both directions */
427
        pt = get_offset(min, pc, -1);
1✔
428
        if (pc == pt)
1✔
429
        {
430
          flags |= MUTT_PDR_ERRORDONE;
×
431
        }
432
        else
433
        {
434
          pc = get_offset(max, pc, 1);
1✔
435
          flags |= MUTT_PDR_WINDOW;
1✔
436
        }
437
        break;
438
      }
439
      default:
×
440
        flags |= MUTT_PDR_ERRORDONE;
×
441
    }
442
    SKIPWS(pc);
4✔
443
  }
444
  if ((flags & MUTT_PDR_ERROR) && !(flags & MUTT_PDR_ABSOLUTE))
5✔
445
  { /* get_date has its own error message, don't overwrite it here */
446
    buf_printf(err, _("Invalid relative date: %s"), pc - 1);
×
447
  }
448
  return (flags & MUTT_PDR_ERROR) ? NULL : pc;
5✔
449
}
450

451
/**
452
 * adjust_date_range - Put a date range in the correct order
453
 * @param[in,out] min Earlier date
454
 * @param[in,out] max Later date
455
 */
456
static void adjust_date_range(struct tm *min, struct tm *max)
13✔
457
{
458
  if ((min->tm_year > max->tm_year) ||
13✔
459
      ((min->tm_year == max->tm_year) && (min->tm_mon > max->tm_mon)) ||
2✔
460
      ((min->tm_year == max->tm_year) && (min->tm_mon == max->tm_mon) &&
12✔
461
       (min->tm_mday > max->tm_mday)))
2✔
462
  {
463
    int tmp;
464

465
    tmp = min->tm_year;
466
    min->tm_year = max->tm_year;
1✔
467
    max->tm_year = tmp;
1✔
468

469
    tmp = min->tm_mon;
1✔
470
    min->tm_mon = max->tm_mon;
1✔
471
    max->tm_mon = tmp;
1✔
472

473
    tmp = min->tm_mday;
1✔
474
    min->tm_mday = max->tm_mday;
1✔
475
    max->tm_mday = tmp;
1✔
476

477
    min->tm_hour = 0;
1✔
478
    min->tm_min = 0;
1✔
479
    min->tm_sec = 0;
1✔
480
    max->tm_hour = 23;
1✔
481
    max->tm_min = 59;
1✔
482
    max->tm_sec = 59;
1✔
483
  }
484
}
13✔
485

486
/**
487
 * eval_date_minmax - Evaluate a date-range pattern against 'now'
488
 * @param pat Pattern to modify
489
 * @param s   Pattern string to use
490
 * @param err Buffer for error messages
491
 * @retval true  Pattern valid and updated
492
 * @retval false Pattern invalid
493
 */
494
bool eval_date_minmax(struct Pattern *pat, const char *s, struct Buffer *err)
17✔
495
{
496
  /* the '0' time is Jan 1, 1970 UTC, so in order to prevent a negative time
497
   * when doing timezone conversion, we use Jan 2, 1970 UTC as the base here */
498
  struct tm min = { 0 };
17✔
499
  min.tm_mday = 2;
17✔
500
  min.tm_year = 70;
17✔
501

502
  /* Arbitrary year in the future.  Don't set this too high or
503
   * mutt_date_make_time() returns something larger than will fit in a time_t
504
   * on some systems */
505
  struct tm max = { 0 };
17✔
506
  max.tm_year = 130;
17✔
507
  max.tm_mon = 11;
17✔
508
  max.tm_mday = 31;
17✔
509
  max.tm_hour = 23;
17✔
510
  max.tm_min = 59;
17✔
511
  max.tm_sec = 59;
17✔
512

513
  if (strchr("<>=", s[0]))
17✔
514
  {
515
    /* offset from current time
516
     *  <3d  less than three days ago
517
     *  >3d  more than three days ago
518
     *  =3d  exactly three days ago */
519
    struct tm *tm = NULL;
520
    bool exact = false;
521

522
    if (s[0] == '<')
8✔
523
    {
524
      min = mutt_date_localtime(mutt_date_now());
8✔
525
      tm = &min;
526
    }
527
    else
528
    {
529
      max = mutt_date_localtime(mutt_date_now());
×
530
      tm = &max;
531

532
      if (s[0] == '=')
×
533
        exact = true;
534
    }
535

536
    /* Reset the HMS unless we are relative matching using one of those
537
     * offsets. */
538
    char *offset_type = NULL;
8✔
539
    strtol(s + 1, &offset_type, 0);
8✔
540
    if (!(*offset_type && strchr("HMS", *offset_type)))
8✔
541
    {
542
      tm->tm_hour = 23;
5✔
543
      tm->tm_min = 59;
5✔
544
      tm->tm_sec = 59;
5✔
545
    }
546

547
    /* force negative offset */
548
    get_offset(tm, s + 1, -1);
8✔
549

550
    if (exact)
8✔
551
    {
552
      /* start at the beginning of the day in question */
553
      memcpy(&min, &max, sizeof(max));
554
      min.tm_hour = 0;
×
555
      min.tm_sec = 0;
×
556
      min.tm_min = 0;
×
557
    }
558
  }
559
  else
560
  {
561
    const char *pc = s;
562

563
    bool have_min = false;
564
    bool until_now = false;
565
    if (mutt_isdigit(*pc))
9✔
566
    {
567
      /* minimum date specified */
568
      pc = get_date(pc, &min, err);
9✔
569
      if (!pc)
9✔
570
      {
571
        return false;
572
      }
573
      have_min = true;
574
      SKIPWS(pc);
5✔
575
      if (*pc == '-')
5✔
576
      {
577
        const char *pt = pc + 1;
2✔
578
        SKIPWS(pt);
2✔
579
        until_now = (*pt == '\0');
2✔
580
      }
581
    }
582

583
    if (!until_now)
5✔
584
    { /* max date or relative range/window */
585

586
      struct tm base_min = { 0 };
5✔
587

588
      if (!have_min)
5✔
589
      { /* save base minimum and set current date, e.g. for "-3d+1d" */
590
        memcpy(&base_min, &min, sizeof(base_min));
591
        min = mutt_date_localtime(mutt_date_now());
×
592
        min.tm_hour = 0;
×
593
        min.tm_sec = 0;
×
594
        min.tm_min = 0;
×
595
      }
596

597
      /* preset max date for relative offsets,
598
       * if nothing follows we search for messages on a specific day */
599
      max.tm_year = min.tm_year;
5✔
600
      max.tm_mon = min.tm_mon;
5✔
601
      max.tm_mday = min.tm_mday;
5✔
602

603
      if (!parse_date_range(pc, &min, &max, have_min, &base_min, err))
5✔
604
      { /* bail out on any parsing error */
605
        return false;
×
606
      }
607
    }
608
  }
609

610
  /* Since we allow two dates to be specified we'll have to adjust that. */
611
  adjust_date_range(&min, &max);
13✔
612

613
  pat->min = mutt_date_make_time(&min, true);
13✔
614
  pat->max = mutt_date_make_time(&max, true);
13✔
615

616
  return true;
13✔
617
}
618

619
/**
620
 * eat_range - Parse a number range - Implements ::eat_arg_t - @ingroup eat_arg_api
621
 */
622
static bool eat_range(struct Pattern *pat, PatternCompFlags flags,
3✔
623
                      struct Buffer *s, struct Buffer *err)
624
{
625
  char *tmp = NULL;
3✔
626
  bool do_exclusive = false;
627
  bool skip_quote = false;
628

629
  /* If simple_search is set to "~m %s", the range will have double quotes
630
   * around it...  */
631
  if (*s->dptr == '"')
3✔
632
  {
633
    s->dptr++;
×
634
    skip_quote = true;
635
  }
636
  if (*s->dptr == '<')
3✔
637
    do_exclusive = true;
638
  if ((*s->dptr != '-') && (*s->dptr != '<'))
3✔
639
  {
640
    /* range minimum */
641
    if (*s->dptr == '>')
2✔
642
    {
643
      pat->max = MUTT_MAXRANGE;
1✔
644
      pat->min = strtol(s->dptr + 1, &tmp, 0) + 1; /* exclusive range */
1✔
645
    }
646
    else
647
    {
648
      pat->min = strtol(s->dptr, &tmp, 0);
1✔
649
    }
650
    if (mutt_toupper(*tmp) == 'K') /* is there a prefix? */
2✔
651
    {
652
      pat->min *= 1024;
×
653
      tmp++;
×
654
    }
655
    else if (mutt_toupper(*tmp) == 'M')
2✔
656
    {
657
      pat->min *= 1048576;
×
658
      tmp++;
×
659
    }
660
    if (*s->dptr == '>')
2✔
661
    {
662
      s->dptr = tmp;
1✔
663
      return true;
1✔
664
    }
665
    if (*tmp != '-')
1✔
666
    {
667
      /* exact value */
668
      pat->max = pat->min;
×
669
      s->dptr = tmp;
×
670
      return true;
×
671
    }
672
    tmp++;
1✔
673
  }
674
  else
675
  {
676
    s->dptr++;
1✔
677
    tmp = s->dptr;
1✔
678
  }
679

680
  if (mutt_isdigit(*tmp))
2✔
681
  {
682
    /* range maximum */
683
    pat->max = strtol(tmp, &tmp, 0);
2✔
684
    if (mutt_toupper(*tmp) == 'K')
2✔
685
    {
686
      pat->max *= 1024;
1✔
687
      tmp++;
1✔
688
    }
689
    else if (mutt_toupper(*tmp) == 'M')
1✔
690
    {
691
      pat->max *= 1048576;
×
692
      tmp++;
×
693
    }
694
    if (do_exclusive)
2✔
695
      (pat->max)--;
1✔
696
  }
697
  else
698
  {
699
    pat->max = MUTT_MAXRANGE;
×
700
  }
701

702
  if (skip_quote && (*tmp == '"'))
2✔
703
    tmp++;
×
704

705
  SKIPWS(tmp);
2✔
706
  s->dptr = tmp;
2✔
707
  return true;
2✔
708
}
709

710
/**
711
 * eat_date - Parse a date pattern - Implements ::eat_arg_t - @ingroup eat_arg_api
712
 */
713
static bool eat_date(struct Pattern *pat, PatternCompFlags flags,
17✔
714
                     struct Buffer *s, struct Buffer *err)
715
{
716
  struct Buffer *tmp = buf_pool_get();
17✔
717
  bool rc = false;
718

719
  char *pexpr = s->dptr;
17✔
720
  if (parse_extract_token(tmp, s, TOKEN_COMMENT | TOKEN_PATTERN) != 0)
17✔
721
  {
722
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
723
    goto out;
×
724
  }
725

726
  if (buf_is_empty(tmp))
17✔
727
  {
728
    buf_addstr(err, _("Empty expression"));
×
729
    goto out;
×
730
  }
731

732
  if (flags & MUTT_PC_PATTERN_DYNAMIC)
17✔
733
  {
734
    pat->dynamic = true;
×
735
    pat->p.str = mutt_str_dup(tmp->data);
×
736
  }
737

738
  rc = eval_date_minmax(pat, tmp->data, err);
17✔
739

740
out:
17✔
741
  buf_pool_release(&tmp);
17✔
742
  return rc;
17✔
743
}
744

745
/**
746
 * find_matching_paren - Find the matching parenthesis
747
 * @param s string to search
748
 * @retval ptr
749
 * - Matching close parenthesis
750
 * - End of string NUL, if not found
751
 */
752
static /* const */ char *find_matching_paren(/* const */ char *s)
7✔
753
{
754
  int level = 1;
755

756
  for (; *s; s++)
57✔
757
  {
758
    if (*s == '(')
57✔
759
    {
760
      level++;
×
761
    }
762
    else if (*s == ')')
57✔
763
    {
764
      level--;
7✔
765
      if (level == 0)
7✔
766
        break;
767
    }
768
  }
769
  return s;
7✔
770
}
771

772
/**
773
 * mutt_pattern_free - Free a Pattern
774
 * @param[out] pat Pattern to free
775
 */
776
void mutt_pattern_free(struct PatternList **pat)
185✔
777
{
778
  if (!pat || !*pat)
185✔
779
    return;
99✔
780

781
  struct Pattern *np = SLIST_FIRST(*pat);
86✔
782
  struct Pattern *next = NULL;
783

784
  while (np)
185✔
785
  {
786
    next = SLIST_NEXT(np, entries);
99✔
787

788
    if (np->is_multi)
99✔
789
    {
790
      mutt_list_free(&np->p.multi_cases);
1✔
791
    }
792
    else if (np->string_match || np->dynamic)
98✔
793
    {
794
      FREE(&np->p.str);
15✔
795
    }
796
    else if (np->group_match)
83✔
797
    {
798
      np->p.group = NULL;
×
799
    }
800
    else if (np->p.regex)
83✔
801
    {
802
      regfree(np->p.regex);
12✔
803
      FREE(&np->p.regex);
12✔
804
    }
805

806
#ifdef USE_DEBUG_GRAPHVIZ
807
    FREE(&np->raw_pattern);
808
#endif
809
    mutt_pattern_free(&np->child);
99✔
810
    FREE(&np);
99✔
811

812
    np = next;
99✔
813
  }
814

815
  FREE(pat);
86✔
816
}
817

818
/**
819
 * mutt_pattern_new - Create a new Pattern
820
 * @retval ptr Newly created Pattern
821
 */
822
static struct Pattern *mutt_pattern_new(void)
823
{
824
  return MUTT_MEM_CALLOC(1, struct Pattern);
12✔
825
}
826

827
/**
828
 * mutt_pattern_list_new - Create a new list containing a Pattern
829
 * @retval ptr Newly created list containing a single node with a Pattern
830
 */
831
static struct PatternList *mutt_pattern_list_new(void)
87✔
832
{
833
  struct PatternList *h = MUTT_MEM_CALLOC(1, struct PatternList);
87✔
834
  SLIST_INIT(h);
87✔
835
  struct Pattern *p = mutt_pattern_new();
836
  SLIST_INSERT_HEAD(h, p, entries);
87✔
837
  return h;
87✔
838
}
839

840
/**
841
 * attach_leaf - Attach a Pattern to a Pattern List
842
 * @param list Pattern List to attach to
843
 * @param leaf Pattern to attach
844
 * @retval ptr Attached leaf
845
 */
846
static struct Pattern *attach_leaf(struct PatternList *list, struct Pattern *leaf)
847
{
848
  struct Pattern *last = NULL;
849
  SLIST_FOREACH(last, list, entries)
14✔
850
  {
851
    // TODO - or we could use a doubly-linked list
852
    if (!SLIST_NEXT(last, entries))
14✔
853
    {
854
      SLIST_NEXT(last, entries) = leaf;
13✔
855
      break;
13✔
856
    }
857
  }
858
  return leaf;
859
}
860

861
/**
862
 * attach_new_root - Create a new Pattern as a parent for a List
863
 * @param curlist Pattern List
864
 * @retval ptr First Pattern in the original List
865
 *
866
 * @note curlist will be altered to the new root Pattern
867
 */
868
static struct Pattern *attach_new_root(struct PatternList **curlist)
869
{
870
  struct PatternList *root = mutt_pattern_list_new();
87✔
871
  struct Pattern *leaf = SLIST_FIRST(root);
87✔
872
  leaf->child = *curlist;
87✔
873
  *curlist = root;
87✔
874
  return leaf;
875
}
876

877
/**
878
 * attach_new_leaf - Attach a new Pattern to a List
879
 * @param curlist Pattern List
880
 * @retval ptr New Pattern in the original List
881
 *
882
 * @note curlist may be altered
883
 */
884
static struct Pattern *attach_new_leaf(struct PatternList **curlist)
87✔
885
{
886
  if (*curlist)
87✔
887
  {
888
    return attach_leaf(*curlist, mutt_pattern_new());
24✔
889
  }
890
  else
891
  {
892
    return attach_new_root(curlist);
75✔
893
  }
894
}
895

896
/**
897
 * mutt_pattern_comp - Create a Pattern
898
 * @param mv    Mailbox view
899
 * @param menu  Current Menu
900
 * @param s     Pattern string
901
 * @param flags Flags, e.g. #MUTT_PC_FULL_MSG
902
 * @param err   Buffer for error messages
903
 * @retval ptr Newly allocated Pattern
904
 */
905
struct PatternList *mutt_pattern_comp(struct MailboxView *mv, struct Menu *menu,
85✔
906
                                      const char *s, PatternCompFlags flags,
907
                                      struct Buffer *err)
908
{
909
  /* curlist when assigned will always point to a list containing at least one node
910
   * with a Pattern value.  */
911
  struct PatternList *curlist = NULL;
85✔
912
  bool pat_not = false;
913
  bool all_addr = false;
914
  bool pat_or = false;
915
  bool implicit = true; /* used to detect logical AND operator */
916
  bool is_alias = false;
917
  const struct PatternFlags *entry = NULL;
918
  char *p = NULL;
919
  char *buf = NULL;
85✔
920
  struct Mailbox *m = mv ? mv->mailbox : NULL;
85✔
921

922
  if (!s || (s[0] == '\0'))
85✔
923
  {
924
    buf_strcpy(err, _("empty pattern"));
1✔
925
    return NULL;
1✔
926
  }
927

928
  struct Buffer *ps = buf_pool_get();
84✔
929
  buf_strcpy(ps, s);
84✔
930
  buf_seek(ps, 0);
84✔
931

932
  SKIPWS(ps->dptr);
84✔
933
  while (*ps->dptr)
175✔
934
  {
935
    switch (*ps->dptr)
104✔
936
    {
937
      case '^':
×
938
        ps->dptr++;
×
939
        all_addr = !all_addr;
×
940
        break;
×
941
      case '!':
3✔
942
        ps->dptr++;
3✔
943
        pat_not = !pat_not;
3✔
944
        break;
3✔
945
      case '@':
×
946
        ps->dptr++;
×
947
        is_alias = !is_alias;
×
948
        break;
×
949
      case '|':
5✔
950
        if (!pat_or)
5✔
951
        {
952
          if (!curlist)
5✔
953
          {
954
            buf_printf(err, _("error in pattern at: %s"), ps->dptr);
1✔
955
            buf_pool_release(&ps);
1✔
956
            return NULL;
1✔
957
          }
958

959
          struct Pattern *pat = SLIST_FIRST(curlist);
4✔
960
          if (SLIST_NEXT(pat, entries))
4✔
961
          {
962
            /* A & B | C == (A & B) | C */
963
            struct Pattern *root = attach_new_root(&curlist);
964
            root->op = MUTT_PAT_AND;
1✔
965
          }
966

967
          pat_or = true;
968
        }
969
        ps->dptr++;
4✔
970
        implicit = false;
971
        pat_not = false;
972
        all_addr = false;
973
        is_alias = false;
974
        break;
4✔
975
      case '%':
91✔
976
      case '=':
977
      case '~':
978
      {
979
        if (ps->dptr[1] == '\0')
91✔
980
        {
981
          buf_printf(err, _("missing pattern: %s"), ps->dptr);
×
982
          goto cleanup;
×
983
        }
984
        short thread_op = 0;
985
        if (ps->dptr[1] == '(')
91✔
986
          thread_op = MUTT_PAT_THREAD;
987
        else if ((ps->dptr[1] == '<') && (ps->dptr[2] == '('))
90✔
988
          thread_op = MUTT_PAT_PARENT;
989
        else if ((ps->dptr[1] == '>') && (ps->dptr[2] == '('))
89✔
990
          thread_op = MUTT_PAT_CHILDREN;
991
        if (thread_op != 0)
992
        {
993
          ps->dptr++; /* skip ~ */
3✔
994
          if ((thread_op == MUTT_PAT_PARENT) || (thread_op == MUTT_PAT_CHILDREN))
3✔
995
            ps->dptr++;
2✔
996
          p = find_matching_paren(ps->dptr + 1);
3✔
997
          if (p[0] != ')')
3✔
998
          {
999
            buf_printf(err, _("mismatched parentheses: %s"), ps->dptr);
×
1000
            goto cleanup;
×
1001
          }
1002
          struct Pattern *leaf = attach_new_leaf(&curlist);
3✔
1003
          leaf->op = thread_op;
3✔
1004
          leaf->pat_not = pat_not;
3✔
1005
          leaf->all_addr = all_addr;
3✔
1006
          leaf->is_alias = is_alias;
3✔
1007
          pat_not = false;
1008
          all_addr = false;
1009
          is_alias = false;
1010
          /* compile the sub-expression */
1011
          buf = mutt_strn_dup(ps->dptr + 1, p - (ps->dptr + 1));
3✔
1012
          leaf->child = mutt_pattern_comp(mv, menu, buf, flags, err);
3✔
1013
          if (!leaf->child)
3✔
1014
          {
1015
            FREE(&buf);
×
1016
            goto cleanup;
×
1017
          }
1018
          FREE(&buf);
3✔
1019
          ps->dptr = p + 1; /* restore location */
3✔
1020
          break;
3✔
1021
        }
1022
        if (implicit && pat_or)
88✔
1023
        {
1024
          /* A | B & C == (A | B) & C */
1025
          struct Pattern *root = attach_new_root(&curlist);
1026
          root->op = MUTT_PAT_OR;
1✔
1027
          pat_or = false;
1028
        }
1029

1030
        entry = lookup_tag(ps->dptr[1]);
88✔
1031
        if (!entry)
88✔
1032
        {
1033
          buf_printf(err, _("%c: invalid pattern modifier"), *ps->dptr);
×
1034
          goto cleanup;
×
1035
        }
1036
        if (entry->flags && ((flags & entry->flags) == 0))
88✔
1037
        {
1038
          buf_printf(err, _("%c: not supported in this mode"), *ps->dptr);
4✔
1039
          goto cleanup;
4✔
1040
        }
1041

1042
        struct Pattern *leaf = attach_new_leaf(&curlist);
84✔
1043
        leaf->pat_not = pat_not;
84✔
1044
        leaf->all_addr = all_addr;
84✔
1045
        leaf->is_alias = is_alias;
84✔
1046
        leaf->string_match = (ps->dptr[0] == '=');
84✔
1047
        leaf->group_match = (ps->dptr[0] == '%');
84✔
1048
        leaf->sendmode = (flags & MUTT_PC_SEND_MODE_SEARCH);
84✔
1049
        leaf->op = entry->op;
84✔
1050
        pat_not = false;
1051
        all_addr = false;
1052
        is_alias = false;
1053

1054
        ps->dptr++; /* move past the ~ */
1055
        ps->dptr++; /* eat the operator and any optional whitespace */
84✔
1056
        SKIPWS(ps->dptr);
139✔
1057
        if (entry->eat_arg)
84✔
1058
        {
1059
          if (ps->dptr[0] == '\0')
50✔
1060
          {
1061
            buf_addstr(err, _("missing parameter"));
1✔
1062
            goto cleanup;
1✔
1063
          }
1064
          switch (entry->eat_arg)
49✔
1065
          {
1066
            case EAT_REGEX:
26✔
1067
              if (!eat_regex(leaf, flags, ps, err))
26✔
1068
                goto cleanup;
×
1069
              break;
1070
            case EAT_DATE:
17✔
1071
              if (!eat_date(leaf, flags, ps, err))
17✔
1072
                goto cleanup;
4✔
1073
              break;
1074
            case EAT_RANGE:
3✔
1075
              if (!eat_range(leaf, flags, ps, err))
3✔
1076
                goto cleanup;
×
1077
              break;
1078
            case EAT_MESSAGE_RANGE:
2✔
1079
              if (!eat_message_range(leaf, flags, ps, err, mv))
2✔
1080
                goto cleanup;
2✔
1081
              break;
1082
            case EAT_QUERY:
1✔
1083
              if (!eat_query(leaf, flags, ps, err, m))
1✔
1084
                goto cleanup;
×
1085
              break;
1086
            default:
1087
              break;
1088
          }
1089
        }
1090
        implicit = true;
1091
        break;
1092
      }
1093

1094
      case '(':
4✔
1095
      {
1096
        p = find_matching_paren(ps->dptr + 1);
4✔
1097
        if (p[0] != ')')
4✔
1098
        {
1099
          buf_printf(err, _("mismatched parentheses: %s"), ps->dptr);
×
1100
          goto cleanup;
×
1101
        }
1102
        /* compile the sub-expression */
1103
        buf = mutt_strn_dup(ps->dptr + 1, p - (ps->dptr + 1));
4✔
1104
        struct PatternList *sub = mutt_pattern_comp(mv, menu, buf, flags, err);
4✔
1105
        FREE(&buf);
4✔
1106
        if (!sub)
4✔
1107
          goto cleanup;
×
1108
        struct Pattern *leaf = SLIST_FIRST(sub);
4✔
1109
        if (curlist)
4✔
1110
        {
1111
          attach_leaf(curlist, leaf);
1112
          FREE(&sub);
1✔
1113
        }
1114
        else
1115
        {
1116
          curlist = sub;
3✔
1117
        }
1118
        leaf->pat_not ^= pat_not;
4✔
1119
        leaf->all_addr |= all_addr;
4✔
1120
        leaf->is_alias |= is_alias;
4✔
1121
        pat_not = false;
1122
        all_addr = false;
1123
        is_alias = false;
1124
        ps->dptr = p + 1; /* restore location */
4✔
1125
        break;
4✔
1126
      }
1127

1128
      default:
1✔
1129
        buf_printf(err, _("error in pattern at: %s"), ps->dptr);
1✔
1130
        goto cleanup;
1✔
1131
    }
1132
    SKIPWS(ps->dptr);
97✔
1133
  }
1134
  buf_pool_release(&ps);
71✔
1135

1136
  if (!curlist)
71✔
1137
  {
1138
    buf_strcpy(err, _("empty pattern"));
×
1139
    return NULL;
×
1140
  }
1141

1142
  if (SLIST_NEXT(SLIST_FIRST(curlist), entries))
71✔
1143
  {
1144
    struct Pattern *root = attach_new_root(&curlist);
1145
    root->op = pat_or ? MUTT_PAT_OR : MUTT_PAT_AND;
17✔
1146
  }
1147

1148
  return curlist;
71✔
1149

1150
cleanup:
12✔
1151
  mutt_pattern_free(&curlist);
12✔
1152
  buf_pool_release(&ps);
12✔
1153
  return NULL;
12✔
1154
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc