Coverage Report

Created: 2026-08-13 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pigeonhole/src/lib-sieve/sieve-parser.c
Line
Count
Source
1
/* Copyright (c) Pigeonhole authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "istream.h"
5
#include "failures.h"
6
7
#include "sieve-common.h"
8
#include "sieve-limits.h"
9
#include "sieve-script.h"
10
#include "sieve-lexer.h"
11
#include "sieve-parser.h"
12
#include "sieve-error.h"
13
#include "sieve-ast.h"
14
15
/*
16
 * Forward declarations
17
 */
18
19
static int
20
sieve_parser_recover(struct sieve_parser *parser,
21
         enum sieve_token_type end_token);
22
23
/*
24
 * Parser object
25
 */
26
27
struct sieve_parser {
28
  pool_t pool;
29
30
  bool valid;
31
32
  struct sieve_script *script;
33
34
  struct sieve_error_handler *ehandler;
35
36
  const struct sieve_lexer *lexer;
37
  struct sieve_ast *ast;
38
};
39
40
struct sieve_parser *
41
sieve_parser_create(struct sieve_script *script,
42
        struct sieve_error_handler *ehandler,
43
        enum sieve_error *error_code_r)
44
0
{
45
0
  struct sieve_parser *parser;
46
0
  const struct sieve_lexer *lexer;
47
48
0
  lexer = sieve_lexer_create(script, ehandler, error_code_r);
49
0
  if (lexer != NULL) {
50
0
    pool_t pool = pool_alloconly_create("sieve_parser", 4096);
51
52
0
    parser = p_new(pool, struct sieve_parser, 1);
53
0
    parser->pool = pool;
54
0
    parser->valid = TRUE;
55
56
0
    parser->ehandler = ehandler;
57
0
    sieve_error_handler_ref(ehandler);
58
59
0
    parser->script = script;
60
0
    sieve_script_ref(script);
61
62
0
    parser->lexer = lexer;
63
0
    parser->ast = NULL;
64
65
0
    return parser;
66
0
  }
67
68
0
  return NULL;
69
0
}
70
71
void sieve_parser_free(struct sieve_parser **parser)
72
0
{
73
0
  if ((*parser)->ast != NULL)
74
0
    sieve_ast_unref(&(*parser)->ast);
75
76
0
  sieve_lexer_free(&(*parser)->lexer);
77
0
  sieve_script_unref(&(*parser)->script);
78
79
0
  sieve_error_handler_unref(&(*parser)->ehandler);
80
81
0
  pool_unref(&(*parser)->pool);
82
83
0
  *parser = NULL;
84
0
}
85
86
/*
87
 * Internal error handling
88
 */
89
90
inline static void ATTR_FORMAT(4, 5)
91
sieve_parser_error(struct sieve_parser *parser,
92
       const char *csrc_filename, unsigned int csrc_linenum,
93
       const char *fmt, ...)
94
0
{
95
0
  struct sieve_error_params params = {
96
0
    .log_type = LOG_TYPE_ERROR,
97
0
    .csrc = {
98
0
      .filename = csrc_filename,
99
0
      .linenum = csrc_linenum,
100
0
    },
101
0
  };
102
0
  va_list args;
103
104
0
  va_start(args, fmt);
105
106
  /* Don't report a parse error if the lexer complained already */
107
0
  if (sieve_lexer_token_type(parser->lexer) != STT_ERROR) {
108
0
    T_BEGIN {
109
0
      params.location = sieve_error_script_location(
110
0
        parser->script,
111
0
        sieve_lexer_token_line(parser->lexer));
112
0
      sieve_logv(parser->ehandler, &params, fmt, args);
113
0
    } T_END;
114
0
  }
115
116
0
  parser->valid = FALSE;
117
118
0
  va_end(args);
119
0
}
120
#define sieve_parser_error(parser, ...) \
121
0
  sieve_parser_error(parser, __FILE__, __LINE__, __VA_ARGS__)
122
123
/*
124
 * Sieve grammar parsing
125
 */
126
127
/* sieve_parse_arguments():
128
129
   Parses both command arguments and sub-tests:
130
     arguments = *argument [test / test-list]
131
     argument = string-list / number / tag
132
     string = quoted-string / multi-line   [[implicitly handled in lexer]]
133
     string-list = "[" string *("," string) "]" / string         ;; if
134
       there is only a single string, the brackets are optional
135
     test-list = "(" test *("," test) ")"
136
     test = identifier arguments
137
 */
138
static int
139
sieve_parse_arguments(struct sieve_parser *parser, struct sieve_ast_node *node,
140
          unsigned int depth)
141
0
{
142
0
  const struct sieve_lexer *lexer = parser->lexer;
143
0
  struct sieve_ast_node *test = NULL;
144
0
  bool test_present = TRUE;
145
0
  bool arg_present = TRUE;
146
0
  int result = 1; /* Indicates whether the parser is in a defined, not
147
                      necessarily error-free state */
148
149
  /* Parse arguments */
150
0
  while (arg_present && result > 0) {
151
0
    struct sieve_ast_argument *arg;
152
153
0
    if (!parser->valid &&
154
0
        !sieve_errors_more_allowed(parser->ehandler)) {
155
0
      result = 0;
156
0
      break;
157
0
    }
158
159
0
    switch (sieve_lexer_token_type(lexer)) {
160
    /* String list */
161
0
    case STT_LSQUARE:
162
      /* Create stinglist object */
163
0
      arg = sieve_ast_argument_stringlist_create(
164
0
        node, sieve_lexer_token_line(parser->lexer));
165
0
      if (arg == NULL) break;
166
0
      sieve_lexer_skip_token(lexer);
167
168
0
      if (sieve_lexer_token_type(lexer) == STT_STRING) {
169
0
        bool add_failed = FALSE;
170
171
        /* Add the string to the list */
172
0
        if (!sieve_ast_stringlist_add(
173
0
          arg, sieve_lexer_token_str(lexer),
174
0
          sieve_lexer_token_line(parser->lexer)))
175
0
          add_failed = TRUE;
176
0
        sieve_lexer_skip_token(lexer);
177
178
0
        while (!add_failed &&
179
0
               sieve_lexer_token_type(lexer) == STT_COMMA) {
180
0
          sieve_lexer_skip_token(lexer);
181
182
          /* Check parser status */
183
0
          if (!parser->valid &&
184
0
              !sieve_errors_more_allowed(parser->ehandler)) {
185
0
            result = sieve_parser_recover(parser, STT_RSQUARE);
186
0
            break;
187
0
          }
188
189
0
          if (sieve_lexer_token_type(lexer) == STT_STRING) {
190
            /* Add the string to the list */
191
0
            if (!sieve_ast_stringlist_add(
192
0
              arg, sieve_lexer_token_str(lexer),
193
0
              sieve_lexer_token_line(parser->lexer)))
194
0
              add_failed = TRUE;
195
196
0
            sieve_lexer_skip_token(lexer);
197
0
          } else {
198
0
            sieve_parser_error(parser,
199
0
              "expecting string after ',' in string list, "
200
0
              "but found %s",
201
0
              sieve_lexer_token_description(lexer));
202
0
            result = sieve_parser_recover(parser, STT_RSQUARE);
203
0
            break;
204
0
          }
205
0
        }
206
207
0
        if (add_failed) {
208
0
          sieve_parser_error(parser,
209
0
            "failed to accept more items in string list");
210
0
          return -1;
211
0
        }
212
0
      } else {
213
0
        sieve_parser_error(parser,
214
0
          "expecting string after '[' in string list, "
215
0
          "but found %s",
216
0
          sieve_lexer_token_description(lexer));
217
0
        result = sieve_parser_recover(parser, STT_RSQUARE);
218
0
      }
219
220
      /* Finish the string list */
221
0
      if (sieve_lexer_token_type(lexer) == STT_RSQUARE) {
222
0
        sieve_lexer_skip_token(lexer);
223
0
      } else {
224
0
        sieve_parser_error(parser,
225
0
          "expecting ',' or end of string list ']', "
226
0
          "but found %s",
227
0
          sieve_lexer_token_description(lexer));
228
229
0
        if ((result = sieve_parser_recover(parser, STT_RSQUARE)) > 0)
230
0
          sieve_lexer_skip_token(lexer);
231
0
      }
232
0
      break;
233
    /* Single string */
234
0
    case STT_STRING:
235
0
      arg = sieve_ast_argument_string_create(
236
0
        node, sieve_lexer_token_str(lexer),
237
0
        sieve_lexer_token_line(parser->lexer));
238
0
      sieve_lexer_skip_token(lexer);
239
0
      break;
240
    /* Number */
241
0
    case STT_NUMBER:
242
0
      arg = sieve_ast_argument_number_create(
243
0
        node, sieve_lexer_token_int(lexer),
244
0
        sieve_lexer_token_line(parser->lexer));
245
0
      sieve_lexer_skip_token(lexer);
246
0
      break;
247
    /* Tag */
248
0
    case STT_TAG:
249
0
      arg = sieve_ast_argument_tag_create(
250
0
        node, sieve_lexer_token_ident(lexer),
251
0
        sieve_lexer_token_line(parser->lexer));
252
0
      sieve_lexer_skip_token(lexer);
253
0
      break;
254
    /* End of argument list, continue with tests */
255
0
    default:
256
0
      arg_present = FALSE;
257
0
      break;
258
0
    }
259
260
0
    if (arg_present && arg == NULL) {
261
0
      sieve_parser_error(parser,
262
0
        "failed to accept more arguments for command '%s'",
263
0
        node->identifier);
264
0
      return -1;
265
0
    }
266
267
0
    if (sieve_ast_argument_count(node) > SIEVE_MAX_COMMAND_ARGUMENTS) {
268
0
      sieve_parser_error(parser,
269
0
        "too many arguments for command '%s'",
270
0
        node->identifier);
271
0
      return 0;
272
0
    }
273
0
  }
274
275
0
  if (result <= 0)
276
0
    return result; /* Defer recovery to caller */
277
278
  /* --> [ test / test-list ]
279
     test-list = "(" test *("," test) ")"
280
     test = identifier arguments
281
   */
282
0
  switch (sieve_lexer_token_type(lexer)) {
283
  /* Single test */
284
0
  case STT_IDENTIFIER:
285
0
    if ((depth + 1) > SIEVE_MAX_TEST_NESTING) {
286
0
      sieve_parser_error(parser,
287
0
        "cannot nest tests deeper than %u levels",
288
0
        SIEVE_MAX_TEST_NESTING);
289
0
      return 0;
290
0
    }
291
292
0
    test = sieve_ast_test_create(
293
0
      node, sieve_lexer_token_ident(lexer),
294
0
      sieve_lexer_token_line(parser->lexer));
295
0
    sieve_lexer_skip_token(lexer);
296
297
    /* Theoretically, test can be NULL */
298
0
    if (test == NULL)
299
0
      break;
300
301
    /* Parse test arguments, which may include more tests (recurse) */
302
0
    if (sieve_parse_arguments(parser, test, depth + 1) <= 0) {
303
0
      return 0; /* Defer recovery to caller */
304
0
    }
305
306
0
    break;
307
308
  /* Test list */
309
0
  case STT_LBRACKET:
310
0
    sieve_lexer_skip_token(lexer);
311
312
0
    if (depth+1 > SIEVE_MAX_TEST_NESTING) {
313
0
      sieve_parser_error(parser,
314
0
        "cannot nest tests deeper than %u levels",
315
0
        SIEVE_MAX_TEST_NESTING);
316
0
      result = sieve_parser_recover(parser, STT_RBRACKET);
317
318
0
      if (result > 0)
319
0
        sieve_lexer_skip_token(lexer);
320
0
      return result;
321
0
    }
322
323
0
    node->test_list = TRUE;
324
325
    /* Test starts with identifier */
326
0
    if (sieve_lexer_token_type(lexer) == STT_IDENTIFIER) {
327
0
      test = sieve_ast_test_create(
328
0
        node, sieve_lexer_token_ident(lexer),
329
0
        sieve_lexer_token_line(parser->lexer));
330
0
      sieve_lexer_skip_token(lexer);
331
332
0
      if (test == NULL)
333
0
        break;
334
335
      /* Parse test arguments, which may include more tests (recurse) */
336
0
      if ((result = sieve_parse_arguments(parser, test, depth+1)) > 0) {
337
338
        /* More tests ? */
339
0
        while (sieve_lexer_token_type(lexer) == STT_COMMA) {
340
0
          sieve_lexer_skip_token(lexer);
341
342
          /* Check parser status */
343
0
          if (!parser->valid &&
344
0
              !sieve_errors_more_allowed(parser->ehandler)) {
345
0
            result = sieve_parser_recover(parser, STT_RBRACKET);
346
0
            break;
347
0
          }
348
349
          /* Test starts with identifier */
350
0
          if (sieve_lexer_token_type(lexer) == STT_IDENTIFIER) {
351
0
            test = sieve_ast_test_create(
352
0
              node, sieve_lexer_token_ident(lexer),
353
0
              sieve_lexer_token_line(parser->lexer));
354
0
            sieve_lexer_skip_token(lexer);
355
356
0
            if (test == NULL)
357
0
              break;
358
359
            /* Parse test arguments, which may include more tests (recurse) */
360
0
            if ((result = sieve_parse_arguments(parser, test, depth+1)) <= 0) {
361
0
              if (result < 0)
362
0
                return result;
363
0
              result = sieve_parser_recover(parser, STT_RBRACKET);
364
0
              break;
365
0
            }
366
0
          } else {
367
0
            sieve_parser_error(parser,
368
0
              "expecting test identifier after ',' in test list, "
369
0
              "but found %s",
370
0
              sieve_lexer_token_description(lexer));
371
0
            result = sieve_parser_recover(parser, STT_RBRACKET);
372
0
            break;
373
0
          }
374
0
        }
375
0
        if (test == NULL)
376
0
          break;
377
0
      } else {
378
0
        if (result < 0)
379
0
          return result;
380
381
0
        result = sieve_parser_recover(parser, STT_RBRACKET);
382
0
      }
383
0
    } else {
384
0
      sieve_parser_error(parser,
385
0
        "expecting test identifier after '(' in test list, "
386
0
        "but found %s",
387
0
        sieve_lexer_token_description(lexer));
388
389
0
      result = sieve_parser_recover(parser, STT_RBRACKET);
390
0
    }
391
392
    /* The next token should be a ')', indicating the end of the
393
       test list
394
         --> previous sieve_parser_recover calls try to restore this
395
       situation after parse errors.
396
     */
397
0
    if (sieve_lexer_token_type(lexer) == STT_RBRACKET) {
398
0
      sieve_lexer_skip_token(lexer);
399
0
    } else {
400
0
      sieve_parser_error(parser,
401
0
        "expecting ',' or end of test list ')', "
402
0
        "but found %s",
403
0
        sieve_lexer_token_description(lexer));
404
405
      /* Recover function tries to make next token equal to
406
         ')'. If it succeeds we need to skip it.
407
       */
408
0
      if ((result = sieve_parser_recover(parser, STT_RBRACKET)) > 0)
409
0
        sieve_lexer_skip_token(lexer);
410
0
    }
411
0
    break;
412
413
0
  default:
414
    /* Not an error: test / test-list is optional
415
         --> any errors are detected by the caller
416
     */
417
0
    test_present = FALSE;
418
0
    break;
419
0
  }
420
421
0
  if (test_present && test == NULL) {
422
0
    sieve_parser_error(parser,
423
0
      "failed to accept more tests for command '%s'",
424
0
      node->identifier);
425
0
    return -1;
426
0
  }
427
428
0
  return result;
429
0
}
430
431
/* commands = *command
432
   command = identifier arguments ( ";" / block )
433
   block = "{" commands "}"
434
 */
435
static int
436
sieve_parse_commands(struct sieve_parser *parser, struct sieve_ast_node *block,
437
         unsigned int depth)
438
0
{
439
0
  const struct sieve_lexer *lexer = parser->lexer;
440
0
  int result = 1;
441
442
0
  while (result > 0 &&
443
0
         sieve_lexer_token_type(lexer) == STT_IDENTIFIER) {
444
0
    struct sieve_ast_node *command;
445
446
    /* Check parser status */
447
0
    if (!parser->valid &&
448
0
        !sieve_errors_more_allowed(parser->ehandler)) {
449
0
      result = sieve_parser_recover(parser, STT_SEMICOLON);
450
0
      break;
451
0
    }
452
453
    /* Create command node */
454
0
    command = sieve_ast_command_create(
455
0
      block, sieve_lexer_token_ident(lexer),
456
0
      sieve_lexer_token_line(parser->lexer));
457
0
    sieve_lexer_skip_token(lexer);
458
459
0
    if (command == NULL) {
460
0
      sieve_parser_error(parser,
461
0
        "failed to accept more commands inside the block of command '%s'",
462
0
        block->identifier);
463
0
      return -1;
464
0
    }
465
466
0
    result = sieve_parse_arguments(parser, command, 1);
467
468
    /* Check whether the command is properly terminated
469
       (i.e. with ; or a new block)
470
     */
471
0
    if (result > 0 &&
472
0
        sieve_lexer_token_type(lexer) != STT_SEMICOLON &&
473
0
        sieve_lexer_token_type(lexer) != STT_LCURLY) {
474
475
0
      sieve_parser_error(parser,
476
0
        "expected end of command ';' or the beginning of a compound block '{', "
477
0
        "but found %s",
478
0
        sieve_lexer_token_description(lexer));
479
0
      result = 0;
480
0
    }
481
482
    /* Try to recover from parse errors to reacquire a defined state
483
     */
484
0
    if (result == 0)
485
0
      result = sieve_parser_recover(parser, STT_SEMICOLON);
486
487
    /* Don't bother to continue if we are not in a defined state */
488
0
    if (result <= 0)
489
0
      return result;
490
491
0
    switch (sieve_lexer_token_type(lexer)) {
492
    /* End of the command */
493
0
    case STT_SEMICOLON:
494
0
      sieve_lexer_skip_token(lexer);
495
0
      break;
496
    /* Command has a block {...} */
497
0
    case STT_LCURLY:
498
0
      sieve_lexer_skip_token(lexer);
499
500
      /* Check current depth first */
501
0
      if ((depth + 1) > SIEVE_MAX_BLOCK_NESTING) {
502
0
        sieve_parser_error(parser,
503
0
          "cannot nest command blocks deeper than %u levels",
504
0
          SIEVE_MAX_BLOCK_NESTING);
505
0
        result = sieve_parser_recover(parser, STT_RCURLY);
506
507
0
        if (result > 0)
508
0
          sieve_lexer_skip_token(lexer);
509
0
        break;
510
0
      }
511
512
0
      command->block = TRUE;
513
514
0
      if ((result = sieve_parse_commands(parser, command, depth + 1)) > 0) {
515
0
        if (sieve_lexer_token_type(lexer) != STT_RCURLY) {
516
0
          sieve_parser_error(parser,
517
0
            "expected end of compound block '}', "
518
0
            "but found %s",
519
0
            sieve_lexer_token_description(lexer));
520
0
          result = sieve_parser_recover(parser, STT_RCURLY);
521
0
        } else {
522
0
          sieve_lexer_skip_token(lexer);
523
0
        }
524
0
      } else {
525
0
        if (result < 0)
526
0
          return result;
527
528
0
        if ((result = sieve_parser_recover(parser, STT_RCURLY)) > 0)
529
0
          sieve_lexer_skip_token(lexer);
530
0
      }
531
532
0
      break;
533
534
0
    default:
535
      /* Recovered previously, so this cannot happen */
536
0
      i_unreached();
537
0
    }
538
0
  }
539
540
0
  return result;
541
0
}
542
543
bool sieve_parser_run(struct sieve_parser *parser, struct sieve_ast **ast)
544
0
{
545
0
  if (parser->ast != NULL)
546
0
    sieve_ast_unref(&parser->ast);
547
548
  /* Create AST object if none is provided */
549
0
  if (*ast == NULL)
550
0
    *ast = sieve_ast_create(parser->script);
551
0
  else
552
0
    sieve_ast_ref(*ast);
553
554
0
  parser->ast = *ast;
555
556
  /* Scan first token */
557
0
  sieve_lexer_skip_token(parser->lexer);
558
559
  /* Parse */
560
0
  if (sieve_parse_commands(parser, sieve_ast_root(parser->ast), 1) > 0 &&
561
0
      parser->valid) {
562
    /* Parsed right to EOF ? */
563
0
    if (sieve_lexer_token_type(parser->lexer) != STT_EOF) {
564
0
      sieve_parser_error(parser,
565
0
        "unexpected %s found at (the presumed) end of file",
566
0
        sieve_lexer_token_description(parser->lexer));
567
0
      parser->valid = FALSE;
568
0
    }
569
0
  } else {
570
0
    parser->valid = FALSE;
571
0
  }
572
573
  /* Clean up AST if parse failed */
574
0
  if (!parser->valid) {
575
0
    parser->ast = NULL;
576
0
    sieve_ast_unref(ast);
577
0
  }
578
579
0
  return parser->valid;
580
0
}
581
582
/* Error recovery:
583
     To continue parsing after an error it is important to find the next
584
     parsible item in the stream. The recover function skips over the remaining
585
     garbage after an error. It tries  to find the end of the failed syntax
586
     structure and takes nesting of structures into account.
587
 */
588
589
/* Assign useful names to priorities for readability */
590
enum sieve_grammatical_prio {
591
  SGP_BLOCK = 3,
592
  SGP_COMMAND = 2,
593
  SGP_TEST_LIST = 1,
594
  SGP_STRING_LIST = 0,
595
596
  SGP_OTHER = -1
597
};
598
599
static inline enum sieve_grammatical_prio
600
__get_token_priority(enum sieve_token_type token)
601
0
{
602
0
  switch (token) {
603
0
  case STT_LCURLY:
604
0
  case STT_RCURLY:
605
0
    return SGP_BLOCK;
606
0
  case STT_SEMICOLON:
607
0
    return SGP_COMMAND;
608
0
  case STT_LBRACKET:
609
0
  case STT_RBRACKET:
610
0
    return SGP_TEST_LIST;
611
0
  case STT_LSQUARE:
612
0
  case STT_RSQUARE:
613
0
    return SGP_STRING_LIST;
614
0
  default:
615
0
    break;
616
0
  }
617
618
0
  return SGP_OTHER;
619
0
}
620
621
static int
622
sieve_parser_recover(struct sieve_parser *parser,
623
         enum sieve_token_type end_token)
624
0
{
625
  /* The tokens that begin/end a specific block/command/list in order
626
     of ascending grammatical priority.
627
   */
628
0
  static const enum sieve_token_type begin_tokens[4] = {
629
0
    STT_LSQUARE, STT_LBRACKET, STT_NONE, STT_LCURLY };
630
0
  static const enum sieve_token_type end_tokens[4] = {
631
0
    STT_RSQUARE, STT_RBRACKET, STT_SEMICOLON, STT_RCURLY};
632
0
  const struct sieve_lexer *lexer = parser->lexer;
633
0
  int nesting = 1;
634
0
  enum sieve_grammatical_prio end_priority =
635
0
    __get_token_priority(end_token);
636
637
0
  i_assert(end_priority != SGP_OTHER);
638
639
0
  while (sieve_lexer_token_type(lexer) != STT_EOF &&
640
0
         __get_token_priority(sieve_lexer_token_type(lexer))
641
0
    <= end_priority) {
642
0
    if (sieve_lexer_token_type(lexer) ==
643
0
      begin_tokens[end_priority]) {
644
0
      nesting++;
645
0
      sieve_lexer_skip_token(lexer);
646
0
      continue;
647
0
    }
648
0
    if (sieve_lexer_token_type(lexer) ==
649
0
      end_tokens[end_priority]) {
650
0
      nesting--;
651
652
0
      if (nesting == 0) {
653
        /* Next character is the end */
654
0
        return 1;
655
0
      }
656
0
    }
657
0
    sieve_lexer_skip_token(lexer);
658
0
  }
659
660
  /* Special case: COMMAND */
661
0
  if (end_token == STT_SEMICOLON &&
662
0
      sieve_lexer_token_type(lexer) == STT_LCURLY) {
663
0
    return 1;
664
0
  }
665
666
  /* End not found before eof or end of surrounding grammatical structure
667
   */
668
0
  return 0;
669
0
}