Coverage Report

Created: 2026-09-01 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pigeonhole/src/lib-sieve/sieve-lexer.c
Line
Count
Source
1
/* Copyright (c) Pigeonhole authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "compat.h"
5
#include "str.h"
6
#include "str-sanitize.h"
7
#include "istream.h"
8
9
#include "sieve-common.h"
10
#include "sieve-limits.h"
11
#include "sieve-error.h"
12
#include "sieve-script.h"
13
14
#include "sieve-lexer.h"
15
16
#include <stdio.h>
17
#include <sys/types.h>
18
#include <sys/stat.h>
19
#include <fcntl.h>
20
#include <unistd.h>
21
#include <ctype.h>
22
23
/*
24
 * Useful macros
25
 */
26
27
#define DIGIT_VAL(c) (c - '0')
28
29
/*
30
 * Lexer object
31
 */
32
33
struct sieve_lexical_scanner {
34
  pool_t pool;
35
  struct sieve_instance *svinst;
36
37
  struct sieve_script *script;
38
  struct istream *input;
39
40
  struct sieve_error_handler *ehandler;
41
42
  /* Currently scanned data */
43
  const unsigned char *buffer;
44
  size_t buffer_size;
45
  size_t buffer_pos;
46
47
  struct sieve_lexer lexer;
48
49
  int current_line;
50
};
51
52
const struct sieve_lexer *
53
sieve_lexer_create(struct sieve_script *script,
54
       struct sieve_error_handler *ehandler,
55
       enum sieve_error *error_code_r)
56
0
{
57
0
  struct sieve_lexical_scanner *scanner;
58
0
  struct sieve_instance *svinst = sieve_script_svinst(script);
59
0
  struct istream *stream;
60
0
  const struct stat *st;
61
62
  /* Open script as stream */
63
0
  if (sieve_script_get_stream(script, &stream, error_code_r) < 0)
64
0
    return NULL;
65
66
  /* Check script size */
67
0
  if (i_stream_stat(stream, TRUE, &st) >= 0 && st->st_size > 0 &&
68
0
      svinst->set->max_script_size > 0 &&
69
0
      (uoff_t)st->st_size > svinst->set->max_script_size) {
70
0
    sieve_error(ehandler, sieve_script_name(script),
71
0
      "sieve script is too large (max %zu bytes)",
72
0
      svinst->set->max_script_size);
73
0
    if (error_code_r != NULL)
74
0
      *error_code_r = SIEVE_ERROR_NOT_POSSIBLE;
75
0
    return NULL;
76
0
  }
77
78
0
  scanner = i_new(struct sieve_lexical_scanner, 1);
79
0
  scanner->lexer.scanner = scanner;
80
81
0
  scanner->ehandler = ehandler;
82
0
  sieve_error_handler_ref(ehandler);
83
84
0
  scanner->input = stream;
85
0
  i_stream_ref(scanner->input);
86
87
0
  scanner->script = script;
88
0
  sieve_script_ref(script);
89
90
0
  scanner->buffer = NULL;
91
0
  scanner->buffer_size = 0;
92
0
  scanner->buffer_pos = 0;
93
94
0
  scanner->lexer.token_type = STT_NONE;
95
0
  scanner->lexer.token_str_value = str_new(default_pool, 256);
96
0
  scanner->lexer.token_int_value = 0;
97
0
  scanner->lexer.token_line = 1;
98
99
0
  scanner->current_line = 1;
100
101
0
  return &scanner->lexer;
102
0
}
103
104
void sieve_lexer_free(const struct sieve_lexer **_lexer)
105
0
{
106
0
  const struct sieve_lexer *lexer = *_lexer;
107
0
  struct sieve_lexical_scanner *scanner = lexer->scanner;
108
109
0
  i_stream_unref(&scanner->input);
110
0
  sieve_script_unref(&scanner->script);
111
0
  sieve_error_handler_unref(&scanner->ehandler);
112
0
  str_free(&scanner->lexer.token_str_value);
113
114
0
  i_free(scanner);
115
0
  *_lexer = NULL;
116
0
}
117
118
/*
119
 * Internal error handling
120
 */
121
122
inline static void ATTR_FORMAT(4, 5)
123
sieve_lexer_error(const struct sieve_lexer *lexer,
124
      const char *csrc_filename, unsigned int csrc_linenum,
125
      const char *fmt, ...)
126
0
{
127
0
  struct sieve_lexical_scanner *scanner = lexer->scanner;
128
0
  struct sieve_error_params params = {
129
0
    .log_type = LOG_TYPE_ERROR,
130
0
    .csrc = {
131
0
      .filename = csrc_filename,
132
0
      .linenum = csrc_linenum,
133
0
    },
134
0
  };
135
0
  va_list args;
136
137
0
  va_start(args, fmt);
138
139
0
  T_BEGIN {
140
0
    params.location =
141
0
      sieve_error_script_location(scanner->script,
142
0
                scanner->current_line);
143
0
    sieve_logv(scanner->ehandler, &params, fmt, args);
144
0
  } T_END;
145
146
0
  va_end(args);
147
0
}
148
#define sieve_lexer_error(lexer, ...) \
149
0
  sieve_lexer_error(lexer, __FILE__, __LINE__, __VA_ARGS__)
150
151
inline static void ATTR_FORMAT(4, 5)
152
sieve_lexer_warning(const struct sieve_lexer *lexer,
153
        const char *csrc_filename, unsigned int csrc_linenum,
154
        const char *fmt, ...)
155
0
{
156
0
  struct sieve_lexical_scanner *scanner = lexer->scanner;
157
0
  struct sieve_error_params params = {
158
0
    .log_type = LOG_TYPE_WARNING,
159
0
    .csrc = {
160
0
      .filename = csrc_filename,
161
0
      .linenum = csrc_linenum,
162
0
    },
163
0
  };
164
0
  va_list args;
165
166
0
  va_start(args, fmt);
167
168
0
  T_BEGIN {
169
0
    params.location =
170
0
      sieve_error_script_location(scanner->script,
171
0
                scanner->current_line);
172
0
    sieve_logv(scanner->ehandler, &params, fmt, args);
173
0
  } T_END;
174
175
0
  va_end(args);
176
0
}
177
#define sieve_lexer_warning(lexer, ...) \
178
0
  sieve_lexer_warning(lexer, __FILE__, __LINE__, __VA_ARGS__)
179
180
const char *sieve_lexer_token_description(const struct sieve_lexer *lexer)
181
0
{
182
0
  switch (lexer->token_type) {
183
0
  case STT_NONE:
184
0
    return "no token (bug)";
185
0
  case STT_WHITESPACE:
186
0
    return "whitespace (bug)";
187
0
  case STT_EOF:
188
0
    return "end of file";
189
190
0
  case STT_NUMBER:
191
0
    return "number";
192
0
  case STT_IDENTIFIER:
193
0
    return "identifier";
194
0
  case STT_TAG:
195
0
    return "tag";
196
0
  case STT_STRING:
197
0
    return "string";
198
199
0
  case STT_RBRACKET:
200
0
    return "')'";
201
0
  case STT_LBRACKET:
202
0
    return "'('";
203
0
  case STT_RCURLY:
204
0
    return "'}'";
205
0
  case STT_LCURLY:
206
0
    return "'{'";
207
0
  case STT_RSQUARE:
208
0
    return "']'";
209
0
  case STT_LSQUARE:
210
0
    return "'['";
211
0
  case STT_SEMICOLON:
212
0
    return "';'";
213
0
  case STT_COMMA:
214
0
    return "','";
215
216
0
  case STT_SLASH:
217
0
    return "'/'";
218
0
  case STT_COLON:
219
0
    return "':'";
220
221
0
  case STT_GARBAGE:
222
0
    return "unknown characters";
223
0
  case STT_ERROR:
224
0
    return "error token (bug)";
225
0
  }
226
227
0
  return "unknown token (bug)";
228
0
}
229
230
/*
231
 * Debug
232
 */
233
234
void sieve_lexer_token_print(const struct sieve_lexer *lexer)
235
0
{
236
0
  switch (lexer->token_type) {
237
0
  case STT_NONE:
238
0
    printf("??NONE?? ");
239
0
    break;
240
0
  case STT_WHITESPACE:
241
0
    printf("??WHITESPACE?? ");
242
0
    break;
243
0
  case STT_EOF:
244
0
    printf("EOF\n");
245
0
    break;
246
247
0
  case STT_NUMBER:
248
0
    printf("NUMBER ");
249
0
    break;
250
0
  case STT_IDENTIFIER:
251
0
    printf("IDENTIFIER ");
252
0
    break;
253
0
  case STT_TAG:
254
0
    printf("TAG ");
255
0
    break;
256
0
  case STT_STRING:
257
0
    printf("STRING ");
258
0
    break;
259
260
0
  case STT_RBRACKET:
261
0
    printf(") ");
262
0
    break;
263
0
  case STT_LBRACKET:
264
0
    printf("( ");
265
0
    break;
266
0
  case STT_RCURLY:
267
0
    printf("}\n");
268
0
    break;
269
0
  case STT_LCURLY:
270
0
    printf("{\n");
271
0
    break;
272
0
  case STT_RSQUARE:
273
0
    printf("] ");
274
0
    break;
275
0
  case STT_LSQUARE:
276
0
    printf("[ ");
277
0
    break;
278
0
  case STT_SEMICOLON:
279
0
    printf(";\n");
280
0
    break;
281
0
  case STT_COMMA:
282
0
    printf(", ");
283
0
    break;
284
285
0
  case STT_SLASH:
286
0
    printf("/ ");
287
0
    break;
288
0
  case STT_COLON:
289
0
    printf(": ");
290
0
    break;
291
292
0
  case STT_GARBAGE:
293
0
    printf(">>GARBAGE<<");
294
0
    break;
295
0
  case STT_ERROR:
296
0
    printf(">>ERROR<<");
297
0
    break;
298
0
  default:
299
0
    printf("UNKNOWN ");
300
0
    break;
301
0
  }
302
0
}
303
304
/*
305
 * Lexical scanning
306
 */
307
308
static void sieve_lexer_shift(struct sieve_lexical_scanner *scanner)
309
0
{
310
0
  if (scanner->buffer_size > 0 &&
311
0
      scanner->buffer[scanner->buffer_pos] == '\n')
312
0
    scanner->current_line++;
313
314
0
  if (scanner->buffer_size > 0 &&
315
0
      scanner->buffer_pos + 1 < scanner->buffer_size)
316
0
    scanner->buffer_pos++;
317
0
  else {
318
0
    if (scanner->buffer_size > 0)
319
0
      i_stream_skip(scanner->input, scanner->buffer_size);
320
321
0
    scanner->buffer = i_stream_get_data(scanner->input,
322
0
                &scanner->buffer_size);
323
324
0
    if (scanner->buffer_size == 0 &&
325
0
        i_stream_read(scanner->input) > 0) {
326
0
        scanner->buffer = i_stream_get_data(
327
0
        scanner->input, &scanner->buffer_size);
328
0
    }
329
330
0
    scanner->buffer_pos = 0;
331
0
  }
332
0
}
333
334
static inline int sieve_lexer_curchar(struct sieve_lexical_scanner *scanner)
335
0
{
336
0
  if (scanner->buffer_size == 0)
337
0
    return -1;
338
339
0
  return scanner->buffer[scanner->buffer_pos];
340
0
}
341
342
static inline const char *_char_sanitize(int ch)
343
0
{
344
0
  if (ch > 31 && ch < 127)
345
0
    return t_strdup_printf("'%c'", ch);
346
347
0
  return t_strdup_printf("0x%02x", ch);
348
0
}
349
350
static bool sieve_lexer_scan_number(struct sieve_lexical_scanner *scanner)
351
0
{
352
0
  struct sieve_lexer *lexer = &scanner->lexer;
353
0
  uintmax_t value;
354
0
  string_t *str;
355
0
  bool overflow = FALSE;
356
357
0
  str_truncate(lexer->token_str_value,0);
358
0
  str = lexer->token_str_value;
359
360
0
  while (i_isdigit(sieve_lexer_curchar(scanner))) {
361
0
    str_append_c(str, sieve_lexer_curchar(scanner));
362
0
    sieve_lexer_shift(scanner);
363
0
  }
364
365
0
  if (str_to_uintmax(str_c(str), &value) < 0 ||
366
0
      value > (sieve_number_t)-1) {
367
0
    overflow = TRUE;
368
0
  } else {
369
0
    switch (sieve_lexer_curchar(scanner)) {
370
0
    case 'k':
371
0
    case 'K': /* Kilo */
372
0
      if (value > (SIEVE_MAX_NUMBER >> 10))
373
0
        overflow = TRUE;
374
0
      else
375
0
        value = value << 10;
376
0
      sieve_lexer_shift(scanner);
377
0
      break;
378
0
    case 'm':
379
0
    case 'M': /* Mega */
380
0
      if (value > (SIEVE_MAX_NUMBER >> 20))
381
0
        overflow = TRUE;
382
0
      else
383
0
        value = value << 20;
384
0
      sieve_lexer_shift(scanner);
385
0
      break;
386
0
    case 'g':
387
0
    case 'G': /* Giga */
388
0
      if (value > (SIEVE_MAX_NUMBER >> 30))
389
0
        overflow = TRUE;
390
0
      else
391
0
        value = value << 30;
392
0
      sieve_lexer_shift(scanner);
393
0
      break;
394
0
    default:
395
      /* Next token */
396
0
      break;
397
0
    }
398
0
  }
399
400
  /* Check for integer overflow */
401
0
  if (overflow) {
402
0
    sieve_lexer_error(lexer,
403
0
          "number exceeds integer limits (max %llu)",
404
0
          (long long) SIEVE_MAX_NUMBER);
405
0
    lexer->token_type = STT_ERROR;
406
0
    return FALSE;
407
0
  }
408
409
0
  lexer->token_type = STT_NUMBER;
410
0
  lexer->token_int_value = (sieve_number_t)value;
411
0
  return TRUE;
412
413
0
}
414
415
static bool
416
sieve_lexer_scan_hash_comment(struct sieve_lexical_scanner *scanner)
417
0
{
418
0
  struct sieve_lexer *lexer = &scanner->lexer;
419
420
0
  while (sieve_lexer_curchar(scanner) != '\n') {
421
0
    switch(sieve_lexer_curchar(scanner)) {
422
0
    case -1:
423
0
      if (!scanner->input->eof) {
424
0
        lexer->token_type = STT_ERROR;
425
0
        return FALSE;
426
0
      }
427
0
      sieve_lexer_warning(lexer,
428
0
        "no newline (CRLF) at end of hash comment at end of file");
429
0
      lexer->token_type = STT_WHITESPACE;
430
0
      return TRUE;
431
0
    case '\0':
432
0
      sieve_lexer_error(lexer,
433
0
            "encountered NUL character in hash comment");
434
0
      lexer->token_type = STT_ERROR;
435
0
      return FALSE;
436
0
    default:
437
0
      break;
438
0
    }
439
440
    /* Stray CR is ignored */
441
0
    sieve_lexer_shift(scanner);
442
0
  }
443
444
0
  sieve_lexer_shift(scanner);
445
446
0
  lexer->token_type = STT_WHITESPACE;
447
0
  return TRUE;
448
0
}
449
450
/* sieve_lexer_scan_raw_token:
451
 *   Scans valid tokens and whitespace
452
 */
453
static bool
454
sieve_lexer_scan_raw_token(struct sieve_lexical_scanner *scanner)
455
0
{
456
0
  struct sieve_lexer *lexer = &scanner->lexer;
457
0
  string_t *str;
458
0
  int ret;
459
460
  /* Read first character */
461
0
  if (lexer->token_type == STT_NONE) {
462
0
    if ((ret = i_stream_read(scanner->input)) < 0) {
463
0
      i_assert(ret != -2);
464
0
      if (!scanner->input->eof) {
465
0
        lexer->token_type = STT_ERROR;
466
0
        return FALSE;
467
0
      }
468
0
    }
469
0
    sieve_lexer_shift(scanner);
470
0
  }
471
472
0
  lexer->token_line = scanner->current_line;
473
474
0
  switch (sieve_lexer_curchar(scanner)) {
475
476
  /* whitespace */
477
478
  // hash-comment = ( "#" *CHAR-NOT-CRLF CRLF )
479
0
  case '#':
480
0
    sieve_lexer_shift(scanner);
481
0
    return sieve_lexer_scan_hash_comment(scanner);
482
483
  // bracket-comment = "/*" *(CHAR-NOT-STAR / ("*" CHAR-NOT-SLASH)) "*/"
484
  //        ;; No */ allowed inside a comment.
485
  //        ;; (No * is allowed unless it is the last character,
486
  //        ;; or unless it is followed by a character that isn't a
487
  //        ;; slash.)
488
0
  case '/':
489
0
    sieve_lexer_shift(scanner);
490
491
0
    if (sieve_lexer_curchar(scanner) == '*') {
492
0
      sieve_lexer_shift(scanner);
493
494
0
      while (TRUE) {
495
0
        switch (sieve_lexer_curchar(scanner)) {
496
0
        case -1:
497
0
          if (scanner->input->eof) {
498
0
            sieve_lexer_error(lexer,
499
0
              "end of file before end of bracket comment "
500
0
              "('/* ... */') "
501
0
              "started at line %d",
502
0
              lexer->token_line);
503
0
          }
504
0
          lexer->token_type = STT_ERROR;
505
0
          return FALSE;
506
0
        case '*':
507
0
          sieve_lexer_shift(scanner);
508
509
0
          if (sieve_lexer_curchar(scanner) == '/') {
510
0
            sieve_lexer_shift(scanner);
511
512
0
            lexer->token_type = STT_WHITESPACE;
513
0
            return TRUE;
514
515
0
          } else if (sieve_lexer_curchar(scanner) == -1) {
516
0
            sieve_lexer_error(lexer,
517
0
              "end of file before end of bracket comment "
518
0
              "('/* ... */') "
519
0
              "started at line %d",
520
0
              lexer->token_line);
521
0
            lexer->token_type = STT_ERROR;
522
0
            return FALSE;
523
0
          }
524
0
          break;
525
0
        case '\0':
526
0
          sieve_lexer_error(lexer,
527
0
            "encountered NUL character in bracket comment");
528
0
          lexer->token_type = STT_ERROR;
529
0
          return FALSE;
530
0
        default:
531
0
          sieve_lexer_shift(scanner);
532
0
        }
533
0
      }
534
535
0
      i_unreached();
536
0
    }
537
538
0
    lexer->token_type = STT_SLASH;
539
0
    return TRUE;
540
541
  // comment = bracket-comment / hash-comment
542
  // white-space = 1*(SP / CRLF / HTAB) / comment
543
0
  case '\t':
544
0
  case '\r':
545
0
  case '\n':
546
0
  case ' ':
547
0
    sieve_lexer_shift(scanner);
548
549
0
    while (sieve_lexer_curchar(scanner) == '\t' ||
550
0
           sieve_lexer_curchar(scanner) == '\r' ||
551
0
           sieve_lexer_curchar(scanner) == '\n' ||
552
0
           sieve_lexer_curchar(scanner) == ' ') {
553
554
0
      sieve_lexer_shift(scanner);
555
0
    }
556
557
0
    lexer->token_type = STT_WHITESPACE;
558
0
    return TRUE;
559
560
  /* quoted-string */
561
0
  case '"':
562
0
    sieve_lexer_shift(scanner);
563
564
0
    str_truncate(lexer->token_str_value, 0);
565
0
    str = lexer->token_str_value;
566
567
0
    while (sieve_lexer_curchar(scanner) != '"') {
568
0
      if (sieve_lexer_curchar(scanner) == '\\')
569
0
        sieve_lexer_shift(scanner);
570
571
0
      switch (sieve_lexer_curchar(scanner)) {
572
573
      /* End of file */
574
0
      case -1:
575
0
        if (scanner->input->eof) {
576
0
          sieve_lexer_error(lexer,
577
0
            "end of file before end of quoted string "
578
0
            "started at line %d", lexer->token_line);
579
0
        }
580
0
        lexer->token_type = STT_ERROR;
581
0
        return FALSE;
582
583
      /* NUL character */
584
0
      case '\0':
585
0
        sieve_lexer_error(lexer,
586
0
          "encountered NUL character in quoted string "
587
0
          "started at line %d", lexer->token_line);
588
0
        lexer->token_type = STT_ERROR;
589
0
        return FALSE;
590
591
      /* CR .. check for LF */
592
0
      case '\r':
593
0
        sieve_lexer_shift(scanner);
594
595
0
        if (sieve_lexer_curchar(scanner) != '\n') {
596
0
          sieve_lexer_error(lexer,
597
0
            "found stray carriage-return (CR) character "
598
0
            "in quoted string started at line %d",
599
0
            lexer->token_line);
600
0
          lexer->token_type = STT_ERROR;
601
0
          return FALSE;
602
0
        }
603
604
0
        if (str_len(str) <= SIEVE_MAX_STRING_LEN)
605
0
          str_append(str, "\r\n");
606
0
        break;
607
608
      /* Loose LF is allowed (non-standard) and converted to CRLF */
609
0
      case '\n':
610
0
        if (str_len(str) <= SIEVE_MAX_STRING_LEN)
611
0
          str_append(str, "\r\n");
612
0
        break;
613
614
      /* Other characters */
615
0
      default:
616
0
        if (str_len(str) <= SIEVE_MAX_STRING_LEN)
617
0
          str_append_c(str, sieve_lexer_curchar(scanner));
618
0
      }
619
620
0
      sieve_lexer_shift(scanner);
621
0
    }
622
623
0
    sieve_lexer_shift(scanner);
624
625
0
    if (str_len(str) > SIEVE_MAX_STRING_LEN) {
626
0
      sieve_lexer_error(lexer,
627
0
        "quoted string started at line %d is too long "
628
0
        "(longer than %llu bytes)", lexer->token_line,
629
0
        (long long) SIEVE_MAX_STRING_LEN);
630
0
      lexer->token_type = STT_ERROR;
631
0
      return FALSE;
632
0
    }
633
634
0
    lexer->token_type = STT_STRING;
635
0
    return TRUE;
636
637
  /* single character tokens */
638
0
  case ']':
639
0
    sieve_lexer_shift(scanner);
640
0
    lexer->token_type = STT_RSQUARE;
641
0
    return TRUE;
642
0
  case '[':
643
0
    sieve_lexer_shift(scanner);
644
0
    lexer->token_type = STT_LSQUARE;
645
0
    return TRUE;
646
0
  case '}':
647
0
    sieve_lexer_shift(scanner);
648
0
    lexer->token_type = STT_RCURLY;
649
0
    return TRUE;
650
0
  case '{':
651
0
    sieve_lexer_shift(scanner);
652
0
    lexer->token_type = STT_LCURLY;
653
0
    return TRUE;
654
0
  case ')':
655
0
    sieve_lexer_shift(scanner);
656
0
    lexer->token_type = STT_RBRACKET;
657
0
    return TRUE;
658
0
  case '(':
659
0
    sieve_lexer_shift(scanner);
660
0
    lexer->token_type = STT_LBRACKET;
661
0
    return TRUE;
662
0
  case ';':
663
0
    sieve_lexer_shift(scanner);
664
0
    lexer->token_type = STT_SEMICOLON;
665
0
    return TRUE;
666
0
  case ',':
667
0
    sieve_lexer_shift(scanner);
668
0
    lexer->token_type = STT_COMMA;
669
0
    return TRUE;
670
671
  /* EOF */
672
0
  case -1:
673
0
    if (!scanner->input->eof) {
674
0
      lexer->token_type = STT_ERROR;
675
0
      return FALSE;
676
0
    }
677
0
    lexer->token_type = STT_EOF;
678
0
    return TRUE;
679
680
0
  default:
681
    /* number */
682
0
    if (i_isdigit(sieve_lexer_curchar(scanner))) {
683
0
      return sieve_lexer_scan_number(scanner);
684
685
    /* identifier / tag */
686
0
    } else if (i_isalpha(sieve_lexer_curchar(scanner)) ||
687
0
         sieve_lexer_curchar(scanner) == '_' ||
688
0
         sieve_lexer_curchar(scanner) == ':') {
689
690
0
      enum sieve_token_type type = STT_IDENTIFIER;
691
0
      str_truncate(lexer->token_str_value,0);
692
0
      str = lexer->token_str_value;
693
694
      /* If it starts with a ':' it is a tag and not an
695
         identifier */
696
0
      if (sieve_lexer_curchar(scanner) == ':') {
697
0
        sieve_lexer_shift(scanner); // discard colon
698
0
        type = STT_TAG;
699
700
        /* First character still can't be a DIGIT */
701
0
        if (i_isalpha(sieve_lexer_curchar(scanner)) ||
702
0
            sieve_lexer_curchar(scanner) == '_') {
703
0
          str_append_c(str, sieve_lexer_curchar(scanner));
704
0
          sieve_lexer_shift(scanner);
705
0
        } else {
706
          /* Hmm, otherwise it is just a spurious
707
             colon */
708
0
          lexer->token_type = STT_COLON;
709
0
          return TRUE;
710
0
        }
711
0
      } else {
712
0
        str_append_c(str, sieve_lexer_curchar(scanner));
713
0
        sieve_lexer_shift(scanner);
714
0
      }
715
716
      /* Scan the rest of the identifier */
717
0
      while (i_isalnum(sieve_lexer_curchar(scanner)) ||
718
0
             sieve_lexer_curchar(scanner) == '_') {
719
720
0
        if (str_len(str) <= SIEVE_MAX_IDENTIFIER_LEN) {
721
0
          str_append_c(str, sieve_lexer_curchar(scanner));
722
0
        }
723
0
        sieve_lexer_shift(scanner);
724
0
      }
725
726
      /* Is this in fact a multiline text string ? */
727
0
      if (sieve_lexer_curchar(scanner) == ':' &&
728
0
          type == STT_IDENTIFIER &&
729
0
          str_len(str) == 4 &&
730
0
          str_begins_icase_with(str_c(str), "text")) {
731
0
        sieve_lexer_shift(scanner); // discard colon
732
733
        /* Discard SP and HTAB whitespace */
734
0
        while (sieve_lexer_curchar(scanner) == ' ' ||
735
0
               sieve_lexer_curchar(scanner) == '\t')
736
0
          sieve_lexer_shift(scanner);
737
738
        /* Discard hash comment or handle single CRLF */
739
0
        if (sieve_lexer_curchar(scanner) == '\r')
740
0
          sieve_lexer_shift(scanner);
741
0
        switch (sieve_lexer_curchar(scanner)) {
742
0
        case '#':
743
0
          if (!sieve_lexer_scan_hash_comment(scanner))
744
0
            return FALSE;
745
0
          if (scanner->input->eof) {
746
0
            sieve_lexer_error(lexer,
747
0
              "end of file before end of multi-line string");
748
0
            lexer->token_type = STT_ERROR;
749
0
            return FALSE;
750
0
          } else if (scanner->input->stream_errno != 0) {
751
0
            lexer->token_type = STT_ERROR;
752
0
            return FALSE;
753
0
          }
754
0
          break;
755
0
        case '\n':
756
0
          sieve_lexer_shift(scanner);
757
0
          break;
758
0
        case -1:
759
0
          if (scanner->input->eof) {
760
0
            sieve_lexer_error(lexer,
761
0
              "end of file before end of multi-line string");
762
0
          }
763
0
          lexer->token_type = STT_ERROR;
764
0
          return FALSE;
765
0
        default:
766
0
          sieve_lexer_error(lexer,
767
0
            "invalid character %s after 'text:' in multiline string",
768
0
            _char_sanitize(sieve_lexer_curchar(scanner)));
769
0
          lexer->token_type = STT_ERROR;
770
0
          return FALSE;
771
0
        }
772
773
        /* Start over */
774
0
        str_truncate(str, 0);
775
776
        /* Parse literal lines */
777
0
        while (TRUE) {
778
0
          bool cr_shifted = FALSE;
779
780
          /* Remove dot-stuffing or detect end of text */
781
0
          if (sieve_lexer_curchar(scanner) == '.') {
782
0
            sieve_lexer_shift(scanner);
783
784
            /* Check for CR.. */
785
0
            if (sieve_lexer_curchar(scanner) == '\r') {
786
0
              sieve_lexer_shift(scanner);
787
0
              cr_shifted = TRUE;
788
0
            }
789
790
            /* ..LF */
791
0
            if (sieve_lexer_curchar(scanner) == '\n') {
792
0
              sieve_lexer_shift(scanner);
793
794
              /* End of multi-line string */
795
796
              /* Check whether length limit was violated */
797
0
              if (str_len(str) > SIEVE_MAX_STRING_LEN) {
798
0
                sieve_lexer_error(lexer,
799
0
                  "multi-line string started at line %d is too long "
800
0
                  "(longer than %llu bytes)", lexer->token_line,
801
0
                  (long long) SIEVE_MAX_STRING_LEN);
802
0
                  lexer->token_type = STT_ERROR;
803
0
                  return FALSE;
804
0
              }
805
806
0
              lexer->token_type = STT_STRING;
807
0
              return TRUE;
808
0
            } else if (cr_shifted) {
809
              /* Seen CR, but no LF */
810
0
              if (sieve_lexer_curchar(scanner) != -1 ||
811
0
                  !scanner->input->eof) {
812
0
                sieve_lexer_error(lexer,
813
0
                  "found stray carriage-return (CR) character "
814
0
                  "in multi-line string started at line %d",
815
0
                  lexer->token_line);
816
0
              }
817
0
              lexer->token_type = STT_ERROR;
818
0
              return FALSE;
819
0
            }
820
821
            /* Handle dot-stuffing */
822
0
            if (str_len(str) <= SIEVE_MAX_STRING_LEN)
823
0
              str_append_c(str, '.');
824
0
            if (sieve_lexer_curchar(scanner) == '.')
825
0
              sieve_lexer_shift(scanner);
826
0
          }
827
828
          /* Scan the rest of the line */
829
0
          while (sieve_lexer_curchar(scanner) != '\n' &&
830
0
                 sieve_lexer_curchar(scanner) != '\r') {
831
832
0
            switch (sieve_lexer_curchar(scanner)) {
833
0
            case -1:
834
0
              if (scanner->input->eof) {
835
0
                sieve_lexer_error(lexer,
836
0
                  "end of file before end of multi-line string");
837
0
              }
838
0
              lexer->token_type = STT_ERROR;
839
0
              return FALSE;
840
0
            case '\0':
841
0
              sieve_lexer_error(lexer,
842
0
                "encountered NUL character in quoted string "
843
0
                "started at line %d", lexer->token_line);
844
0
              lexer->token_type = STT_ERROR;
845
0
              return FALSE;
846
0
            default:
847
0
              if (str_len(str) <= SIEVE_MAX_STRING_LEN)
848
0
                  str_append_c(str, sieve_lexer_curchar(scanner));
849
0
            }
850
851
0
            sieve_lexer_shift(scanner);
852
0
          }
853
854
          /* If exited loop due to CR, skip it */
855
0
          if (sieve_lexer_curchar(scanner) == '\r')
856
0
            sieve_lexer_shift(scanner);
857
858
          /* Now we must see an LF */
859
0
          if (sieve_lexer_curchar(scanner) != '\n') {
860
0
            if (sieve_lexer_curchar(scanner) != -1 ||
861
0
                !scanner->input->eof) {
862
0
              sieve_lexer_error(lexer,
863
0
                "found stray carriage-return (CR) character "
864
0
                "in multi-line string started at line %d",
865
0
                lexer->token_line);
866
0
            }
867
0
            lexer->token_type = STT_ERROR;
868
0
            return FALSE;
869
0
          }
870
871
0
          if (str_len(str) <= SIEVE_MAX_STRING_LEN)
872
0
            str_append(str, "\r\n");
873
874
0
          sieve_lexer_shift(scanner);
875
0
        }
876
877
0
        i_unreached();
878
0
      }
879
880
0
      if (str_len(str) > SIEVE_MAX_IDENTIFIER_LEN) {
881
0
        sieve_lexer_error(lexer,
882
0
          "encountered impossibly long %s%s'",
883
0
          (type == STT_TAG ? "tag identifier ':" :
884
0
           "identifier '"),
885
0
          str_sanitize(str_c(str),
886
0
                 SIEVE_MAX_IDENTIFIER_LEN));
887
0
        lexer->token_type = STT_ERROR;
888
0
        return FALSE;
889
0
      }
890
891
0
      lexer->token_type = type;
892
0
      return TRUE;
893
0
    }
894
895
    /* Error (unknown character and EOF handled already) */
896
0
    if (lexer->token_type != STT_GARBAGE) {
897
0
      sieve_lexer_error(lexer,
898
0
        "unexpected character(s) starting with %s",
899
0
        _char_sanitize(sieve_lexer_curchar(scanner)));
900
0
    }
901
0
    sieve_lexer_shift(scanner);
902
0
    lexer->token_type = STT_GARBAGE;
903
0
    return FALSE;
904
0
  }
905
0
}
906
907
void sieve_lexer_skip_token(const struct sieve_lexer *lexer)
908
0
{
909
  /* Scan token while skipping whitespace */
910
0
  do {
911
0
    struct sieve_lexical_scanner *scanner = lexer->scanner;
912
913
0
    if (!sieve_lexer_scan_raw_token(scanner)) {
914
0
      if (!scanner->input->eof &&
915
0
          scanner->input->stream_errno != 0) {
916
0
        sieve_critical(scanner->svinst, scanner->ehandler,
917
0
          sieve_error_script_location(scanner->script,
918
0
                    scanner->current_line),
919
0
          "error reading script",
920
0
          "error reading script during lexical analysis: %s",
921
0
          i_stream_get_error(scanner->input));
922
0
      }
923
0
      return;
924
0
    }
925
0
  } while (lexer->token_type == STT_WHITESPACE);
926
0
}
927