UNPKG

@thi.ng/bencode

Version:

Bencode binary encoder / decoder with optional UTF8 encoding & floating point support

142 lines (141 loc) 3.67 kB
import { peek } from "@thi.ng/arrays/peek"; import { assert } from "@thi.ng/errors/assert"; import { illegalState } from "@thi.ng/errors/illegal-state"; import { utf8Decode } from "@thi.ng/transducers-binary/utf8"; const [MINUS, DOT, ZERO, NINE, COLON, DICT, END, FLOAT, INT, LIST] = [ 45, 46, 48, 57, 58, 100, 101, 102, 105, 108 ]; const decode = (buf, utf8 = true) => { const ctx = { i: 0, n: buf.length, buf }; const stack = []; let x; while (ctx.i < buf.length) { x = buf[ctx.i++]; switch (x) { case DICT: __ensureNotKey(stack, "dict"); stack.push({ type: DICT, val: {} }); break; case LIST: __ensureNotKey(stack, "list"); stack.push({ type: LIST, val: [] }); break; case INT: x = __collect(stack, __readInt(ctx, 0)); if (x !== void 0) { return x; } break; case FLOAT: x = __collect(stack, __readFloat(ctx)); if (x !== void 0) { return x; } break; case END: x = stack.pop(); if (x) { const parent = peek(stack); if (parent) { if (parent.type === LIST) { parent.val.push(x.val); } else if (parent.type === DICT) { parent.val[parent.key] = x.val; parent.key = null; } } else { return x.val; } } else { illegalState("unmatched end literal"); } break; default: if (x >= ZERO && x <= NINE) { x = __readBytes(ctx, __readInt(ctx, x - ZERO, COLON)); x = __collect(stack, x, utf8); if (x !== void 0) { return x; } } else { illegalState(`unexpected value type: 0x${x.toString(16)}`); } } } return peek(stack).val; }; const __ensureNotKey = (stack, type) => { const x = peek(stack); assert(!x || x.type !== DICT || x.key, type + " not supported as dict key"); }; const __collect = (stack, x, utf8 = false) => { const parent = peek(stack); if (!parent) return x; utf8 &&= x instanceof Uint8Array; if (parent.type === LIST) { parent.val.push(utf8 ? utf8Decode(x) : x); } else { if (!parent.key) { parent.key = utf8 ? utf8Decode(x) : __decodeAscii(x); } else { parent.val[parent.key] = utf8 ? utf8Decode(x) : x; parent.key = null; } } }; const __readInt = (ctx, acc, end = END) => { let x; let isSigned = false; while (ctx.i < ctx.n) { x = ctx.buf[ctx.i++]; if (x >= ZERO && x <= NINE) { acc = acc * 10 + x - ZERO; } else if (x === MINUS) { assert(!isSigned, `invalid int literal`); isSigned = true; } else if (x === end) { return isSigned ? -acc : acc; } else { illegalState(`expected digit, got 0x${x.toString(16)}`); } } illegalState(`incomplete int`); }; const __readFloat = (ctx) => { let x; let acc = ""; while (ctx.i < ctx.n) { x = ctx.buf[ctx.i++]; if (x >= ZERO && x <= NINE || x === DOT || x === MINUS) { acc += String.fromCharCode(x); } else if (x === END) { return +acc; } else { illegalState(`expected digit or dot, got 0x${x.toString(16)}`); } } illegalState(`incomplete float`); }; const __readBytes = (ctx, len) => { if (ctx.i + len > ctx.n) illegalState(`expected ${len} bytes, but reached EOF`); return ctx.buf.subarray(ctx.i, ctx.i += len); }; const __decodeAscii = (buf) => { let res = ""; for (let i = 0, n = buf.length; i < n; i++) res += String.fromCharCode(buf[i]); return res; }; export { decode };