Skip to content

Instantly share code, notes, and snippets.

@kuczmama
Last active July 24, 2020 12:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kuczmama/c40ed2bebd34ca8e35a6fb809d022699 to your computer and use it in GitHub Desktop.
Save kuczmama/c40ed2bebd34ca8e35a6fb809d022699 to your computer and use it in GitHub Desktop.
A zero dependency bencode encoder and decoder in javascript
const encode = (data) => {
if(data == null) return null;
if(typeof data === 'number') {
return `i${data}e`;
}
if(typeof data === 'string') {
return `${data.length}:${data}`;
}
if(Array.isArray(data)) {
return `l${data.map(el => encode(el)).join('')}e`;
}
if(typeof data === 'object') {
return `d${Object.keys(data).sort().map((k) => `${encode(k)}${encode(data[k])}`).join('')}e`;
}
throw `Unable to encode ${data}`;
};
const decode = (str) => decodeHelper(str)[1];
const decodeHelper = (data) => {
if(data == null || data.length === 0) return [0, null];
let idx = 0;
// Integer
if(data[idx] === 'i') {
let curr = '';
while(data[++idx] !== 'e') curr = `${curr}${data[idx]}`;
return [curr.length + 2, Number(curr)]; // +2 for i & e
}
// Bytes
if (/\d/.test(data[idx])) {
let numBytes = data[idx];
while(data[++idx] != ':') numBytes = `${numBytes}${data[idx]}`;
const startPos = idx + 1;
const sol = data.substring(startPos, startPos + Number(numBytes));
return [startPos + Number(numBytes), sol]; //
}
// List
if(data[idx] === 'l') {
let cursor = 1;
const arr = [];
while(data[cursor] !== 'e') {
const [entryLength, entry] = decodeHelper(data.substring(cursor));
cursor += entryLength;
arr.push(entry);
}
return [cursor + 1, arr];
}
// Dictionary
if(data[idx] === 'd') {
let cursor = 1;
const obj = {};
while(data[cursor] !== 'e') {
const [keyLength, key] = decodeHelper(data.substring(cursor));
const [valueLength, value] = decodeHelper(data.substring(cursor + keyLength));
cursor += keyLength + valueLength;
obj[key] = value;
}
return [cursor + 1, obj];
}
return [0, null];
}
const assert = (actual, expected) => {
if(isEqual(expected, actual)) {
console.log(".");
} else {
console.log(`Expected: ${JSON.stringify(expected)}, but got: ${JSON.stringify(actual)}`);
}
}
const isEqual = (expected, actual) => {
if(expected === actual) return true;
if(typeof expected !== typeof actual) return false;
if(Array.isArray(expected) && Array.isArray(actual)) {
if(expected.length !== actual.length) return false;
let sol = true;
for(let i = 0; i < expected.length; i++) {
sol = sol && isEqual(expected[i], actual[i]);
}
return sol;
}
return JSON.stringify(expected) === JSON.stringify(actual);
}
const test = (data, encoded) => {
if(encoded == null) encoded = encode(data);
assert(encode(data), encoded);
assert(decode(encoded), data);
}
test(42, 'i42e');
test(0, 'i0e');
test(-42, 'i-42e');
test('spam', '4:spam');
test('abcdefghij', '10:abcdefghij');
test(['spam', 42], 'l4:spami42ee');
test({bar: 'spam', foo: 42}, 'd3:bar4:spam3:fooi42ee');
test({a: [1,2,[3]], o: 1});
test(null);
test([[],[],[],[[],[]]]);
let data = {
string: 'Hello World',
integer: 12345,
dict: {
key: 'This is a string within a dictionary'
},
list: [ 1, 2, 3, 4, 'string', 5, {} ]
};
assert(encode(data), 'd4:dictd3:key36:This is a string within a dictionarye7:integeri12345e4:listli1ei2ei3ei4e6:stringi5edee6:string11:Hello Worlde')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment