diff --git a/Dockerfile b/Dockerfile index 5bab956e..f70ba966 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds # This is because npm "chromedriver" package is not compatiable with all platforms # For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation -FROM --platform=$BUILDPLATFORM node:24-alpine@sha256:fb71d01345f11b708a3553c66e7c74074f2d506400ea81973343d915cb64eef0 AS builder +FROM --platform=$BUILDPLATFORM node:24-alpine@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS builder WORKDIR /app @@ -27,7 +27,7 @@ RUN npm run build ######################################### # Package static build files into nginx # ######################################### -FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:37f356a5eba5d187365b4f59cd6cc29f1f922ad18146d554b576a80983377e6a AS cyberchef +FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:fafa1102c789119971b3d83f9293f1ef5526bc73583a12e13ff5cd1299ed8b6c AS cyberchef LABEL maintainer="GCHQ " diff --git a/package-lock.json b/package-lock.json index 779b1b6b..46d2fa53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "assert": "^2.1.0", "avsc": "^5.7.9", "bcryptjs": "^3.0.3", - "bignumber.js": "^11.1.3", + "bignumber.js": "^11.1.4", "blakejs": "^1.2.1", "bootstrap": "4.6.2", "bootstrap-colorpicker": "^3.4.0", @@ -39,7 +39,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.9", + "dompurify": "^3.4.11", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -85,7 +85,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^8.6.2", + "protobufjs": "^8.6.4", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", @@ -116,7 +116,7 @@ "@babel/runtime": "^7.29.7", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.3", - "@codemirror/search": "^6.7.0", + "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.43.1", "@puppeteer/browsers": "3.0.4", @@ -159,7 +159,7 @@ "terser": "^5.48.0", "webpack": "^5.107.2", "webpack-bundle-analyzer": "^5.3.0", - "webpack-dev-server": "^5.2.4", + "webpack-dev-server": "^5.2.5", "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8" }, @@ -1876,9 +1876,9 @@ } }, "node_modules/@codemirror/search": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", - "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", "dev": true, "license": "MIT", "dependencies": { @@ -5794,9 +5794,9 @@ } }, "node_modules/bignumber.js": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.3.tgz", - "integrity": "sha512-+esZiNSo6VgFokTsYX6mYqNJfFd/IczzZCd4Z7cR8e+AQWhvIcj6nqQ1h9814D9u/TApU0jjTVmfWL0Pd1ZBdA==", + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", + "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", "license": "MIT" }, "node_modules/binary-extensions": { @@ -8585,9 +8585,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", - "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -9865,17 +9865,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -10907,9 +10907,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -15086,9 +15086,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.2.tgz", - "integrity": "sha512-CCERJxzRvKMeEdJSLwdQf40TXWNPc8M4RkN7j/lxY6FQB+4do8rETWqj60AqxP9n0XIsxnSefZ8uhAaGKg2njw==", + "version": "8.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.4.tgz", + "integrity": "sha512-/+XMv9JalknuncEJSwsyEVlwcxVLKx2iaoSUXFZA86MJkdqyOdfrlB1sB7S6aKyUk9tl20YY+SgQe5J2sJHTcg==", "license": "BSD-3-Clause", "dependencies": { "long": "^5.3.2" @@ -18112,9 +18112,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", - "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz", + "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 10a4475f..f20fba60 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@babel/runtime": "^7.29.7", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.3", - "@codemirror/search": "^6.7.0", + "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.43.1", "@puppeteer/browsers": "3.0.4", @@ -89,7 +89,7 @@ "terser": "^5.48.0", "webpack": "^5.107.2", "webpack-bundle-analyzer": "^5.3.0", - "webpack-dev-server": "^5.2.4", + "webpack-dev-server": "^5.2.5", "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8" }, @@ -105,7 +105,7 @@ "assert": "^2.1.0", "avsc": "^5.7.9", "bcryptjs": "^3.0.3", - "bignumber.js": "^11.1.3", + "bignumber.js": "^11.1.4", "blakejs": "^1.2.1", "bootstrap": "4.6.2", "bootstrap-colorpicker": "^3.4.0", @@ -123,7 +123,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.9", + "dompurify": "^3.4.11", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -169,7 +169,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^8.6.2", + "protobufjs": "^8.6.4", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs index 0dd31707..4f81f83b 100644 --- a/src/core/Ingredient.mjs +++ b/src/core/Ingredient.mjs @@ -32,6 +32,8 @@ class Ingredient { this.min = null; this.max = null; this.step = 1; + this.integer = false; + this.allowEmpty = true; if (ingredientConfig) { this._parseConfig(ingredientConfig); @@ -59,6 +61,70 @@ class Ingredient { this.min = ingredientConfig.min; this.max = ingredientConfig.max; this.step = ingredientConfig.step; + this.integer = typeof ingredientConfig.integer !== "undefined" ? !!ingredientConfig.integer : false; + this.allowEmpty = typeof ingredientConfig.allowEmpty !== "undefined" ? !!ingredientConfig.allowEmpty : true; + } + + + /** + * Validates the given value against the constraints of this ingredient. + * + * @param {*} val + * @returns {boolean} + */ + validate(val) { + if (this.disabled) return true; + + let checkVal = val; + if (this.type === "toggleString" && val && typeof val === "object" && "string" in val) { + checkVal = val.string; + } + + // 1. check if empty + let isEmpty = false; + if (checkVal === null || checkVal === undefined || checkVal === "") { + isEmpty = true; + } else if (typeof checkVal.length === "number" && checkVal.length === 0) { + isEmpty = true; + } + + if (isEmpty) { + if (this.allowEmpty === false) { + throw new OperationError(`${this.name} cannot be empty.`); + } + return true; + } + + // 2. maxLength check + if (typeof this.maxLength === "number" && checkVal !== null && checkVal !== undefined) { + if (typeof checkVal === "string" && checkVal.length > this.maxLength) { + throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`); + } + if (Array.isArray(checkVal) && checkVal.length > this.maxLength) { + throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`); + } + if (checkVal instanceof Uint8Array && checkVal.length > this.maxLength) { + throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`); + } + } + + // 3. number checks + if (this.type === "number") { + if (val === null || val === undefined || isNaN(val)) { + throw new OperationError(`${this.name} must be a number.`); + } + if (this.integer && !Number.isInteger(val)) { + throw new OperationError(`${this.name} must be an integer.`); + } + if (typeof this.min === "number" && val < this.min) { + throw new OperationError(`${this.name} must be greater than or equal to ${this.min}.`); + } + if (typeof this.max === "number" && val > this.max) { + throw new OperationError(`${this.name} must be less than or equal to ${this.max}.`); + } + } + + return true; } diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs index 09058766..b35a49a6 100755 --- a/src/core/Operation.mjs +++ b/src/core/Operation.mjs @@ -189,11 +189,30 @@ class Operation { if (typeof ing.min === "number") conf.min = ing.min; if (typeof ing.max === "number") conf.max = ing.max; if (ing.step) conf.step = ing.step; + if (typeof ing.integer !== "undefined") conf.integer = ing.integer; + if (typeof ing.allowEmpty !== "undefined") conf.allowEmpty = ing.allowEmpty; return conf; }); } + /** + * Validates the operation's ingredients against their defined constraints. + * + * @param {Object[]} [args] - Optional list of argument values to validate. If not provided, validates the current ingredient values. + * @returns {boolean} - True if valid, throws an OperationError if invalid. + */ + validateIngredients(args) { + const values = args || this.ingValues; + this._ingList.forEach((ing, i) => { + if (i < values.length) { + ing.validate(values[i]); + } + }); + return true; + } + + /** * Returns the value of the Operation as it should be displayed in a recipe config. * diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index 84c91d61..0886e994 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -212,6 +212,8 @@ class Recipe { self.sendProgressMessage(i + 1, this.opList.length); } + op.validateIngredients(op.ingValues); + if (op.flowControl) { // Package up the current state let state = { diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 46cd644a..0f9b502a 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -585,7 +585,8 @@ "HTML To Text", "Generate Lorem Ipsum", "Numberwang", - "XKCD Random Number" + "XKCD Random Number", + "Automated Validation Test Op" ] }, { diff --git a/src/core/operations/AutomatedValidationTestOp.mjs b/src/core/operations/AutomatedValidationTestOp.mjs new file mode 100644 index 00000000..315eb417 --- /dev/null +++ b/src/core/operations/AutomatedValidationTestOp.mjs @@ -0,0 +1,78 @@ +/** + * @author CyberChef + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Automated validation test operation + */ +class AutomatedValidationTestOp extends Operation { + + /** + * AutomatedValidationTestOp constructor + */ + constructor() { + super(); + + this.name = "Automated Validation Test Op"; + this.module = "Default"; + this.description = "Operation used specifically to test automated parameter validation."; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Integer Number", + "type": "number", + "value": 5, + "min": 5, + "max": 10, + "integer": true + }, + { + "name": "Real Number", + "type": "number", + "value": 1.5, + "min": 1.5, + "max": 5.5 + }, + { + "name": "Non Empty String", + "type": "string", + "value": "hello", + "maxLength": 5, + "allowEmpty": false + }, + { + "name": "Empty Allowed String", + "type": "string", + "value": "", + "allowEmpty": true + }, + { + "name": "Non Empty Toggle String", + "type": "toggleString", + "value": { + "option": "Option A", + "string": "test" + }, + "toggleValues": ["Option A", "Option B"], + "allowEmpty": false + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + return "Success"; + } + +} + +export default AutomatedValidationTestOp; diff --git a/src/core/operations/URLEncode.mjs b/src/core/operations/URLEncode.mjs index a5efd213..99eec91d 100644 --- a/src/core/operations/URLEncode.mjs +++ b/src/core/operations/URLEncode.mjs @@ -21,7 +21,7 @@ class URLEncode extends Operation { this.module = "URL"; this.description = "Encodes problematic characters into percent-encoding, a format supported by URIs/URLs.

e.g. = becomes %3d"; this.infoURL = "https://wikipedia.org/wiki/Percent-encoding"; - this.inputType = "string"; + this.inputType = "byteArray"; this.outputType = "string"; this.args = [ { @@ -33,34 +33,38 @@ class URLEncode extends Operation { } /** - * @param {string} input + * @param {byteArray} input * @param {Object[]} args * @returns {string} */ run(input, args) { const encodeAll = args[0]; - return encodeAll ? this.encodeAllChars(input) : encodeURI(input); + return this.encodeBytes(input, encodeAll); } /** - * Encode characters in URL outside of encodeURI() function spec + * Encode bytes in URL using percent encoding. * - * @param {string} str + * @param {byteArray} bytes + * @param {boolean} encodeAll * @returns {string} */ - encodeAllChars (str) { - // TODO Do this programmatically - return encodeURIComponent(str) - .replace(/!/g, "%21") - .replace(/#/g, "%23") - .replace(/'/g, "%27") - .replace(/\(/g, "%28") - .replace(/\)/g, "%29") - .replace(/\*/g, "%2A") - .replace(/-/g, "%2D") - .replace(/\./g, "%2E") - .replace(/_/g, "%5F") - .replace(/~/g, "%7E"); + encodeBytes(bytes, encodeAll) { + const safeChars = encodeAll ? + /^[A-Za-z0-9]$/ : + /^[A-Za-z0-9:/?#[\]@!$&'()*+,;=%]$/; + + let output = ""; + + for (const byte of bytes) { + const char = String.fromCharCode(byte); + + output += safeChars.test(char) ? + char : + "%" + byte.toString(16).toUpperCase().padStart(2, "0"); + } + + return output; } } diff --git a/src/node/api.mjs b/src/node/api.mjs index 8002a8ac..f41feb23 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -193,6 +193,8 @@ export function _wrap(OpClass) { wrapped = async (input, args=null) => { const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + opInstance.validateIngredients(transformedArgs); + // SPECIAL CASE for Magic. Other flowControl operations will // not work because the opList is not passed in. if (isFlowControl) { @@ -229,6 +231,7 @@ export function _wrap(OpClass) { */ wrapped = (input, args=null) => { const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + opInstance.validateIngredients(transformedArgs); const result = opInstance.run(transformedInput, transformedArgs); return new NodeDish({ value: result, diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 0146bf27..abca9d84 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -24,6 +24,14 @@ height: 100%; user-select: auto; } + +#output-html > img { + display: block; + max-width: 100%; + max-height: 100%; + margin: auto; +} + #output-text.html-output .cm-line .cm-widgetBuffer, #output-text.html-output .cm-line>br { display: none; diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index e295f08c..9139a53a 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -492,7 +492,22 @@ function testOpImage(browser, opName, filename, args=[]) { browser .waitForElementVisible("#output-html img") - .expect.element("#output-html img").to.have.css("width").which.matches(/^[^0]\d*px/); + .expect.element("#output-html img").to.have.css("width").which.matches(/^(?!0+(?:\.0+)?px$)\d+(?:\.\d+)?px$/); + + browser.execute(function() { + const output = document.getElementById("output-html"); + const img = output.querySelector("img"); + const outputRect = output.getBoundingClientRect(); + const imgRect = img.getBoundingClientRect(); + + return { + imageFitsWidth: imgRect.width <= outputRect.width, + imageFitsHeight: imgRect.height <= outputRect.height, + }; + }, [], function({value}) { + browser.expect(value.imageFitsWidth).to.be.equal(true); + browser.expect(value.imageFitsHeight).to.be.equal(true); + }); } /** @function diff --git a/tests/operations/tests/AutomatedValidation.mjs b/tests/operations/tests/AutomatedValidation.mjs new file mode 100644 index 00000000..da84de11 --- /dev/null +++ b/tests/operations/tests/AutomatedValidation.mjs @@ -0,0 +1,121 @@ +/** + * Automated Parameter Validation tests + * + * @author CyberChef + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Automated Validation: Valid values", + input: "test", + expectedOutput: "Success", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Integer Number under min limit", + input: "test", + expectedOutput: "Integer Number must be greater than or equal to 5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [4, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Integer Number over max limit", + input: "test", + expectedOutput: "Integer Number must be less than or equal to 10.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [11, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Integer Number not an integer", + input: "test", + expectedOutput: "Integer Number must be an integer.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5.5, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Real Number under min limit", + input: "test", + expectedOutput: "Real Number must be greater than or equal to 1.5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.4, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Real Number over max limit", + input: "test", + expectedOutput: "Real Number must be less than or equal to 5.5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 5.6, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Non Empty String over maxLength limit", + input: "test", + expectedOutput: "Non Empty String length cannot exceed 5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "helloooo", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Non Empty String is empty", + input: "test", + expectedOutput: "Non Empty String cannot be empty.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Empty Allowed String is empty (allowed)", + input: "test", + expectedOutput: "Success", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Non Empty Toggle String is empty", + input: "test", + expectedOutput: "Non Empty Toggle String cannot be empty.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "" }] + } + ] + } +]); diff --git a/tests/operations/tests/Hexdump.mjs b/tests/operations/tests/Hexdump.mjs index 12d04492..be071e23 100644 --- a/tests/operations/tests/Hexdump.mjs +++ b/tests/operations/tests/Hexdump.mjs @@ -129,7 +129,7 @@ TestRegister.addTests([ { name: "To Hexdump: Width too large", input: "H", - expectedOutput: "Width must be no more than 65536", + expectedOutput: "Width must be less than or equal to 65536.", recipeConfig: [ { op: "To Hexdump", diff --git a/tests/operations/tests/URLEncodeDecode.mjs b/tests/operations/tests/URLEncodeDecode.mjs index 444f76d3..8d9d09db 100644 --- a/tests/operations/tests/URLEncodeDecode.mjs +++ b/tests/operations/tests/URLEncodeDecode.mjs @@ -89,4 +89,30 @@ TestRegister.addTests([ }, ], }, + { + name: "URLEncode: encodes UTF-8 text as UTF-8 bytes", + input: "你好", + expectedOutput: "%E4%BD%A0%E5%A5%BD", + recipeConfig: [ + { + op: "URL Encode", + args: [false], + }, + ], + }, + { + name: "URLEncode: preserves raw bytes from From Hex", + input: "6c6567697466696c6580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090746869737761737375706f736564746f6265616e6578706c6f6974", + expectedOutput: "legitfile%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90thiswassuposedtobeanexploit", + recipeConfig: [ + { + op: "From Hex", + args: ["None"], + }, + { + op: "URL Encode", + args: [false], + }, + ], + }, ]);