Skip to content

Commit

Permalink
fix(bencode): fix #342, support signed ints
Browse files Browse the repository at this point in the history
- add inf/NaN checks
- add tests
  • Loading branch information
postspectacular committed Apr 7, 2022
1 parent 5a51f7d commit 66615be
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 28 deletions.
19 changes: 10 additions & 9 deletions packages/bencode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ decoder for structured data.

#### Booleans

Will be converted to `0` or `1`.
Will be converted to `0` or `1` integers.

#### String handling

Expand All @@ -40,13 +40,14 @@ Bencode strings (e.g. `len:xxx...`), but are used as is.

#### Floating point values

This implementation has optional (non-standard) support for floating
point values. If these are not desired (e.g. for compatibility reasons),
all numeric values MUST be pre-rounded to integers. The encoder only
chooses the custom float encoding iff a number has a fractional part.
Floats are encoded similarly to standard ints (i.e. as text), but using
`f` as prefix. Furthermore, only floats with an absolute value in the
semi-open `[1e-6,1e21)` interval can be encoded.
This implementation has optional (non-standard) support for floating point
values. If these are not desired (e.g. for compatibility reasons), all numeric
values MUST be pre-rounded to integers. The encoder only chooses the custom
float encoding iff a number has a fractional part. Floats are encoded similarly
to standard ints (i.e. as text), but using `f` as prefix. Furthermore, only
floats with an absolute value in the semi-open `[1e-6,1e21)` interval can be
encoded. Float values requiring exponential notation will throw an error during
encoding.

### Status

Expand Down Expand Up @@ -77,7 +78,7 @@ node --experimental-repl-await
> const bencode = await import("@thi.ng/bencode");
```

Package sizes (gzipped, pre-treeshake): ESM: 1.22 KB
Package sizes (gzipped, pre-treeshake): ESM: 1.30 KB

## Dependencies

Expand Down
6 changes: 5 additions & 1 deletion packages/bencode/src/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,16 @@ const collect = (stack: any[], x: any, utf8 = false) => {
const readInt = (iter: Iterator<number>, acc: number, end = Lit.END) => {
let i: IteratorResult<number>;
let x: number;
let isSigned = false;
while (!(i = iter.next()).done) {
x = i.value;
if (x >= Lit.ZERO && x <= Lit.NINE) {
acc = acc * 10 + x - Lit.ZERO;
} else if (x === Lit.MINUS) {
assert(!isSigned, `invalid int literal`);
isSigned = true;
} else if (x === end) {
return acc;
return isSigned ? -acc : acc;
} else {
illegalState(`expected digit, got 0x${x.toString(16)}`);
}
Expand Down
14 changes: 12 additions & 2 deletions packages/bencode/src/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@ const encodeBin: MultiFn1<any, BinStructItem[]> = defmulti<
: unsupported(`unsupported data type: ${x}`),
{},
{
[Type.INT]: (x: number) => [str(`i${Math.floor(x)}e`)],
[Type.INT]: (x: number) => {
__ensureValidNumber(x);
return [str(`i${Math.floor(x)}e`)];
},

[Type.FLOAT]: (x: number) => {
__ensureValidNumber(x);
assert(
FLOAT_RE.test(x.toString()),
`exponential notation not allowed (${x})`
`values requiring exponential notation not allowed (${x})`
);
return [str(`f${x}e`)];
},
Expand Down Expand Up @@ -86,3 +90,9 @@ const encodeBin: MultiFn1<any, BinStructItem[]> = defmulti<
],
}
);

/** @internal */
const __ensureValidNumber = (x: number) => {
assert(isFinite(x), `can't encode infinite value`);
assert(!isNaN(x), `can't encode NaN`);
};
27 changes: 19 additions & 8 deletions packages/bencode/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { equiv } from "@thi.ng/equiv";
import { group } from "@thi.ng/testament";
import * as assert from "assert";
import { decode, encode } from "../src/index.js"
import { decode, encode } from "../src/index.js";

const src = { foo: [1, "a", { bar: "baz" }, [42.123]] };
const src2 = { foo: new Uint8Array([0, 1, 2, 3, 255, 254, 253]) };
Expand All @@ -11,11 +11,12 @@ group("bencode", {
"roundtrip (utf8)": () => {
let bytes;
assert.deepStrictEqual(
bytes = [...encode(src)],
(bytes = [...encode(src)]),
[
0x64, 0x33, 0x3a, 0x66, 0x6f, 0x6f, 0x6c, 0x69, 0x31, 0x65, 0x31, 0x3a, 0x61, 0x64, 0x33, 0x3a,
0x62, 0x61, 0x72, 0x33, 0x3a, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x66, 0x34, 0x32, 0x2e, 0x31, 0x32,
0x33, 0x65, 0x65, 0x65, 0x65,
0x64, 0x33, 0x3a, 0x66, 0x6f, 0x6f, 0x6c, 0x69, 0x31, 0x65,
0x31, 0x3a, 0x61, 0x64, 0x33, 0x3a, 0x62, 0x61, 0x72, 0x33,
0x3a, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x66, 0x34, 0x32, 0x2e,
0x31, 0x32, 0x33, 0x65, 0x65, 0x65, 0x65,
]
);
assert.deepStrictEqual(decode(bytes), src);
Expand All @@ -25,10 +26,20 @@ group("bencode", {
let bytes;
assert.ok(
equiv(
bytes = encode(src2),
[0x64, 0x33, 0x3a, 0x66, 0x6f, 0x6f, 0x37, 0x3a, 0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0x65]
(bytes = encode(src2)),
[
0x64, 0x33, 0x3a, 0x66, 0x6f, 0x6f, 0x37, 0x3a, 0x00, 0x01,
0x02, 0x03, 0xff, 0xfe, 0xfd, 0x65,
]
)
)
);
assert.ok(equiv(decode(bytes, false), src2));
},

"signed int": () => {
assert.ok(equiv(decode(encode(42), false), 42));
assert.ok(equiv(decode(encode(-42), false), -42));
assert.throws(() => decode([0x69, 0x2d, 0x2d, 0x31, 0x65])); // i--1e
assert.throws(() => decode([0x69, 0x2d, 0x31, 0x2e, 0x32, 0x65])); // i-1.2e
},
});
17 changes: 9 additions & 8 deletions packages/bencode/tpl.readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ decoder for structured data.

#### Booleans

Will be converted to `0` or `1`.
Will be converted to `0` or `1` integers.

#### String handling

Expand All @@ -28,13 +28,14 @@ Bencode strings (e.g. `len:xxx...`), but are used as is.

#### Floating point values

This implementation has optional (non-standard) support for floating
point values. If these are not desired (e.g. for compatibility reasons),
all numeric values MUST be pre-rounded to integers. The encoder only
chooses the custom float encoding iff a number has a fractional part.
Floats are encoded similarly to standard ints (i.e. as text), but using
`f` as prefix. Furthermore, only floats with an absolute value in the
semi-open `[1e-6,1e21)` interval can be encoded.
This implementation has optional (non-standard) support for floating point
values. If these are not desired (e.g. for compatibility reasons), all numeric
values MUST be pre-rounded to integers. The encoder only chooses the custom
float encoding iff a number has a fractional part. Floats are encoded similarly
to standard ints (i.e. as text), but using `f` as prefix. Furthermore, only
floats with an absolute value in the semi-open `[1e-6,1e21)` interval can be
encoded. Float values requiring exponential notation will throw an error during
encoding.

${status}

Expand Down

0 comments on commit 66615be

Please sign in to comment.