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

neomutt / neomutt / 13362243721

16 Feb 2025 04:04PM UTC coverage: 49.522% (+0.06%) from 49.467%
13362243721

push

github

flatcap
main: drop -B (batch mode) option

Drop the -B (batch mode) command line option.
It didn't do anything.

8915 of 18002 relevant lines covered (49.52%)

245.47 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 <ctype.h>
33
#include <stdbool.h>
34
#include <stdint.h>
35
#include <stdio.h>
36
#include <stdlib.h>
37
#include <string.h>
38
#include <sys/types.h>
39
#include <time.h>
40
#include "private.h"
41
#include "mutt/lib.h"
42
#include "address/lib.h"
43
#include "config/lib.h"
44
#include "core/lib.h"
45
#include "lib.h"
46
#include "parse/lib.h"
47
#include "mview.h"
48

49
struct Menu;
50

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

62
#define MUTT_PDR_ERRORDONE (MUTT_PDR_ERROR | MUTT_PDR_DONE)
63

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

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

111
  rc = true;
112

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

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

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

149
  FILE *fp = NULL;
1✔
150

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

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

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

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

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

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

203
  rc = true;
204

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

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

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

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

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

288
    iso8601 = false;
289
    break;
290
  }
291

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

617
  return true;
13✔
618
}
619

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

813
    np = next;
99✔
814
  }
815

816
  FREE(pat);
86✔
817
}
818

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1149
  return curlist;
71✔
1150

1151
cleanup:
12✔
1152
  mutt_pattern_free(&curlist);
12✔
1153
  buf_pool_release(&ps);
12✔
1154
  return NULL;
12✔
1155
}
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