@thi.ng/bencode
Version:
Bencode binary encoder / decoder with optional UTF8 encoding & floating point support
142 lines (141 loc) • 3.67 kB
JavaScript
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
};