Skip to content

Commit 0838b13

Browse files
authored
Merge pull request #563 from streamich/bencode
Bencode
2 parents e4f60dd + 0eb397b commit 0838b13

File tree

7 files changed

+856
-0
lines changed

7 files changed

+856
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {Reader} from '../../util/buffers/Reader';
2+
import type {BinaryJsonDecoder, PackValue} from '../types';
3+
4+
export class BencodeDecoder implements BinaryJsonDecoder {
5+
public reader = new Reader();
6+
7+
public read(uint8: Uint8Array): unknown {
8+
this.reader.reset(uint8);
9+
return this.readAny();
10+
}
11+
12+
public decode(uint8: Uint8Array): unknown {
13+
this.reader.reset(uint8);
14+
return this.readAny();
15+
}
16+
17+
public readAny(): unknown {
18+
const reader = this.reader;
19+
const x = reader.x;
20+
const uint8 = reader.uint8;
21+
const char = uint8[x];
22+
switch (char) {
23+
case 0x69: // i
24+
return this.readNum();
25+
case 0x64: // d
26+
return this.readObj();
27+
case 0x6c: // l
28+
return this.readArr();
29+
case 0x66: // f
30+
return this.readFalse();
31+
case 0x74: // t
32+
return this.readTrue();
33+
case 110: // n
34+
return this.readNull();
35+
case 117: // u
36+
return this.readUndef();
37+
default:
38+
if (char >= 48 && char <= 57) return this.readBin();
39+
}
40+
throw new Error('INVALID_BENCODE');
41+
}
42+
43+
public readNull(): null {
44+
if (this.reader.u8() !== 0x6e) throw new Error('INVALID_BENCODE');
45+
return null;
46+
}
47+
48+
public readUndef(): undefined {
49+
if (this.reader.u8() !== 117) throw new Error('INVALID_BENCODE');
50+
return undefined;
51+
}
52+
53+
public readTrue(): true {
54+
if (this.reader.u8() !== 0x74) throw new Error('INVALID_BENCODE');
55+
return true;
56+
}
57+
58+
public readFalse(): false {
59+
if (this.reader.u8() !== 0x66) throw new Error('INVALID_BENCODE');
60+
return false;
61+
}
62+
63+
public readBool(): unknown {
64+
const reader = this.reader;
65+
switch (reader.uint8[reader.x]) {
66+
case 0x66: // f
67+
return this.readFalse();
68+
case 0x74: // t
69+
return this.readTrue();
70+
default:
71+
throw new Error('INVALID_BENCODE');
72+
}
73+
}
74+
75+
public readNum(): number {
76+
const reader = this.reader;
77+
const startChar = reader.u8();
78+
if (startChar !== 0x69) throw new Error('INVALID_BENCODE');
79+
const u8 = reader.uint8;
80+
let x = reader.x;
81+
let numStr = '';
82+
let c = u8[x++];
83+
let i = 0;
84+
while (c !== 0x65) {
85+
numStr += String.fromCharCode(c);
86+
c = u8[x++];
87+
if (i > 25) throw new Error('INVALID_BENCODE');
88+
i++;
89+
}
90+
if (!numStr) throw new Error('INVALID_BENCODE');
91+
reader.x = x;
92+
return +numStr;
93+
}
94+
95+
public readStr(): string {
96+
const bin = this.readBin();
97+
return new TextDecoder().decode(bin);
98+
}
99+
100+
public readBin(): Uint8Array {
101+
const reader = this.reader;
102+
const u8 = reader.uint8;
103+
let lenStr = '';
104+
let x = reader.x;
105+
let c = u8[x++];
106+
let i = 0;
107+
while (c !== 0x3a) {
108+
if (c < 48 || c > 57) throw new Error('INVALID_BENCODE');
109+
lenStr += String.fromCharCode(c);
110+
c = u8[x++];
111+
if (i > 10) throw new Error('INVALID_BENCODE');
112+
i++;
113+
}
114+
reader.x = x;
115+
const len = +lenStr;
116+
const bin = reader.buf(len);
117+
return bin;
118+
}
119+
120+
public readArr(): unknown[] {
121+
const reader = this.reader;
122+
if (reader.u8() !== 0x6c) throw new Error('INVALID_BENCODE');
123+
const arr: unknown[] = [];
124+
const uint8 = reader.uint8;
125+
while (true) {
126+
const char = uint8[reader.x];
127+
if (char === 0x65) {
128+
reader.x++;
129+
return arr;
130+
}
131+
arr.push(this.readAny());
132+
}
133+
}
134+
135+
public readObj(): PackValue | Record<string, unknown> | unknown {
136+
const reader = this.reader;
137+
if (reader.u8() !== 0x64) throw new Error('INVALID_BENCODE');
138+
const obj: Record<string, unknown> = {};
139+
const uint8 = reader.uint8;
140+
while (true) {
141+
const char = uint8[reader.x];
142+
if (char === 0x65) {
143+
reader.x++;
144+
return obj;
145+
}
146+
const key = this.readStr();
147+
if (key === '__proto__') throw new Error('INVALID_KEY');
148+
obj[key] = this.readAny();
149+
}
150+
}
151+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import {utf8Size} from '../../util/strings/utf8';
2+
import {sort} from '../../util/sort/insertion';
3+
import type {IWriter, IWriterGrowable} from '../../util/buffers';
4+
import type {BinaryJsonEncoder} from '../types';
5+
6+
export class BencodeEncoder implements BinaryJsonEncoder {
7+
constructor(public readonly writer: IWriter & IWriterGrowable) {}
8+
9+
public encode(value: unknown): Uint8Array {
10+
const writer = this.writer;
11+
writer.reset();
12+
this.writeAny(value);
13+
return writer.flush();
14+
}
15+
16+
/**
17+
* Called when the encoder encounters a value that it does not know how to encode.
18+
*
19+
* @param value Some JavaScript value.
20+
*/
21+
public writeUnknown(value: unknown): void {
22+
this.writeNull();
23+
}
24+
25+
public writeAny(value: unknown): void {
26+
switch (typeof value) {
27+
case 'boolean':
28+
return this.writeBoolean(value);
29+
case 'number':
30+
return this.writeNumber(value as number);
31+
case 'string':
32+
return this.writeStr(value);
33+
case 'object': {
34+
if (value === null) return this.writeNull();
35+
const constructor = value.constructor;
36+
switch (constructor) {
37+
case Object:
38+
return this.writeObj(value as Record<string, unknown>);
39+
case Array:
40+
return this.writeArr(value as unknown[]);
41+
case Uint8Array:
42+
return this.writeBin(value as Uint8Array);
43+
case Map:
44+
return this.writeMap(value as Map<unknown, unknown>);
45+
case Set:
46+
return this.writeSet(value as Set<unknown>);
47+
default:
48+
return this.writeUnknown(value);
49+
}
50+
}
51+
case 'bigint': {
52+
return this.writeBigint(value);
53+
}
54+
case 'undefined': {
55+
return this.writeUndef();
56+
}
57+
default:
58+
return this.writeUnknown(value);
59+
}
60+
}
61+
62+
public writeNull(): void {
63+
this.writer.u8(110); // 'n'
64+
}
65+
66+
public writeUndef(): void {
67+
this.writer.u8(117); // 'u'
68+
}
69+
70+
public writeBoolean(bool: boolean): void {
71+
this.writer.u8(bool ? 0x74 : 0x66); // 't' or 'f'
72+
}
73+
74+
public writeNumber(num: number): void {
75+
const writer = this.writer;
76+
writer.u8(0x69); // 'i'
77+
writer.ascii(Math.round(num) + '');
78+
writer.u8(0x65); // 'e'
79+
}
80+
81+
public writeInteger(int: number): void {
82+
const writer = this.writer;
83+
writer.u8(0x69); // 'i'
84+
writer.ascii(int + '');
85+
writer.u8(0x65); // 'e'
86+
}
87+
88+
public writeUInteger(uint: number): void {
89+
this.writeInteger(uint);
90+
}
91+
92+
public writeFloat(float: number): void {
93+
this.writeNumber(float);
94+
}
95+
96+
public writeBigint(int: bigint): void {
97+
const writer = this.writer;
98+
writer.u8(0x69); // 'i'
99+
writer.ascii(int + '');
100+
writer.u8(0x65); // 'e'
101+
}
102+
103+
public writeBin(buf: Uint8Array): void {
104+
const writer = this.writer;
105+
const length = buf.length;
106+
writer.ascii(length + '');
107+
writer.u8(0x3a); // ':'
108+
writer.buf(buf, length);
109+
}
110+
111+
public writeStr(str: string): void {
112+
const writer = this.writer;
113+
const length = utf8Size(str);
114+
writer.ascii(length + '');
115+
writer.u8(0x3a); // ':'
116+
writer.ensureCapacity(length);
117+
writer.utf8(str);
118+
}
119+
120+
public writeAsciiStr(str: string): void {
121+
const writer = this.writer;
122+
writer.ascii(str.length + '');
123+
writer.u8(0x3a); // ':'
124+
writer.ascii(str);
125+
}
126+
127+
public writeArr(arr: unknown[]): void {
128+
const writer = this.writer;
129+
writer.u8(0x6c); // 'l'
130+
const length = arr.length;
131+
for (let i = 0; i < length; i++) this.writeAny(arr[i]);
132+
writer.u8(0x65); // 'e'
133+
}
134+
135+
public writeObj(obj: Record<string, unknown>): void {
136+
const writer = this.writer;
137+
writer.u8(0x64); // 'd'
138+
const keys = sort(Object.keys(obj));
139+
const length = keys.length;
140+
for (let i = 0; i < length; i++) {
141+
const key = keys[i];
142+
this.writeStr(key);
143+
this.writeAny(obj[key]);
144+
}
145+
writer.u8(0x65); // 'e'
146+
}
147+
148+
public writeMap(obj: Map<unknown, unknown>): void {
149+
const writer = this.writer;
150+
writer.u8(0x64); // 'd'
151+
const keys = sort([...obj.keys()]);
152+
const length = keys.length;
153+
for (let i = 0; i < length; i++) {
154+
const key = keys[i];
155+
this.writeStr(key + '');
156+
this.writeAny(obj.get(key));
157+
}
158+
writer.u8(0x65); // 'e'
159+
}
160+
161+
public writeSet(set: Set<unknown>): void {
162+
this.writeArr([...set.values()]);
163+
}
164+
}

src/json-pack/bencode/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Bencode codecs
2+
3+
Implements [Bencode][bencode] encoder and decoder.
4+
5+
[bencode]: https://en.wikipedia.org/wiki/Bencode
6+
7+
Type coercion:
8+
9+
- Strings and `Uint8Array` are encoded as Bencode byte strings, decoded as `Uint8Array`.
10+
- `Object` and `Map` are encoded as Bencode dictionaries, decoded as `Object`.
11+
- `Array` and `Set` are encoded as Bencode lists, decoded as `Array`.
12+
- `number` and `bigint` are encoded as Bencode integers, decoded as `number`.
13+
- Float `number` are rounded and encoded as Bencode integers, decoded as `number`.
14+
15+
16+
## Extensions
17+
18+
This codec extends the Bencode specification to support the following types:
19+
20+
- `null` (encoded as `n`)
21+
- `undefined` (encoded as `u`)
22+
- `boolean` (encoded as `t` for `true` and `f` for `false`)

0 commit comments

Comments
 (0)