From 89b5c7dee32a811f7c7cb06c07aaec1f8f226f44 Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:35:21 +0100 Subject: [PATCH 1/8] Fix option ingredients being overwriten (#2341) --- src/web/HTMLIngredient.mjs | 1 + src/web/waiters/RecipeWaiter.mjs | 10 ++++++- tests/browser/03_recipe_load.js | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/browser/03_recipe_load.js diff --git a/src/web/HTMLIngredient.mjs b/src/web/HTMLIngredient.mjs index 91cbed89..9f61b75a 100755 --- a/src/web/HTMLIngredient.mjs +++ b/src/web/HTMLIngredient.mjs @@ -166,6 +166,7 @@ class HTMLIngredient { id="${this.id}" tabindex="${this.tabIndex}" arg-name="${this.name}" + data-target="${this.target}" ${this.disabled ? "disabled" : ""}>`; for (i = 0; i < this.value.length; i++) { if ((m = this.value[i].name.match(/\[([a-z0-9 -()^]+)\]/i))) { diff --git a/src/web/waiters/RecipeWaiter.mjs b/src/web/waiters/RecipeWaiter.mjs index 4272ef3b..e4198cd5 100755 --- a/src/web/waiters/RecipeWaiter.mjs +++ b/src/web/waiters/RecipeWaiter.mjs @@ -487,11 +487,19 @@ class RecipeWaiter { * @param {HTMLElement} op */ triggerArgEvents(op) { - // Trigger populateOption and argSelector events + // Trigger argSelector events and populateOption events only where the target is empty. + // When loading a saved recipe, arguments are populated before this method is called, so + // re-triggering populateOption events would overwrite saved custom values with defaults. + const args = op.querySelectorAll(".arg"); const triggerableOptions = op.querySelectorAll(".populate-option, .arg-selector"); const evt = new Event("change", {bubbles: true}); + if (triggerableOptions.length) { for (const el of triggerableOptions) { + if (el.classList.contains("populate-option")) { + const target = args[el.getAttribute("data-target")]; + if (target && target.value !== "") continue; + } el.dispatchEvent(evt); } } diff --git a/tests/browser/03_recipe_load.js b/tests/browser/03_recipe_load.js new file mode 100644 index 00000000..58609d99 --- /dev/null +++ b/tests/browser/03_recipe_load.js @@ -0,0 +1,48 @@ +/** + * Regression tests for recipe loading behaviour. + * + * @author C85297 [95289555+C85297@users.noreply.github.com] + * @copyright Crown Copyright + * @license Apache-2.0 + */ + +const utils = require("./browserUtils.js"); + +module.exports = { + before: browser => { + browser + .resizeWindow(1280, 800) + .url(browser.launchUrl) + .useCss() + .waitForElementNotPresent("#preloader", 10000); + }, + + "Recipe load preserves populated arguments": browser => { + const inputFormat = "HH:mm:ss a MMM DD, YYYY "; + const input = "10:20:30 pm Sep 26, 2019 "; + + utils.loadRecipe( + browser, + "Translate DateTime Format", + input, + [ + "Standard date and time", + inputFormat, + "UTC", + "DD/MM/YYYY HH:mm:ss", + "UTC" + ] + ); + + browser.execute(() => { + return Array.from(document.querySelectorAll("#rec-list li.operation .arg")) + .map(arg => arg.value); + }, [], function({value}) { + browser.expect(value[1]).to.equal(inputFormat); + }); + }, + + after: browser => { + browser.end(); + } +}; From 72ca1aeec7a50f4c596d44b755a6ca71f792ffcf Mon Sep 17 00:00:00 2001 From: A Normal Ladd Date: Wed, 3 Jun 2026 03:57:13 -0600 Subject: [PATCH 2/8] Add remove ANSI escape codes operation (#2143) Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (additional test case) --- src/core/config/Categories.json | 1 + src/core/operations/RemoveANSIEscapeCodes.mjs | 41 ++++++++++++ tests/operations/index.mjs | 1 + .../tests/RemoveANSIEscapeCodes.mjs | 62 +++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 src/core/operations/RemoveANSIEscapeCodes.mjs create mode 100644 tests/operations/tests/RemoveANSIEscapeCodes.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 3404dc6d..dd6918dc 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -304,6 +304,7 @@ "Diff", "Remove whitespace", "Remove null bytes", + "Remove ANSI Escape Codes", "To Upper case", "To Lower case", "Swap case", diff --git a/src/core/operations/RemoveANSIEscapeCodes.mjs b/src/core/operations/RemoveANSIEscapeCodes.mjs new file mode 100644 index 00000000..2301f852 --- /dev/null +++ b/src/core/operations/RemoveANSIEscapeCodes.mjs @@ -0,0 +1,41 @@ +/** + * @author Louis-Ladd [lewisharshman1@gmail.com] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Remove ANSI Escape Codes operation + */ +class RemoveANSIEscapeCodes extends Operation { + + /** + * RemoveANSIEscapeCodes constructor + */ + constructor() { + super(); + + this.name = "Remove ANSI Escape Codes"; + this.module = "Default"; + this.description = "Removes ANSI Escape Codes."; + this.infoURL = "https://wikipedia.org/wiki/ANSI_escape_code"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const ansiRegex = /\x1B\[[0-?]*[ -/]*[@-~]/g; + return input.replace(ansiRegex, ""); + } + +} + +export default RemoveANSIEscapeCodes; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index c12c2710..c44270fd 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -148,6 +148,7 @@ import "./tests/Rabbit.mjs"; import "./tests/RAKE.mjs"; import "./tests/Regex.mjs"; import "./tests/Register.mjs"; +import "./tests/RemoveANSIEscapeCodes.mjs"; import "./tests/RegularExpression.mjs"; import "./tests/RenderMarkdown.mjs"; import "./tests/RisonEncodeDecode.mjs"; diff --git a/tests/operations/tests/RemoveANSIEscapeCodes.mjs b/tests/operations/tests/RemoveANSIEscapeCodes.mjs new file mode 100644 index 00000000..491795c3 --- /dev/null +++ b/tests/operations/tests/RemoveANSIEscapeCodes.mjs @@ -0,0 +1,62 @@ +/** + * @author Louis-Ladd [lewisharshman1@gmail.com] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + "name": "Remove ANSI Escape Codes: text using x1b escape code", + "input": "\x1b[31mHello, \x1b[31mWorld!", + "expectedOutput": "Hello, World!", + "recipeConfig": [ + { + "op": "Remove ANSI Escape Codes", + "args": [], + }, + ], + }, + { + "name": "Remove ANSI Escape Codes: text with incomplete codes", + "input": "\x1b[31 Hello, World!", + "expectedOutput": "ello, World!", + "recipeConfig": [ + { + "op": "Remove ANSI Escape Codes", + "args": [], + }, + ], + }, + { + "name": "Remove ANSI Escape Codes: cursor commands and clear screen", + "input": "\x1b[2J\x1b[H\x1b[3BHello, World!", + "expectedOutput": "Hello, World!", + "recipeConfig": [ + { + "op": "Remove ANSI Escape Codes", + "args": [], + }, + ], + }, + { + "name": "Remove ANSI Escape Codes: text containing javascript escape representation of ansi escape codes", + // input/output expressed in hex to avoid accidental interpretation of Javascript escapes and to make the test case explicit + "input": "5c 30 33 33 5b 33 32 3b 31 3b 33 3b 34 3b 39 6d 48 65 6c 6c 6f 2c 20 5c 30 33 33 5b 33 32 3b 31 3b 33 3b 34 3b 39 6d 57 6f 72 6c 64 21", + "expectedOutput": "5c 30 33 33 5b 33 32 3b 31 3b 33 3b 34 3b 39 6d 48 65 6c 6c 6f 2c 20 5c 30 33 33 5b 33 32 3b 31 3b 33 3b 34 3b 39 6d 57 6f 72 6c 64 21", + "recipeConfig": [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Remove ANSI Escape Codes", + "args": [] + }, + { + "op": "To Hex", + "args": ["Space", 0] + } + ], + }, +]); From 5318bab19fe842a0696a40317c779d105e7df16c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:40:18 +0100 Subject: [PATCH 3/8] chore (deps): bump nginxinc/nginx-unprivileged from `df0e9ed` to `0a1e718` in the docker-dependencies group (#2498) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 864f1606..6e6ee6f6 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:df0e9edf92b8436ff797fe5a2cbfc66be1df775c113d322ccadf5c7f3100eda8 AS cyberchef +FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:0a1e718ff1e1a22fc519d0c2e5b6872681f01e37c8a2817ec43ce6e716103929 AS cyberchef LABEL maintainer="GCHQ " From 8a5c60ac74d445073ca012bdcf118281098155ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:47:23 +0100 Subject: [PATCH 4/8] chore (deps): bump the patch-updates group with 2 updates (#2499) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 134641ce..f3e9e881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^5.2.2", - "dompurify": "^3.4.6", + "dompurify": "^3.4.7", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -85,7 +85,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^7.6.1", + "protobufjs": "^7.6.2", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", @@ -8620,9 +8620,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.6.tgz", - "integrity": "sha512-+7gzEI8trIIQkVCvQ3ucGtNfH3nOmDgVTzc62rAAOlMxLth78pwpPoZCPc7CyRzAQF89MqcfPdEWkDwnjgqktg==", + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz", + "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -15101,9 +15101,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", - "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", + "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/package.json b/package.json index d7d1b48a..6faf49f3 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^5.2.2", - "dompurify": "^3.4.6", + "dompurify": "^3.4.7", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -169,7 +169,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^7.6.1", + "protobufjs": "^7.6.2", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", From 3034d63f90f3e2d5b3079fc7f4f0d774131b05f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:26:43 +0100 Subject: [PATCH 5/8] chore (deps): bump the minor-updates group with 5 updates (#2500) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> (removed jq-web update, and added it to dependabot.yml block list) --- .github/dependabot.yml | 2 ++ package-lock.json | 77 +++++++++++++++++++++++------------------- package.json | 8 ++--- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ded5077c..16cd1c3a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -41,6 +41,8 @@ updates: versions: [ '>=2.0.0' ] - dependency-name: 'jimp' versions: [ '1.6.1' ] + - dependency-name: 'jq-web' + versions: [ '>=0.6.0' ] groups: # # Grouping so patch version updates are batched together in a single PR diff --git a/package-lock.json b/package-lock.json index f3e9e881..623c8440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.1.0", "json5": "^2.2.3", - "jsonata": "^2.1.0", + "jsonata": "^2.2.1", "jsonpath-plus": "^10.4.0", "jsonwebtoken": "9.0.3", "jsqr": "^1.4.0", @@ -71,7 +71,7 @@ "loglevel-message-prefix": "^3.0.0", "lz-string": "^1.5.0", "lz4js": "^0.2.0", - "markdown-it": "^14.1.1", + "markdown-it": "^14.2.0", "moment": "^2.30.1", "moment-timezone": "^0.6.2", "ngeohash": "^0.6.3", @@ -109,7 +109,7 @@ "zlibjs": "^0.3.1" }, "devDependencies": { - "@babel/eslint-parser": "^7.28.6", + "@babel/eslint-parser": "^7.29.7", "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-transform-runtime": "^7.29.7", "@babel/preset-env": "^7.29.7", @@ -149,7 +149,7 @@ "imports-loader": "^5.0.0", "mini-css-extract-plugin": "2.10.2", "modify-source-webpack-plugin": "^4.1.0", - "nightwatch": "^3.15.0", + "nightwatch": "^3.16.0", "postcss": "^8.5.15", "postcss-css-variables": "^0.19.0", "postcss-import": "^16.1.1", @@ -279,9 +279,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", - "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.29.7.tgz", + "integrity": "sha512-zxt+UJTOMKvUt3yOg+D58MLuz334pHp93qifMFcjIIO+9hN6t+ufw2gi7vDPMpxvfnHRR+3VVXvIjineCcgyXw==", "dev": true, "license": "MIT", "dependencies": { @@ -12413,9 +12413,9 @@ } }, "node_modules/jsonata": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.1.0.tgz", - "integrity": "sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.2.1.tgz", + "integrity": "sha512-xd1uwUrKeIcJbsWhaoS3qAX4Ea8m0Mw0G5nlnAQvPT7TbZ5qaPdzBVTQia9KfyuyQm+nenfyjvzUDTRYHsC2sw==", "license": "MIT", "engines": { "node": ">= 8" @@ -12683,9 +12683,19 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -13002,14 +13012,24 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -13557,9 +13577,9 @@ } }, "node_modules/nightwatch": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-3.15.0.tgz", - "integrity": "sha512-Vvh7TsDyEN1YzOsDNoafEUPJDQ6jfnmJPAsWo/EmygljZiRk1Ja/pEqNAhE5UdYJzF38SNO46gJS8IRk9mUNfA==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-3.16.0.tgz", + "integrity": "sha512-B0/zFPY5ujEwIWIPqo2ClgITZ3chB3Nfq86YNWCyE3/P8BrCSvv2Y6BNUA+9mgu8WM/XF6GNlV3LONzZ1S+JSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13584,7 +13604,7 @@ "glob": "7.2.3", "jsdom": "^24.1.0", "lodash": "^4.17.21", - "minimatch": "3.1.2", + "minimatch": "3.1.5", "minimist": "1.2.6", "mocha": "10.8.2", "nightwatch-axe-verbose": "^2.3.0", @@ -13602,7 +13622,7 @@ "nightwatch": "bin/nightwatch" }, "engines": { - "node": ">= 16" + "node": ">= 18.20.5" }, "peerDependencies": { "@cucumber/cucumber": "*" @@ -13681,19 +13701,6 @@ "node": ">=10" } }, - "node_modules/nightwatch/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/nightwatch/node_modules/minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", diff --git a/package.json b/package.json index 6faf49f3..8a82b685 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "node >= 24" ], "devDependencies": { - "@babel/eslint-parser": "^7.28.6", + "@babel/eslint-parser": "^7.29.7", "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-transform-runtime": "^7.29.7", "@babel/preset-env": "^7.29.7", @@ -79,7 +79,7 @@ "imports-loader": "^5.0.0", "mini-css-extract-plugin": "2.10.2", "modify-source-webpack-plugin": "^4.1.0", - "nightwatch": "^3.15.0", + "nightwatch": "^3.16.0", "postcss": "^8.5.15", "postcss-css-variables": "^0.19.0", "postcss-import": "^16.1.1", @@ -142,7 +142,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.1.0", "json5": "^2.2.3", - "jsonata": "^2.1.0", + "jsonata": "^2.2.1", "jsonpath-plus": "^10.4.0", "jsonwebtoken": "9.0.3", "jsqr": "^1.4.0", @@ -155,7 +155,7 @@ "loglevel-message-prefix": "^3.0.0", "lz-string": "^1.5.0", "lz4js": "^0.2.0", - "markdown-it": "^14.1.1", + "markdown-it": "^14.2.0", "moment": "^2.30.1", "moment-timezone": "^0.6.2", "ngeohash": "^0.6.3", From 20b46bf1a0a788fbb9bd095ed45e3bb2e1d548f8 Mon Sep 17 00:00:00 2001 From: Syed Ishmum Ahnaf Date: Fri, 5 Jun 2026 15:47:48 +0600 Subject: [PATCH 6/8] fix: validate text encoding options (#2497) --- src/core/operations/DecodeText.mjs | 4 ++++ src/core/operations/EncodeText.mjs | 4 ++++ tests/operations/tests/CharEnc.mjs | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/core/operations/DecodeText.mjs b/src/core/operations/DecodeText.mjs index 0fc9d2b5..baf23336 100644 --- a/src/core/operations/DecodeText.mjs +++ b/src/core/operations/DecodeText.mjs @@ -5,6 +5,7 @@ */ import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; import cptable from "codepage"; import {CHR_ENC_CODE_PAGES} from "../lib/ChrEnc.mjs"; @@ -48,6 +49,9 @@ class DecodeText extends Operation { */ run(input, args) { const format = CHR_ENC_CODE_PAGES[args[0]]; + if (!format) { + throw new OperationError("Invalid encoding"); + } return cptable.utils.decode(format, new Uint8Array(input)); } diff --git a/src/core/operations/EncodeText.mjs b/src/core/operations/EncodeText.mjs index 8cc1450f..5cc09742 100644 --- a/src/core/operations/EncodeText.mjs +++ b/src/core/operations/EncodeText.mjs @@ -5,6 +5,7 @@ */ import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; import cptable from "codepage"; import {CHR_ENC_CODE_PAGES} from "../lib/ChrEnc.mjs"; @@ -48,6 +49,9 @@ class EncodeText extends Operation { */ run(input, args) { const format = CHR_ENC_CODE_PAGES[args[0]]; + if (!format) { + throw new OperationError("Invalid encoding"); + } const encoded = cptable.utils.encode(format, input); return new Uint8Array(encoded).buffer; } diff --git a/tests/operations/tests/CharEnc.mjs b/tests/operations/tests/CharEnc.mjs index eecfaab6..83f71ca9 100644 --- a/tests/operations/tests/CharEnc.mjs +++ b/tests/operations/tests/CharEnc.mjs @@ -68,6 +68,32 @@ TestRegister.addTests([ }, ], }, + { + name: "Encode text: empty encoding", + input: "hello", + expectedOutput: "Invalid encoding", + recipeConfig: [ + { + "op": "Encode text", + "args": [""] + }, + ], + }, + { + name: "Decode text: empty encoding", + input: "68 65 6c 6c 6f", + expectedOutput: "Invalid encoding", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Space"] + }, + { + "op": "Decode text", + "args": [""] + }, + ], + }, { name: "Generate Base64 Windows PowerShell", input: "ZABpAHIAIAAiAGMAOgBcAHAAcgBvAGcAcgBhAG0AIABmAGkAbABlAHMAIgAgAA==", From da202d54f160fb07728f7070b4eccd1b765543cb Mon Sep 17 00:00:00 2001 From: andreas Date: Fri, 5 Jun 2026 12:23:38 +0200 Subject: [PATCH 7/8] feat: Get AES IV from input (QoL) (#2471) --- src/core/operations/AESDecrypt.mjs | 77 ++++-- src/core/operations/AESEncrypt.mjs | 37 ++- tests/browser/02_ops.js | 4 +- tests/node/tests/operations.mjs | 39 ++++ tests/operations/tests/Crypt.mjs | 350 +++++++++++++++++++++++----- tests/operations/tests/Register.mjs | 4 +- 6 files changed, 419 insertions(+), 92 deletions(-) diff --git a/src/core/operations/AESDecrypt.mjs b/src/core/operations/AESDecrypt.mjs index 5e6cec26..44e6cab2 100644 --- a/src/core/operations/AESDecrypt.mjs +++ b/src/core/operations/AESDecrypt.mjs @@ -39,41 +39,46 @@ class AESDecrypt extends Operation { "value": "", "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] }, + { + "name": "IV Length", + "type": "number", + "value": 16 + }, { "name": "Mode", "type": "argSelector", "value": [ { name: "CBC", - off: [5, 6] + off: [6, 7] }, { name: "CFB", - off: [5, 6] + off: [6, 7] }, { name: "OFB", - off: [5, 6] + off: [6, 7] }, { name: "CTR", - off: [5, 6] + off: [6, 7] }, { name: "GCM", - on: [5, 6] + on: [6, 7] }, { name: "ECB", - off: [5, 6] + off: [6, 7] }, { name: "CBC/NoPadding", - off: [5, 6] + off: [6, 7] }, { name: "ECB/NoPadding", - off: [5, 6] + off: [6, 7] } ] }, @@ -98,6 +103,26 @@ class AESDecrypt extends Operation { "type": "toggleString", "value": "", "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "IV from input", + "type": "argSelector", + "value": [ + { + name: "Off", + on: [1], + off: [2] + }, + { + name: "From start", + on: [2], + off: [1] + }, { + name: "From end", + on: [2], + off: [1] + } + ] } ]; } @@ -110,14 +135,18 @@ class AESDecrypt extends Operation { * @throws {OperationError} if cannot decrypt input or invalid key length */ run(input, args) { + let iv; + const key = Utils.convertToByteString(args[0].string, args[0].option), - iv = Utils.convertToByteString(args[1].string, args[1].option), - mode = args[2].split("/")[0], - noPadding = args[2].endsWith("NoPadding"), - inputType = args[3], - outputType = args[4], - gcmTag = Utils.convertToByteString(args[5].string, args[5].option), - aad = Utils.convertToByteString(args[6].string, args[6].option); + ivLength = args[2], + mode = args[3].split("/")[0], + noPadding = args[3].endsWith("NoPadding"), + inputType = args[4], + outputType = args[5], + gcmTag = Utils.convertToByteString(args[6].string, args[6].option), + aad = Utils.convertToByteString(args[7].string, args[7].option), + ivFromInput = args[8]; + if ([16, 24, 32].indexOf(key.length) < 0) { throw new OperationError(`Invalid key length: ${key.length} bytes @@ -130,11 +159,27 @@ The following algorithms will be used based on the size of the key: input = Utils.convertToByteString(input, inputType); + if (ivFromInput !== "Off") { + if (input.length <= ivLength) { + throw new OperationError(`Input is too short to contain an IV of ${ivLength} bytes.`); + } + + if (ivFromInput === "From start") { + iv = input.substr(0, ivLength); + input = input.substr(ivLength); + } else { + iv = input.substr(input.length - ivLength); + input = input.substr(0, input.length - ivLength); + } + } else { + iv = Utils.convertToByteString(args[1].string, args[1].option); + } + const decipher = forge.cipher.createDecipher("AES-" + mode, key); /* Allow for a "no padding" mode */ if (noPadding) { - decipher.mode.unpad = function(output, options) { + decipher.mode.unpad = function (output, options) { return true; }; } diff --git a/src/core/operations/AESEncrypt.mjs b/src/core/operations/AESEncrypt.mjs index 84e1c540..8a1c25de 100644 --- a/src/core/operations/AESEncrypt.mjs +++ b/src/core/operations/AESEncrypt.mjs @@ -8,6 +8,7 @@ import Operation from "../Operation.mjs"; import Utils from "../Utils.mjs"; import forge from "node-forge"; import OperationError from "../errors/OperationError.mjs"; +import { toHexFast } from "../lib/Hex.mjs"; /** * AES Encrypt operation @@ -92,6 +93,11 @@ class AESEncrypt extends Operation { "type": "toggleString", "value": "", "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Include IV in output", + "type": "option", + "value": ["Off", "Prepend", "Append"] } ]; } @@ -107,10 +113,11 @@ class AESEncrypt extends Operation { const key = Utils.convertToByteString(args[0].string, args[0].option), iv = Utils.convertToByteString(args[1].string, args[1].option), mode = args[2].split("/")[0], - noPadding = args[2].endsWith("NoPadding"), + noPadding = args[2].endsWith("NoPadding"), inputType = args[3], outputType = args[4], - aad = Utils.convertToByteString(args[5].string, args[5].option); + aad = Utils.convertToByteString(args[5].string, args[5].option), + includeIV = args[6]; if ([16, 24, 32].indexOf(key.length) < 0) { throw new OperationError(`Invalid key length: ${key.length} bytes @@ -133,26 +140,34 @@ The following algorithms will be used based on the size of the key: additionalData: mode === "GCM" ? aad : undefined }); if (noPadding) { - cipher.mode.pad = function(output, options) { + cipher.mode.pad = function (output, options) { return true; }; } cipher.update(forge.util.createBuffer(input)); cipher.finish(); + let output = cipher.output.getBytes(); + + if (includeIV === "Prepend") { + output = iv + output; + } else if (includeIV === "Append") { + output = output + iv; + } + if (outputType === "Hex") { + output = toHexFast(Utils.strToByteArray(output)); + if (mode === "GCM") { - return cipher.output.toHex() + "\n\n" + + return output + "\n\n" + "Tag: " + cipher.mode.tag.toHex(); } - return cipher.output.toHex(); - } else { - if (mode === "GCM") { - return cipher.output.getBytes() + "\n\n" + - "Tag: " + cipher.mode.tag.getBytes(); - } - return cipher.output.getBytes(); + } else if (mode === "GCM") { + return output + "\n\n" + + "Tag: " + cipher.mode.tag.getBytes(); } + + return output; } } diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 9d29f50d..32bd14d6 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -30,8 +30,8 @@ module.exports = { testOp(browser, "A1Z26 Cipher Decode", "20 5 19 20 15 21 20 16 21 20", "testoutput"); testOp(browser, "A1Z26 Cipher Encode", "test input", "20 5 19 20 9 14 16 21 20"); testOp(browser, "ADD", "test input", "Ê»ÉÊv¿ÄÆËÊ", [{ "option": "Hex", "string": "56" }]); - testOp(browser, "AES Decrypt", "b443f7f7c16ac5396a34273f6f639caa", "test output", [{ "option": "Hex", "string": "00112233445566778899aabbccddeeff" }, { "option": "Hex", "string": "00000000000000000000000000000000" }, "CBC", "Hex", "Raw", { "option": "Hex", "string": "" }]); - testOp(browser, "AES Encrypt", "test input", "e42eb8fbfb7a98fff061cd2c1a794d92", [{"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, "CBC", "Raw", "Hex"]); + testOp(browser, "AES Decrypt", "b443f7f7c16ac5396a34273f6f639caa", "test output", [{ "option": "Hex", "string": "00112233445566778899aabbccddeeff" }, { "option": "Hex", "string": "00000000000000000000000000000000" }, 16, "CBC", "Hex", "Raw", { "option": "Hex", "string": "" }, { "option": "Hex", "string": "" }, "Off"]); + testOp(browser, "AES Encrypt", "test input", "e42eb8fbfb7a98fff061cd2c1a794d92", [{"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, "CBC", "Raw", "Hex", "Off"]); testOp(browser, "AND", "test input", "4$04 $044", [{ "option": "Hex", "string": "34" }]); testOp(browser, "Add line numbers", "test input", "1 test input"); testOp(browser, ["From Hex", "Add Text To Image", "SHA2"], Images.PNG_HEX, "50cdf8ea483c55564a091650c2bccb4586f919b721e5fe9d6a61660505b4346d6ebdb2ef0cf075a7728cd84cb26ea3e477b5bd86a94a49a27d79423994afb60a", [[], ["Chef", "Center", "Middle", 0, 0, 16, "Roboto"], []]); diff --git a/tests/node/tests/operations.mjs b/tests/node/tests/operations.mjs index 3b2bbda6..6cf85718 100644 --- a/tests/node/tests/operations.mjs +++ b/tests/node/tests/operations.mjs @@ -79,7 +79,46 @@ TestRegister.addApiTests([ string: "some iv some iv1", option: "utf8", }, + ivLength: 16, mode: "OFB", + inputType: "Hex", + outputType: "Raw", + gcmTag: { + option: "Hex", + string: "" + }, + aad: { + option: "Hex", + string: "" + }, + ivFromInput: "Off" + }); + assert.equal(result.toString(), "a slightly longer sampleinput?"); + }), + + it("AES decrypt: IV from input", () => { + const result = AESDecrypt("4a123af235a507bbc9d5871721d61b98504d569a9a5a7847e2d78315fec7736f6d6520697620736f6d6520697631", { + key: { + string: "some longer key1", + option: "utf8", + }, + iv: { + string: "", + option: "Hex", + }, + ivLength: 16, + mode: "OFB", + inputType: "Hex", + outputType: "Raw", + gcmTag: { + option: "Hex", + string: "" + }, + aad: { + option: "Hex", + string: "" + }, + ivFromInput: "From end" }); assert.equal(result.toString(), "a slightly longer sampleinput?"); }), diff --git a/tests/operations/tests/Crypt.mjs b/tests/operations/tests/Crypt.mjs index 504f64b9..18b1c6e5 100644 --- a/tests/operations/tests/Crypt.mjs +++ b/tests/operations/tests/Crypt.mjs @@ -74,7 +74,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": ""}, {"option": "Hex", "string": ""}, "CBC", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -90,7 +91,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, "CBC", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -106,7 +108,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, "CTR", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -122,7 +125,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, "CBC", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -138,7 +142,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, "CFB", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -154,7 +159,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, "OFB", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -170,7 +176,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, "CTR", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -186,7 +193,8 @@ The following algorithms will be used based on the size of the key: {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": ""}, "ECB", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -204,7 +212,8 @@ Tag: 16a3e732a605cc9ca29108f742ca0743`, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": ""}, "GCM", "Raw", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -222,7 +231,8 @@ Tag: 3b5378917f67b0aade9891fc6c291646`, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "ffeeddccbbaa99887766554433221100"}, "GCM", "Raw", "Hex", - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -238,7 +248,8 @@ Tag: 3b5378917f67b0aade9891fc6c291646`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CBC", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -254,7 +265,8 @@ Tag: 3b5378917f67b0aade9891fc6c291646`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CFB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -270,7 +282,8 @@ Tag: 3b5378917f67b0aade9891fc6c291646`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "OFB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -286,7 +299,8 @@ Tag: 3b5378917f67b0aade9891fc6c291646`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CTR", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -304,7 +318,8 @@ Tag: 70fad2ca19412c20f40fd06918736e56`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -322,7 +337,8 @@ Tag: 61cc4b70809452b0b3e38f913fa0a109`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -338,7 +354,8 @@ Tag: 61cc4b70809452b0b3e38f913fa0a109`, {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "ECB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -354,7 +371,8 @@ Tag: 61cc4b70809452b0b3e38f913fa0a109`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CBC", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -370,7 +388,8 @@ Tag: 61cc4b70809452b0b3e38f913fa0a109`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CFB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -386,7 +405,8 @@ Tag: 61cc4b70809452b0b3e38f913fa0a109`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "OFB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -402,7 +422,8 @@ Tag: 61cc4b70809452b0b3e38f913fa0a109`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CTR", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -420,7 +441,8 @@ Tag: 86db597d5302595223cadbd990f1309b`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -438,7 +460,8 @@ Tag: aeedf3e6ca4201577c0cf3e9ce58159d`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -454,7 +477,8 @@ Tag: aeedf3e6ca4201577c0cf3e9ce58159d`, {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "ECB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -470,7 +494,8 @@ Tag: aeedf3e6ca4201577c0cf3e9ce58159d`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CBC", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -486,7 +511,8 @@ Tag: aeedf3e6ca4201577c0cf3e9ce58159d`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CFB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -502,7 +528,8 @@ Tag: aeedf3e6ca4201577c0cf3e9ce58159d`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "OFB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -518,7 +545,8 @@ Tag: aeedf3e6ca4201577c0cf3e9ce58159d`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "CTR", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -536,7 +564,8 @@ Tag: 821b1e5f32dad052e502775a523d957a`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -554,7 +583,8 @@ Tag: a8f04c4d93bbef82bef61a103371aef9`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "GCM", "Hex", "Hex", - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -570,7 +600,46 @@ Tag: a8f04c4d93bbef82bef61a103371aef9`, {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, "ECB", "Hex", "Hex", - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" + ] + } + ], + }, + { + name: "AES Encrypt: AES-256-GCM, Binary, AAD, prepend IV to output", + input: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", + expectedOutput: `1748e7179bd56570d51fa4ba287cc3e51287f188ad4d7ab0d9ff69b3c29cb11f861389532d8cb9337181da2e8cfc74a84927e8c0dd7a28a32fd485afe694259a63c199b199b95edd87c7aa95329feac340f2b78b72956a85f367044d821766b1b7135815571df44900695f1518cf3ae38ecb650f + +Tag: a8f04c4d93bbef82bef61a103371aef9`, + recipeConfig: [ + { + "op": "AES Encrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + "GCM", "Hex", "Hex", + {"option": "UTF8", "string": "additional data"}, + "Prepend" + ] + } + ], + }, + { + name: "AES Encrypt: AES-256-GCM, Binary, AAD, append IV to output", + input: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", + expectedOutput: `1287f188ad4d7ab0d9ff69b3c29cb11f861389532d8cb9337181da2e8cfc74a84927e8c0dd7a28a32fd485afe694259a63c199b199b95edd87c7aa95329feac340f2b78b72956a85f367044d821766b1b7135815571df44900695f1518cf3ae38ecb650f1748e7179bd56570d51fa4ba287cc3e5 + +Tag: a8f04c4d93bbef82bef61a103371aef9`, + recipeConfig: [ + { + "op": "AES Encrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + "GCM", "Hex", "Hex", + {"option": "UTF8", "string": "additional data"}, + "Append" ] } ], @@ -776,9 +845,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": ""}, {"option": "Hex", "string": ""}, + 16, "CBC", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -793,9 +864,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, + 16, "CBC", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -810,9 +883,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00000000000000000000000000000000"}, + 16, "CTR", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -827,9 +902,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, + 16, "CBC", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -844,9 +921,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, + 16, "CFB", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -861,9 +940,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, + 16, "OFB", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -878,9 +959,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, + 16, "CTR", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -895,9 +978,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": ""}, + 16, "ECB", "Hex", "Raw", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -912,9 +997,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": ""}, + 16, "GCM", "Hex", "Raw", {"option": "Hex", "string": "16a3e732a605cc9ca29108f742ca0743"}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -929,9 +1016,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "00112233445566778899aabbccddeeff"}, {"option": "Hex", "string": "ffeeddccbbaa99887766554433221100"}, + 16, "GCM", "Hex", "Raw", {"option": "Hex", "string": "3b5378917f67b0aade9891fc6c291646"}, - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -946,9 +1035,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CBC", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -963,9 +1054,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CFB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -980,9 +1073,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "OFB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -997,9 +1092,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CTR", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1014,9 +1111,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "GCM", "Hex", "Hex", {"option": "Hex", "string": "70fad2ca19412c20f40fd06918736e56"}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1031,9 +1130,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "GCM", "Hex", "Hex", {"option": "Hex", "string": "61cc4b70809452b0b3e38f913fa0a109"}, - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -1048,9 +1149,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "ECB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1065,9 +1168,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CBC", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1082,9 +1187,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CFB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1099,9 +1206,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "OFB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1116,9 +1225,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CTR", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1133,9 +1244,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "GCM", "Hex", "Hex", {"option": "Hex", "string": "86db597d5302595223cadbd990f1309b"}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1150,9 +1263,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "GCM", "Hex", "Hex", {"option": "Hex", "string": "aeedf3e6ca4201577c0cf3e9ce58159d"}, - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -1167,9 +1282,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "ECB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1184,9 +1301,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CBC", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1201,9 +1320,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CFB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1218,9 +1339,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "OFB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1235,9 +1358,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "CTR", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1252,9 +1377,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "GCM", "Hex", "Hex", {"option": "Hex", "string": "821b1e5f32dad052e502775a523d957a"}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" ] } ], @@ -1269,9 +1396,11 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "GCM", "Hex", "Hex", {"option": "Hex", "string": "a8f04c4d93bbef82bef61a103371aef9"}, - {"option": "UTF8", "string": "additional data"} + {"option": "UTF8", "string": "additional data"}, + "Off" ] } ], @@ -1286,9 +1415,106 @@ The following algorithms will be used based on the size of the key: "args": [ {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, {"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"}, + 16, "ECB", "Hex", "Hex", {"option": "Hex", "string": ""}, - {"option": "Hex", "string": ""} + {"option": "Hex", "string": ""}, + "Off" + ] + } + ], + }, + { + name: "AES Decrypt: IV from input, with too short input", + input: "1748e7179bd56570d51fa4ba287cc3e5", + expectedOutput: "Input is too short to contain an IV of 16 bytes.", + recipeConfig: [ + { + "op": "AES Decrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": ""}, + 16, + "ECB", "Hex", "Hex", + {"option": "Hex", "string": ""}, + {"option": "Hex", "string": ""}, + "From start" + ] + } + ], + }, + { + name: "AES Decrypt: AES-256-ECB with IV from input start, Binary", + input: "1748e7179bd56570d51fa4ba287cc3e57e8521ba3f356ef692a51841807e141464aadc07bbc0ef2b628b8745bae356d245682a220688afca7be987b60cb120681ed42680ee93a67065619a3beaac11111a6cd88a6afa9e367722cb57df343f8548f2d691b295184da4ed5f3b763aaa8558502cb348ab58e81986337096e90caa", + expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", + recipeConfig: [ + { + "op": "AES Decrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": ""}, + 16, + "ECB", "Hex", "Hex", + {"option": "Hex", "string": ""}, + {"option": "Hex", "string": ""}, + "From start" + ] + } + ], + }, + { + name: "AES Decrypt: AES-256-ECB with IV from input end, Binary", + input: "7e8521ba3f356ef692a51841807e141464aadc07bbc0ef2b628b8745bae356d245682a220688afca7be987b60cb120681ed42680ee93a67065619a3beaac11111a6cd88a6afa9e367722cb57df343f8548f2d691b295184da4ed5f3b763aaa8558502cb348ab58e81986337096e90caa1748e7179bd56570d51fa4ba287cc3e5", + expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", + recipeConfig: [ + { + "op": "AES Decrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": ""}, + 16, + "ECB", "Hex", "Hex", + {"option": "Hex", "string": ""}, + {"option": "Hex", "string": ""}, + "From end" + ] + } + ], + }, + { + name: "AES Decrypt: AES-256-GCM with IV from input start, Binary, AAD", + input: "1748e7179bd56570d51fa4ba287cc3e51287f188ad4d7ab0d9ff69b3c29cb11f861389532d8cb9337181da2e8cfc74a84927e8c0dd7a28a32fd485afe694259a63c199b199b95edd87c7aa95329feac340f2b78b72956a85f367044d821766b1b7135815571df44900695f1518cf3ae38ecb650f", + expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", + recipeConfig: [ + { + "op": "AES Decrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": ""}, + 16, + "GCM", "Hex", "Hex", + {"option": "Hex", "string": "a8f04c4d93bbef82bef61a103371aef9"}, + {"option": "UTF8", "string": "additional data"}, + "From start" + ] + } + ], + }, + { + name: "AES Decrypt: AES-256-GCM with 12-byte IV from input start, Binary, AAD", + input: "1748e7179bd56570d51fa4ba623c81f4605da9ac3df29c67c43abe4aad5230dca82a98ab31f042fe871b81a0a1e8b8af41044d46f627828e7d11eca2d04ac27f4e7c7c9a20da87854df9868a2ddbd67d85f7db92f9ff1272cfb7955a2d279dbe715965011fddf6e730e79e7b22f89817", + expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018", + recipeConfig: [ + { + "op": "AES Decrypt", + "args": [ + {"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"}, + {"option": "Hex", "string": ""}, + 12, + "GCM", "Hex", "Hex", + {"option": "Hex", "string": "c311c9144f8ae145ec46e2c69179a4b7"}, + {"option": "UTF8", "string": "additional data"}, + "From start" ] } ], diff --git a/tests/operations/tests/Register.mjs b/tests/operations/tests/Register.mjs index 3ef7ef94..e455f0f8 100644 --- a/tests/operations/tests/Register.mjs +++ b/tests/operations/tests/Register.mjs @@ -59,6 +59,7 @@ TestRegister.addTests([ "option": "Hex", "string": "$R0" }, + 16, "CTR", "Hex", "Raw", { "option": "Hex", @@ -67,7 +68,8 @@ TestRegister.addTests([ { "option": "Hex", "string": "" - } + }, + "Off" ] } ] From 49436179536d669ef013fadff07689c7f3441806 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:10:04 +0300 Subject: [PATCH 8/8] Refactor Thrift serialization and deserialization operations - Moved binary and compact protocol parsing logic from ThriftDeserialize and ThriftSerialize classes to utility functions in Thrift.mjs. - Updated ThriftDeserialize to use new parseBinaryProtocol and parseCompactProtocol functions. - Simplified ThriftSerialize by utilizing buildBinaryStruct and writeValue functions for serialization. - Added comprehensive tests for Thrift serialization and deserialization, covering various data types and structures. - Ensured proper handling of edge cases in readVarint and fromZigZag functions. --- src/core/lib/Thrift.mjs | 374 +++++++++++++ src/core/operations/ThriftDeserialize.mjs | 265 +-------- src/core/operations/ThriftSerialize.mjs | 116 +--- tests/browser/02_ops.js | 2 + tests/node/tests/lib/Thrift.mjs | 644 ++++++++++++++++++++++ 5 files changed, 1031 insertions(+), 370 deletions(-) create mode 100644 src/core/lib/Thrift.mjs create mode 100644 tests/node/tests/lib/Thrift.mjs diff --git a/src/core/lib/Thrift.mjs b/src/core/lib/Thrift.mjs new file mode 100644 index 00000000..28212cf1 --- /dev/null +++ b/src/core/lib/Thrift.mjs @@ -0,0 +1,374 @@ +/** + * @author Engin Kaya + * @author engin0223 [engineda2014@hotmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Utils from "../Utils.mjs"; + +/** + * Recursively parses the JSON object to build out the Thrift byte array structure. + * + * @param {Object} jsonStruct + * @param {number[]} bytes + */ +export function buildBinaryStruct(jsonStruct, bytes) { + const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; + + for (const [key, fieldData] of Object.entries(jsonStruct)) { + const fieldId = parseInt(key.replace("field_", ""), 10); + if (isNaN(fieldId)) continue; + + const typeName = fieldData.type; + const fieldType = typeMap[typeName]; + const value = fieldData.value; + + // 1. Write Field Type (1 byte) + bytes.push(fieldType); + // 2. Write Field ID (2 bytes, Big Endian) + bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); + + // 3. Write Value + writeValue(typeName, value, bytes, typeMap); + } + + // Write T_STOP to close the struct + bytes.push(0); +} + +/** + * Serializes values into the byte stream according to their specific Thrift types. + * + * @param {string} typeName + * @param {*} value + * @param {number[]} bytes + * @param {Object} typeMap + */ +export function writeValue(typeName, value, bytes, typeMap) { + switch (typeName) { + case "BOOL": + bytes.push(value ? 1 : 0); + break; + case "I8": + bytes.push(value & 0xFF); + break; + case "I16": + bytes.push((value >> 8) & 0xFF, value & 0xFF); + break; + case "I32": + bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); + break; + case "I64": { + const bigVal = BigInt(value); + for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); + break; + } + case "DOUBLE": { + // 8 bytes IEEE 754 floating point (Big Endian) + const floatView = new DataView(new ArrayBuffer(8)); + floatView.setFloat64(0, value, false); + for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); + break; + } + case "BINARY": { + const textEncoder = new TextEncoder(); + const strBytes = textEncoder.encode(value); + const len = strBytes.length; + bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); + strBytes.forEach(b => bytes.push(b)); + break; + } + case "STRUCT": + // Recursively build nested structs + buildBinaryStruct(value, bytes); + break; + case "LIST": + case "SET": { + // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } + const elType = typeMap[value.elementType]; + bytes.push(elType); // 1 byte element type + + const listSize = value.elements.length; + bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size + + // Write each element recursively + value.elements.forEach(el => writeValue(value.elementType, el, bytes, typeMap)); + break; + } + case "MAP": { + // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } + const kType = typeMap[value.keyType]; + const vType = typeMap[value.valType]; + bytes.push(kType, vType); // 1 byte key type, 1 byte val type + + const mapSize = value.elements.length; + bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size + + // Write pairs + value.elements.forEach(pair => { + writeValue(value.keyType, pair.key, bytes, typeMap); + writeValue(value.valType, pair.val, bytes, typeMap); + }); + break; + } + default: + throw new Error(`Unsupported serialization type: ${typeName}`); + } +} + +// --- TBinaryProtocol Deserialization Functions --- + +/** + * Parses the incoming schema using TBinaryProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function parseBinaryProtocol(data, offset) { + const result = {}; + while (offset < data.byteLength) { + const fieldType = data.getUint8(offset++); + if (fieldType === 0) break; // T_STOP + + const fieldId = data.getInt16(offset); + offset += 2; + + const parsed = readBinaryType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: getBinaryTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; +} + +/** + * Reads and transforms binary datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ +export function readBinaryType(data, offset, type) { + let value; + switch (type) { + case 2: // BOOL + value = data.getUint8(offset++) === 1; + break; + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // DOUBLE + value = data.getFloat64(offset); + offset += 8; + break; + case 6: // I16 + value = data.getInt16(offset); + offset += 2; + break; + case 8: // I32 + value = data.getInt32(offset); + offset += 4; + break; + case 10: // I64 + // Note: using BigInt to avoid precision loss on 64-bit integers + value = data.getBigInt64(offset).toString(); + offset += 8; + break; + case 11: { // BINARY/STRING + const strLen = data.getInt32(offset); + offset += 4; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = parseBinaryProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + case 13: { // MAP + const keyType = data.getUint8(offset++); + const valType = data.getUint8(offset++); + const mapSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < mapSize; i++) { + const k = readBinaryType(data, offset, keyType); + offset = k.offset; + const v = readBinaryType(data, offset, valType); + offset = v.offset; + value.push({ key: k.value, val: v.value }); + } + break; + } + case 14: // SET + case 15: { // LIST + const elemType = data.getUint8(offset++); + const listSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < listSize; i++) { + const elem = readBinaryType(data, offset, elemType); + value.push(elem.value); + offset = elem.offset; + } + break; + } + default: + throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); + } + return { value, offset }; +} + +/** + * Returns string names for Binary protocol types. + * + * @param {number} type + * @returns {string} + */ +export function getBinaryTypeName(type) { + const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; + return types[type] || `UNKNOWN(${type})`; +} + +// --- TCompactProtocol Deserialization Functions --- + +/** + * Parses the incoming schema using TCompactProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function parseCompactProtocol(data, offset) { + const result = {}; + let lastFieldId = 0; + + while (offset < data.byteLength) { + const byte = data.getUint8(offset++); + if (byte === 0) break; // STOP field + + const modifier = (byte & 0xf0) >> 4; + const fieldType = byte & 0x0f; + + let fieldId; + if (modifier === 0) { + // Long form: read zigzag varint field ID + const idParsed = readVarint(data, offset); + fieldId = fromZigZag(idParsed.value); + offset = idParsed.offset; + } else { + // Short form: delta + fieldId = lastFieldId + modifier; + } + lastFieldId = fieldId; + + // Types 1 and 2 are boolean true/false encoded directly in the modifier + if (fieldType === 1) { + result[`field_${fieldId}`] = { type: "BOOL", value: true }; + continue; + } else if (fieldType === 2) { + result[`field_${fieldId}`] = { type: "BOOL", value: false }; + continue; + } + + const parsed = readCompactType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: getCompactTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; +} + +/** + * Reads and transforms compact datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ +export function readCompactType(data, offset, type) { + let value, varintParsed; + switch (type) { + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // I16 + case 5: // I32 + case 6: // I64 + varintParsed = readVarint(data, offset); + value = fromZigZag(varintParsed.value); // Decodes ZigZag + offset = varintParsed.offset; + break; + case 7: // DOUBLE + value = data.getFloat64(offset, true); // Little endian + offset += 8; + break; + case 8: { // BINARY/STRING + varintParsed = readVarint(data, offset); + const strLen = Number(varintParsed.value); // Not zigzagged + offset = varintParsed.offset; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = parseCompactProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact + // Implement based on the spec provided (e.g., sssstttt for lists) + default: + throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); + } + return { value, offset }; +} + +/** + * Returns string names for Compact protocol types. + * + * @param {number} type + * @returns {string} + */ +export function getCompactTypeName(type) { + const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; + return types[type] || `UNKNOWN(${type})`; +} + +/** + * Variable-length integer parsing logic helper. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function readVarint(data, offset) { + let result = 0n; + let shift = 0n; + while (true) { + if (offset >= data.byteLength) throw new Error("EOF reading varint"); + const byte = BigInt(data.getUint8(offset++)); + result |= (byte & 0x7fn) << shift; + if ((byte & 0x80n) === 0n) break; + shift += 7n; + } + return { value: result, offset: offset }; +} + +/** + * Decodes ZigZag parameters to system numbers. + * + * @param {bigint} n + * @returns {bigint} + */ +export function fromZigZag(n) { + // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation + return (n >> 1n) ^ -(n & 1n); +} diff --git a/src/core/operations/ThriftDeserialize.mjs b/src/core/operations/ThriftDeserialize.mjs index 7b04e7ce..0a00af70 100644 --- a/src/core/operations/ThriftDeserialize.mjs +++ b/src/core/operations/ThriftDeserialize.mjs @@ -6,7 +6,10 @@ */ import Operation from "../Operation.mjs"; -import Utils from "../Utils.mjs"; +import { + parseBinaryProtocol, + parseCompactProtocol +} from "../lib/Thrift.mjs"; /** * Operation to decode Apache Thrift binary blobs into JSON structures. @@ -47,271 +50,15 @@ class ThriftDeserialize extends Operation { let decodedObject = {}; try { if (protocol === "TBinaryProtocol") { - decodedObject = this.parseBinaryProtocol(data, 0).result; + decodedObject = parseBinaryProtocol(data, 0).result; } else if (protocol === "TCompactProtocol") { - decodedObject = this.parseCompactProtocol(data, 0).result; + decodedObject = parseCompactProtocol(data, 0).result; } return JSON.stringify(decodedObject, null, 4); } catch (err) { return `Error decoding Thrift payload: ${err.message}\n\nPartial output:\n${JSON.stringify(decodedObject, null, 4)}`; } } - - // --- TBinaryProtocol Implementation --- - - /** - * Parses the incoming schema using TBinaryProtocol constraints. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - parseBinaryProtocol(data, offset) { - const result = {}; - while (offset < data.byteLength) { - const fieldType = data.getUint8(offset++); - if (fieldType === 0) break; // T_STOP - - const fieldId = data.getInt16(offset); - offset += 2; - - const parsed = this.readBinaryType(data, offset, fieldType); - result[`field_${fieldId}`] = { type: this.getBinaryTypeName(fieldType), value: parsed.value }; - offset = parsed.offset; - } - return { result, offset }; - } - - /** - * Reads and transforms binary datatypes based on identifier rules. - * - * @param {DataView} data - * @param {number} offset - * @param {number} type - * @returns {Object} - */ - readBinaryType(data, offset, type) { - let value; - switch (type) { - case 2: // BOOL - value = data.getUint8(offset++) === 1; - break; - case 3: // I8 - value = data.getInt8(offset++); - break; - case 4: // DOUBLE - value = data.getFloat64(offset); - offset += 8; - break; - case 6: // I16 - value = data.getInt16(offset); - offset += 2; - break; - case 8: // I32 - value = data.getInt32(offset); - offset += 4; - break; - case 10: // I64 - // Note: using BigInt to avoid precision loss on 64-bit integers - value = data.getBigInt64(offset).toString(); - offset += 8; - break; - case 11: { // BINARY/STRING - const strLen = data.getInt32(offset); - offset += 4; - const strBytes = new Uint8Array(data.buffer, offset, strLen); - value = Utils.byteArrayToUtf8(strBytes); - offset += strLen; - break; - } - case 12: { // STRUCT - const structParsed = this.parseBinaryProtocol(data, offset); - value = structParsed.result; - offset = structParsed.offset; - break; - } - case 13: { // MAP - const keyType = data.getUint8(offset++); - const valType = data.getUint8(offset++); - const mapSize = data.getInt32(offset); - offset += 4; - value = []; - for (let i = 0; i < mapSize; i++) { - const k = this.readBinaryType(data, offset, keyType); - offset = k.offset; - const v = this.readBinaryType(data, offset, valType); - offset = v.offset; - value.push({ key: k.value, val: v.value }); - } - break; - } - case 14: // SET - case 15: { // LIST - const elemType = data.getUint8(offset++); - const listSize = data.getInt32(offset); - offset += 4; - value = []; - for (let i = 0; i < listSize; i++) { - const elem = this.readBinaryType(data, offset, elemType); - value.push(elem.value); - offset = elem.offset; - } - break; - } - default: - throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); - } - return { value, offset }; - } - - /** - * Returns string names for Binary protocol types. - * - * @param {number} type - * @returns {string} - */ - getBinaryTypeName(type) { - const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; - return types[type] || `UNKNOWN(${type})`; - } - - // --- TCompactProtocol Implementation --- - - /** - * Parses the incoming schema using TCompactProtocol constraints. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - parseCompactProtocol(data, offset) { - const result = {}; - let lastFieldId = 0; - - while (offset < data.byteLength) { - const byte = data.getUint8(offset++); - if (byte === 0) break; // STOP field - - const modifier = (byte & 0xf0) >> 4; - const fieldType = byte & 0x0f; - - let fieldId; - if (modifier === 0) { - // Long form: read zigzag varint field ID - const idParsed = this.readVarint(data, offset); - fieldId = this.fromZigZag(idParsed.value); - offset = idParsed.offset; - } else { - // Short form: delta - fieldId = lastFieldId + modifier; - } - lastFieldId = fieldId; - - // Types 1 and 2 are boolean true/false encoded directly in the modifier - if (fieldType === 1) { - result[`field_${fieldId}`] = { type: "BOOL", value: true }; - continue; - } else if (fieldType === 2) { - result[`field_${fieldId}`] = { type: "BOOL", value: false }; - continue; - } - - const parsed = this.readCompactType(data, offset, fieldType); - result[`field_${fieldId}`] = { type: this.getCompactTypeName(fieldType), value: parsed.value }; - offset = parsed.offset; - } - return { result, offset }; - } - - /** - * Reads and transforms compact datatypes based on identifier rules. - * - * @param {DataView} data - * @param {number} offset - * @param {number} type - * @returns {Object} - */ - readCompactType(data, offset, type) { - let value, varintParsed; - switch (type) { - case 3: // I8 - value = data.getInt8(offset++); - break; - case 4: // I16 - case 5: // I32 - case 6: // I64 - varintParsed = this.readVarint(data, offset); - value = this.fromZigZag(varintParsed.value); // Decodes ZigZag - offset = varintParsed.offset; - break; - case 7: // DOUBLE - value = data.getFloat64(offset, true); // Little endian - offset += 8; - break; - case 8: { // BINARY/STRING - varintParsed = this.readVarint(data, offset); - const strLen = Number(varintParsed.value); // Not zigzagged - offset = varintParsed.offset; - const strBytes = new Uint8Array(data.buffer, offset, strLen); - value = Utils.byteArrayToUtf8(strBytes); - offset += strLen; - break; - } - case 12: { // STRUCT - const structParsed = this.parseCompactProtocol(data, offset); - value = structParsed.result; - offset = structParsed.offset; - break; - } - // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact - // Implement based on the spec provided (e.g., sssstttt for lists) - default: - throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); - } - return { value, offset }; - } - - /** - * Returns string names for Compact protocol types. - * - * @param {number} type - * @returns {string} - */ - getCompactTypeName(type) { - const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; - return types[type] || `UNKNOWN(${type})`; - } - - /** - * Variable-length integer parsing logic helper. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - readVarint(data, offset) { - let result = 0n; - let shift = 0n; - while (true) { - if (offset >= data.byteLength) throw new Error("EOF reading varint"); - const byte = BigInt(data.getUint8(offset++)); - result |= (byte & 0x7fn) << shift; - if ((byte & 0x80n) === 0n) break; - shift += 7n; - } - return { value: result, offset: offset }; - } - - /** - * Decodes ZigZag parameters to system numbers. - * - * @param {bigint} n - * @returns {bigint} - */ - fromZigZag(n) { - // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation - return (n >> 1n) ^ -(n & 1n); - } } export default ThriftDeserialize; diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs index ae20948b..5fd357a7 100644 --- a/src/core/operations/ThriftSerialize.mjs +++ b/src/core/operations/ThriftSerialize.mjs @@ -6,6 +6,10 @@ */ import Operation from "../Operation.mjs"; +import { + buildBinaryStruct, + writeValue +} from "../lib/Thrift.mjs"; /** * Operation to encode a JSON structure into Apache Thrift TBinaryProtocol binary format. @@ -43,119 +47,9 @@ class ThriftSerialize extends Operation { } const bytes = []; - this.buildBinaryStruct(parsedInput, bytes); + buildBinaryStruct(parsedInput, bytes); return new Uint8Array(bytes).buffer; } - - /** - * Recursively parses the JSON object to build out the Thrift byte array structure. - * - * @param {Object} jsonStruct - * @param {number[]} bytes - */ - buildBinaryStruct(jsonStruct, bytes) { - const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; - - for (const [key, fieldData] of Object.entries(jsonStruct)) { - const fieldId = parseInt(key.replace("field_", ""), 10); - if (isNaN(fieldId)) continue; - - const typeName = fieldData.type; - const fieldType = typeMap[typeName]; - const value = fieldData.value; - - // 1. Write Field Type (1 byte) - bytes.push(fieldType); - // 2. Write Field ID (2 bytes, Big Endian) - bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); - - // 3. Write Value - this.writeValue(typeName, value, bytes, typeMap); - } - - // Write T_STOP to close the struct - bytes.push(0); - } - - /** - * Serializes values into the byte stream according to their specific Thrift types. - * - * @param {string} typeName - * @param {*} value - * @param {number[]} bytes - * @param {Object} typeMap - */ - writeValue(typeName, value, bytes, typeMap) { - switch (typeName) { - case "BOOL": - bytes.push(value ? 1 : 0); - break; - case "I8": - bytes.push(value & 0xFF); - break; - case "I16": - bytes.push((value >> 8) & 0xFF, value & 0xFF); - break; - case "I32": - bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); - break; - case "I64": { - const bigVal = BigInt(value); - for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); - break; - } - case "DOUBLE": { - // 8 bytes IEEE 754 floating point (Big Endian) - const floatView = new DataView(new ArrayBuffer(8)); - floatView.setFloat64(0, value, false); - for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); - break; - } - case "BINARY": { - const textEncoder = new TextEncoder(); - const strBytes = textEncoder.encode(value); - const len = strBytes.length; - bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); - strBytes.forEach(b => bytes.push(b)); - break; - } - case "STRUCT": - // Recursively build nested structs - this.buildBinaryStruct(value, bytes); - break; - case "LIST": - case "SET": { - // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } - const elType = typeMap[value.elementType]; - bytes.push(elType); // 1 byte element type - - const listSize = value.elements.length; - bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size - - // Write each element recursively - value.elements.forEach(el => this.writeValue(value.elementType, el, bytes, typeMap)); - break; - } - case "MAP": { - // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } - const kType = typeMap[value.keyType]; - const vType = typeMap[value.valType]; - bytes.push(kType, vType); // 1 byte key type, 1 byte val type - - const mapSize = value.elements.length; - bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size - - // Write pairs - value.elements.forEach(pair => { - this.writeValue(value.keyType, pair.key, bytes, typeMap); - this.writeValue(value.valType, pair.val, bytes, typeMap); - }); - break; - } - default: - throw new Error(`Unsupported serialization type: ${typeName}`); - } - } } export default ThriftSerialize; diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 9d29f50d..3a23506f 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -355,6 +355,8 @@ module.exports = { // testOp(browser, "Take bytes", "test input", "test_output"); testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/); testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]); + testOp(browser, ["Thrift Serialize", "To Hex"], "{\"field_1\": { \"type\": \"I32\", \"value\": 100 }}", "08 00 01 00 00 00 64 00"); + testOpHtml(browser, ["From Hex", "Thrift Deserialize"], "08 00 01 00 00 00 64 00", ".json-dict .json-literal", "100", [["Auto"], ["TBinaryProtocol"]]); testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/); // testOp(browser, "To BCD", "test input", "test_output"); // testOp(browser, "To Base", "test input", "test_output"); diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs new file mode 100644 index 00000000..7a975c86 --- /dev/null +++ b/tests/node/tests/lib/Thrift.mjs @@ -0,0 +1,644 @@ +import TestRegister from "../../../lib/TestRegister.mjs"; +import { + buildBinaryStruct, + writeValue, + parseBinaryProtocol, + readBinaryType, + getBinaryTypeName, + parseCompactProtocol, + readCompactType, + getCompactTypeName, + readVarint, + fromZigZag +} from "../../../../src/core/lib/Thrift.mjs"; +import it from "../../assertionHandler.mjs"; +import assert from "assert"; + +TestRegister.addApiTests([ + // ===== readVarint tests ===== + it("Thrift: readVarint - single byte (0)", () => { + const bytes = new Uint8Array([0x00]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 0n); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readVarint - single byte (127)", () => { + const bytes = new Uint8Array([0x7F]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 127n); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readVarint - two bytes (128)", () => { + const bytes = new Uint8Array([0x80, 0x01]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 128n); + assert.strictEqual(result.offset, 2); + }), + + it("Thrift: readVarint - multiple bytes (16384)", () => { + const bytes = new Uint8Array([0x80, 0x80, 0x01]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 16384n); + assert.strictEqual(result.offset, 3); + }), + + it("Thrift: readVarint - starting from non-zero offset", () => { + const bytes = new Uint8Array([0xFF, 0xFF, 0x42]); // 0xFF is continuation, 0xFF is continuation, 0x42 is end + const data = new DataView(bytes.buffer); + const result = readVarint(data, 1); + assert.strictEqual(result.offset, 3); + }), + + it("Thrift: readVarint - EOF error", () => { + const bytes = new Uint8Array([0x80]); // Missing continuation byte + const data = new DataView(bytes.buffer); + assert.throws(() => readVarint(data, 0), /EOF reading varint/); + }), + + // ===== fromZigZag tests ===== + it("Thrift: fromZigZag - zero", () => { + assert.strictEqual(fromZigZag(0n), 0n); + }), + + it("Thrift: fromZigZag - positive numbers", () => { + assert.strictEqual(fromZigZag(2n), 1n); + assert.strictEqual(fromZigZag(4n), 2n); + assert.strictEqual(fromZigZag(6n), 3n); + }), + + it("Thrift: fromZigZag - negative numbers", () => { + assert.strictEqual(fromZigZag(1n), -1n); + assert.strictEqual(fromZigZag(3n), -2n); + assert.strictEqual(fromZigZag(5n), -3n); + }), + + it("Thrift: fromZigZag - large positive", () => { + assert.strictEqual(fromZigZag(BigInt("1000000000")), BigInt("500000000")); + }), + + it("Thrift: fromZigZag - large negative", () => { + assert.strictEqual(fromZigZag(BigInt("1000000001")), BigInt("-500000001")); + }), + + // ===== getBinaryTypeName tests ===== + it("Thrift: getBinaryTypeName - all type mappings", () => { + assert.strictEqual(getBinaryTypeName(2), "BOOL"); + assert.strictEqual(getBinaryTypeName(3), "I8"); + assert.strictEqual(getBinaryTypeName(4), "DOUBLE"); + assert.strictEqual(getBinaryTypeName(6), "I16"); + assert.strictEqual(getBinaryTypeName(8), "I32"); + assert.strictEqual(getBinaryTypeName(10), "I64"); + assert.strictEqual(getBinaryTypeName(11), "BINARY"); + assert.strictEqual(getBinaryTypeName(12), "STRUCT"); + assert.strictEqual(getBinaryTypeName(13), "MAP"); + assert.strictEqual(getBinaryTypeName(14), "SET"); + assert.strictEqual(getBinaryTypeName(15), "LIST"); + }), + + it("Thrift: getBinaryTypeName - unknown type", () => { + assert.strictEqual(getBinaryTypeName(99), "UNKNOWN(99)"); + }), + + // ===== getCompactTypeName tests ===== + it("Thrift: getCompactTypeName - all type mappings", () => { + assert.strictEqual(getCompactTypeName(1), "BOOLEAN_TRUE"); + assert.strictEqual(getCompactTypeName(2), "BOOLEAN_FALSE"); + assert.strictEqual(getCompactTypeName(3), "I8"); + assert.strictEqual(getCompactTypeName(4), "I16"); + assert.strictEqual(getCompactTypeName(5), "I32"); + assert.strictEqual(getCompactTypeName(6), "I64"); + assert.strictEqual(getCompactTypeName(7), "DOUBLE"); + assert.strictEqual(getCompactTypeName(8), "BINARY"); + assert.strictEqual(getCompactTypeName(9), "LIST"); + assert.strictEqual(getCompactTypeName(10), "SET"); + assert.strictEqual(getCompactTypeName(11), "MAP"); + assert.strictEqual(getCompactTypeName(12), "STRUCT"); + assert.strictEqual(getCompactTypeName(13), "UUID"); + }), + + it("Thrift: getCompactTypeName - unknown type", () => { + assert.strictEqual(getCompactTypeName(99), "UNKNOWN(99)"); + }), + + // ===== writeValue tests - basic types ===== + it("Thrift: writeValue - BOOL true", () => { + const bytes = []; + const typeMap = { "BOOL": 2 }; + writeValue("BOOL", true, bytes, typeMap); + assert.deepStrictEqual(bytes, [1]); + }), + + it("Thrift: writeValue - BOOL false", () => { + const bytes = []; + const typeMap = { "BOOL": 2 }; + writeValue("BOOL", false, bytes, typeMap); + assert.deepStrictEqual(bytes, [0]); + }), + + it("Thrift: writeValue - I8 zero", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0]); + }), + + it("Thrift: writeValue - I8 positive", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 42, bytes, typeMap); + assert.deepStrictEqual(bytes, [42]); + }), + + it("Thrift: writeValue - I8 max", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 127, bytes, typeMap); + assert.deepStrictEqual(bytes, [127]); + }), + + it("Thrift: writeValue - I16", () => { + const bytes = []; + const typeMap = { "I16": 6 }; + writeValue("I16", 0x1234, bytes, typeMap); + assert.deepStrictEqual(bytes, [0x12, 0x34]); + }), + + it("Thrift: writeValue - I16 zero", () => { + const bytes = []; + const typeMap = { "I16": 6 }; + writeValue("I16", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0]); + }), + + it("Thrift: writeValue - I32", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", 0x12345678, bytes, typeMap); + assert.deepStrictEqual(bytes, [0x12, 0x34, 0x56, 0x78]); + }), + + it("Thrift: writeValue - I32 zero", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0]); + }), + + it("Thrift: writeValue - I32 negative", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", -1, bytes, typeMap); + assert.deepStrictEqual(bytes, [0xFF, 0xFF, 0xFF, 0xFF]); + }), + + it("Thrift: writeValue - I64", () => { + const bytes = []; + const typeMap = { "I64": 10 }; + writeValue("I64", "0x0102030405060708", bytes, typeMap); + assert.deepStrictEqual(bytes, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + }), + + it("Thrift: writeValue - I64 zero", () => { + const bytes = []; + const typeMap = { "I64": 10 }; + writeValue("I64", "0", bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0, 0, 0, 0, 0]); + }), + + it("Thrift: writeValue - DOUBLE", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", 3.14159, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + // Verify it's a valid IEEE 754 double by reading it back + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.ok(Math.abs(view.getFloat64(0, false) - 3.14159) < 0.00001); + }), + + it("Thrift: writeValue - DOUBLE zero", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", 0.0, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.strictEqual(view.getFloat64(0, false), 0); + }), + + it("Thrift: writeValue - DOUBLE negative", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", -42.5, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.strictEqual(view.getFloat64(0, false), -42.5); + }), + + it("Thrift: writeValue - BINARY empty string", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "", bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0]); + }), + + it("Thrift: writeValue - BINARY ASCII", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "hello", bytes, typeMap); + const expected = [0, 0, 0, 5, ...new TextEncoder().encode("hello")]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - BINARY UTF-8", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "你好", bytes, typeMap); + const encoded = new TextEncoder().encode("你好"); + const expected = [0, 0, 0, encoded.length, ...Array.from(encoded)]; + assert.deepStrictEqual(bytes, expected); + }), + + // ===== writeValue tests - collections ===== + it("Thrift: writeValue - LIST of I32 empty", () => { + const bytes = []; + const typeMap = { "I32": 8, "LIST": 15 }; + const listValue = { elementType: "I32", elements: [] }; + writeValue("LIST", listValue, bytes, typeMap); + assert.deepStrictEqual(bytes, [8, 0, 0, 0, 0]); + }), + + it("Thrift: writeValue - LIST of I32", () => { + const bytes = []; + const typeMap = { "I32": 8, "LIST": 15 }; + const listValue = { elementType: "I32", elements: [1, 2, 3] }; + writeValue("LIST", listValue, bytes, typeMap); + const expected = [ + 8, // element type (I32) + 0, 0, 0, 3, // list size = 3 + 0, 0, 0, 1, // element 1 + 0, 0, 0, 2, // element 2 + 0, 0, 0, 3 // element 3 + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - SET of BINARY", () => { + const bytes = []; + const typeMap = { "BINARY": 11, "SET": 14 }; + const setValue = { elementType: "BINARY", elements: ["a"] }; + writeValue("SET", setValue, bytes, typeMap); + const encoded = new TextEncoder().encode("a"); + const expected = [ + 11, // element type (BINARY) + 0, 0, 0, 1, // set size = 1 + 0, 0, 0, 1, // string length + ...encoded // string data + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - MAP empty", () => { + const bytes = []; + const typeMap = { "I32": 8, "BINARY": 11, "MAP": 13 }; + const mapValue = { keyType: "I32", valType: "BINARY", elements: [] }; + writeValue("MAP", mapValue, bytes, typeMap); + assert.deepStrictEqual(bytes, [8, 11, 0, 0, 0, 0]); // key type, val type, size = 0 + }), + + it("Thrift: writeValue - MAP with entry", () => { + const bytes = []; + const typeMap = { "I32": 8, "BINARY": 11, "MAP": 13 }; + const mapValue = { + keyType: "I32", + valType: "BINARY", + elements: [{ key: 1, val: "test" }] + }; + writeValue("MAP", mapValue, bytes, typeMap); + const encoded = new TextEncoder().encode("test"); + const expected = [ + 8, 11, // key type, val type + 0, 0, 0, 1, // map size = 1 + 0, 0, 0, 1, // key value + 0, 0, 0, 4, // val length + ...encoded // val data + ]; + assert.deepStrictEqual(bytes, expected); + }), + + // ===== buildBinaryStruct tests ===== + it("Thrift: buildBinaryStruct - simple struct", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "I32", value: 42 } + }; + buildBinaryStruct(jsonStruct, bytes); + const expected = [ + 8, 0, 1, // field type (I32), field id = 1 + 0, 0, 0, 42, // value + 0 // T_STOP + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: buildBinaryStruct - multiple fields", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "I32", value: 42 }, + field_2: { type: "BINARY", value: "hello" } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should end with T_STOP + assert.strictEqual(bytes[bytes.length - 1], 0); + }), + + it("Thrift: buildBinaryStruct - BOOL field", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "BOOL", value: true } + }; + buildBinaryStruct(jsonStruct, bytes); + const expected = [ + 2, 0, 1, // field type (BOOL), field id = 1 + 1, // value = true + 0 // T_STOP + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: buildBinaryStruct - nested struct", () => { + const bytes = []; + const jsonStruct = { + field_1: { + type: "STRUCT", + value: { + field_1: { type: "I32", value: 10 } + } + } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should contain nested structure with T_STOP markers + assert.ok(bytes.length > 0); + assert.strictEqual(bytes[bytes.length - 1], 0); // ends with T_STOP + }), + + it("Thrift: buildBinaryStruct - invalid field ID skipped", () => { + const bytes = []; + const jsonStruct = { + invalid_field: { type: "I32", value: 42 }, + field_1: { type: "I32", value: 10 } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should skip invalid_field and only process field_1 + assert.strictEqual(bytes[0], 8); // field type + assert.strictEqual(bytes[3], 10); // field value + }), + + // ===== readBinaryType tests ===== + it("Thrift: readBinaryType - BOOL true", () => { + const bytes = new Uint8Array([1]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 2); + assert.strictEqual(result.value, true); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - BOOL false", () => { + const bytes = new Uint8Array([0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 2); + assert.strictEqual(result.value, false); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - I8", () => { + const bytes = new Uint8Array([42]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 3); + assert.strictEqual(result.value, 42); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - I16", () => { + const bytes = new Uint8Array([0x12, 0x34]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 6); + assert.strictEqual(result.value, 0x1234); + assert.strictEqual(result.offset, 2); + }), + + it("Thrift: readBinaryType - I32", () => { + const bytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 8); + assert.strictEqual(result.value, 0x12345678); + assert.strictEqual(result.offset, 4); + }), + + it("Thrift: readBinaryType - I64", () => { + const bytes = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 10); + assert.strictEqual(result.value, "66"); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readBinaryType - DOUBLE", () => { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, 3.14159, false); + const result = readBinaryType(view, 0, 4); + assert.ok(Math.abs(result.value - 3.14159) < 0.00001); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readBinaryType - BINARY empty", () => { + const bytes = new Uint8Array([0, 0, 0, 0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 11); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.offset, 4); + }), + + it("Thrift: readBinaryType - BINARY with data", () => { + const str = "hello"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([0, 0, 0, 5, ...encoded]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 11); + assert.strictEqual(result.value, str); + assert.strictEqual(result.offset, 9); + }), + + it("Thrift: readBinaryType - LIST of I32", () => { + const bytes = new Uint8Array([ + 8, // element type (I32) + 0, 0, 0, 2, // size = 2 + 0, 0, 0, 1, // element 1 + 0, 0, 0, 2 // element 2 + ]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 15); + assert.deepStrictEqual(result.value, [1, 2]); + assert.strictEqual(result.offset, 13); + }), + + it("Thrift: readBinaryType - LIST empty", () => { + const bytes = new Uint8Array([8, 0, 0, 0, 0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 15); + assert.deepStrictEqual(result.value, []); + assert.strictEqual(result.offset, 5); + }), + + it("Thrift: readBinaryType - MAP", () => { + const str = "key"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([ + 8, 11, // key type (I32), val type (BINARY) + 0, 0, 0, 1, // size = 1 + 0, 0, 0, 1, // key value + 0, 0, 0, 3, // val length + ...encoded // val data + ]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 13); + assert.deepStrictEqual(result.value, [{ key: 1, val: "key" }]); + assert.strictEqual(result.offset, 13); + }), + + // ===== parseBinaryProtocol tests ===== + it("Thrift: parseBinaryProtocol - simple I32", () => { + const bytes = new Uint8Array([ + 8, 0, 1, // field type (I32), field id = 1 + 0, 0, 0, 42, // value + 0 // T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.deepStrictEqual(result.result.field_1, { type: "I32", value: 42 }); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: parseBinaryProtocol - multiple fields", () => { + const bytes = new Uint8Array([ + 2, 0, 1, // BOOL field 1 + 1, // value = true + 8, 0, 2, // I32 field 2 + 0, 0, 0, 42, // value = 42 + 0 // T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOL"); + assert.strictEqual(result.result.field_1.value, true); + assert.strictEqual(result.result.field_2.type, "I32"); + assert.strictEqual(result.result.field_2.value, 42); + }), + + it("Thrift: parseBinaryProtocol - nested struct", () => { + const bytes = new Uint8Array([ + 12, 0, 1, // STRUCT field 1 + 8, 0, 1, // nested I32 field 1 + 0, 0, 0, 10, // value + 0, // nested T_STOP + 0 // outer T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.ok(result.result.field_1); + assert.strictEqual(result.result.field_1.type, "STRUCT"); + assert.deepStrictEqual(result.result.field_1.value.field_1, { type: "I32", value: 10 }); + }), + + // ===== readCompactType tests ===== + it("Thrift: readCompactType - I8", () => { + const bytes = new Uint8Array([42]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 3); + assert.strictEqual(result.value, 42); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readCompactType - I16 zigzag", () => { + // ZigZag of -1 is 1 + const bytes = new Uint8Array([1]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 4); + assert.strictEqual(result.value, -1n); + }), + + it("Thrift: readCompactType - I32 zigzag", () => { + // ZigZag of 100 is 200 + const bytes = new Uint8Array([200]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 5); + assert.strictEqual(result.value, 100n); + }), + + it("Thrift: readCompactType - DOUBLE", () => { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, 2.71828, true); + const result = readCompactType(view, 0, 7); + assert.ok(Math.abs(result.value - 2.71828) < 0.00001); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readCompactType - BINARY empty", () => { + const bytes = new Uint8Array([0]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 8); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readCompactType - BINARY with data", () => { + const str = "test"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([4, ...encoded]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 8); + assert.strictEqual(result.value, str); + assert.strictEqual(result.offset, 5); + }), + + // ===== parseCompactProtocol tests ===== + it("Thrift: parseCompactProtocol - empty struct", () => { + const bytes = new Uint8Array([0]); // Just STOP + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.deepStrictEqual(result.result, {}); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: parseCompactProtocol - boolean true field", () => { + const bytes = new Uint8Array([ + 0x11, // field modifier=1 (delta=1), type=1 (BOOL_TRUE) + 0 // STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOLEAN_TRUE"); + assert.strictEqual(result.result.field_1.value, true); + }), + + it("Thrift: parseCompactProtocol - boolean false field", () => { + const bytes = new Uint8Array([ + 0x12, // field modifier=1 (delta=1), type=2 (BOOL_FALSE) + 0 // STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOLEAN_FALSE"); + assert.strictEqual(result.result.field_1.value, false); + }), + +]);