From d8d594a493e299518db6dffc90d65cb0634f86b4 Mon Sep 17 00:00:00 2001 From: jg42526 <210032080+jg42526@users.noreply.github.com> Date: Mon, 12 May 2025 15:24:44 +0000 Subject: [PATCH 1/3] Add Bencode function operation --- package-lock.json | 37 ++++++++++++ package.json | 1 + src/core/config/Categories.json | 4 +- src/core/operations/BencodeDecode.mjs | 63 +++++++++++++++++++++ src/core/operations/BencodeEncode.mjs | 55 ++++++++++++++++++ tests/operations/index.mjs | 2 + tests/operations/tests/BencodeDecode.mjs | 66 ++++++++++++++++++++++ tests/operations/tests/BencodeEncode.mjs | 72 ++++++++++++++++++++++++ 8 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/BencodeDecode.mjs create mode 100644 src/core/operations/BencodeEncode.mjs create mode 100644 tests/operations/tests/BencodeDecode.mjs create mode 100644 tests/operations/tests/BencodeEncode.mjs diff --git a/package-lock.json b/package-lock.json index 8dd38b7a..974f9810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "arrive": "^2.4.1", "avsc": "^5.7.7", "bcryptjs": "^2.4.3", + "bencodec": "^3.0.1", "bignumber.js": "^9.1.2", "blakejs": "^1.2.1", "bootstrap": "4.6.2", @@ -5403,6 +5404,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", @@ -5473,6 +5483,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", @@ -17850,6 +17878,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/package.json b/package.json index feb18bc8..4b116619 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "arrive": "^2.4.1", "avsc": "^5.7.7", "bcryptjs": "^2.4.3", + "bencodec": "^3.0.1", "bignumber.js": "^9.1.2", "blakejs": "^1.2.1", "bootstrap": "4.6.2", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 7f9591e0..a44b65a7 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..a4a680bf --- /dev/null +++ b/src/core/operations/BencodeDecode.mjs @@ -0,0 +1,63 @@ +/** + * @author jg42526 + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import bencodec from "bencodec"; + +/** + * URL Decode operation + */ +class BencodeDecode extends Operation { + + /** + * URLDecode 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) return toStringRepresentation(bencodec.decode(input, { stringify: true })); + 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); +} diff --git a/src/core/operations/BencodeEncode.mjs b/src/core/operations/BencodeEncode.mjs new file mode 100644 index 00000000..91d519ed --- /dev/null +++ b/src/core/operations/BencodeEncode.mjs @@ -0,0 +1,55 @@ +/** + * @author jg42526 + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import bencodec from "bencodec"; + +/** + * 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) { + return bencodec.encode(parseValue(input), { stringify: true }); + } + +} + +export default BencodeEncode; + +/** + * Parses string, returns appropraite 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; + } +} diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index ab1ceb8f..8d195553 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": [] + } + ] + }, +]); From 41ce2eceffcabcd5db2fe61d2246997ae0a50c86 Mon Sep 17 00:00:00 2001 From: jg42526 <210032080+jg42526@users.noreply.github.com> Date: Fri, 16 May 2025 09:35:46 +0000 Subject: [PATCH 2/3] Removed dependency on external package by bringing it in-line --- package-lock.json | 1 - package.json | 1 - src/core/operations/BencodeDecode.mjs | 145 +++++++++++++++++++++++++- src/core/operations/BencodeEncode.mjs | 139 +++++++++++++++++++++++- 4 files changed, 278 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 974f9810..3a3e200a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "arrive": "^2.4.1", "avsc": "^5.7.7", "bcryptjs": "^2.4.3", - "bencodec": "^3.0.1", "bignumber.js": "^9.1.2", "blakejs": "^1.2.1", "bootstrap": "4.6.2", diff --git a/package.json b/package.json index 4b116619..feb18bc8 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,6 @@ "arrive": "^2.4.1", "avsc": "^5.7.7", "bcryptjs": "^2.4.3", - "bencodec": "^3.0.1", "bignumber.js": "^9.1.2", "blakejs": "^1.2.1", "bootstrap": "4.6.2", diff --git a/src/core/operations/BencodeDecode.mjs b/src/core/operations/BencodeDecode.mjs index a4a680bf..0f7c659b 100644 --- a/src/core/operations/BencodeDecode.mjs +++ b/src/core/operations/BencodeDecode.mjs @@ -5,7 +5,6 @@ */ import Operation from "../Operation.mjs"; -import bencodec from "bencodec"; /** * URL Decode operation @@ -13,7 +12,7 @@ import bencodec from "bencodec"; class BencodeDecode extends Operation { /** - * URLDecode constructor + * URL Decode constructor */ constructor() { super(); @@ -33,7 +32,10 @@ class BencodeDecode extends Operation { * @returns {string} */ run(input, args) { - if (input) return toStringRepresentation(bencodec.decode(input, { stringify: true })); + if (input) { + const decoder = new BencodeDecoder(input, {stringify: true}).decode(); + return toStringRepresentation(decoder); + } return ""; } @@ -61,3 +63,140 @@ function toStringRepresentation(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. + */ +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 index 91d519ed..2cedac8b 100644 --- a/src/core/operations/BencodeEncode.mjs +++ b/src/core/operations/BencodeEncode.mjs @@ -5,7 +5,6 @@ */ import Operation from "../Operation.mjs"; -import bencodec from "bencodec"; /** * URL Decode operation @@ -33,7 +32,8 @@ class BencodeEncode extends Operation { * @returns {string} */ run(input, args) { - return bencodec.encode(parseValue(input), { stringify: true }); + const encoder = new BencodeEncoder({ stringify: true }); + return encoder.encode(parseValue(input)); } } @@ -41,7 +41,7 @@ class BencodeEncode extends Operation { export default BencodeEncode; /** - * Parses string, returns appropraite data structure + * Parses string, returns appropriate data structure */ function parseValue(str) { const trimmed = str.trim(); @@ -53,3 +53,136 @@ function parseValue(str) { 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. + */ +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); + } +} From 7b0a92ad1e87a2aed71bf8990389114a046afed6 Mon Sep 17 00:00:00 2001 From: jg42526 <210032080+jg42526@users.noreply.github.com> Date: Fri, 16 May 2025 09:37:08 +0000 Subject: [PATCH 3/3] Giving credit to author of bencoding operation --- src/core/operations/BencodeDecode.mjs | 2 ++ src/core/operations/BencodeEncode.mjs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/core/operations/BencodeDecode.mjs b/src/core/operations/BencodeDecode.mjs index 0f7c659b..7b76820f 100644 --- a/src/core/operations/BencodeDecode.mjs +++ b/src/core/operations/BencodeDecode.mjs @@ -77,6 +77,8 @@ const FLAG = { /** * Class for decoding data from the Bencode format. + * Credit to @isolomak: + * https://github.com/isolomak/bencodec */ class BencodeDecoder { /** diff --git a/src/core/operations/BencodeEncode.mjs b/src/core/operations/BencodeEncode.mjs index 2cedac8b..7e952ef2 100644 --- a/src/core/operations/BencodeEncode.mjs +++ b/src/core/operations/BencodeEncode.mjs @@ -64,6 +64,8 @@ const FLAG = { /** * BencodeEncoder class for encoding data into bencode format. + * Credit to @isolomak: + * https://github.com/isolomak/bencodec */ class BencodeEncoder { /**