From f759f4c43b098c7984c68c4c7a629c9cd5512df8 Mon Sep 17 00:00:00 2001
From: p-leriche <7701190+p-leriche@users.noreply.github.com>
Date: Sun, 8 Mar 2026 13:46:36 +0000
Subject: [PATCH] Add Text/Integer Converter operation (#2213)
Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> - Additional test case added.
---
src/core/config/Categories.json | 1 +
src/core/operations/TextIntegerConverter.mjs | 123 +++++++++++
tests/operations/index.mjs | 1 +
.../operations/tests/TextIntegerConverter.mjs | 199 ++++++++++++++++++
4 files changed, 324 insertions(+)
create mode 100644 src/core/operations/TextIntegerConverter.mjs
create mode 100644 tests/operations/tests/TextIntegerConverter.mjs
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index e59c6aeb..f03fb8ea 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -41,6 +41,7 @@
"From Base",
"To BCD",
"From BCD",
+ "Text-Integer Conversion",
"To HTML Entity",
"From HTML Entity",
"URL Encode",
diff --git a/src/core/operations/TextIntegerConverter.mjs b/src/core/operations/TextIntegerConverter.mjs
new file mode 100644
index 00000000..4e740100
--- /dev/null
+++ b/src/core/operations/TextIntegerConverter.mjs
@@ -0,0 +1,123 @@
+/**
+ * @author p-leriche [philip.leriche@cantab.net]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/* ---------- helper functions ---------- */
+
+/**
+ * Convert text to BigInt (big-endian byte interpretation)
+ */
+function textToBigInt(text) {
+ if (text.length === 0) return 0n;
+
+ let result = 0n;
+ for (let i = 0; i < text.length; i++) {
+ const charCode = BigInt(text.charCodeAt(i));
+ if (charCode > 255n) {
+ throw new OperationError(
+ `Character at position ${i} exceeds Latin-1 range (0-255).\n` +
+ "Only ASCII and Latin-1 characters are supported.");
+ }
+ result = (result << 8n) | charCode;
+ }
+ return result;
+}
+
+/**
+ * Convert BigInt to text (big-endian byte interpretation)
+ */
+function bigIntToText(value) {
+ if (value === 0n) return "";
+
+ const bytes = [];
+ let num = value;
+
+ while (num > 0n) {
+ bytes.unshift(Number(num & 0xFFn));
+ num >>= 8n;
+ }
+
+ return String.fromCharCode(...bytes);
+}
+
+/* ---------- operation class ---------- */
+
+/**
+ * Text/Integer Converter operation
+ */
+class TextIntegerConverter extends Operation {
+ /**
+ * TextIntegerConverter constructor
+ */
+ constructor() {
+ super();
+
+ this.description =
+ "Converts between text strings and large integers (decimal or hexadecimal).
" +
+ "Text is interpreted as a big-endian sequence of character codes. For example:
" +
+ "ABC is 0x414243 (hex) is 4276803 (decimal)
" +
+ "Input format detection:
" +
+ "Decimal: digits 0-9 only
" +
+ "Hexadecimal: 0x... prefix
" +
+ "Quoted or unquoted text: treated as string
" +
+ "Character limitations:
" +
+ "Text input may only contain ASCII and Latin-1 characters (code point < 256).
" +
+ "Multi-byte Unicode characters will generate an error.
." ;
+ this.infoURL = "https://wikipedia.org/wiki/Endianness";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Output format",
+ type: "option",
+ value: ["String", "Decimal", "Hexadecimal"]
+ }
+ ];
+ this.name = "Text-Integer Conversion";
+ this.module = "Default";
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const outputFormat = args[0];
+ const trimmed = input.trim();
+
+ let bigIntValue;
+
+ if (!trimmed) {
+ // Null input - treat as zero
+ bigIntValue = 0;
+ } else if (/^0x[0-9a-f]+$/i.test(trimmed) ||
+ /^[+-]?[0-9]+$/.test(trimmed)) {
+ // Hex or decimal integer
+ bigIntValue = BigInt(trimmed);
+ } else if (/^["'].*["']$/.test(trimmed)) {
+ // Quoted string: Remove quotes and convert text to BigInt
+ const text = trimmed.slice(1, -1);
+ bigIntValue = textToBigInt(text);
+ } else {
+ // Assume it's unquoted text
+ bigIntValue = textToBigInt(trimmed);
+ }
+
+ // Convert to output format
+ if (outputFormat === "String") {
+ return bigIntToText(bigIntValue);
+ } else if (outputFormat === "Decimal") {
+ return bigIntValue.toString();
+ } else { // Hexadecimal
+ return "0x" + bigIntValue.toString(16);
+ }
+ }
+}
+
+export default TextIntegerConverter;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 493afd67..48ff4c8e 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -165,6 +165,7 @@ import "./tests/SymmetricDifference.mjs";
import "./tests/TakeNthBytes.mjs";
import "./tests/Template.mjs";
import "./tests/TextEncodingBruteForce.mjs";
+import "./tests/TextIntegerConverter.mjs";
import "./tests/ToFromInsensitiveRegex.mjs";
import "./tests/TranslateDateTimeFormat.mjs";
import "./tests/Typex.mjs";
diff --git a/tests/operations/tests/TextIntegerConverter.mjs b/tests/operations/tests/TextIntegerConverter.mjs
new file mode 100644
index 00000000..fed8f6c3
--- /dev/null
+++ b/tests/operations/tests/TextIntegerConverter.mjs
@@ -0,0 +1,199 @@
+/**
+ * Text-Integer Conversion tests.
+ *
+ * @author p-leriche [philip.leriche@cantab.net]
+ *
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Text-Integer Conversion quoted string to decimal",
+ input: "\"ABC\"",
+ expectedOutput: "4276803",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion quoted string to hexadecimal",
+ input: "\"ABC\"",
+ expectedOutput: "0x414243",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Hexadecimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion single quoted string to decimal",
+ input: "'Hello'",
+ expectedOutput: "310939249775",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion decimal to string",
+ input: "4276803",
+ expectedOutput: "ABC",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["String"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion hexadecimal to string",
+ input: "0x48656C6C6F",
+ expectedOutput: "Hello",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["String"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion round-trip string.decimal.string",
+ input: "\"Test\"",
+ expectedOutput: "Test",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ {
+ op: "Text-Integer Conversion",
+ args: ["String"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion round-trip string.hex.string",
+ input: "\"CyberChef\"",
+ expectedOutput: "CyberChef",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Hexadecimal"],
+ },
+ {
+ op: "Text-Integer Conversion",
+ args: ["String"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion implicit round trip string-string Latin-1",
+ input: "U+00FF",
+ expectedOutput: "U+00FF", // U+00FF (Latin small letter y with diaeresis)
+ recipeConfig: [
+ {
+ op: "Unescape Unicode Characters",
+ args: ["U+"],
+ },
+ {
+ op: "Text-Integer Conversion",
+ args: ["String"],
+ },
+ {
+ op: "Escape Unicode Characters",
+ args: ["U+", false, 4, true],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion unquoted text to decimal",
+ input: "Hi",
+ expectedOutput: "18537",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion single character",
+ input: "\"A\"",
+ expectedOutput: "65",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion hex to decimal conversion",
+ input: "0xFF",
+ expectedOutput: "255",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion decimal to hex conversion",
+ input: "255",
+ expectedOutput: "0xff",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Hexadecimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion large number to string",
+ input: "113091951015816448506195587157728348242683688608116",
+ expectedOutput: "Mary had a little cat",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["String"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion whitespace handling (quoted)",
+ input: "\" test \"",
+ expectedOutput: "2314978187545944096",
+ recipeConfig: [
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+ {
+ name: "Text-Integer Conversion non-Latin1 character in input",
+ input: "61 ce 93 61",
+ expectedOutput:
+`Character at position 1 exceeds Latin-1 range (0-255).
+Only ASCII and Latin-1 characters are supported.`,
+ recipeConfig: [
+ {
+ "op": "From Hex",
+ "args": ["Auto"]
+ },
+ {
+ op: "Text-Integer Conversion",
+ args: ["Decimal"],
+ },
+ ],
+ },
+]);