@substrate-system/bencode
Version:
Bencode de/encoder
213 lines (172 loc) • 5.23 kB
text/typescript
import { arr2text, text2arr, arr2hex } from '@substrate-system/uint8-util'
const INTEGER_START = 0x69 // 'i'
const STRING_DELIM = 0x3A // ':'
const DICTIONARY_START = 0x64 // 'd'
const LIST_START = 0x6C // 'l'
const END_OF_TYPE = 0x65 // 'e'
export type Decoded =
| Record<string, any>
| Array<any>
| string
| number;
export interface Decoder {
(data:Uint8Array|string):Decoded|null
(data:Uint8Array|string, encoding:string):Decoded|null
(data:Uint8Array|string, start:number, encoding:string):Decoded|null
(data:Uint8Array|string, start:number, end:number, encoding:string):Decoded|null
data:Uint8Array|null;
bytes:number;
position:number;
encoding:string|null;
next:()=>Record<string, any>|Array<any>|Uint8Array|string|number|null;
dictionary:()=>Record<string, any>|null;
list:()=>any[];
buffer:()=>Uint8Array|string;
find:(ch:number)=>number|null;
integer:()=>number;
}
/**
* Decode bencoded data.
*
* @param {Uint8Array|string} data The buffer to decode
* @param {number} [start] Optional start index
* @param {number} [end] Optional end index
* @param {string} [encoding] Optional encoding type (utf8, etc)
* @return {Decoded}
*/
const decode:Decoder = function decode (
data:Uint8Array|string,
start?:number|string,
end?:number|string,
encoding?:string
):Decoded|null {
if (!data || data.length === 0) {
throw new Error('Missing data to decode.')
}
if (typeof start !== 'number' && encoding == null) {
encoding = start
start = undefined
}
if (typeof end !== 'number' && encoding == null) {
encoding = end
end = undefined
}
decode.position = 0
decode.encoding = encoding || null
decode.data = !(ArrayBuffer.isView(data)) ?
text2arr(data) :
new Uint8Array(data.slice(
start as number|undefined,
end as number|undefined)
)
decode.bytes = decode.data.length
return decode.next()
}
decode.bytes = 0
decode.position = 0
decode.data = null
decode.encoding = null
decode.next = function ():Record<string, any>|Array<any>|Uint8Array|string|number|null {
switch (decode.data![decode.position]) {
case DICTIONARY_START:
return decode.dictionary()
case LIST_START:
return decode.list()
case INTEGER_START:
return decode.integer()
default:
return decode.buffer()
}
}
decode.find = function (chr:number):number|null {
if (!decode.data?.length) return null
let i = decode.position
const c = decode.data.length
const d = decode.data
while (i < c) {
if (d[i] === chr) return i
i++
}
throw new Error(
'Invalid data: Missing delimiter "' +
String.fromCharCode(chr) + '" [0x' +
chr.toString(16) + ']'
)
}
decode.dictionary = function ():Record<string, any>|null {
if (!decode.data) return null
decode.position++
const dict = {}
while (decode.data[decode.position] !== END_OF_TYPE) {
const buffer = decode.buffer()
if (typeof buffer === 'string') {
dict[buffer] = decode.next()
continue
}
let key = arr2text(buffer)
if (key.includes('\uFFFD')) key = arr2hex(buffer)
dict[key] = decode.next()
}
decode.position++
return dict
}
decode.list = function ():any[] {
decode.position++
const lst:any[] = []
while (decode.data![decode.position] !== END_OF_TYPE) {
lst.push(decode.next())
}
decode.position++
return lst
}
decode.integer = function () {
const end = decode.find(END_OF_TYPE)
const number = getIntFromBuffer(decode.data, decode.position + 1, end)
if (!end) throw new Error('not end')
decode.position += end + 1 - decode.position
return number
}
decode.buffer = function ():Uint8Array|string {
const sep = decode.find(STRING_DELIM)
const newIndex = (sep || 0) + 1
const length = getIntFromBuffer(decode.data, decode.position, sep)
const end = newIndex + length
decode.position = end
return decode.encoding ?
arr2text(decode.data!.slice(newIndex, end)) :
decode.data!.slice(newIndex, end)
}
export default decode
/**
* replaces parseInt(buffer.toString('ascii', start, end)).
* For strings with less then ~30 charachters, this is actually a lot faster.
*
* @param {Uint8Array} data
* @param {Number} start
* @param {Number} end
* @return {Number} calculated number
*/
function getIntFromBuffer (buffer, start, end) {
let sum = 0
let sign = 1
for (let i = start; i < end; i++) {
const num = buffer[i]
if (num < 58 && num >= 48) {
sum = sum * 10 + (num - 48)
continue
}
if (i === start && num === 43) { // +
continue
}
if (i === start && num === 45) { // -
sign = -1
continue
}
if (num === 46) { // .
// its a float. break here.
break
}
throw new Error('not a number: buffer[' + i + '] = ' + num)
}
return sum * sign
}