UNPKG

bencodec

Version:

Universal library for decoding and encoding bencode data

533 lines (528 loc) 20.7 kB
/** * 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 };