@marcuspuchalla/nachos 0.1.1 → 0.1.4
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 +55 -0
- package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
- package/dist/chunk-5IWW5H47.js.map +1 -0
- package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
- package/dist/chunk-RVG2BY32.cjs.map +1 -0
- package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
- package/dist/chunk-S4RXO6IB.cjs.map +1 -0
- package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
- package/dist/chunk-UMAX5MX5.js.map +1 -0
- package/dist/encoder/index.cjs +13 -13
- package/dist/encoder/index.d.cts +2 -2
- package/dist/encoder/index.d.ts +2 -2
- package/dist/encoder/index.js +1 -1
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -19
- package/dist/index.d.ts +28 -19
- package/dist/index.js +16 -16
- 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 +14 -14
- package/dist/parser/index.d.cts +3 -1
- package/dist/parser/index.d.ts +3 -1
- package/dist/parser/index.js +1 -1
- package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
- package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
- package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
- package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
- package/package.json +1 -1
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +701 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
- package/src/encoder/composables/useCborEncoder.ts +27 -1
- package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
- package/src/encoder/composables/useCborStringEncoder.ts +23 -10
- package/src/encoder/types.ts +0 -2
- package/src/index.ts +29 -20
- package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
- package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
- package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
- package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
- package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
- package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
- package/src/parser/composables/useCborCollection.ts +45 -42
- package/src/parser/composables/useCborFloat.ts +95 -9
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +387 -216
- package/src/parser/composables/useCborString.ts +22 -4
- package/src/parser/composables/useCborTag.ts +149 -53
- package/src/parser/utils.ts +11 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
- package/dist/chunk-2HBCILJS.cjs.map +0 -1
- package/dist/chunk-7CFYWHS6.js.map +0 -1
- package/dist/chunk-ZRPJUEIZ.js.map +0 -1
- package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
|
@@ -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 } from '../utils'
|
|
10
10
|
import { useCborInteger } from './useCborInteger'
|
|
11
11
|
import { useCborString } from './useCborString'
|
|
12
12
|
import { useCborCollection } from './useCborCollection'
|
|
@@ -73,11 +73,11 @@ export function useCborParser() {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const { parseInteger } = useCborInteger()
|
|
77
|
-
const { parseString } = useCborString()
|
|
76
|
+
const { parseInteger, parseIntegerFromBuffer: integerFromBuffer } = useCborInteger()
|
|
77
|
+
const { parseString, parseByteString: byteStringFromBuffer, parseTextString: textStringFromBuffer } = useCborString()
|
|
78
78
|
const { parseArray, parseMap } = useCborCollection()
|
|
79
|
-
const { parseTag } = useCborTag()
|
|
80
|
-
const { parse: parseFloatOrSimple } = useCborFloat()
|
|
79
|
+
const { parseTag, validateTagSemantics, decodePlutusConstructor } = useCborTag()
|
|
80
|
+
const { parse: parseFloatOrSimple, parseFromBuffer: floatOrSimpleFromBuffer } = useCborFloat()
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Parses a CBOR hex string, auto-detecting the type
|
|
@@ -100,9 +100,26 @@ export function useCborParser() {
|
|
|
100
100
|
* parse('6449455446', { strict: true })
|
|
101
101
|
* ```
|
|
102
102
|
*/
|
|
103
|
-
const parse = (
|
|
104
|
-
//
|
|
105
|
-
const
|
|
103
|
+
const parse = (input: string | Uint8Array, options?: ParseOptions): ParseResult => {
|
|
104
|
+
// Merge options with defaults
|
|
105
|
+
const mergedOptions = mergeOptions(options)
|
|
106
|
+
|
|
107
|
+
// Uint8Array fast path: skip hex conversion entirely
|
|
108
|
+
if (input instanceof Uint8Array) {
|
|
109
|
+
if (input.length === 0) {
|
|
110
|
+
throw new Error('Empty input')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check input size limit
|
|
114
|
+
if (mergedOptions.limits?.maxInputSize && input.length > mergedOptions.limits.maxInputSize) {
|
|
115
|
+
throw new Error(`Input size ${input.length} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return dispatchFromBuffer(input, 0, mergedOptions)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Hex string path
|
|
122
|
+
const cleanHex = input.replace(/\s+/g, '')
|
|
106
123
|
|
|
107
124
|
// Validate hex string
|
|
108
125
|
if (!cleanHex || cleanHex.length === 0) {
|
|
@@ -117,9 +134,6 @@ export function useCborParser() {
|
|
|
117
134
|
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
// Merge options with defaults
|
|
121
|
-
const mergedOptions = mergeOptions(options)
|
|
122
|
-
|
|
123
137
|
// Check input size limit
|
|
124
138
|
const inputSize = cleanHex.length / 2 // Convert hex chars to bytes
|
|
125
139
|
if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
|
|
@@ -165,30 +179,46 @@ export function useCborParser() {
|
|
|
165
179
|
* @param options - Parser options (optional)
|
|
166
180
|
* @returns Parsed value, bytes read, and source map
|
|
167
181
|
*/
|
|
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
|
-
|
|
182
|
+
const parseWithSourceMap = (input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap => {
|
|
182
183
|
// Merge options with defaults
|
|
183
184
|
const mergedOptions = mergeOptions(options)
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
186
|
+
let buffer: Uint8Array
|
|
187
|
+
|
|
188
|
+
if (input instanceof Uint8Array) {
|
|
189
|
+
if (input.length === 0) {
|
|
190
|
+
throw new Error('Empty input')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check input size limit
|
|
194
|
+
if (mergedOptions.limits?.maxInputSize && input.length > mergedOptions.limits.maxInputSize) {
|
|
195
|
+
throw new Error(`Input size ${input.length} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
buffer = input
|
|
199
|
+
} else {
|
|
200
|
+
const cleanHex = input.replace(/\s+/g, '')
|
|
201
|
+
|
|
202
|
+
// Validate hex string
|
|
203
|
+
if (!cleanHex || cleanHex.length === 0) {
|
|
204
|
+
throw new Error('Empty hex string')
|
|
205
|
+
}
|
|
206
|
+
if (cleanHex.length % 2 !== 0) {
|
|
207
|
+
throw new Error('Hex string must have even length')
|
|
208
|
+
}
|
|
209
|
+
if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
|
|
210
|
+
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check input size limit
|
|
214
|
+
const inputSize = cleanHex.length / 2
|
|
215
|
+
if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
|
|
216
|
+
throw new Error(`Input size ${inputSize} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
buffer = hexToBytes(cleanHex)
|
|
189
220
|
}
|
|
190
221
|
|
|
191
|
-
const buffer = hexToBytes(cleanHex)
|
|
192
222
|
const sourceMap: SourceMapEntry[] = []
|
|
193
223
|
|
|
194
224
|
// Create context with tracking
|
|
@@ -391,33 +421,87 @@ export function useCborParser() {
|
|
|
391
421
|
}
|
|
392
422
|
|
|
393
423
|
/**
|
|
394
|
-
*
|
|
424
|
+
* Dispatches CBOR parsing from buffer by major type
|
|
425
|
+
* Used by parseSequence and parseValueWithMap helpers
|
|
426
|
+
*
|
|
427
|
+
* @param buffer - Data buffer
|
|
428
|
+
* @param offset - Current offset
|
|
429
|
+
* @param options - Parser options
|
|
430
|
+
* @returns Parsed value and bytes read
|
|
431
|
+
*/
|
|
432
|
+
const dispatchFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
433
|
+
const initialByte = readByte(buffer, offset)
|
|
434
|
+
const { majorType } = extractCborHeader(initialByte)
|
|
435
|
+
|
|
436
|
+
switch (majorType) {
|
|
437
|
+
case 0: // Unsigned integer
|
|
438
|
+
case 1: // Negative integer
|
|
439
|
+
return integerFromBuffer(buffer, offset, options)
|
|
440
|
+
|
|
441
|
+
case 2: // Byte string
|
|
442
|
+
return byteStringFromBuffer(buffer, offset, options)
|
|
443
|
+
|
|
444
|
+
case 3: // Text string
|
|
445
|
+
return textStringFromBuffer(buffer, offset, options)
|
|
446
|
+
|
|
447
|
+
case 4: // Array
|
|
448
|
+
{
|
|
449
|
+
// Use parseArray via hex for now - arrays/maps already use buffer internally
|
|
450
|
+
const hexString = Array.from(buffer.slice(offset))
|
|
451
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
452
|
+
.join('')
|
|
453
|
+
return parseArray(hexString, options)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
case 5: // Map
|
|
457
|
+
{
|
|
458
|
+
const hexString = Array.from(buffer.slice(offset))
|
|
459
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
460
|
+
.join('')
|
|
461
|
+
return parseMap(hexString, options)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case 6: // Tag
|
|
465
|
+
{
|
|
466
|
+
const hexString = Array.from(buffer.slice(offset))
|
|
467
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
468
|
+
.join('')
|
|
469
|
+
return parseTag(hexString, options)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
case 7: // Float/Simple
|
|
473
|
+
return floatOrSimpleFromBuffer(buffer, offset, options)
|
|
474
|
+
|
|
475
|
+
default:
|
|
476
|
+
throw new Error(`Unknown major type: ${majorType}`)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Helper to parse integer from buffer (delegates to buffer-native implementation)
|
|
395
482
|
*/
|
|
396
483
|
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)
|
|
484
|
+
return integerFromBuffer(buffer, offset, options)
|
|
401
485
|
}
|
|
402
486
|
|
|
403
487
|
/**
|
|
404
488
|
* Helper to parse string from buffer
|
|
489
|
+
* Dispatches to byte string or text string based on major type
|
|
405
490
|
*/
|
|
406
491
|
const parseStringFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
492
|
+
const initialByte = readByte(buffer, offset)
|
|
493
|
+
const { majorType } = extractCborHeader(initialByte)
|
|
494
|
+
if (majorType === 2) {
|
|
495
|
+
return byteStringFromBuffer(buffer, offset, options)
|
|
496
|
+
}
|
|
497
|
+
return textStringFromBuffer(buffer, offset, options)
|
|
411
498
|
}
|
|
412
499
|
|
|
413
500
|
/**
|
|
414
|
-
* Helper to parse float from buffer
|
|
501
|
+
* Helper to parse float/simple from buffer (delegates to buffer-native implementation)
|
|
415
502
|
*/
|
|
416
503
|
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)
|
|
504
|
+
return floatOrSimpleFromBuffer(buffer, offset, options)
|
|
421
505
|
}
|
|
422
506
|
|
|
423
507
|
/**
|
|
@@ -429,6 +513,13 @@ export function useCborParser() {
|
|
|
429
513
|
path: string,
|
|
430
514
|
sourceMap: SourceMapEntry[]
|
|
431
515
|
): ParseResult => {
|
|
516
|
+
const previousDepth = ctx.currentDepth ?? 0
|
|
517
|
+
const maxDepth = ctx.options?.limits?.maxDepth
|
|
518
|
+
if (maxDepth !== undefined && previousDepth >= maxDepth) {
|
|
519
|
+
throw new Error(`Maximum nesting depth ${maxDepth} exceeded`)
|
|
520
|
+
}
|
|
521
|
+
ctx.currentDepth = previousDepth + 1
|
|
522
|
+
|
|
432
523
|
const startOffset = offset
|
|
433
524
|
const initialByte = readByte(ctx.buffer, offset)
|
|
434
525
|
const { additionalInfo } = extractCborHeader(initialByte)
|
|
@@ -459,6 +550,10 @@ export function useCborParser() {
|
|
|
459
550
|
length = Number(bigLength)
|
|
460
551
|
currentOffset += 8
|
|
461
552
|
} else if (additionalInfo === 31) {
|
|
553
|
+
const isIndefiniteAllowed = ctx.options?.allowIndefinite ?? !(ctx.options?.validateCanonical || ctx.options?.strict)
|
|
554
|
+
if (!isIndefiniteAllowed) {
|
|
555
|
+
throw new Error('Indefinite-length encoding is not allowed (strict/canonical mode)')
|
|
556
|
+
}
|
|
462
557
|
isIndefinite = true
|
|
463
558
|
length = 0
|
|
464
559
|
} else {
|
|
@@ -480,56 +575,71 @@ export function useCborParser() {
|
|
|
480
575
|
headerEnd
|
|
481
576
|
})
|
|
482
577
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
currentOffset
|
|
491
|
-
|
|
578
|
+
try {
|
|
579
|
+
// Parse array elements
|
|
580
|
+
const childPaths: string[] = []
|
|
581
|
+
if (isIndefinite) {
|
|
582
|
+
let index = 0
|
|
583
|
+
let foundBreak = false
|
|
584
|
+
while (currentOffset < ctx.buffer.length) {
|
|
585
|
+
const nextByte = readByte(ctx.buffer, currentOffset)
|
|
586
|
+
if (nextByte === 0xff) {
|
|
587
|
+
currentOffset++
|
|
588
|
+
foundBreak = true
|
|
589
|
+
break
|
|
590
|
+
}
|
|
591
|
+
if (ctx.options?.limits?.maxArrayLength && index >= ctx.options.limits.maxArrayLength) {
|
|
592
|
+
throw new Error(`Array length exceeds limit of ${ctx.options.limits.maxArrayLength}`)
|
|
593
|
+
}
|
|
594
|
+
const elementPath = `${path}[${index}]`
|
|
595
|
+
childPaths.push(elementPath)
|
|
596
|
+
const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
|
|
597
|
+
items.push(elementResult.value)
|
|
598
|
+
currentOffset += elementResult.bytesRead
|
|
599
|
+
|
|
600
|
+
// Mark element as child of this array
|
|
601
|
+
const elementEntry = sourceMap.find(e => e.path === elementPath)
|
|
602
|
+
if (elementEntry) {
|
|
603
|
+
elementEntry.parent = path
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
index++
|
|
492
607
|
}
|
|
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
|
|
608
|
+
if (!foundBreak) {
|
|
609
|
+
throw new Error('Indefinite-length array missing break code (0xFF)')
|
|
503
610
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
611
|
+
} else {
|
|
612
|
+
if (ctx.options?.limits?.maxArrayLength && length > ctx.options.limits.maxArrayLength) {
|
|
613
|
+
throw new Error(`Array length ${length} exceeds limit of ${ctx.options.limits.maxArrayLength}`)
|
|
614
|
+
}
|
|
615
|
+
for (let i = 0; i < length; i++) {
|
|
616
|
+
const elementPath = `${path}[${i}]`
|
|
617
|
+
childPaths.push(elementPath)
|
|
618
|
+
const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
|
|
619
|
+
items.push(elementResult.value)
|
|
620
|
+
currentOffset += elementResult.bytesRead
|
|
621
|
+
|
|
622
|
+
// Mark element as child of this array
|
|
623
|
+
const elementEntry = sourceMap.find(e => e.path === elementPath)
|
|
624
|
+
if (elementEntry) {
|
|
625
|
+
elementEntry.parent = path
|
|
626
|
+
}
|
|
519
627
|
}
|
|
520
628
|
}
|
|
521
|
-
}
|
|
522
629
|
|
|
523
|
-
|
|
630
|
+
const bytesRead = currentOffset - offset
|
|
524
631
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
632
|
+
// Only set children if array is non-empty
|
|
633
|
+
if (childPaths.length > 0 && sourceMap[arrayEntryIndex]) {
|
|
634
|
+
sourceMap[arrayEntryIndex].children = childPaths
|
|
635
|
+
}
|
|
529
636
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
637
|
+
return {
|
|
638
|
+
value: items,
|
|
639
|
+
bytesRead
|
|
640
|
+
}
|
|
641
|
+
} finally {
|
|
642
|
+
ctx.currentDepth = previousDepth
|
|
533
643
|
}
|
|
534
644
|
}
|
|
535
645
|
|
|
@@ -542,6 +652,13 @@ export function useCborParser() {
|
|
|
542
652
|
path: string,
|
|
543
653
|
sourceMap: SourceMapEntry[]
|
|
544
654
|
): ParseResult => {
|
|
655
|
+
const previousDepth = ctx.currentDepth ?? 0
|
|
656
|
+
const maxDepth = ctx.options?.limits?.maxDepth
|
|
657
|
+
if (maxDepth !== undefined && previousDepth >= maxDepth) {
|
|
658
|
+
throw new Error(`Maximum nesting depth ${maxDepth} exceeded`)
|
|
659
|
+
}
|
|
660
|
+
ctx.currentDepth = previousDepth + 1
|
|
661
|
+
|
|
545
662
|
const startOffset = offset
|
|
546
663
|
const initialByte = readByte(ctx.buffer, offset)
|
|
547
664
|
const { additionalInfo } = extractCborHeader(initialByte)
|
|
@@ -572,6 +689,10 @@ export function useCborParser() {
|
|
|
572
689
|
length = Number(bigLength)
|
|
573
690
|
currentOffset += 8
|
|
574
691
|
} else if (additionalInfo === 31) {
|
|
692
|
+
const isIndefiniteAllowed = ctx.options?.allowIndefinite ?? !(ctx.options?.validateCanonical || ctx.options?.strict)
|
|
693
|
+
if (!isIndefiniteAllowed) {
|
|
694
|
+
throw new Error('Indefinite-length encoding is not allowed (strict/canonical mode)')
|
|
695
|
+
}
|
|
575
696
|
isIndefinite = true
|
|
576
697
|
length = 0
|
|
577
698
|
} else {
|
|
@@ -593,100 +714,122 @@ export function useCborParser() {
|
|
|
593
714
|
headerEnd
|
|
594
715
|
})
|
|
595
716
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
717
|
+
try {
|
|
718
|
+
// Parse map entries
|
|
719
|
+
const childPaths: string[] = []
|
|
720
|
+
const seenKeys = new Set<string>()
|
|
721
|
+
|
|
722
|
+
if (isIndefinite) {
|
|
723
|
+
let count = 0
|
|
724
|
+
let foundBreak = false
|
|
725
|
+
while (currentOffset < ctx.buffer.length) {
|
|
726
|
+
const nextByte = readByte(ctx.buffer, currentOffset)
|
|
727
|
+
if (nextByte === 0xff) {
|
|
728
|
+
currentOffset++
|
|
729
|
+
foundBreak = true
|
|
730
|
+
break
|
|
731
|
+
}
|
|
732
|
+
if (ctx.options?.limits?.maxMapSize && count >= ctx.options.limits.maxMapSize) {
|
|
733
|
+
throw new Error(`Map size exceeds limit of ${ctx.options.limits.maxMapSize}`)
|
|
734
|
+
}
|
|
607
735
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
736
|
+
// Parse key with path suffix to indicate it's a key
|
|
737
|
+
const keyPath = `${path}${path ? '.' : ''}#key`
|
|
738
|
+
const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
|
|
739
|
+
currentOffset += keyResult.bytesRead
|
|
740
|
+
|
|
741
|
+
// For duplicate detection, use semantic comparison (RFC 8949 Section 5.6)
|
|
742
|
+
const keyForDupCheck = serializeValueForComparison(keyResult.value)
|
|
743
|
+
// For path generation, use display-friendly stringification
|
|
744
|
+
const keyString = keyResult.value instanceof Uint8Array
|
|
745
|
+
? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
746
|
+
: String(keyResult.value)
|
|
747
|
+
|
|
748
|
+
// Check for duplicate keys based on dupMapKeyMode
|
|
749
|
+
if (seenKeys.has(keyForDupCheck)) {
|
|
750
|
+
const mode = ctx.options?.dupMapKeyMode || 'allow'
|
|
751
|
+
if (mode === 'reject') {
|
|
752
|
+
throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
753
|
+
} else if (mode === 'warn') {
|
|
754
|
+
logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
seenKeys.add(keyForDupCheck)
|
|
758
|
+
|
|
759
|
+
// Parse value
|
|
760
|
+
const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
|
|
761
|
+
childPaths.push(valuePath)
|
|
762
|
+
const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
|
|
763
|
+
map.set(keyResult.value, valueResult.value)
|
|
764
|
+
currentOffset += valueResult.bytesRead
|
|
765
|
+
|
|
766
|
+
// Mark value entry as child of this map
|
|
767
|
+
const valueEntry = sourceMap.find(e => e.path === valuePath)
|
|
768
|
+
if (valueEntry) {
|
|
769
|
+
valueEntry.parent = path
|
|
625
770
|
}
|
|
771
|
+
|
|
772
|
+
count++
|
|
626
773
|
}
|
|
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
|
|
774
|
+
if (!foundBreak) {
|
|
775
|
+
throw new Error('Indefinite-length map missing break code (0xFF)')
|
|
640
776
|
}
|
|
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
|
-
}
|
|
777
|
+
} else {
|
|
778
|
+
if (ctx.options?.limits?.maxMapSize && length > ctx.options.limits.maxMapSize) {
|
|
779
|
+
throw new Error(`Map size ${length} exceeds limit of ${ctx.options.limits.maxMapSize}`)
|
|
662
780
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
781
|
+
for (let i = 0; i < length; i++) {
|
|
782
|
+
// Parse key with path suffix to indicate it's a key
|
|
783
|
+
const keyPath = `${path}${path ? '.' : ''}#key${i}`
|
|
784
|
+
const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
|
|
785
|
+
currentOffset += keyResult.bytesRead
|
|
786
|
+
|
|
787
|
+
// For duplicate detection, use semantic comparison (RFC 8949 Section 5.6)
|
|
788
|
+
const keyForDupCheck = serializeValueForComparison(keyResult.value)
|
|
789
|
+
// For path generation, use display-friendly stringification
|
|
790
|
+
const keyString = keyResult.value instanceof Uint8Array
|
|
791
|
+
? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
792
|
+
: String(keyResult.value)
|
|
793
|
+
|
|
794
|
+
// Check for duplicate keys based on dupMapKeyMode
|
|
795
|
+
if (seenKeys.has(keyForDupCheck)) {
|
|
796
|
+
const mode = ctx.options?.dupMapKeyMode || 'allow'
|
|
797
|
+
if (mode === 'reject') {
|
|
798
|
+
throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
799
|
+
} else if (mode === 'warn') {
|
|
800
|
+
logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
seenKeys.add(keyForDupCheck)
|
|
804
|
+
|
|
805
|
+
// Parse value
|
|
806
|
+
const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
|
|
807
|
+
childPaths.push(valuePath)
|
|
808
|
+
const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
|
|
809
|
+
map.set(keyResult.value, valueResult.value)
|
|
810
|
+
currentOffset += valueResult.bytesRead
|
|
811
|
+
|
|
812
|
+
// Mark value entry as child of this map
|
|
813
|
+
const valueEntry = sourceMap.find(e => e.path === valuePath)
|
|
814
|
+
if (valueEntry) {
|
|
815
|
+
valueEntry.parent = path
|
|
816
|
+
}
|
|
676
817
|
}
|
|
677
818
|
}
|
|
678
|
-
}
|
|
679
819
|
|
|
680
|
-
|
|
820
|
+
const bytesRead = currentOffset - offset
|
|
681
821
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
822
|
+
// Set children for the map entry
|
|
823
|
+
if (sourceMap[mapEntryIndex]) {
|
|
824
|
+
sourceMap[mapEntryIndex].children = childPaths
|
|
825
|
+
}
|
|
686
826
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
827
|
+
return {
|
|
828
|
+
value: map,
|
|
829
|
+
bytesRead
|
|
830
|
+
}
|
|
831
|
+
} finally {
|
|
832
|
+
ctx.currentDepth = previousDepth
|
|
690
833
|
}
|
|
691
834
|
}
|
|
692
835
|
|
|
@@ -781,37 +924,47 @@ export function useCborParser() {
|
|
|
781
924
|
valueEntry.parent = path
|
|
782
925
|
}
|
|
783
926
|
|
|
784
|
-
// Build TaggedValue
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
927
|
+
// Build TaggedValue directly from already-parsed value (no re-parsing)
|
|
928
|
+
// This avoids O(D^2) complexity for nested tags (Task 2-B fix)
|
|
929
|
+
let finalValue = valueResult.value
|
|
930
|
+
|
|
931
|
+
// Handle bignum conversion (tags 2 and 3) - mirrors parseTagFromBuffer logic
|
|
932
|
+
if ((tagNumber === 2 || tagNumber === 3) && finalValue instanceof Uint8Array) {
|
|
933
|
+
const maxBignumBytes = ctx.options?.limits?.maxBignumBytes ?? DEFAULT_LIMITS.maxBignumBytes
|
|
934
|
+
if (finalValue.length > maxBignumBytes) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Bignum (tag ${tagNumber}) size ${finalValue.length} bytes exceeds limit of ${maxBignumBytes} bytes`
|
|
937
|
+
)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Convert bytes to BigInt (big-endian)
|
|
941
|
+
let bigintValue = 0n
|
|
942
|
+
for (let i = 0; i < finalValue.length; i++) {
|
|
943
|
+
bigintValue = (bigintValue << 8n) | BigInt(finalValue[i]!)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Tag 2: Positive bignum, Tag 3: Negative bignum (-1 - n)
|
|
947
|
+
finalValue = tagNumber === 2 ? bigintValue : -1n - bigintValue
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Validate semantic constraints for specific tags
|
|
951
|
+
validateTagSemantics(tagNumber, finalValue, ctx.options)
|
|
952
|
+
|
|
953
|
+
// Decode Plutus constructor if applicable
|
|
954
|
+
const plutusConstr = decodePlutusConstructor(tagNumber, finalValue)
|
|
955
|
+
|
|
956
|
+
const taggedValue: TaggedValue = {
|
|
957
|
+
tag: tagNumber,
|
|
958
|
+
value: finalValue,
|
|
959
|
+
...(plutusConstr && { plutus: plutusConstr })
|
|
960
|
+
}
|
|
789
961
|
|
|
790
962
|
return {
|
|
791
|
-
value:
|
|
963
|
+
value: taggedValue,
|
|
792
964
|
bytesRead: currentOffset - startOffset
|
|
793
965
|
}
|
|
794
966
|
}
|
|
795
967
|
|
|
796
|
-
/**
|
|
797
|
-
* Get tag number for description
|
|
798
|
-
* Note: Reserved for future tag description features
|
|
799
|
-
*/
|
|
800
|
-
// const getTagNumber = (buffer: Uint8Array, offset: number): number => {
|
|
801
|
-
// const initialByte = readByte(buffer, offset)
|
|
802
|
-
// const { additionalInfo } = extractCborHeader(initialByte)
|
|
803
|
-
//
|
|
804
|
-
// if (additionalInfo < 24) return additionalInfo
|
|
805
|
-
// if (additionalInfo === 24) return readByte(buffer, offset + 1)
|
|
806
|
-
// if (additionalInfo === 25) return readUint(buffer, offset + 1, 2)
|
|
807
|
-
// if (additionalInfo === 26) return readUint(buffer, offset + 1, 4)
|
|
808
|
-
// if (additionalInfo === 27) {
|
|
809
|
-
// const bigNum = readBigUint(buffer, offset + 1, 8)
|
|
810
|
-
// return bigNum <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bigNum) : -1
|
|
811
|
-
// }
|
|
812
|
-
// return -1
|
|
813
|
-
// }
|
|
814
|
-
|
|
815
968
|
/**
|
|
816
969
|
* Get simple type description
|
|
817
970
|
*/
|
|
@@ -843,40 +996,58 @@ export function useCborParser() {
|
|
|
843
996
|
* parseSequence('') // [] - empty sequence
|
|
844
997
|
* ```
|
|
845
998
|
*/
|
|
846
|
-
const parseSequence = (
|
|
847
|
-
const
|
|
999
|
+
const parseSequence = (input: string | Uint8Array, options?: ParseOptions): CborValue[] => {
|
|
1000
|
+
const mergedOptions = mergeOptions(options)
|
|
1001
|
+
let buffer: Uint8Array
|
|
848
1002
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1003
|
+
if (input instanceof Uint8Array) {
|
|
1004
|
+
// Empty sequence is valid
|
|
1005
|
+
if (input.length === 0) {
|
|
1006
|
+
return []
|
|
1007
|
+
}
|
|
1008
|
+
buffer = input
|
|
1009
|
+
} else {
|
|
1010
|
+
const cleanHex = input.replace(/\s+/g, '')
|
|
853
1011
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1012
|
+
// Empty sequence is valid
|
|
1013
|
+
if (!cleanHex || cleanHex.length === 0) {
|
|
1014
|
+
return []
|
|
1015
|
+
}
|
|
857
1016
|
|
|
858
|
-
|
|
859
|
-
|
|
1017
|
+
if (cleanHex.length % 2 !== 0) {
|
|
1018
|
+
throw new Error('Hex string must have even length')
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
|
|
1022
|
+
throw new Error(`Invalid hex character in: ${cleanHex}`)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
buffer = hexToBytes(cleanHex)
|
|
860
1026
|
}
|
|
861
1027
|
|
|
862
|
-
const mergedOptions = mergeOptions(options)
|
|
863
|
-
const buffer = hexToBytes(cleanHex)
|
|
864
1028
|
const results: CborValue[] = []
|
|
865
1029
|
let offset = 0
|
|
866
1030
|
|
|
1031
|
+
// Track start time for timeout enforcement across the entire sequence
|
|
1032
|
+
const sequenceStartTime = mergedOptions.limits?.maxParseTime ? Date.now() : 0
|
|
1033
|
+
|
|
867
1034
|
while (offset < buffer.length) {
|
|
1035
|
+
// Check timeout on each sequence item
|
|
1036
|
+
if (sequenceStartTime > 0 && mergedOptions.limits?.maxParseTime) {
|
|
1037
|
+
const elapsed = Date.now() - sequenceStartTime
|
|
1038
|
+
if (elapsed > mergedOptions.limits.maxParseTime) {
|
|
1039
|
+
throw new Error(`Parse timeout: exceeded ${mergedOptions.limits.maxParseTime}ms limit`)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
868
1043
|
// Check for break code outside indefinite context (invalid in sequence)
|
|
869
1044
|
const byte = readByte(buffer, offset)
|
|
870
1045
|
if (byte === 0xff) {
|
|
871
1046
|
throw new Error(`Unexpected break code (0xff) at offset ${offset} - not inside indefinite-length item`)
|
|
872
1047
|
}
|
|
873
1048
|
|
|
874
|
-
// Parse next item from
|
|
875
|
-
const
|
|
876
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
877
|
-
.join('')
|
|
878
|
-
|
|
879
|
-
const result = parse(remainingHex, mergedOptions)
|
|
1049
|
+
// Parse next item directly from buffer (no hex conversion)
|
|
1050
|
+
const result = dispatchFromBuffer(buffer, offset, mergedOptions)
|
|
880
1051
|
results.push(result.value)
|
|
881
1052
|
offset += result.bytesRead
|
|
882
1053
|
}
|