bencodec
Version:
Universal library for decoding and encoding bencode data
357 lines (260 loc) • 9.25 kB
Markdown
# bencodec

[](https://coveralls.io/github/isolomak/bencodec)
[](https://www.npmjs.com/package/bencodec)
[](https://www.npmjs.com/package/bencodec)
[](LICENSE.md)


**A fast, secure, zero-dependency bencode encoder/decoder for modern JavaScript runtimes.**
Universal TypeScript library compliant with the [BitTorrent bencoding specification](https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding). Works in Node.js, browsers, Deno, and Bun.
## Highlights
- **Zero Dependencies** - No external packages, minimal attack surface
- **Universal** - Works in Node.js, browsers, Deno, and Bun
- **TypeScript First** - Full type definitions with generics support
- **Security Built-in** - DoS protection with configurable limits
- **BitTorrent Compliant** - Strict mode for spec validation
- **Modern API** - Uses `Uint8Array` (not Node.js Buffer)
- **Dual Package** - ESM and CommonJS exports
- **100% Tested** - Complete code coverage
## Installation
```bash
# npm
npm install bencodec
# yarn
yarn add bencodec
# pnpm
pnpm add bencodec
# bun
bun add bencodec
```
## Quick Start
```typescript
import { encodeToBytes, encodeToString, decode } from 'bencodec';
// Encode JavaScript values to bencode (Uint8Array)
const bytes = encodeToBytes({ announce: 'http://tracker.example.com', info: { name: 'file.txt' } });
// Encode JavaScript values to bencode (string)
const str = encodeToString({ announce: 'http://tracker.example.com', info: { name: 'file.txt' } });
// Decode bencode data
const decoded = decode(bytes, { stringify: true });
// { announce: 'http://tracker.example.com', info: { name: 'file.txt' } }
```
## Usage
### Decoding
```typescript
import { decode } from 'bencodec';
// Decode integers
decode('i42e'); // 42
// Decode strings (returns Uint8Array by default)
decode('5:hello'); // Uint8Array [0x68, 0x65, 0x6c, 0x6c, 0x6f]
// Decode strings as JavaScript strings
decode('5:hello', { stringify: true }); // 'hello'
// Decode lists
decode('li1ei2ei3ee', { stringify: true }); // [1, 2, 3]
// Decode dictionaries
decode('d3:fooi42e3:bar4:spame', { stringify: true }); // { bar: 'spam', foo: 42 }
// Type the result with generics
interface Torrent {
announce: string;
info: { name: string };
}
const torrent = decode<Torrent>(buffer, { stringify: true });
```
### Encoding
#### Encode to Bytes (Uint8Array)
```typescript
import { encodeToBytes } from 'bencodec';
// Encode integers
encodeToBytes(42); // Uint8Array for 'i42e'
// Encode strings
encodeToBytes('hello'); // Uint8Array for '5:hello'
// Encode lists
encodeToBytes([1, 2, 3]); // Uint8Array for 'li1ei2ei3ee'
// Encode dictionaries (keys auto-sorted per spec)
encodeToBytes({ z: 1, a: 2 }); // Uint8Array for 'd1:ai2e1:zi1ee'
// Encode binary data
encodeToBytes(new Uint8Array([0x00, 0xff]));
```
#### Encode to String
```typescript
import { encodeToString } from 'bencodec';
// Encode to string (UTF-8 by default)
encodeToString({ foo: 'bar' }); // 'd3:foo3:bare'
// Encode integers
encodeToString(42); // 'i42e'
// Use latin1 encoding for binary data preservation
encodeToString(new Uint8Array([0x00, 0xff]), { encoding: 'latin1' }); // '2:\x00\xff'
// Supported encodings: 'utf8', 'utf-8', 'latin1', 'binary', 'ascii'
encodeToString({ foo: 42 }, { encoding: 'utf8' });
```
#### Legacy encode() (Deprecated)
```typescript
import { encode } from 'bencodec';
// Deprecated - use encodeToBytes or encodeToString instead
encode(42); // Returns Uint8Array
encode({ foo: 'bar' }, { stringify: true }); // Returns string
```
### Default Export
```typescript
import bencodec from 'bencodec';
bencodec.encodeToBytes({ foo: 42 });
bencodec.encodeToString({ foo: 42 });
bencodec.decode('d3:fooi42ee');
```
### Options
#### Decode Options (IBencodecOptions)
```typescript
interface IBencodecOptions {
/** Return strings instead of Uint8Array (default: false) */
stringify?: boolean;
/** Enable strict BitTorrent spec validation (default: false) */
strict?: boolean;
/** Character encoding: 'utf8' | 'latin1' | 'ascii' | 'binary' (default: 'utf8') */
encoding?: ByteEncoding;
/** Maximum string length in bytes - security limit */
maxStringLength?: number;
/** Maximum nesting depth - security limit */
maxDepth?: number;
}
```
#### Encode to String Options (IBencodeEncodeOptions)
```typescript
interface IBencodeEncodeOptions {
/** Character encoding for output (default: 'utf8') */
encoding?: 'utf8' | 'utf-8' | 'latin1' | 'binary' | 'ascii';
}
```
## Strict Mode
Enable strict mode for BitTorrent specification compliance:
```typescript
// Enforces sorted dictionary keys
decode('d1:bi1e1:ai2ee', { strict: true });
// Throws: UNSORTED_KEYS
// Rejects trailing data
decode('i42eextra', { strict: true });
// Throws: TRAILING_DATA
```
## Security
Bencodec includes built-in protections against denial-of-service attacks when parsing untrusted data.
### Memory Exhaustion Protection
Prevent memory exhaustion from maliciously large strings:
```typescript
decode(untrustedData, {
maxStringLength: 10 * 1024 * 1024 // 10 MB limit
});
```
### Stack Overflow Protection
Prevent stack overflow from deeply nested structures:
```typescript
decode(untrustedData, {
maxDepth: 100 // Maximum nesting depth
});
```
### Recommended Settings for Untrusted Data
```typescript
const SAFE_OPTIONS = {
maxStringLength: 10 * 1024 * 1024, // 10 MB
maxDepth: 100,
strict: true
};
decode(untrustedData, SAFE_OPTIONS);
```
## Error Handling
Bencodec provides structured error classes with specific error codes for programmatic handling.
### Error Classes
```typescript
import {
BencodeError, // Base class
BencodeDecodeError, // Decode errors (includes position)
BencodeEncodeError, // Encode errors (includes path)
BencodeErrorCode
} from 'bencodec';
```
### Error Codes
| Code | Description |
|------|-------------|
| `EMPTY_INPUT` | Input data is empty or falsy |
| `UNEXPECTED_END` | Data ends unexpectedly |
| `INVALID_FORMAT` | Invalid bencode format |
| `LEADING_ZEROS` | Integer has leading zeros (e.g., `i03e`) |
| `NEGATIVE_ZERO` | Negative zero (`i-0e`) is not allowed |
| `UNSORTED_KEYS` | Dictionary keys not sorted (strict mode) |
| `TRAILING_DATA` | Extra data after valid bencode (strict mode) |
| `MAX_DEPTH_EXCEEDED` | Nesting depth exceeds limit |
| `MAX_SIZE_EXCEEDED` | String length exceeds limit |
| `UNSUPPORTED_TYPE` | Attempted to encode unsupported type |
| `CIRCULAR_REFERENCE` | Circular reference detected |
### Decode Error Example
```typescript
import { decode, BencodeDecodeError, BencodeErrorCode } from 'bencodec';
try {
decode(untrustedData, { strict: true, maxDepth: 50 });
} catch (error) {
if (error instanceof BencodeDecodeError) {
switch (error.code) {
case BencodeErrorCode.MAX_DEPTH_EXCEEDED:
console.error(`Too deeply nested at position ${error.position}`);
break;
case BencodeErrorCode.INVALID_FORMAT:
console.error(`Malformed data at position ${error.position}`);
break;
}
}
}
```
### Encode Error Example
```typescript
import { encode, BencodeEncodeError } from 'bencodec';
try {
const circular: any = { a: 1 };
circular.self = circular;
encode(circular);
} catch (error) {
if (error instanceof BencodeEncodeError) {
console.error(`Error at path: ${error.path?.join('.')}`);
// Output: Error at path: self
}
}
```
## Platform Support
| Platform | Version | Notes |
|----------|---------|-------|
| Node.js | 18+ | Full support |
| Browsers | Modern | Chrome, Firefox, Safari, Edge |
| Deno | 1.0+ | Full support |
| Bun | 1.0+ | Full support |
### Browser Usage
```html
<script type="module">
import { encode, decode } from 'https://esm.sh/bencodec';
const encoded = encode({ hello: 'world' });
console.log(decode(encoded, { stringify: true }));
</script>
```
### Deno Usage
```typescript
import { encode, decode } from 'npm:bencodec';
const encoded = encode({ hello: 'world' });
console.log(decode(encoded, { stringify: true }));
```
## Type Definitions
Full TypeScript support with exported types:
```typescript
import type {
IBencodecOptions,
IBencodeEncodeOptions,
BencodeDecodedValue,
BencodeEncodableValue,
ByteEncoding
} from 'bencodec';
```
## Non-Standard Behaviors
For maximum compatibility, bencodec handles some edge cases beyond the strict spec:
| Behavior | Description |
|----------|-------------|
| Plus sign in integers | Leading `+` is silently ignored (`i+42e` -> `42`) |
| Float truncation | Decimal numbers truncated toward zero |
| Boolean encoding | Booleans encoded as integers (`true` -> `i1e`) |
| Null/undefined | Silently skipped in lists and dictionaries |
## License
[MIT](LICENSE.md)