data/SieveScript.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QByteArray> | ||
| 4 | #include <QList> | ||
| 5 | #include <QString> | ||
| 6 | #include <QStringList> | ||
| 7 | |||
| 8 | // Lossless Sieve (RFC 5228) syntax tree (Sprint 95 S95.1.6, SIEVE-PARSE). | ||
| 9 | // | ||
| 10 | // The parser works on the raw UTF-8 bytes of a script and records a byte | ||
| 11 | // span for every command, test, argument and string. The tree never owns a | ||
| 12 | // rewritten copy of the text: serialize() concatenates the original bytes, | ||
| 13 | // so serialize(parse(x)) == x for every input, valid or not (gate G1). | ||
| 14 | // Writers (SieveRuleCodec) patch the script by replacing whole spans and | ||
| 15 | // copy every untouched byte. | ||
| 16 | // | ||
| 17 | // Invariants: | ||
| 18 | // - Spans are half-open byte ranges [begin, end) into Script::source. | ||
| 19 | // - Identifiers and tags are stored lower case (Sieve is case-insensitive | ||
| 20 | // for them); string values keep their case. | ||
| 21 | // - A script that fails to parse keeps its source and every top-level | ||
| 22 | // command that ended before the error; `error` names line and column. | ||
| 23 | // - Limits (size, nesting depth, command count) turn hostile input into a | ||
| 24 | // parse error instead of a hang or deep recursion (SR-11). | ||
| 25 | namespace Sieve { | ||
| 26 | |||
| 27 | struct Span { | ||
| 28 | int begin = 0; | ||
| 29 | int end = 0; | ||
| 30 | 72872 | int length() const { return end - begin; } | |
| 31 | bool isEmpty() const { return end <= begin; } | ||
| 32 | }; | ||
| 33 | |||
| 34 | struct StringValue { | ||
| 35 | QString value; // decoded: escapes removed, dot-unstuffed | ||
| 36 | Span span; // raw bytes incl. quotes or "text:" ... "." | ||
| 37 | bool multiLine = false; // written as "text:" multi-line string | ||
| 38 | }; | ||
| 39 | |||
| 40 | struct Argument { | ||
| 41 | enum class Kind { StringList, Number, Tag }; | ||
| 42 | Kind kind = Kind::Tag; | ||
| 43 | Span span; | ||
| 44 | QList<StringValue> strings; // StringList only | ||
| 45 | bool bracketed = false; // StringList written as [ ... ] | ||
| 46 | quint64 number = 0; // Number only, quantifier applied | ||
| 47 | QString text; // Tag: lower case incl. ':'; Number: raw text | ||
| 48 | |||
| 49 | QStringList values() const; | ||
| 50 | }; | ||
| 51 | |||
| 52 | struct Test { | ||
| 53 | QString name; // lower case | ||
| 54 | Span span; | ||
| 55 | int line = 0; | ||
| 56 | QList<Argument> arguments; | ||
| 57 | QList<Test> tests; // operands of allof/anyof/not | ||
| 58 | bool testList = false; // operands written as "( ... )" | ||
| 59 | }; | ||
| 60 | |||
| 61 | struct Command { | ||
| 62 | QString name; // lower case | ||
| 63 | Span span; // identifier .. ";" or "}" | ||
| 64 | int line = 0; | ||
| 65 | QList<Argument> arguments; | ||
| 66 | QList<Test> tests; | ||
| 67 | bool testList = false; | ||
| 68 | bool hasBlock = false; | ||
| 69 | Span blockSpan; // "{" .. "}" | ||
| 70 | QList<Command> block; | ||
| 71 | }; | ||
| 72 | |||
| 73 | struct ParseError { | ||
| 74 | QString message; // English, technical; UI maps it to plain words | ||
| 75 | int offset = -1; | ||
| 76 | int line = 0; // 1-based | ||
| 77 | int column = 0; // 1-based, in characters | ||
| 78 | 1905204 | bool isNull() const { return offset < 0; } | |
| 79 | }; | ||
| 80 | |||
| 81 | struct Limits { | ||
| 82 | int maxBytes = 1024 * 1024; | ||
| 83 | int maxDepth = 64; | ||
| 84 | int maxCommands = 10000; | ||
| 85 | }; | ||
| 86 | |||
| 87 | struct Script { | ||
| 88 | QByteArray source; | ||
| 89 | QList<Command> commands; // top level, in source order | ||
| 90 | ParseError error; | ||
| 91 | 7277 | bool isValid() const { return error.isNull(); } | |
| 92 | }; | ||
| 93 | |||
| 94 | Script parse(const QByteArray &source, const Limits &limits = {}); | ||
| 95 | |||
| 96 | // Concatenates the gaps and spans of the top-level commands and the tail; | ||
| 97 | // byte-identical to the parsed source. | ||
| 98 | QByteArray serialize(const Script &script); | ||
| 99 | |||
| 100 | // 1-based line and character column of a byte offset. | ||
| 101 | int lineOfOffset(const QByteArray &source, int offset); | ||
| 102 | int columnOfOffset(const QByteArray &source, int offset); | ||
| 103 | |||
| 104 | // Writes `value` as a quoted string ("..." with \" and \\ escaped), or as a | ||
| 105 | // multi-line "text:" string when it ends with a line break. `lineEnd` is the | ||
| 106 | // line terminator written for every line break of the value. | ||
| 107 | // Invariant (S106-091): parsing the result yields `value` again, with each | ||
| 108 | // line break written as `lineEnd`; a value with an inner line break but no | ||
| 109 | // trailing one stays a quoted string (a "text:" string always ends with | ||
| 110 | // one). S106-092: no Sieve string can hold NUL or a lone CR (RFC 5228 | ||
| 111 | // §8.1) — a lone CR is written as a line break and NUL is dropped. | ||
| 112 | QByteArray stringLiteral(const QString &value, const QByteArray &lineEnd = "\n"); | ||
| 113 | // A string list: a single string as is, several as [ "a", "b" ]. | ||
| 114 | QByteArray stringListLiteral(const QStringList &values, const QByteArray &lineEnd = "\n"); | ||
| 115 | // True when stringLiteral() writes `value` unchanged: no NUL and no CR | ||
| 116 | // outside a CRLF pair. | ||
| 117 | bool representableString(const QString &value); | ||
| 118 | |||
| 119 | // Parses one test expression (e.g. the commented-out test of Roundcube's | ||
| 120 | // disabled rule form "if false # <test>"). Returns false on any error or | ||
| 121 | // trailing content. | ||
| 122 | bool parseTestExpression(const QByteArray &text, Test *test); | ||
| 123 | |||
| 124 | } // namespace Sieve | ||
| 125 |