Last active
July 24, 2020 12:28
-
-
Save kuczmama/c40ed2bebd34ca8e35a6fb809d022699 to your computer and use it in GitHub Desktop.
A zero dependency bencode encoder and decoder in javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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