bencodec
Version:
Universal library for decoding and encoding bencode data
813 lines (806 loc) • 28 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
BencodeDecodeError: () => BencodeDecodeError,
BencodeEncodeError: () => BencodeEncodeError,
BencodeError: () => BencodeError,
BencodeErrorCode: () => BencodeErrorCode,
bencodec: () => bencodec,
decode: () => decode,
default: () => index_default,
encode: () => encode,
encodeToBytes: () => encodeToBytes,
encodeToString: () => encodeToString
});
module.exports = __toCommonJS(index_exports);
// src/errors.ts
var BencodeErrorCode = /* @__PURE__ */ ((BencodeErrorCode2) => {
BencodeErrorCode2["EMPTY_INPUT"] = "EMPTY_INPUT";
BencodeErrorCode2["UNEXPECTED_END"] = "UNEXPECTED_END";
BencodeErrorCode2["INVALID_FORMAT"] = "INVALID_FORMAT";
BencodeErrorCode2["LEADING_ZEROS"] = "LEADING_ZEROS";
BencodeErrorCode2["NEGATIVE_ZERO"] = "NEGATIVE_ZERO";
BencodeErrorCode2["UNSORTED_KEYS"] = "UNSORTED_KEYS";
BencodeErrorCode2["TRAILING_DATA"] = "TRAILING_DATA";
BencodeErrorCode2["MAX_DEPTH_EXCEEDED"] = "MAX_DEPTH_EXCEEDED";
BencodeErrorCode2["MAX_SIZE_EXCEEDED"] = "MAX_SIZE_EXCEEDED";
BencodeErrorCode2["UNSUPPORTED_TYPE"] = "UNSUPPORTED_TYPE";
BencodeErrorCode2["CIRCULAR_REFERENCE"] = "CIRCULAR_REFERENCE";
return BencodeErrorCode2;
})(BencodeErrorCode || {});
var BencodeError = class extends Error {
/**
* @param code - The error code identifying the error type
* @param message - Human-readable error message
*/
constructor(code, message) {
super(message);
this.code = code;
this.name = this.constructor.name;
Error.captureStackTrace?.(this, this.constructor);
}
};
var BencodeDecodeError = class extends BencodeError {
/**
* @param code - The error code identifying the error type
* @param message - Human-readable error message
* @param position - The byte position where the error occurred (optional)
*/
constructor(code, message, position) {
super(code, message);
this.position = position;
}
};
var BencodeEncodeError = class extends BencodeError {
/**
* @param code - The error code identifying the error type
* @param message - Human-readable error message
* @param path - The path to the problematic value (optional)
*/
constructor(code, message, path) {
super(code, message);
this.path = path;
}
};
// src/bytes.ts
var _Bytes = class _Bytes {
/**
* Convert a string to Uint8Array using the specified encoding.
*
* @param str - The string to convert
* @param encoding - Character encoding (default: 'utf8')
* @returns Uint8Array containing the encoded bytes
*
* @example
* ```typescript
* Bytes.fromString('hello'); // UTF-8 encoded
* Bytes.fromString('hello', 'latin1'); // Latin-1 encoded
* ```
*/
static fromString(str, encoding = "utf8") {
if (encoding === "utf8" || encoding === "utf-8") {
return _Bytes._encoder.encode(str);
}
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i) & 255;
}
return bytes;
}
/**
* Convert Uint8Array to string using the specified encoding.
*
* @param bytes - The byte array to convert
* @param encoding - Character encoding (default: 'utf8')
* @returns The decoded string
*
* @example
* ```typescript
* Bytes.toString(bytes); // UTF-8 decoded
* Bytes.toString(bytes, 'latin1'); // Latin-1 decoded
* ```
*/
static toString(bytes, encoding = "utf8") {
if (encoding === "utf8" || encoding === "utf-8") {
return _Bytes._decoder.decode(bytes);
}
let result = "";
for (let i = 0; i < bytes.length; i++) {
result += String.fromCharCode(bytes[i]);
}
return result;
}
/**
* Concatenate multiple Uint8Arrays into a single Uint8Array.
*
* @param arrays - Array of Uint8Arrays to concatenate
* @returns A new Uint8Array containing all bytes
*
* @example
* ```typescript
* const combined = Bytes.concat([bytes1, bytes2, bytes3]);
* ```
*/
static concat(arrays) {
let totalLength = 0;
for (const arr of arrays) {
totalLength += arr.length;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
/**
* Compare two Uint8Arrays lexicographically.
*
* @param a - First byte array
* @param b - Second byte array
* @returns -1 if a < b, 0 if a === b, 1 if a > b
*
* @example
* ```typescript
* Bytes.compare(a, b); // -1, 0, or 1
* ```
*/
static compare(a, b) {
const minLength = Math.min(a.length, b.length);
for (let i = 0; i < minLength; i++) {
if (a[i] < b[i]) {
return -1;
}
if (a[i] > b[i]) {
return 1;
}
}
if (a.length < b.length) {
return -1;
}
if (a.length > b.length) {
return 1;
}
return 0;
}
/**
* Calculate the byte length of a string when encoded as UTF-8.
*
* @param str - The string to measure
* @returns The byte length in UTF-8 encoding
*
* @example
* ```typescript
* Bytes.byteLength('hello'); // 5
* Bytes.byteLength(''); // 6 (2 bytes per character)
* ```
*/
static byteLength(str) {
return _Bytes._encoder.encode(str).length;
}
/**
* Type guard to check if a value is a Uint8Array or Buffer.
*
* @param value - The value to check
* @returns true if the value is a Uint8Array (or Buffer in Node.js)
*
* @example
* ```typescript
* if (Bytes.isBytes(data)) {
* // data is Uint8Array
* }
* ```
*/
static isBytes(value) {
return value instanceof Uint8Array;
}
};
_Bytes._encoder = new TextEncoder();
_Bytes._decoder = new TextDecoder();
var Bytes = _Bytes;
// src/BencodeDecoder.ts
var BencodeDecoder = class _BencodeDecoder {
/**
* Checks if a byte value represents an ASCII digit (0-9).
*
* @param char - The byte value to check
* @returns `true` if the byte is an ASCII digit (0x30-0x39)
*/
static _isInteger(char) {
return char >= 48 && char <= 57;
}
/**
* Creates a new BencodeDecoder instance.
*
* @param data - The bencode data to decode. Strings are converted to Uint8Array internally.
* @param options - Configuration options for decoding behavior.
*
* @throws {BencodeDecodeError} With code `EMPTY_INPUT` if data is empty or falsy.
*
* @example
* ```typescript
* // From string
* const decoder = new BencodeDecoder('i42e');
*
* // From Uint8Array
* const decoder = new BencodeDecoder(new Uint8Array([0x69, 0x34, 0x32, 0x65]));
*
* // With options
* const decoder = new BencodeDecoder(data, { stringify: true, strict: true });
* ```
*/
constructor(data, options) {
if (!data) {
throw new BencodeDecodeError("EMPTY_INPUT" /* EMPTY_INPUT */, "Nothing to decode");
}
this._index = 0;
this._currentDepth = 0;
this._options = options || {};
this._buffer = typeof data === "string" ? Bytes.fromString(data) : data;
}
/**
* Checks if there is remaining data in the buffer after decoding.
*
* Useful for detecting trailing garbage data, which may indicate malformed input
* or concatenated bencode values.
*
* @returns `true` if the current position is before the end of the buffer.
*
* @example
* ```typescript
* const decoder = new BencodeDecoder('i42eextra');
* decoder.decode(); // 42
* decoder.hasRemainingData(); // true (5 bytes remaining: "extra")
* ```
*/
hasRemainingData() {
return this._index < this._buffer.length;
}
/**
* Gets the current byte position in the buffer.
*
* Useful for error reporting, debugging, or implementing custom parsing logic.
*
* @returns The zero-based byte offset of the current position.
*
* @example
* ```typescript
* const decoder = new BencodeDecoder('i42e5:hello');
* decoder.decode(); // 42
* decoder.getCurrentPosition(); // 4 (after 'i42e')
* decoder.decode(); // <Buffer 68 65 6c 6c 6f>
* decoder.getCurrentPosition(); // 11 (end of buffer)
* ```
*/
getCurrentPosition() {
return this._index;
}
/**
* Decodes the next bencode value from the buffer.
*
* Advances the internal position pointer past the decoded value.
* Can be called multiple times to decode concatenated bencode values.
*
* @returns The decoded JavaScript value:
* - Bencode integers → `number`
* - Bencode strings → `Uint8Array` (default) or `string` (if `stringify: true`)
* - Bencode lists → `BencodeDecodedList`
* - Bencode dictionaries → `BencodeDecodedDictionary`
*
* @throws {BencodeDecodeError} With code `UNEXPECTED_END` if the buffer ends unexpectedly.
* @throws {BencodeDecodeError} With code `INVALID_FORMAT` if an invalid type marker is found.
* @throws {BencodeDecodeError} With code `LEADING_ZEROS` if an integer has leading zeros.
* @throws {BencodeDecodeError} With code `NEGATIVE_ZERO` if negative zero is encountered.
* @throws {BencodeDecodeError} With code `UNSORTED_KEYS` if `strict: true` and dictionary
* keys are not in lexicographic order.
* @throws {BencodeDecodeError} With code `MAX_SIZE_EXCEEDED` if a string exceeds `maxStringLength`.
* @throws {BencodeDecodeError} With code `MAX_DEPTH_EXCEEDED` if nesting exceeds `maxDepth`.
*
* @example
* ```typescript
* const decoder = new BencodeDecoder('i42e');
* const value = decoder.decode(); // 42
*
* // Decode multiple values
* const decoder = new BencodeDecoder('i1ei2ei3e');
* while (!decoder.hasRemainingData() === false) {
* console.log(decoder.decode()); // 1, 2, 3
* }
* ```
*/
decode() {
if (this._isEOF()) {
throw this._decodeError("UNEXPECTED_END" /* UNEXPECTED_END */, "Unexpected end of data");
}
if (_BencodeDecoder._isInteger(this._currentChar())) {
return this._decodeString();
}
if (this._currentChar() === 105 /* INTEGER */) {
return this._decodeInteger();
}
if (this._currentChar() === 108 /* LIST */) {
return this._decodeList();
}
if (this._currentChar() === 100 /* DICTIONARY */) {
return this._decodeDictionary();
}
throw this._decodeError("INVALID_FORMAT" /* INVALID_FORMAT */, "Invalid bencode data");
}
/**
* Gets the byte at the current buffer position without advancing.
*
* @returns The byte value at the current position.
*/
_currentChar() {
return this._buffer[this._index];
}
/**
* Checks if the current position has reached or exceeded the buffer length.
*
* @returns `true` if at or past end of buffer.
*/
_isEOF() {
return this._index >= this._buffer.length;
}
/**
* Formats a byte value for display in error messages.
*
* Printable ASCII characters (0x20-0x7e) are shown as quoted characters.
* Non-printable bytes are shown as hex values.
*
* @param char - The byte value to format.
* @returns A human-readable string representation.
*/
static _formatChar(char) {
if (char === void 0 || char === null) {
return "undefined";
}
if (char >= 32 && char <= 126) {
return `'${String.fromCharCode(char)}'`;
}
return `0x${char.toString(16).padStart(2, "0")}`;
}
/**
* Creates a decode error with position context.
*
* Appends the current buffer position and (if not at EOF) the current character
* to the error message for debugging.
*
* @param code - The error code identifying the error type.
* @param message - The base error message.
* @returns A BencodeDecodeError with position information.
*/
_decodeError(code, message) {
let fullMessage;
if (this._isEOF()) {
fullMessage = `${message} at position ${this._index}`;
} else {
fullMessage = `${message} at position ${this._index} (found ${_BencodeDecoder._formatChar(this._currentChar())})`;
}
return new BencodeDecodeError(code, fullMessage, this._index);
}
/**
* Gets the byte at the current position and advances to the next position.
*
* @returns The byte value at the current position before advancing.
*/
_next() {
return this._buffer[this._index++];
}
/**
* Decodes a bencode string value.
*
* Bencode strings are formatted as `<length>:<content>` where length is a
* non-negative integer. The content is returned as a Uint8Array by default,
* or as a string if the `stringify` option is enabled.
*
* @returns The decoded string as a Uint8Array or string.
* @throws {BencodeDecodeError} With code `MAX_SIZE_EXCEEDED` if length exceeds `maxStringLength`.
* @throws {BencodeDecodeError} With code `UNEXPECTED_END` if buffer doesn't contain enough bytes.
*/
_decodeString() {
const length = this._decodeInteger();
if (this._options.maxStringLength && length > this._options.maxStringLength) {
throw new BencodeDecodeError("MAX_SIZE_EXCEEDED" /* MAX_SIZE_EXCEEDED */, `String length ${length} exceeds maximum ${this._options.maxStringLength}`, this._index);
}
if (this._index + length > this._buffer.length) {
throw this._decodeError("UNEXPECTED_END" /* UNEXPECTED_END */, `Unexpected end of data: expected ${length} bytes for string`);
}
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = this._next();
}
return this._options.stringify ? Bytes.toString(bytes, this._options.encoding || "utf8") : bytes;
}
/**
* Decodes a bencode integer value or string length prefix.
*
* Bencode integers are formatted as `i<number>e` (e.g., `i42e`, `i-17e`).
* String length prefixes are just digits followed by `:` (e.g., `5:`).
*
* **Non-standard extensions:**
* - A leading `+` sign is silently ignored (`i+42e` → `42`)
* - Decimal points cause the fractional part to be discarded (`i3.14e` → `3`)
*
* @returns The decoded integer value.
* @throws {BencodeDecodeError} With code `LEADING_ZEROS` if the integer has leading zeros.
* @throws {BencodeDecodeError} With code `NEGATIVE_ZERO` if negative zero is encountered.
* @throws {BencodeDecodeError} With code `UNEXPECTED_END` if the terminating `e` is missing.
*/
_decodeInteger() {
let sign = 1;
let isFloat = false;
let integer = 0;
let isBencodeInteger = false;
if (this._currentChar() === 105 /* INTEGER */) {
this._index++;
isBencodeInteger = true;
}
if (this._currentChar() === 43 /* PLUS */) {
this._index++;
}
if (this._currentChar() === 45 /* MINUS */) {
this._index++;
sign = -1;
}
if (isBencodeInteger && this._currentChar() === 48 && _BencodeDecoder._isInteger(this._buffer[this._index + 1])) {
throw this._decodeError("LEADING_ZEROS" /* LEADING_ZEROS */, "Invalid bencode: leading zeros are not allowed");
}
while (_BencodeDecoder._isInteger(this._currentChar()) || this._currentChar() === 46 /* DOT */) {
if (this._currentChar() === 46 /* DOT */) {
isFloat = true;
}
isFloat === false ? integer = integer * 10 + (this._next() - 48) : this._index++;
}
if (isBencodeInteger) {
if (this._isEOF() || this._currentChar() !== 101 /* END */) {
throw this._decodeError("UNEXPECTED_END" /* UNEXPECTED_END */, "Unexpected end of data: expected 'e' to terminate integer");
}
this._index++;
} else if (this._currentChar() === 58 /* STR_DELIMITER */) {
this._index++;
}
if (sign === -1 && integer === 0) {
throw this._decodeError("NEGATIVE_ZERO" /* NEGATIVE_ZERO */, "Invalid bencode: negative zero is not allowed");
}
return integer * sign;
}
/**
* Decodes a bencode list value.
*
* Bencode lists are formatted as `l<items>e` where items are any valid
* bencode values. Lists can be nested and contain mixed types.
*
* @returns The decoded list as a JavaScript array.
* @throws {BencodeDecodeError} With code `MAX_DEPTH_EXCEEDED` if nesting exceeds `maxDepth`.
* @throws {BencodeDecodeError} With code `UNEXPECTED_END` if the terminating `e` is missing.
*/
_decodeList() {
this._currentDepth++;
if (this._options.maxDepth && this._currentDepth > this._options.maxDepth) {
throw new BencodeDecodeError("MAX_DEPTH_EXCEEDED" /* MAX_DEPTH_EXCEEDED */, `Nesting depth ${this._currentDepth} exceeds maximum ${this._options.maxDepth}`, this._index);
}
const acc = [];
this._next();
while (!this._isEOF() && this._currentChar() !== 101 /* END */) {
acc.push(this.decode());
}
if (this._isEOF()) {
this._currentDepth--;
throw this._decodeError("UNEXPECTED_END" /* UNEXPECTED_END */, "Unexpected end of data: expected 'e' to terminate list");
}
this._next();
this._currentDepth--;
return acc;
}
/**
* Decodes a bencode dictionary value.
*
* Bencode dictionaries are formatted as `d<key><value>...e` where keys are
* bencode strings and values are any valid bencode values. According to the
* specification, keys must be in sorted lexicographic order, but this is only
* enforced when `strict: true`.
*
* @returns The decoded dictionary as a JavaScript object.
* @throws {BencodeDecodeError} With code `MAX_DEPTH_EXCEEDED` if nesting exceeds `maxDepth`.
* @throws {BencodeDecodeError} With code `UNSORTED_KEYS` if `strict: true` and keys are not sorted.
* @throws {BencodeDecodeError} With code `UNEXPECTED_END` if the terminating `e` is missing.
*/
_decodeDictionary() {
this._currentDepth++;
if (this._options.maxDepth && this._currentDepth > this._options.maxDepth) {
throw new BencodeDecodeError("MAX_DEPTH_EXCEEDED" /* MAX_DEPTH_EXCEEDED */, `Nesting depth ${this._currentDepth} exceeds maximum ${this._options.maxDepth}`, this._index);
}
const acc = {};
let prevKey = null;
this._next();
while (!this._isEOF() && this._currentChar() !== 101 /* END */) {
const key = this._decodeString();
const keyBytes = Bytes.isBytes(key) ? key : Bytes.fromString(key);
if (this._options.strict && prevKey !== null && Bytes.compare(prevKey, keyBytes) >= 0) {
const keyStr = typeof key === "string" ? key : Bytes.toString(key);
const prevKeyStr = Bytes.toString(prevKey);
throw this._decodeError("UNSORTED_KEYS" /* UNSORTED_KEYS */, `Invalid bencode: dictionary keys must be in sorted order (key '${keyStr}' after '${prevKeyStr}')`);
}
prevKey = keyBytes;
acc[typeof key === "string" ? key : Bytes.toString(key)] = this.decode();
}
if (this._isEOF()) {
this._currentDepth--;
throw this._decodeError("UNEXPECTED_END" /* UNEXPECTED_END */, "Unexpected end of data: expected 'e' to terminate dictionary");
}
this._next();
this._currentDepth--;
return acc;
}
};
// src/BencodeEncoder.ts
var BencodeEncoder = class {
/**
* Creates a new BencodeEncoder instance.
*
* @param options - Configuration options for encoding behavior.
*
* @example
* ```typescript
* // Default options (returns Uint8Array)
* const encoder = new BencodeEncoder();
*
* // Return string instead of Uint8Array
* const encoder = new BencodeEncoder({ stringify: true });
* ```
*/
constructor(options) {
/** Bencode integer start marker: 'i' */
this._integerIdentifier = new Uint8Array([105 /* INTEGER */]);
/** Bencode string delimiter: ':' */
this._stringDelimiterIdentifier = new Uint8Array([58 /* STR_DELIMITER */]);
/** Bencode list start marker: 'l' */
this._listIdentifier = new Uint8Array([108 /* LIST */]);
/** Bencode dictionary start marker: 'd' */
this._dictionaryIdentifier = new Uint8Array([100 /* DICTIONARY */]);
/** Bencode end marker: 'e' */
this._endIdentifier = new Uint8Array([101 /* END */]);
this._buffer = [];
this._options = options || {};
this._visited = /* @__PURE__ */ new WeakSet();
this._path = [];
}
/**
* Encodes a JavaScript value to bencode format.
*
* @param data - The value to encode. See {@link BencodeEncodableValue} for supported types.
*
* @returns The bencode-encoded data as a Uint8Array (default) or string (if `stringify: true`).
*
* @throws {BencodeEncodeError} With code `UNSUPPORTED_TYPE` if the value contains an
* unsupported type (e.g., functions, symbols, BigInt).
* @throws {BencodeEncodeError} With code `CIRCULAR_REFERENCE` if the data contains
* circular references.
*
* @example
* ```typescript
* const encoder = new BencodeEncoder();
*
* encoder.encode(42); // Uint8Array [0x69, 0x34, 0x32, 0x65] ('i42e')
* encoder.encode('hello'); // Uint8Array [0x35, 0x3a, 0x68, 0x65, 0x6c, 0x6c, 0x6f] ('5:hello')
* encoder.encode([1, 2]); // Uint8Array [...] ('li1ei2ee')
* encoder.encode({ a: 1 }); // Uint8Array [...] ('d1:ai1ee')
* ```
*/
encode(data) {
this._encodeType(data);
return this._options.stringify ? Bytes.toString(Bytes.concat(this._buffer)) : Bytes.concat(this._buffer);
}
/**
* Routes encoding to the appropriate type-specific method.
*
* Determines the JavaScript type of the input and calls the corresponding
* encoder method. Type detection order matters for correct handling of
* Uint8Array vs ArrayBufferView vs generic object.
*
* @param data - The value to encode.
* @throws {BencodeEncodeError} With code `UNSUPPORTED_TYPE` for unsupported types.
*/
_encodeType(data) {
if (Bytes.isBytes(data)) {
return this._encodeBytes(data);
}
if (Array.isArray(data)) {
return this._encodeList(data);
}
if (ArrayBuffer.isView(data)) {
return this._encodeBytes(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
}
if (data instanceof ArrayBuffer) {
return this._encodeBytes(new Uint8Array(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 BencodeEncodeError(
"UNSUPPORTED_TYPE" /* UNSUPPORTED_TYPE */,
`${typeof data} is unsupported type.`,
[...this._path]
);
}
/**
* Encodes a Uint8Array as a bencode string.
*
* Bencode strings are formatted as `<length>:<content>` where length is the
* byte length of the content.
*
* @param data - The Uint8Array to encode.
*/
_encodeBytes(data) {
this._buffer.push(
Bytes.fromString(String(data.length)),
this._stringDelimiterIdentifier,
data
);
}
/**
* Encodes a JavaScript string as a bencode string.
*
* The string is converted to UTF-8 bytes and encoded as `<byte_length>:<utf8_bytes>`.
* Note that the length prefix is the byte length, not the character count.
*
* @param data - The string to encode.
*/
_encodeString(data) {
const encoded = Bytes.fromString(data);
this._buffer.push(
Bytes.fromString(String(encoded.length)),
this._stringDelimiterIdentifier,
encoded
);
}
/**
* Encodes a number as a bencode integer.
*
* Bencode integers are formatted as `i<number>e`. Floating-point numbers are
* truncated toward zero (not rounded) before encoding.
*
* @param data - The number to encode.
*/
_encodeInteger(data) {
this._buffer.push(
this._integerIdentifier,
Bytes.fromString(String(Math.trunc(data))),
this._endIdentifier
);
}
/**
* Encodes a JavaScript array as a bencode list.
*
* Bencode lists are formatted as `l<items>e`. Elements are encoded in order.
* `null` and `undefined` values are silently skipped.
*
* Tracks visited objects to detect circular references.
*
* @param data - The array to encode.
* @throws {BencodeEncodeError} With code `CIRCULAR_REFERENCE` if the array was already visited.
*/
_encodeList(data) {
if (this._visited.has(data)) {
throw new BencodeEncodeError(
"CIRCULAR_REFERENCE" /* CIRCULAR_REFERENCE */,
"Circular reference detected",
[...this._path]
);
}
this._visited.add(data);
this._buffer.push(this._listIdentifier);
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (item === null || item === void 0) {
continue;
}
this._path.push(i);
this._encodeType(item);
this._path.pop();
}
this._buffer.push(this._endIdentifier);
this._visited.delete(data);
}
/**
* Encodes a JavaScript object as a bencode dictionary.
*
* Bencode dictionaries are formatted as `d<key><value>...e`. Keys are automatically
* sorted lexicographically (by raw byte value) to comply with the bencode specification.
* Properties with `null` or `undefined` values are silently skipped.
*
* Tracks visited objects to detect circular references.
*
* @param data - The object to encode.
* @throws {BencodeEncodeError} With code `CIRCULAR_REFERENCE` if the object was already visited.
*/
_encodeDictionary(data) {
if (this._visited.has(data)) {
throw new BencodeEncodeError(
"CIRCULAR_REFERENCE" /* CIRCULAR_REFERENCE */,
"Circular reference detected",
[...this._path]
);
}
this._visited.add(data);
this._buffer.push(this._dictionaryIdentifier);
const keys = Object.keys(data).sort();
for (const key of keys) {
if (data[key] === null || data[key] === void 0) {
continue;
}
this._encodeString(key);
this._path.push(key);
this._encodeType(data[key]);
this._path.pop();
}
this._buffer.push(this._endIdentifier);
this._visited.delete(data);
}
};
// src/index.ts
function decode(data, options) {
const decoder = new BencodeDecoder(data, options);
const result = decoder.decode();
if (options?.strict && decoder.hasRemainingData()) {
throw new BencodeDecodeError("TRAILING_DATA" /* TRAILING_DATA */, "Invalid bencode: unexpected data after valid bencode");
}
return result;
}
function encode(data, options) {
const encoder = new BencodeEncoder(options);
return encoder.encode(data);
}
function encodeToBytes(data) {
const encoder = new BencodeEncoder();
return encoder.encode(data);
}
function encodeToString(data, options) {
const encoding = options?.encoding ?? "utf8";
const encoder = new BencodeEncoder({ stringify: false });
const bytes = encoder.encode(data);
return Bytes.toString(bytes, encoding);
}
var bencodec = { decode, encode, encodeToBytes, encodeToString };
var index_default = bencodec;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BencodeDecodeError,
BencodeEncodeError,
BencodeError,
BencodeErrorCode,
bencodec,
decode,
encode,
encodeToBytes,
encodeToString
});