From 68fc9f21bddff3f364036074b4507087b1599734 Mon Sep 17 00:00:00 2001 From: djmaze Date: Fri, 15 Jan 2021 11:34:16 +0100 Subject: [PATCH] Centralize some argument parsing Improved parser error handling Fix Sieve Vacation extension --- dev/Sieve/Extensions/rfc5173.js | 10 +-- dev/Sieve/Extensions/rfc5230.js | 105 ++++++++++++++++---------------- dev/Sieve/Extensions/rfc5232.js | 12 +--- dev/Sieve/Extensions/rfc5235.js | 34 +++-------- dev/Sieve/Grammar.js | 2 +- dev/Sieve/Parser.js | 90 ++++++++++++++------------- dev/Sieve/Tests.js | 46 +++++--------- 7 files changed, 131 insertions(+), 168 deletions(-) diff --git a/dev/Sieve/Extensions/rfc5173.js b/dev/Sieve/Extensions/rfc5173.js index e5cb517b5..4cd41ec2e 100644 --- a/dev/Sieve/Extensions/rfc5173.js +++ b/dev/Sieve/Extensions/rfc5173.js @@ -11,7 +11,7 @@ class Body extends Grammar.Test constructor() { super('body'); - this.comparator = 'i;ascii-casemap', + this.comparator = '', this.match_type = ':is', this.body_transform = ''; // :raw, :content , :text this.key_list = new Grammar.StringList; @@ -31,14 +31,10 @@ class Body extends Grammar.Test pushArguments(args) { args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (':raw' === arg || ':text' === arg) { + if (':raw' === arg || ':text' === arg) { this.body_transform = arg; } else if (arg instanceof Grammar.StringList || arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else if (':content' === args[i-1]) { + if (':content' === args[i-1]) { this.body_transform = ':content ' + arg; } else { this[args[i+1] ? 'content_list' : 'key_list'] = arg; diff --git a/dev/Sieve/Extensions/rfc5230.js b/dev/Sieve/Extensions/rfc5230.js index a42e4e324..9f75a5860 100644 --- a/dev/Sieve/Extensions/rfc5230.js +++ b/dev/Sieve/Extensions/rfc5230.js @@ -11,15 +11,13 @@ class Vacation extends Grammar.Command constructor() { super('vacation'); - this.arguments = { - ':days' : new Grammar.Number, - ':subject' : new Grammar.QuotedString, - ':from' : new Grammar.QuotedString, - ':addresses': new Grammar.StringList, - ':mime' : false, - ':handle' : new Grammar.QuotedString - }; - this.reason = ''; // QuotedString / MultiLine + this._days = new Grammar.Number; + this._subject = new Grammar.QuotedString; + this._from = new Grammar.QuotedString; + this.addresses = new Grammar.StringList; + this.mime = false; + this._handle = new Grammar.QuotedString; + this._reason = new Grammar.QuotedString; // QuotedString / MultiLine } get require() { return 'vacation'; } @@ -27,63 +25,66 @@ class Vacation extends Grammar.Command toString() { let result = 'vacation'; - if (0 < this.arguments[':days'].value) { - result += ' :days ' + this.arguments[':days']; + if (0 < this._days.value) { + result += ' :days ' + this._days; } - if (this.arguments[':subject'].length()) { - result += ' :subject ' + this.arguments[':subject']; + if (this._subject.length) { + result += ' :subject ' + this._subject; } - if (this.arguments[':from'].length()) { + if (this._from.length) { result += ' :from ' + this.arguments[':from']; } - if (this.arguments[':addresses'].length) { - result += ' :addresses ' + this.arguments[':addresses']; + if (this.addresses.length) { + result += ' :addresses ' + this.addresses; } - if (this.arguments[':mime']) { + if (this.mime) { result += ' :mime'; } - if (this.arguments[':handle'].length()) { - result += ' :handle ' + this.arguments[':handle']; - } - return result + ' ' + this.reason; - } -/* - function __get($key) - { - if ('reason' === $key) { - return this.reason; - } - if (isset(this.arguments[":{$key}"])) { - return this.arguments[":{$key}"]; + if (this._handle.length) { + result += ' :handle ' + this._handle; } + return result + ' ' + this._reason; } - function __set($key, $value) - { - if ('days' === $key) { - this.arguments[":{$key}"] = (int) $value; - } else if ('mime' === $key) { - this.arguments[":{$key}"] = (bool) $value; - } else if ('reason' === $key) { - this.reason = (string) $value; - } else if ('addresses' !== $key && isset(this.arguments[":{$key}"])) { - this.arguments[":{$key}"] = (string) $value; - } - } - public function pushArguments(array $args): void + get days() { return this._days.value; } + get subject() { return this._subject.value; } + get from() { return this._from.value; } + get handle() { return this._handle.value; } + get reason() { return this._reason.value; } + + set days(int) { this._days.value = int; } + set subject(str) { this._subject.value = str; } + set from(str) { this._from.value = str; } + set handle(str) { this._handle.value = str; } + set reason(str) { this._reason.value = str; } + + pushArguments(args) { - foreach ($args as $i => $arg) { - if (\in_array($arg, [':days',':subject',':from',':addresses', ':handle'])) { - this.arguments[$arg] = $args[$i+1]; - } else if (':mime' === $arg) { - this.arguments[':mime'] = true; - } else if (!isset($args[$i+1])) { - this.reason = $arg; + args.forEach((arg, i) => { + if (':mime' === arg) { + this.mime = true; + } else if (i === args.length-1) { + this._reason.value = arg.value; // Grammar.QuotedString + } else switch (args[i-1]) { + case ':days': + this._days.value = arg.value; // Grammar.Number + break; + case ':subject': + this._subject.value = arg.value; // Grammar.QuotedString + break; + case ':from': + this._from.value = arg.value; // Grammar.QuotedString + break; + case ':addresses': + this.addresses = arg; // Grammar.StringList + break; + case ':handle': + this._from.value = arg.value; // Grammar.QuotedString + break; } - } + }); } -*/ } Sieve.Commands.vacation = Vacation; diff --git a/dev/Sieve/Extensions/rfc5232.js b/dev/Sieve/Extensions/rfc5232.js index 51f08435c..e862d9d87 100644 --- a/dev/Sieve/Extensions/rfc5232.js +++ b/dev/Sieve/Extensions/rfc5232.js @@ -76,7 +76,7 @@ class HasFlag extends Grammar.Test constructor() { super('hasflag'); - this.comparator = 'i;ascii-casemap', + this.comparator = '', this.match_type = ':is', this.variable_list = new Grammar.StringList; this.list_of_flags = new Grammar.StringList; @@ -96,14 +96,8 @@ class HasFlag extends Grammar.Test pushArguments(args) { args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (arg instanceof Grammar.StringList || arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else { - this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg; - } + if (arg instanceof Grammar.StringList || arg instanceof Grammar.StringType) { + this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg; } }); } diff --git a/dev/Sieve/Extensions/rfc5235.js b/dev/Sieve/Extensions/rfc5235.js index 8a9ac146c..9b5199ce1 100644 --- a/dev/Sieve/Extensions/rfc5235.js +++ b/dev/Sieve/Extensions/rfc5235.js @@ -12,7 +12,7 @@ class SpamTest extends Grammar.Test { super('spamtest'); this.percent = false, // 0 - 100 else 0 - 10 - this.comparator = 'i;ascii-casemap', + this.comparator = '', this.match_type = ':is', this.value = new Grammar.QuotedString; } @@ -32,20 +32,11 @@ class SpamTest extends Grammar.Test pushArguments(args) { - args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (':percent' === arg) { + args.forEach(arg => { + if (':percent' === arg) { this.percent = true; } else if (arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else if (':value' === args[i-1] || ':count' === args[i-1]) { - // Sieve relational [RFC5231] match types - this.match_type = args[i-1] + ' ' + arg; - } else { - this.value = arg; - } + this.value = arg; } }); } @@ -56,7 +47,7 @@ class VirusTest extends Grammar.Test constructor() { super('virustest'); - this.comparator = 'i;ascii-casemap', + this.comparator = '', this.match_type = ':is', this.value = new Grammar.QuotedString; // 1 - 5 } @@ -74,18 +65,9 @@ class VirusTest extends Grammar.Test pushArguments(args) { - args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else if (':value' === args[i-1] || ':count' === args[i-1]) { - // Sieve relational [RFC5231] match types - this.match_type = args[i-1] + ' ' + arg; - } else { - this.value = arg; - } + args.forEach(arg => { + if (arg instanceof Grammar.StringType) { + this.value = arg; } }); } diff --git a/dev/Sieve/Grammar.js b/dev/Sieve/Grammar.js index d4730945d..cdac6a3d0 100644 --- a/dev/Sieve/Grammar.js +++ b/dev/Sieve/Grammar.js @@ -31,7 +31,7 @@ class StringType /*extends String*/ this._value = value; } - length() + get length() { return this._value.length; } diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js index 9ffa25aee..ac155186e 100644 --- a/dev/Sieve/Parser.js +++ b/dev/Sieve/Parser.js @@ -45,7 +45,7 @@ const /* T_UNKNOWN */ '[^ \\r\\n\\t]+' ].join(')|(') + ')'; -Sieve.parseScript = script => { +Sieve.parseScript = (script, name = 'script.sieve') => { let match, line = 1, tree = [], @@ -61,11 +61,30 @@ Sieve.parseScript = script => { const error = message => { - throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, 'script.sieve', line) +// throw new SyntaxError(message + ' at ' + regex.lastIndex + ' line ' + line, name, line) + throw new SyntaxError(message + ' on line ' + line + + ' around: ' + script.substr(regex.lastIndex - 10, 20).replace(/\r\n/gm, '\\r\\n'), name, line) + }, + pushArg = arg => { + command || error('Argument not part of command'); + let prev_arg = args[args.length-1]; + if (':is' === arg || ':contains' === arg || ':matches' === arg) { + command.match_type = arg; + } else if (':value' === prev_arg || ':count' === prev_arg) { + // Sieve relational [RFC5231] match types + command.match_type = prev_arg + ' ' + arg; + --args.length; +// requires.push('relational'); + } else if (':comparator' === prev_arg) { + command.comparator = arg; + --args.length; + } else { + args.push(arg); + } }, pushArgs = () => { - if (command && args.length) { - command.pushArguments(args); + if (args.length) { + command && command.pushArguments(args); args = []; } }; @@ -91,7 +110,7 @@ Sieve.parseScript = script => { new_command = new Commands.conditional(value); } else if (Commands[value]) { if ('allof' === value || 'anyof' === value) { - (command instanceof Commands.conditional) || error('Test-list not in conditional'); +// (command instanceof Commands.conditional || command instanceof Commands.not) || error('Test-list not in conditional'); } new_command = new Commands[value](); } else { @@ -115,14 +134,13 @@ Sieve.parseScript = script => { // allof/anyof .tests[] new_command command.tests.push(new_command); } else { - error('Test not allowed here'); + error('Test "' + value + '" not allowed in "' + command.identifier + '" command'); } } else if (command) { if (command.commands) { command.commands.push(new_command); } else { - console.dir(command); - error('commands not allowed'); + error('commands not allowed in "' + command.identifier + '" command'); } } else { tree.push(new_command); @@ -137,48 +155,36 @@ Sieve.parseScript = script => { // Arguments case T_TAG: -/* - if (':value' === value || ':count' === value) { - requires.push('relational'); - } -*/ - command - ? args.push(value.toLowerCase()) - : error('Tag must be command argument'); + pushArg(value.toLowerCase()); break; case T_STRING_LIST: - command - ? args.push(Grammar.StringList.fromString(value)) - : error('String list must be command argument'); + pushArg(Grammar.StringList.fromString(value)); break; case T_MULTILINE_STRING: - command - ? args.push(new Grammar.MultiLine(value)) - : error('Multi-line string must be command argument'); + pushArg(new Grammar.MultiLine(value)); break; case T_QUOTED_STRING: - command - ? args.push(new Grammar.QuotedString(value.substr(1,value.length-2))) - : error('Quoted string must be command argument'); + pushArg(new Grammar.QuotedString(value.substr(1,value.length-2))); break; case T_NUMBER: - command - ? args.push(new Grammar.Number(value)) - : error('Number must be command argument'); + pushArg(new Grammar.Number(value)); break; // Comments case T_BRACKET_COMMENT: - (command ? command.commands : tree).push( - new Grammar.BracketComment(value.substr(2, value.length-4)) - ); - break; - - case T_HASH_COMMENT: - (command ? command.commands : tree).push( - new Grammar.HashComment(value.substr(1).trim()) - ); - break; + case T_HASH_COMMENT: { + let obj = (T_HASH_COMMENT == type) + ? new Grammar.HashComment(value.substr(1).trim()) + : new Grammar.BracketComment(value.substr(2, value.length-4)); + if (command) { + if (!command.comments) { + command.comments = []; + } + (command.commands || command.comments).push(obj); + } else { + tree.push(obj); + } + break; } case T_WHITESPACE: // (command ? command.commands : tree).push(value.trim()); @@ -231,10 +237,12 @@ Sieve.parseScript = script => { break; case T_COMMA: pushArgs(); - levels.pop(); - command = levels.last(); // Must be inside PARENTHESIS aka test-list - (command.tests instanceof Grammar.TestList) || error('Comma not part of test-list'); + while (command && !(command.tests instanceof Grammar.TestList)) { + levels.pop(); + command = levels.last(); + } + command || error('Comma not part of test-list'); break; case T_UNKNOWN: diff --git a/dev/Sieve/Tests.js b/dev/Sieve/Tests.js index ded014d5a..65119767a 100644 --- a/dev/Sieve/Tests.js +++ b/dev/Sieve/Tests.js @@ -16,7 +16,7 @@ class Address extends Test constructor() { super('address'); - this.comparator = 'i;ascii-casemap'; + this.comparator = ''; this.address_part = ':all'; // :localpart | :domain | :all this.match_type = ':is'; this.header_list = new StringList; @@ -36,17 +36,11 @@ class Address extends Test pushArguments(args) { args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (':localpart' === arg || ':domain' === arg || ':all' === arg) { + if (':localpart' === arg || ':domain' === arg || ':all' === arg) { this.address_part = arg; } else if (arg instanceof StringList || arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else { - this[args[i+1] ? 'header_list' : 'key_list'] = arg; -// (args[i+1] ? this.header_list : this.key_list) = arg; - } + this[args[i+1] ? 'header_list' : 'key_list'] = arg; +// (args[i+1] ? this.header_list : this.key_list) = arg; } }); } @@ -65,7 +59,7 @@ class AllOf extends Test toString() { - return 'allof' + this.tests; + return 'allof ' + this.tests; } } @@ -82,7 +76,7 @@ class AnyOf extends Test toString() { - return 'anyof' + this.tests; + return 'anyof ' + this.tests; } } @@ -94,7 +88,7 @@ class Envelope extends Test constructor() { super('envelope'); - this.comparator = 'i;ascii-casemap'; + this.comparator = ''; this.address_part = ':all'; // :localpart | :domain | :all this.match_type = ':is'; this.envelope_part = new StringList; @@ -116,17 +110,11 @@ class Envelope extends Test pushArguments(args) { args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (':localpart' === arg || ':domain' === arg || ':all' === arg) { + if (':localpart' === arg || ':domain' === arg || ':all' === arg) { this.address_part = arg; } else if (arg instanceof StringList || arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else { - this[args[i+1] ? 'envelope_part' : 'key_list'] = arg; -// (args[i+1] ? this.envelope_part : this.key_list) = arg; - } + this[args[i+1] ? 'envelope_part' : 'key_list'] = arg; +// (args[i+1] ? this.envelope_part : this.key_list) = arg; } }); } @@ -177,7 +165,7 @@ class Header extends Test constructor() { super('header'); - this.comparator = 'i;ascii-casemap'; + this.comparator = ''; this.address_part = ':all'; // :localpart | :domain | :all this.match_type = ':is'; this.header_names = new StringList; @@ -196,17 +184,11 @@ class Header extends Test pushArguments(args) { args.forEach((arg, i) => { - if (':is' === arg || ':contains' === arg || ':matches' === arg) { - this.match_type = arg; - } else if (':localpart' === arg || ':domain' === arg || ':all' === arg) { + if (':localpart' === arg || ':domain' === arg || ':all' === arg) { this.address_part = arg; } else if (arg instanceof StringList || arg instanceof Grammar.StringType) { - if (':comparator' === args[i-1]) { - this.comparator = arg; - } else { - this[args[i+1] ? 'header_names' : 'key_list'] = arg; -// (args[i+1] ? this.header_names : this.key_list) = arg; - } + this[args[i+1] ? 'header_names' : 'key_list'] = arg; +// (args[i+1] ? this.header_names : this.key_list) = arg; } }); }