Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	README.md
This commit is contained in:
github-actions[bot] 2026-06-22 11:30:27 +00:00
commit edacc3f810
56 changed files with 1345 additions and 114 deletions

View File

@ -13,6 +13,17 @@ All major and minor version changes will be documented in this file. Details of
## Details ## Details
### [11.2.0] - 2026-06-17
This release includes a security fix ([#2569])
- Security: Chart operation prototype protection [@C85297] | [#2569]
- Update website references [@C85297] | [#2566]
- Fix: Add input validation for XOR Checksum blocksize (#2537) [@dweep-js] | [#2542]
- Fix: Reverse highlights unwind incorrectly [@kendallgoto] [@C85297] | [#2022]
- Fix Uint8Array concat crash in Parse IPv4 header [@Zish19] | [#2409]
- Fix typos and documentation errors (bytes→bits, wrong release link, spelling) [@qa2me] [@GCHQDeveloper581] | [#2404]
- Add integer check for alphabet size [@heapframe] [@GCHQDeveloper581] | [#2458]
- fix: validate hexdump width upper bound [@skyswordw] | [#2514]
### [11.1.0] - 2026-06-13 ### [11.1.0] - 2026-06-13
This release includes a security fix ([#2557]) This release includes a security fix ([#2557])
- Security: Add fix, and tests, for Lorem Ipsum DoS issue [@GCHQDeveloper581] | [#2557] - Security: Add fix, and tests, for Lorem Ipsum DoS issue [@GCHQDeveloper581] | [#2557]
@ -707,6 +718,7 @@ Breaking changes:
## [4.0.0] - 2016-11-28 ## [4.0.0] - 2016-11-28
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
[11.2.0]: https://github.com/gchq/CyberChef/releases/tag/v11.2.0
[11.1.0]: https://github.com/gchq/CyberChef/releases/tag/v11.1.0 [11.1.0]: https://github.com/gchq/CyberChef/releases/tag/v11.1.0
[11.0.0]: https://github.com/gchq/CyberChef/releases/tag/v11.0.0 [11.0.0]: https://github.com/gchq/CyberChef/releases/tag/v11.0.0
[10.24.0]: https://github.com/gchq/CyberChef/releases/tag/v10.24.0 [10.24.0]: https://github.com/gchq/CyberChef/releases/tag/v10.24.0
@ -1001,6 +1013,11 @@ Breaking changes:
[@Louis-Ladd]: https://github.com/Louis-Ladd [@Louis-Ladd]: https://github.com/Louis-Ladd
[@Blank0120]: https://github.com/Blank0120 [@Blank0120]: https://github.com/Blank0120
[@zachbowden]: https://github.com/zachbowden [@zachbowden]: https://github.com/zachbowden
[@dweep-js]: https://github.com/dweep-js
[@Zish19]: https://github.com/Zish19
[@qa2me]: https://github.com/qa2me
[@heapframe]: https://github.com/heapframe
[@skyswordw]: https://github.com/skyswordw
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
@ -1364,4 +1381,12 @@ Breaking changes:
[#2332]: https://github.com/gchq/CyberChef/pull/2332 [#2332]: https://github.com/gchq/CyberChef/pull/2332
[#2353]: https://github.com/gchq/CyberChef/pull/2353 [#2353]: https://github.com/gchq/CyberChef/pull/2353
[#2351]: https://github.com/gchq/CyberChef/pull/2351 [#2351]: https://github.com/gchq/CyberChef/pull/2351
[#2569]: https://github.com/gchq/CyberChef/pull/2569
[#2566]: https://github.com/gchq/CyberChef/pull/2566
[#2542]: https://github.com/gchq/CyberChef/pull/2542
[#2022]: https://github.com/gchq/CyberChef/pull/2022
[#2409]: https://github.com/gchq/CyberChef/pull/2409
[#2404]: https://github.com/gchq/CyberChef/pull/2404
[#2458]: https://github.com/gchq/CyberChef/pull/2458
[#2514]: https://github.com/gchq/CyberChef/pull/2514

View File

@ -4,7 +4,7 @@
# Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds # 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 # 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 # 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 WORKDIR /app
@ -27,7 +27,7 @@ RUN npm run build
######################################### #########################################
# Package static build files into nginx # # 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 <oss@gchq.gov.uk>" LABEL maintainer="GCHQ <oss@gchq.gov.uk>"

View File

@ -1,11 +1,13 @@
# Security Policy # Security Policy
## Supported Versions ## Support
CyberChef is supported on a best endeavours basis. CyberChef is supported on a best endeavours basis.
Patches will be applied to the latest version rather than retroactively to older versions. Patches will be applied to the latest version rather than retroactively to older versions.
To ensure you are using the most secure version of CyberChef, please make sure you have the [latest release](https://github.com/gchq/CyberChef/releases/latest). [The official website](https://gchq.github.io/CyberChef/) is always up to date. To ensure you are using the most secure version of CyberChef, please make sure you have the [latest release](https://github.com/gchq/CyberChef/releases/latest). [The official website](https://gchq.github.io/CyberChef/) is always up to date.
No guarantee is offered for the correctness or security of CyberChef. In paticular, the security of cryptographic operations should not be relied upon.
## Reporting a Vulnerability ## Reporting a Vulnerability
If you discover a vulnerability in CyberChef, please do not publicly disclose it, and do not create a GitHub issue. If you discover a vulnerability in CyberChef, please do not publicly disclose it, and do not create a GitHub issue.

68
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "cyberchef", "name": "cyberchef",
"version": "11.1.0", "version": "11.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cyberchef", "name": "cyberchef",
"version": "11.1.0", "version": "11.2.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -21,7 +21,7 @@
"assert": "^2.1.0", "assert": "^2.1.0",
"avsc": "^5.7.9", "avsc": "^5.7.9",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"bignumber.js": "^11.1.3", "bignumber.js": "^11.1.4",
"blakejs": "^1.2.1", "blakejs": "^1.2.1",
"bootstrap": "4.6.2", "bootstrap": "4.6.2",
"bootstrap-colorpicker": "^3.4.0", "bootstrap-colorpicker": "^3.4.0",
@ -39,7 +39,7 @@
"d3": "7.9.0", "d3": "7.9.0",
"d3-hexbin": "^0.2.2", "d3-hexbin": "^0.2.2",
"diff": "^9.0.0", "diff": "^9.0.0",
"dompurify": "^3.4.8", "dompurify": "^3.4.11",
"es6-promisify": "^7.0.0", "es6-promisify": "^7.0.0",
"escodegen": "^2.1.0", "escodegen": "^2.1.0",
"esprima": "^4.0.1", "esprima": "^4.0.1",
@ -85,7 +85,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"process": "^0.11.10", "process": "^0.11.10",
"protobufjs": "^8.6.2", "protobufjs": "^8.6.4",
"punycode.js": "^2.3.1", "punycode.js": "^2.3.1",
"qr-image": "^3.2.0", "qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
@ -116,7 +116,7 @@
"@babel/runtime": "^7.29.7", "@babel/runtime": "^7.29.7",
"@codemirror/commands": "^6.10.3", "@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3", "@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.7.0", "@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.5.4", "@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.1", "@codemirror/view": "^6.43.1",
"@puppeteer/browsers": "3.0.4", "@puppeteer/browsers": "3.0.4",
@ -159,7 +159,7 @@
"terser": "^5.48.0", "terser": "^5.48.0",
"webpack": "^5.107.2", "webpack": "^5.107.2",
"webpack-bundle-analyzer": "^5.3.0", "webpack-bundle-analyzer": "^5.3.0",
"webpack-dev-server": "^5.2.4", "webpack-dev-server": "^5.2.5",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
"worker-loader": "^3.0.8" "worker-loader": "^3.0.8"
}, },
@ -1876,9 +1876,9 @@
} }
}, },
"node_modules/@codemirror/search": { "node_modules/@codemirror/search": {
"version": "6.7.0", "version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
"integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5794,9 +5794,9 @@
} }
}, },
"node_modules/bignumber.js": { "node_modules/bignumber.js": {
"version": "11.1.3", "version": "11.1.4",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.3.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz",
"integrity": "sha512-+esZiNSo6VgFokTsYX6mYqNJfFd/IczzZCd4Z7cR8e+AQWhvIcj6nqQ1h9814D9u/TApU0jjTVmfWL0Pd1ZBdA==", "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
@ -8585,9 +8585,9 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.4.8", "version": "3.4.11",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
"integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
@ -9865,17 +9865,17 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.5", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2", "hasown": "^2.0.4",
"mime-types": "^2.1.12" "mime-types": "^2.1.35"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@ -10907,9 +10907,9 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -12561,14 +12561,14 @@
} }
}, },
"node_modules/launch-editor": { "node_modules/launch-editor": {
"version": "2.13.1", "version": "2.14.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
"integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==", "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"shell-quote": "^1.8.3" "shell-quote": "^1.8.4"
} }
}, },
"node_modules/lazystream": { "node_modules/lazystream": {
@ -15086,9 +15086,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/protobufjs": { "node_modules/protobufjs": {
"version": "8.6.2", "version": "8.6.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.2.tgz", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.4.tgz",
"integrity": "sha512-CCERJxzRvKMeEdJSLwdQf40TXWNPc8M4RkN7j/lxY6FQB+4do8rETWqj60AqxP9n0XIsxnSefZ8uhAaGKg2njw==", "integrity": "sha512-/+XMv9JalknuncEJSwsyEVlwcxVLKx2iaoSUXFZA86MJkdqyOdfrlB1sB7S6aKyUk9tl20YY+SgQe5J2sJHTcg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"long": "^5.3.2" "long": "^5.3.2"
@ -18112,9 +18112,9 @@
} }
}, },
"node_modules/webpack-dev-server": { "node_modules/webpack-dev-server": {
"version": "5.2.4", "version": "5.2.5",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz",
"integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "cyberchef", "name": "cyberchef",
"version": "11.1.0", "version": "11.2.0",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "GCHQ <CyberChef@gchq.gov.uk>", "author": "GCHQ <CyberChef@gchq.gov.uk>",
"homepage": "https://gchq.github.io/CyberChef", "homepage": "https://gchq.github.io/CyberChef",
@ -46,7 +46,7 @@
"@babel/runtime": "^7.29.7", "@babel/runtime": "^7.29.7",
"@codemirror/commands": "^6.10.3", "@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3", "@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.7.0", "@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.5.4", "@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.1", "@codemirror/view": "^6.43.1",
"@puppeteer/browsers": "3.0.4", "@puppeteer/browsers": "3.0.4",
@ -89,7 +89,7 @@
"terser": "^5.48.0", "terser": "^5.48.0",
"webpack": "^5.107.2", "webpack": "^5.107.2",
"webpack-bundle-analyzer": "^5.3.0", "webpack-bundle-analyzer": "^5.3.0",
"webpack-dev-server": "^5.2.4", "webpack-dev-server": "^5.2.5",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
"worker-loader": "^3.0.8" "worker-loader": "^3.0.8"
}, },
@ -105,7 +105,7 @@
"assert": "^2.1.0", "assert": "^2.1.0",
"avsc": "^5.7.9", "avsc": "^5.7.9",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"bignumber.js": "^11.1.3", "bignumber.js": "^11.1.4",
"blakejs": "^1.2.1", "blakejs": "^1.2.1",
"bootstrap": "4.6.2", "bootstrap": "4.6.2",
"bootstrap-colorpicker": "^3.4.0", "bootstrap-colorpicker": "^3.4.0",
@ -123,7 +123,7 @@
"d3": "7.9.0", "d3": "7.9.0",
"d3-hexbin": "^0.2.2", "d3-hexbin": "^0.2.2",
"diff": "^9.0.0", "diff": "^9.0.0",
"dompurify": "^3.4.8", "dompurify": "^3.4.11",
"es6-promisify": "^7.0.0", "es6-promisify": "^7.0.0",
"escodegen": "^2.1.0", "escodegen": "^2.1.0",
"esprima": "^4.0.1", "esprima": "^4.0.1",
@ -169,7 +169,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"process": "^0.11.10", "process": "^0.11.10",
"protobufjs": "^8.6.2", "protobufjs": "^8.6.4",
"punycode.js": "^2.3.1", "punycode.js": "^2.3.1",
"qr-image": "^3.2.0", "qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",

View File

@ -32,6 +32,8 @@ class Ingredient {
this.min = null; this.min = null;
this.max = null; this.max = null;
this.step = 1; this.step = 1;
this.integer = false;
this.allowEmpty = true;
if (ingredientConfig) { if (ingredientConfig) {
this._parseConfig(ingredientConfig); this._parseConfig(ingredientConfig);
@ -59,6 +61,70 @@ class Ingredient {
this.min = ingredientConfig.min; this.min = ingredientConfig.min;
this.max = ingredientConfig.max; this.max = ingredientConfig.max;
this.step = ingredientConfig.step; 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;
} }

View File

@ -192,11 +192,30 @@ class Operation {
if (typeof ing.min === "number") conf.min = ing.min; if (typeof ing.min === "number") conf.min = ing.min;
if (typeof ing.max === "number") conf.max = ing.max; if (typeof ing.max === "number") conf.max = ing.max;
if (ing.step) conf.step = ing.step; 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; 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. * Returns the value of the Operation as it should be displayed in a recipe config.
* *

View File

@ -212,6 +212,8 @@ class Recipe {
self.sendProgressMessage(i + 1, this.opList.length); self.sendProgressMessage(i + 1, this.opList.length);
} }
op.validateIngredients(op.ingValues);
if (op.flowControl) { if (op.flowControl) {
// Package up the current state // Package up the current state
let state = { let state = {

View File

@ -559,7 +559,7 @@
"Scatter chart", "Scatter chart",
"Series chart", "Series chart",
"Heatmap chart", "Heatmap chart",
"Extract Audio Metadata" "Render PDF"
] ]
}, },
{ {
@ -585,7 +585,8 @@
"HTML To Text", "HTML To Text",
"Generate Lorem Ipsum", "Generate Lorem Ipsum",
"Numberwang", "Numberwang",
"XKCD Random Number" "XKCD Random Number",
"Automated Validation Test Op"
] ]
}, },
{ {

View File

@ -108,14 +108,17 @@ export function mean(data) {
* @returns {BigNumber} * @returns {BigNumber}
*/ */
export function median(data) { export function median(data) {
if ((data.length % 2) === 0 && data.length > 0) { if (data.length > 0) {
data.sort(function(a, b) { data.sort(function(a, b) {
return a.minus(b); return a.minus(b);
}); });
if ((data.length % 2) === 0) {
const first = data[Math.floor(data.length / 2)]; const first = data[Math.floor(data.length / 2)];
const second = data[Math.floor(data.length / 2) - 1]; const second = data[Math.floor(data.length / 2) - 1];
return mean([first, second]); return mean([first, second]);
} else { }
return data[Math.floor(data.length / 2)]; return data[Math.floor(data.length / 2)];
} }
} }

View File

@ -153,7 +153,7 @@ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHe
); );
let xValues = new Set(); let xValues = new Set();
const series = {}; const series = Object.create(null);
values.forEach(row => { values.forEach(row => {
const serie = row[0], const serie = row[0],
@ -163,14 +163,14 @@ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHe
if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10."); if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
xValues.add(xVal); xValues.add(xVal);
if (typeof series[serie] === "undefined") series[serie] = {}; if (typeof series[serie] === "undefined") series[serie] = Object.create(null);
series[serie][xVal] = val; series[serie][xVal] = val;
}); });
xValues = new Array(...xValues); xValues = new Array(...xValues);
const seriesList = []; const seriesList = [];
for (const seriesName in series) { for (const seriesName of Object.keys(series)) {
const serie = series[seriesName]; const serie = series[seriesName];
seriesList.push({name: seriesName, data: serie}); seriesList.push({name: seriesName, data: serie});
} }

View File

@ -8,6 +8,7 @@
import BigNumber from "bignumber.js"; import BigNumber from "bignumber.js";
import {toHexFast} from "../lib/Hex.mjs"; import {toHexFast} from "../lib/Hex.mjs";
import Utils from "../Utils.mjs";
/** /**
* Recursively displays a JSON object as an HTML table * Recursively displays a JSON object as an HTML table
@ -25,15 +26,16 @@ export function objToTable(obj, nested=false) {
<th>Value</th> <th>Value</th>
</tr>`; </tr>`;
for (const key in obj) { for (const key of Object.keys(obj)) {
if (typeof obj[key] === "function") const value = obj[key];
if (typeof value === "function")
continue; continue;
html += `<tr><td style='word-wrap: break-word'>${key}</td>`; html += `<tr><td style='word-wrap: break-word'>${Utils.escapeHtml(String(key))}</td>`;
if (typeof obj[key] === "object") if (value !== null && typeof value === "object")
html += `<td style='padding: 0'>${objToTable(obj[key], true)}</td>`; html += `<td style='padding: 0'>${objToTable(value, true)}</td>`;
else else
html += `<td>${obj[key]}</td>`; html += `<td>${Utils.escapeHtml(String(value))}</td>`;
html += "</tr>"; html += "</tr>";
} }
html += "</table>"; html += "</table>";

View File

@ -33,20 +33,29 @@ export default class TLVParser {
* @returns {number} * @returns {number}
*/ */
getLength() { getLength() {
let bytesInLength = this.bytesInLength;
let bigEndian = false;
if (this.basicEncodingRules) { if (this.basicEncodingRules) {
const bit = this.input[this.location]; const firstLengthByte = this.input[this.location];
if (bit & 0x80) {
this.bytesInLength = bit & ~0x80;
} else {
this.location++; this.location++;
return bit & ~0x80;
if (firstLengthByte & 0x80) {
bytesInLength = firstLengthByte & ~0x80;
bigEndian = true;
} else {
return firstLengthByte & ~0x80;
} }
} }
let length = 0; let length = 0;
for (let i = 0; i < this.bytesInLength; i++) { for (let i = 0; i < bytesInLength; i++) {
if (bigEndian) {
length = (length << 8) + this.input[this.location];
} else {
length += this.input[this.location] * Math.pow(Math.pow(2, 8), i); length += this.input[this.location] * Math.pow(Math.pow(2, 8), i);
}
this.location++; this.location++;
} }

View File

@ -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;

View File

@ -30,7 +30,11 @@ class BLAKE3 extends Operation {
this.args = [ this.args = [
{ {
"name": "Size (bytes)", "name": "Size (bytes)",
"type": "number" "type": "number",
"value": 16,
"min": 1,
"max": 65535, // arbitrary limit to prevent resource exhaustion
"integer": true,
}, { }, {
"name": "Key", "name": "Key",
"type": "string", "type": "string",

View File

@ -27,7 +27,10 @@ class BitShiftLeft extends Operation {
{ {
"name": "Amount", "name": "Amount",
"type": "number", "type": "number",
"value": 1 "value": 1,
"min": 0,
"max": 7,
"integer": true,
} }
]; ];
} }

View File

@ -45,12 +45,15 @@ class DechunkHTTPResponse extends Operation {
const lineEndingsLength = lineEndings.length; const lineEndingsLength = lineEndings.length;
let chunkSize = parseInt(input.slice(0, chunkSizeEnd), 16); let chunkSize = parseInt(input.slice(0, chunkSizeEnd), 16);
while (!isNaN(chunkSize)) { while (!isNaN(chunkSize)) {
if (chunkSize === 0) {
break;
}
chunks.push(input.slice(chunkSizeEnd, chunkSize + chunkSizeEnd)); chunks.push(input.slice(chunkSizeEnd, chunkSize + chunkSizeEnd));
input = input.slice(chunkSizeEnd + chunkSize + lineEndingsLength); input = input.slice(chunkSizeEnd + chunkSize + lineEndingsLength);
chunkSizeEnd = input.indexOf(lineEndings) + lineEndingsLength; chunkSizeEnd = input.indexOf(lineEndings) + lineEndingsLength;
chunkSize = parseInt(input.slice(0, chunkSizeEnd), 16); chunkSize = parseInt(input.slice(0, chunkSizeEnd), 16);
} }
return chunks.join("") + input; return chunks.join("");
} }
} }

View File

@ -51,9 +51,10 @@ class FromBase extends Operation {
if (number.length === 1) return result; if (number.length === 1) return result;
// Fractional part // Fractional part
const radixBN = new BigNumber(radix);
for (let i = 0; i < number[1].length; i++) { for (let i = 0; i < number[1].length; i++) {
const digit = new BigNumber(number[1][i], radix); const digit = new BigNumber(number[1][i], radix);
result += digit.div(Math.pow(radix, i+1)); result = result.plus(digit.div(radixBN.pow(i + 1)));
} }
return result; return result;

View File

@ -74,13 +74,11 @@ class Gzip extends Operation {
} }
if (comment.length) { if (comment.length) {
options.flags.comment = true; options.flags.comment = true;
options.flags.fcomment = true;
options.comment = comment; options.comment = comment;
} }
const gzipObj = new Zlib.Gzip(new Uint8Array(input), options); const gzipObj = new Zlib.Gzip(new Uint8Array(input), options);
const compressed = new Uint8Array(gzipObj.compress()); const compressed = new Uint8Array(gzipObj.compress());
if (options.flags.comment && !(compressed[3] & 0x10)) {
compressed[3] |= 0x10;
}
return compressed.buffer; return compressed.buffer;
} }

View File

@ -51,6 +51,18 @@ class JsonataQuery extends Operation {
try { try {
const expression = jsonata(query); const expression = jsonata(query);
// Override built-in base64 functions which fail in Web Worker
// context where `window` is undefined. The jsonata library falls
// back to `global.Buffer` which also does not exist in workers.
// `atob`/`btoa` are available in both browser and worker scopes.
expression.registerFunction("base64decode", (str) => {
if (typeof str === "undefined") return undefined;
return atob(str);
}, "<s-:s>");
expression.registerFunction("base64encode", (str) => {
if (typeof str === "undefined") return undefined;
return btoa(str);
}, "<s-:s>");
result = await expression.evaluate(jsonObj); result = await expression.evaluate(jsonObj);
} catch (err) { } catch (err) {
throw new OperationError( throw new OperationError(

View File

@ -87,7 +87,7 @@ class MIMEDecoding extends Operation {
end = cur + j + "?=".length; end = cur + j + "?=".length;
if (encoding.toLowerCase() === "b") { if (encoding.toLowerCase() === "b") {
text = fromBase64(text); text = fromBase64(text, undefined, "byteArray");
} else if (encoding.toLowerCase() === "q") { } else if (encoding.toLowerCase() === "q") {
text = this.parseQEncodedWord(text); text = this.parseQEncodedWord(text);
} else { } else {

View File

@ -31,7 +31,8 @@ class PseudoRandomNumberGenerator extends Operation {
{ {
"name": "Number of bytes", "name": "Number of bytes",
"type": "number", "type": "number",
"value": 32 "value": 32,
"min": 1
}, },
{ {
"name": "Output as", "name": "Output as",

View File

@ -0,0 +1,100 @@
/**
* @author Shailendra [singhshailendra.in]
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import { fromBase64, toBase64 } from "../lib/Base64.mjs";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
/**
* Render PDF operation
*/
class RenderPDF extends Operation {
/**
* RenderPDF constructor
*/
constructor() {
super();
this.name = "Render PDF";
this.module = "File";
this.description = "Displays the input as a PDF preview. Supports Raw and Base64 input formats.";
this.inputType = "string";
this.outputType = "byteArray";
this.presentType = "html";
this.args = [
{
"name": "Input format",
"type": "option",
"value": ["Base64", "Raw"],
}
];
this.checks = [
{
pattern: "^%PDF-",
flags: "",
args: ["Raw"],
useful: true,
output: {
mime: "application/pdf"
}
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
const inputFormat = args[0];
if (!input.length) return [];
// Convert input to raw bytes
switch (inputFormat) {
case "Base64":
input = fromBase64(input, undefined, "byteArray");
break;
case "Raw":
default:
input = Utils.strToByteArray(input);
break;
}
// Check PDF signature
if (
input[0] !== 0x25 || // %
input[1] !== 0x50 || // P
input[2] !== 0x44 || // D
input[3] !== 0x46 // F
) {
throw new OperationError("Input does not appear to be a PDF file.");
}
return input;
}
/**
* Displays the PDF using HTML for web apps.
*
* @param {byteArray} data
* @returns {html}
*/
async present(data) {
if (!data.length) return "";
const base64 = toBase64(data);
const dataURI = "data:application/pdf;base64," + base64;
return `<iframe src="${dataURI}" style="width:100%;height:100%;border:1px solid #ccc;"></iframe>`;
}
}
export default RenderPDF;

View File

@ -75,9 +75,16 @@ class SetDifference extends Operation {
* @returns {Object[]} * @returns {Object[]}
*/ */
runSetDifference(a, b) { runSetDifference(a, b) {
const excluded = new Set(b);
const seen = new Set();
return a return a
.filter((item) => { .filter((item) => {
return b.indexOf(item) === -1; if (excluded.has(item) || seen.has(item)) {
return false;
}
seen.add(item);
return true;
}) })
.join(this.itemDelimiter); .join(this.itemDelimiter);
} }

View File

@ -75,9 +75,16 @@ class SetIntersection extends Operation {
* @returns {Object[]} * @returns {Object[]}
*/ */
runIntersect(a, b) { runIntersect(a, b) {
const included = new Set(b);
const seen = new Set();
return a return a
.filter((item) => { .filter((item) => {
return b.indexOf(item) > -1; if (!included.has(item) || seen.has(item)) {
return false;
}
seen.add(item);
return true;
}) })
.join(this.itemDelimiter); .join(this.itemDelimiter);
} }

View File

@ -71,6 +71,16 @@ class ShowOnMap extends Operation {
} }
latLong = latLong.replace(/[,]$/, ""); latLong = latLong.replace(/[,]$/, "");
latLong = latLong.replace(/°/g, ""); latLong = latLong.replace(/°/g, "");
// The map requires a latitude and longitude pair. If the conversion only produced a
// single value (e.g. because the chosen input delimiter didn't match the input), bail
// out with a helpful message rather than passing it on to the map, which would throw an
// uncaught TypeError in the browser.
const coords = latLong.split(",").map(v => v.trim());
if (coords.length !== 2 || coords.some(v => v === "" || isNaN(Number(v)))) {
throw new OperationError(`Could not show coordinates '${latLong}' on the map. Expected a latitude and longitude pair - check that the input format and delimiter are correct.`);
}
return latLong; return latLong;
} }
return input; return input;

View File

@ -28,7 +28,10 @@ class ToBase extends Operation {
{ {
"name": "Radix", "name": "Radix",
"type": "number", "type": "number",
"value": 36 "value": 36,
"min": 2,
"max": 36,
"integer": true,
} }
]; ];
} }
@ -43,9 +46,6 @@ class ToBase extends Operation {
throw new OperationError("Error: Input must be a number"); throw new OperationError("Error: Input must be a number");
} }
const radix = args[0]; const radix = args[0];
if (radix < 2 || radix > 36) {
throw new OperationError("Error: Radix argument must be between 2 and 36");
}
return input.toString(radix); return input.toString(radix);
} }

View File

@ -35,7 +35,10 @@ class ToBinary extends Operation {
{ {
"name": "Byte Length", "name": "Byte Length",
"type": "number", "type": "number",
"value": 8 "value": 8,
"min": 1,
"max": 256, // arbitrary - significantly larger than word size for any known machine ("640k ought to be enough for anybody")
"integer": true
} }
]; ];
} }

View File

@ -21,7 +21,7 @@ class URLEncode extends Operation {
this.module = "URL"; this.module = "URL";
this.description = "Encodes problematic characters into percent-encoding, a format supported by URIs/URLs.<br><br>e.g. <code>=</code> becomes <code>%3d</code>"; this.description = "Encodes problematic characters into percent-encoding, a format supported by URIs/URLs.<br><br>e.g. <code>=</code> becomes <code>%3d</code>";
this.infoURL = "https://wikipedia.org/wiki/Percent-encoding"; this.infoURL = "https://wikipedia.org/wiki/Percent-encoding";
this.inputType = "string"; this.inputType = "byteArray";
this.outputType = "string"; this.outputType = "string";
this.args = [ this.args = [
{ {
@ -33,34 +33,38 @@ class URLEncode extends Operation {
} }
/** /**
* @param {string} input * @param {byteArray} input
* @param {Object[]} args * @param {Object[]} args
* @returns {string} * @returns {string}
*/ */
run(input, args) { run(input, args) {
const encodeAll = args[0]; 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} * @returns {string}
*/ */
encodeAllChars (str) { encodeBytes(bytes, encodeAll) {
// TODO Do this programmatically const safeChars = encodeAll ?
return encodeURIComponent(str) /^[A-Za-z0-9]$/ :
.replace(/!/g, "%21") /^[A-Za-z0-9:/?#[\]@!$&'()*+,;=%]$/;
.replace(/#/g, "%23")
.replace(/'/g, "%27") let output = "";
.replace(/\(/g, "%28")
.replace(/\)/g, "%29") for (const byte of bytes) {
.replace(/\*/g, "%2A") const char = String.fromCharCode(byte);
.replace(/-/g, "%2D")
.replace(/\./g, "%2E") output += safeChars.test(char) ?
.replace(/_/g, "%5F") char :
.replace(/~/g, "%7E"); "%" + byte.toString(16).toUpperCase().padStart(2, "0");
}
return output;
} }
} }

View File

@ -56,7 +56,8 @@ class UnescapeUnicodeCharacters extends Operation {
*/ */
run(input, args) { run(input, args) {
const prefix = prefixToRegex[args[0]], const prefix = prefixToRegex[args[0]],
regex = new RegExp(prefix+"([a-f\\d]{4})", "ig"); quantifier = args[0] === "U+" ? "{4,6}" : "{4}",
regex = new RegExp(prefix+"([a-f\\d]"+quantifier+")", "ig");
let output = "", let output = "",
m, m,
i = 0; i = 0;

View File

@ -31,7 +31,10 @@ class XORBruteForce extends Operation {
{ {
"name": "Key length", "name": "Key length",
"type": "number", "type": "number",
"value": 1 "value": 1,
"min": 1,
"max": 2,
"integer": true
}, },
{ {
"name": "Sample length", "name": "Sample length",

View File

@ -7,12 +7,12 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs"; import Utils from "../Utils.mjs";
import { toHex } from "../lib/Hex.mjs"; import { toHex } from "../lib/Hex.mjs";
import OperationError from "../errors/OperationError.mjs";
/** /**
* XOR Checksum operation * XOR Checksum operation
*/ */
class XORChecksum extends Operation { class XORChecksum extends Operation {
/** /**
* XORChecksum constructor * XORChecksum constructor
*/ */
@ -21,7 +21,8 @@ class XORChecksum extends Operation {
this.name = "XOR Checksum"; this.name = "XOR Checksum";
this.module = "Crypto"; this.module = "Crypto";
this.description = "XOR Checksum splits the input into blocks of a configurable size and performs the XOR operation on these blocks."; this.description =
"XOR Checksum splits the input into blocks of a configurable size and performs the XOR operation on these blocks.";
this.infoURL = "https://wikipedia.org/wiki/XOR"; this.infoURL = "https://wikipedia.org/wiki/XOR";
this.inputType = "ArrayBuffer"; this.inputType = "ArrayBuffer";
this.outputType = "string"; this.outputType = "string";
@ -29,7 +30,7 @@ class XORChecksum extends Operation {
{ {
name: "Blocksize", name: "Blocksize",
type: "number", type: "number",
value: 4 value: 4,
}, },
]; ];
} }
@ -41,6 +42,12 @@ class XORChecksum extends Operation {
*/ */
run(input, args) { run(input, args) {
const blocksize = args[0]; const blocksize = args[0];
if (!Number.isInteger(blocksize) || blocksize <= 0) {
throw new OperationError("Blocksize must be a positive integer.");
}
input = new Uint8Array(input); input = new Uint8Array(input);
const res = Array(blocksize); const res = Array(blocksize);

View File

@ -193,6 +193,8 @@ export function _wrap(OpClass) {
wrapped = async (input, args=null) => { wrapped = async (input, args=null) => {
const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args);
opInstance.validateIngredients(transformedArgs);
// SPECIAL CASE for Magic. Other flowControl operations will // SPECIAL CASE for Magic. Other flowControl operations will
// not work because the opList is not passed in. // not work because the opList is not passed in.
if (isFlowControl) { if (isFlowControl) {
@ -229,6 +231,7 @@ export function _wrap(OpClass) {
*/ */
wrapped = (input, args=null) => { wrapped = (input, args=null) => {
const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args);
opInstance.validateIngredients(transformedArgs);
const result = opInstance.run(transformedInput, transformedArgs); const result = opInstance.run(transformedInput, transformedArgs);
return new NodeDish({ return new NodeDish({
value: result, value: result,

View File

@ -58,9 +58,10 @@ class HTMLOperation {
if (this.description) { if (this.description) {
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : ""; const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
const content = Utils.escapeHtml(this.description + infoLink);
html += ` data-container='body' data-toggle='popover' data-placement='right' html += ` data-container='body' data-toggle='popover' data-placement='right'
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover' data-content="${content}" data-html='true' data-trigger='hover'
data-boundary='viewport' role='button'`; data-boundary='viewport' role='button'`;
} }

View File

@ -24,6 +24,14 @@
height: 100%; height: 100%;
user-select: auto; 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 .cm-widgetBuffer,
#output-text.html-output .cm-line>br { #output-text.html-output .cm-line>br {
display: none; display: none;

View File

@ -56,6 +56,32 @@ module.exports = {
browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present; browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present; browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present; browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present;
browser.expect.element("//li[contains(@class, 'operation') and text()='Escape Smart Characters']").to.be.present;
},
"Operation popover descriptions render HTML safely": browser => {
const favouritesCat = "//a[contains(@class, 'category-title') and contains(@data-target, '#catFavourites')]",
op = "//ul[@id='search-results']//li[contains(@class, 'operation') and contains(., 'Escape Smart Characters')]";
browser
.useCss()
.clearValue("#search")
.setValue("#search", "Escape Smart Characters")
.useXpath()
.waitForElementVisible(op, 1000)
.moveToElement(op, 10, 10)
.useCss()
.waitForElementVisible(".popover-body code:last-of-type", 1000)
.expect.element(".popover-body code:last-of-type").text.to.contain("\"Hello\" -- world...");
browser
.useCss()
.moveToElement("#operations .title", 1, 1)
.waitForElementNotPresent(".popover-body", 1000)
.clearValue("#search")
.useXpath()
.getLocationInView(favouritesCat)
.click(favouritesCat);
}, },
"Recipe can be run": browser => { "Recipe can be run": browser => {

View File

@ -218,6 +218,7 @@ module.exports = {
testOpHtml(browser, "JSON Beautify", "{a:1}", ".json-dict .json-literal", "1"); testOpHtml(browser, "JSON Beautify", "{a:1}", ".json-dict .json-literal", "1");
// testOp(browser, "JSON Minify", "test input", "test_output"); // testOp(browser, "JSON Minify", "test input", "test_output");
// testOp(browser, "JSON to CSV", "test input", "test_output"); // testOp(browser, "JSON to CSV", "test input", "test_output");
testOp(browser, "Jsonata Query", '{"a": "SGVsbG8gV29ybGQh"}', '"Hello World!"', ["$base64decode($.a)"]);
// testOp(browser, "JWT Decode", "test input", "test_output"); // testOp(browser, "JWT Decode", "test input", "test_output");
// testOp(browser, "JWT Sign", "test input", "test_output"); // testOp(browser, "JWT Sign", "test input", "test_output");
// testOp(browser, "JWT Verify", "test input", "test_output"); // testOp(browser, "JWT Verify", "test input", "test_output");
@ -492,7 +493,22 @@ function testOpImage(browser, opName, filename, args=[]) {
browser browser
.waitForElementVisible("#output-html img") .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 /** @function

View File

@ -25,6 +25,7 @@ import "./tests/NodeDish.mjs";
import "./tests/Utils.mjs"; import "./tests/Utils.mjs";
import "./tests/Categories.mjs"; import "./tests/Categories.mjs";
import "./tests/lib/BigIntUtils.mjs"; import "./tests/lib/BigIntUtils.mjs";
import "./tests/lib/ChartsProtocolPrototypePollution.mjs";
const testStatus = { const testStatus = {
allTestsPassing: true, allTestsPassing: true,

View File

@ -0,0 +1,90 @@
import TestRegister from "../../../lib/TestRegister.mjs";
import {getSeriesValues} from "../../../../src/core/lib/Charts.mjs";
import {objToTable} from "../../../../src/core/lib/Protocol.mjs";
import SeriesChart from "../../../../src/core/operations/SeriesChart.mjs";
import ParseUDP from "../../../../src/core/operations/ParseUDP.mjs";
import it from "../../assertionHandler.mjs";
import assert from "assert";
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
TestRegister.addApiTests([
it("Charts: should not pollute Object.prototype from a __proto__ series name", () => {
const xVal = "<img src=x onerror=alert(1)>";
delete Object.prototype[xVal];
try {
const result = getSeriesValues(`__proto__,${xVal},1`, "\n", ",", false);
assert.equal(Object.prototype[xVal], undefined);
assert.deepEqual(result.xValues, [xVal]);
assert.equal(result.series.length, 1);
assert.equal(result.series[0].name, "__proto__");
assert.equal(Object.getPrototypeOf(result.series[0].data), null);
assert(hasOwn(result.series[0].data, xVal));
assert.equal(result.series[0].data[xVal], 1);
} finally {
delete Object.prototype[xVal];
}
}),
it("Charts: should keep __proto__ x-axis names as own data keys", () => {
const result = getSeriesValues("safe,__proto__,1", "\n", ",", false);
assert.equal(result.series.length, 1);
assert.equal(Object.getPrototypeOf(result.series[0].data), null);
assert(hasOwn(result.series[0].data, "__proto__"));
assert.equal(result.series[0].data.__proto__, 1);
}),
it("Protocol: should ignore inherited properties when rendering tables", () => {
const inheritedKey = "<img src=x onerror=alert(1)>";
delete Object.prototype[inheritedKey];
try {
Object.prototype[inheritedKey] = "polluted";
const html = objToTable({safe: "value"});
assert(!html.includes(inheritedKey));
assert(!html.includes("polluted"));
assert(html.includes("safe"));
assert(html.includes("value"));
} finally {
delete Object.prototype[inheritedKey];
}
}),
it("Protocol: should escape table keys and scalar values", () => {
const obj = {
"<b>field</b>": "<img src=x onerror=alert(1)>",
};
const html = objToTable(obj);
assert(!html.includes("<b>field</b>"));
assert(!html.includes("<img src=x onerror=alert(1)>"));
assert(html.includes("&lt;b&gt;field&lt;/b&gt;"));
assert(html.includes("&lt;img src=x onerror=alert(1)&gt;"));
}),
it("Series chart and Parse UDP: should not expose polluted prototype data as HTML", () => {
const xVal = "<img src=x onerror=alert(document.domain)>";
delete Object.prototype[xVal];
try {
const chartHtml = new SeriesChart().run(
`__proto__,${xVal},1`,
["Line feed", "Comma", "", 1, "red"]
);
assert.equal(Object.prototype[xVal], undefined);
const parseUDP = new ParseUDP();
const tableHtml = parseUDP.present(parseUDP.run(chartHtml, ["Raw"]));
assert(!/<img|onerror|alert\(/.test(tableHtml));
} finally {
delete Object.prototype[xVal];
}
}),
]);

View File

@ -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": "" }]
}
]
}
]);

View File

@ -69,5 +69,43 @@ TestRegister.addTests([
{ "op": "BLAKE3", { "op": "BLAKE3",
"args": [16390, "ThiskeyisexactlythirtytwoBytesLo"] } "args": [16390, "ThiskeyisexactlythirtytwoBytesLo"] }
] ]
},
// test vectors from https://github.com/BLAKE3-team/BLAKE3/blob/master/test_vectors/test_vectors.json
{
name: "BLAKE3: Std test vector - 0 bytes input, plain hash",
input: "",
expectedOutput: "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d",
recipeConfig: [
{
"op": "BLAKE3",
"args": [131, ""]
} }
]
},
{
name: "BLAKE3: Std test vector - 0 bytes input, keyed hash",
input: "",
expectedOutput: "92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f",
recipeConfig: [
{
"op": "BLAKE3",
"args": [131, "whats the Elvish word for friend"]
}
]
},
{
name: "BLAKE3: Std test vector - 7 bytes input, keyed hash",
input: "0001020304050607",
expectedOutput: "be2f5495c61cba1bb348a34948c004045e3bd4dae8f0fe82bf44d0da245a060048eb5e68ce6dea1eb0229e144f578b3aa7e9f4f85febd135df8525e6fe40c6f0340d13dd09b255ccd5112a94238f2be3c0b5b7ecde06580426a93e0708555a265305abf86d874e34b4995b788e37a823491f25127a502fe0704baa6bfdf04e76c13276",
recipeConfig: [
{
"op": "From Hex",
args: [],
},
{
"op": "BLAKE3",
"args": [131, "whats the Elvish word for friend"]
}
]
},
]); ]);

View File

@ -0,0 +1,66 @@
/**
* DechunkHTTPResponse operation tests.
*
* @author Willi Ballenthin
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Dechunk HTTP response: CRLF line endings",
input: "7\r\nMozilla\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\n",
expectedOutput: "MozillaDeveloperNetwork",
recipeConfig: [
{
op: "Dechunk HTTP response",
args: [],
},
],
},
{
name: "Dechunk HTTP response: LF line endings",
input: "7\nMozilla\n9\nDeveloper\n7\nNetwork\n0\n\n",
expectedOutput: "MozillaDeveloperNetwork",
recipeConfig: [
{
op: "Dechunk HTTP response",
args: [],
},
],
},
{
name: "Dechunk HTTP response: single chunk",
input: "5\r\nHello\r\n0\r\n\r\n",
expectedOutput: "Hello",
recipeConfig: [
{
op: "Dechunk HTTP response",
args: [],
},
],
},
{
name: "Dechunk HTTP response: trailing headers discarded",
input: "7\nMozilla\n9\nDeveloper\n7\nNetwork\n0\nExpires: Wed, 21 Oct 2015 07:28:00 GMT\n",
expectedOutput: "MozillaDeveloperNetwork",
recipeConfig: [
{
op: "Dechunk HTTP response",
args: [],
},
],
},
{
name: "Dechunk HTTP response: hex chunk sizes",
input: "a\r\n0123456789\r\n0\r\n\r\n",
expectedOutput: "0123456789",
recipeConfig: [
{
op: "Dechunk HTTP response",
args: [],
},
],
},
]);

View File

@ -0,0 +1,66 @@
/**
* From Base operation tests.
*
* @author Willi Ballenthin
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "From Base: binary integer",
input: "1010",
expectedOutput: "10",
recipeConfig: [
{
op: "From Base",
args: [2],
},
],
},
{
name: "From Base: binary fraction",
input: "10.1",
expectedOutput: "2.5",
recipeConfig: [
{
op: "From Base",
args: [2],
},
],
},
{
name: "From Base: hex fraction",
input: "a.8",
expectedOutput: "10.5",
recipeConfig: [
{
op: "From Base",
args: [16],
},
],
},
{
name: "From Base: octal integer",
input: "77",
expectedOutput: "63",
recipeConfig: [
{
op: "From Base",
args: [8],
},
],
},
{
name: "From Base: octal fraction",
input: "7.4",
expectedOutput: "7.5",
recipeConfig: [
{
op: "From Base",
args: [8],
},
],
},
]);

View File

@ -86,4 +86,64 @@ TestRegister.addTests([
} }
] ]
}, },
{
name: "Gzip: Comment with checksum round-trips through Gunzip",
input: "hello hello hello",
expectedOutput: "hello hello hello",
recipeConfig: [
{
op: "Gzip",
args: ["Dynamic Huffman Coding", "", "test", true]
},
{
op: "Gunzip",
args: []
}
]
},
{
name: "Gzip: Filename and comment with checksum round-trips through Gunzip",
input: "The quick brown fox jumped over the slow dog",
expectedOutput: "The quick brown fox jumped over the slow dog",
recipeConfig: [
{
op: "Gzip",
args: ["Dynamic Huffman Coding", "file.txt", "a comment", true]
},
{
op: "Gunzip",
args: []
}
]
},
{
name: "Gzip: No comment, with checksum round-trips through Gunzip",
input: "The quick brown fox jumped over the slow dog",
expectedOutput: "The quick brown fox jumped over the slow dog",
recipeConfig: [
{
op: "Gzip",
args: ["Dynamic Huffman Coding", "", "", true]
},
{
op: "Gunzip",
args: []
}
]
},
{
name: "Gzip: No options round-trips through Gunzip",
input: "The quick brown fox jumped over the slow dog",
expectedOutput: "The quick brown fox jumped over the slow dog",
recipeConfig: [
{
op: "Gzip",
args: ["Dynamic Huffman Coding", "", "", false]
},
{
op: "Gunzip",
args: []
}
]
},
]); ]);

View File

@ -129,7 +129,7 @@ TestRegister.addTests([
{ {
name: "To Hexdump: Width too large", name: "To Hexdump: Width too large",
input: "H", input: "H",
expectedOutput: "Width must be no more than 65536", expectedOutput: "Width must be less than or equal to 65536.",
recipeConfig: [ recipeConfig: [
{ {
op: "To Hexdump", op: "To Hexdump",

View File

@ -548,4 +548,27 @@ TestRegister.addTests([
}, },
], ],
}, },
// Base64 functions (issue #2063)
{
name: "Jsonata: $base64decode",
input: "{}",
expectedOutput: '"Hello World!"',
recipeConfig: [
{
op: "Jsonata Query",
args: ['$base64decode("SGVsbG8gV29ybGQh")'],
},
],
},
{
name: "Jsonata: $base64encode",
input: "{}",
expectedOutput: '"SGVsbG8gV29ybGQh"',
recipeConfig: [
{
op: "Jsonata Query",
args: ['$base64encode("Hello World!")'],
},
],
},
]); ]);

View File

@ -75,6 +75,39 @@ TestRegister.addTests([
} }
] ]
}, },
{
name: "UTF-8 Base64 non-ASCII",
input: "Subject: =?UTF-8?B?Y2Fmw6k=?=",
expectedOutput: "Subject: café",
recipeConfig: [
{
"op": "MIME Decoding",
"args": []
}
]
},
{
name: "UTF-8 Base64 multibyte CJK",
input: "Subject: =?UTF-8?B?5pel5pys6Kqe?=",
expectedOutput: "Subject: 日本語",
recipeConfig: [
{
"op": "MIME Decoding",
"args": []
}
]
},
{
name: "UTF-8 Base64 ASCII-only",
input: "Subject: =?UTF-8?B?aGVsbG8=?=",
expectedOutput: "Subject: hello",
recipeConfig: [
{
"op": "MIME Decoding",
"args": []
}
]
},
{ {
name: "ISO Decoding", name: "ISO Decoding",
input: "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\nTo: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\nCC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\nSubject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=", input: "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\nTo: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\nCC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\nSubject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=",

View File

@ -0,0 +1,33 @@
/**
* Median operation tests.
*
* @author copilot-swe-agent[bot]
* @copyright Crown Copyright 2018
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Median: odd-length input",
input: "10 1 2",
expectedOutput: "2",
recipeConfig: [
{
op: "Median",
args: ["Space"],
},
],
},
{
name: "Median: even-length input",
input: "10 1 2 5",
expectedOutput: "3.5",
recipeConfig: [
{
op: "Median",
args: ["Space"],
},
],
},
]);

View File

@ -52,5 +52,46 @@ TestRegister.addTests([
"args": [1, 4, true] // length value is patently wrong, should be ignored by BER. "args": [1, 4, true] // length value is patently wrong, should be ignored by BER.
} }
] ]
},
{
name: "Parse TLV: BER long-form length (two-byte length encoding)",
input: "\x01\x82\x01\x00" + "A".repeat(256) + "\x02\x03\x41\x42\x43",
expectedOutput: JSON.stringify([
{"key": [1], "length": 256, "value": Array(256).fill(65)},
{"key": [2], "length": 3, "value": [65, 66, 67]}
], null, 4),
recipeConfig: [
{
"op": "Parse TLV",
"args": [1, 1, true]
}
]
},
{
name: "Parse TLV: BER long-form length (one-byte length encoding)",
input: "\x01\x81\x80" + "B".repeat(128),
expectedOutput: JSON.stringify([
{"key": [1], "length": 128, "value": Array(128).fill(66)}
], null, 4),
recipeConfig: [
{
"op": "Parse TLV",
"args": [1, 1, true]
}
]
},
{
name: "Parse TLV: BER multiple entries with mixed short and long-form lengths",
input: "\x01\x05\x48\x65\x6c\x6c\x6f\x02\x81\x05\x57\x6f\x72\x6c\x64",
expectedOutput: JSON.stringify([
{"key": [1], "length": 5, "value": [72, 101, 108, 108, 111]},
{"key": [2], "length": 5, "value": [87, 111, 114, 108, 100]}
], null, 4),
recipeConfig: [
{
"op": "Parse TLV",
"args": [1, 1, true]
}
]
} }
]); ]);

View File

@ -0,0 +1,37 @@
/**
* RenderPDF tests.
*
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "RenderPDF",
input: "Not a PDF",
expectedOutput: "Input does not appear to be a PDF file.",
recipeConfig: [
{
op: "Render PDF",
args: ["Raw"]
},
],
},
{
name: "RenderPDF",
input: "",
expectedMatch: /^<iframe src="data:application\/pdf;base64,JVBERi0xLjAKCjEgMCBvYmogPDwg/,
recipeConfig: [
{
"op": "Generate QR Code",
"args": ["PDF", 1, 1, "Low"]
},
{
"op": "Render PDF",
"args": ["Raw"]
}
],
},
]);

View File

@ -53,4 +53,26 @@ TestRegister.addTests([
}, },
], ],
}, },
{
name: "Set Difference: duplicates in first set are removed",
input: "red,red,blue\n\nblue",
expectedOutput: "red",
recipeConfig: [
{
op: "Set Difference",
args: ["\n\n", ","],
},
],
},
{
name: "Set Difference: duplicates in both sets",
input: "1 1 2 2 3\n\n2 2 3 3",
expectedOutput: "1",
recipeConfig: [
{
op: "Set Difference",
args: ["\n\n", " "],
},
],
},
]); ]);

View File

@ -52,5 +52,27 @@ TestRegister.addTests([
args: ["z", "-"], args: ["z", "-"],
}, },
], ],
} },
{
name: "Set Intersection: duplicates in first set are removed",
input: "red,red,blue\n\nred,blue",
expectedOutput: "red,blue",
recipeConfig: [
{
op: "Set Intersection",
args: ["\n\n", ","],
},
],
},
{
name: "Set Intersection: duplicates in both sets",
input: "1 1 2 2 3\n\n2 2 3 3 4",
expectedOutput: "2 3",
recipeConfig: [
{
op: "Set Intersection",
args: ["\n\n", " "],
},
],
},
]); ]);

View File

@ -0,0 +1,39 @@
/**
* Show on map tests
*
* @author Leon Zandman [leon@wirwar.com]
*
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Show on map: valid coordinate pair",
input: "51.5007, -0.1246",
// The presented output is the Leaflet map HTML; just check the coordinates made it through.
expectedMatch: /51\.5007,-0\.1246/,
recipeConfig: [
{
op: "Show on map",
args: [13, "Auto", "Auto"]
},
],
},
{
// Regression test: a comma-separated input with the delimiter set to "\n" used to be
// mis-detected as a single Degrees Decimal Minutes value (1° 24' = 1.4°), producing a single
// coordinate. That single value was then passed to Leaflet's setView([1.4], ...), throwing
// an uncaught "Cannot read properties of null (reading 'lat')" TypeError in the browser.
name: "Show on map: single value is rejected with a helpful error",
input: "1, 24",
expectedOutput: "Could not show coordinates '1.4' on the map. Expected a latitude and longitude pair - check that the input format and delimiter are correct.",
recipeConfig: [
{
op: "Show on map",
args: [13, "Auto", "\\n"]
},
],
},
]);

View File

@ -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],
},
],
},
]); ]);

View File

@ -0,0 +1,88 @@
/**
* Unescape Unicode Characters operation tests.
*
* @author williballenthin
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Unescape Unicode Characters: \\u 4-digit BMP",
input: "\\u03c3\\u03bf\\u03c5",
expectedOutput: "σου",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["\\u"],
},
],
},
{
name: "Unescape Unicode Characters: %u 4-digit BMP",
input: "%u03c3%u03bf%u03c5",
expectedOutput: "σου",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["%u"],
},
],
},
{
name: "Unescape Unicode Characters: U+ 4-digit BMP",
input: "U+0041",
expectedOutput: "A",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["U+"],
},
],
},
{
name: "Unescape Unicode Characters: U+ 5-digit astral plane emoji",
input: "U+1F600",
expectedOutput: "\u{1F600}",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["U+"],
},
],
},
{
name: "Unescape Unicode Characters: U+ 6-digit zero-padded",
input: "U+000041",
expectedOutput: "A",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["U+"],
},
],
},
{
name: "Unescape Unicode Characters: U+ mixed lengths",
input: "U+0041 U+1F600 U+000042",
expectedOutput: "A \u{1F600} B",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["U+"],
},
],
},
{
name: "Unescape Unicode Characters: passthrough with no matches",
input: "hello world",
expectedOutput: "hello world",
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["\\u"],
},
],
},
]);

View File

@ -88,8 +88,8 @@ module.exports = {
from: "tesseract/**/*", from: "tesseract/**/*",
to: "assets/" to: "assets/"
}, { }, {
context: "node_modules/tesseract.js/", context: "node_modules/tesseract.js/dist",
from: "dist/worker.min.js", from: "worker.min.js",
to: "assets/tesseract" to: "assets/tesseract"
}, { }, {
context: "node_modules/tesseract.js-core/", context: "node_modules/tesseract.js-core/",
@ -221,7 +221,7 @@ module.exports = {
}, },
{ // Third party images are inlined { // Third party images are inlined
test: /\.(png|jpg|gif)$/, test: /\.(png|jpg|gif)$/,
exclude: /web\/static/, include: /node_modules/,
type: "asset/inline", type: "asset/inline",
}, },
] ]