bt-bencode
A bencode codec that keeps bytes as bytes, plus a reader for BitTorrent v1, v2 and hybrid torrents.
Install
npm install bt-bencode
Development
npm test # node --test, runs .ts sources directly (Node >= 22.18)
npm run typecheck
npm run build # emits ESM + .d.ts to dist/
Layout
src/bencode/ byte-faithful bencode codec; knows nothing about torrents
bdict.ts BDict + the Bencode type
decode.ts encode.ts
inspect.ts read-only render for debugging
brand.ts the tag that keeps a render out of a builder
errors.ts DecodeError, TypeMismatchError
src/torrent/ BEP 3/12/47/52 knowledge
schema.ts known keys, their types and scopes
torrent.ts typed read-only view over a decoded torrent
infohash.ts hashing a dictionary you built or modified
errors.ts InvalidTorrentError
The codec layer never imports the torrent layer.
inspect() output is one-way: it is branded, so a debug render cannot be fed
back into BDict.from() at any nesting depth, and encode() does not accept
it. A value mangled for display cannot round trip into a file.
Torrent is a different thing -- a read-only projection over a live,
mutable BDict. Mutating torrent.raw and re-encoding is the intended way to
modify a torrent. Infohashes are computed from the source bytes, so they do not
track such mutations; use infoHashV1(info) / infoHashV2(info) to hash a
dictionary you have changed.
Test fixtures
The fixtures in src/__fixtures__/ are generated, not hand-authored, and
regenerating them requires only Docker:
npm run fixtures # regenerate + verify against libtorrent
npm run fixtures:generate # regenerate only
Both generation and verification run in the container defined by
docker/Dockerfile, so output is byte-identical across machines and nothing is
installed on the host. scripts/make-fixtures.py uses the Python standard
library only and is deliberately independent of this package — fixtures built
with our own encoder would inherit our own bugs and the roundtrip tests would
pass vacuously.
The source tree is synthesised from a hash keystream rather than committed, and file sizes are chosen so each fixture exercises a distinct branch:
| File | Size | Exercises |
|---|---|---|
a.bin |
96 KiB | 3 whole pieces; no padding file in the hybrid torrent |
b.txt |
1 KiB | under one piece, so it is omitted from piece layers |
empty.bin |
0 | no pieces root at all — the case that breaks a type requiring one |
sub/c.bin |
48 KiB | 1.5 pieces, so the final piece subtree needs zero-padding |
Six fixtures are produced: v1, v2 and hybrid over that tree, plus
single-v1, single-v2 and single-hybrid — single-file torrents use
info.length instead of info.files and are probably the most common shape in
existence.
Verification also writes src/__fixtures__/expected.json, holding libtorrent's
own infohashes for every fixture. Nothing else in the repo knows what those
should be, so without an independently derived oracle an off-by-one in slicing
the info dictionary would go unnoticed.
Piece length is 32 KiB — two 16 KiB blocks per piece, so v2 piece subtrees have real structure. At the 16 KiB minimum the merkle tree collapses into a flat list and tests nothing.
Verification (scripts/verify-fixtures.py) checks each fixture twice against
libtorrent 2.x: parsing validates piece layers against each file's pieces root, and a forced recheck against the real source tree validates pieces root
for files smaller than one piece, which have no piece layer to cross-check
against. BEP 52 does not state whether a sub-piece file's leaf count rounds to
the next power of two or to a full piece, so that reading is confirmed
empirically rather than assumed.