Skip to content

Commit

Permalink
Merge pull request #563 from streamich/bencode
Browse files Browse the repository at this point in the history
Bencode
  • Loading branch information
streamich authored Apr 5, 2024
2 parents e4f60dd + 0eb397b commit 0838b13
Show file tree
Hide file tree
Showing 7 changed files with 856 additions and 0 deletions.
151 changes: 151 additions & 0 deletions src/json-pack/bencode/BencodeDecoder.ts
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();
}
}
}
164 changes: 164 additions & 0 deletions src/json-pack/bencode/BencodeEncoder.ts
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()]);
}
}
22 changes: 22 additions & 0 deletions src/json-pack/bencode/README.md
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`)
Loading

0 comments on commit 0838b13

Please sign in to comment.