From 2786d0d95930efd9b5dbcbc81fcd1fa42f241b69 Mon Sep 17 00:00:00 2001 From: djmaze Date: Thu, 14 Jan 2021 12:45:51 +0100 Subject: [PATCH] Not perfect but it can parse the rainloop script to a tree and back to string --- dev/Sieve/Commands.js | 15 +++-- dev/Sieve/Grammar.js | 60 +++++++++++++------ dev/Sieve/Parser.js | 136 +++++++++++++++++++++--------------------- dev/Sieve/RegEx.js | 52 +++++++++------- dev/sieve.js | 5 +- 5 files changed, 155 insertions(+), 113 deletions(-) diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js index 8555b33dc..38457b8bf 100644 --- a/dev/Sieve/Commands.js +++ b/dev/Sieve/Commands.js @@ -24,7 +24,10 @@ class Conditional { return this.identifier + ('else' !== this.identifier ? ' ' + this.test : '') - + " {\r\n\t" + this.commands.join(";\r\n\t") + "\r\n}"; + + (this.commands.length + ? ' {\r\n\t' + Sieve.arrayToString(this.commands, ';\r\n\t') + ';\r\n}' + : ';' + ); } /* public function pushArguments(array $args): void @@ -38,7 +41,7 @@ class Conditional /** * https://tools.ietf.org/html/rfc5228#section-3.2 */ -class Requires /* extends Array*/ +class Require /* extends Array*/ { constructor() { @@ -47,7 +50,7 @@ class Requires /* extends Array*/ toString() { - return 'require ' + this.capabilities; + return 'require ' + this.capabilities.toString(); } pushArguments(args) @@ -86,7 +89,7 @@ class FileInto toString() { - return 'fileinto ' + this.mailbox; + return 'fileinto ' + this._mailbox; } get mailbox() @@ -159,11 +162,11 @@ class Discard Sieve.Commands = { // Control commands Conditional: Conditional, - Requires: Requires, + Require: Require, Stop: Stop, // Action commands Discard: Discard, - FileInto: FileInto, + Fileinto: FileInto, Keep: Keep, Redirect: Redirect }; diff --git a/dev/Sieve/Grammar.js b/dev/Sieve/Grammar.js index 308b4c99d..f31bc890b 100644 --- a/dev/Sieve/Grammar.js +++ b/dev/Sieve/Grammar.js @@ -4,6 +4,8 @@ (Sieve => { +const RegEx = Sieve.RegEx; + /** * abstract */ @@ -14,7 +16,10 @@ class StringType /*extends String*/ this._value = value; } -// abstract function toString(); + toString() + { + return this._value; + } get value() { @@ -57,9 +62,7 @@ class Command { let result = this.identifier; if ('anyof' === result || 'allof' === result) { - let tests = []; - this.commands.forEach(cmd => tests.push(cmd.toString().replace(/;$/, ''))); - result += ' (\r\n\t' + tests.join(',\r\n\t') + '\r\n)'; + result += ' (\r\n\t' + Sieve.arrayToString(this.commands, ',\r\n\t') + '\r\n)'; } else { this.arguments.forEach(arg => { if (Array.isArray(arg)) { @@ -69,9 +72,7 @@ class Command } }); if (this.commands) { - result += ' {\r\n'; - this.commands.forEach(cmd => result += '\t' + cmd + '\r\n'); - result += '}'; + result += ' {\r\n\t' + Sieve.arrayToString(this.commands, ';\r\n\t') + '\r\n}'; } else { result += ';'; } @@ -95,11 +96,30 @@ class Command } } +/* +class Commands extends Array +{ + toString() + { + return this.length + ? '{\r\n\t' + Sieve.arrayToString(this, ';\r\n\t') + ';\r\n}' + : ';'; + } + + push(value) + { + if (value instanceof Command) { + super.push(value); + } + } +} +*/ + class BracketComment extends Comment { toString() { - return '/* ' + super() + ' */'; + return '/* ' + super.toString() + ' */'; } } @@ -107,7 +127,7 @@ class HashComment extends Comment { toString() { - return '# ' + super(); + return '# ' + super.toString(); } } @@ -149,20 +169,23 @@ class StringList extends Array if (!(value instanceof StringType)) { value = new QuotedString(value); } - super(value); + super.push(value); } } StringList.fromString = list => { let string, obj = new StringList, - regex = RegExp('(?:^\\s*|\\s*,\\s*)(' + Sieve.RegEx.STRING + ')', 'g'); + regex = RegExp('(?:^\\s*|\\s*,\\s*)(?:"(' + RegEx.QUOTED_TEXT + ')"|text:[ \\t]*(' + + RegEx.HASH_COMMENT + ')?\\r\\n' + + '((?:' + RegEx.MULTILINE_LITERAL + '|' + RegEx.MULTILINE_DOTSTART + ')*)' + + '\\.\\r\\n)', 'gm'); list = list.replace(/^[\r\n\t[]+/, ''); while ((string = regex.exec(list))) { - if (string[4]) { - obj.push(new MultiLine(string[4], string[3])); + if (string[3]) { + obj.push(new MultiLine(string[3], string[2])); } else { - obj.push(new QuotedString(string[2])); + obj.push(new QuotedString(string[1])); } } return obj; @@ -173,6 +196,7 @@ class QuotedString extends StringType toString() { return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"'; +// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"'; } } @@ -211,7 +235,7 @@ class Test toString() { - return (this.identifier + ' ' + this.arguments.join(' ')).trim(); + return (this.identifier + ' ' + Sieve.arrayToString(this.arguments, ' ')).trim(); } pushArguments(args) @@ -247,20 +271,20 @@ class TestList extends Array value = new Command($value); } */ - super(value); + super.push(value); } } TestList.fromString = list => { let test, obj = new TestList, - regex = RegExp('(?:^\\s*|\\s*,\\s*)(' + Sieve.RegEx.IDENTIFIER + ')((?:\\s+' + Sieve.RegEx.ARGUMENT + ')*)', 'g'); + regex = RegExp('(?:^\\s*|\\s*,\\s*)(' + RegEx.IDENTIFIER + ')((?:\\s+' + RegEx.ARGUMENT + ')*)', 'g'); list = list.replace(/^[\r\n\t(]+/, ''); while ((test = regex.exec(list))) { let command = new Sieve.Commands.Command(test[1]); obj.push(command); /* - if (\preg_match_all('@\\s+(' . Sieve.RegEx.ARGUMENT . ')@', $test[2], $args, PREG_SET_ORDER)) { + if (\preg_match_all('@\\s+(' . RegEx.ARGUMENT . ')@', $test[2], $args, PREG_SET_ORDER)) { foreach ($args as $arg) { $command->arguments[] = $arg; } diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js index 7b5eea2ff..a6e7a8c26 100644 --- a/dev/Sieve/Parser.js +++ b/dev/Sieve/Parser.js @@ -7,59 +7,59 @@ const RegEx = Sieve.RegEx, - M = { - UNKNOWN : 0, - STRING_LIST : 1, - QUOTED_STRING : 2, - MULTILINE_STRING : 3, - HASH_COMMENT : 4, - BRACKET_COMMENT : 5, - BLOCK_START : 6, - BLOCK_END : 7, - LEFT_PARENTHESIS : 8, - RIGHT_PARENTHESIS: 9, - COMMA : 10, - SEMICOLON : 11, - TAG : 12, - IDENTIFIER : 13, - NUMBER : 14, - WHITESPACE : 15 - }, + T_UNKNOWN = 0, + T_STRING_LIST = 1, + T_QUOTED_STRING = 2, + T_MULTILINE_STRING = 3, + T_HASH_COMMENT = 4, + T_BRACKET_COMMENT = 5, + T_BLOCK_START = 6, + T_BLOCK_END = 7, + T_LEFT_PARENTHESIS = 8, + T_RIGHT_PARENTHESIS = 9, + T_COMMA = 10, + T_SEMICOLON = 11, + T_TAG = 12, + T_IDENTIFIER = 13, + T_NUMBER = 14, + T_WHITESPACE = 15, - MRegEx = [ - /* M.STRING_LIST */ RegEx.STRING_LIST, - /* M.QUOTED_STRING */ RegEx.QUOTED_STRING, - /* M.MULTILINE_STRING */ RegEx.MULTI_LINE, - /* M.HASH_COMMENT */ RegEx.HASH_COMMENT, - /* M.BRACKET_COMMENT */ RegEx.BRACKET_COMMENT, - /* M.BLOCK_START */ '\\{', - /* M.BLOCK_END */ '\\}', - /* M.LEFT_PARENTHESIS */ '\\(', // anyof / allof - /* M.RIGHT_PARENTHESIS */ '\\)', // anyof / allof - /* M.COMMA */ ',', - /* M.SEMICOLON */ ';', - /* M.TAG */ RegEx.TAG, - /* M.IDENTIFIER */ RegEx.IDENTIFIER, - /* M.NUMBER */ RegEx.NUMBER, - /* M.WHITESPACE */ '(?: |\\r\\n|\\t)+', - /* M.UNKNOWN */ '[^ \\r\\n\\t]+' - ].join(')|('); + TokensRegEx = '(' + [ + /* T_STRING_LIST */ RegEx.STRING_LIST, + /* T_QUOTED_STRING */ RegEx.QUOTED_STRING, + /* T_MULTILINE_STRING */ RegEx.MULTI_LINE, + /* T_HASH_COMMENT */ RegEx.HASH_COMMENT, + /* T_BRACKET_COMMENT */ RegEx.BRACKET_COMMENT, + /* T_BLOCK_START */ '\\{', + /* T_BLOCK_END */ '\\}', + /* T_LEFT_PARENTHESIS */ '\\(', // anyof / allof + /* T_RIGHT_PARENTHESIS */ '\\)', // anyof / allof + /* T_COMMA */ ',', + /* T_SEMICOLON */ ';', + /* T_TAG */ RegEx.TAG, + /* T_IDENTIFIER */ RegEx.IDENTIFIER, + /* T_NUMBER */ RegEx.NUMBER, + /* T_WHITESPACE */ '(?: |\\r\\n|\\t)+', + /* T_UNKNOWN */ '[^ \\r\\n\\t]+' + ].join(')|(') + ')'; Sieve.parseScript = script => { let match, line = 1, tree = [], - // create one regex to find the right match - // avoids looping over all possible tokens: increases performance - regex = RegExp(MRegEx, 'g'), + // Create one regex to find the tokens + // Use exec() to forward since lastIndex + regex = RegExp(TokensRegEx, 'gm'), levels = [], command = null, args = []; const - error = message => { throw new SyntaxError(message + ' at ' + regex.lastIndex, 'script.sieve', line) }, + error = message => { + throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, 'script.sieve', line) + }, pushArgs = () => { if (command && args.length) { command.pushArguments(args); @@ -77,9 +77,11 @@ Sieve.parseScript = script => { // create the part switch (type) { - case M.IDENTIFIER: { + case T_IDENTIFIER: { pushArgs(); - let new_command; + value = value.toLowerCase(); + let new_command, + className = value[0].toUpperCase() + value.substring(1); if ('if' === value) { new_command = new Sieve.Commands.Conditional(value); } else if ('elsif' === value || 'else' === value) { @@ -89,16 +91,16 @@ Sieve.parseScript = script => { } else if ('allof' === value || 'anyof' === value) { (command instanceof Sieve.Commands.Conditional) || error('Test-list not in conditional'); - new_command = new Sieve.Tests[value](); - } else if (Sieve.Tests[value]) { + new_command = new Sieve.Tests[className](); + } else if (Sieve.Tests[className]) { // address / envelope / exists / header / not / size - new_command = new Sieve.Tests[value](); - } else if (Sieve.Commands[value]) { + new_command = new Sieve.Tests[className](); + } else if (Sieve.Commands[className]) { // discard / fileinto / keep / redirect / require / stop - new_command = new Sieve.Commands[value](); - } else if (Sieve.Extensions[value]) { + new_command = new Sieve.Commands[className](); + } else if (Sieve.Extensions[className]) { // body / ereject / reject / imap4flags / vacation - new_command = new Sieve.Extensions[value](); + new_command = new Sieve.Extensions[className](); } else { error('Unknown command ' + value); // new_command = new Sieve.Grammar.Command(value); @@ -122,64 +124,64 @@ Sieve.parseScript = script => { } else { tree.push(new_command); } - if (new_command instanceof Sieve.Commands.Conditional || new_command instanceof Sieve.Commands.TestList) { + if (new_command instanceof Sieve.Commands.Conditional || new_command instanceof Sieve.Grammar.TestList) { levels.push(new_command); } } command = new_command; break; } - case M.TAG: + case T_TAG: command - ? args.push(value) + ? args.push(value.toLowerCase()) : error('Tag must be command argument'); break; - case M.STRING_LIST: + case T_STRING_LIST: command ? args.push(Sieve.Grammar.StringList.fromString(value)) : error('String list must be command argument'); break; - case M.MULTILINE_STRING: + case T_MULTILINE_STRING: command ? args.push(new Sieve.Grammar.MultiLine(value)) : error('Multi-line string must be command argument'); break; - case M.QUOTED_STRING: + case T_QUOTED_STRING: command ? args.push(new Sieve.Grammar.QuotedString(value.substr(1,value.length-2))) : error('Quoted string must be command argument'); break; - case M.NUMBER: + case T_NUMBER: command ? args.push(new Sieve.Grammar.Number(value)) : error('Number must be command argument'); break; - case M.BRACKET_COMMENT: + case T_BRACKET_COMMENT: (command ? command.commands : tree).push( - new Sieve.Grammar.BracketComment(value.substr(1,value.length-4)) + new Sieve.Grammar.BracketComment(value.substr(2, value.length-4)) ); break; - case M.HASH_COMMENT: + case T_HASH_COMMENT: (command ? command.commands : tree).push( new Sieve.Grammar.HashComment(value.substr(1).trim()) ); break; - case M.WHITESPACE: + case T_WHITESPACE: // (command ? command.commands : tree).push(value.trim()); command || tree.push(value.trim()); break; - case M.SEMICOLON: + case T_SEMICOLON: command || error('Semicolon not at end of command'); pushArgs(); // levels.pop(); command = levels.last(); break; - case M.BLOCK_START: + case T_BLOCK_START: // https://tools.ietf.org/html/rfc5228#section-2.9 // Action commands do not take tests or blocks levels.length || error('Block start not part of control command'); @@ -187,28 +189,28 @@ Sieve.parseScript = script => { pushArgs(); command = levels.last(); break; - case M.BLOCK_END: + case T_BLOCK_END: levels.length || error('Block end has no matching block start'); levels.pop(); command = levels.last(); break; // anyof / allof - case M.LEFT_PARENTHESIS: + case T_LEFT_PARENTHESIS: (levels.last() instanceof Sieve.Grammar.TestList) || error('Test start not part of test-list'); command || error('Not inside command'); pushArgs(); command = levels.last(); break; - case M.RIGHT_PARENTHESIS: + case T_RIGHT_PARENTHESIS: (levels.last() instanceof Sieve.Grammar.TestList) || error('Test end not part of test-list'); pushArgs(); levels.pop(); command = levels.last(); break; - case M.COMMA: + case T_COMMA: // Must be inside PARENTHESIS aka test-list (levels.last() instanceof Sieve.Grammar.TestList) || error('Comma not part of test-list'); @@ -216,7 +218,7 @@ Sieve.parseScript = script => { command = levels.last(); break; - case M.UNKNOWN: + case T_UNKNOWN: error('Invalid token ' + value); } diff --git a/dev/Sieve/RegEx.js b/dev/Sieve/RegEx.js index 61ee72972..af3b4f625 100644 --- a/dev/Sieve/RegEx.js +++ b/dev/Sieve/RegEx.js @@ -27,11 +27,22 @@ const */ OCTET_NOT_QSPECIAL = '[^\\x00\\r\\n"\\\\]', + /** + * hash-comment = "#" *octet-not-crlf CRLF + */ + HASH_COMMENT = '#' + OCTET_NOT_CRLF + '*\\r\\n', + /** * QUANTIFIER = "K" / "M" / "G" */ QUANTIFIER = '[KMGkmg]', + /** + * quoted-safe = CRLF / octet-not-qspecial + * either a CRLF pair, OR a single octet other than NUL, CR, LF, double-quote, or backslash + */ + QUOTED_SAFE = '\\r\\n|' + OCTET_NOT_QSPECIAL, + /** * quoted-special = "\" (DQUOTE / "\") * represents just a double-quote or backslash @@ -43,12 +54,6 @@ const */ QUOTED_TEXT = '(?:' + QUOTED_SAFE + '|' + QUOTED_SPECIAL + ')*', - /** - * quoted-safe = CRLF / octet-not-qspecial - * either a CRLF pair, OR a single octet other than NUL, CR, LF, double-quote, or backslash - */ - QUOTED_SAFE = '\\r\\n|' + OCTET_NOT_QSPECIAL, - /** * multiline-literal = [ octet-not-period *octet-not-crlf ] CRLF */ @@ -83,18 +88,30 @@ const /** * bracket-comment = "/*" *not-star 1*STAR *(not-star-slash *not-star 1*STAR) "/" */ - BRACKET_COMMENT: '/\\*.*?\\*/', // '/\\*[\\s\\S]*?\\*/' + BRACKET_COMMENT: '/\\*[\\s\\S]*?\\*/', /** * hash-comment = "#" *octet-not-crlf CRLF */ - HASH_COMMENT: '#' + OCTET_NOT_CRLF + '*\\r\\n', + HASH_COMMENT: HASH_COMMENT, /** * identifier = (ALPHA / "_") *(ALPHA / DIGIT / "_") */ IDENTIFIER: '[a-zA-Z_][a-zA-Z0-9_]*', + /** + * multi-line = "text:" *(SP / HTAB) (hash-comment / CRLF) + *(multiline-literal / multiline-dotstart) + "." CRLF + */ + MULTI_LINE: 'text:[ \\t]*(?:' + HASH_COMMENT + ')?\\r\\n' + + '(?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*' + + '\\.\\r\\n', + + MULTILINE_LITERAL: MULTILINE_LITERAL, + MULTILINE_DOTSTART: MULTILINE_DOTSTART, + /** * number = 1*DIGIT [ QUANTIFIER ] */ @@ -105,6 +122,8 @@ const */ QUOTED_STRING: '"' + QUOTED_TEXT + '"', + QUOTED_TEXT: QUOTED_TEXT, + /** * tag = ":" identifier */ @@ -129,16 +148,7 @@ const /** * comment = bracket-comment / hash-comment */ -//RegEx.COMMENT = RegEx.BRACKET_COMMENT + '|' + RegEx.HASH_COMMENT; - -/** - * multi-line = "text:" *(SP / HTAB) (hash-comment / CRLF) - *(multiline-literal / multiline-dotstart) - "." CRLF - */ -RegEx.MULTI_LINE = 'text:[ \\t]*(?:' + RegEx.HASH_COMMENT + ')?\\r\\n' - + '(?:' + MULTILINE_LITERAL + '|' + MULTILINE_DOTSTART + ')*' - + '\\.\\r\\n'; +//RegEx.COMMENT = RegEx.BRACKET_COMMENT + '|' + HASH_COMMENT; /************************************************** * https://tools.ietf.org/html/rfc5228#section-8.2 @@ -153,13 +163,13 @@ RegEx.MULTI_LINE = 'text:[ \\t]*(?:' + RegEx.HASH_COMMENT + ')?\\r\\n' * string-list = "[" string *("," string) "]" / string * if there is only a single string, the brackets are optional */ - RegEx.STRING_LIST = '\\[\\s*(?:' + RegEx.STRING + ')(?:\\s*,\\s*(?:' + RegEx.STRING + '))*\\s*\\]' - + '|(?:' + RegEx.STRING + ')'; + RegEx.STRING_LIST = '\\[\\s*(?:' + RegEx.STRING + ')(?:\\s*,\\s*(?:' + RegEx.STRING + '))*\\s*\\]'; +// + '|(?:' + RegEx.STRING + ')'; /** * argument = string-list / number / tag */ - RegEx.ARGUMENT = RegEx.STRING_LIST + '|' + RegEx.NUMBER + '|' + RegEx.TAG; + RegEx.ARGUMENT = RegEx.STRING_LIST + '|' + RegEx.STRING + '|' + RegEx.NUMBER + '|' + RegEx.TAG; /** * arguments = *argument [ test / test-list ] diff --git a/dev/sieve.js b/dev/sieve.js index f36f6425d..01e6fc163 100644 --- a/dev/sieve.js +++ b/dev/sieve.js @@ -9,7 +9,10 @@ Tests: {}, parseScript: ()=>{}, */ - Extensions: {} + Extensions: {}, + + arrayToString: (arr, separator) => + arr.map(item => item.toString ? item.toString() : item).join(separator) }; })(this);