json-as 1.4.0 → 1.6.0
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 +54 -29
- package/README.md +126 -33
- package/assembly/custom/chars.ts +39 -78
- package/assembly/deserialize/error.ts +21 -0
- package/assembly/deserialize/index/arbitrary.ts +50 -11
- package/assembly/deserialize/index/array.ts +2 -1
- package/assembly/deserialize/index/bool.ts +1 -1
- package/assembly/deserialize/index/float.ts +2 -4
- package/assembly/deserialize/index/integer.ts +2 -4
- package/assembly/deserialize/index/object.ts +9 -1
- package/assembly/deserialize/index/string.ts +39 -7
- package/assembly/deserialize/index/unsigned.ts +2 -4
- package/assembly/deserialize/index.ts +0 -1
- package/assembly/deserialize/naive/array/arbitrary.ts +3 -2
- package/assembly/deserialize/naive/array/array.ts +1 -0
- package/assembly/deserialize/naive/array/bool.ts +21 -11
- package/assembly/deserialize/naive/array/box.ts +13 -33
- package/assembly/deserialize/naive/array/float.ts +23 -13
- package/assembly/deserialize/naive/array/integer.ts +34 -22
- package/assembly/deserialize/naive/array/map.ts +1 -1
- package/assembly/deserialize/naive/array/object.ts +2 -1
- package/assembly/deserialize/naive/array/string.ts +29 -16
- package/assembly/deserialize/naive/array/struct.ts +19 -1
- package/assembly/deserialize/naive/bool.ts +18 -10
- package/assembly/deserialize/naive/date.ts +1 -2
- package/assembly/deserialize/naive/float.ts +3 -9
- package/assembly/deserialize/naive/integer.ts +1 -2
- package/assembly/deserialize/naive/map.ts +342 -43
- package/assembly/deserialize/naive/object.ts +250 -55
- package/assembly/deserialize/naive/raw.ts +10 -2
- package/assembly/deserialize/naive/set.ts +2 -5
- package/assembly/deserialize/naive/staticarray.ts +4 -4
- package/assembly/deserialize/naive/string.ts +46 -60
- package/assembly/deserialize/naive/unsigned.ts +1 -2
- package/assembly/deserialize/parseMode.ts +216 -0
- package/assembly/deserialize/simd/array/integer.ts +8 -10
- package/assembly/deserialize/simd/float.ts +38 -34
- package/assembly/deserialize/simd/integer.ts +2 -7
- package/assembly/deserialize/simd/string.ts +84 -39
- package/assembly/deserialize/string-validation.ts +34 -0
- package/assembly/deserialize/swar/array/arbitrary.ts +1 -2
- package/assembly/deserialize/swar/array/array.ts +64 -15
- package/assembly/deserialize/swar/array/bool.ts +15 -5
- package/assembly/deserialize/swar/array/box.ts +1 -2
- package/assembly/deserialize/swar/array/float.ts +399 -30
- package/assembly/deserialize/swar/array/generic.ts +13 -4
- package/assembly/deserialize/swar/array/integer.ts +29 -78
- package/assembly/deserialize/swar/array/map.ts +1 -2
- package/assembly/deserialize/swar/array/object.ts +12 -4
- package/assembly/deserialize/swar/array/raw.ts +1 -2
- package/assembly/deserialize/swar/array/shared.ts +7 -14
- package/assembly/deserialize/swar/array/string.ts +37 -13
- package/assembly/deserialize/swar/array/struct.ts +109 -21
- package/assembly/deserialize/swar/array.ts +1 -3
- package/assembly/deserialize/swar/float.ts +23 -21
- package/assembly/deserialize/swar/integer.ts +2 -7
- package/assembly/deserialize/swar/string.ts +81 -124
- package/assembly/deserialize/swar/typedarray.ts +4 -4
- package/assembly/index.d.ts +35 -24
- package/assembly/index.ts +1775 -281
- package/assembly/serialize/index/arbitrary.ts +70 -4
- package/assembly/serialize/index/bool.ts +1 -1
- package/assembly/serialize/index/float.ts +1 -5
- package/assembly/serialize/index/integer.ts +1 -1
- package/assembly/serialize/index/jsonarray.ts +51 -0
- package/assembly/serialize/index/object.ts +42 -3
- package/assembly/serialize/index/string.ts +1 -2
- package/assembly/serialize/index/typedarray.ts +1 -2
- package/assembly/serialize/index.ts +1 -0
- package/assembly/serialize/naive/array.ts +87 -34
- package/assembly/serialize/naive/bool.ts +0 -1
- package/assembly/serialize/naive/float.ts +15 -29
- package/assembly/serialize/naive/integer.ts +1 -5
- package/assembly/serialize/naive/map.ts +37 -0
- package/assembly/serialize/naive/raw.ts +1 -2
- package/assembly/serialize/naive/set.ts +0 -5
- package/assembly/serialize/naive/staticarray.ts +0 -6
- package/assembly/serialize/naive/string.ts +1 -10
- package/assembly/serialize/naive/typedarray.ts +0 -7
- package/assembly/serialize/simd/string.ts +1 -3
- package/assembly/serialize/swar/string.ts +1 -2
- package/assembly/util/atoi-fast.ts +4 -14
- package/assembly/util/bytes.ts +1 -2
- package/assembly/util/eisel-lemire.ts +179 -0
- package/assembly/util/idofd.ts +1 -2
- package/assembly/util/isSpace.ts +1 -2
- package/assembly/util/itoa-fast.ts +6 -9
- package/assembly/util/nextPowerOf2.ts +1 -2
- package/assembly/util/parsefloat-fast.ts +3 -5
- package/assembly/util/prettyWhitespaceSimd.ts +69 -0
- package/assembly/util/ptrToStr.ts +1 -2
- package/assembly/util/scanValueEndSimd.ts +83 -22
- package/assembly/util/scanValueEndSwar.ts +67 -25
- package/assembly/util/scientific.ts +5 -8
- package/assembly/util/snp.ts +1 -2
- package/assembly/util/swar-int.ts +5 -10
- package/assembly/util/swar.ts +2 -4
- package/assembly/util/validateJson.ts +338 -0
- package/lib/as-bs.ts +51 -51
- package/package.json +25 -8
- package/transform/lib/index.d.ts +6 -0
- package/transform/lib/index.js +874 -171
- package/transform/lib/types.d.ts +3 -1
- package/transform/lib/types.js +6 -0
- package/assembly/deserialize/index/struct.ts +0 -1
- package/assembly/deserialize/naive/struct.ts +0 -21
- package/assembly/util/dragonbox-cache.ts +0 -445
- package/assembly/util/dragonbox.ts +0 -652
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
BRACE_RIGHT,
|
|
5
5
|
BRACKET_LEFT,
|
|
6
6
|
BRACKET_RIGHT,
|
|
7
|
+
COLON,
|
|
7
8
|
COMMA,
|
|
8
9
|
QUOTE,
|
|
9
10
|
} from "../custom/chars";
|
|
@@ -11,33 +12,45 @@ import { isSpace } from "./isSpace";
|
|
|
11
12
|
|
|
12
13
|
// SWAR analogue of `scanValueEndSimd.ts`, processing four UTF-16 lanes per
|
|
13
14
|
// 64-bit word for the SWAR build mode (no SIMD feature). Each mask is a fast
|
|
14
|
-
// FILTER
|
|
15
|
+
// FILTER - a matched lane is re-checked with a real `load<u16>` before acting -
|
|
15
16
|
// so the masks may over-match non-ASCII lanes whose low byte equals a target
|
|
16
17
|
// (the verify rejects them). Lane byte offset within a hit word is
|
|
17
18
|
// `ctz(mask) >> 3` (detection bit sits at lane*16 + 7).
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// @ts-expect-error: @inline is a valid decorator
|
|
22
|
-
@inline const HI: u64 = 0x0080_0080_0080_0080;
|
|
20
|
+
const ONES: u64 = 0x0001_0001_0001_0001;
|
|
21
|
+
const HI: u64 = 0x0080_0080_0080_0080;
|
|
23
22
|
|
|
24
23
|
// 16-bit-lane "equals" partials (pre-`& HI`); OR several, then `& HI` once.
|
|
25
|
-
|
|
26
|
-
@inline function eqPart(block: u64, splat: u64): u64 {
|
|
24
|
+
function eqPart(block: u64, splat: u64): u64 {
|
|
27
25
|
const t = block ^ splat;
|
|
28
26
|
return (t - ONES) & ~t;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const S_QUOTE: u64 = 0x0022_0022_0022_0022;
|
|
30
|
+
const S_BACK_SLASH: u64 = 0x005c_005c_005c_005c;
|
|
31
|
+
const S_BRACKET_LEFT: u64 = 0x005b_005b_005b_005b;
|
|
32
|
+
const S_BRACKET_RIGHT: u64 = 0x005d_005d_005d_005d;
|
|
33
|
+
// Clears bit 5 (0x20) of each lane, folding `{`/`}` onto `[`/`]`.
|
|
34
|
+
const FOLD: u64 = 0xffdf_ffdf_ffdf_ffdf;
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
@inline function quoteOrBackslashMask(block: u64): u64 {
|
|
36
|
+
function quoteOrBackslashMask(block: u64): u64 {
|
|
38
37
|
return (eqPart(block, S_QUOTE) | eqPart(block, S_BACK_SLASH)) & HI;
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
// Filter for lanes equal to `"`, `{`, `}`, `[`, or `]` - the only bytes that,
|
|
41
|
+
// outside a string, change depth or open a string. As with the other SWAR
|
|
42
|
+
// masks, a hit is a candidate to verify with a real load (it may over-match a
|
|
43
|
+
// non-ASCII lane whose low byte collides).
|
|
44
|
+
function structuralOrQuoteMask(block: u64): u64 {
|
|
45
|
+
const folded = block & FOLD;
|
|
46
|
+
return (
|
|
47
|
+
(eqPart(folded, S_BRACKET_LEFT) |
|
|
48
|
+
eqPart(folded, S_BRACKET_RIGHT) |
|
|
49
|
+
eqPart(block, S_QUOTE)) &
|
|
50
|
+
HI
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
function scanQuotedValueEnd_SWAR(srcStart: usize, srcEnd: usize): usize {
|
|
42
55
|
srcStart += 2;
|
|
43
56
|
const srcEnd8 = srcEnd >= 8 ? srcEnd - 8 : 0;
|
|
@@ -63,21 +76,30 @@ function scanQuotedValueEnd_SWAR(srcStart: usize, srcEnd: usize): usize {
|
|
|
63
76
|
break;
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
// Resolve escapes by consuming a backslash *and the char it escapes* together,
|
|
80
|
+
// so escape parity is tracked exactly. A look-back `prev != BACK_SLASH` test is
|
|
81
|
+
// wrong for an escaped backslash: in `"x\\"` the closing quote follows a `\`
|
|
82
|
+
// (the second of the pair) yet still closes the string.
|
|
66
83
|
while (srcStart < srcEnd) {
|
|
67
84
|
const code = load<u16>(srcStart);
|
|
68
|
-
if (code ==
|
|
69
|
-
|
|
85
|
+
if (code == BACK_SLASH) {
|
|
86
|
+
srcStart += 4;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (code == QUOTE) return srcStart + 2;
|
|
70
90
|
srcStart += 2;
|
|
71
91
|
}
|
|
72
92
|
return 0;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
function scanCompositeValueEnd_SWAR(srcStart: usize, srcEnd: usize): usize {
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
96
|
+
// Process structural tokens scalar-side, but bulk-skip the bytes between them:
|
|
97
|
+
// nested string VALUES via the SWAR quoted scan, and runs of digits /
|
|
98
|
+
// punctuation / whitespace (numeric arrays) via a SWAR hunt for the next
|
|
99
|
+
// `"`/`{`/`}`/`[`/`]`.
|
|
79
100
|
let depth: i32 = 1;
|
|
80
101
|
let ptr = srcStart + 2;
|
|
102
|
+
const srcEnd8 = srcEnd >= 8 ? srcEnd - 8 : 0;
|
|
81
103
|
while (ptr < srcEnd) {
|
|
82
104
|
const code = load<u16>(ptr);
|
|
83
105
|
if (code == QUOTE) {
|
|
@@ -85,12 +107,36 @@ function scanCompositeValueEnd_SWAR(srcStart: usize, srcEnd: usize): usize {
|
|
|
85
107
|
if (!ptr) return 0;
|
|
86
108
|
continue;
|
|
87
109
|
}
|
|
88
|
-
|
|
110
|
+
const folded = code & 0xffdf;
|
|
111
|
+
if (folded == BRACKET_LEFT) {
|
|
89
112
|
depth++;
|
|
90
|
-
|
|
113
|
+
ptr += 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (folded == BRACKET_RIGHT) {
|
|
91
117
|
if (--depth == 0) return ptr + 2;
|
|
118
|
+
ptr += 2;
|
|
119
|
+
continue;
|
|
92
120
|
}
|
|
93
121
|
ptr += 2;
|
|
122
|
+
// `,`/`:` sit one byte from the next token - stay scalar (string-dense
|
|
123
|
+
// objects); other fillers can run long, so SWAR-skip past them.
|
|
124
|
+
if (code == COMMA || code == COLON) continue;
|
|
125
|
+
while (ptr <= srcEnd8) {
|
|
126
|
+
const mask = structuralOrQuoteMask(load<u64>(ptr));
|
|
127
|
+
if (mask == 0) {
|
|
128
|
+
ptr += 8;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const idx = ptr + (usize(ctz(mask)) >> 3);
|
|
132
|
+
const c = load<u16>(idx);
|
|
133
|
+
const f = c & 0xffdf;
|
|
134
|
+
if (c == QUOTE || f == BRACKET_LEFT || f == BRACKET_RIGHT) {
|
|
135
|
+
ptr = idx; // real token - the outer loop processes it
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
ptr = idx + 2; // spurious lane match - keep scanning
|
|
139
|
+
}
|
|
94
140
|
}
|
|
95
141
|
return 0;
|
|
96
142
|
}
|
|
@@ -117,11 +163,7 @@ function scanScalarValueEnd_SWAR(srcStart: usize, srcEnd: usize): usize {
|
|
|
117
163
|
* objects/arrays use the SWAR token scans above; scalars use a short scalar
|
|
118
164
|
* loop. Returns 0 on empty input or an unterminated string/composite.
|
|
119
165
|
*/
|
|
120
|
-
|
|
121
|
-
@inline export function scanValueEnd_SWAR<T>(
|
|
122
|
-
srcStart: usize,
|
|
123
|
-
srcEnd: usize,
|
|
124
|
-
): usize {
|
|
166
|
+
export function scanValueEnd_SWAR<T>(srcStart: usize, srcEnd: usize): usize {
|
|
125
167
|
if (srcStart >= srcEnd) return 0;
|
|
126
168
|
const first = load<u16>(srcStart);
|
|
127
169
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `scientific` directly skips both costs.
|
|
11
11
|
//
|
|
12
12
|
// scientific() is correctly rounded for all u64 mantissas and decimal
|
|
13
|
-
// exponents that fit in IEEE-754 f64's range
|
|
13
|
+
// exponents that fit in IEEE-754 f64's range - including the [2^53, 2^64)
|
|
14
14
|
// mantissa range that breaks Lemire's single-fmul fast path.
|
|
15
15
|
|
|
16
16
|
const POWERS10: usize = memory.data<f64>([
|
|
@@ -25,13 +25,11 @@ const POWERS5: usize = memory.data<i32>([
|
|
|
25
25
|
244140625, 1220703125,
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
@inline function pow10(n: i32): f64 {
|
|
28
|
+
function pow10(n: i32): f64 {
|
|
30
29
|
return load<f64>(POWERS10 + ((<usize>n) << alignof<f64>()));
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
@inline function pow5_32(n: i32): i32 {
|
|
32
|
+
function pow5_32(n: i32): i32 {
|
|
35
33
|
return load<i32>(POWERS5 + ((<usize>n) << alignof<i32>()));
|
|
36
34
|
}
|
|
37
35
|
|
|
@@ -40,8 +38,7 @@ const POWERS5: usize = memory.data<i32>([
|
|
|
40
38
|
// @ts-ignore: lazy decorator
|
|
41
39
|
@lazy let __fixmulShift: u64 = 0;
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
@inline function fixmul(a: u64, b: u32): u64 {
|
|
41
|
+
function fixmul(a: u64, b: u32): u64 {
|
|
45
42
|
const low = (a & 0xffffffff) * b;
|
|
46
43
|
const high = (a >> 32) * b + (low >> 32);
|
|
47
44
|
const overflow = <u32>(high >> 32);
|
|
@@ -98,7 +95,7 @@ function scaleup(significand: u64, exp: i32): f64 {
|
|
|
98
95
|
|
|
99
96
|
/**
|
|
100
97
|
* Construct an f64 from a u64 mantissa and decimal exponent. Result is
|
|
101
|
-
* correctly rounded
|
|
98
|
+
* correctly rounded - bit-identical to `f64.parse` for any input the SWAR
|
|
102
99
|
* float deserializer can pre-parse into this form.
|
|
103
100
|
*
|
|
104
101
|
* Caller guarantees the digit run that produced `significand` was already
|
package/assembly/util/snp.ts
CHANGED
|
@@ -58,8 +58,7 @@ export function snp<T extends number>(srcStart: usize, srcEnd: usize): T {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
@inline function pow10<T extends number>(x: u16): T {
|
|
61
|
+
function pow10<T extends number>(x: u16): T {
|
|
63
62
|
if (sizeof<T>() == 8) {
|
|
64
63
|
return <T>load<u64>(POW_TEN_TABLE_64 + x);
|
|
65
64
|
} else {
|
|
@@ -26,8 +26,7 @@ const FINAL_4_MAGIC: u64 = 0x0000_0064_0000_0001;
|
|
|
26
26
|
* @param block Four UTF-16 code units packed into a `u64`.
|
|
27
27
|
* @returns The parsed 4-digit value, or `U32.MAX_VALUE` on invalid input.
|
|
28
28
|
*/
|
|
29
|
-
|
|
30
|
-
@inline export function parse4Digits_Baseline(block: u64): u32 {
|
|
29
|
+
export function parse4Digits_Baseline(block: u64): u32 {
|
|
31
30
|
const digits = (block & LANE_LO_4) - ZERO_4;
|
|
32
31
|
if (((digits | (digits + RANGE_ADD_4)) & RANGE_MASK_4) != 0) {
|
|
33
32
|
return U32.MAX_VALUE;
|
|
@@ -58,8 +57,7 @@ const FINAL_4_MAGIC: u64 = 0x0000_0064_0000_0001;
|
|
|
58
57
|
* @param block Four UTF-16 code units packed into a `u64`.
|
|
59
58
|
* @returns The parsed 4-digit value, or `U32.MAX_VALUE` on invalid input.
|
|
60
59
|
*/
|
|
61
|
-
|
|
62
|
-
@inline export function parse4Digits_PairMul(block: u64): u32 {
|
|
60
|
+
export function parse4Digits_PairMul(block: u64): u32 {
|
|
63
61
|
const digits = block - ZERO_4;
|
|
64
62
|
if (((digits | (digits + RANGE_ADD_4)) & RANGE_MASK_4) != 0) {
|
|
65
63
|
return U32.MAX_VALUE;
|
|
@@ -76,8 +74,7 @@ const FINAL_4_MAGIC: u64 = 0x0000_0064_0000_0001;
|
|
|
76
74
|
* @param block Four UTF-16 code units packed into a `u64`.
|
|
77
75
|
* @returns The parsed 4-digit value.
|
|
78
76
|
*/
|
|
79
|
-
|
|
80
|
-
@inline export function parse4Digits_PairMul_Unsafe(block: u64): u32 {
|
|
77
|
+
export function parse4Digits_PairMul_Unsafe(block: u64): u32 {
|
|
81
78
|
const digits = block - ZERO_4;
|
|
82
79
|
const pairs = (digits * 10 + (digits >> 16)) & U32_LO_PAIR;
|
|
83
80
|
return <u32>((pairs * FINAL_4_MAGIC) >> 32);
|
|
@@ -123,8 +120,7 @@ export function parse8Digits_PairMul(lo: u64, hi: u64): u32 {
|
|
|
123
120
|
* @param hi The second `u64`, four UTF-16 code units.
|
|
124
121
|
* @returns The parsed 8-digit value.
|
|
125
122
|
*/
|
|
126
|
-
|
|
127
|
-
@inline export function parse8Digits_PairMul_Unsafe(lo: u64, hi: u64): u32 {
|
|
123
|
+
export function parse8Digits_PairMul_Unsafe(lo: u64, hi: u64): u32 {
|
|
128
124
|
const loDigits = lo - ZERO_4;
|
|
129
125
|
const hiDigits = hi - ZERO_4;
|
|
130
126
|
const loPairs = (loDigits * 10 + (loDigits >> 16)) & U32_LO_PAIR;
|
|
@@ -149,8 +145,7 @@ export function parse8Digits_PairMul(lo: u64, hi: u64): u32 {
|
|
|
149
145
|
* @param block Four UTF-16 code units packed into a `u64`.
|
|
150
146
|
* @returns A mask with non-digit lanes flagged in their high bit, or 0.
|
|
151
147
|
*/
|
|
152
|
-
|
|
153
|
-
@inline export function nonDigitMask4(block: u64): u64 {
|
|
148
|
+
export function nonDigitMask4(block: u64): u64 {
|
|
154
149
|
const digits = (block & LANE_LO_4) - ZERO_4;
|
|
155
150
|
return (digits | (digits + RANGE_ADD_4)) & RANGE_MASK_4;
|
|
156
151
|
}
|
package/assembly/util/swar.ts
CHANGED
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
* @param block Packed UTF-16 ASCII hex digits.
|
|
19
19
|
* @returns The decoded 16-bit value.
|
|
20
20
|
*/
|
|
21
|
-
|
|
22
|
-
@inline export function hex4_to_u16_swar(block: u64): u16 {
|
|
21
|
+
export function hex4_to_u16_swar(block: u64): u16 {
|
|
23
22
|
// (c & 0xF) + 9 * (c >> 6)
|
|
24
23
|
block = (block & 0x0f000f000f000f) + ((block >> 6) & 0x03000300030003) * 9;
|
|
25
24
|
|
|
@@ -51,8 +50,7 @@
|
|
|
51
50
|
* @param code The 16-bit value to encode.
|
|
52
51
|
* @returns Four packed UTF-16 ASCII hex digits.
|
|
53
52
|
*/
|
|
54
|
-
|
|
55
|
-
@inline export function u16_to_hex4_swar(code: u16): u64 {
|
|
53
|
+
export function u16_to_hex4_swar(code: u16): u64 {
|
|
56
54
|
let block =
|
|
57
55
|
(<u64>((code >> 12) & 0xf)) |
|
|
58
56
|
((<u64>((code >> 8) & 0xf)) << 16) |
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Scalar RFC 8259 syntax validator used by strict builds. It keeps an explicit
|
|
2
|
+
// container-state stack so hostile nesting is rejected without recursing
|
|
3
|
+
// through the Wasm call stack. This is intentionally a cold correctness pass:
|
|
4
|
+
// optimized backends retain their existing scanners, and the compile-time
|
|
5
|
+
// JSON_STRICT guard removes this pass entirely from normal builds.
|
|
6
|
+
|
|
7
|
+
const ARRAY_FIRST_OR_END: u8 = 0;
|
|
8
|
+
const ARRAY_VALUE: u8 = 1;
|
|
9
|
+
const ARRAY_COMMA_OR_END: u8 = 2;
|
|
10
|
+
const OBJECT_FIRST_KEY_OR_END: u8 = 3;
|
|
11
|
+
const OBJECT_KEY: u8 = 4;
|
|
12
|
+
const OBJECT_COLON: u8 = 5;
|
|
13
|
+
const OBJECT_VALUE: u8 = 6;
|
|
14
|
+
const OBJECT_COMMA_OR_END: u8 = 7;
|
|
15
|
+
// RFC 8259 permits implementations to set a maximum nesting depth. Keep the
|
|
16
|
+
// limit below typical Wasm shadow-stack exhaustion while accepting practical
|
|
17
|
+
// documents and reporting excessive nesting as a normal parse error.
|
|
18
|
+
const MAX_JSON_DEPTH: i32 = 256;
|
|
19
|
+
// Validation completes before target-specific parsing begins, so a single
|
|
20
|
+
// fixed scratch stack is sufficient and avoids allocating a managed Array for
|
|
21
|
+
// every strict parse. The public parser is already synchronous; a custom
|
|
22
|
+
// deserializer can only start a nested parse after this pass has returned.
|
|
23
|
+
// @ts-expect-error: decorator is valid for module-level scratch storage
|
|
24
|
+
@lazy const JSON_STATE_STACK = new StaticArray<u8>(MAX_JSON_DEPTH);
|
|
25
|
+
let jsonStateDepth: i32 = 0;
|
|
26
|
+
|
|
27
|
+
// Four UTF-16 lanes per word. These masks only identify candidates; every hit
|
|
28
|
+
// is re-read as u16 before it can affect validation, so lane-borrow false
|
|
29
|
+
// positives are harmless while plain string runs advance eight bytes at once.
|
|
30
|
+
const JSON_LANE_ONES: u64 = 0x0001_0001_0001_0001;
|
|
31
|
+
const JSON_LANE_HI: u64 = 0x0080_0080_0080_0080;
|
|
32
|
+
const JSON_QUOTE_SPLAT: u64 = 0x0022_0022_0022_0022;
|
|
33
|
+
const JSON_SLASH_SPLAT: u64 = 0x005c_005c_005c_005c;
|
|
34
|
+
const JSON_CONTROL_MASK: u64 = 0xffe0_ffe0_ffe0_ffe0;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@inline
|
|
38
|
+
function jsonEqualPart(block: u64, splat: u64): u64 {
|
|
39
|
+
const diff = block ^ splat;
|
|
40
|
+
return (diff - JSON_LANE_ONES) & ~diff;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@inline
|
|
45
|
+
function jsonStringSpecialMask(block: u64): u64 {
|
|
46
|
+
return (
|
|
47
|
+
(jsonEqualPart(block, JSON_QUOTE_SPLAT) |
|
|
48
|
+
jsonEqualPart(block, JSON_SLASH_SPLAT) |
|
|
49
|
+
jsonEqualPart(block & JSON_CONTROL_MASK, 0)) &
|
|
50
|
+
JSON_LANE_HI
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@inline
|
|
56
|
+
function isJSONWhitespace(code: u16): bool {
|
|
57
|
+
return code == 0x20 || code == 0x09 || code == 0x0a || code == 0x0d;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@inline
|
|
62
|
+
function isDigit(code: u16): bool {
|
|
63
|
+
return code >= 0x30 && code <= 0x39;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@inline
|
|
68
|
+
function isHexDigit(code: u16): bool {
|
|
69
|
+
return (
|
|
70
|
+
isDigit(code) ||
|
|
71
|
+
(code >= 0x41 && code <= 0x46) ||
|
|
72
|
+
(code >= 0x61 && code <= 0x66)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function skipJSONWhitespace(ptr: usize, end: usize): usize {
|
|
77
|
+
while (ptr < end && isJSONWhitespace(load<u16>(ptr))) ptr += 2;
|
|
78
|
+
return ptr;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scanJSONString(ptr: usize, end: usize): usize {
|
|
82
|
+
if (ptr >= end || load<u16>(ptr) != 0x22) return 0;
|
|
83
|
+
ptr += 2;
|
|
84
|
+
const end8 = end >= 8 ? end - 8 : 0;
|
|
85
|
+
|
|
86
|
+
while (ptr < end) {
|
|
87
|
+
while (ptr <= end8) {
|
|
88
|
+
const mask = jsonStringSpecialMask(load<u64>(ptr));
|
|
89
|
+
if (!mask) {
|
|
90
|
+
ptr += 8;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const candidate = ptr + (usize(ctz(mask)) >> 3);
|
|
95
|
+
const code = load<u16>(candidate);
|
|
96
|
+
if (code == 0x22) return candidate + 2;
|
|
97
|
+
if (code < 0x20) return 0;
|
|
98
|
+
if (code != 0x5c) {
|
|
99
|
+
// A non-ASCII lane may collide with the filter's low bits.
|
|
100
|
+
ptr = candidate + 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
ptr = candidate;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (ptr >= end) return 0;
|
|
108
|
+
const code = load<u16>(ptr);
|
|
109
|
+
if (code == 0x22) return ptr + 2;
|
|
110
|
+
if (code < 0x20) return 0;
|
|
111
|
+
|
|
112
|
+
if (code == 0x5c) {
|
|
113
|
+
ptr += 2;
|
|
114
|
+
if (ptr >= end) return 0;
|
|
115
|
+
const escape = load<u16>(ptr);
|
|
116
|
+
if (escape == 0x75) {
|
|
117
|
+
if (ptr + 10 > end) return 0;
|
|
118
|
+
if (
|
|
119
|
+
!isHexDigit(load<u16>(ptr, 2)) ||
|
|
120
|
+
!isHexDigit(load<u16>(ptr, 4)) ||
|
|
121
|
+
!isHexDigit(load<u16>(ptr, 6)) ||
|
|
122
|
+
!isHexDigit(load<u16>(ptr, 8))
|
|
123
|
+
)
|
|
124
|
+
return 0;
|
|
125
|
+
ptr += 10;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
escape != 0x22 &&
|
|
130
|
+
escape != 0x5c &&
|
|
131
|
+
escape != 0x2f &&
|
|
132
|
+
escape != 0x62 &&
|
|
133
|
+
escape != 0x66 &&
|
|
134
|
+
escape != 0x6e &&
|
|
135
|
+
escape != 0x72 &&
|
|
136
|
+
escape != 0x74
|
|
137
|
+
)
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ptr += 2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function scanJSONNumber(ptr: usize, end: usize): usize {
|
|
148
|
+
if (ptr < end && load<u16>(ptr) == 0x2d) ptr += 2;
|
|
149
|
+
if (ptr >= end) return 0;
|
|
150
|
+
|
|
151
|
+
let code = load<u16>(ptr);
|
|
152
|
+
if (code == 0x30) {
|
|
153
|
+
ptr += 2;
|
|
154
|
+
if (ptr < end && isDigit(load<u16>(ptr))) return 0;
|
|
155
|
+
} else if (code >= 0x31 && code <= 0x39) {
|
|
156
|
+
do {
|
|
157
|
+
ptr += 2;
|
|
158
|
+
} while (ptr < end && isDigit(load<u16>(ptr)));
|
|
159
|
+
} else {
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (ptr < end && load<u16>(ptr) == 0x2e) {
|
|
164
|
+
ptr += 2;
|
|
165
|
+
if (ptr >= end || !isDigit(load<u16>(ptr))) return 0;
|
|
166
|
+
do {
|
|
167
|
+
ptr += 2;
|
|
168
|
+
} while (ptr < end && isDigit(load<u16>(ptr)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (ptr < end) {
|
|
172
|
+
code = load<u16>(ptr);
|
|
173
|
+
if (code == 0x65 || code == 0x45) {
|
|
174
|
+
ptr += 2;
|
|
175
|
+
if (ptr < end) {
|
|
176
|
+
code = load<u16>(ptr);
|
|
177
|
+
if (code == 0x2b || code == 0x2d) ptr += 2;
|
|
178
|
+
}
|
|
179
|
+
if (ptr >= end || !isDigit(load<u16>(ptr))) return 0;
|
|
180
|
+
do {
|
|
181
|
+
ptr += 2;
|
|
182
|
+
} while (ptr < end && isDigit(load<u16>(ptr)));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return ptr;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function scanLiteral(
|
|
190
|
+
ptr: usize,
|
|
191
|
+
end: usize,
|
|
192
|
+
a: u16,
|
|
193
|
+
b: u16,
|
|
194
|
+
c: u16,
|
|
195
|
+
d: u16,
|
|
196
|
+
e: u16 = 0,
|
|
197
|
+
): usize {
|
|
198
|
+
const byteLength: usize = e ? 10 : 8;
|
|
199
|
+
if (ptr + byteLength > end) return 0;
|
|
200
|
+
if (
|
|
201
|
+
load<u16>(ptr) != a ||
|
|
202
|
+
load<u16>(ptr, 2) != b ||
|
|
203
|
+
load<u16>(ptr, 4) != c ||
|
|
204
|
+
load<u16>(ptr, 6) != d ||
|
|
205
|
+
(e && load<u16>(ptr, 8) != e)
|
|
206
|
+
)
|
|
207
|
+
return 0;
|
|
208
|
+
return ptr + byteLength;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scanJSONValue(ptr: usize, end: usize): usize {
|
|
212
|
+
if (ptr >= end) return 0;
|
|
213
|
+
const code = load<u16>(ptr);
|
|
214
|
+
if (code == 0x22) return scanJSONString(ptr, end);
|
|
215
|
+
if (code == 0x5b) {
|
|
216
|
+
if (jsonStateDepth >= MAX_JSON_DEPTH) return 0;
|
|
217
|
+
unchecked((JSON_STATE_STACK[jsonStateDepth++] = ARRAY_FIRST_OR_END));
|
|
218
|
+
return ptr + 2;
|
|
219
|
+
}
|
|
220
|
+
if (code == 0x7b) {
|
|
221
|
+
if (jsonStateDepth >= MAX_JSON_DEPTH) return 0;
|
|
222
|
+
unchecked((JSON_STATE_STACK[jsonStateDepth++] = OBJECT_FIRST_KEY_OR_END));
|
|
223
|
+
return ptr + 2;
|
|
224
|
+
}
|
|
225
|
+
if (code == 0x74) return scanLiteral(ptr, end, 0x74, 0x72, 0x75, 0x65); // true
|
|
226
|
+
if (code == 0x66) return scanLiteral(ptr, end, 0x66, 0x61, 0x6c, 0x73, 0x65); // false
|
|
227
|
+
if (code == 0x6e) return scanLiteral(ptr, end, 0x6e, 0x75, 0x6c, 0x6c); // null
|
|
228
|
+
return scanJSONNumber(ptr, end);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// JSONTestSuite's implementation-defined UTF-16-without-BOM fixtures arrive
|
|
232
|
+
// in an AssemblyScript string as alternating byte-valued code units. Recover
|
|
233
|
+
// the intended UTF-16 code units before validating or dispatching them.
|
|
234
|
+
export function normalizeJSONEncoding(data: string): string {
|
|
235
|
+
const length = data.length;
|
|
236
|
+
if (length < 4) return data;
|
|
237
|
+
|
|
238
|
+
let byteIndex = -1;
|
|
239
|
+
if (data.charCodeAt(0) == 0) {
|
|
240
|
+
byteIndex = 0; // UTF-16BE bytes: NUL, code unit, NUL, code unit, ...
|
|
241
|
+
} else if (data.charCodeAt(1) == 0) {
|
|
242
|
+
byteIndex = 1; // UTF-16LE bytes: code unit, NUL, code unit, NUL, ...
|
|
243
|
+
} else {
|
|
244
|
+
return data;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (let i = byteIndex; i < length; i += 2) {
|
|
248
|
+
if (data.charCodeAt(i) != 0) return data;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let out = "";
|
|
252
|
+
const codeIndex = byteIndex ^ 1;
|
|
253
|
+
for (let i = codeIndex; i < length; i += 2)
|
|
254
|
+
out += String.fromCharCode(data.charCodeAt(i));
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function validateJSON(data: string): bool {
|
|
259
|
+
let ptr = changetype<usize>(data);
|
|
260
|
+
const end = ptr + ((<usize>data.length) << 1);
|
|
261
|
+
ptr = skipJSONWhitespace(ptr, end);
|
|
262
|
+
if (ptr >= end) return false;
|
|
263
|
+
|
|
264
|
+
jsonStateDepth = 0;
|
|
265
|
+
ptr = scanJSONValue(ptr, end);
|
|
266
|
+
if (!ptr) return false;
|
|
267
|
+
let rootComplete = jsonStateDepth == 0;
|
|
268
|
+
|
|
269
|
+
while (true) {
|
|
270
|
+
ptr = skipJSONWhitespace(ptr, end);
|
|
271
|
+
if (jsonStateDepth == 0) return rootComplete && ptr == end;
|
|
272
|
+
if (ptr >= end) return false;
|
|
273
|
+
|
|
274
|
+
const top = jsonStateDepth - 1;
|
|
275
|
+
const state = unchecked(JSON_STATE_STACK[top]);
|
|
276
|
+
const code = load<u16>(ptr);
|
|
277
|
+
|
|
278
|
+
if (state == ARRAY_FIRST_OR_END) {
|
|
279
|
+
if (code == 0x5d) {
|
|
280
|
+
jsonStateDepth--;
|
|
281
|
+
ptr += 2;
|
|
282
|
+
if (jsonStateDepth == 0) rootComplete = true;
|
|
283
|
+
} else {
|
|
284
|
+
unchecked((JSON_STATE_STACK[top] = ARRAY_COMMA_OR_END));
|
|
285
|
+
ptr = scanJSONValue(ptr, end);
|
|
286
|
+
if (!ptr) return false;
|
|
287
|
+
}
|
|
288
|
+
} else if (state == ARRAY_VALUE) {
|
|
289
|
+
unchecked((JSON_STATE_STACK[top] = ARRAY_COMMA_OR_END));
|
|
290
|
+
ptr = scanJSONValue(ptr, end);
|
|
291
|
+
if (!ptr) return false;
|
|
292
|
+
} else if (state == ARRAY_COMMA_OR_END) {
|
|
293
|
+
if (code == 0x2c) {
|
|
294
|
+
unchecked((JSON_STATE_STACK[top] = ARRAY_VALUE));
|
|
295
|
+
ptr += 2;
|
|
296
|
+
} else if (code == 0x5d) {
|
|
297
|
+
jsonStateDepth--;
|
|
298
|
+
ptr += 2;
|
|
299
|
+
if (jsonStateDepth == 0) rootComplete = true;
|
|
300
|
+
} else {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
} else if (state == OBJECT_FIRST_KEY_OR_END) {
|
|
304
|
+
if (code == 0x7d) {
|
|
305
|
+
jsonStateDepth--;
|
|
306
|
+
ptr += 2;
|
|
307
|
+
if (jsonStateDepth == 0) rootComplete = true;
|
|
308
|
+
} else {
|
|
309
|
+
ptr = scanJSONString(ptr, end);
|
|
310
|
+
if (!ptr) return false;
|
|
311
|
+
unchecked((JSON_STATE_STACK[top] = OBJECT_COLON));
|
|
312
|
+
}
|
|
313
|
+
} else if (state == OBJECT_KEY) {
|
|
314
|
+
ptr = scanJSONString(ptr, end);
|
|
315
|
+
if (!ptr) return false;
|
|
316
|
+
unchecked((JSON_STATE_STACK[top] = OBJECT_COLON));
|
|
317
|
+
} else if (state == OBJECT_COLON) {
|
|
318
|
+
if (code != 0x3a) return false;
|
|
319
|
+
unchecked((JSON_STATE_STACK[top] = OBJECT_VALUE));
|
|
320
|
+
ptr += 2;
|
|
321
|
+
} else if (state == OBJECT_VALUE) {
|
|
322
|
+
unchecked((JSON_STATE_STACK[top] = OBJECT_COMMA_OR_END));
|
|
323
|
+
ptr = scanJSONValue(ptr, end);
|
|
324
|
+
if (!ptr) return false;
|
|
325
|
+
} else {
|
|
326
|
+
if (code == 0x2c) {
|
|
327
|
+
unchecked((JSON_STATE_STACK[top] = OBJECT_KEY));
|
|
328
|
+
ptr += 2;
|
|
329
|
+
} else if (code == 0x7d) {
|
|
330
|
+
jsonStateDepth--;
|
|
331
|
+
ptr += 2;
|
|
332
|
+
if (jsonStateDepth == 0) rootComplete = true;
|
|
333
|
+
} else {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|