Skip to content

Commit

Permalink
feat: parse double-quoted string with fast path (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Jun 12, 2023
1 parent fa4d366 commit 2a4383f
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 39 deletions.
60 changes: 30 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,49 +110,49 @@ safeDestr("[foo", { strict: true });

## Benchmarks

Locally try with `pnpm benchmark`. Below are esults on Node.js 18.11.0 with MBA M2.
Locally try with `pnpm benchmark`. Below are esults on Node.js **v18.16.0** with MBA M2.

**Note** `destr` is sometimes little bit slower than `JSON.parse` when parsing a valid JSON string mainly because of transform to avoid [prototype pollution](https://learn.snyk.io/lessons/prototype-pollution/javascript/) which can lead to serious security issues if not being sanitized. In the other words, `destr` is better when input is not always a json string or from untrusted source like request body.

```
=== Non-string fallback ==
JSON.parse x 10,323,718 ops/sec ±0.45% (96 runs sampled)
destr x 1,057,268,114 ops/sec ±1.71% (90 runs sampled)
destr (strict) x 977,215,995 ops/sec ±1.43% (97 runs sampled)
JSON.parse x 9,498,532 ops/sec ±0.57% (96 runs sampled)
destr x 153,323,211 ops/sec ±0.13% (99 runs sampled)
safeDestr x 64,237,062 ops/sec ±0.22% (96 runs sampled)
sjson:
@hapi/bourne x 10,151,985 ops/sec ±0.76% (96 runs sampled)
@hapi/bourne x 9,190,459 ops/sec ±0.50% (93 runs sampled)
Fastest is destr
=== Known values ==
JSON.parse x 16,359,358 ops/sec ±0.90% (92 runs sampled)
destr x 107,849,085 ops/sec ±0.34% (97 runs sampled)
destr (strict) x 107,891,427 ops/sec ±0.34% (99 runs sampled)
sjson x 14,216,957 ops/sec ±0.98% (89 runs sampled)
@hapi/bourne x 15,209,152 ops/sec ±1.08% (88 runs sampled)
Fastest is destr (strict),destr
=== Plain string ==
JSON.parse (try-catch) x 211,560 ops/sec ±0.84% (92 runs sampled)
destr x 60,315,113 ops/sec ±0.46% (98 runs sampled)
destr (strict):
sjson (try-catch) x 186,492 ops/sec ±0.70% (97 runs sampled)
@hapi/bourne:
JSON.parse x 14,260,909 ops/sec ±0.54% (95 runs sampled)
destr x 72,916,945 ops/sec ±0.15% (98 runs sampled)
safeDestr x 36,544,906 ops/sec ±0.31% (98 runs sampled)
sjson x 11,157,730 ops/sec ±0.53% (96 runs sampled)
@hapi/bourne x 13,241,853 ops/sec ±0.73% (93 runs sampled)
Fastest is destr
=== standard object ==
JSON.parse x 492,180 ops/sec ±0.98% (98 runs sampled)
destr x 356,819 ops/sec ±0.40% (98 runs sampled)
destr (strict) x 412,955 ops/sec ±0.88% (94 runs sampled)
sjson x 437,376 ops/sec ±0.42% (102 runs sampled)
@hapi/bourne x 457,020 ops/sec ±0.81% (99 runs sampled)
=== plain string ==
JSON.parse (try-catch) x 10,603,912 ops/sec ±0.75% (91 runs sampled)
destr x 82,123,481 ops/sec ±2.37% (99 runs sampled)
safeDestr x 40,737,935 ops/sec ±0.97% (96 runs sampled)
sjson (try-catch) x 9,194,305 ops/sec ±1.96% (94 runs sampled)
@hapi/bourne x 10,816,232 ops/sec ±1.59% (90 runs sampled)
Fastest is destr
=== package.json ==
JSON.parse x 403,428 ops/sec ±0.31% (101 runs sampled)
destr x 338,668 ops/sec ±0.27% (97 runs sampled)
safeDestr x 335,756 ops/sec ±0.29% (98 runs sampled)
sjson x 355,493 ops/sec ±0.15% (101 runs sampled)
@hapi/bourne x 384,948 ops/sec ±0.24% (98 runs sampled)
Fastest is JSON.parse
=== invalid syntax ==
JSON.parse (try-catch) x 493,739 ops/sec ±0.51% (98 runs sampled)
destr x 405,848 ops/sec ±0.56% (100 runs sampled)
destr (strict) x 409,514 ops/sec ±0.57% (101 runs sampled)
sjson (try-catch) x 435,406 ops/sec ±0.41% (100 runs sampled)
@hapi/bourne x 467,163 ops/sec ±0.42% (99 runs sampled)
=== broken object ==
JSON.parse (try-catch) x 406,262 ops/sec ±0.18% (100 runs sampled)
destr x 337,602 ops/sec ±0.37% (99 runs sampled)
safeDestr x 320,071 ops/sec ±0.35% (97 runs sampled)
sjson (try-catch) x 326,689 ops/sec ±0.41% (97 runs sampled)
@hapi/bourne x 313,024 ops/sec ±0.91% (94 runs sampled)
Fastest is JSON.parse (try-catch)
```

Expand Down
16 changes: 8 additions & 8 deletions bench.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "node:fs";
import Benchmark from "benchmark";
import sjson from "secure-json-parse";
import bourne from "@hapi/bourne";
import { destr } from "destr";
import { destr, safeDestr } from "destr";

const { log } = console;

Expand All @@ -28,8 +28,8 @@ function bench(name, val) {
suite.add("destr", () => {
destr(val);
});
suite.add("destr (strict)", () => {
destr(val, { strict: true });
suite.add("safeDestr", () => {
safeDestr(val);
});
suite.add("sjson", () => {
sjson.parse(val);
Expand All @@ -52,8 +52,8 @@ function benchTryCatch(name, val) {
suite.add("destr", () => {
destr(val);
});
suite.add("destr (strict)", () => {
destr(val, { strict: true });
suite.add("safeDestr", () => {
safeDestr(val);
});
suite.add("sjson (try-catch)", () => {
try {
Expand All @@ -70,8 +70,8 @@ function benchTryCatch(name, val) {

bench("Non-string fallback", 3.14159265359);
bench("Known values", "true");
benchTryCatch("Plain string", `"SALAM"`);
benchTryCatch("plain string", `"SALAM"`);

const pkg = fs.readFileSync("./package.json", "utf-8");
bench("standard object", pkg);
benchTryCatch("invalid syntax", pkg.substring(0, pkg.length - 1));
bench("package.json", pkg);
benchTryCatch("broken object", pkg.substring(0, pkg.length - 1));
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ export function destr<T = unknown>(value: any, options: Options = {}): T {
return value;
}

const _lval = value.toLowerCase().trim();
const _value = value.trim();
// eslint-disable-next-line unicorn/prefer-at
if (value[0] === '"' && value[value.length - 1] === '"') {
return _value.slice(1, -1) as T;
}

const _lval = _value.toLowerCase();
if (_lval === "true") {
return true as T;
}
Expand Down
13 changes: 13 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,26 @@ describe("destr", () => {
}
});

it("parses valid string as is with fast path", () => {
const testCases = [
{ input: '"Hello"', output: "Hello" },
{ input: ' "Hello" ', output: "Hello" },
{ input: ' "Invalid', output: ' "Invalid' },
];

for (const testCase of testCases) {
expect(destr(testCase.input)).toStrictEqual(testCase.output);
}
});

it("throws an error if it's a invalid JSON texts with safeDestr", () => {
const testCases = [
{ input: "{ ", output: "Unexpected end of JSON input" },
{ input: "[ ", output: "Unexpected end of JSON input" },
{ input: '" ', output: "Unexpected end of JSON input" },
{ input: "[1,2,3]?", output: "Unexpected token" },
{ input: "invalid JSON text", output: "Invalid JSON" },
{ input: ' "Invalid', output: "Unexpected end of JSON input" },
];

for (const testCase of testCases) {
Expand Down

0 comments on commit 2a4383f

Please sign in to comment.