@atproto/lex-json 0.1.1 → 0.1.3
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 +22 -0
- package/dist/blob.d.ts +1 -1
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +1 -1
- package/dist/blob.js.map +1 -1
- package/dist/bytes.d.ts +1 -1
- package/dist/bytes.d.ts.map +1 -1
- package/dist/bytes.js.map +1 -1
- package/dist/json-bytes-decoder.d.ts +1 -1
- package/dist/json-bytes-decoder.d.ts.map +1 -1
- package/dist/json-bytes-decoder.js.map +1 -1
- package/dist/lex-json.d.ts +2 -2
- package/dist/lex-json.d.ts.map +1 -1
- package/dist/lex-json.js +1 -1
- package/dist/lex-json.js.map +1 -1
- package/dist/link.d.ts +2 -2
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +1 -1
- package/dist/link.js.map +1 -1
- package/package.json +5 -9
- package/src/blob.ts +0 -70
- package/src/bytes.test.ts +0 -55
- package/src/bytes.ts +0 -73
- package/src/index.ts +0 -4
- package/src/json-bytes-decoder.bench.ts +0 -252
- package/src/json-bytes-decoder.test.ts +0 -889
- package/src/json-bytes-decoder.ts +0 -672
- package/src/json.ts +0 -48
- package/src/lex-json.bench.ts +0 -125
- package/src/lex-json.test.ts +0 -991
- package/src/lex-json.ts +0 -352
- package/src/link.ts +0 -101
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
|
@@ -1,672 +0,0 @@
|
|
|
1
|
-
import { LexValue, fromBase64, parseCid } from '@atproto/lex-data'
|
|
2
|
-
import { parseTypedBlobRef } from './blob.js'
|
|
3
|
-
|
|
4
|
-
const CHAR_TAB = 0x09
|
|
5
|
-
const CHAR_NEWLINE = 0x0a
|
|
6
|
-
const CHAR_CARRIAGE_RETURN = 0x0d
|
|
7
|
-
const CHAR_SPACE = 0x20
|
|
8
|
-
const CHAR_DOUBLE_QUOTE = 0x22
|
|
9
|
-
const CHAR_PLUS = 0x2b
|
|
10
|
-
const CHAR_COMMA = 0x2c
|
|
11
|
-
const CHAR_MINUS = 0x2d
|
|
12
|
-
const CHAR_PERIOD = 0x2e
|
|
13
|
-
const CHAR_SLASH = 0x2f
|
|
14
|
-
const CHAR_DIGIT_0 = 0x30
|
|
15
|
-
const CHAR_DIGIT_1 = 0x31
|
|
16
|
-
const CHAR_DIGIT_9 = 0x39
|
|
17
|
-
const CHAR_COLON = 0x3a
|
|
18
|
-
const CHAR_EQUAL = 0x3d
|
|
19
|
-
const CHAR_UPPER_A = 0x41
|
|
20
|
-
const CHAR_UPPER_E = 0x45
|
|
21
|
-
const CHAR_UPPER_F = 0x46
|
|
22
|
-
const CHAR_UPPER_Z = 0x5a
|
|
23
|
-
const CHAR_LEFT_BRACKET = 0x5b
|
|
24
|
-
const CHAR_BACKSLASH = 0x5c
|
|
25
|
-
const CHAR_RIGHT_BRACKET = 0x5d
|
|
26
|
-
const CHAR_UNDERSCORE = 0x5f
|
|
27
|
-
const CHAR_DOLLAR = 0x24
|
|
28
|
-
const CHAR_LOWER_A = 0x61
|
|
29
|
-
const CHAR_LOWER_B = 0x62
|
|
30
|
-
const CHAR_LOWER_E = 0x65
|
|
31
|
-
const CHAR_LOWER_F = 0x66
|
|
32
|
-
const CHAR_LOWER_L = 0x6c
|
|
33
|
-
const CHAR_LOWER_N = 0x6e
|
|
34
|
-
const CHAR_LOWER_R = 0x72
|
|
35
|
-
const CHAR_LOWER_S = 0x73
|
|
36
|
-
const CHAR_LOWER_T = 0x74
|
|
37
|
-
const CHAR_LOWER_U = 0x75
|
|
38
|
-
const CHAR_LOWER_Z = 0x7a
|
|
39
|
-
const CHAR_LEFT_BRACE = 0x7b
|
|
40
|
-
const CHAR_RIGHT_BRACE = 0x7d
|
|
41
|
-
|
|
42
|
-
const DECODER = new TextDecoder('utf-8', { fatal: true })
|
|
43
|
-
|
|
44
|
-
const BASE64_LOOKUP = new Int8Array(256)
|
|
45
|
-
BASE64_LOOKUP.fill(-1)
|
|
46
|
-
for (let i = CHAR_UPPER_A; i <= CHAR_UPPER_Z; i++)
|
|
47
|
-
BASE64_LOOKUP[i] = i - CHAR_UPPER_A
|
|
48
|
-
for (let i = CHAR_LOWER_A; i <= CHAR_LOWER_Z; i++)
|
|
49
|
-
BASE64_LOOKUP[i] = i - CHAR_LOWER_A + 26
|
|
50
|
-
for (let i = CHAR_DIGIT_0; i <= CHAR_DIGIT_9; i++)
|
|
51
|
-
BASE64_LOOKUP[i] = i - CHAR_DIGIT_0 + 52
|
|
52
|
-
BASE64_LOOKUP[CHAR_PLUS] = 62
|
|
53
|
-
BASE64_LOOKUP[CHAR_MINUS] = 62
|
|
54
|
-
BASE64_LOOKUP[CHAR_SLASH] = 63
|
|
55
|
-
BASE64_LOOKUP[CHAR_UNDERSCORE] = 63
|
|
56
|
-
|
|
57
|
-
const HEX_LOOKUP = new Int8Array(256)
|
|
58
|
-
HEX_LOOKUP.fill(-1)
|
|
59
|
-
for (let i = CHAR_DIGIT_0; i <= CHAR_DIGIT_9; i++)
|
|
60
|
-
HEX_LOOKUP[i] = i - CHAR_DIGIT_0
|
|
61
|
-
for (let i = CHAR_UPPER_A; i <= CHAR_UPPER_F; i++)
|
|
62
|
-
HEX_LOOKUP[i] = i - CHAR_UPPER_A + 10
|
|
63
|
-
for (let i = CHAR_LOWER_A; i <= CHAR_LOWER_F; i++)
|
|
64
|
-
HEX_LOOKUP[i] = i - CHAR_LOWER_A + 10
|
|
65
|
-
|
|
66
|
-
// Thresholds for optimization heuristics
|
|
67
|
-
export const BASE64_NATIVE_THRESHOLD = 256 // Use native decoding for base64 strings > this length
|
|
68
|
-
|
|
69
|
-
export class JsonBytesDecoder {
|
|
70
|
-
private pos = 0
|
|
71
|
-
|
|
72
|
-
constructor(
|
|
73
|
-
private readonly data: Uint8Array,
|
|
74
|
-
private readonly strict = true,
|
|
75
|
-
) {}
|
|
76
|
-
|
|
77
|
-
decode(): LexValue {
|
|
78
|
-
this.skipWhitespace()
|
|
79
|
-
const value = this.parseValue()
|
|
80
|
-
this.skipWhitespace()
|
|
81
|
-
|
|
82
|
-
if (this.pos < this.data.length) {
|
|
83
|
-
throw new TypeError(`Unexpected data after JSON at position ${this.pos}`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return value
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private parseValue(): LexValue {
|
|
90
|
-
const ch = this.data[this.pos]
|
|
91
|
-
|
|
92
|
-
// Optimize by checking most common value types first
|
|
93
|
-
// Strings and objects are very common in real JSON
|
|
94
|
-
if (ch === CHAR_DOUBLE_QUOTE) {
|
|
95
|
-
return this.parseString()
|
|
96
|
-
} else if (ch === CHAR_LEFT_BRACE) {
|
|
97
|
-
return this.parseObject()
|
|
98
|
-
} else if (ch === CHAR_LEFT_BRACKET) {
|
|
99
|
-
return this.parseArray()
|
|
100
|
-
} else if (ch === CHAR_LOWER_T) {
|
|
101
|
-
return this.parseTrue()
|
|
102
|
-
} else if (ch === CHAR_LOWER_F) {
|
|
103
|
-
return this.parseFalse()
|
|
104
|
-
} else if (ch === CHAR_LOWER_N) {
|
|
105
|
-
return this.parseNull()
|
|
106
|
-
} else {
|
|
107
|
-
// Fallback for unexpected input
|
|
108
|
-
return this.parseNumber()
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private parseObject(): LexValue {
|
|
113
|
-
this.pos++ // skip '{'
|
|
114
|
-
this.skipWhitespace()
|
|
115
|
-
|
|
116
|
-
// Check for empty object
|
|
117
|
-
if (this.data[this.pos] === CHAR_RIGHT_BRACE) {
|
|
118
|
-
this.pos++
|
|
119
|
-
return {}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let obj: Record<string, LexValue>
|
|
123
|
-
let hasDollarKey = false // Track if we've seen any $ key for validation
|
|
124
|
-
|
|
125
|
-
for (let i = 0; ; i++) {
|
|
126
|
-
this.skipWhitespace()
|
|
127
|
-
|
|
128
|
-
// Parse key
|
|
129
|
-
if (this.data[this.pos] !== CHAR_DOUBLE_QUOTE) {
|
|
130
|
-
throw new TypeError(`Expected string key at position ${this.pos}`)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Track special keys for later validation
|
|
134
|
-
if (this.data[this.pos + 1] === CHAR_DOLLAR) {
|
|
135
|
-
hasDollarKey = true
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const key = this.parseString()
|
|
139
|
-
|
|
140
|
-
// Prevent prototype pollution
|
|
141
|
-
if (key === '__proto__') {
|
|
142
|
-
throw new TypeError('JSON object keys cannot be "__proto__"')
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.skipWhitespace()
|
|
146
|
-
|
|
147
|
-
// Parse colon
|
|
148
|
-
if (this.data[this.pos] !== CHAR_COLON) {
|
|
149
|
-
throw new TypeError(`Expected ':' at position ${this.pos}`)
|
|
150
|
-
}
|
|
151
|
-
this.pos++
|
|
152
|
-
this.skipWhitespace()
|
|
153
|
-
|
|
154
|
-
// Parse $bytes or $link if it's the first and only key
|
|
155
|
-
if (i === 0) {
|
|
156
|
-
if (key === '$bytes' && this.data[this.pos] === CHAR_DOUBLE_QUOTE) {
|
|
157
|
-
const initialPos = this.pos
|
|
158
|
-
const b64Start = initialPos + 1
|
|
159
|
-
const b64End = this.data.indexOf(CHAR_DOUBLE_QUOTE, b64Start)
|
|
160
|
-
if (b64End !== -1) {
|
|
161
|
-
this.pos = b64End + 1
|
|
162
|
-
this.skipWhitespace()
|
|
163
|
-
if (this.data[this.pos] === CHAR_RIGHT_BRACE) {
|
|
164
|
-
this.pos++
|
|
165
|
-
|
|
166
|
-
const base64Len = b64End - b64Start
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
// Use native decoding for large base64 strings (much faster
|
|
170
|
-
// based on benchmarks)
|
|
171
|
-
if (base64Len > BASE64_NATIVE_THRESHOLD) {
|
|
172
|
-
const b64Str = this.decodeUnescapedString(b64Start, b64End)
|
|
173
|
-
return fromBase64(b64Str) // Validate and convert to LexValue bytes
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Manual decoding for smaller strings (optimized path)
|
|
177
|
-
// Skip padding characters
|
|
178
|
-
let b64EndNoPadding = b64End
|
|
179
|
-
while (
|
|
180
|
-
b64EndNoPadding > b64Start &&
|
|
181
|
-
this.data[b64EndNoPadding - 1] === CHAR_EQUAL
|
|
182
|
-
) {
|
|
183
|
-
b64EndNoPadding--
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const base64LenNoPadding = b64EndNoPadding - b64Start
|
|
187
|
-
const bytesLen = Math.floor((base64LenNoPadding * 3) / 4)
|
|
188
|
-
const result = new Uint8Array(bytesLen)
|
|
189
|
-
|
|
190
|
-
for (
|
|
191
|
-
let i = b64Start, j = 0;
|
|
192
|
-
i <= b64EndNoPadding - 4;
|
|
193
|
-
i += 4
|
|
194
|
-
) {
|
|
195
|
-
const chunk =
|
|
196
|
-
(this.base64Value(this.data[i]) << 18) |
|
|
197
|
-
(this.base64Value(this.data[i + 1]) << 12) |
|
|
198
|
-
(this.base64Value(this.data[i + 2]) << 6) |
|
|
199
|
-
this.base64Value(this.data[i + 3])
|
|
200
|
-
|
|
201
|
-
result[j++] = (chunk >> 16) & 0xff
|
|
202
|
-
result[j++] = (chunk >> 8) & 0xff
|
|
203
|
-
result[j++] = chunk & 0xff
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Handle remaining characters (if any)
|
|
207
|
-
if (base64LenNoPadding % 4 === 2) {
|
|
208
|
-
const chunk =
|
|
209
|
-
(this.base64Value(this.data[b64EndNoPadding - 2]) << 18) |
|
|
210
|
-
(this.base64Value(this.data[b64EndNoPadding - 1]) << 12)
|
|
211
|
-
result[bytesLen - 1] = (chunk >> 16) & 0xff
|
|
212
|
-
} else if (base64LenNoPadding % 4 === 3) {
|
|
213
|
-
const chunk =
|
|
214
|
-
(this.base64Value(this.data[b64EndNoPadding - 3]) << 18) |
|
|
215
|
-
(this.base64Value(this.data[b64EndNoPadding - 2]) << 12) |
|
|
216
|
-
(this.base64Value(this.data[b64EndNoPadding - 1]) << 6)
|
|
217
|
-
result[bytesLen - 2] = (chunk >> 16) & 0xff
|
|
218
|
-
result[bytesLen - 1] = (chunk >> 8) & 0xff
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return result
|
|
222
|
-
} catch (cause) {
|
|
223
|
-
if (this.strict) {
|
|
224
|
-
throw new TypeError('Invalid $bytes object', { cause })
|
|
225
|
-
}
|
|
226
|
-
// ignore and parse as regular object
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
this.pos = initialPos // reset position to parse string properly
|
|
232
|
-
} else if (
|
|
233
|
-
key === '$link' &&
|
|
234
|
-
this.data[this.pos] === CHAR_DOUBLE_QUOTE
|
|
235
|
-
) {
|
|
236
|
-
const initialPos = this.pos
|
|
237
|
-
const cidStart = initialPos + 1
|
|
238
|
-
const cidEnd = this.data.indexOf(CHAR_DOUBLE_QUOTE, cidStart)
|
|
239
|
-
if (cidEnd !== -1) {
|
|
240
|
-
this.pos = cidEnd + 1
|
|
241
|
-
this.skipWhitespace()
|
|
242
|
-
if (this.data[this.pos] === CHAR_RIGHT_BRACE) {
|
|
243
|
-
this.pos++
|
|
244
|
-
const cidStr = this.decodeUnescapedString(cidStart, cidEnd)
|
|
245
|
-
try {
|
|
246
|
-
return parseCid(cidStr)
|
|
247
|
-
} catch (cause) {
|
|
248
|
-
if (this.strict) {
|
|
249
|
-
throw new TypeError('Invalid $link object', { cause })
|
|
250
|
-
}
|
|
251
|
-
// ignore
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
this.pos = initialPos // reset position to parse string properly
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Parse value
|
|
261
|
-
obj ??= {}
|
|
262
|
-
obj[key] = this.parseValue()
|
|
263
|
-
|
|
264
|
-
this.skipWhitespace()
|
|
265
|
-
|
|
266
|
-
const next = this.data[this.pos]
|
|
267
|
-
if (next === CHAR_RIGHT_BRACE) {
|
|
268
|
-
this.pos++
|
|
269
|
-
break
|
|
270
|
-
} else if (next === CHAR_COMMA) {
|
|
271
|
-
this.pos++
|
|
272
|
-
} else {
|
|
273
|
-
throw new TypeError(`Expected ',' or '}' at position ${this.pos}`)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// In strict mode, validate special objects with extra keys
|
|
278
|
-
// Only check if we've seen a $ key (optimization)
|
|
279
|
-
if (hasDollarKey && this.strict) {
|
|
280
|
-
if (obj.$bytes !== undefined) {
|
|
281
|
-
throw new TypeError('Invalid $bytes object')
|
|
282
|
-
} else if (obj.$link !== undefined) {
|
|
283
|
-
throw new TypeError('Invalid $link object')
|
|
284
|
-
} else if (obj.$type === 'blob') {
|
|
285
|
-
const blob = parseTypedBlobRef(obj, { strict: this.strict })
|
|
286
|
-
if (blob) return blob
|
|
287
|
-
throw new TypeError(`Invalid blob object`)
|
|
288
|
-
} else if (obj.$type !== undefined) {
|
|
289
|
-
if (typeof obj.$type !== 'string') {
|
|
290
|
-
throw new TypeError(`Invalid $type property (${typeof obj.$type})`)
|
|
291
|
-
} else if (obj.$type.length === 0) {
|
|
292
|
-
throw new TypeError(`Empty $type property`)
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return obj
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private parseArray(): LexValue[] {
|
|
301
|
-
this.pos++ // skip '['
|
|
302
|
-
this.skipWhitespace()
|
|
303
|
-
|
|
304
|
-
const arr: LexValue[] = []
|
|
305
|
-
|
|
306
|
-
// Check for empty array
|
|
307
|
-
if (this.data[this.pos] === CHAR_RIGHT_BRACKET) {
|
|
308
|
-
this.pos++
|
|
309
|
-
return arr
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
for (;;) {
|
|
313
|
-
this.skipWhitespace()
|
|
314
|
-
arr.push(this.parseValue())
|
|
315
|
-
this.skipWhitespace()
|
|
316
|
-
|
|
317
|
-
const next = this.data[this.pos]
|
|
318
|
-
if (next === CHAR_RIGHT_BRACKET) {
|
|
319
|
-
this.pos++
|
|
320
|
-
break
|
|
321
|
-
} else if (next === CHAR_COMMA) {
|
|
322
|
-
this.pos++
|
|
323
|
-
} else {
|
|
324
|
-
throw new TypeError(`Expected ',' or ']' at position ${this.pos}`)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return arr
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private parseString(): string {
|
|
332
|
-
this.pos++ // skip opening quote
|
|
333
|
-
const start = this.pos
|
|
334
|
-
|
|
335
|
-
// Fast path: scan for quote, checking for escapes and control chars inline
|
|
336
|
-
// Optimized for the common case of strings without escapes
|
|
337
|
-
let i = this.pos
|
|
338
|
-
while (i < this.data.length) {
|
|
339
|
-
const ch = this.data[i]
|
|
340
|
-
|
|
341
|
-
if (ch === CHAR_DOUBLE_QUOTE) {
|
|
342
|
-
// Found end quote - fast path success
|
|
343
|
-
this.pos = i + 1
|
|
344
|
-
return this.decodeUnescapedString(start, i)
|
|
345
|
-
} else if (ch === CHAR_BACKSLASH) {
|
|
346
|
-
// Found escape or control character - need slow path
|
|
347
|
-
break
|
|
348
|
-
} else if (ch < 0x20) {
|
|
349
|
-
throw new TypeError(`Unescaped control character at position ${i}`)
|
|
350
|
-
}
|
|
351
|
-
i++
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Slow path: handle escapes or control characters
|
|
355
|
-
if (i >= this.data.length) {
|
|
356
|
-
throw new TypeError('Unterminated string')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// We hit a backslash - need to process escape sequences
|
|
360
|
-
let result = ''
|
|
361
|
-
let segmentStart = start
|
|
362
|
-
|
|
363
|
-
this.pos = i
|
|
364
|
-
while (this.pos < this.data.length) {
|
|
365
|
-
const ch = this.data[this.pos]
|
|
366
|
-
|
|
367
|
-
if (ch === CHAR_DOUBLE_QUOTE) {
|
|
368
|
-
// Found end of string
|
|
369
|
-
if (segmentStart < this.pos) {
|
|
370
|
-
result += this.decodeUnescapedString(segmentStart, this.pos)
|
|
371
|
-
}
|
|
372
|
-
this.pos++
|
|
373
|
-
return result
|
|
374
|
-
} else if (ch === CHAR_BACKSLASH) {
|
|
375
|
-
// Process escape sequence
|
|
376
|
-
if (segmentStart < this.pos) {
|
|
377
|
-
result += this.decodeUnescapedString(segmentStart, this.pos)
|
|
378
|
-
}
|
|
379
|
-
this.pos++ // skip backslash
|
|
380
|
-
result += this.parseEscapeSequence()
|
|
381
|
-
segmentStart = this.pos
|
|
382
|
-
} else if (ch < 0x20) {
|
|
383
|
-
throw new TypeError(
|
|
384
|
-
`Unescaped control character at position ${this.pos}`,
|
|
385
|
-
)
|
|
386
|
-
} else {
|
|
387
|
-
this.pos++
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
throw new TypeError('Unterminated string')
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
private parseEscapeSequence(): string {
|
|
395
|
-
const ch = this.data[this.pos++]
|
|
396
|
-
|
|
397
|
-
switch (ch) {
|
|
398
|
-
case CHAR_DOUBLE_QUOTE:
|
|
399
|
-
return '"'
|
|
400
|
-
case CHAR_BACKSLASH:
|
|
401
|
-
return '\\'
|
|
402
|
-
case CHAR_SLASH:
|
|
403
|
-
return '/'
|
|
404
|
-
case CHAR_LOWER_B:
|
|
405
|
-
return '\b'
|
|
406
|
-
case CHAR_LOWER_F:
|
|
407
|
-
return '\f'
|
|
408
|
-
case CHAR_LOWER_N:
|
|
409
|
-
return '\n'
|
|
410
|
-
case CHAR_LOWER_R:
|
|
411
|
-
return '\r'
|
|
412
|
-
case CHAR_LOWER_T:
|
|
413
|
-
return '\t'
|
|
414
|
-
case CHAR_LOWER_U:
|
|
415
|
-
return this.parseUnicodeEscape()
|
|
416
|
-
default:
|
|
417
|
-
throw new TypeError(`Invalid escape sequence at position ${this.pos}`)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
private parseUnicodeEscape(): string {
|
|
422
|
-
// Parse \uXXXX
|
|
423
|
-
let codePoint = 0
|
|
424
|
-
for (let i = 0; i < 4; i++) {
|
|
425
|
-
const ch = this.data[this.pos++]
|
|
426
|
-
const hex = this.hexValue(ch)
|
|
427
|
-
codePoint = (codePoint << 4) | hex
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Handle surrogate pairs
|
|
431
|
-
if (codePoint >= 0xd800 && codePoint <= 0xdbff) {
|
|
432
|
-
// High surrogate, check if followed by low surrogate
|
|
433
|
-
if (
|
|
434
|
-
this.pos + 5 < this.data.length &&
|
|
435
|
-
this.data[this.pos] === CHAR_BACKSLASH &&
|
|
436
|
-
this.data[this.pos + 1] === CHAR_LOWER_U
|
|
437
|
-
) {
|
|
438
|
-
// Save position in case we need to backtrack
|
|
439
|
-
const savedPos = this.pos
|
|
440
|
-
this.pos += 2
|
|
441
|
-
let low = 0
|
|
442
|
-
for (let i = 0; i < 4; i++) {
|
|
443
|
-
const ch = this.data[this.pos++]
|
|
444
|
-
const hex = this.hexValue(ch)
|
|
445
|
-
low = (low << 4) | hex
|
|
446
|
-
}
|
|
447
|
-
// Check if it's a valid low surrogate
|
|
448
|
-
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
449
|
-
// Valid pair - combine into single codepoint
|
|
450
|
-
codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (low - 0xdc00)
|
|
451
|
-
} else {
|
|
452
|
-
// Not a low surrogate - backtrack so it gets processed separately
|
|
453
|
-
this.pos = savedPos
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return String.fromCodePoint(codePoint)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private hexValue(ch: number): number {
|
|
462
|
-
const value = HEX_LOOKUP[ch]
|
|
463
|
-
if (value !== -1) return value
|
|
464
|
-
throw new TypeError(`Invalid unicode escape at position ${this.pos}`)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private base64Value(ch: number): number {
|
|
468
|
-
const value = BASE64_LOOKUP[ch]
|
|
469
|
-
if (value !== -1) return value
|
|
470
|
-
throw new TypeError(
|
|
471
|
-
`Invalid base64 character: ${String.fromCharCode(ch)} at position ${this.pos}`,
|
|
472
|
-
)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
private decodeUnescapedString(start: number, end: number): string {
|
|
476
|
-
const len = end - start
|
|
477
|
-
if (len === 0) return ''
|
|
478
|
-
|
|
479
|
-
// Fast path for very short ASCII strings (common for object keys like "id", "name", etc.)
|
|
480
|
-
// Heuristic: only worth it for strings <= 20 chars where String.fromCharCode is faster
|
|
481
|
-
// This is a hot path for object keys
|
|
482
|
-
if (len <= 20) {
|
|
483
|
-
let result = ''
|
|
484
|
-
for (let i = start; i < end; i++) {
|
|
485
|
-
const byte = this.data[i]
|
|
486
|
-
if (byte > 0x7f) {
|
|
487
|
-
// Hit non-ASCII, fall back to TextDecoder for full UTF-8 decoding
|
|
488
|
-
const subView = new Uint8Array(
|
|
489
|
-
this.data.buffer,
|
|
490
|
-
this.data.byteOffset + start,
|
|
491
|
-
len,
|
|
492
|
-
)
|
|
493
|
-
return DECODER.decode(subView)
|
|
494
|
-
}
|
|
495
|
-
result += String.fromCharCode(byte)
|
|
496
|
-
}
|
|
497
|
-
return result
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// For longer strings, use utf8FromBytes directly (it's highly optimized)
|
|
501
|
-
const subView = new Uint8Array(
|
|
502
|
-
this.data.buffer,
|
|
503
|
-
this.data.byteOffset + start,
|
|
504
|
-
len,
|
|
505
|
-
)
|
|
506
|
-
return DECODER.decode(subView)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private parseNumber(): number {
|
|
510
|
-
const start = this.pos
|
|
511
|
-
|
|
512
|
-
let sign = 1
|
|
513
|
-
let int = 0
|
|
514
|
-
let decimal = 0
|
|
515
|
-
let expSign = 1
|
|
516
|
-
let exp = 0
|
|
517
|
-
|
|
518
|
-
// Parse sign
|
|
519
|
-
if (this.data[this.pos] === CHAR_MINUS) {
|
|
520
|
-
sign = -1
|
|
521
|
-
this.pos++
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Parse integer part
|
|
525
|
-
if (this.data[this.pos] === CHAR_DIGIT_0) {
|
|
526
|
-
this.pos++
|
|
527
|
-
// Leading zero must be followed by decimal, exponent, or end
|
|
528
|
-
} else if (
|
|
529
|
-
// Note: cannot start with "0"
|
|
530
|
-
this.data[this.pos] >= CHAR_DIGIT_1 &&
|
|
531
|
-
this.data[this.pos] <= CHAR_DIGIT_9
|
|
532
|
-
) {
|
|
533
|
-
do {
|
|
534
|
-
int = int * 10 + (this.data[this.pos] - CHAR_DIGIT_0)
|
|
535
|
-
this.pos++
|
|
536
|
-
} while (
|
|
537
|
-
this.pos < this.data.length &&
|
|
538
|
-
this.data[this.pos] >= CHAR_DIGIT_0 &&
|
|
539
|
-
this.data[this.pos] <= CHAR_DIGIT_9
|
|
540
|
-
)
|
|
541
|
-
} else {
|
|
542
|
-
throw new TypeError(`Unexpected character at position ${this.pos}`)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Strict mode validation is deferred until after decimal/exponent parsing
|
|
546
|
-
// so that we can include the complete number value in the error message.
|
|
547
|
-
|
|
548
|
-
// Parse decimal part
|
|
549
|
-
if (this.pos < this.data.length && this.data[this.pos] === CHAR_PERIOD) {
|
|
550
|
-
this.pos++
|
|
551
|
-
if (
|
|
552
|
-
this.pos >= this.data.length ||
|
|
553
|
-
this.data[this.pos] < CHAR_DIGIT_0 ||
|
|
554
|
-
this.data[this.pos] > CHAR_DIGIT_9
|
|
555
|
-
) {
|
|
556
|
-
throw new TypeError(`Invalid number at position ${start}`)
|
|
557
|
-
}
|
|
558
|
-
let decimalPlace = 0.1
|
|
559
|
-
do {
|
|
560
|
-
decimal += (this.data[this.pos] - CHAR_DIGIT_0) * decimalPlace
|
|
561
|
-
decimalPlace *= 0.1
|
|
562
|
-
this.pos++
|
|
563
|
-
} while (
|
|
564
|
-
this.pos < this.data.length &&
|
|
565
|
-
this.data[this.pos] >= CHAR_DIGIT_0 &&
|
|
566
|
-
this.data[this.pos] <= CHAR_DIGIT_9
|
|
567
|
-
)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Parse exponent part
|
|
571
|
-
if (
|
|
572
|
-
this.pos < this.data.length &&
|
|
573
|
-
(this.data[this.pos] === CHAR_LOWER_E ||
|
|
574
|
-
this.data[this.pos] === CHAR_UPPER_E)
|
|
575
|
-
) {
|
|
576
|
-
this.pos++
|
|
577
|
-
if (
|
|
578
|
-
this.pos < this.data.length &&
|
|
579
|
-
(this.data[this.pos] === CHAR_PLUS ||
|
|
580
|
-
this.data[this.pos] === CHAR_MINUS)
|
|
581
|
-
) {
|
|
582
|
-
expSign = this.data[this.pos] === CHAR_MINUS ? -1 : 1
|
|
583
|
-
this.pos++ // skip + or -
|
|
584
|
-
}
|
|
585
|
-
if (
|
|
586
|
-
this.pos >= this.data.length ||
|
|
587
|
-
this.data[this.pos] < CHAR_DIGIT_0 ||
|
|
588
|
-
this.data[this.pos] > CHAR_DIGIT_9
|
|
589
|
-
) {
|
|
590
|
-
throw new TypeError(`Invalid number at position ${start}`)
|
|
591
|
-
}
|
|
592
|
-
do {
|
|
593
|
-
exp = exp * 10 + (this.data[this.pos] - CHAR_DIGIT_0)
|
|
594
|
-
this.pos++
|
|
595
|
-
} while (
|
|
596
|
-
this.pos < this.data.length &&
|
|
597
|
-
this.data[this.pos] >= CHAR_DIGIT_0 &&
|
|
598
|
-
this.data[this.pos] <= CHAR_DIGIT_9
|
|
599
|
-
)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const num = sign * (int + decimal) * Math.pow(10, expSign * exp)
|
|
603
|
-
|
|
604
|
-
if (this.strict && !Number.isSafeInteger(num)) {
|
|
605
|
-
throw new TypeError(`Invalid non-integer number: ${num}`)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
return num
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
private parseTrue(): boolean {
|
|
612
|
-
if (
|
|
613
|
-
this.pos + 4 <= this.data.length &&
|
|
614
|
-
this.data[this.pos] === CHAR_LOWER_T &&
|
|
615
|
-
this.data[this.pos + 1] === CHAR_LOWER_R &&
|
|
616
|
-
this.data[this.pos + 2] === CHAR_LOWER_U &&
|
|
617
|
-
this.data[this.pos + 3] === CHAR_LOWER_E
|
|
618
|
-
) {
|
|
619
|
-
this.pos += 4
|
|
620
|
-
return true
|
|
621
|
-
}
|
|
622
|
-
throw new TypeError(`Unexpected token at position ${this.pos}`)
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
private parseFalse(): boolean {
|
|
626
|
-
if (
|
|
627
|
-
this.pos + 5 <= this.data.length &&
|
|
628
|
-
this.data[this.pos] === CHAR_LOWER_F &&
|
|
629
|
-
this.data[this.pos + 1] === CHAR_LOWER_A &&
|
|
630
|
-
this.data[this.pos + 2] === CHAR_LOWER_L &&
|
|
631
|
-
this.data[this.pos + 3] === CHAR_LOWER_S &&
|
|
632
|
-
this.data[this.pos + 4] === CHAR_LOWER_E
|
|
633
|
-
) {
|
|
634
|
-
this.pos += 5
|
|
635
|
-
return false
|
|
636
|
-
}
|
|
637
|
-
throw new TypeError(`Unexpected token at position ${this.pos}`)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private parseNull(): null {
|
|
641
|
-
if (
|
|
642
|
-
this.pos + 4 <= this.data.length &&
|
|
643
|
-
this.data[this.pos] === CHAR_LOWER_N &&
|
|
644
|
-
this.data[this.pos + 1] === CHAR_LOWER_U &&
|
|
645
|
-
this.data[this.pos + 2] === CHAR_LOWER_L &&
|
|
646
|
-
this.data[this.pos + 3] === CHAR_LOWER_L
|
|
647
|
-
) {
|
|
648
|
-
this.pos += 4
|
|
649
|
-
return null
|
|
650
|
-
}
|
|
651
|
-
throw new TypeError(`Unexpected token at position ${this.pos}`)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
private skipWhitespace(): void {
|
|
655
|
-
// Optimized: check most common case (space) first, and use <= for compact check
|
|
656
|
-
while (this.pos < this.data.length) {
|
|
657
|
-
const ch = this.data[this.pos]
|
|
658
|
-
// Optimize for the most common case: space (0x20)
|
|
659
|
-
if (ch === CHAR_SPACE) {
|
|
660
|
-
this.pos++
|
|
661
|
-
} else if (
|
|
662
|
-
ch === CHAR_TAB ||
|
|
663
|
-
ch === CHAR_NEWLINE ||
|
|
664
|
-
ch === CHAR_CARRIAGE_RETURN
|
|
665
|
-
) {
|
|
666
|
-
this.pos++
|
|
667
|
-
} else {
|
|
668
|
-
break
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
package/src/json.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Primitive JSON values: string, number, boolean, or null.
|
|
3
|
-
*
|
|
4
|
-
* These are the scalar (non-composite) types that can appear in JSON data.
|
|
5
|
-
* In the context of the AT Protocol:
|
|
6
|
-
* - `string` - Text values, including special encoded types like `$link` and `$bytes`
|
|
7
|
-
* - `number` - Numeric values (note: Lex only supports safe integers)
|
|
8
|
-
* - `boolean` - True/false values
|
|
9
|
-
* - `null` - Explicit null values
|
|
10
|
-
*
|
|
11
|
-
* @see {@link JsonValue} for the complete JSON type including arrays and objects
|
|
12
|
-
*/
|
|
13
|
-
export type JsonScalar = number | string | boolean | null
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Any valid JSON value.
|
|
17
|
-
*
|
|
18
|
-
* This is a recursive type that represents the full JSON data model:
|
|
19
|
-
* - Scalars: string, number, boolean, null
|
|
20
|
-
* - Arrays: ordered lists of JSON values
|
|
21
|
-
* - Objects: string-keyed maps of JSON values
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* const scalar: JsonValue = "hello"
|
|
26
|
-
* const array: JsonValue = [1, 2, 3]
|
|
27
|
-
* const object: JsonValue = { name: "Alice", age: 30 }
|
|
28
|
-
* const nested: JsonValue = { users: [{ name: "Bob" }] }
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export type JsonValue = JsonScalar | JsonValue[] | { [_ in string]?: JsonValue }
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A JSON object with string keys and JSON values.
|
|
35
|
-
*
|
|
36
|
-
* This type represents a plain JavaScript object that is valid JSON,
|
|
37
|
-
* where all keys are strings and all values are valid JSON values.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```typescript
|
|
41
|
-
* const obj: JsonObject = {
|
|
42
|
-
* name: "Alice",
|
|
43
|
-
* tags: ["admin", "user"],
|
|
44
|
-
* metadata: { created: "2024-01-01" }
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
export type JsonObject = { [_ in string]?: JsonValue }
|