From 3b3eb2f78e7e0f4855ec2334356a20559a3d333f Mon Sep 17 00:00:00 2001 From: NOVA-Openclaw <258138572+NOVA-Openclaw@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:33:34 +0000 Subject: [PATCH] Add To Base26 and From Base26 operations Implements Base26 encoding/decoding using uppercase A-Z as digits (A=0, B=1, ..., Z=25). Input bytes are interpreted as a big-endian unsigned integer and converted to/from radix 26. Includes tests for the reference vector (EAT -> JYGBS), round-trips, empty input, zero bytes, and binary data. Closes gchq/CyberChef#1511 --- src/core/config/Categories.json | 2 + src/core/operations/FromBase26.mjs | 61 ++++++++++++ src/core/operations/ToBase26.mjs | 58 +++++++++++ tests/operations/tests/Base26.mjs | 149 +++++++++++++++++++++++++++++ 4 files changed, 270 insertions(+) create mode 100644 src/core/operations/FromBase26.mjs create mode 100644 src/core/operations/ToBase26.mjs create mode 100644 tests/operations/tests/Base26.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 6ad3adb776..15052e62b5 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -30,6 +30,8 @@ "From Bech32", "To Base62", "From Base62", + "To Base26", + "From Base26", "To Base64", "From Base64", "Show Base64 offsets", diff --git a/src/core/operations/FromBase26.mjs b/src/core/operations/FromBase26.mjs new file mode 100644 index 0000000000..3296099ce4 --- /dev/null +++ b/src/core/operations/FromBase26.mjs @@ -0,0 +1,61 @@ +/** + * @author NOVA-Openclaw + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * From Base26 operation + */ +class FromBase26 extends Operation { + + /** + * FromBase26 constructor + */ + constructor() { + super(); + + this.name = "From Base26"; + this.module = "Default"; + this.description = "Base26 is a notation for encoding arbitrary byte data using the uppercase letters A-Z. The input string is interpreted as a radix-26 number using A=0, B=1, ..., Z=25, and converted back to its big-endian byte representation."; + this.infoURL = "https://wikipedia.org/wiki/List_of_numeral_systems"; + this.inputType = "string"; + this.outputType = "byteArray"; + this.args = []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + if (input.length < 1) return []; + + const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const re = new RegExp("[^A-Za-z]", "g"); + input = input.replace(re, "").toUpperCase(); + + if (input.length < 1) return []; + + let number = 0n; + for (const c of input) { + number = number * 26n + BigInt(ALPHABET.indexOf(c)); + } + + if (number === 0n) return [0]; + + const bytes = []; + while (number > 0n) { + bytes.unshift(Number(number & 0xFFn)); + number >>= 8n; + } + + return bytes; + } + +} + +export default FromBase26; diff --git a/src/core/operations/ToBase26.mjs b/src/core/operations/ToBase26.mjs new file mode 100644 index 0000000000..043511e346 --- /dev/null +++ b/src/core/operations/ToBase26.mjs @@ -0,0 +1,58 @@ +/** + * @author NOVA-Openclaw + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * To Base26 operation + */ +class ToBase26 extends Operation { + + /** + * ToBase26 constructor + */ + constructor() { + super(); + + this.name = "To Base26"; + this.module = "Default"; + this.description = "Base26 is a notation for encoding arbitrary byte data using the uppercase letters A-Z. The input bytes are interpreted as a big-endian unsigned integer, which is then represented in radix 26 using A=0, B=1, ..., Z=25."; + this.infoURL = "https://wikipedia.org/wiki/List_of_numeral_systems"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + input = new Uint8Array(input); + if (input.length < 1) return ""; + + const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + let number = 0n; + for (const byte of input) { + number = (number << 8n) | BigInt(byte); + } + + if (number === 0n) return ALPHABET[0]; + + let output = ""; + while (number > 0n) { + output = ALPHABET[Number(number % 26n)] + output; + number /= 26n; + } + + return output; + } + +} + +export default ToBase26; diff --git a/tests/operations/tests/Base26.mjs b/tests/operations/tests/Base26.mjs new file mode 100644 index 0000000000..311e096370 --- /dev/null +++ b/tests/operations/tests/Base26.mjs @@ -0,0 +1,149 @@ +/** + * Base26 tests. + * + * @author NOVA-Openclaw + * + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "To Base26: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + ], + }, + { + name: "To Base26: EAT -> JYGBS", + input: "EAT", + expectedOutput: "JYGBS", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + ], + }, + { + name: "To Base26: Hello, World!", + input: "Hello, World!", + expectedOutput: "LBVLPVLUNRXHXRHSZIHQLL", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + ], + }, + { + name: "To Base26: UTF-8", + input: "ნუ პანიკას", + expectedOutput: "HLPHPKXWXPTREJHEBQENHKRQAKLFKOWIWEXTDNAOPRQFRQLL", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + ], + }, + { + name: "To Base26: zero byte", + input: "\x00", + expectedOutput: "A", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + ], + }, + { + name: "To Base26: binary", + input: "\x01\x02\x03", + expectedOutput: "DTSL", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + ], + }, + { + name: "From Base26: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "From Base26", + args: [], + }, + ], + }, + { + name: "From Base26: JYGBS -> EAT", + input: "JYGBS", + expectedOutput: "EAT", + recipeConfig: [ + { + op: "From Base26", + args: [], + }, + ], + }, + { + name: "From Base26: lowercase and ignored characters", + input: "j y-g!b@s", + expectedOutput: "EAT", + recipeConfig: [ + { + op: "From Base26", + args: [], + }, + ], + }, + { + name: "From Base26: A -> zero byte", + input: "A", + expectedOutput: "\x00", + recipeConfig: [ + { + op: "From Base26", + args: [], + }, + ], + }, + { + name: "From Base26: DTSL -> binary", + input: "DTSL", + expectedOutput: "\x01\x02\x03", + recipeConfig: [ + { + op: "From Base26", + args: [], + }, + ], + }, + { + name: "To Base26 round-trip", + input: "The quick brown fox jumps over the lazy dog.", + expectedOutput: "The quick brown fox jumps over the lazy dog.", + recipeConfig: [ + { + op: "To Base26", + args: [], + }, + { + op: "From Base26", + args: [], + }, + ], + } +]);