@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
  3. package/dist/chunk-5IWW5H47.js.map +1 -0
  4. package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
  5. package/dist/chunk-RVG2BY32.cjs.map +1 -0
  6. package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
  7. package/dist/chunk-S4RXO6IB.cjs.map +1 -0
  8. package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
  9. package/dist/chunk-UMAX5MX5.js.map +1 -0
  10. package/dist/encoder/index.cjs +13 -13
  11. package/dist/encoder/index.d.cts +2 -2
  12. package/dist/encoder/index.d.ts +2 -2
  13. package/dist/encoder/index.js +1 -1
  14. package/dist/index.cjs +32 -32
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +28 -19
  17. package/dist/index.d.ts +28 -19
  18. package/dist/index.js +16 -16
  19. package/dist/index.js.map +1 -1
  20. package/dist/metafile-cjs.json +1 -1
  21. package/dist/metafile-esm.json +1 -1
  22. package/dist/parser/index.cjs +14 -14
  23. package/dist/parser/index.d.cts +3 -1
  24. package/dist/parser/index.d.ts +3 -1
  25. package/dist/parser/index.js +1 -1
  26. package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
  27. package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
  28. package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
  29. package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
  30. package/package.json +1 -1
  31. package/src/__tests__/public-api.test.ts +153 -0
  32. package/src/__tests__/roundtrip.test.ts +701 -0
  33. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
  34. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
  35. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  36. package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
  37. package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
  38. package/src/encoder/composables/useCborEncoder.ts +27 -1
  39. package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
  40. package/src/encoder/composables/useCborStringEncoder.ts +23 -10
  41. package/src/encoder/types.ts +0 -2
  42. package/src/index.ts +29 -20
  43. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  44. package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
  45. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  46. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
  47. package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
  48. package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
  49. package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
  50. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  51. package/src/parser/composables/useCborCollection.ts +45 -42
  52. package/src/parser/composables/useCborFloat.ts +95 -9
  53. package/src/parser/composables/useCborInteger.ts +24 -10
  54. package/src/parser/composables/useCborParser.ts +387 -216
  55. package/src/parser/composables/useCborString.ts +22 -4
  56. package/src/parser/composables/useCborTag.ts +149 -53
  57. package/src/parser/utils.ts +11 -0
  58. package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
  59. package/dist/chunk-2HBCILJS.cjs.map +0 -1
  60. package/dist/chunk-7CFYWHS6.js.map +0 -1
  61. package/dist/chunk-ZRPJUEIZ.js.map +0 -1
  62. 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 = (hexString: string, options?: ParseOptions): ParseResult => {
104
- // Remove spaces from hex string
105
- const cleanHex = hexString.replace(/\s+/g, '')
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 = (hexString: string, options?: ParseOptions): ParseResultWithMap => {
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
- // Check input size limit
186
- const inputSize = cleanHex.length / 2
187
- if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
188
- throw new Error(`Input size ${inputSize} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
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
- * Helper to parse integer from buffer
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
- const hexString = Array.from(buffer.slice(offset))
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 hexString = Array.from(buffer.slice(offset))
408
- .map(b => b.toString(16).padStart(2, '0'))
409
- .join('')
410
- return parseString(hexString, options)
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
- const hexString = Array.from(buffer.slice(offset))
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
- // Parse array elements
484
- const childPaths: string[] = []
485
- if (isIndefinite) {
486
- let index = 0
487
- while (currentOffset < ctx.buffer.length) {
488
- const nextByte = readByte(ctx.buffer, currentOffset)
489
- if (nextByte === 0xff) {
490
- currentOffset++
491
- break
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
- const elementPath = `${path}[${index}]`
494
- childPaths.push(elementPath)
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
- index++
506
- }
507
- } else {
508
- for (let i = 0; i < length; i++) {
509
- const elementPath = `${path}[${i}]`
510
- childPaths.push(elementPath)
511
- const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
512
- items.push(elementResult.value)
513
- currentOffset += elementResult.bytesRead
514
-
515
- // Mark element as child of this array
516
- const elementEntry = sourceMap.find(e => e.path === elementPath)
517
- if (elementEntry) {
518
- elementEntry.parent = path
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
- const bytesRead = currentOffset - offset
630
+ const bytesRead = currentOffset - offset
524
631
 
525
- // Only set children if array is non-empty
526
- if (childPaths.length > 0 && sourceMap[arrayEntryIndex]) {
527
- sourceMap[arrayEntryIndex].children = childPaths
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
- return {
531
- value: items,
532
- bytesRead
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
- // Parse map entries
597
- const childPaths: string[] = []
598
- const seenKeys = new Set<string>()
599
-
600
- if (isIndefinite) {
601
- while (currentOffset < ctx.buffer.length) {
602
- const nextByte = readByte(ctx.buffer, currentOffset)
603
- if (nextByte === 0xff) {
604
- currentOffset++
605
- break
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
- // Parse key with path suffix to indicate it's a key
609
- const keyPath = `${path}${path ? '.' : ''}#key`
610
- const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
611
- currentOffset += keyResult.bytesRead
612
-
613
- // For duplicate detection and path generation, stringify the key
614
- const keyString = keyResult.value instanceof Uint8Array
615
- ? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
616
- : String(keyResult.value)
617
-
618
- // Check for duplicate keys based on dupMapKeyMode
619
- if (seenKeys.has(keyString)) {
620
- const mode = ctx.options?.dupMapKeyMode || 'allow'
621
- if (mode === 'reject') {
622
- throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
623
- } else if (mode === 'warn') {
624
- logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
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
- seenKeys.add(keyString)
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
- } else {
643
- for (let i = 0; i < length; i++) {
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
- seenKeys.add(keyString)
664
-
665
- // Parse value
666
- const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
667
- childPaths.push(valuePath)
668
- const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
669
- map.set(keyResult.value, valueResult.value)
670
- currentOffset += valueResult.bytesRead
671
-
672
- // Mark value entry as child of this map
673
- const valueEntry = sourceMap.find(e => e.path === valuePath)
674
- if (valueEntry) {
675
- valueEntry.parent = path
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
- const bytesRead = currentOffset - offset
820
+ const bytesRead = currentOffset - offset
681
821
 
682
- // Set children for the map entry
683
- if (sourceMap[mapEntryIndex]) {
684
- sourceMap[mapEntryIndex].children = childPaths
685
- }
822
+ // Set children for the map entry
823
+ if (sourceMap[mapEntryIndex]) {
824
+ sourceMap[mapEntryIndex].children = childPaths
825
+ }
686
826
 
687
- return {
688
- value: map,
689
- bytesRead
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 object (call parseTag to get validation and plutus decoding)
785
- const hexString = Array.from(ctx.buffer.slice(startOffset, currentOffset))
786
- .map(b => b.toString(16).padStart(2, '0'))
787
- .join('')
788
- const tagResult = parseTag(hexString, ctx.options)
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: tagResult.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 = (hexString: string, options?: ParseOptions): CborValue[] => {
847
- const cleanHex = hexString.replace(/\s+/g, '')
999
+ const parseSequence = (input: string | Uint8Array, options?: ParseOptions): CborValue[] => {
1000
+ const mergedOptions = mergeOptions(options)
1001
+ let buffer: Uint8Array
848
1002
 
849
- // Empty sequence is valid
850
- if (!cleanHex || cleanHex.length === 0) {
851
- return []
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
- if (cleanHex.length % 2 !== 0) {
855
- throw new Error('Hex string must have even length')
856
- }
1012
+ // Empty sequence is valid
1013
+ if (!cleanHex || cleanHex.length === 0) {
1014
+ return []
1015
+ }
857
1016
 
858
- if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
859
- throw new Error(`Invalid hex character in: ${cleanHex}`)
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 remaining hex
875
- const remainingHex = Array.from(buffer.slice(offset))
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
  }