// BENCODE — the encoding of a .torrent file (BEP 3), decoded. // // Pure and dependency-free: the archive.org torrent fetch reads one file's // index and byte range out of an item's `_archive.torrent` // (lib/archiveOrgTorrent.ts), and that is all this is for. A byte string stays // a Buffer (a torrent's `pieces` is raw SHA-1 bytes, and a path may not be // UTF-8); a dictionary's keys are read as UTF-8 strings, which every key a // torrent defines is. // // STRICT where it is cheap: a truncated or malformed input throws // BencodeError naming the offset, rather than returning half a value. There is // no encoder — nothing here writes a torrent. export type BValue = number | Buffer | BValue[] | BDict; export type BDict = { [key: string]: BValue }; export class BencodeError extends Error { constructor(message: string, readonly offset: number) { super(`${message} at byte ${offset}`); this.name = "BencodeError"; } } const CH_I = 0x69; // i const CH_L = 0x6c; // l const CH_D = 0x64; // d const CH_E = 0x65; // e const CH_COLON = 0x3a; const CH_MINUS = 0x2d; const CH_0 = 0x30; const CH_9 = 0x39; // Nesting deeper than this is not a torrent; it is an attempt to blow the stack. const MAX_DEPTH = 64; export function bdecode(buf: Buffer): BValue { let pos = 0; function readInt(end: number): number { const start = pos; let neg = false; if (buf[pos] === CH_MINUS) { neg = true; pos++; } if (pos >= end) throw new BencodeError("empty integer", start); let n = 0; for (; pos < end; pos++) { const c = buf[pos]; if (c < CH_0 || c > CH_9) throw new BencodeError("bad digit", pos); n = n * 10 + (c - CH_0); } if (!Number.isSafeInteger(n)) throw new BencodeError("integer too large", start); return neg ? -n : n; } function indexOf(byte: number, from: number): number { const i = buf.indexOf(byte, from); if (i < 0) throw new BencodeError("unterminated value", from); return i; } function readBytes(): Buffer { const colon = indexOf(CH_COLON, pos); const len = readInt(colon); if (len < 0) throw new BencodeError("negative length", pos); pos = colon + 1; if (pos + len > buf.length) throw new BencodeError("string past end of input", pos); const out = buf.subarray(pos, pos + len); pos += len; return out; } function readValue(depth: number): BValue { if (depth > MAX_DEPTH) throw new BencodeError("nested too deeply", pos); if (pos >= buf.length) throw new BencodeError("unexpected end of input", pos); const c = buf[pos]; if (c === CH_I) { pos++; const end = indexOf(CH_E, pos); const n = readInt(end); pos = end + 1; return n; } if (c === CH_L) { pos++; const list: BValue[] = []; while (true) { if (pos >= buf.length) throw new BencodeError("unterminated list", pos); if (buf[pos] === CH_E) break; list.push(readValue(depth + 1)); } pos++; return list; } if (c === CH_D) { pos++; const dict: BDict = {}; while (true) { if (pos >= buf.length) throw new BencodeError("unterminated dictionary", pos); if (buf[pos] === CH_E) break; const key = readBytes().toString("utf8"); dict[key] = readValue(depth + 1); } pos++; return dict; } if (c >= CH_0 && c <= CH_9) return readBytes(); throw new BencodeError(`unexpected byte 0x${c.toString(16)}`, pos); } const value = readValue(0); if (pos !== buf.length) throw new BencodeError("trailing bytes", pos); return value; } // ─── Reading a decoded value ─── export function isDict(v: BValue | undefined): v is BDict { return !!v && typeof v === "object" && !Array.isArray(v) && !Buffer.isBuffer(v); } export function bString(v: BValue | undefined): string | undefined { return Buffer.isBuffer(v) ? v.toString("utf8") : undefined; } export function bInt(v: BValue | undefined): number | undefined { return typeof v === "number" ? v : undefined; } export function bList(v: BValue | undefined): BValue[] { return Array.isArray(v) ? v : []; }