diff --git a/package-lock.json b/package-lock.json
index b374df4b..af3f31e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5406,6 +5406,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5476,6 +5485,24 @@
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
+ "node_modules/bencode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/bencode/-/bencode-4.0.0.tgz",
+ "integrity": "sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uint8-util": "^2.2.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/bencodec": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/bencodec/-/bencodec-3.0.1.tgz",
+ "integrity": "sha512-5Ntc3E7R1vSnBcOddG65L9kObEmZGI0Vool6z/7apwO5Hc9OlziwK0LyxvaTK5Il+nSWNxlVSuh2zJM+TN9O3g==",
+ "license": "MIT"
+ },
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -17895,6 +17922,15 @@
"integrity": "sha512-w+VZSp8hSZ/xWZfZNMppWNF6iqY+dcMYtG5CpwRDgxi94HIE6ematSdkzHGzVC4SDEaTsG65zrajN+oKoWG6ew==",
"license": "MIT"
},
+ "node_modules/uint8-util": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/uint8-util/-/uint8-util-2.2.5.tgz",
+ "integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 434c8bb6..a4dd111a 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -162,7 +162,9 @@
"Typex",
"Lorenz",
"Colossus",
- "SIGABA"
+ "SIGABA",
+ "Bencode Encode",
+ "Bencode Decode"
]
},
{
diff --git a/src/core/operations/BencodeDecode.mjs b/src/core/operations/BencodeDecode.mjs
new file mode 100644
index 00000000..7b76820f
--- /dev/null
+++ b/src/core/operations/BencodeDecode.mjs
@@ -0,0 +1,204 @@
+/**
+ * @author jg42526
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * URL Decode operation
+ */
+class BencodeDecode extends Operation {
+
+ /**
+ * URL Decode constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Bencode Decode";
+ this.module = "Encodings";
+ this.description = "Decodes a Bencoded string.
e.g. 7:bencode
becomes bencode
";
+ this.infoURL = "https://en.wikipedia.org/wiki/Bencode";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ if (input) {
+ const decoder = new BencodeDecoder(input, {stringify: true}).decode();
+ return toStringRepresentation(decoder);
+ }
+ return "";
+ }
+
+}
+
+export default BencodeDecode;
+
+/**
+ * Returns string representation of object
+ */
+function toStringRepresentation(value) {
+ if (typeof value === "string") {
+ return value;
+ }
+
+ if (typeof value === "number" || typeof value === "boolean") {
+ return String(value);
+ }
+
+ if (Array.isArray(value) || (value !== null && typeof value === "object")) {
+ // For arrays and objects, output JSON string
+ return JSON.stringify(value);
+ }
+
+ // For other types (undefined, null), handle as you see fit, e.g.:
+ return String(value);
+}
+
+const FLAG = {
+ INTEGER: 0x69, // 'i'
+ STR_DELIMITER: 0x3a, // ':'
+ LIST: 0x6c, // 'l'
+ DICTIONARY: 0x64, // 'd'
+ END: 0x65, // 'e'
+ MINUS: 0x2d, // '-'
+ PLUS: 0x2b, // '+'
+ DOT: 0x2e, // '.'
+};
+
+/**
+ * Class for decoding data from the Bencode format.
+ * Credit to @isolomak:
+ * https://github.com/isolomak/bencodec
+ */
+class BencodeDecoder {
+ /**
+ * Creates an instance of BencodeDecoder.
+ * @param {Buffer|string} data - The bencoded data to decode.
+ * @param {Object} [options={}] - Optional decoding options.
+ * @param {boolean} [options.stringify=false] - Whether to return strings instead of Buffers.
+ */
+ constructor(data, options = {}) {
+ if (!data) throw new Error("Nothing to decode");
+ this._index = 0;
+ this._options = options;
+ this._buffer = typeof data === "string" ? Buffer.from(data) : data;
+ }
+
+ /**
+ * Checks if a character code represents a digit (0–9).
+ * @param {number} char - The character code to check.
+ * @returns {boolean} - True if the character is a digit.
+ */
+ static _isInteger(char) {
+ return char >= 0x30 && char <= 0x39;
+ }
+
+ /**
+ * Returns the current character code in the buffer.
+ * @returns {number} - The current character code.
+ */
+ _currentChar() {
+ return this._buffer[this._index];
+ }
+
+ /**
+ * Returns the next character code in the buffer and advances the index.
+ * @returns {number} - The next character code.
+ */
+ _next() {
+ return this._buffer[this._index++];
+ }
+
+ /**
+ * Decodes the bencoded data.
+ * @returns {*} - The decoded value (string, number, list, or dictionary).
+ */
+ decode() {
+ const char = this._currentChar();
+ if (BencodeDecoder._isInteger(char)) return this._decodeString();
+ if (char === FLAG.INTEGER) return this._decodeInteger();
+ if (char === FLAG.LIST) return this._decodeList();
+ if (char === FLAG.DICTIONARY) return this._decodeDictionary();
+ throw new Error("Invalid bencode data");
+ }
+
+ /**
+ * Decodes a bencoded string.
+ * @returns {Buffer|string} - The decoded string or Buffer.
+ */
+ _decodeString() {
+ const length = this._decodeInteger();
+ const acc = [];
+ for (let i = 0; i < length; i++) acc.push(this._next());
+ const result = Buffer.from(acc);
+ return this._options.stringify ? result.toString("utf8") : result;
+ }
+
+ /**
+ * Decodes a bencoded integer.
+ * @returns {number} - The decoded integer.
+ */
+ _decodeInteger() {
+ let sign = 1;
+ let integer = 0;
+
+ if (this._currentChar() === FLAG.INTEGER) this._index++;
+ if (this._currentChar() === FLAG.PLUS) this._index++;
+ if (this._currentChar() === FLAG.MINUS) {
+ this._index++;
+ sign = -1;
+ }
+
+ while (BencodeDecoder._isInteger(this._currentChar()) || this._currentChar() === FLAG.DOT) {
+ if (this._currentChar() === FLAG.DOT) {
+ this._index++; // Skip dot (float not supported)
+ } else {
+ integer = integer * 10 + (this._next() - 0x30);
+ }
+ }
+
+ if (this._currentChar() === FLAG.END) this._index++;
+ if (this._currentChar() === FLAG.STR_DELIMITER) this._index++;
+
+ return integer * sign;
+ }
+
+ /**
+ * Decodes a bencoded list.
+ * @returns {Array} - The decoded list.
+ */
+ _decodeList() {
+ const acc = [];
+ this._next(); // Skip 'l'
+ while (this._currentChar() !== FLAG.END) {
+ acc.push(this.decode());
+ }
+ this._next(); // Skip 'e'
+ return acc;
+ }
+
+ /**
+ * Decodes a bencoded dictionary.
+ * @returns {Object} - The decoded dictionary.
+ */
+ _decodeDictionary() {
+ const acc = {};
+ this._next(); // Skip 'd'
+ while (this._currentChar() !== FLAG.END) {
+ const key = this._decodeString();
+ acc[key.toString()] = this.decode();
+ }
+ this._next(); // Skip 'e'
+ return acc;
+ }
+}
diff --git a/src/core/operations/BencodeEncode.mjs b/src/core/operations/BencodeEncode.mjs
new file mode 100644
index 00000000..7e952ef2
--- /dev/null
+++ b/src/core/operations/BencodeEncode.mjs
@@ -0,0 +1,190 @@
+/**
+ * @author jg42526
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * URL Decode operation
+ */
+class BencodeEncode extends Operation {
+
+ /**
+ * URLDecode constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Bencode Encode";
+ this.module = "Encodings";
+ this.description = "Bencodes a string.
e.g. bencode
becomes 7:bencode
";
+ this.infoURL = "https://en.wikipedia.org/wiki/Bencode";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const encoder = new BencodeEncoder({ stringify: true });
+ return encoder.encode(parseValue(input));
+ }
+
+}
+
+export default BencodeEncode;
+
+/**
+ * Parses string, returns appropriate data structure
+ */
+function parseValue(str) {
+ const trimmed = str.trim();
+ try {
+ // Attempt to parse with JSON.parse
+ return JSON.parse(trimmed);
+ } catch (e) {
+ // If JSON.parse fails, treat input as a plain string (assuming it's unquoted)
+ return trimmed;
+ }
+}
+
+const FLAG = {
+ INTEGER: 0x69, // 'i'
+ STR_DELIMITER: 0x3a, // ':'
+ LIST: 0x6c, // 'l'
+ DICTIONARY: 0x64, // 'd'
+ END: 0x65, // 'e'
+};
+
+/**
+ * BencodeEncoder class for encoding data into bencode format.
+ * Credit to @isolomak:
+ * https://github.com/isolomak/bencodec
+ */
+class BencodeEncoder {
+ /**
+ *
+ */
+ constructor(options = {}) {
+ this._integerIdentifier = Buffer.from([FLAG.INTEGER]);
+ this._stringDelimiterIdentifier = Buffer.from([FLAG.STR_DELIMITER]);
+ this._listIdentifier = Buffer.from([FLAG.LIST]);
+ this._dictionaryIdentifier = Buffer.from([FLAG.DICTIONARY]);
+ this._endIdentifier = Buffer.from([FLAG.END]);
+ this._buffer = [];
+ this._options = options;
+ }
+
+ /**
+ * Encodes the given data into bencode format.
+ * @param {*} data - The data to encode.
+ * @returns {Buffer|string} - The encoded data as a Buffer or string.
+ */
+ encode(data) {
+ this._encodeType(data);
+ const result = Buffer.concat(this._buffer);
+ return this._options.stringify ? result.toString("utf8") : result;
+ }
+
+ /**
+ * Determines the type of data and encodes it accordingly.
+ * @param {*} data - The data to encode.
+ */
+ _encodeType(data) {
+ if (Buffer.isBuffer(data)) {
+ return this._encodeBuffer(data);
+ }
+ if (Array.isArray(data)) {
+ return this._encodeList(data);
+ }
+ if (ArrayBuffer.isView(data)) {
+ return this._encodeBuffer(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
+ }
+ if (data instanceof ArrayBuffer) {
+ return this._encodeBuffer(Buffer.from(data));
+ }
+ if (typeof data === "boolean") {
+ return this._encodeInteger(data ? 1 : 0);
+ }
+ if (typeof data === "number") {
+ return this._encodeInteger(data);
+ }
+ if (typeof data === "string") {
+ return this._encodeString(data);
+ }
+ if (typeof data === "object") {
+ return this._encodeDictionary(data);
+ }
+ throw new Error(`${typeof data} is unsupported type.`);
+ }
+
+ /**
+ * Buffer into bencode format.
+ * @param {Buffer} data - The buffer to encode.
+ */
+ _encodeBuffer(data) {
+ this._buffer.push(
+ Buffer.from(String(data.length)),
+ this._stringDelimiterIdentifier,
+ data
+ );
+ }
+
+ /**
+ * Encodes a string into bencode format.
+ * @param {string} data - The string to encode.
+ */
+ _encodeString(data) {
+ this._buffer.push(
+ Buffer.from(String(Buffer.byteLength(data))),
+ this._stringDelimiterIdentifier,
+ Buffer.from(data)
+ );
+ }
+
+ /**
+ * Encodes an integer into bencode format.
+ * @param {number} data - The integer to encode.
+ */
+ _encodeInteger(data) {
+ this._buffer.push(
+ this._integerIdentifier,
+ Buffer.from(String(Math.round(data))),
+ this._endIdentifier
+ );
+ }
+
+ /**
+ * Encodes a list (array) into bencode format.
+ * @param {Array} data - The list to encode.
+ */
+ _encodeList(data) {
+ this._buffer.push(this._listIdentifier);
+ for (const item of data) {
+ if (item === null || item === undefined) continue;
+ this._encodeType(item);
+ }
+ this._buffer.push(this._endIdentifier);
+ }
+
+ /**
+ * Encodes a dictionary (object) into bencode format.
+ * @param {Object} data - The dictionary to encode.
+ */
+ _encodeDictionary(data) {
+ this._buffer.push(this._dictionaryIdentifier);
+ const keys = Object.keys(data).sort();
+ for (const key of keys) {
+ if (data[key] === null || data[key] === undefined) continue;
+ this._encodeString(key);
+ this._encodeType(data[key]);
+ }
+ this._buffer.push(this._endIdentifier);
+ }
+}
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index f147e9e7..3577f0f2 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -26,6 +26,8 @@ import "./tests/Base64.mjs";
import "./tests/Base85.mjs";
import "./tests/Base92.mjs";
import "./tests/BCD.mjs";
+import "./tests/BencodeEncode.mjs";
+import "./tests/BencodeDecode.mjs";
import "./tests/BitwiseOp.mjs";
import "./tests/BLAKE2b.mjs";
import "./tests/BLAKE2s.mjs";
diff --git a/tests/operations/tests/BencodeDecode.mjs b/tests/operations/tests/BencodeDecode.mjs
new file mode 100644
index 00000000..c0297d8b
--- /dev/null
+++ b/tests/operations/tests/BencodeDecode.mjs
@@ -0,0 +1,66 @@
+/**
+ * Bencode Encode tests.
+ *
+ * @author jg42526
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Bencode Decode: nothing",
+ input: "",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "Bencode Decode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Decode: integer",
+ input: "i42e",
+ expectedOutput: "42",
+ recipeConfig: [
+ {
+ "op": "Bencode Decode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Decode: byte string",
+ input: "7:bencode",
+ expectedOutput: "bencode",
+ recipeConfig: [
+ {
+ "op": "Bencode Decode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Decode: list",
+ input: "l7:bencodei-20ee",
+ expectedOutput: `["bencode",-20]`,
+ recipeConfig: [
+ {
+ "op": "Bencode Decode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Decode: dictionary",
+ input: "d7:meaningi42e4:wiki7:bencodee",
+ expectedOutput: `{"meaning":42,"wiki":"bencode"}`,
+ recipeConfig: [
+ {
+ "op": "Bencode Decode",
+ "args": []
+ }
+ ]
+ },
+]);
diff --git a/tests/operations/tests/BencodeEncode.mjs b/tests/operations/tests/BencodeEncode.mjs
new file mode 100644
index 00000000..51b96826
--- /dev/null
+++ b/tests/operations/tests/BencodeEncode.mjs
@@ -0,0 +1,72 @@
+/**
+ * Bencode Encode tests.
+ *
+ * @author jg42526
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Bencode Encode: nothing",
+ input: "",
+ expectedOutput: "0:",
+ recipeConfig: [
+ {
+ "op": "Bencode Encode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Encode: integer",
+ input: "42",
+ expectedOutput: "i42e",
+ recipeConfig: [
+ {
+ "op": "Bencode Encode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Encode: byte string",
+ input: "bencode",
+ expectedOutput: "7:bencode",
+ recipeConfig: [
+ {
+ "op": "Bencode Encode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Encode: list",
+ input: `[
+ "bencode",
+ -20
+ ]`,
+ expectedOutput: "l7:bencodei-20ee",
+ recipeConfig: [
+ {
+ "op": "Bencode Encode",
+ "args": []
+ }
+ ]
+ },
+ {
+ name: "Bencode Encode: dictionary",
+ input: `{
+ "meaning": 42,
+ "wiki": "bencode"
+ }`,
+ expectedOutput: "d7:meaningi42e4:wiki7:bencodee",
+ recipeConfig: [
+ {
+ "op": "Bencode Encode",
+ "args": []
+ }
+ ]
+ },
+]);