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

neomutt / neomutt / 20421147915

21 Dec 2025 11:26PM UTC coverage: 50.236% (+0.3%) from 49.984%
20421147915

push

github

flatcap
merge: refactor command parse() API

No functional changes

 * parse: rename buf to token
 * parse: rename s to line
 * parse: use buf_string()
 * parse: rename functions
 * parse: rename labels
 * parse: rename local variable
 * parse: rename local variable
 * parse: rename local Buffer variables
 * parse: parse_rc_line(), parse_rc_buffer()
 * parse: add Command param to API
 * parase: standardise too few/many args
 * trans: tidy command messages

92 of 194 new or added lines in 7 files covered. (47.42%)

22 existing lines in 4 files now uncovered.

9271 of 18455 relevant lines covered (50.24%)

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

59
#define MUTT_PDR_ERRORDONE (MUTT_PDR_ERROR | MUTT_PDR_DONE)
60

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

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

109
  rc = true;
110

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

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

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

147
  FILE *fp = NULL;
1✔
148

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

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

168
  buf_addstr(cmd, c_external_search_command);
1✔
169
  buf_addch(cmd, ' ');
1✔
170

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

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

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

200
  rc = true;
201

202
out:
1✔
203
  buf_pool_release(&cmd);
1✔
204
  buf_pool_release(&tok);
1✔
205
  return rc;
1✔
206
}
207

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

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

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

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

285
    iso8601 = false;
286
    break;
287
  }
288

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

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

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

313
    return (s + 8);
1✔
314
  }
315

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

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

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

463
    tmp = min->tm_year;
464
    min->tm_year = max->tm_year;
1✔
465
    max->tm_year = tmp;
1✔
466

467
    tmp = min->tm_mon;
1✔
468
    min->tm_mon = max->tm_mon;
1✔
469
    max->tm_mon = tmp;
1✔
470

471
    tmp = min->tm_mday;
1✔
472
    min->tm_mday = max->tm_mday;
1✔
473
    max->tm_mday = tmp;
1✔
474

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

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

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

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

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

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

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

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

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

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

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

584
      struct tm base_min = { 0 };
5✔
585

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

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

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

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

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

614
  return true;
13✔
615
}
616

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

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

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

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

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

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

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

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

730
  if (flags & MUTT_PC_PATTERN_DYNAMIC)
17✔
731
  {
732
    pat->dynamic = true;
×
NEW
733
    pat->p.str = buf_strdup(tmp);
×
734
  }
735

736
  rc = eval_date_minmax(pat, tmp->data, err);
17✔
737

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

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

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

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

779
  struct Pattern *np = SLIST_FIRST(*pat);
86✔
780
  struct Pattern *next = NULL;
781

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

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

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

810
    np = next;
99✔
811
  }
812

813
  FREE(pat);
86✔
814
}
815

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

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

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

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

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

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

918
  if (!s || (s[0] == '\0'))
85✔
919
  {
920
    buf_strcpy(err, _("empty pattern"));
1✔
921
    return NULL;
1✔
922
  }
923

924
  struct Buffer *ps = buf_pool_get();
84✔
925
  buf_strcpy(ps, s);
84✔
926
  buf_seek(ps, 0);
84✔
927

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

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

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

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

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

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

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

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

1132
  if (!curlist)
71✔
1133
  {
1134
    buf_strcpy(err, _("empty pattern"));
×
1135
    return NULL;
×
1136
  }
1137

1138
  if (SLIST_NEXT(SLIST_FIRST(curlist), entries))
71✔
1139
  {
1140
    struct Pattern *root = attach_new_root(&curlist);
1141
    root->op = pat_or ? MUTT_PAT_OR : MUTT_PAT_AND;
17✔
1142
  }
1143

1144
  return curlist;
71✔
1145

1146
cleanup:
12✔
1147
  mutt_pattern_free(&curlist);
12✔
1148
  buf_pool_release(&ps);
12✔
1149
  return NULL;
12✔
1150
}
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