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); + } +}