bencodec
Version:
Universal library for decoding and encoding bencode data
533 lines (528 loc) • 20.7 kB
TypeScript
/**
* Supported character encodings for byte/string conversions.
* Limited set for cross-platform compatibility.
*/
type ByteEncoding = 'utf8' | 'utf-8' | 'latin1' | 'binary' | 'ascii';
/**
* Configuration options for bencode encoding and decoding operations.
*
* @example
* ```typescript
* // Decode with string output and strict validation
* const data = decode(buffer, { stringify: true, strict: true });
*
* // Decode with security limits
* const data = decode(buffer, { maxStringLength: 1024 * 1024, maxDepth: 100 });
* ```
*/
interface IBencodecOptions {
/**
* When `true`, returns strings instead of Buffers.
*
* - **Decoding**: String values are returned as JavaScript strings instead of Buffers.
* Uses the `encoding` option (default: 'utf8') for conversion.
* - **Encoding**: Returns the encoded bencode data as a UTF-8 string instead of a Buffer.
*
* @default false
*/
stringify?: boolean;
/**
* Enables strict bencode validation according to the BitTorrent specification.
*
* When `true`, the following additional validations are performed during decoding:
* - Dictionary keys must be in sorted (lexicographic) order
* - No trailing data is allowed after the decoded value
*
* @default false
* @throws {BencodeDecodeError} With code `UNSORTED_KEYS` if dictionary keys are not sorted
* @throws {BencodeDecodeError} With code `TRAILING_DATA` if extra data follows the bencode value
*/
strict?: boolean;
/**
* Character encoding to use when `stringify` is `true`.
* Only applies to decoding operations.
*
* @default 'utf8'
*/
encoding?: ByteEncoding;
/**
* Maximum allowed length for decoded strings in bytes.
* Provides protection against memory exhaustion from malicious input.
*
* @throws {BencodeDecodeError} With code `MAX_SIZE_EXCEEDED` if a string exceeds this limit
*/
maxStringLength?: number;
/**
* Maximum allowed nesting depth for lists and dictionaries.
* Provides protection against stack overflow from deeply nested structures.
*
* @throws {BencodeDecodeError} With code `MAX_DEPTH_EXCEEDED` if nesting exceeds this limit
*/
maxDepth?: number;
}
/**
* A decoded bencode list (array of decoded values).
*
* @see {@link BencodeDecodedValue} for possible element types
*/
type BencodeDecodedList = Array<BencodeDecodedValue>;
/**
* A decoded bencode dictionary (object with string keys and decoded values).
*
* Keys are always strings (decoded from bencode byte strings).
*
* @see {@link BencodeDecodedValue} for possible value types
*/
type BencodeDecodedDictionary = {
[key: string]: BencodeDecodedValue;
};
/**
* Union type representing all possible values returned by the decoder.
*
* - `number` - Decoded bencode integers
* - `Uint8Array` - Decoded bencode strings (when `stringify: false`, the default)
* - `string` - Decoded bencode strings (when `stringify: true`)
* - `BencodeDecodedList` - Decoded bencode lists
* - `BencodeDecodedDictionary` - Decoded bencode dictionaries
*/
type BencodeDecodedValue = number | Uint8Array | string | BencodeDecodedList | BencodeDecodedDictionary;
/**
* An encodable list (array of encodable values).
*
* @see {@link BencodeEncodableValue} for possible element types
*/
type BencodeEncodableList = Array<BencodeEncodableValue>;
/**
* An encodable dictionary (object with string keys and encodable values).
*
* Keys will be automatically sorted lexicographically during encoding
* to comply with the bencode specification.
*
* @see {@link BencodeEncodableValue} for possible value types
*/
type BencodeEncodableDictionary = {
[key: string]: BencodeEncodableValue;
};
/**
* Union type representing all JavaScript values that can be encoded to bencode.
*
* **Supported types and their encoding:**
* - `number` - Encoded as bencode integer. Floats are truncated toward zero.
* - `boolean` - Encoded as bencode integer (`true` → `i1e`, `false` → `i0e`)
* - `string` - Encoded as bencode string (UTF-8 byte length prefix)
* - `Uint8Array` - Encoded as bencode string (raw bytes)
* - `ArrayBuffer` - Encoded as bencode string (raw bytes)
* - `ArrayBufferView` - Encoded as bencode string (e.g., Uint8Array, DataView)
* - `BencodeEncodableList` - Encoded as bencode list
* - `BencodeEncodableDictionary` - Encoded as bencode dictionary
* - `null` / `undefined` - Silently skipped in lists and dictionaries
*
* @example
* ```typescript
* // All of these are valid encodable values
* encode(42); // 'i42e'
* encode(true); // 'i1e'
* encode('hello'); // '5:hello'
* encode([1, 2, 3]); // 'li1ei2ei3ee'
* encode({ a: 1, b: 2 }); // 'd1:ai1e1:bi2ee'
* ```
*/
type BencodeEncodableValue = number | boolean | string | Uint8Array | ArrayBuffer | ArrayBufferView | BencodeEncodableList | BencodeEncodableDictionary | null | undefined;
/**
* Configuration options for {@link encodeToString} function.
*
* @example
* ```typescript
* const str = encodeToString({ foo: 'bar' }, { encoding: 'latin1' });
* ```
*/
interface IBencodeEncodeOptions {
/**
* Character encoding for string output.
*
* @default 'utf8'
*/
encoding?: ByteEncoding;
}
/**
* Error codes for bencode operations.
* Used to programmatically identify error types without parsing error messages.
*
* @example
* ```typescript
* import { decode, BencodeDecodeError, BencodeErrorCode } from 'bencodec';
*
* try {
* decode(data);
* } catch (error) {
* if (error instanceof BencodeDecodeError) {
* switch (error.code) {
* case BencodeErrorCode.EMPTY_INPUT:
* console.log('No data provided');
* break;
* case BencodeErrorCode.INVALID_FORMAT:
* console.log('Malformed bencode data');
* break;
* }
* }
* }
* ```
*/
declare enum BencodeErrorCode {
/** Input data is empty or falsy */
EMPTY_INPUT = "EMPTY_INPUT",
/** Reached end of data unexpectedly while parsing */
UNEXPECTED_END = "UNEXPECTED_END",
/** Invalid bencode format (unrecognized type marker) */
INVALID_FORMAT = "INVALID_FORMAT",
/** Integer has leading zeros (e.g., i03e) which is invalid in bencode */
LEADING_ZEROS = "LEADING_ZEROS",
/** Negative zero (i-0e) is not allowed in bencode */
NEGATIVE_ZERO = "NEGATIVE_ZERO",
/** Dictionary keys are not in sorted order (strict mode only) */
UNSORTED_KEYS = "UNSORTED_KEYS",
/** Extra data found after valid bencode (strict mode only) */
TRAILING_DATA = "TRAILING_DATA",
/** Maximum nesting depth exceeded */
MAX_DEPTH_EXCEEDED = "MAX_DEPTH_EXCEEDED",
/** Maximum input size exceeded */
MAX_SIZE_EXCEEDED = "MAX_SIZE_EXCEEDED",
/** Attempted to encode an unsupported JavaScript type */
UNSUPPORTED_TYPE = "UNSUPPORTED_TYPE",
/** Circular reference detected in data structure */
CIRCULAR_REFERENCE = "CIRCULAR_REFERENCE"
}
/**
* Base error class for all bencode operations.
* Extends Error with a structured error code for programmatic handling.
*
* @example
* ```typescript
* try {
* decode(data);
* } catch (error) {
* if (error instanceof BencodeError) {
* console.log(`Bencode error: ${error.code}`);
* }
* }
* ```
*/
declare abstract class BencodeError extends Error {
/**
* The structured error code identifying the error type.
* Use this for programmatic error handling instead of parsing error messages.
*/
readonly code: BencodeErrorCode;
/**
* @param code - The error code identifying the error type
* @param message - Human-readable error message
*/
protected constructor(code: BencodeErrorCode, message: string);
}
/**
* Error thrown during bencode decoding operations.
* Includes the buffer position where the error occurred for debugging.
*
* @example
* ```typescript
* try {
* decode('i03e'); // Leading zeros not allowed
* } catch (error) {
* if (error instanceof BencodeDecodeError) {
* console.log(`Error code: ${error.code}`);
* console.log(`Position: ${error.position}`);
* console.log(`Message: ${error.message}`);
* // Output:
* // Error code: LEADING_ZEROS
* // Position: 1
* // Message: Invalid bencode: leading zeros are not allowed at position 1 (found '0')
* }
* }
* ```
*/
declare class BencodeDecodeError extends BencodeError {
/**
* The byte position in the input buffer where the error occurred.
* Undefined for errors that occur before parsing begins (e.g., empty input)
* or after successful parsing (e.g., trailing data in strict mode).
*/
readonly position?: number;
/**
* @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: BencodeErrorCode, message: string, position?: number);
}
/**
* Error thrown during bencode encoding operations.
* Includes the path to the problematic value for debugging nested structures.
*
* @example
* ```typescript
* const data = { outer: { inner: circularRef } };
* try {
* encode(data);
* } catch (error) {
* if (error instanceof BencodeEncodeError) {
* console.log(`Error code: ${error.code}`);
* console.log(`Path: ${error.path?.join('.')}`);
* // Output:
* // Error code: CIRCULAR_REFERENCE
* // Path: outer.inner
* }
* }
* ```
*/
declare class BencodeEncodeError extends BencodeError {
/**
* The path to the value that caused the error.
* Array indices are numbers, object keys are strings.
* Undefined for top-level errors (e.g., encoding a function directly).
*/
readonly path?: (string | number)[];
/**
* @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: BencodeErrorCode, message: string, path?: (string | number)[]);
}
/**
* Decodes bencode data into JavaScript values.
*
* Parses bencode-encoded data according to the
* {@link https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding | BitTorrent specification}.
*
* @typeParam Type - The expected return type. Defaults to `unknown`. Use with caution as
* no runtime validation is performed.
*
* @param data - The bencode data to decode. Can be a Uint8Array or a string (which will be
* converted to a Uint8Array internally).
* @param options - Configuration options for decoding behavior.
*
* @returns The decoded JavaScript value. The return type depends on the bencode data:
* - Bencode integers → `number`
* - Bencode strings → `Uint8Array` (default) or `string` (if `stringify: true`)
* - Bencode lists → `Array`
* - Bencode dictionaries → `Object`
*
* @throws {BencodeDecodeError} With code `EMPTY_INPUT` if data is empty or falsy.
* @throws {BencodeDecodeError} With code `INVALID_FORMAT` if data is not valid bencode.
* @throws {BencodeDecodeError} With code `UNEXPECTED_END` if data ends unexpectedly.
* @throws {BencodeDecodeError} With code `LEADING_ZEROS` if an integer has leading zeros (e.g., `i03e`).
* @throws {BencodeDecodeError} With code `NEGATIVE_ZERO` if negative zero is encountered (`i-0e`).
* @throws {BencodeDecodeError} With code `UNSORTED_KEYS` if `strict: true` and dictionary keys
* are not in lexicographic order.
* @throws {BencodeDecodeError} With code `TRAILING_DATA` if `strict: true` and extra data
* follows the decoded value.
* @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
* import { decode } from 'bencodec';
*
* // Decode an integer
* decode('i42e'); // 42
*
* // Decode a string (returns Uint8Array by default)
* decode('5:hello'); // Uint8Array [0x68, 0x65, 0x6c, 0x6c, 0x6f]
*
* // Decode a string as JavaScript string
* decode('5:hello', { stringify: true }); // 'hello'
*
* // Decode a list
* decode('li1ei2ei3ee', { stringify: true }); // [1, 2, 3]
*
* // Decode a dictionary
* decode('d3:fooi42ee', { stringify: true }); // { foo: 42 }
*
* // Use strict mode for validation
* decode('i42eextra', { strict: true }); // throws TRAILING_DATA error
*
* // Type the result
* interface Torrent { announce: string; info: { name: string } }
* const torrent = decode<Torrent>(buffer, { stringify: true });
* ```
*
* @remarks
* **Non-standard behaviors:**
* - **Plus sign in integers**: A leading `+` sign (e.g., `i+42e`) is silently ignored.
* - **Float truncation**: Decimal numbers (e.g., `i3.14e`) are truncated toward zero,
* keeping only the integer part (`3`). The fractional part is discarded.
*
* **Strict mode:**
* When `options.strict` is `true`, additional validations are performed:
* - Dictionary keys must be in sorted lexicographic order
* - No trailing data is allowed after the decoded value
*
* These checks are disabled by default for performance and to handle
* non-compliant data from some BitTorrent clients.
*/
declare function decode<Type = unknown>(data: Uint8Array | string, options?: IBencodecOptions): Type;
/**
* Encodes JavaScript values into bencode format.
*
* Produces bencode-encoded data according to the
* {@link https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding | BitTorrent specification}.
*
* @deprecated Use {@link encodeToBytes} for Uint8Array output or {@link encodeToString} for string output.
* This function will be removed in a future major version.
*
* @param data - The value to encode. See {@link BencodeEncodableValue} for supported types.
* @param options - Configuration options for encoding behavior.
*
* @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
* import { encode } from 'bencodec';
*
* // Encode an integer
* encode(42); // Uint8Array [0x69, 0x34, 0x32, 0x65] ('i42e')
*
* // Encode a string
* encode('hello'); // Uint8Array [0x35, 0x3a, 0x68, 0x65, 0x6c, 0x6c, 0x6f] ('5:hello')
*
* // Encode a list
* encode([1, 2, 3]); // 'li1ei2ei3ee'
*
* // Encode a dictionary (keys are auto-sorted)
* encode({ z: 1, a: 2 }); // 'd1:ai2e1:zi1ee' (keys sorted: a, z)
*
* // Get result as string instead of Uint8Array
* encode({ foo: 'bar' }, { stringify: true }); // 'd3:foo3:bare'
*
* // Encode binary data
* encode(new Uint8Array([0x00, 0xff])); // '2:\x00\xff'
* encode(new Uint8Array([1, 2, 3])); // '3:\x01\x02\x03'
* ```
*
* @remarks
* **Non-standard behaviors:**
* - **Boolean encoding**: Booleans are encoded as integers (`true` → `i1e`, `false` → `i0e`).
* Standard bencode does not define boolean types.
* - **Float truncation**: Floating-point numbers are truncated toward zero before encoding.
* For example, `3.7` becomes `i3e` and `-2.9` becomes `i-2e`.
* - **Null/undefined handling**: `null` and `undefined` values are silently skipped
* in lists and dictionaries. They cannot be encoded as top-level values.
*
* **Dictionary key sorting:**
* Dictionary keys are automatically sorted lexicographically (by raw byte value)
* to comply with the bencode specification. The original key order is not preserved.
*/
declare function encode<T extends object>(data: T, options?: IBencodecOptions): Uint8Array | string;
declare function encode(data: BencodeEncodableValue, options?: IBencodecOptions): Uint8Array | string;
/**
* Encodes JavaScript values into bencode format as a Uint8Array.
*
* Produces bencode-encoded data according to the
* {@link https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding | BitTorrent specification}.
*
* @param data - The value to encode. See {@link BencodeEncodableValue} for supported types.
*
* @returns The bencode-encoded data as a Uint8Array.
*
* @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
* import { encodeToBytes } from 'bencodec';
*
* // Encode an integer
* encodeToBytes(42); // Uint8Array [0x69, 0x34, 0x32, 0x65] ('i42e')
*
* // Encode a string
* encodeToBytes('hello'); // Uint8Array [0x35, 0x3a, 0x68, 0x65, 0x6c, 0x6c, 0x6f] ('5:hello')
*
* // Encode a dictionary
* encodeToBytes({ foo: 'bar' }); // Uint8Array for 'd3:foo3:bare'
*
* // Encode binary data
* encodeToBytes(new Uint8Array([0x00, 0xff])); // Uint8Array for '2:\x00\xff'
* ```
*
* @remarks
* **Non-standard behaviors:**
* - **Boolean encoding**: Booleans are encoded as integers (`true` → `i1e`, `false` → `i0e`).
* - **Float truncation**: Floating-point numbers are truncated toward zero before encoding.
* - **Null/undefined handling**: `null` and `undefined` values are silently skipped
* in lists and dictionaries. They cannot be encoded as top-level values.
*
* **Dictionary key sorting:**
* Dictionary keys are automatically sorted lexicographically (by raw byte value)
* to comply with the bencode specification.
*/
declare function encodeToBytes<T extends object>(data: T): Uint8Array;
declare function encodeToBytes(data: BencodeEncodableValue): Uint8Array;
/**
* Encodes JavaScript values into bencode format as a string.
*
* Produces bencode-encoded data according to the
* {@link https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding | BitTorrent specification}.
*
* @param data - The value to encode. See {@link BencodeEncodableValue} for supported types.
* @param options - Configuration options for encoding behavior.
* Use `options.encoding` to specify the character encoding (default: `'utf8'`).
* Supported encodings: `'utf8'`, `'utf-8'`, `'latin1'`, `'binary'`, `'ascii'`.
*
* @returns The bencode-encoded data as a string.
*
* @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
* import { encodeToString } from 'bencodec';
*
* // Encode an integer
* encodeToString(42); // 'i42e'
*
* // Encode a string
* encodeToString('hello'); // '5:hello'
*
* // Encode a dictionary
* encodeToString({ foo: 'bar' }); // 'd3:foo3:bare'
*
* // Use latin1 encoding for binary data
* encodeToString(new Uint8Array([0x00, 0xff]), { encoding: 'latin1' }); // '2:\x00\xff'
*
* // Specify encoding explicitly
* encodeToString({ foo: 42 }, { encoding: 'utf8' }); // 'd3:fooi42ee'
* ```
*
* @remarks
* **Non-standard behaviors:**
* - **Boolean encoding**: Booleans are encoded as integers (`true` → `i1e`, `false` → `i0e`).
* - **Float truncation**: Floating-point numbers are truncated toward zero before encoding.
* - **Null/undefined handling**: `null` and `undefined` values are silently skipped
* in lists and dictionaries. They cannot be encoded as top-level values.
*
* **Dictionary key sorting:**
* Dictionary keys are automatically sorted lexicographically (by raw byte value)
* to comply with the bencode specification.
*
* **Encoding note:**
* For binary data containing non-UTF8 bytes, use `{ encoding: 'latin1' }` or `{ encoding: 'binary' }`
* to preserve byte values in the output string.
*/
declare function encodeToString<T extends object>(data: T, options?: IBencodeEncodeOptions): string;
declare function encodeToString(data: BencodeEncodableValue, options?: IBencodeEncodeOptions): string;
declare const bencodec: {
decode: typeof decode;
encode: typeof encode;
encodeToBytes: typeof encodeToBytes;
encodeToString: typeof encodeToString;
};
export { BencodeDecodeError, type BencodeDecodedValue, type BencodeEncodableValue, BencodeEncodeError, BencodeError, BencodeErrorCode, type ByteEncoding, type IBencodeEncodeOptions, type IBencodecOptions, bencodec, decode, bencodec as default, encode, encodeToBytes, encodeToString };