torrent/src/core/bencode/encoder.ts

58 lines
1.9 KiB
TypeScript

/**
* Bencode encoder — serializes JS values to bencode bytes.
* Used primarily for UDP tracker connect/announce requests.
*/
/**
* Encoder-only type hierarchy — mirrors BencodeValue but adds `string` at every level.
* The *decoder* never returns strings (bencode byte-strings are always Buffer), so
* BencodeValue intentionally excludes string. The encoder is more permissive: callers
* can pass plain JS strings, and they are serialized as bencode byte-strings.
*/
export type EncodableDict = { [key: string]: EncodableValue };
export type EncodableList = EncodableValue[];
export type EncodableValue = number | string | Buffer | EncodableList | EncodableDict;
export function encode(value: EncodableValue): Buffer {
if (typeof value === "number") {
return Buffer.from(`i${Math.trunc(value)}e`, "ascii");
}
if (Buffer.isBuffer(value)) {
return Buffer.concat([Buffer.from(`${value.length}:`, "ascii"), value]);
}
if (typeof value === "string") {
const strBuf = Buffer.from(value, "utf8");
return Buffer.concat([Buffer.from(`${strBuf.length}:`, "ascii"), strBuf]);
}
if (Array.isArray(value)) {
return encodeList(value);
}
return encodeDict(value);
}
function encodeList(list: EncodableList): Buffer {
const parts: Buffer[] = [Buffer.from("l", "ascii")];
for (const item of list) {
parts.push(encode(item));
}
parts.push(Buffer.from("e", "ascii"));
return Buffer.concat(parts);
}
function encodeDict(dict: EncodableDict): Buffer {
const parts: Buffer[] = [Buffer.from("d", "ascii")];
// Keys must be sorted lexicographically
const keys = Object.keys(dict).sort();
for (const key of keys) {
const keyBuf = Buffer.from(key, "utf8");
parts.push(Buffer.from(`${keyBuf.length}:`, "ascii"), keyBuf);
parts.push(encode(dict[key]));
}
parts.push(Buffer.from("e", "ascii"));
return Buffer.concat(parts);
}