diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index e8a120ed..f3c6f711 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,10 +16,10 @@ jobs: pages: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set node version - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index aee3d22a..5ee27086 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -12,10 +12,10 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set node version - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index ae92cdf9..81e1ae1c 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -22,10 +22,10 @@ jobs: contents: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set node version - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 registry-url: "https://registry.npmjs.org" @@ -110,10 +110,10 @@ jobs: needs: main runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set node version - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 registry-url: "https://registry.npmjs.org" diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a55714..79786716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,75 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [11.3.0] - 2026-07-24 +This release includes a security fix ([#2687]) +- Security: Fix pretty recipe parser ReDoS [@zainnadeem786] | [#2687] +- feat: add modulo operation [@thomasnemer] [@GCHQDeveloper581] | [#2103] +- Add HMAC regression tests for Decimal key parsing [@alleria173] | [#2680] +- fix: await Node API operations whose run() returns a non-async Promise [@roberson-io] | [#2659] +- chore (deps): bump morgan from 1.10.1 to 1.11.0 | [#2676] +- fix: fromDecimal Auto delimiter now correctly parses multiple numbers [@min23asdw] | [#2270] +- Add Generate Prime Number operation [@p-leriche] | [#2212] +- Add Modular Inverse operation [@p-leriche] | [#2207] +- Consolidate HTML entity tables into a single spec-generated source (#2645) [@roberson-io] | [#2671] +- Add Extended GCD operation [@p-leriche] | [#2206] +- Add COBS encoding/decoding operations [@giesmininkas] | [#2185] +- chore (deps): bump websocket-driver from 0.7.4 to 0.7.5 | [#2673] +- fix: remove stray punctuation from malformed To HTML Entity table values [@roberson-io] | [#2660] +- chore (deps): bump the actions-dependencies group across 1 directory with 6 updates | [#2668] +- chore (deps): bump the minor-updates group across 1 directory with 3 updates | [#2669] +- chore (deps): bump the patch-updates group with 5 updates | [#2654] +- feat: add TEA and XTEA block ciphers [@thomasxm] | [#2225] +- feat: add PRESENT and Twofish ciphers [@thomasxm] | [#2157] +- fix: support constructor and __proto__ parameters in Parse URI (#2578) [@mansiverma897993] | [#2581] +- feat: Implement automated option-type ingredient validation [@mansiverma897993] | [#2625] +- Add Ascon (NIST SP 800-232) operations: Hash, MAC, Encrypt, Decrypt [@thomasxm] | [#2155] +- chore (deps): bump the patch-updates group across 1 directory with 6 updates | [#2638] +- chore (deps): bump webpack from 5.107.2 to 5.108.3 in the minor-updates group | [#2635] +- chore (deps): bump nginxinc/nginx-unprivileged from `458ecbe` to `fd3314e` in the docker-dependencies group | [#2633] +- Feature: automatically expire PRs if CLA remains unsigned for an extended period [@GCHQDeveloper581] | [#2636] +- fix/2445 HOTP (and 2426 TOTP) type errors [@alleria173] | [#2620] +- Fix base32 unicode alphabet [@loki1205] | [#2380] +- Add a workflow to automatically flag PRs without a signed CLA [@GCHQDeveloper581] | [#2627] +- fix/2444 TOTP input validation for correct otpauth uri generation [@alleria173] | [#2621] +- Validate Wrap line width [@vetrovk] [@GCHQDeveloper581] [@C85297] | [#2606] +- Handle malformed image parser errors in View Bit Plane [@zainnadeem786] | [#2612] +- Fixes #2446 hotp otpauth uri validation [@alleria173] | [#2614] +- Handle invalid bcrypt salt errors in Bcrypt compare [@zainnadeem786] | [#2615] +- Validate empty Show On Map options [@vetrovk] | [#2631] +- Create AGENTS.md file [@C85297] | [#2619] +- Set parameter validation Metadata for GenerateImage operations [@GCHQDeveloper581] | [#2611] +- Update 4 vulnerable dependencies [@GCHQDeveloper581] | [#2616] +- Fix BigNumber deserialisation in Dish, and add tests [@GCHQDeveloper581] | [#2607] +- chore (deps): bump the docker-dependencies group with 2 updates | [#2600] +- chore (deps): bump the patch-updates group with 8 updates | [#2602] +- chore (deps): bump actions/checkout from 6.0.3 to 7.0.0 in the actions-dependencies group | [#2601] +- chore (deps): bump the minor-updates group with 2 updates | [#2603] +- Handle empty Generate Image mode [@vetrovk] | [#2598] +- Fix stale presenter after expected operation errors [@zainnadeem786] [@GCHQDeveloper581] | [#2589] +- Clean up/rationalise webpack paths and thereby increase compatibility for Win… [@GCHQDeveloper581] | [#2585] +- Improve parameter validation for a number of operations where exceptions otherwise caused. [@GCHQDeveloper581] | [#2586] +- Fix uncaught TypeError in "Show on map" operation. [@lzandman] | [#2453] +- fix: jsonata $base64decode/$base64encode in Web Worker [@min23asdw] | [#2275] +- fix Dechunk HTTP Response leaks terminating chunk and trailers into output [@williballenthin] | [#2290] +- fix: MIME Decoding corrupts non-ASCII characters in Base64-encoded words [@williballenthin] | [#2291] +- fix: Gzip comment with header checksum produces corrupt streams [@williballenthin] | [#2288] +- fix TLV Parser BER long-form length parsing [@williballenthin] | [#2289] +- fix: Unescape Unicode Characters accepts 4-6 hex digits for U+ prefix [@williballenthin] | [#2287] +- fix Set Difference and Set Intersection preserve duplicates from first sample [@williballenthin] | [#2286] +- fix: From Base operation produces wrong results for fractional inputs [@williballenthin] | [#2285] +- fix Median operation returns incorrect result for unsorted odd-length inputs [@williballenthin] | [#2284] +- Added RenderPDF functionality [@Shailendra1703] [@GCHQDeveloper581] | [#2105] +- Fix URL encoding incorrectly converting input to UTF-8 [@C85297] | [#2340] +- feat: Add automated parameter validation framework [@mansiverma897993] | [#2561] +- Fix: added viewport styles to img tag in RenderImage Dish [@Shailendra1703] [@C85297] | [#2109] +- chore (deps): bump form-data from 4.0.5 to 4.0.6 | [#2572] +- chore (deps): bump the patch-updates group with 5 updates [@GCHQDeveloper581] | [#2580] +- chore (deps): bump the docker-dependencies group with 2 updates | [#2579] +- Fix operation description rendering [@C85297] | [#2577] +- chore (deps): bump launch-editor from 2.13.1 to 2.14.1 | [#2574] +- chore (deps): bump dompurify from 3.4.8 to 3.4.9 | [#2573] + ### [11.2.0] - 2026-06-17 This release includes a security fix ([#2569]) - Security: Chart operation prototype protection [@C85297] | [#2569] @@ -718,6 +787,7 @@ Breaking changes: ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[11.3.0]: https://github.com/gchq/CyberChef/releases/tag/v11.3.0 [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.0.0]: https://github.com/gchq/CyberChef/releases/tag/v11.0.0 @@ -1018,6 +1088,17 @@ Breaking changes: [@qa2me]: https://github.com/qa2me [@heapframe]: https://github.com/heapframe [@skyswordw]: https://github.com/skyswordw +[@thomasnemer]: https://github.com/thomasnemer +[@alleria173]: https://github.com/alleria173 +[@roberson-io]: https://github.com/roberson-io +[@min23asdw]: https://github.com/min23asdw +[@giesmininkas]: https://github.com/giesmininkas +[@mansiverma897993]: https://github.com/mansiverma897993 +[@loki1205]: https://github.com/loki1205 +[@vetrovk]: https://github.com/vetrovk +[@zainnadeem786]: https://github.com/zainnadeem786 +[@williballenthin]: https://github.com/williballenthin +[@Shailendra1703]: https://github.com/Shailendra1703 [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 @@ -1389,4 +1470,69 @@ Breaking changes: [#2404]: https://github.com/gchq/CyberChef/pull/2404 [#2458]: https://github.com/gchq/CyberChef/pull/2458 [#2514]: https://github.com/gchq/CyberChef/pull/2514 - +[#2687]: https://github.com/gchq/CyberChef/pull/2687 +[#2103]: https://github.com/gchq/CyberChef/pull/2103 +[#2680]: https://github.com/gchq/CyberChef/pull/2680 +[#2659]: https://github.com/gchq/CyberChef/pull/2659 +[#2676]: https://github.com/gchq/CyberChef/pull/2676 +[#2270]: https://github.com/gchq/CyberChef/pull/2270 +[#2212]: https://github.com/gchq/CyberChef/pull/2212 +[#2207]: https://github.com/gchq/CyberChef/pull/2207 +[#2671]: https://github.com/gchq/CyberChef/pull/2671 +[#2206]: https://github.com/gchq/CyberChef/pull/2206 +[#2185]: https://github.com/gchq/CyberChef/pull/2185 +[#2673]: https://github.com/gchq/CyberChef/pull/2673 +[#2660]: https://github.com/gchq/CyberChef/pull/2660 +[#2668]: https://github.com/gchq/CyberChef/pull/2668 +[#2669]: https://github.com/gchq/CyberChef/pull/2669 +[#2654]: https://github.com/gchq/CyberChef/pull/2654 +[#2225]: https://github.com/gchq/CyberChef/pull/2225 +[#2157]: https://github.com/gchq/CyberChef/pull/2157 +[#2581]: https://github.com/gchq/CyberChef/pull/2581 +[#2625]: https://github.com/gchq/CyberChef/pull/2625 +[#2155]: https://github.com/gchq/CyberChef/pull/2155 +[#2638]: https://github.com/gchq/CyberChef/pull/2638 +[#2635]: https://github.com/gchq/CyberChef/pull/2635 +[#2633]: https://github.com/gchq/CyberChef/pull/2633 +[#2636]: https://github.com/gchq/CyberChef/pull/2636 +[#2620]: https://github.com/gchq/CyberChef/pull/2620 +[#2380]: https://github.com/gchq/CyberChef/pull/2380 +[#2627]: https://github.com/gchq/CyberChef/pull/2627 +[#2621]: https://github.com/gchq/CyberChef/pull/2621 +[#2606]: https://github.com/gchq/CyberChef/pull/2606 +[#2612]: https://github.com/gchq/CyberChef/pull/2612 +[#2614]: https://github.com/gchq/CyberChef/pull/2614 +[#2615]: https://github.com/gchq/CyberChef/pull/2615 +[#2631]: https://github.com/gchq/CyberChef/pull/2631 +[#2619]: https://github.com/gchq/CyberChef/pull/2619 +[#2611]: https://github.com/gchq/CyberChef/pull/2611 +[#2616]: https://github.com/gchq/CyberChef/pull/2616 +[#2607]: https://github.com/gchq/CyberChef/pull/2607 +[#2600]: https://github.com/gchq/CyberChef/pull/2600 +[#2602]: https://github.com/gchq/CyberChef/pull/2602 +[#2601]: https://github.com/gchq/CyberChef/pull/2601 +[#2603]: https://github.com/gchq/CyberChef/pull/2603 +[#2598]: https://github.com/gchq/CyberChef/pull/2598 +[#2589]: https://github.com/gchq/CyberChef/pull/2589 +[#2585]: https://github.com/gchq/CyberChef/pull/2585 +[#2586]: https://github.com/gchq/CyberChef/pull/2586 +[#2453]: https://github.com/gchq/CyberChef/pull/2453 +[#2275]: https://github.com/gchq/CyberChef/pull/2275 +[#2290]: https://github.com/gchq/CyberChef/pull/2290 +[#2291]: https://github.com/gchq/CyberChef/pull/2291 +[#2288]: https://github.com/gchq/CyberChef/pull/2288 +[#2289]: https://github.com/gchq/CyberChef/pull/2289 +[#2287]: https://github.com/gchq/CyberChef/pull/2287 +[#2286]: https://github.com/gchq/CyberChef/pull/2286 +[#2285]: https://github.com/gchq/CyberChef/pull/2285 +[#2284]: https://github.com/gchq/CyberChef/pull/2284 +[#2105]: https://github.com/gchq/CyberChef/pull/2105 +[#2340]: https://github.com/gchq/CyberChef/pull/2340 +[#2561]: https://github.com/gchq/CyberChef/pull/2561 +[#2109]: https://github.com/gchq/CyberChef/pull/2109 +[#2572]: https://github.com/gchq/CyberChef/pull/2572 +[#2580]: https://github.com/gchq/CyberChef/pull/2580 +[#2579]: https://github.com/gchq/CyberChef/pull/2579 +[#2577]: https://github.com/gchq/CyberChef/pull/2577 +[#2574]: https://github.com/gchq/CyberChef/pull/2574 +[#2573]: https://github.com/gchq/CyberChef/pull/2573 diff --git a/Dockerfile b/Dockerfile index 564e8d66..e7076f42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN npm run build ######################################### # Package static build files into nginx # ######################################### -FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:fd3314e343bad2de4e1127ef58be122abbfa7e09572fa46ae62fcddb6b3f21c5 AS cyberchef +FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:44e36330f74d4f3a1d4e222acca9e23b401fb87811a7597024502bb759c4dd49 AS cyberchef LABEL maintainer="GCHQ " diff --git a/package-lock.json b/package-lock.json index a8068e3f..940f4086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "11.2.0", + "version": "11.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "11.2.0", + "version": "11.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -39,7 +39,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.11", + "dompurify": "^3.4.12", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -59,7 +59,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.1.0", "json5": "^2.2.3", - "jsonata": "^2.2.1", + "jsonata": "^2.2.2", "jsonpath-plus": "^10.4.0", "jsonwebtoken": "9.0.3", "jsqr": "^1.4.0", @@ -74,7 +74,7 @@ "lz4js": "^0.2.0", "markdown-it": "^14.3.0", "moment": "^2.30.1", - "moment-timezone": "^0.6.2", + "moment-timezone": "^0.6.3", "ngeohash": "^0.6.3", "node-forge": "^1.4.0", "node-md6": "^0.1.0", @@ -86,7 +86,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^8.7.0", + "protobufjs": "^8.7.1", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", @@ -121,10 +121,10 @@ "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.6", "@puppeteer/browsers": "3.0.6", - "autoprefixer": "^10.5.2", + "autoprefixer": "^10.5.4", "babel-loader": "^10.1.1", "base64-loader": "^1.0.0", - "chromedriver": "^150.0.3", + "chromedriver": "^150.0.4", "cli-progress": "^3.12.0", "colors": "^1.4.0", "compression-webpack-plugin": "^12.0.0", @@ -132,7 +132,7 @@ "core-js": "^3.49.0", "cspell": "^10.0.1", "css-loader": "^7.1.4", - "eslint": "^9.39.4", + "eslint": "^9.39.5", "eslint-plugin-jsdoc": "^50.8.0", "globals": "^17.7.0", "grunt": "^1.6.2", @@ -151,7 +151,7 @@ "mini-css-extract-plugin": "2.10.2", "modify-source-webpack-plugin": "^4.1.0", "nightwatch": "^3.16.0", - "postcss": "^8.5.16", + "postcss": "^8.5.21", "postcss-css-variables": "^0.19.0", "postcss-import": "^16.1.1", "postcss-loader": "^8.2.1", @@ -159,7 +159,7 @@ "sitemap": "^9.0.1", "terser": "^5.49.0", "webpack": "^5.108.4", - "webpack-bundle-analyzer": "^5.3.0", + "webpack-bundle-analyzer": "^5.3.1", "webpack-dev-server": "^5.2.6", "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8" @@ -2776,9 +2776,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -2788,7 +2788,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -2813,9 +2813,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -5620,9 +5620,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", - "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", "dev": true, "funding": [ { @@ -5640,8 +5640,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.4", - "caniuse-lite": "^1.0.30001799", + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -5842,9 +5842,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.40", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", - "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz", + "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6024,9 +6024,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "dev": true, "license": "MIT", "dependencies": { @@ -6259,9 +6259,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6376,9 +6376,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, "funding": [ { @@ -6396,10 +6396,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -6591,9 +6591,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -6733,9 +6733,9 @@ } }, "node_modules/chromedriver": { - "version": "150.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-150.0.3.tgz", - "integrity": "sha512-i2L979d6YDTVVUDUPWXz75HGKKVhjNXo74gLiy/f8Adb5zHLU+h3ABdg8RRoiegtjJB0m9vUEiPTdecD0ifa3w==", + "version": "150.0.4", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-150.0.4.tgz", + "integrity": "sha512-Yg2oXBONi1mqAMOG2ybwUneSvxIn04NF4qNAJ+yGO2H7WwX1WYLGQRHVHsbySEjIuqIyDmS1ZfAw6p0wtl43zQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -8702,9 +8702,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", - "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -8809,9 +8809,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.379", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", - "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", "dev": true, "license": "ISC" }, @@ -9112,9 +9112,9 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", "dependencies": { @@ -9123,8 +9123,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -9692,9 +9692,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -9772,9 +9772,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -12535,9 +12535,9 @@ } }, "node_modules/jsonata": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.2.1.tgz", - "integrity": "sha512-xd1uwUrKeIcJbsWhaoS3qAX4Ea8m0Mw0G5nlnAQvPT7TbZ5qaPdzBVTQia9KfyuyQm+nenfyjvzUDTRYHsC2sw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.2.2.tgz", + "integrity": "sha512-XDFH2PuaAXv0AXJEWwElXbqADmKFGKMLMkFb+qOz0EhjQW2EIJ6pgtordLU+4u3IVERyBfqy6bi6kZKEncvRqA==", "license": "MIT", "engines": { "node": ">= 8" @@ -13497,9 +13497,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -13584,9 +13584,9 @@ } }, "node_modules/moment-timezone": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.2.tgz", - "integrity": "sha512-lDsQv8FoGdBUdf0+TjGsq2orxKuXdwFlQ6Zw6TX3xIcTwTfEpCLyKqvEauvCHJ8iu3KBV8+uPhlv70YsNGdUBQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.3.tgz", + "integrity": "sha512-pVEPA/HCFHHbwJ130ywnzYuZpkEGcP6Daa/OwNebpA18MybeFHmQilAGGovXgWijQ8vQtmud9jZrziUBgsykfg==", "license": "MIT", "dependencies": { "moment": "^2.29.4" @@ -13698,9 +13698,9 @@ "license": "ISC" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -14033,9 +14033,9 @@ "license": "CC0-1.0" }, "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { @@ -15041,9 +15041,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", + "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", "dev": true, "funding": [ { @@ -15061,7 +15061,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15310,9 +15310,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.7.0.tgz", - "integrity": "sha512-uu52JNxLh3vsL7tXU/h0gDaywufvuUCTbGSi0NKQKBZ2ZopkmrWQJSQO/EFqzu/5YhiwgVM8rq/a/iVpx4eZ0g==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.7.1.tgz", + "integrity": "sha512-agdGHrXNTv0IrYscJPDou/PlEJk1c/hBZ9o/B5NH2i/nSPtPqacNxzgwf1CebXxFMjMrZH5sqv9uQuw96aGt/A==", "license": "BSD-3-Clause", "dependencies": { "long": "^5.3.2" @@ -15760,9 +15760,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -16616,9 +16616,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, "license": "MIT", "engines": { @@ -18212,9 +18212,9 @@ } }, "node_modules/webpack-bundle-analyzer": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-5.3.0.tgz", - "integrity": "sha512-PEhAoqiJ+47d0uLMx/+zo5XOvaU+Vk6N2ZLht7H3n09QLy/fhyvqGNwjdRUHJDgMN8crBR2ZwVHkIswT3Xuawg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-5.3.1.tgz", + "integrity": "sha512-wP2EusncRGL1tZyMHC/umLkjPdYMkTL9nPEKh8G8dkYCJ9TyF6xnXFqjhdqmv5J900irN/g0P5jMvLT22krEXQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8512412b..f8c2961e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "11.2.0", + "version": "11.3.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "GCHQ ", "homepage": "https://gchq.github.io/CyberChef", @@ -50,10 +50,10 @@ "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.6", "@puppeteer/browsers": "3.0.6", - "autoprefixer": "^10.5.2", + "autoprefixer": "^10.5.4", "babel-loader": "^10.1.1", "base64-loader": "^1.0.0", - "chromedriver": "^150.0.3", + "chromedriver": "^150.0.4", "cli-progress": "^3.12.0", "colors": "^1.4.0", "compression-webpack-plugin": "^12.0.0", @@ -61,7 +61,7 @@ "core-js": "^3.49.0", "cspell": "^10.0.1", "css-loader": "^7.1.4", - "eslint": "^9.39.4", + "eslint": "^9.39.5", "eslint-plugin-jsdoc": "^50.8.0", "globals": "^17.7.0", "grunt": "^1.6.2", @@ -80,7 +80,7 @@ "mini-css-extract-plugin": "2.10.2", "modify-source-webpack-plugin": "^4.1.0", "nightwatch": "^3.16.0", - "postcss": "^8.5.16", + "postcss": "^8.5.21", "postcss-css-variables": "^0.19.0", "postcss-import": "^16.1.1", "postcss-loader": "^8.2.1", @@ -88,7 +88,7 @@ "sitemap": "^9.0.1", "terser": "^5.49.0", "webpack": "^5.108.4", - "webpack-bundle-analyzer": "^5.3.0", + "webpack-bundle-analyzer": "^5.3.1", "webpack-dev-server": "^5.2.6", "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8" @@ -123,7 +123,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.11", + "dompurify": "^3.4.12", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -143,7 +143,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.1.0", "json5": "^2.2.3", - "jsonata": "^2.2.1", + "jsonata": "^2.2.2", "jsonpath-plus": "^10.4.0", "jsonwebtoken": "9.0.3", "jsqr": "^1.4.0", @@ -158,7 +158,7 @@ "lz4js": "^0.2.0", "markdown-it": "^14.3.0", "moment": "^2.30.1", - "moment-timezone": "^0.6.2", + "moment-timezone": "^0.6.3", "ngeohash": "^0.6.3", "node-forge": "^1.4.0", "node-md6": "^0.1.0", @@ -170,7 +170,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^8.7.0", + "protobufjs": "^8.7.1", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", @@ -193,6 +193,14 @@ "xregexp": "^5.1.2", "zlibjs": "^0.3.1" }, + "allowScripts": { + "@nightwatch/nightwatch-inspector@1.0.1": true, + "chromedriver@150.0.3": true, + "core-js": false, + "core-js-pure": false, + "fsevents": false, + "tesseract.js": false + }, "scripts": { "start": "npx grunt dev", "build": "npx grunt prod", diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs index 1c0f0cc3..57c3efa6 100644 --- a/src/core/Ingredient.mjs +++ b/src/core/Ingredient.mjs @@ -86,6 +86,9 @@ class Ingredient { if (this.type === "option" && Array.isArray(checkVal)) { checkVal = checkVal[this.defaultIndex ?? 0]; } + if (this.type === "argSelector" && Array.isArray(checkVal)) { + checkVal = checkVal[this.defaultIndex ?? 0]?.name || ""; + } // 1. check if empty let isEmpty = false; @@ -99,8 +102,10 @@ class Ingredient { let isAllowedOptionEmpty = false; if (this.type === "option" && Array.isArray(this.defaultValue)) { isAllowedOptionEmpty = this.defaultValue.includes(""); + } else if (this.type === "argSelector" && Array.isArray(this.defaultValue)) { + isAllowedOptionEmpty = this.defaultValue.some(opt => opt.name === ""); } - if (this.allowEmpty === false || (this.type === "option" && !isAllowedOptionEmpty)) { + if (this.allowEmpty === false || ((this.type === "option" || this.type === "argSelector") && !isAllowedOptionEmpty)) { throw new OperationError(`${this.name} cannot be empty.`); } return true; @@ -150,6 +155,23 @@ class Ingredient { } } + // 5. argSelector checks + if (this.type === "argSelector") { + if (Array.isArray(this.defaultValue)) { + const permittedOptions = this.defaultValue + .map(opt => opt.name) + .filter(optName => { + if (typeof optName !== "string") return false; + return !optName.match(/^\[\/?[a-z0-9 -()^]+\]$/i); + }); + const valStr = (checkVal !== null && checkVal !== undefined) ? String(checkVal).toLowerCase() : ""; + const matchedOption = permittedOptions.find(opt => opt.toLowerCase() === valStr); + if (!matchedOption) { + throw new OperationError(`${this.name} must be one of the following: ${permittedOptions.join(", ")}.`); + } + } + } + return true; } diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index eae86374..cb641e3d 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -1015,6 +1015,7 @@ class Utils { // Parse bespoke recipe format recipe = recipe.replace(/\n/g, ""); + Utils._validatePrettyRecipe(recipe); let m, args; const recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/'])*)(\/[^)]+)?\)/g, recipeConfig = []; @@ -1040,6 +1041,53 @@ class Utils { } + /** + * Performs a linear structural validation pass over pretty recipe syntax. + * + * @param {string} recipe + * @throws {Error} if the recipe is structurally invalid + */ + static _validatePrettyRecipe(recipe) { + let i = 0; + + while (i < recipe.length) { + const openParen = recipe.indexOf("(", i); + if (openParen === -1 || openParen === i) { + throw new Error("Invalid recipe"); + } + + i = openParen + 1; + let inString = false, + escaped = false, + foundCloseParen = false; + + for (; i < recipe.length; i++) { + const c = recipe[i]; + + if (inString) { + if (escaped) { + escaped = false; + } else if (c === "\\") { + escaped = true; + } else if (c === "'") { + inString = false; + } + } else if (c === "'") { + inString = true; + } else if (c === ")") { + foundCloseParen = true; + i++; + break; + } + } + + if (!foundCloseParen || inString || escaped) { + throw new Error("Invalid recipe"); + } + } + } + + /** * Formats a list of files or directories. * diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index d69316f9..55e3467d 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -245,7 +245,9 @@ "Subtract", "Multiply", "Divide", + "MOD", "Extended GCD", + "Modular Exponentiation", "Modular Inverse", "Mean", "Median", diff --git a/src/core/lib/Arithmetic.mjs b/src/core/lib/Arithmetic.mjs index 5fbc5771..d951253c 100644 --- a/src/core/lib/Arithmetic.mjs +++ b/src/core/lib/Arithmetic.mjs @@ -124,6 +124,19 @@ export function median(data) { } +/** + * Computes modulo of two numbers and returns the value. + * + * @param {BigNumber[]} data + * @returns {BigNumber} + */ +export function mod(data) { + if (data.length > 0) { + return data.reduce((acc, curr) => acc.mod(curr)); + } +} + + /** * Computes standard deviation of a number array and returns the value. * diff --git a/src/core/operations/A1Z26CipherDecode.mjs b/src/core/operations/A1Z26CipherDecode.mjs index cc9aafbf..4cbe605d 100644 --- a/src/core/operations/A1Z26CipherDecode.mjs +++ b/src/core/operations/A1Z26CipherDecode.mjs @@ -35,32 +35,32 @@ class A1Z26CipherDecode extends Operation { ]; this.checks = [ { - pattern: "^\\s*([12]?[0-9] )+[12]?[0-9]\\s*$", + pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]) )+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$", flags: "", args: ["Space"] }, { - pattern: "^\\s*([12]?[0-9],)+[12]?[0-9]\\s*$", + pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]),)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$", flags: "", args: ["Comma"] }, { - pattern: "^\\s*([12]?[0-9];)+[12]?[0-9]\\s*$", + pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]);)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$", flags: "", args: ["Semi-colon"] }, { - pattern: "^\\s*([12]?[0-9]:)+[12]?[0-9]\\s*$", + pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]):)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$", flags: "", args: ["Colon"] }, { - pattern: "^\\s*([12]?[0-9]\\n)+[12]?[0-9]\\s*$", + pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6])\\n)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$", flags: "", args: ["Line feed"] }, { - pattern: "^\\s*([12]?[0-9]\\r\\n)+[12]?[0-9]\\s*$", + pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6])\\r\\n)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$", flags: "", args: ["CRLF"] } diff --git a/src/core/operations/AutomatedValidationTestOp.mjs b/src/core/operations/AutomatedValidationTestOp.mjs index 92f803ae..52af20c2 100644 --- a/src/core/operations/AutomatedValidationTestOp.mjs +++ b/src/core/operations/AutomatedValidationTestOp.mjs @@ -66,6 +66,23 @@ class AutomatedValidationTestOp extends Operation { "type": "option", "value": ["[Group 1]", "Option 1", "Option 2", "[/Group 1]", "[Group 2]", "Option 3", "[/Group 2]"], "allowEmpty": false + }, + { + "name": "Arg Selector Ingredient", + "type": "argSelector", + "value": [ + { + name: "Option 1", + on: [0], + off: [1] + }, + { + name: "Option 2", + on: [1], + off: [0] + } + ], + "allowEmpty": false } ]; } diff --git a/src/core/operations/AvroToJSON.mjs b/src/core/operations/AvroToJSON.mjs index 497a3872..585bd872 100644 --- a/src/core/operations/AvroToJSON.mjs +++ b/src/core/operations/AvroToJSON.mjs @@ -39,7 +39,7 @@ class AvroToJSON extends Operation { * @param {Object[]} args * @returns {string} */ - run(input, args) { + async run(input, args) { if (input.byteLength <= 0) { throw new OperationError("Please provide an input."); } diff --git a/src/core/operations/Bzip2Compress.mjs b/src/core/operations/Bzip2Compress.mjs index 45dbfae6..b5105e98 100644 --- a/src/core/operations/Bzip2Compress.mjs +++ b/src/core/operations/Bzip2Compress.mjs @@ -47,7 +47,7 @@ class Bzip2Compress extends Operation { * @param {Object[]} args * @returns {File} */ - run(input, args) { + async run(input, args) { const [blockSize, workFactor] = args; if (input.byteLength <= 0) { throw new OperationError("Please provide an input."); diff --git a/src/core/operations/FromDecimal.mjs b/src/core/operations/FromDecimal.mjs index 16af47e9..f718fb50 100644 --- a/src/core/operations/FromDecimal.mjs +++ b/src/core/operations/FromDecimal.mjs @@ -9,7 +9,7 @@ import {DELIM_OPTIONS} from "../lib/Delim.mjs"; import {fromDecimal} from "../lib/Decimal.mjs"; /** - * From Decimal delimiters, plus auto-detection. + * From Decimal delimiters, plus auto-detection. */ const FROM_DECIMAL_DELIM_OPTIONS = [...DELIM_OPTIONS, "Auto"]; diff --git a/src/core/operations/MOD.mjs b/src/core/operations/MOD.mjs new file mode 100644 index 00000000..e50b339a --- /dev/null +++ b/src/core/operations/MOD.mjs @@ -0,0 +1,62 @@ +/** + * @license Apache-2.0 + */ + +import BigNumber from "bignumber.js"; +import Operation from "../Operation.mjs"; +import { createNumArray } from "../lib/Arithmetic.mjs"; +import { ARITHMETIC_DELIM_OPTIONS } from "../lib/Delim.mjs"; + + +/** + * MOD operation + */ +class MOD extends Operation { + + /** + * MOD constructor + */ + constructor() { + super(); + + this.name = "MOD"; + this.module = "Default"; + this.description = "Computes the modulo of each number in a list with a given modulus value. Numbers are extracted from the input based on the delimiter, and non-numeric values are ignored.

e.g. 15 4 7 with modulus 3 becomes 0 1 1"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Modulus", + "type": "number", + "value": 2 + }, + { + "name": "Delimiter", + "type": "option", + "value": ARITHMETIC_DELIM_OPTIONS, + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const modulus = new BigNumber(args[0]); + const delimiter = args[1]; + + if (modulus.isZero()) { + throw new Error("Modulus cannot be zero"); + } + + const numbers = createNumArray(input, delimiter); + const results = numbers.map(num => num.mod(modulus)); + + return results.join(" "); + } + +} + +export default MOD; diff --git a/src/core/operations/ModularExponentiation.mjs b/src/core/operations/ModularExponentiation.mjs new file mode 100644 index 00000000..6a08a97d --- /dev/null +++ b/src/core/operations/ModularExponentiation.mjs @@ -0,0 +1,111 @@ +/** + * @author p-leriche [philip.leriche@cantab.net] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { parseBigInt, modPow } from "../lib/BigIntUtils.mjs"; + +/* ---------- operation class ---------- */ + +/** + * Modular Exponentiation operation + */ +class ModularExponentiation extends Operation { + + /** + * ModularExponentiation constructor + */ + constructor() { + super(); + + this.name = "Modular Exponentiation"; + this.module = "Crypto"; + this.description = "Performs modular exponentiation, as used in Diffie-Hellman and RSA.

" + + "Computes Base ^ Exponent mod Modulus.

" + + "Input handling: If either Base or Exponent is left blank, " + + "its value is taken from the Input field."; + this.infoURL = "https://wikipedia.org/wiki/Modular_exponentiation"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Base", + type: "string", + value: "" + }, + { + name: "Modulus", + type: "string", + value: "1" + }, + { + name: "Exponent", + type: "string", + value: "" + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [baseStr, modStr, expStr] = args; + + // Trim everything so "" and " " count as empty + const baseParam = baseStr?.trim(); + const expParam = expStr?.trim(); + const modParam = modStr?.trim(); + const inputVal = input?.trim(); + + const mod = modParam; + if (!mod) { + throw new OperationError("Modulus must be defined"); + } + + // Base *or* Exponent (but not both) are taken from the Input + // if their boxes are empty. + let base, exp; + if (baseParam && expParam) { + // Case 1: base and exponent both given as parameters + base = baseParam; + exp = expParam; + } else if (!baseParam && expParam) { + // Case 2: base missing - take from input + base = inputVal; + exp = expParam; + if (!base) { + throw new OperationError("Base must be defined"); + } + } else if (baseParam && !expParam) { + // Case 3: exponent missing - take from input + base = baseParam; + exp = inputVal; + if (!exp) { + throw new OperationError("Exponent must be defined"); + } + } else if (!inputVal) { + // Case 4: base and exponent both missing + throw new OperationError("Base and Exponent must be defined"); + } else throw new OperationError("Ambiguous input: specify either Base or Exponent when using Input"); + + // Parse numbers + const baseBI = parseBigInt(base, "Base"); + const expBI = parseBigInt(exp, "Exponent"); + const modBI = parseBigInt(mod, "Modulus"); + + // Check for invalid modulus (parseBigInt eliminates negatives) + if (modBI === 0n) { + throw new OperationError("Modulus must be greater than zero"); + } + + return modPow(baseBI, expBI, modBI).toString(); + } +} + +export default ModularExponentiation; diff --git a/src/core/operations/ParseQRCode.mjs b/src/core/operations/ParseQRCode.mjs index 89eaddee..addc1f17 100644 --- a/src/core/operations/ParseQRCode.mjs +++ b/src/core/operations/ParseQRCode.mjs @@ -33,15 +33,11 @@ class ParseQRCode extends Operation { value: false, }, ]; - this.checks = [ - { - pattern: - "^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)", - flags: "", - args: [false], - useful: true, - }, - ]; + // No Magic checks: detecting a QR code in arbitrary image data requires + // actually attempting to parse one, which is expensive and produces + // spurious "Could not read a QR code from the image" log messages for + // any image input via Magic. Users can add Parse QR Code manually when + // they know the image contains a QR code. See issue #2610. } /** diff --git a/src/node/api.mjs b/src/node/api.mjs index 83163d37..a48ed3a9 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -77,6 +77,10 @@ function transformArgs(opArgsList, newArgs) { return !Array.isArray(arg.value) ? arg.value : arg.value[arg.defaultIndex ?? 0]; } + if (arg.type === "argSelector") { + return !Array.isArray(arg.value) ? arg.value : (arg.value[arg.defaultIndex ?? 0]?.name || ""); + } + if (arg.type === "editableOption") { return typeof arg.value === "string" ? arg.value : arg.value[arg.defaultIndex ?? 0].value; } diff --git a/tests/node/index.mjs b/tests/node/index.mjs index e53ca7b2..454479b3 100644 --- a/tests/node/index.mjs +++ b/tests/node/index.mjs @@ -27,6 +27,7 @@ import "./tests/Categories.mjs"; import "./tests/ToHTMLEntity.mjs"; import "./tests/lib/BigIntUtils.mjs"; import "./tests/lib/ChartsProtocolPrototypePollution.mjs"; +import "./tests/ParseQRCode.mjs"; const testStatus = { allTestsPassing: true, diff --git a/tests/node/tests/ParseQRCode.mjs b/tests/node/tests/ParseQRCode.mjs new file mode 100644 index 00000000..c063946a --- /dev/null +++ b/tests/node/tests/ParseQRCode.mjs @@ -0,0 +1,35 @@ +/** + * ParseQRCode API tests. + * + * @author Sanjays2402 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; +import OperationConfig from "../../../src/core/config/OperationConfig.json" with { type: "json" }; +import it from "../assertionHandler.mjs"; +import assert from "assert"; + +TestRegister.addApiTests([ + /* + * Regression test for #2610. + * + * Parse QR Code used to declare a `checks` regex that matched any JPEG, + * PNG, GIF, WEBP or BMP magic bytes. Magic aggregates every operation + * with a `checks` property, so any image input ran through a full QR + * parse attempt, which in turn emitted a "Could not read a QR code from + * the image" warning to the browser console for every image. There is + * no cheap way to detect a QR code without attempting a full parse, so + * Parse QR Code must not participate in Magic; users can add it + * manually when they know an image contains a QR code. + */ + it("Parse QR Code: must not participate in Magic (#2610)", () => { + const op = OperationConfig["Parse QR Code"]; + assert(op, "Parse QR Code operation is missing from OperationConfig"); + assert( + !op.checks || op.checks.length === 0, + "Parse QR Code must not declare `checks`; otherwise Magic will run a " + + "QR parse on every image and spam the console (see issue #2610)." + ); + }), +]); diff --git a/tests/node/tests/Utils.mjs b/tests/node/tests/Utils.mjs index 5ee7c936..ff4ffd96 100644 --- a/tests/node/tests/Utils.mjs +++ b/tests/node/tests/Utils.mjs @@ -26,4 +26,81 @@ TestRegister.addApiTests([ "\x7e...", ); }), + + it("Utils: should parse normal pretty recipes", () => { + assert.deepStrictEqual( + Utils.parseRecipeConfig("From_Base64('A-Za-z0-9+/=',true)To_Hex('Space')"), + [ + { + op: "From Base64", + args: ["A-Za-z0-9+/=", true], + }, + { + op: "To Hex", + args: ["Space"], + }, + ], + ); + }), + + it("Utils: should parse pretty recipe options", () => { + assert.deepStrictEqual( + Utils.parseRecipeConfig("A(/disabled/breakpoint)"), + [ + { + op: "A", + args: [], + disabled: true, + breakpoint: true, + }, + ], + ); + }), + + it("Utils: should parse escaped quotes and backslashes in pretty recipes", () => { + assert.deepStrictEqual( + Utils.parseRecipeConfig("A('\\'\\\\')"), + [ + { + op: "A", + args: ["'\\"], + }, + ], + ); + }), + + it("Utils: should parse large valid quoted pretty recipe arguments", () => { + const value = "x".repeat(10000); + + assert.deepStrictEqual( + Utils.parseRecipeConfig(`A('${value}')`), + [ + { + op: "A", + args: [value], + }, + ], + ); + }), + + it("Utils: should reject malformed pretty recipes with unmatched quotes", () => { + assert.throws( + () => Utils.parseRecipeConfig("A(" + "'".repeat(10000)), + /Invalid recipe/, + ); + }), + + it("Utils: should reject malformed pretty recipes with malformed parentheses", () => { + assert.throws( + () => Utils.parseRecipeConfig("A("), + /Invalid recipe/, + ); + }), + + it("Utils: should reject malformed pretty recipes with malformed escapes", () => { + assert.throws( + () => Utils.parseRecipeConfig("A('" + "\\".repeat(10000)), + /Invalid recipe/, + ); + }), ]); diff --git a/tests/node/tests/operations.mjs b/tests/node/tests/operations.mjs index 0046574c..c6f0e44f 100644 --- a/tests/node/tests/operations.mjs +++ b/tests/node/tests/operations.mjs @@ -264,6 +264,18 @@ Full hash: $2a$10$ODeP1.6fMsb.ENk2ngPUCO7qTGVPyHA9TqDVcyupyed8FjsiF65L6`; assert.strictEqual(result.toString(), "Fit as a Fiddle"); }), + it("Bzip2 Compress: round-trips through the Node API", async () => { + const compressed = await chef.bzip2Compress("The quick brown fox."); + const result = await chef.bzip2Decompress(compressed); + assert.strictEqual(result.toString(), "The quick brown fox."); + }), + + it("Avro to JSON: decodes an object container file through the Node API", async () => { + const avro = chef.fromHex("4f626a0104166176726f2e736368656d6196017b2274797065223a227265636f7264222c226e616d65223a22736d616c6c222c226669656c6473223a5b7b226e616d65223a226e616d65222c2274797065223a22737472696e67227d5d7d146176726f2e636f646563086e756c6c004e0247632e3702e5b75cdab9a62f1541020e0c6d796e616d654e0247632e3702e5b75cdab9a62f1541"); + const result = await chef.avroToJSON(avro); + assert.strictEqual(result.toString(), "{\n \"name\": \"myname\"\n}"); + }), + it("cartesianProduct: binary string", () => { const result = cartesianProduct("1:2\\n\\n3:4", { itemDelimiter: ":", diff --git a/tests/operations/tests/AutomatedValidation.mjs b/tests/operations/tests/AutomatedValidation.mjs index f55efa01..6190e2d2 100644 --- a/tests/operations/tests/AutomatedValidation.mjs +++ b/tests/operations/tests/AutomatedValidation.mjs @@ -150,5 +150,38 @@ TestRegister.addTests([ args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, ""] } ] + }, + { + name: "Automated Validation: Valid Arg Selector value", + input: "test", + expectedOutput: "Success", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, "Option 1", "Option 2"] + } + ] + }, + { + name: "Automated Validation: Invalid Arg Selector value", + input: "test", + expectedOutput: "Arg Selector Ingredient must be one of the following: Option 1, Option 2.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, "Option 1", "Option 3"] + } + ] + }, + { + name: "Automated Validation: Arg Selector value empty (invalid)", + input: "test", + expectedOutput: "Arg Selector Ingredient cannot be empty.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, "Option 1", ""] + } + ] } ]); diff --git a/tests/operations/tests/Hash.mjs b/tests/operations/tests/Hash.mjs index 1ffb749c..a805b94f 100644 --- a/tests/operations/tests/Hash.mjs +++ b/tests/operations/tests/Hash.mjs @@ -722,6 +722,39 @@ TestRegister.addTests([ } ] }, + { + name: "HMAC: Decimal key space-delimited matches Latin1 key (fromDecimal Auto delimiter regression)", + input: "Hello, World!", + expectedOutput: "9b641a008211bb0464a10899d5b37a24ab08ffcf839f5e74d17d99f856530957", + recipeConfig: [ + { + "op": "HMAC", + "args": [{"option": "Decimal", "string": "65 65"}, "SHA256"] + } + ] + }, + { + name: "HMAC: Latin1 key matches space-delimited Decimal key (fromDecimal Auto delimiter regression)", + input: "Hello, World!", + expectedOutput: "9b641a008211bb0464a10899d5b37a24ab08ffcf839f5e74d17d99f856530957", + recipeConfig: [ + { + "op": "HMAC", + "args": [{"option": "Latin1", "string": "AA"}, "SHA256"] + } + ] + }, + { + name: "HMAC: Decimal key comma-delimited matches Latin1 key (fromDecimal Auto delimiter regression)", + input: "Hello, World!", + expectedOutput: "9b641a008211bb0464a10899d5b37a24ab08ffcf839f5e74d17d99f856530957", + recipeConfig: [ + { + "op": "HMAC", + "args": [{"option": "Decimal", "string": "65,65"}, "SHA256"] + } + ] + }, { name: "MD5: Complex bytes", input: "10dc10e32010de10d010dc10d810d910d010e12e", diff --git a/tests/operations/tests/MOD.mjs b/tests/operations/tests/MOD.mjs new file mode 100644 index 00000000..2ecd25f6 --- /dev/null +++ b/tests/operations/tests/MOD.mjs @@ -0,0 +1,208 @@ +/** + * MOD tests + * + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "MOD: Basic modulo operation", + input: "15 4 7", + expectedOutput: "0 1 1", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Single number", + input: "10", + expectedOutput: "1", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Comma-separated numbers", + input: "15,8,23,16,5", + expectedOutput: "1 1 2 2 5", + recipeConfig: [ + { + "op": "MOD", + "args": [7, "Comma"] + } + ], + }, + { + name: "MOD: Line feed separated numbers", + input: "25\n13\n44\n7", + expectedOutput: "0 3 4 2", + recipeConfig: [ + { + "op": "MOD", + "args": [5, "Line feed"] + } + ], + }, + { + name: "MOD: Large numbers", + input: "123456789012345 987654321098765", + expectedOutput: "123456789012345 987654321098765", + recipeConfig: [ + { + "op": "MOD", + "args": [1234567890123456, "Space"] + } + ], + }, + { + name: "MOD: Mixed with non-numeric values", + input: "15 abc 4 def 7 xyz 23", + expectedOutput: "0 1 1 2", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Decimal numbers", + input: "10.5 15.7 8.2", + expectedOutput: "1.5 0.7 2.2", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Negative numbers", + input: "-15 -8 25 -10", + expectedOutput: "0 -2 1 -1", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Zero in input", + input: "0 5 10 15 20", + expectedOutput: "0 2 1 0 2", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Modulus of 2 (even/odd check)", + input: "1 2 3 4 5 6 7 8 9 10", + expectedOutput: "1 0 1 0 1 0 1 0 1 0", + recipeConfig: [ + { + "op": "MOD", + "args": [2, "Space"] + } + ], + }, + { + name: "MOD: Numbers with extra whitespace", + input: " 15 4 7 ", + expectedOutput: "0 1 1", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Empty input", + input: "", + expectedOutput: "", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Scientific notation", + input: "1e3 2e2 5e1", + expectedOutput: "1 2 2", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Floating point precision", + input: "10.123456789 20.987654321", + expectedOutput: "1.123456789 2.987654321", + recipeConfig: [ + { + "op": "MOD", + "args": [3, "Space"] + } + ], + }, + { + name: "MOD: Zero modulus error", + input: "15 4 7", + expectedError: true, + expectedOutput: "MOD - Modulus cannot be zero", + recipeConfig: [ + { + "op": "MOD", + "args": [0, "Space"] + } + ], + }, + { + name: "MOD: Semi-colon separated numbers", + input: "17;5;8;13", + expectedOutput: "2 0 3 3", + recipeConfig: [ + { + "op": "MOD", + "args": [5, "Semi-colon"] + } + ], + }, + { + name: "MOD: Colon separated numbers", + input: "25:9:14:7", + expectedOutput: "1 1 2 3", + recipeConfig: [ + { + "op": "MOD", + "args": [4, "Colon"] + } + ], + }, + { + name: "MOD: CRLF separated numbers", + input: "30\r\n18\r\n22\r\n11", + expectedOutput: "0 0 4 5", + recipeConfig: [ + { + "op": "MOD", + "args": [6, "CRLF"] + } + ], + }, +]); diff --git a/tests/operations/tests/Magic.mjs b/tests/operations/tests/Magic.mjs index 90401dc1..59486454 100644 --- a/tests/operations/tests/Magic.mjs +++ b/tests/operations/tests/Magic.mjs @@ -152,5 +152,49 @@ TestRegister.addTests([ args: [1, false, false] } ] + }, + { + name: "Magic: invalid zero A1Z26 values do not match", + input: "0,0", + unexpectedMatch: /A1Z26 Cipher Decode/, + recipeConfig: [ + { + op: "Magic", + args: [0, false, false] + } + ] + }, + { + name: "Magic: invalid high A1Z26 values do not match", + input: "27,1", + unexpectedMatch: /A1Z26 Cipher Decode/, + recipeConfig: [ + { + op: "Magic", + args: [0, false, false] + } + ] + }, + { + name: "Magic: valid leading-zero A1Z26 values match", + input: "26,01", + expectedMatch: /A1Z26 Cipher Decode/, + recipeConfig: [ + { + op: "Magic", + args: [0, false, false] + } + ] + }, + { + name: "Magic: valid leading-zero A1Z26 values match with two digits", + input: "09,10", + expectedMatch: /A1Z26 Cipher Decode/, + recipeConfig: [ + { + op: "Magic", + args: [0, false, false] + } + ] } ]); diff --git a/tests/operations/tests/ModularExponentiation.mjs b/tests/operations/tests/ModularExponentiation.mjs new file mode 100644 index 00000000..4912530f --- /dev/null +++ b/tests/operations/tests/ModularExponentiation.mjs @@ -0,0 +1,137 @@ +/** + * Modular Exponentiation tests. + * + * @author p-leriche [philip.leriche@cantab.net] + * + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Modular Exponentiation: basic example (2^10 mod 1000)", + input: "", + expectedOutput: "24", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["2", "1000", "10"], + }, + ], + }, + { + name: "Modular Exponentiation: small values (3^5 mod 7)", + input: "", + expectedOutput: "5", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["3", "7", "5"], + }, + ], + }, + { + name: "Modular Exponentiation: exponent zero", + input: "", + expectedOutput: "1", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["999", "100", "0"], + }, + ], + }, + { + name: "Modular Exponentiation: base one", + input: "", + expectedOutput: "1", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["1", "1000", "999"], + }, + ], + }, + { + name: "Modular Exponentiation: hexadecimal input", + input: "", + expectedOutput: "256", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["0x10", "1000", "0x2"], + }, + ], + }, + { + name: "Modular Exponentiation: using input field for base", + input: "5", + expectedOutput: "6", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["", "7", "3"], + }, + ], + }, + { + name: "Modular Exponentiation: using input field for exponent", + input: "4", + expectedOutput: "5", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["2", "11", ""], + }, + ], + }, + { + name: "Modular Exponentiation: RSA-like example (small)", + input: "", + expectedOutput: "561", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["123", "1000", "456"], + }, + ], + }, + { + name: "Modular Exponentiation: large base and exponent", + input: "", + expectedOutput: "560583526", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["123456789", "1000000007", "65537"], + }, + ], + }, + { + name: "Modular Exponentiation: crypto-sized numbers (RSA-2048 simulation)", + input: "", + expectedOutput: "1", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: [ + "12345678901234567890123456789012345678901234567890", + "99999999999999999999999999999999999999999999999999", + "0" + ], + }, + ], + }, + { + name: "Modular Exponentiation: Fermat's Little Theorem (a^(p-1) mod p = 1)", + input: "", + expectedOutput: "1", + recipeConfig: [ + { + op: "Modular Exponentiation", + args: ["3", "11", "10"], + }, + ], + }, +]);