From 6a676e5e453a459d3c8b26ff06aae86c0499337e Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Fri, 15 Sep 2023 06:33:00 -0700 Subject: [PATCH 1/3] fix: template check should allow templates embedded in JSON also, new tests for this case. --- lib/package/plugins/_pluginUtil.js | 137 ++++++++++++++++++----------- test/specs/testTemplateCheck.js | 3 + 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/lib/package/plugins/_pluginUtil.js b/lib/package/plugins/_pluginUtil.js index 8845bbdb..291f16e8 100644 --- a/lib/package/plugins/_pluginUtil.js +++ b/lib/package/plugins/_pluginUtil.js @@ -1,72 +1,107 @@ +const debug = require("debug")("apigeelint:template"); + const STATES = { OUTSIDE_CURLY: 0, INSIDE_CURLY_NO_TEXT: 1, INSIDE_CURLY: 2, - INSIDE_FUNCTION_NO_TEXT: 3, - INSIDE_FUNCTION: 4 + INSIDE_VARREF: 3, + INSIDE_FUNCTION_NO_TEXT: 4, + INSIDE_FUNCTION: 5 }; +const UPPER_A = 65, + UPPER_Z = 90, + LOWER_a = 97, + LOWER_z = 122; + +const stateToString = s => + Object.keys(STATES).find( key => STATES[key] == s); + + const isValidTemplate = (expr) => { let state = STATES.OUTSIDE_CURLY; - for (const ch of expr) { - switch (state) { - case STATES.OUTSIDE_CURLY: - if (ch == '{') { - state = STATES.INSIDE_CURLY_NO_TEXT; - } - if (ch == '}') { - return false; - } - break; + const states = []; + let code; + try { + for (const ch of expr) { + debug(`state(${stateToString(state)}) ch(${ch})`); + switch (state) { + case STATES.OUTSIDE_CURLY: + case STATES.INSIDE_CURLY: + if (ch == '{') { + states.push(state); + state = STATES.INSIDE_CURLY_NO_TEXT; + } + if (ch == '}') { + if (state == STATES.OUTSIDE_CURLY) { + return false; + } + state = states.pop(); + } + break; - case STATES.INSIDE_CURLY_NO_TEXT: - if (ch == '}' || ch == '{' || ch == '[' || ch == '(') { - return false; - } - state = STATES.INSIDE_CURLY; - break; + case STATES.INSIDE_CURLY_NO_TEXT: + if (ch == '}' || ch == '{' || ch == '[' || ch == '(') { + return false; + } + code = ch.charCodeAt(0); + if (!(code >= UPPER_A && code <= UPPER_Z) && + !(code >= LOWER_a && code <= LOWER_z)) { + state = STATES.INSIDE_CURLY; + } + else { + state = STATES.INSIDE_VARREF; + } + break; - case STATES.INSIDE_CURLY: - if (ch == '{' || ch == '[' || ch == ')') { - return false; - } - if (ch == '(') { - state = STATES.INSIDE_FUNCTION_NO_TEXT; - } - if (ch == '}') { - state = STATES.OUTSIDE_CURLY; - } - break; + case STATES.INSIDE_VARREF: + if (ch == '{' || ch == '[' || ch == ')') { + return false; + } + if (ch == '(') { + state = STATES.INSIDE_FUNCTION_NO_TEXT; + } + if (ch == '}') { + state = states.pop(); + } + break; - case STATES.INSIDE_FUNCTION_NO_TEXT: - if (ch == '{' || ch == '[' || ch == '(' || ch == ')' || ch == '}' || ch == ']') { - return false; - } - state = STATES.INSIDE_FUNCTION; - break; + case STATES.INSIDE_FUNCTION_NO_TEXT: + if (ch == '{' || ch == '[' || ch == '(' || ch == ')' || ch == '}' || ch == ']') { + return false; + } + state = STATES.INSIDE_FUNCTION; + break; - case STATES.INSIDE_FUNCTION: - if (ch == '{' || ch == '[' || ch == '(') { - return false; - } - if (ch == ')') { - state = STATES.AWAITING_CLOSE_CURLY; - } - break; + case STATES.INSIDE_FUNCTION: + if (ch == '{' || ch == '[' || ch == '(') { + return false; + } + if (ch == ')') { + state = STATES.AWAITING_CLOSE_CURLY; + } + break; + + case STATES.AWAITING_CLOSE_CURLY: + if (ch != '}') { + return false; + } + state = states.pop(); + break; - case STATES.AWAITING_CLOSE_CURLY: - if (ch != '}') { + default: + // should not happen + debug(`unknown state (${state})`); return false; } - state = STATES.OUTSIDE_CURLY; - break; - - default: - // should not happen - return false; } + } catch(e) { + // stack underflow + debug(`stack underflow? exception (${e})`); + return false; } + debug(`final state(${stateToString(state)})`); return state == STATES.OUTSIDE_CURLY; }; diff --git a/test/specs/testTemplateCheck.js b/test/specs/testTemplateCheck.js index 9d0cfbea..2fc92334 100644 --- a/test/specs/testTemplateCheck.js +++ b/test/specs/testTemplateCheck.js @@ -35,6 +35,9 @@ describe("TemplateCheck", function() { ["{a}{b}", true], ["{timeFormatUTCMs(propertyset.set1.timeformat,system.timestamp)}", true], ["{timeFormatUTCMs(propertyset.set1.timeformat,system.timestamp) }", false], + [`{ "organization": "{organization.name}", "environment": "{environment.name}" } `, true], + [`{ "organization": "{organization.name}", "environment": "{environment.{name}}" } `, false], + [`{ "organization": "{organization.name}", "other": {"environment": "{environment.name}" } } `, true], ]; testCases.forEach( (item, _ix) => { From 889b1bcfee75466b37b22a668b92463e88106128 Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Fri, 15 Sep 2023 06:33:00 -0700 Subject: [PATCH 2/3] fix: template check should allow templates embedded in JSON also, new tests for this case. --- .../PO026-assignVariable-policies/Template-1.xml | 14 ++++++++++++++ .../PO026-assignVariable-policies/Template-2.xml | 14 ++++++++++++++ test/specs/PO026-profile.js | 12 ++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 test/fixtures/resources/PO026-assignVariable-policies/Template-1.xml create mode 100644 test/fixtures/resources/PO026-assignVariable-policies/Template-2.xml diff --git a/test/fixtures/resources/PO026-assignVariable-policies/Template-1.xml b/test/fixtures/resources/PO026-assignVariable-policies/Template-1.xml new file mode 100644 index 00000000..1860953e --- /dev/null +++ b/test/fixtures/resources/PO026-assignVariable-policies/Template-1.xml @@ -0,0 +1,14 @@ + + + assigned + + + diff --git a/test/fixtures/resources/PO026-assignVariable-policies/Template-2.xml b/test/fixtures/resources/PO026-assignVariable-policies/Template-2.xml new file mode 100644 index 00000000..724b2dcb --- /dev/null +++ b/test/fixtures/resources/PO026-assignVariable-policies/Template-2.xml @@ -0,0 +1,14 @@ + + + assigned + + + diff --git a/test/specs/PO026-profile.js b/test/specs/PO026-profile.js index 3fd5a49d..f57e4fcb 100644 --- a/test/specs/PO026-profile.js +++ b/test/specs/PO026-profile.js @@ -156,4 +156,16 @@ describe(`PO026 - AssignVariable with PropertySetRef`, () => { assert.equal(p.getReport().messages.length, 0, JSON.stringify(p.getReport().messages)); }); + po026Test(`Template-1.xml`, 'apigeex', (p, foundIssues) => { + assert.equal(foundIssues, false); + assert.ok(p.getReport().messages, "messages undefined"); + assert.equal(p.getReport().messages.length, 0, JSON.stringify(p.getReport().messages)); + }); + + po026Test(`Template-2.xml`, 'apigeex', (p, foundIssues) => { + assert.equal(foundIssues, true); + assert.ok(p.getReport().messages, "messages undefined"); + assert.equal(p.getReport().messages.length, 1, JSON.stringify(p.getReport().messages)); + }); + }); From 0e63688e60c72bdbf6a51a7cfd73470cf86031f2 Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Fri, 15 Sep 2023 10:53:53 -0700 Subject: [PATCH 3/3] fix: validate function names and basic args --- .../plugins/PO026-assignVariableUsage.js | 17 +- lib/package/plugins/_pluginUtil.js | 150 +++++++++++++----- test/specs/testTemplateCheck.js | 65 +++++--- 3 files changed, 160 insertions(+), 72 deletions(-) diff --git a/lib/package/plugins/PO026-assignVariableUsage.js b/lib/package/plugins/PO026-assignVariableUsage.js index 30ab669e..26db4683 100644 --- a/lib/package/plugins/PO026-assignVariableUsage.js +++ b/lib/package/plugins/PO026-assignVariableUsage.js @@ -107,13 +107,17 @@ const onPolicy = function(policy, cb) { addMessage(textnode[0].lineNumber, textnode[0].columnNumber, "The text of the PropertySetRef element must not be empty"); } - if ( ! pluginUtil.isValidTemplate(psrText)) { + let r = pluginUtil.validateTemplate(psrText); + if ( r) { addMessage(textnode[0].lineNumber, textnode[0].columnNumber, - "The text of the PropertySetRef element must be a valid message template"); + `The text of the PropertySetRef element must be a valid message template (${r})`); } - else if ( !pluginUtil.isValidPropertySetRef(psrText)) { + else { + r = pluginUtil.validatePropertySetRef(psrText); + if (r) { addMessage(textnode[0].lineNumber, textnode[0].columnNumber, - "The text of the PropertySetRef element must resolve to a string that contains exactly one dot"); + `Error in the text of the PropertySetRef element: ${r}`); + } } } @@ -124,9 +128,10 @@ const onPolicy = function(policy, cb) { const textnode = xpath.select("Template/text()", node), templateText = textnode[0] && textnode[0].data; if (templateText) { - if ( ! pluginUtil.isValidTemplate(templateText)) { + const r = pluginUtil.validateTemplate(templateText); + if ( r ) { addMessage(textnode[0].lineNumber, textnode[0].columnNumber, - "The text of the Template element must be a valid message template"); + r.error); } } } diff --git a/lib/package/plugins/_pluginUtil.js b/lib/package/plugins/_pluginUtil.js index 291f16e8..1cfc7866 100644 --- a/lib/package/plugins/_pluginUtil.js +++ b/lib/package/plugins/_pluginUtil.js @@ -1,30 +1,71 @@ - const debug = require("debug")("apigeelint:template"); const STATES = { - OUTSIDE_CURLY: 0, - INSIDE_CURLY_NO_TEXT: 1, - INSIDE_CURLY: 2, - INSIDE_VARREF: 3, - INSIDE_FUNCTION_NO_TEXT: 4, - INSIDE_FUNCTION: 5 + UNDEFINED : -1, + OUTSIDE_CURLY : 0, + INSIDE_CURLY_NO_TEXT : 1, + INSIDE_CURLY : 2, + INSIDE_VARREF : 3, + INSIDE_FNARGS_NO_TEXT : 4, + INSIDE_FNARGS : 5 + }; + +const CODES = { + UPPER_A : 65, + UPPER_Z : 90, + LOWER_a : 97, + LOWER_z : 122, + ZERO : 48, + NINE : 57 + }; + +const ZERO_ARG_FUNCTIONS = [ 'createUuid', 'randomLong' ]; + +const FUNCTION_NAMES = [ + "sha1Hex", "sha1Base64", "sha256Hex", "sha256Base64", "sha384Hex", "sha384Base64", + "sha512Hex", "sha512Base64", "md5Hex", "md5Base64", "xPath", "xpath", "jsonPath", + "substring", "firstnonnull", "replaceAll", "replaceFirst", "createUuid", "randomLong", + "hmacMd5", "hmacSha1", "hmacSha224", "hmacSha256", "hmacSha384", "hmacSha512", + "timeFormat", "timeFormatMs", "timeFormatUTC", "timeFormatUTCMs", "xeger", + "encodeHTML", "escapeJSON", "escapeXML", "escapeXML11", "toUpperCase", "toLowerCase", + "encodeBase64", "decodeBase64" + ]; + +const validateFunction = (expr, start, end) => { + const fn = expr.substring(start, end); + if (FUNCTION_NAMES.find( f => fn == f)) { + return { fn }; + } + return {fn, error: 'unknown function'}; }; -const UPPER_A = 65, - UPPER_Z = 90, - LOWER_a = 97, - LOWER_z = 122; +const validateFunctionArgs = (expr, fnStart, argsStart, end) => { + const fn = expr.substring(fnStart, argsStart - 1); + const argsString = expr.substring(argsStart, end); + if (argsString.includes(' ')) { + return {fn, error: 'spaces in argument string'}; + } + if (argsString == '' && !ZERO_ARG_FUNCTIONS.includes(fn)) { + return {fn, error: `empty arg string`}; + } + return { fn }; + }; const stateToString = s => Object.keys(STATES).find( key => STATES[key] == s); -const isValidTemplate = (expr) => { +/* + * returns undefined for valid template. Returns an explanatory string if invalid. + **/ +const validateTemplate = (expr) => { let state = STATES.OUTSIDE_CURLY; const states = []; - let code; + let code, fnStart = -1, argsStart = -1; + let ix = -1; try { for (const ch of expr) { + ix++; debug(`state(${stateToString(state)}) ch(${ch})`); switch (state) { case STATES.OUTSIDE_CURLY: @@ -32,22 +73,30 @@ const isValidTemplate = (expr) => { if (ch == '{') { states.push(state); state = STATES.INSIDE_CURLY_NO_TEXT; + fnStart = ix; } if (ch == '}') { if (state == STATES.OUTSIDE_CURLY) { - return false; + return `unexpected close curly at position ${ix}`; } state = states.pop(); } break; case STATES.INSIDE_CURLY_NO_TEXT: - if (ch == '}' || ch == '{' || ch == '[' || ch == '(') { - return false; + if (ch == '}') { + return `empty function name at position ${ix}`; + } + if (ch == '{' || ch == '[' || ch == '(') { + return `unexpected character at position ${ix}: ${ch}`; } code = ch.charCodeAt(0); - if (!(code >= UPPER_A && code <= UPPER_Z) && - !(code >= LOWER_a && code <= LOWER_z)) { + // examine first character. A numeric is invalid. + if (code >= CODES.ZERO && code <= CODES.NINE) { + return `unexpected character at position ${ix}: ${ch}`; + } + // a non-alphabetic means it's not a variable or function. + else if (!(code >= CODES.UPPER_A && code <= CODES.UPPER_Z) && !(code >= CODES.LOWER_a && code <= CODES.LOWER_z)) { state = STATES.INSIDE_CURLY; } else { @@ -57,35 +106,53 @@ const isValidTemplate = (expr) => { case STATES.INSIDE_VARREF: if (ch == '{' || ch == '[' || ch == ')') { - return false; + return `unexpected character at position ${ix}: ${ch}`; } if (ch == '(') { - state = STATES.INSIDE_FUNCTION_NO_TEXT; + const r = validateFunction(expr, fnStart + 1, ix); + if ( r.error ) { + return `unsupported function name (${r.fn})`; + } + argsStart = ix; + state = STATES.INSIDE_FNARGS_NO_TEXT; } if (ch == '}') { state = states.pop(); } break; - case STATES.INSIDE_FUNCTION_NO_TEXT: - if (ch == '{' || ch == '[' || ch == '(' || ch == ')' || ch == '}' || ch == ']') { - return false; + case STATES.INSIDE_FNARGS_NO_TEXT: + if (ch == '{' || ch == '[' || ch == '(' || ch == '}' || ch == ']') { + return `unexpected character at position ${ix}: ${ch}`; + } + if (ch == ')') { + const r = validateFunctionArgs(expr, fnStart + 1, argsStart + 1, ix); + if ( r.error ) { + return `${r.error} for function ${r.fn}`; + } + state = STATES.AWAITING_CLOSE_CURLY; + } + else { + state = STATES.INSIDE_FNARGS; } - state = STATES.INSIDE_FUNCTION; break; - case STATES.INSIDE_FUNCTION: + case STATES.INSIDE_FNARGS: if (ch == '{' || ch == '[' || ch == '(') { - return false; + return `unexpected character at position ${ix}: ${ch}`; } if (ch == ')') { + const r = validateFunctionArgs(expr, fnStart + 1, argsStart + 1, ix); + if ( r.error ) { + return `${r.error} for function ${r.fn}`; + } state = STATES.AWAITING_CLOSE_CURLY; } break; case STATES.AWAITING_CLOSE_CURLY: if (ch != '}') { - return false; + return `unexpected character at position ${ix}: ${ch}`; } state = states.pop(); break; @@ -93,34 +160,37 @@ const isValidTemplate = (expr) => { default: // should not happen debug(`unknown state (${state})`); - return false; + return 'parse faillure'; } } } catch(e) { // stack underflow debug(`stack underflow? exception (${e})`); - return false; + return 'extraneous close-brace at position ${ix}'; } debug(`final state(${stateToString(state)})`); - return state == STATES.OUTSIDE_CURLY; + return state == STATES.OUTSIDE_CURLY ? undefined : 'unterminated curly brace'; }; -const isValidPropertySetRef = (expr) => { - if ( ! isValidTemplate(expr)) { - return false; +const validatePropertySetRef = (expr) => { + let r = validateTemplate(expr); + if ( r ) { + return r; } if (expr.includes('{')) { // There is a variable reference. - // Simulate a variable substitution; the result must have AT MOST one dot. - const r = new RegExp('{[^}]+}', 'g'), - result = expr.replaceAll(r, 'xxx'); - return (result.match(/\./g) || []).length <= 1; + // Simulate a variable substitution; the result must have at most one dot. + const re = new RegExp('{[^}]+}', 'g'), + result = expr.replaceAll(re, 'xxx'); + return ((result.match(/\./g) || []).length > 1) ? + 'there is more than one dot in the template result' : undefined; } // No variable reference; the expression must have exactly one dot. - return (expr.match(/\./g) || []).length == 1; + r = (expr.match(/\./g) || []).length; + return (r == 1) ? undefined : `there are ${r} dots ( !=1 ) in the template result`; }; module.exports = { - isValidTemplate, - isValidPropertySetRef + validateTemplate, + validatePropertySetRef }; diff --git a/test/specs/testTemplateCheck.js b/test/specs/testTemplateCheck.js index 2fc92334..3c239bda 100644 --- a/test/specs/testTemplateCheck.js +++ b/test/specs/testTemplateCheck.js @@ -21,46 +21,59 @@ const assert = require("assert"), describe("TemplateCheck", function() { const testCases = [ - ["{}", false], - ["{a}", true], - ["{{a}", false], - ["{{a}}", false], - ["{[]}", false], - ["{a[]}", false], - ["{a", false], - ["}a", false], - ["{a}}", false], - ["}a{", false], - ["{a}b", true], - ["{a}{b}", true], - ["{timeFormatUTCMs(propertyset.set1.timeformat,system.timestamp)}", true], - ["{timeFormatUTCMs(propertyset.set1.timeformat,system.timestamp) }", false], - [`{ "organization": "{organization.name}", "environment": "{environment.name}" } `, true], - [`{ "organization": "{organization.name}", "environment": "{environment.{name}}" } `, false], - [`{ "organization": "{organization.name}", "other": {"environment": "{environment.name}" } } `, true], + ["{}", "empty function name at position 1"], + ["{a}", undefined], + ["{{a}", 'unexpected character at position 1: {'], + ["{{a}}", 'unexpected character at position 1: {'], + ["{[]}", 'unexpected character at position 1: ['], + ["{a[]}", 'unexpected character at position 2: ['], + ["{a", 'unterminated curly brace'], + ["}a", 'unexpected close curly at position 0'], + ["{a}}", 'unexpected close curly at position 3'], + ["}a{", 'unexpected close curly at position 0'], + ["{a}b", undefined], + ["{a}{b}", undefined], + ["{a}.{b}", undefined], + ["{a.b}", undefined], + ["{3}", 'unexpected character at position 1: 3'], + ["{timeFormatUTCMs(propertyset.set1.timeformat,system.timestamp)}", undefined], + ["{timeFormatUTCMs(propertyset.set1.timeformat,system.timestamp) }", + 'unexpected character at position 62: '], + [`{ "organization": "{organization.name}", "environment": "{environment.name}" } `, undefined], + [`{ "organization": "{organization.name}", "environment": "{environment.{name}}" } `, + 'unexpected character at position 71: {'], + [`{ "organization": "{organization.name}", "other": {"environment": "{environment.name}" } } `, + undefined], + ["{createUuid()}", undefined], + ["{createUuid( )}", 'spaces in argument string for function createUuid'], + ["{timeFormatUTCMs()}", 'empty arg string for function timeFormatUTCMs'], + ["{notARealfunction()}", 'unsupported function name (notARealfunction)'], + ["{createUuid[]}", 'unexpected character at position 11: ['], + ["{ createUuid() }", undefined], // but ineffective ]; testCases.forEach( (item, _ix) => { - const [expression, expectedResult] = item; - it(`Check template (${expression}) should return ${expectedResult}`, function() { - const actualResult = pu.isValidTemplate(expression); - assert.equal(actualResult, expectedResult); + const [expression, expectedError] = item; + it(`validateTemplate (${expression}) should return ${expectedError}`, function() { + const actualResult = pu.validateTemplate(expression); + assert.equal(actualResult, expectedError); }); }); }); describe("PropertySetRefCheck", function() { const testCases = [ - ["{foo.bar}", true], - ["{foo.bar}.{var2}.setting", false], - ["{foo.bar}.setting", true], - ["{foo.bar}.{flow.var2}", true], + ["{foo.bar}", undefined], + ["{foo.bar}.{var2}.setting", 'there is more than one dot in the template result'], + ["foo.bar.var2.setting", 'there are 3 dots ( !=1 ) in the template result'], + ["{foo.bar}.setting", undefined], + ["{foo.bar}.{flow.var2}", undefined], ]; testCases.forEach( (item, _ix) => { const [expression, expectedResult] = item; it(`PropertySetRef (${expression}) should be ${expectedResult?'valid':'not valid'}`, function() { - const actualResult = pu.isValidPropertySetRef(expression); + const actualResult = pu.validatePropertySetRef(expression); assert.equal(actualResult, expectedResult); }); });