-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #563 from streamich/bencode
Bencode
- Loading branch information
Showing
7 changed files
with
856 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import {Reader} from '../../util/buffers/Reader'; | ||
import type {BinaryJsonDecoder, PackValue} from '../types'; | ||
|
||
export class BencodeDecoder implements BinaryJsonDecoder { | ||
public reader = new Reader(); | ||
|
||
public read(uint8: Uint8Array): unknown { | ||
this.reader.reset(uint8); | ||
return this.readAny(); | ||
} | ||
|
||
public decode(uint8: Uint8Array): unknown { | ||
this.reader.reset(uint8); | ||
return this.readAny(); | ||
} | ||
|
||
public readAny(): unknown { | ||
const reader = this.reader; | ||
const x = reader.x; | ||
const uint8 = reader.uint8; | ||
const char = uint8[x]; | ||
switch (char) { | ||
case 0x69: // i | ||
return this.readNum(); | ||
case 0x64: // d | ||
return this.readObj(); | ||
case 0x6c: // l | ||
return this.readArr(); | ||
case 0x66: // f | ||
return this.readFalse(); | ||
case 0x74: // t | ||
return this.readTrue(); | ||
case 110: // n | ||
return this.readNull(); | ||
case 117: // u | ||
return this.readUndef(); | ||
default: | ||
if (char >= 48 && char <= 57) return this.readBin(); | ||
} | ||
throw new Error('INVALID_BENCODE'); | ||
} | ||
|
||
public readNull(): null { | ||
if (this.reader.u8() !== 0x6e) throw new Error('INVALID_BENCODE'); | ||
return null; | ||
} | ||
|
||
public readUndef(): undefined { | ||
if (this.reader.u8() !== 117) throw new Error('INVALID_BENCODE'); | ||
return undefined; | ||
} | ||
|
||
public readTrue(): true { | ||
if (this.reader.u8() !== 0x74) throw new Error('INVALID_BENCODE'); | ||
return true; | ||
} | ||
|
||
public readFalse(): false { | ||
if (this.reader.u8() !== 0x66) throw new Error('INVALID_BENCODE'); | ||
return false; | ||
} | ||
|
||
public readBool(): unknown { | ||
const reader = this.reader; | ||
switch (reader.uint8[reader.x]) { | ||
case 0x66: // f | ||
return this.readFalse(); | ||
case 0x74: // t | ||
return this.readTrue(); | ||
default: | ||
throw new Error('INVALID_BENCODE'); | ||
} | ||
} | ||
|
||
public readNum(): number { | ||
const reader = this.reader; | ||
const startChar = reader.u8(); | ||
if (startChar !== 0x69) throw new Error('INVALID_BENCODE'); | ||
const u8 = reader.uint8; | ||
let x = reader.x; | ||
let numStr = ''; | ||
let c = u8[x++]; | ||
let i = 0; | ||
while (c !== 0x65) { | ||
numStr += String.fromCharCode(c); | ||
c = u8[x++]; | ||
if (i > 25) throw new Error('INVALID_BENCODE'); | ||
i++; | ||
} | ||
if (!numStr) throw new Error('INVALID_BENCODE'); | ||
reader.x = x; | ||
return +numStr; | ||
} | ||
|
||
public readStr(): string { | ||
const bin = this.readBin(); | ||
return new TextDecoder().decode(bin); | ||
} | ||
|
||
public readBin(): Uint8Array { | ||
const reader = this.reader; | ||
const u8 = reader.uint8; | ||
let lenStr = ''; | ||
let x = reader.x; | ||
let c = u8[x++]; | ||
let i = 0; | ||
while (c !== 0x3a) { | ||
if (c < 48 || c > 57) throw new Error('INVALID_BENCODE'); | ||
lenStr += String.fromCharCode(c); | ||
c = u8[x++]; | ||
if (i > 10) throw new Error('INVALID_BENCODE'); | ||
i++; | ||
} | ||
reader.x = x; | ||
const len = +lenStr; | ||
const bin = reader.buf(len); | ||
return bin; | ||
} | ||
|
||
public readArr(): unknown[] { | ||
const reader = this.reader; | ||
if (reader.u8() !== 0x6c) throw new Error('INVALID_BENCODE'); | ||
const arr: unknown[] = []; | ||
const uint8 = reader.uint8; | ||
while (true) { | ||
const char = uint8[reader.x]; | ||
if (char === 0x65) { | ||
reader.x++; | ||
return arr; | ||
} | ||
arr.push(this.readAny()); | ||
} | ||
} | ||
|
||
public readObj(): PackValue | Record<string, unknown> | unknown { | ||
const reader = this.reader; | ||
if (reader.u8() !== 0x64) throw new Error('INVALID_BENCODE'); | ||
const obj: Record<string, unknown> = {}; | ||
const uint8 = reader.uint8; | ||
while (true) { | ||
const char = uint8[reader.x]; | ||
if (char === 0x65) { | ||
reader.x++; | ||
return obj; | ||
} | ||
const key = this.readStr(); | ||
if (key === '__proto__') throw new Error('INVALID_KEY'); | ||
obj[key] = this.readAny(); | ||
} | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import {utf8Size} from '../../util/strings/utf8'; | ||
import {sort} from '../../util/sort/insertion'; | ||
import type {IWriter, IWriterGrowable} from '../../util/buffers'; | ||
import type {BinaryJsonEncoder} from '../types'; | ||
|
||
export class BencodeEncoder implements BinaryJsonEncoder { | ||
constructor(public readonly writer: IWriter & IWriterGrowable) {} | ||
|
||
public encode(value: unknown): Uint8Array { | ||
const writer = this.writer; | ||
writer.reset(); | ||
this.writeAny(value); | ||
return writer.flush(); | ||
} | ||
|
||
/** | ||
* Called when the encoder encounters a value that it does not know how to encode. | ||
* | ||
* @param value Some JavaScript value. | ||
*/ | ||
public writeUnknown(value: unknown): void { | ||
this.writeNull(); | ||
} | ||
|
||
public writeAny(value: unknown): void { | ||
switch (typeof value) { | ||
case 'boolean': | ||
return this.writeBoolean(value); | ||
case 'number': | ||
return this.writeNumber(value as number); | ||
case 'string': | ||
return this.writeStr(value); | ||
case 'object': { | ||
if (value === null) return this.writeNull(); | ||
const constructor = value.constructor; | ||
switch (constructor) { | ||
case Object: | ||
return this.writeObj(value as Record<string, unknown>); | ||
case Array: | ||
return this.writeArr(value as unknown[]); | ||
case Uint8Array: | ||
return this.writeBin(value as Uint8Array); | ||
case Map: | ||
return this.writeMap(value as Map<unknown, unknown>); | ||
case Set: | ||
return this.writeSet(value as Set<unknown>); | ||
default: | ||
return this.writeUnknown(value); | ||
} | ||
} | ||
case 'bigint': { | ||
return this.writeBigint(value); | ||
} | ||
case 'undefined': { | ||
return this.writeUndef(); | ||
} | ||
default: | ||
return this.writeUnknown(value); | ||
} | ||
} | ||
|
||
public writeNull(): void { | ||
this.writer.u8(110); // 'n' | ||
} | ||
|
||
public writeUndef(): void { | ||
this.writer.u8(117); // 'u' | ||
} | ||
|
||
public writeBoolean(bool: boolean): void { | ||
this.writer.u8(bool ? 0x74 : 0x66); // 't' or 'f' | ||
} | ||
|
||
public writeNumber(num: number): void { | ||
const writer = this.writer; | ||
writer.u8(0x69); // 'i' | ||
writer.ascii(Math.round(num) + ''); | ||
writer.u8(0x65); // 'e' | ||
} | ||
|
||
public writeInteger(int: number): void { | ||
const writer = this.writer; | ||
writer.u8(0x69); // 'i' | ||
writer.ascii(int + ''); | ||
writer.u8(0x65); // 'e' | ||
} | ||
|
||
public writeUInteger(uint: number): void { | ||
this.writeInteger(uint); | ||
} | ||
|
||
public writeFloat(float: number): void { | ||
this.writeNumber(float); | ||
} | ||
|
||
public writeBigint(int: bigint): void { | ||
const writer = this.writer; | ||
writer.u8(0x69); // 'i' | ||
writer.ascii(int + ''); | ||
writer.u8(0x65); // 'e' | ||
} | ||
|
||
public writeBin(buf: Uint8Array): void { | ||
const writer = this.writer; | ||
const length = buf.length; | ||
writer.ascii(length + ''); | ||
writer.u8(0x3a); // ':' | ||
writer.buf(buf, length); | ||
} | ||
|
||
public writeStr(str: string): void { | ||
const writer = this.writer; | ||
const length = utf8Size(str); | ||
writer.ascii(length + ''); | ||
writer.u8(0x3a); // ':' | ||
writer.ensureCapacity(length); | ||
writer.utf8(str); | ||
} | ||
|
||
public writeAsciiStr(str: string): void { | ||
const writer = this.writer; | ||
writer.ascii(str.length + ''); | ||
writer.u8(0x3a); // ':' | ||
writer.ascii(str); | ||
} | ||
|
||
public writeArr(arr: unknown[]): void { | ||
const writer = this.writer; | ||
writer.u8(0x6c); // 'l' | ||
const length = arr.length; | ||
for (let i = 0; i < length; i++) this.writeAny(arr[i]); | ||
writer.u8(0x65); // 'e' | ||
} | ||
|
||
public writeObj(obj: Record<string, unknown>): void { | ||
const writer = this.writer; | ||
writer.u8(0x64); // 'd' | ||
const keys = sort(Object.keys(obj)); | ||
const length = keys.length; | ||
for (let i = 0; i < length; i++) { | ||
const key = keys[i]; | ||
this.writeStr(key); | ||
this.writeAny(obj[key]); | ||
} | ||
writer.u8(0x65); // 'e' | ||
} | ||
|
||
public writeMap(obj: Map<unknown, unknown>): void { | ||
const writer = this.writer; | ||
writer.u8(0x64); // 'd' | ||
const keys = sort([...obj.keys()]); | ||
const length = keys.length; | ||
for (let i = 0; i < length; i++) { | ||
const key = keys[i]; | ||
this.writeStr(key + ''); | ||
this.writeAny(obj.get(key)); | ||
} | ||
writer.u8(0x65); // 'e' | ||
} | ||
|
||
public writeSet(set: Set<unknown>): void { | ||
this.writeArr([...set.values()]); | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Bencode codecs | ||
|
||
Implements [Bencode][bencode] encoder and decoder. | ||
|
||
[bencode]: https://en.wikipedia.org/wiki/Bencode | ||
|
||
Type coercion: | ||
|
||
- Strings and `Uint8Array` are encoded as Bencode byte strings, decoded as `Uint8Array`. | ||
- `Object` and `Map` are encoded as Bencode dictionaries, decoded as `Object`. | ||
- `Array` and `Set` are encoded as Bencode lists, decoded as `Array`. | ||
- `number` and `bigint` are encoded as Bencode integers, decoded as `number`. | ||
- Float `number` are rounded and encoded as Bencode integers, decoded as `number`. | ||
|
||
|
||
## Extensions | ||
|
||
This codec extends the Bencode specification to support the following types: | ||
|
||
- `null` (encoded as `n`) | ||
- `undefined` (encoded as `u`) | ||
- `boolean` (encoded as `t` for `true` and `f` for `false`) |
Oops, something went wrong.