@thi.ng/bencode 2.1.148 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +10 -2
- package/decode.d.ts +12 -1
- package/decode.js +56 -74
- package/encode.js +26 -52
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-01-
|
|
3
|
+
- **Last updated**: 2025-01-14T12:23:33Z
|
|
4
4
|
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
|
|
5
5
|
|
|
6
6
|
All notable changes to this project will be documented in this file.
|
|
@@ -9,6 +9,21 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
|
|
|
9
9
|
**Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
|
|
10
10
|
and/or version bumps of transitive dependencies.
|
|
11
11
|
|
|
12
|
+
# [3.0.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/bencode@3.0.0) (2025-01-14)
|
|
13
|
+
|
|
14
|
+
#### 🛑 Breaking changes
|
|
15
|
+
|
|
16
|
+
- major internal refactor & MUCH faster decode ([ce6d528](https://github.com/thi-ng/umbrella/commit/ce6d528))
|
|
17
|
+
- BREAKING CHANGE: `decode()` now requires `Uint8Array` as input
|
|
18
|
+
- update `decode()` & `encode()` internals
|
|
19
|
+
- update non-UTF-8 decoding (also add note to readme)
|
|
20
|
+
- avoid use of iterators in `decode()`
|
|
21
|
+
- update `__readBytes()` to return sub-arrays (zero copy op)
|
|
22
|
+
- remove [@thi.ng/defmulti](https://github.com/thi-ng/umbrella/tree/main/packages/defmulti) dependency
|
|
23
|
+
- remove internal use of `const enum`s
|
|
24
|
+
- update tests
|
|
25
|
+
- update readme
|
|
26
|
+
|
|
12
27
|
### [2.1.128](https://github.com/thi-ng/umbrella/tree/@thi.ng/bencode@2.1.128) (2024-07-22)
|
|
13
28
|
|
|
14
29
|
#### 🩹 Bug fixes
|
package/README.md
CHANGED
|
@@ -43,6 +43,12 @@ All JS strings will be UTF-8 encoded. To write raw bytes without
|
|
|
43
43
|
transformation, wrap them as `Uint8Array`. These too will be written as
|
|
44
44
|
Bencode strings (e.g. `len:xxx...`), but are used as is.
|
|
45
45
|
|
|
46
|
+
When decoding, UTF-8 is used by default, but can be disabled by passing `false`
|
|
47
|
+
as 2nd argument to
|
|
48
|
+
[`decode()`](https://docs.thi.ng/umbrella/bencode/functions/decode.html). In
|
|
49
|
+
that case, strings will be decoded as `Uint8Array`s, with the exception of
|
|
50
|
+
dictionary keys, which will be decoded via `String.fromCharCode()`.
|
|
51
|
+
|
|
46
52
|
#### Floating point values
|
|
47
53
|
|
|
48
54
|
This implementation has optional (non-standard) support for floating point
|
|
@@ -86,18 +92,20 @@ For Node.js REPL:
|
|
|
86
92
|
const bc = await import("@thi.ng/bencode");
|
|
87
93
|
```
|
|
88
94
|
|
|
89
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 1.
|
|
95
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 1.22 KB
|
|
90
96
|
|
|
91
97
|
## Dependencies
|
|
92
98
|
|
|
99
|
+
- [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
|
|
93
100
|
- [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
|
|
94
101
|
- [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
|
|
95
|
-
- [@thi.ng/defmulti](https://github.com/thi-ng/umbrella/tree/develop/packages/defmulti)
|
|
96
102
|
- [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
|
|
97
103
|
- [@thi.ng/strings](https://github.com/thi-ng/umbrella/tree/develop/packages/strings)
|
|
98
104
|
- [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/develop/packages/transducers)
|
|
99
105
|
- [@thi.ng/transducers-binary](https://github.com/thi-ng/umbrella/tree/develop/packages/transducers-binary)
|
|
100
106
|
|
|
107
|
+
Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
|
|
108
|
+
|
|
101
109
|
## API
|
|
102
110
|
|
|
103
111
|
[Generated API docs](https://docs.thi.ng/umbrella/bencode/)
|
package/decode.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Decodes given byte array back into JS data(structure).
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* UTF-8 is used by default, but can be disabled by setting `utf8` to false. In
|
|
6
|
+
* that case, strings will be decoded as `Uint8Array`s, with the exception of
|
|
7
|
+
* dictionary keys, which will be decoded via `String.fromCharCode()`.
|
|
8
|
+
*
|
|
9
|
+
* @param buf
|
|
10
|
+
* @param utf8
|
|
11
|
+
*/
|
|
12
|
+
export declare const decode: (buf: Uint8Array, utf8?: boolean) => any;
|
|
2
13
|
//# sourceMappingURL=decode.d.ts.map
|
package/decode.js
CHANGED
|
@@ -1,66 +1,54 @@
|
|
|
1
1
|
import { peek } from "@thi.ng/arrays/peek";
|
|
2
|
-
import { isArray } from "@thi.ng/checks/is-array";
|
|
3
2
|
import { assert } from "@thi.ng/errors/assert";
|
|
4
3
|
import { illegalState } from "@thi.ng/errors/illegal-state";
|
|
5
4
|
import { utf8Decode } from "@thi.ng/transducers-binary/utf8";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Lit2[Lit2["ZERO"] = 48] = "ZERO";
|
|
19
|
-
Lit2[Lit2["NINE"] = 57] = "NINE";
|
|
20
|
-
Lit2[Lit2["COLON"] = 58] = "COLON";
|
|
21
|
-
Lit2[Lit2["DICT"] = 100] = "DICT";
|
|
22
|
-
Lit2[Lit2["END"] = 101] = "END";
|
|
23
|
-
Lit2[Lit2["FLOAT"] = 102] = "FLOAT";
|
|
24
|
-
Lit2[Lit2["INT"] = 105] = "INT";
|
|
25
|
-
Lit2[Lit2["LIST"] = 108] = "LIST";
|
|
26
|
-
return Lit2;
|
|
27
|
-
})(Lit || {});
|
|
5
|
+
const [MINUS, DOT, ZERO, NINE, COLON, DICT, END, FLOAT, INT, LIST] = [
|
|
6
|
+
45,
|
|
7
|
+
46,
|
|
8
|
+
48,
|
|
9
|
+
57,
|
|
10
|
+
58,
|
|
11
|
+
100,
|
|
12
|
+
101,
|
|
13
|
+
102,
|
|
14
|
+
105,
|
|
15
|
+
108
|
|
16
|
+
];
|
|
28
17
|
const decode = (buf, utf8 = true) => {
|
|
29
|
-
const
|
|
18
|
+
const ctx = { i: 0, n: buf.length, buf };
|
|
30
19
|
const stack = [];
|
|
31
|
-
let i;
|
|
32
20
|
let x;
|
|
33
|
-
while (
|
|
34
|
-
x = i
|
|
21
|
+
while (ctx.i < buf.length) {
|
|
22
|
+
x = buf[ctx.i++];
|
|
35
23
|
switch (x) {
|
|
36
|
-
case
|
|
24
|
+
case DICT:
|
|
37
25
|
__ensureNotKey(stack, "dict");
|
|
38
|
-
stack.push({ type:
|
|
26
|
+
stack.push({ type: DICT, val: {} });
|
|
39
27
|
break;
|
|
40
|
-
case
|
|
28
|
+
case LIST:
|
|
41
29
|
__ensureNotKey(stack, "list");
|
|
42
|
-
stack.push({ type:
|
|
30
|
+
stack.push({ type: LIST, val: [] });
|
|
43
31
|
break;
|
|
44
|
-
case
|
|
45
|
-
x = __collect(stack, __readInt(
|
|
32
|
+
case INT:
|
|
33
|
+
x = __collect(stack, __readInt(ctx, 0));
|
|
46
34
|
if (x !== void 0) {
|
|
47
35
|
return x;
|
|
48
36
|
}
|
|
49
37
|
break;
|
|
50
|
-
case
|
|
51
|
-
x = __collect(stack, __readFloat(
|
|
38
|
+
case FLOAT:
|
|
39
|
+
x = __collect(stack, __readFloat(ctx));
|
|
52
40
|
if (x !== void 0) {
|
|
53
41
|
return x;
|
|
54
42
|
}
|
|
55
43
|
break;
|
|
56
|
-
case
|
|
44
|
+
case END:
|
|
57
45
|
x = stack.pop();
|
|
58
46
|
if (x) {
|
|
59
47
|
const parent = peek(stack);
|
|
60
48
|
if (parent) {
|
|
61
|
-
if (parent.type ===
|
|
49
|
+
if (parent.type === LIST) {
|
|
62
50
|
parent.val.push(x.val);
|
|
63
|
-
} else if (parent.type ===
|
|
51
|
+
} else if (parent.type === DICT) {
|
|
64
52
|
parent.val[parent.key] = x.val;
|
|
65
53
|
parent.key = null;
|
|
66
54
|
}
|
|
@@ -72,19 +60,14 @@ const decode = (buf, utf8 = true) => {
|
|
|
72
60
|
}
|
|
73
61
|
break;
|
|
74
62
|
default:
|
|
75
|
-
if (x >=
|
|
76
|
-
x = __readBytes(
|
|
77
|
-
iter,
|
|
78
|
-
__readInt(iter, x - 48 /* ZERO */, 58 /* COLON */)
|
|
79
|
-
);
|
|
63
|
+
if (x >= ZERO && x <= NINE) {
|
|
64
|
+
x = __readBytes(ctx, __readInt(ctx, x - ZERO, COLON));
|
|
80
65
|
x = __collect(stack, x, utf8);
|
|
81
66
|
if (x !== void 0) {
|
|
82
67
|
return x;
|
|
83
68
|
}
|
|
84
69
|
} else {
|
|
85
|
-
illegalState(
|
|
86
|
-
`unexpected value type: 0x${i.value.toString(16)}`
|
|
87
|
-
);
|
|
70
|
+
illegalState(`unexpected value type: 0x${x.toString(16)}`);
|
|
88
71
|
}
|
|
89
72
|
}
|
|
90
73
|
}
|
|
@@ -92,34 +75,31 @@ const decode = (buf, utf8 = true) => {
|
|
|
92
75
|
};
|
|
93
76
|
const __ensureNotKey = (stack, type) => {
|
|
94
77
|
const x = peek(stack);
|
|
95
|
-
assert(
|
|
96
|
-
!x || x.type !== 4 /* DICT */ || x.key,
|
|
97
|
-
type + " not supported as dict key"
|
|
98
|
-
);
|
|
78
|
+
assert(!x || x.type !== DICT || x.key, type + " not supported as dict key");
|
|
99
79
|
};
|
|
100
80
|
const __collect = (stack, x, utf8 = false) => {
|
|
101
81
|
const parent = peek(stack);
|
|
102
82
|
if (!parent) return x;
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
utf8 &&= x instanceof Uint8Array;
|
|
84
|
+
if (parent.type === LIST) {
|
|
85
|
+
parent.val.push(utf8 ? utf8Decode(x) : x);
|
|
105
86
|
} else {
|
|
106
87
|
if (!parent.key) {
|
|
107
|
-
parent.key =
|
|
88
|
+
parent.key = utf8 ? utf8Decode(x) : __decodeAscii(x);
|
|
108
89
|
} else {
|
|
109
90
|
parent.val[parent.key] = utf8 ? utf8Decode(x) : x;
|
|
110
91
|
parent.key = null;
|
|
111
92
|
}
|
|
112
93
|
}
|
|
113
94
|
};
|
|
114
|
-
const __readInt = (
|
|
115
|
-
let i;
|
|
95
|
+
const __readInt = (ctx, acc, end = END) => {
|
|
116
96
|
let x;
|
|
117
97
|
let isSigned = false;
|
|
118
|
-
while (
|
|
119
|
-
x = i
|
|
120
|
-
if (x >=
|
|
121
|
-
acc = acc * 10 + x -
|
|
122
|
-
} else if (x ===
|
|
98
|
+
while (ctx.i < ctx.n) {
|
|
99
|
+
x = ctx.buf[ctx.i++];
|
|
100
|
+
if (x >= ZERO && x <= NINE) {
|
|
101
|
+
acc = acc * 10 + x - ZERO;
|
|
102
|
+
} else if (x === MINUS) {
|
|
123
103
|
assert(!isSigned, `invalid int literal`);
|
|
124
104
|
isSigned = true;
|
|
125
105
|
} else if (x === end) {
|
|
@@ -130,29 +110,31 @@ const __readInt = (iter, acc, end = 101 /* END */) => {
|
|
|
130
110
|
}
|
|
131
111
|
illegalState(`incomplete int`);
|
|
132
112
|
};
|
|
133
|
-
const __readFloat = (
|
|
134
|
-
let i;
|
|
113
|
+
const __readFloat = (ctx) => {
|
|
135
114
|
let x;
|
|
136
115
|
let acc = "";
|
|
137
|
-
while (
|
|
138
|
-
x = i
|
|
139
|
-
if (x >=
|
|
116
|
+
while (ctx.i < ctx.n) {
|
|
117
|
+
x = ctx.buf[ctx.i++];
|
|
118
|
+
if (x >= ZERO && x <= NINE || x === DOT || x === MINUS) {
|
|
140
119
|
acc += String.fromCharCode(x);
|
|
141
|
-
} else if (x ===
|
|
142
|
-
return
|
|
120
|
+
} else if (x === END) {
|
|
121
|
+
return +acc;
|
|
143
122
|
} else {
|
|
144
123
|
illegalState(`expected digit or dot, got 0x${x.toString(16)}`);
|
|
145
124
|
}
|
|
146
125
|
}
|
|
147
126
|
illegalState(`incomplete float`);
|
|
148
127
|
};
|
|
149
|
-
const __readBytes = (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
128
|
+
const __readBytes = (ctx, len) => {
|
|
129
|
+
if (ctx.i + len > ctx.n)
|
|
130
|
+
illegalState(`expected ${len} bytes, but reached EOF`);
|
|
131
|
+
return ctx.buf.subarray(ctx.i, ctx.i += len);
|
|
132
|
+
};
|
|
133
|
+
const __decodeAscii = (buf) => {
|
|
134
|
+
let res = "";
|
|
135
|
+
for (let i = 0, n = buf.length; i < n; i++)
|
|
136
|
+
res += String.fromCharCode(buf[i]);
|
|
137
|
+
return res;
|
|
156
138
|
};
|
|
157
139
|
export {
|
|
158
140
|
decode
|
package/encode.js
CHANGED
|
@@ -3,65 +3,39 @@ import { isBoolean } from "@thi.ng/checks/is-boolean";
|
|
|
3
3
|
import { isNumber } from "@thi.ng/checks/is-number";
|
|
4
4
|
import { isPlainObject } from "@thi.ng/checks/is-plain-object";
|
|
5
5
|
import { isString } from "@thi.ng/checks/is-string";
|
|
6
|
-
import { defmulti } from "@thi.ng/defmulti/defmulti";
|
|
7
6
|
import { assert } from "@thi.ng/errors/assert";
|
|
8
7
|
import { unsupported } from "@thi.ng/errors/unsupported";
|
|
9
8
|
import { utf8Length } from "@thi.ng/strings/utf8";
|
|
10
9
|
import { bytes, str, u8, u8array } from "@thi.ng/transducers-binary/bytes";
|
|
11
10
|
import { mapcat } from "@thi.ng/transducers/mapcat";
|
|
12
|
-
var Type = /* @__PURE__ */ ((Type2) => {
|
|
13
|
-
Type2[Type2["INT"] = 0] = "INT";
|
|
14
|
-
Type2[Type2["FLOAT"] = 1] = "FLOAT";
|
|
15
|
-
Type2[Type2["STR"] = 2] = "STR";
|
|
16
|
-
Type2[Type2["BINARY"] = 3] = "BINARY";
|
|
17
|
-
Type2[Type2["DICT"] = 4] = "DICT";
|
|
18
|
-
Type2[Type2["LIST"] = 5] = "LIST";
|
|
19
|
-
return Type2;
|
|
20
|
-
})(Type || {});
|
|
21
|
-
var Lit = /* @__PURE__ */ ((Lit2) => {
|
|
22
|
-
Lit2[Lit2["DICT"] = 100] = "DICT";
|
|
23
|
-
Lit2[Lit2["END"] = 101] = "END";
|
|
24
|
-
Lit2[Lit2["LIST"] = 108] = "LIST";
|
|
25
|
-
return Lit2;
|
|
26
|
-
})(Lit || {});
|
|
27
11
|
const FLOAT_RE = /^[0-9.-]+$/;
|
|
12
|
+
const ENCODERS = {
|
|
13
|
+
i: (x) => {
|
|
14
|
+
__ensureValidNumber(x);
|
|
15
|
+
return [str(`i${Math.floor(x)}e`)];
|
|
16
|
+
},
|
|
17
|
+
f: (x) => {
|
|
18
|
+
__ensureValidNumber(x);
|
|
19
|
+
assert(
|
|
20
|
+
FLOAT_RE.test(x.toString()),
|
|
21
|
+
`values requiring exponential notation not allowed (${x})`
|
|
22
|
+
);
|
|
23
|
+
return [str(`f${x}e`)];
|
|
24
|
+
},
|
|
25
|
+
b: (buf) => [str(buf.length + ":"), u8array(buf)],
|
|
26
|
+
s: (x) => [str(utf8Length(x) + ":" + x)],
|
|
27
|
+
l: (x) => [u8(108), ...mapcat(__encodeBin, x), u8(101)],
|
|
28
|
+
d: (x) => [
|
|
29
|
+
u8(100),
|
|
30
|
+
...mapcat(
|
|
31
|
+
(k) => __encodeBin(k).concat(__encodeBin(x[k])),
|
|
32
|
+
Object.keys(x).sort()
|
|
33
|
+
),
|
|
34
|
+
u8(101)
|
|
35
|
+
]
|
|
36
|
+
};
|
|
28
37
|
const encode = (x, cap = 1024) => bytes(cap, __encodeBin(x));
|
|
29
|
-
const __encodeBin =
|
|
30
|
-
(x) => isNumber(x) ? Math.floor(x) !== x ? 1 /* FLOAT */ : 0 /* INT */ : isBoolean(x) ? 0 /* INT */ : isString(x) ? 2 /* STR */ : x instanceof Uint8Array ? 3 /* BINARY */ : isPlainObject(x) ? 4 /* DICT */ : isArrayLike(x) ? 5 /* LIST */ : unsupported(`unsupported data type: ${x}`),
|
|
31
|
-
{},
|
|
32
|
-
{
|
|
33
|
-
[0 /* INT */]: (x) => {
|
|
34
|
-
__ensureValidNumber(x);
|
|
35
|
-
return [str(`i${Math.floor(x)}e`)];
|
|
36
|
-
},
|
|
37
|
-
[1 /* FLOAT */]: (x) => {
|
|
38
|
-
__ensureValidNumber(x);
|
|
39
|
-
assert(
|
|
40
|
-
FLOAT_RE.test(x.toString()),
|
|
41
|
-
`values requiring exponential notation not allowed (${x})`
|
|
42
|
-
);
|
|
43
|
-
return [str(`f${x}e`)];
|
|
44
|
-
},
|
|
45
|
-
[3 /* BINARY */]: (buf) => [
|
|
46
|
-
str(buf.length + ":"),
|
|
47
|
-
u8array(buf)
|
|
48
|
-
],
|
|
49
|
-
[2 /* STR */]: (x) => [str(utf8Length(x) + ":" + x)],
|
|
50
|
-
[5 /* LIST */]: (x) => [
|
|
51
|
-
u8(108 /* LIST */),
|
|
52
|
-
...mapcat(__encodeBin, x),
|
|
53
|
-
u8(101 /* END */)
|
|
54
|
-
],
|
|
55
|
-
[4 /* DICT */]: (x) => [
|
|
56
|
-
u8(100 /* DICT */),
|
|
57
|
-
...mapcat(
|
|
58
|
-
(k) => __encodeBin(k).concat(__encodeBin(x[k])),
|
|
59
|
-
Object.keys(x).sort()
|
|
60
|
-
),
|
|
61
|
-
u8(101 /* END */)
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
);
|
|
38
|
+
const __encodeBin = (x) => ENCODERS[isNumber(x) ? Math.floor(x) !== x ? "f" : "i" : isBoolean(x) ? "i" : isString(x) ? "s" : x instanceof Uint8Array ? "b" : isPlainObject(x) ? "d" : isArrayLike(x) ? "l" : unsupported(`unsupported data type: ${x}`)](x);
|
|
65
39
|
const __ensureValidNumber = (x) => {
|
|
66
40
|
assert(isFinite(x), `can't encode infinite value`);
|
|
67
41
|
assert(!isNaN(x), `can't encode NaN`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/bencode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Bencode binary encoder / decoder with optional UTF8 encoding & floating point support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@thi.ng/
|
|
44
|
-
"@thi.ng/
|
|
45
|
-
"@thi.ng/
|
|
43
|
+
"@thi.ng/api": "^8.11.16",
|
|
44
|
+
"@thi.ng/arrays": "^2.10.11",
|
|
45
|
+
"@thi.ng/checks": "^3.6.19",
|
|
46
46
|
"@thi.ng/errors": "^2.5.22",
|
|
47
47
|
"@thi.ng/strings": "^3.9.0",
|
|
48
|
-
"@thi.ng/transducers": "^9.2.
|
|
49
|
-
"@thi.ng/transducers-binary": "^2.1.
|
|
48
|
+
"@thi.ng/transducers": "^9.2.14",
|
|
49
|
+
"@thi.ng/transducers-binary": "^2.1.148"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@microsoft/api-extractor": "^7.48.1",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"thi.ng": {
|
|
90
90
|
"alias": "bc"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "d888087b36b086fd8c3e7dc98d35857266f78942\n"
|
|
93
93
|
}
|