@marcuspuchalla/nachos 0.1.3 → 0.2.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 +75 -0
- package/dist/{chunk-PTWN7K3Y.cjs → chunk-3Z45RBZP.cjs} +469 -244
- package/dist/chunk-3Z45RBZP.cjs.map +1 -0
- package/dist/{chunk-2MTLSQ7E.js → chunk-EDXZTSIA.js} +224 -166
- package/dist/chunk-EDXZTSIA.js.map +1 -0
- package/dist/{chunk-R62CQQNI.cjs → chunk-HMUA5KLG.cjs} +239 -181
- package/dist/chunk-HMUA5KLG.cjs.map +1 -0
- package/dist/{chunk-ZDZ2B5PE.js → chunk-JESIF5IF.js} +7 -3
- package/dist/chunk-JESIF5IF.js.map +1 -0
- package/dist/{chunk-5A5T56JB.js → chunk-LWNWC2O7.js} +442 -217
- package/dist/chunk-LWNWC2O7.js.map +1 -0
- package/dist/{chunk-PD72MVTX.cjs → chunk-P6A2OOIY.cjs} +7 -3
- package/dist/chunk-P6A2OOIY.cjs.map +1 -0
- package/dist/encoder/index.cjs +14 -14
- package/dist/encoder/index.d.cts +5 -4
- package/dist/encoder/index.d.ts +5 -4
- package/dist/encoder/index.js +2 -2
- package/dist/index.cjs +58 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -21
- package/dist/index.d.ts +40 -21
- package/dist/index.js +37 -17
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/parser/index.cjs +21 -21
- package/dist/parser/index.d.cts +4 -2
- package/dist/parser/index.d.ts +4 -2
- package/dist/parser/index.js +2 -2
- package/dist/{types-DvNlfbKB.d.cts → types-eG2qalpr.d.cts} +27 -1
- package/dist/{types-DvNlfbKB.d.ts → types-eG2qalpr.d.ts} +27 -1
- package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -3
- package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -3
- package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-D4d7xG3-.d.cts} +9 -4
- package/dist/{useCborTag-Cs1CZuXZ.d.cts → useCborTag-TYst1KR6.d.ts} +9 -4
- package/package.json +1 -1
- package/src/__tests__/audit-fixes.test.ts +141 -0
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +5 -6
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +30 -26
- package/src/encoder/composables/useCborEncoder.ts +40 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +40 -9
- package/src/encoder/types.ts +9 -4
- package/src/encoder/utils.ts +33 -1
- package/src/index.ts +39 -20
- package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
- package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
- package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
- package/src/parser/__tests__/utils-errors.test.ts +11 -3
- package/src/parser/composables/useCborCollection.ts +51 -45
- package/src/parser/composables/useCborDiagnostic.ts +28 -0
- package/src/parser/composables/useCborFloat.ts +2 -1
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +448 -208
- package/src/parser/composables/useCborTag.ts +53 -38
- package/src/parser/types.ts +32 -1
- package/src/parser/utils.ts +52 -0
- package/dist/chunk-2MTLSQ7E.js.map +0 -1
- package/dist/chunk-5A5T56JB.js.map +0 -1
- package/dist/chunk-PD72MVTX.cjs.map +0 -1
- package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
- package/dist/chunk-R62CQQNI.cjs.map +0 -1
- package/dist/chunk-ZDZ2B5PE.js.map +0 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Auto-detects major type and dispatches to appropriate parser
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ParseResult, ParseResultWithMap, SourceMapEntry, ParseOptions, CborContext, CborValue } from '../types'
|
|
7
|
+
import type { ParseResult, ParseResultWithMap, SourceMapEntry, ParseOptions, CborContext, CborValue, TaggedValue } from '../types'
|
|
8
8
|
import { DEFAULT_OPTIONS, DEFAULT_LIMITS } from '../types'
|
|
9
|
-
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader } from '../utils'
|
|
9
|
+
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, serializeValueForComparison, validateCanonicalInteger } from '../utils'
|
|
10
10
|
import { useCborInteger } from './useCborInteger'
|
|
11
11
|
import { useCborString } from './useCborString'
|
|
12
12
|
import { useCborCollection } from './useCborCollection'
|
|
@@ -47,6 +47,9 @@ export function useCborParser() {
|
|
|
47
47
|
validateSetUniqueness: options.validateSetUniqueness ?? (options.strict ? true : DEFAULT_OPTIONS.validateSetUniqueness),
|
|
48
48
|
validateTagSemantics: options.validateTagSemantics ?? (options.strict ? true : DEFAULT_OPTIONS.validateTagSemantics),
|
|
49
49
|
validatePlutusSemantics: options.validatePlutusSemantics ?? (options.strict ? true : DEFAULT_OPTIONS.validatePlutusSemantics),
|
|
50
|
+
mapKeyOrder: options.mapKeyOrder ?? DEFAULT_OPTIONS.mapKeyOrder,
|
|
51
|
+
// Strict mode rejects trailing data after the top-level item (well-formedness).
|
|
52
|
+
allowTrailingData: options.allowTrailingData ?? (options.strict ? false : DEFAULT_OPTIONS.allowTrailingData),
|
|
50
53
|
limits: {
|
|
51
54
|
maxInputSize: options.limits?.maxInputSize ?? DEFAULT_LIMITS.maxInputSize,
|
|
52
55
|
maxOutputSize: options.limits?.maxOutputSize ?? DEFAULT_LIMITS.maxOutputSize,
|
|
@@ -73,11 +76,11 @@ export function useCborParser() {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
const { parseInteger } = useCborInteger()
|
|
77
|
-
const { parseString } = useCborString()
|
|
79
|
+
const { parseInteger, parseIntegerFromBuffer: integerFromBuffer } = useCborInteger()
|
|
80
|
+
const { parseString, parseByteString: byteStringFromBuffer, parseTextString: textStringFromBuffer } = useCborString()
|
|
78
81
|
const { parseArray, parseMap } = useCborCollection()
|
|
79
|
-
const { parseTag } = useCborTag()
|
|
80
|
-
const { parse: parseFloatOrSimple } = useCborFloat()
|
|
82
|
+
const { parseTag, validateTagSemantics, decodePlutusConstructor } = useCborTag()
|
|
83
|
+
const { parse: parseFloatOrSimple, parseFromBuffer: floatOrSimpleFromBuffer } = useCborFloat()
|
|
81
84
|
|
|
82
85
|
/**
|
|
83
86
|
* Parses a CBOR hex string, auto-detecting the type
|
|
@@ -100,9 +103,28 @@ export function useCborParser() {
|
|
|
100
103
|
* parse('6449455446', { strict: true })
|
|
101
104
|
* ```
|
|
102
105
|
*/
|
|
103
|
-
const parse = (
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
+
const parse = (input: string | Uint8Array, options?: ParseOptions): ParseResult => {
|
|
107
|
+
// Merge options with defaults
|
|
108
|
+
const mergedOptions = mergeOptions(options)
|
|
109
|
+
|
|
110
|
+
// Uint8Array fast path: skip hex conversion entirely
|
|
111
|
+
if (input instanceof Uint8Array) {
|
|
112
|
+
if (input.length === 0) {
|
|
113
|
+
throw new Error('Empty input')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check input size limit
|
|
117
|
+
if (mergedOptions.limits?.maxInputSize && input.length > mergedOptions.limits.maxInputSize) {
|
|
118
|
+
throw new Error(`Input size ${input.length} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const bufResult = dispatchFromBuffer(input, 0, mergedOptions)
|
|
122
|
+
checkTrailingData(bufResult.bytesRead, input.length, mergedOptions)
|
|
123
|
+
return bufResult
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Hex string path
|
|
127
|
+
const cleanHex = input.replace(/\s+/g, '')
|
|
106
128
|
|
|
107
129
|
// Validate hex string
|
|
108
130
|
if (!cleanHex || cleanHex.length === 0) {
|
|
@@ -117,9 +139,6 @@ export function useCborParser() {
|
|
|
117
139
|
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
// Merge options with defaults
|
|
121
|
-
const mergedOptions = mergeOptions(options)
|
|
122
|
-
|
|
123
142
|
// Check input size limit
|
|
124
143
|
const inputSize = cleanHex.length / 2 // Convert hex chars to bytes
|
|
125
144
|
if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
|
|
@@ -132,30 +151,57 @@ export function useCborParser() {
|
|
|
132
151
|
const { majorType } = extractCborHeader(initialByte)
|
|
133
152
|
|
|
134
153
|
// Dispatch to appropriate parser based on major type
|
|
154
|
+
let result: ParseResult
|
|
135
155
|
switch (majorType) {
|
|
136
156
|
case 0: // Unsigned integer
|
|
137
157
|
case 1: // Negative integer
|
|
138
|
-
|
|
158
|
+
result = parseInteger(cleanHex, mergedOptions)
|
|
159
|
+
break
|
|
139
160
|
|
|
140
161
|
case 2: // Byte string
|
|
141
162
|
case 3: // Text string
|
|
142
|
-
|
|
163
|
+
result = parseString(cleanHex, mergedOptions)
|
|
164
|
+
break
|
|
143
165
|
|
|
144
166
|
case 4: // Array
|
|
145
|
-
|
|
167
|
+
result = parseArray(cleanHex, mergedOptions)
|
|
168
|
+
break
|
|
146
169
|
|
|
147
170
|
case 5: // Map
|
|
148
|
-
|
|
171
|
+
result = parseMap(cleanHex, mergedOptions)
|
|
172
|
+
break
|
|
149
173
|
|
|
150
174
|
case 6: // Tagged value
|
|
151
|
-
|
|
175
|
+
result = parseTag(cleanHex, mergedOptions)
|
|
176
|
+
break
|
|
152
177
|
|
|
153
178
|
case 7: // Floating-point or simple value
|
|
154
|
-
|
|
179
|
+
result = parseFloatOrSimple(cleanHex, mergedOptions)
|
|
180
|
+
break
|
|
155
181
|
|
|
156
182
|
default:
|
|
157
183
|
throw new Error(`Unknown major type: ${majorType}`)
|
|
158
184
|
}
|
|
185
|
+
|
|
186
|
+
checkTrailingData(result.bytesRead, buffer.length, mergedOptions)
|
|
187
|
+
return result
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Rejects trailing bytes after the top-level data item when
|
|
192
|
+
* allowTrailingData is false (RFC 8949 well-formedness for a single item).
|
|
193
|
+
*/
|
|
194
|
+
const checkTrailingData = (
|
|
195
|
+
bytesRead: number,
|
|
196
|
+
totalLength: number,
|
|
197
|
+
opts: Required<ParseOptions>
|
|
198
|
+
): void => {
|
|
199
|
+
if (!opts.allowTrailingData && bytesRead < totalLength) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Trailing data: ${totalLength - bytesRead} byte(s) remain after the top-level CBOR item ` +
|
|
202
|
+
`(bytesRead=${bytesRead}, length=${totalLength}). Use parseSequence to decode multiple items.`
|
|
203
|
+
)
|
|
204
|
+
}
|
|
159
205
|
}
|
|
160
206
|
|
|
161
207
|
/**
|
|
@@ -165,30 +211,46 @@ export function useCborParser() {
|
|
|
165
211
|
* @param options - Parser options (optional)
|
|
166
212
|
* @returns Parsed value, bytes read, and source map
|
|
167
213
|
*/
|
|
168
|
-
const parseWithSourceMap = (
|
|
169
|
-
const cleanHex = hexString.replace(/\s+/g, '')
|
|
170
|
-
|
|
171
|
-
// Validate hex string
|
|
172
|
-
if (!cleanHex || cleanHex.length === 0) {
|
|
173
|
-
throw new Error('Empty hex string')
|
|
174
|
-
}
|
|
175
|
-
if (cleanHex.length % 2 !== 0) {
|
|
176
|
-
throw new Error('Hex string must have even length')
|
|
177
|
-
}
|
|
178
|
-
if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
|
|
179
|
-
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
180
|
-
}
|
|
181
|
-
|
|
214
|
+
const parseWithSourceMap = (input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap => {
|
|
182
215
|
// Merge options with defaults
|
|
183
216
|
const mergedOptions = mergeOptions(options)
|
|
184
217
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
218
|
+
let buffer: Uint8Array
|
|
219
|
+
|
|
220
|
+
if (input instanceof Uint8Array) {
|
|
221
|
+
if (input.length === 0) {
|
|
222
|
+
throw new Error('Empty input')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check input size limit
|
|
226
|
+
if (mergedOptions.limits?.maxInputSize && input.length > mergedOptions.limits.maxInputSize) {
|
|
227
|
+
throw new Error(`Input size ${input.length} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
buffer = input
|
|
231
|
+
} else {
|
|
232
|
+
const cleanHex = input.replace(/\s+/g, '')
|
|
233
|
+
|
|
234
|
+
// Validate hex string
|
|
235
|
+
if (!cleanHex || cleanHex.length === 0) {
|
|
236
|
+
throw new Error('Empty hex string')
|
|
237
|
+
}
|
|
238
|
+
if (cleanHex.length % 2 !== 0) {
|
|
239
|
+
throw new Error('Hex string must have even length')
|
|
240
|
+
}
|
|
241
|
+
if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
|
|
242
|
+
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check input size limit
|
|
246
|
+
const inputSize = cleanHex.length / 2
|
|
247
|
+
if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
|
|
248
|
+
throw new Error(`Input size ${inputSize} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
buffer = hexToBytes(cleanHex)
|
|
189
252
|
}
|
|
190
253
|
|
|
191
|
-
const buffer = hexToBytes(cleanHex)
|
|
192
254
|
const sourceMap: SourceMapEntry[] = []
|
|
193
255
|
|
|
194
256
|
// Create context with tracking
|
|
@@ -391,33 +453,87 @@ export function useCborParser() {
|
|
|
391
453
|
}
|
|
392
454
|
|
|
393
455
|
/**
|
|
394
|
-
*
|
|
456
|
+
* Dispatches CBOR parsing from buffer by major type
|
|
457
|
+
* Used by parseSequence and parseValueWithMap helpers
|
|
458
|
+
*
|
|
459
|
+
* @param buffer - Data buffer
|
|
460
|
+
* @param offset - Current offset
|
|
461
|
+
* @param options - Parser options
|
|
462
|
+
* @returns Parsed value and bytes read
|
|
463
|
+
*/
|
|
464
|
+
const dispatchFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
465
|
+
const initialByte = readByte(buffer, offset)
|
|
466
|
+
const { majorType } = extractCborHeader(initialByte)
|
|
467
|
+
|
|
468
|
+
switch (majorType) {
|
|
469
|
+
case 0: // Unsigned integer
|
|
470
|
+
case 1: // Negative integer
|
|
471
|
+
return integerFromBuffer(buffer, offset, options)
|
|
472
|
+
|
|
473
|
+
case 2: // Byte string
|
|
474
|
+
return byteStringFromBuffer(buffer, offset, options)
|
|
475
|
+
|
|
476
|
+
case 3: // Text string
|
|
477
|
+
return textStringFromBuffer(buffer, offset, options)
|
|
478
|
+
|
|
479
|
+
case 4: // Array
|
|
480
|
+
{
|
|
481
|
+
// Use parseArray via hex for now - arrays/maps already use buffer internally
|
|
482
|
+
const hexString = Array.from(buffer.slice(offset))
|
|
483
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
484
|
+
.join('')
|
|
485
|
+
return parseArray(hexString, options)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
case 5: // Map
|
|
489
|
+
{
|
|
490
|
+
const hexString = Array.from(buffer.slice(offset))
|
|
491
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
492
|
+
.join('')
|
|
493
|
+
return parseMap(hexString, options)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
case 6: // Tag
|
|
497
|
+
{
|
|
498
|
+
const hexString = Array.from(buffer.slice(offset))
|
|
499
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
500
|
+
.join('')
|
|
501
|
+
return parseTag(hexString, options)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case 7: // Float/Simple
|
|
505
|
+
return floatOrSimpleFromBuffer(buffer, offset, options)
|
|
506
|
+
|
|
507
|
+
default:
|
|
508
|
+
throw new Error(`Unknown major type: ${majorType}`)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Helper to parse integer from buffer (delegates to buffer-native implementation)
|
|
395
514
|
*/
|
|
396
515
|
const parseIntegerFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
397
|
-
|
|
398
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
399
|
-
.join('')
|
|
400
|
-
return parseInteger(hexString, options)
|
|
516
|
+
return integerFromBuffer(buffer, offset, options)
|
|
401
517
|
}
|
|
402
518
|
|
|
403
519
|
/**
|
|
404
520
|
* Helper to parse string from buffer
|
|
521
|
+
* Dispatches to byte string or text string based on major type
|
|
405
522
|
*/
|
|
406
523
|
const parseStringFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
524
|
+
const initialByte = readByte(buffer, offset)
|
|
525
|
+
const { majorType } = extractCborHeader(initialByte)
|
|
526
|
+
if (majorType === 2) {
|
|
527
|
+
return byteStringFromBuffer(buffer, offset, options)
|
|
528
|
+
}
|
|
529
|
+
return textStringFromBuffer(buffer, offset, options)
|
|
411
530
|
}
|
|
412
531
|
|
|
413
532
|
/**
|
|
414
|
-
* Helper to parse float from buffer
|
|
533
|
+
* Helper to parse float/simple from buffer (delegates to buffer-native implementation)
|
|
415
534
|
*/
|
|
416
535
|
const parseFloatFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
417
|
-
|
|
418
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
419
|
-
.join('')
|
|
420
|
-
return parseFloatOrSimple(hexString, options)
|
|
536
|
+
return floatOrSimpleFromBuffer(buffer, offset, options)
|
|
421
537
|
}
|
|
422
538
|
|
|
423
539
|
/**
|
|
@@ -429,6 +545,13 @@ export function useCborParser() {
|
|
|
429
545
|
path: string,
|
|
430
546
|
sourceMap: SourceMapEntry[]
|
|
431
547
|
): ParseResult => {
|
|
548
|
+
const previousDepth = ctx.currentDepth ?? 0
|
|
549
|
+
const maxDepth = ctx.options?.limits?.maxDepth
|
|
550
|
+
if (maxDepth !== undefined && previousDepth >= maxDepth) {
|
|
551
|
+
throw new Error(`Maximum nesting depth ${maxDepth} exceeded`)
|
|
552
|
+
}
|
|
553
|
+
ctx.currentDepth = previousDepth + 1
|
|
554
|
+
|
|
432
555
|
const startOffset = offset
|
|
433
556
|
const initialByte = readByte(ctx.buffer, offset)
|
|
434
557
|
const { additionalInfo } = extractCborHeader(initialByte)
|
|
@@ -459,6 +582,10 @@ export function useCborParser() {
|
|
|
459
582
|
length = Number(bigLength)
|
|
460
583
|
currentOffset += 8
|
|
461
584
|
} else if (additionalInfo === 31) {
|
|
585
|
+
const isIndefiniteAllowed = ctx.options?.allowIndefinite ?? !(ctx.options?.validateCanonical || ctx.options?.strict)
|
|
586
|
+
if (!isIndefiniteAllowed) {
|
|
587
|
+
throw new Error('Indefinite-length encoding is not allowed (strict/canonical mode)')
|
|
588
|
+
}
|
|
462
589
|
isIndefinite = true
|
|
463
590
|
length = 0
|
|
464
591
|
} else {
|
|
@@ -480,56 +607,71 @@ export function useCborParser() {
|
|
|
480
607
|
headerEnd
|
|
481
608
|
})
|
|
482
609
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
currentOffset
|
|
491
|
-
|
|
610
|
+
try {
|
|
611
|
+
// Parse array elements
|
|
612
|
+
const childPaths: string[] = []
|
|
613
|
+
if (isIndefinite) {
|
|
614
|
+
let index = 0
|
|
615
|
+
let foundBreak = false
|
|
616
|
+
while (currentOffset < ctx.buffer.length) {
|
|
617
|
+
const nextByte = readByte(ctx.buffer, currentOffset)
|
|
618
|
+
if (nextByte === 0xff) {
|
|
619
|
+
currentOffset++
|
|
620
|
+
foundBreak = true
|
|
621
|
+
break
|
|
622
|
+
}
|
|
623
|
+
if (ctx.options?.limits?.maxArrayLength && index >= ctx.options.limits.maxArrayLength) {
|
|
624
|
+
throw new Error(`Array length exceeds limit of ${ctx.options.limits.maxArrayLength}`)
|
|
625
|
+
}
|
|
626
|
+
const elementPath = `${path}[${index}]`
|
|
627
|
+
childPaths.push(elementPath)
|
|
628
|
+
const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
|
|
629
|
+
items.push(elementResult.value)
|
|
630
|
+
currentOffset += elementResult.bytesRead
|
|
631
|
+
|
|
632
|
+
// Mark element as child of this array
|
|
633
|
+
const elementEntry = sourceMap.find(e => e.path === elementPath)
|
|
634
|
+
if (elementEntry) {
|
|
635
|
+
elementEntry.parent = path
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
index++
|
|
492
639
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
|
|
496
|
-
items.push(elementResult.value)
|
|
497
|
-
currentOffset += elementResult.bytesRead
|
|
498
|
-
|
|
499
|
-
// Mark element as child of this array
|
|
500
|
-
const elementEntry = sourceMap.find(e => e.path === elementPath)
|
|
501
|
-
if (elementEntry) {
|
|
502
|
-
elementEntry.parent = path
|
|
640
|
+
if (!foundBreak) {
|
|
641
|
+
throw new Error('Indefinite-length array missing break code (0xFF)')
|
|
503
642
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
643
|
+
} else {
|
|
644
|
+
if (ctx.options?.limits?.maxArrayLength && length > ctx.options.limits.maxArrayLength) {
|
|
645
|
+
throw new Error(`Array length ${length} exceeds limit of ${ctx.options.limits.maxArrayLength}`)
|
|
646
|
+
}
|
|
647
|
+
for (let i = 0; i < length; i++) {
|
|
648
|
+
const elementPath = `${path}[${i}]`
|
|
649
|
+
childPaths.push(elementPath)
|
|
650
|
+
const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
|
|
651
|
+
items.push(elementResult.value)
|
|
652
|
+
currentOffset += elementResult.bytesRead
|
|
653
|
+
|
|
654
|
+
// Mark element as child of this array
|
|
655
|
+
const elementEntry = sourceMap.find(e => e.path === elementPath)
|
|
656
|
+
if (elementEntry) {
|
|
657
|
+
elementEntry.parent = path
|
|
658
|
+
}
|
|
519
659
|
}
|
|
520
660
|
}
|
|
521
|
-
}
|
|
522
661
|
|
|
523
|
-
|
|
662
|
+
const bytesRead = currentOffset - offset
|
|
524
663
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
664
|
+
// Only set children if array is non-empty
|
|
665
|
+
if (childPaths.length > 0 && sourceMap[arrayEntryIndex]) {
|
|
666
|
+
sourceMap[arrayEntryIndex].children = childPaths
|
|
667
|
+
}
|
|
529
668
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
669
|
+
return {
|
|
670
|
+
value: items,
|
|
671
|
+
bytesRead
|
|
672
|
+
}
|
|
673
|
+
} finally {
|
|
674
|
+
ctx.currentDepth = previousDepth
|
|
533
675
|
}
|
|
534
676
|
}
|
|
535
677
|
|
|
@@ -542,6 +684,13 @@ export function useCborParser() {
|
|
|
542
684
|
path: string,
|
|
543
685
|
sourceMap: SourceMapEntry[]
|
|
544
686
|
): ParseResult => {
|
|
687
|
+
const previousDepth = ctx.currentDepth ?? 0
|
|
688
|
+
const maxDepth = ctx.options?.limits?.maxDepth
|
|
689
|
+
if (maxDepth !== undefined && previousDepth >= maxDepth) {
|
|
690
|
+
throw new Error(`Maximum nesting depth ${maxDepth} exceeded`)
|
|
691
|
+
}
|
|
692
|
+
ctx.currentDepth = previousDepth + 1
|
|
693
|
+
|
|
545
694
|
const startOffset = offset
|
|
546
695
|
const initialByte = readByte(ctx.buffer, offset)
|
|
547
696
|
const { additionalInfo } = extractCborHeader(initialByte)
|
|
@@ -572,6 +721,10 @@ export function useCborParser() {
|
|
|
572
721
|
length = Number(bigLength)
|
|
573
722
|
currentOffset += 8
|
|
574
723
|
} else if (additionalInfo === 31) {
|
|
724
|
+
const isIndefiniteAllowed = ctx.options?.allowIndefinite ?? !(ctx.options?.validateCanonical || ctx.options?.strict)
|
|
725
|
+
if (!isIndefiniteAllowed) {
|
|
726
|
+
throw new Error('Indefinite-length encoding is not allowed (strict/canonical mode)')
|
|
727
|
+
}
|
|
575
728
|
isIndefinite = true
|
|
576
729
|
length = 0
|
|
577
730
|
} else {
|
|
@@ -593,100 +746,122 @@ export function useCborParser() {
|
|
|
593
746
|
headerEnd
|
|
594
747
|
})
|
|
595
748
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
749
|
+
try {
|
|
750
|
+
// Parse map entries
|
|
751
|
+
const childPaths: string[] = []
|
|
752
|
+
const seenKeys = new Set<string>()
|
|
753
|
+
|
|
754
|
+
if (isIndefinite) {
|
|
755
|
+
let count = 0
|
|
756
|
+
let foundBreak = false
|
|
757
|
+
while (currentOffset < ctx.buffer.length) {
|
|
758
|
+
const nextByte = readByte(ctx.buffer, currentOffset)
|
|
759
|
+
if (nextByte === 0xff) {
|
|
760
|
+
currentOffset++
|
|
761
|
+
foundBreak = true
|
|
762
|
+
break
|
|
763
|
+
}
|
|
764
|
+
if (ctx.options?.limits?.maxMapSize && count >= ctx.options.limits.maxMapSize) {
|
|
765
|
+
throw new Error(`Map size exceeds limit of ${ctx.options.limits.maxMapSize}`)
|
|
766
|
+
}
|
|
607
767
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
768
|
+
// Parse key with path suffix to indicate it's a key
|
|
769
|
+
const keyPath = `${path}${path ? '.' : ''}#key`
|
|
770
|
+
const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
|
|
771
|
+
currentOffset += keyResult.bytesRead
|
|
772
|
+
|
|
773
|
+
// For duplicate detection, use semantic comparison (RFC 8949 Section 5.6)
|
|
774
|
+
const keyForDupCheck = serializeValueForComparison(keyResult.value)
|
|
775
|
+
// For path generation, use display-friendly stringification
|
|
776
|
+
const keyString = keyResult.value instanceof Uint8Array
|
|
777
|
+
? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
778
|
+
: String(keyResult.value)
|
|
779
|
+
|
|
780
|
+
// Check for duplicate keys based on dupMapKeyMode
|
|
781
|
+
if (seenKeys.has(keyForDupCheck)) {
|
|
782
|
+
const mode = ctx.options?.dupMapKeyMode || 'allow'
|
|
783
|
+
if (mode === 'reject') {
|
|
784
|
+
throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
785
|
+
} else if (mode === 'warn') {
|
|
786
|
+
logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
787
|
+
}
|
|
625
788
|
}
|
|
789
|
+
seenKeys.add(keyForDupCheck)
|
|
790
|
+
|
|
791
|
+
// Parse value
|
|
792
|
+
const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
|
|
793
|
+
childPaths.push(valuePath)
|
|
794
|
+
const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
|
|
795
|
+
map.set(keyResult.value, valueResult.value)
|
|
796
|
+
currentOffset += valueResult.bytesRead
|
|
797
|
+
|
|
798
|
+
// Mark value entry as child of this map
|
|
799
|
+
const valueEntry = sourceMap.find(e => e.path === valuePath)
|
|
800
|
+
if (valueEntry) {
|
|
801
|
+
valueEntry.parent = path
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
count++
|
|
626
805
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
// Parse value
|
|
630
|
-
const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
|
|
631
|
-
childPaths.push(valuePath)
|
|
632
|
-
const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
|
|
633
|
-
map.set(keyResult.value, valueResult.value)
|
|
634
|
-
currentOffset += valueResult.bytesRead
|
|
635
|
-
|
|
636
|
-
// Mark value entry as child of this map
|
|
637
|
-
const valueEntry = sourceMap.find(e => e.path === valuePath)
|
|
638
|
-
if (valueEntry) {
|
|
639
|
-
valueEntry.parent = path
|
|
806
|
+
if (!foundBreak) {
|
|
807
|
+
throw new Error('Indefinite-length map missing break code (0xFF)')
|
|
640
808
|
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
// Parse key with path suffix to indicate it's a key
|
|
645
|
-
const keyPath = `${path}${path ? '.' : ''}#key${i}`
|
|
646
|
-
const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
|
|
647
|
-
currentOffset += keyResult.bytesRead
|
|
648
|
-
|
|
649
|
-
// For duplicate detection and path generation, stringify the key
|
|
650
|
-
const keyString = keyResult.value instanceof Uint8Array
|
|
651
|
-
? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
652
|
-
: String(keyResult.value)
|
|
653
|
-
|
|
654
|
-
// Check for duplicate keys based on dupMapKeyMode
|
|
655
|
-
if (seenKeys.has(keyString)) {
|
|
656
|
-
const mode = ctx.options?.dupMapKeyMode || 'allow'
|
|
657
|
-
if (mode === 'reject') {
|
|
658
|
-
throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
659
|
-
} else if (mode === 'warn') {
|
|
660
|
-
logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
661
|
-
}
|
|
809
|
+
} else {
|
|
810
|
+
if (ctx.options?.limits?.maxMapSize && length > ctx.options.limits.maxMapSize) {
|
|
811
|
+
throw new Error(`Map size ${length} exceeds limit of ${ctx.options.limits.maxMapSize}`)
|
|
662
812
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
813
|
+
for (let i = 0; i < length; i++) {
|
|
814
|
+
// Parse key with path suffix to indicate it's a key
|
|
815
|
+
const keyPath = `${path}${path ? '.' : ''}#key${i}`
|
|
816
|
+
const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
|
|
817
|
+
currentOffset += keyResult.bytesRead
|
|
818
|
+
|
|
819
|
+
// For duplicate detection, use semantic comparison (RFC 8949 Section 5.6)
|
|
820
|
+
const keyForDupCheck = serializeValueForComparison(keyResult.value)
|
|
821
|
+
// For path generation, use display-friendly stringification
|
|
822
|
+
const keyString = keyResult.value instanceof Uint8Array
|
|
823
|
+
? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
824
|
+
: String(keyResult.value)
|
|
825
|
+
|
|
826
|
+
// Check for duplicate keys based on dupMapKeyMode
|
|
827
|
+
if (seenKeys.has(keyForDupCheck)) {
|
|
828
|
+
const mode = ctx.options?.dupMapKeyMode || 'allow'
|
|
829
|
+
if (mode === 'reject') {
|
|
830
|
+
throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
831
|
+
} else if (mode === 'warn') {
|
|
832
|
+
logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
seenKeys.add(keyForDupCheck)
|
|
836
|
+
|
|
837
|
+
// Parse value
|
|
838
|
+
const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
|
|
839
|
+
childPaths.push(valuePath)
|
|
840
|
+
const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
|
|
841
|
+
map.set(keyResult.value, valueResult.value)
|
|
842
|
+
currentOffset += valueResult.bytesRead
|
|
843
|
+
|
|
844
|
+
// Mark value entry as child of this map
|
|
845
|
+
const valueEntry = sourceMap.find(e => e.path === valuePath)
|
|
846
|
+
if (valueEntry) {
|
|
847
|
+
valueEntry.parent = path
|
|
848
|
+
}
|
|
676
849
|
}
|
|
677
850
|
}
|
|
678
|
-
}
|
|
679
851
|
|
|
680
|
-
|
|
852
|
+
const bytesRead = currentOffset - offset
|
|
681
853
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
854
|
+
// Set children for the map entry
|
|
855
|
+
if (sourceMap[mapEntryIndex]) {
|
|
856
|
+
sourceMap[mapEntryIndex].children = childPaths
|
|
857
|
+
}
|
|
686
858
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
859
|
+
return {
|
|
860
|
+
value: map,
|
|
861
|
+
bytesRead
|
|
862
|
+
}
|
|
863
|
+
} finally {
|
|
864
|
+
ctx.currentDepth = previousDepth
|
|
690
865
|
}
|
|
691
866
|
}
|
|
692
867
|
|
|
@@ -738,6 +913,17 @@ export function useCborParser() {
|
|
|
738
913
|
path: string,
|
|
739
914
|
sourceMap: SourceMapEntry[]
|
|
740
915
|
): ParseResult => {
|
|
916
|
+
// Enforce tag nesting depth (RUSTSEC-2019-0025). The source-map path
|
|
917
|
+
// previously lacked this guard, allowing a deeply nested tag chain to
|
|
918
|
+
// overflow the call stack with an uncatchable RangeError instead of a
|
|
919
|
+
// clean error — matching the decode() path's behaviour here.
|
|
920
|
+
const previousTagDepth = ctx.currentTagDepth ?? 0
|
|
921
|
+
const maxTagDepth = ctx.options?.limits?.maxTagDepth ?? DEFAULT_LIMITS.maxTagDepth
|
|
922
|
+
if (previousTagDepth >= maxTagDepth) {
|
|
923
|
+
throw new Error(`Tag nesting depth ${previousTagDepth} exceeds limit of ${maxTagDepth}`)
|
|
924
|
+
}
|
|
925
|
+
ctx.currentTagDepth = previousTagDepth + 1
|
|
926
|
+
|
|
741
927
|
const startOffset = offset
|
|
742
928
|
const initialByte = readByte(ctx.buffer, offset)
|
|
743
929
|
const { additionalInfo } = extractCborHeader(initialByte)
|
|
@@ -749,6 +935,11 @@ export function useCborParser() {
|
|
|
749
935
|
additionalInfo
|
|
750
936
|
)
|
|
751
937
|
|
|
938
|
+
// Enforce canonical (shortest-form) tag number encoding when requested.
|
|
939
|
+
if (ctx.options?.validateCanonical) {
|
|
940
|
+
validateCanonicalInteger(tagNumber, additionalInfo)
|
|
941
|
+
}
|
|
942
|
+
|
|
752
943
|
let currentOffset = offset + 1 + bytesConsumed
|
|
753
944
|
const headerEnd = currentOffset
|
|
754
945
|
|
|
@@ -781,14 +972,46 @@ export function useCborParser() {
|
|
|
781
972
|
valueEntry.parent = path
|
|
782
973
|
}
|
|
783
974
|
|
|
784
|
-
// Build TaggedValue
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
975
|
+
// Build TaggedValue directly from already-parsed value (no re-parsing)
|
|
976
|
+
// This avoids O(D^2) complexity for nested tags (Task 2-B fix)
|
|
977
|
+
let finalValue = valueResult.value
|
|
978
|
+
|
|
979
|
+
// Handle bignum conversion (tags 2 and 3) - mirrors parseTagFromBuffer logic
|
|
980
|
+
if ((tagNumber === 2 || tagNumber === 3) && finalValue instanceof Uint8Array) {
|
|
981
|
+
const maxBignumBytes = ctx.options?.limits?.maxBignumBytes ?? DEFAULT_LIMITS.maxBignumBytes
|
|
982
|
+
if (finalValue.length > maxBignumBytes) {
|
|
983
|
+
throw new Error(
|
|
984
|
+
`Bignum (tag ${tagNumber}) size ${finalValue.length} bytes exceeds limit of ${maxBignumBytes} bytes`
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Convert bytes to BigInt (big-endian)
|
|
989
|
+
let bigintValue = 0n
|
|
990
|
+
for (let i = 0; i < finalValue.length; i++) {
|
|
991
|
+
bigintValue = (bigintValue << 8n) | BigInt(finalValue[i]!)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Tag 2: Positive bignum, Tag 3: Negative bignum (-1 - n)
|
|
995
|
+
finalValue = tagNumber === 2 ? bigintValue : -1n - bigintValue
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Validate semantic constraints for specific tags
|
|
999
|
+
validateTagSemantics(tagNumber, finalValue, ctx.options)
|
|
1000
|
+
|
|
1001
|
+
// Decode Plutus constructor if applicable
|
|
1002
|
+
const plutusConstr = decodePlutusConstructor(tagNumber, finalValue)
|
|
1003
|
+
|
|
1004
|
+
const taggedValue: TaggedValue = {
|
|
1005
|
+
tag: tagNumber,
|
|
1006
|
+
value: finalValue,
|
|
1007
|
+
...(plutusConstr && { plutus: plutusConstr })
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Restore tag depth so sibling tags don't accumulate against the limit.
|
|
1011
|
+
ctx.currentTagDepth = previousTagDepth
|
|
789
1012
|
|
|
790
1013
|
return {
|
|
791
|
-
value:
|
|
1014
|
+
value: taggedValue,
|
|
792
1015
|
bytesRead: currentOffset - startOffset
|
|
793
1016
|
}
|
|
794
1017
|
}
|
|
@@ -824,40 +1047,58 @@ export function useCborParser() {
|
|
|
824
1047
|
* parseSequence('') // [] - empty sequence
|
|
825
1048
|
* ```
|
|
826
1049
|
*/
|
|
827
|
-
const parseSequence = (
|
|
828
|
-
const
|
|
1050
|
+
const parseSequence = (input: string | Uint8Array, options?: ParseOptions): CborValue[] => {
|
|
1051
|
+
const mergedOptions = mergeOptions(options)
|
|
1052
|
+
let buffer: Uint8Array
|
|
829
1053
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1054
|
+
if (input instanceof Uint8Array) {
|
|
1055
|
+
// Empty sequence is valid
|
|
1056
|
+
if (input.length === 0) {
|
|
1057
|
+
return []
|
|
1058
|
+
}
|
|
1059
|
+
buffer = input
|
|
1060
|
+
} else {
|
|
1061
|
+
const cleanHex = input.replace(/\s+/g, '')
|
|
834
1062
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1063
|
+
// Empty sequence is valid
|
|
1064
|
+
if (!cleanHex || cleanHex.length === 0) {
|
|
1065
|
+
return []
|
|
1066
|
+
}
|
|
838
1067
|
|
|
839
|
-
|
|
840
|
-
|
|
1068
|
+
if (cleanHex.length % 2 !== 0) {
|
|
1069
|
+
throw new Error('Hex string must have even length')
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
|
|
1073
|
+
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
buffer = hexToBytes(cleanHex)
|
|
841
1077
|
}
|
|
842
1078
|
|
|
843
|
-
const mergedOptions = mergeOptions(options)
|
|
844
|
-
const buffer = hexToBytes(cleanHex)
|
|
845
1079
|
const results: CborValue[] = []
|
|
846
1080
|
let offset = 0
|
|
847
1081
|
|
|
1082
|
+
// Track start time for timeout enforcement across the entire sequence
|
|
1083
|
+
const sequenceStartTime = mergedOptions.limits?.maxParseTime ? Date.now() : 0
|
|
1084
|
+
|
|
848
1085
|
while (offset < buffer.length) {
|
|
1086
|
+
// Check timeout on each sequence item
|
|
1087
|
+
if (sequenceStartTime > 0 && mergedOptions.limits?.maxParseTime) {
|
|
1088
|
+
const elapsed = Date.now() - sequenceStartTime
|
|
1089
|
+
if (elapsed > mergedOptions.limits.maxParseTime) {
|
|
1090
|
+
throw new Error(`Parse timeout: exceeded ${mergedOptions.limits.maxParseTime}ms limit`)
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
849
1094
|
// Check for break code outside indefinite context (invalid in sequence)
|
|
850
1095
|
const byte = readByte(buffer, offset)
|
|
851
1096
|
if (byte === 0xff) {
|
|
852
1097
|
throw new Error(`Unexpected break code (0xff) at offset ${offset} - not inside indefinite-length item`)
|
|
853
1098
|
}
|
|
854
1099
|
|
|
855
|
-
// Parse next item from
|
|
856
|
-
const
|
|
857
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
858
|
-
.join('')
|
|
859
|
-
|
|
860
|
-
const result = parse(remainingHex, mergedOptions)
|
|
1100
|
+
// Parse next item directly from buffer (no hex conversion)
|
|
1101
|
+
const result = dispatchFromBuffer(buffer, offset, mergedOptions)
|
|
861
1102
|
results.push(result.value)
|
|
862
1103
|
offset += result.bytesRead
|
|
863
1104
|
}
|
|
@@ -902,11 +1143,10 @@ export function useCborParser() {
|
|
|
902
1143
|
throw new Error(`Unexpected break code (0xff) at offset ${offset}`)
|
|
903
1144
|
}
|
|
904
1145
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const result = parseWithSourceMap(remainingHex, mergedOptions)
|
|
1146
|
+
// Zero-copy view of the remaining bytes (parseWithSourceMap accepts
|
|
1147
|
+
// Uint8Array). Avoids the previous O(N^2) per-item hex re-encode that
|
|
1148
|
+
// re-stringified the whole tail of the buffer on every sequence item.
|
|
1149
|
+
const result = parseWithSourceMap(buffer.subarray(offset), mergedOptions)
|
|
910
1150
|
|
|
911
1151
|
// Adjust source map offsets to account for sequence position
|
|
912
1152
|
const adjustedSourceMap = result.sourceMap.map(entry => ({
|