@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/{chunk-PTWN7K3Y.cjs → chunk-3Z45RBZP.cjs} +469 -244
  3. package/dist/chunk-3Z45RBZP.cjs.map +1 -0
  4. package/dist/{chunk-2MTLSQ7E.js → chunk-EDXZTSIA.js} +224 -166
  5. package/dist/chunk-EDXZTSIA.js.map +1 -0
  6. package/dist/{chunk-R62CQQNI.cjs → chunk-HMUA5KLG.cjs} +239 -181
  7. package/dist/chunk-HMUA5KLG.cjs.map +1 -0
  8. package/dist/{chunk-ZDZ2B5PE.js → chunk-JESIF5IF.js} +7 -3
  9. package/dist/chunk-JESIF5IF.js.map +1 -0
  10. package/dist/{chunk-5A5T56JB.js → chunk-LWNWC2O7.js} +442 -217
  11. package/dist/chunk-LWNWC2O7.js.map +1 -0
  12. package/dist/{chunk-PD72MVTX.cjs → chunk-P6A2OOIY.cjs} +7 -3
  13. package/dist/chunk-P6A2OOIY.cjs.map +1 -0
  14. package/dist/encoder/index.cjs +14 -14
  15. package/dist/encoder/index.d.cts +5 -4
  16. package/dist/encoder/index.d.ts +5 -4
  17. package/dist/encoder/index.js +2 -2
  18. package/dist/index.cjs +58 -39
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +40 -21
  21. package/dist/index.d.ts +40 -21
  22. package/dist/index.js +37 -17
  23. package/dist/index.js.map +1 -1
  24. package/dist/metafile-cjs.json +1 -1
  25. package/dist/metafile-esm.json +1 -1
  26. package/dist/parser/index.cjs +21 -21
  27. package/dist/parser/index.d.cts +4 -2
  28. package/dist/parser/index.d.ts +4 -2
  29. package/dist/parser/index.js +2 -2
  30. package/dist/{types-DvNlfbKB.d.cts → types-eG2qalpr.d.cts} +27 -1
  31. package/dist/{types-DvNlfbKB.d.ts → types-eG2qalpr.d.ts} +27 -1
  32. package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -3
  33. package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -3
  34. package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-D4d7xG3-.d.cts} +9 -4
  35. package/dist/{useCborTag-Cs1CZuXZ.d.cts → useCborTag-TYst1KR6.d.ts} +9 -4
  36. package/package.json +1 -1
  37. package/src/__tests__/audit-fixes.test.ts +141 -0
  38. package/src/__tests__/public-api.test.ts +153 -0
  39. package/src/__tests__/roundtrip.test.ts +5 -6
  40. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
  41. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
  42. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  43. package/src/encoder/composables/useCborCollectionEncoder.ts +30 -26
  44. package/src/encoder/composables/useCborEncoder.ts +40 -0
  45. package/src/encoder/composables/useCborSimpleEncoder.ts +40 -9
  46. package/src/encoder/types.ts +9 -4
  47. package/src/encoder/utils.ts +33 -1
  48. package/src/index.ts +39 -20
  49. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  50. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  51. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
  52. package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
  53. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  54. package/src/parser/__tests__/utils-errors.test.ts +11 -3
  55. package/src/parser/composables/useCborCollection.ts +51 -45
  56. package/src/parser/composables/useCborDiagnostic.ts +28 -0
  57. package/src/parser/composables/useCborFloat.ts +2 -1
  58. package/src/parser/composables/useCborInteger.ts +24 -10
  59. package/src/parser/composables/useCborParser.ts +448 -208
  60. package/src/parser/composables/useCborTag.ts +53 -38
  61. package/src/parser/types.ts +32 -1
  62. package/src/parser/utils.ts +52 -0
  63. package/dist/chunk-2MTLSQ7E.js.map +0 -1
  64. package/dist/chunk-5A5T56JB.js.map +0 -1
  65. package/dist/chunk-PD72MVTX.cjs.map +0 -1
  66. package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
  67. package/dist/chunk-R62CQQNI.cjs.map +0 -1
  68. 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 = (hexString: string, options?: ParseOptions): ParseResult => {
104
- // Remove spaces from hex string
105
- const cleanHex = hexString.replace(/\s+/g, '')
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
- return parseInteger(cleanHex, mergedOptions)
158
+ result = parseInteger(cleanHex, mergedOptions)
159
+ break
139
160
 
140
161
  case 2: // Byte string
141
162
  case 3: // Text string
142
- return parseString(cleanHex, mergedOptions)
163
+ result = parseString(cleanHex, mergedOptions)
164
+ break
143
165
 
144
166
  case 4: // Array
145
- return parseArray(cleanHex, mergedOptions)
167
+ result = parseArray(cleanHex, mergedOptions)
168
+ break
146
169
 
147
170
  case 5: // Map
148
- return parseMap(cleanHex, mergedOptions)
171
+ result = parseMap(cleanHex, mergedOptions)
172
+ break
149
173
 
150
174
  case 6: // Tagged value
151
- return parseTag(cleanHex, mergedOptions)
175
+ result = parseTag(cleanHex, mergedOptions)
176
+ break
152
177
 
153
178
  case 7: // Floating-point or simple value
154
- return parseFloatOrSimple(cleanHex, mergedOptions)
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 = (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
-
214
+ const parseWithSourceMap = (input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap => {
182
215
  // Merge options with defaults
183
216
  const mergedOptions = mergeOptions(options)
184
217
 
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`)
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
- * Helper to parse integer from buffer
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
- const hexString = Array.from(buffer.slice(offset))
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 hexString = Array.from(buffer.slice(offset))
408
- .map(b => b.toString(16).padStart(2, '0'))
409
- .join('')
410
- return parseString(hexString, options)
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
- const hexString = Array.from(buffer.slice(offset))
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
- // 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
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
- 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
640
+ if (!foundBreak) {
641
+ throw new Error('Indefinite-length array missing break code (0xFF)')
503
642
  }
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
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
- const bytesRead = currentOffset - offset
662
+ const bytesRead = currentOffset - offset
524
663
 
525
- // Only set children if array is non-empty
526
- if (childPaths.length > 0 && sourceMap[arrayEntryIndex]) {
527
- sourceMap[arrayEntryIndex].children = childPaths
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
- return {
531
- value: items,
532
- bytesRead
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
- // 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
- }
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
- // 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}`)
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
- 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
806
+ if (!foundBreak) {
807
+ throw new Error('Indefinite-length map missing break code (0xFF)')
640
808
  }
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
- }
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
- 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
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
- const bytesRead = currentOffset - offset
852
+ const bytesRead = currentOffset - offset
681
853
 
682
- // Set children for the map entry
683
- if (sourceMap[mapEntryIndex]) {
684
- sourceMap[mapEntryIndex].children = childPaths
685
- }
854
+ // Set children for the map entry
855
+ if (sourceMap[mapEntryIndex]) {
856
+ sourceMap[mapEntryIndex].children = childPaths
857
+ }
686
858
 
687
- return {
688
- value: map,
689
- bytesRead
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 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)
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: tagResult.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 = (hexString: string, options?: ParseOptions): CborValue[] => {
828
- const cleanHex = hexString.replace(/\s+/g, '')
1050
+ const parseSequence = (input: string | Uint8Array, options?: ParseOptions): CborValue[] => {
1051
+ const mergedOptions = mergeOptions(options)
1052
+ let buffer: Uint8Array
829
1053
 
830
- // Empty sequence is valid
831
- if (!cleanHex || cleanHex.length === 0) {
832
- return []
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
- if (cleanHex.length % 2 !== 0) {
836
- throw new Error('Hex string must have even length')
837
- }
1063
+ // Empty sequence is valid
1064
+ if (!cleanHex || cleanHex.length === 0) {
1065
+ return []
1066
+ }
838
1067
 
839
- if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
840
- throw new Error(`Invalid hex character in: ${cleanHex}`)
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 remaining hex
856
- const remainingHex = Array.from(buffer.slice(offset))
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
- const remainingHex = Array.from(buffer.slice(offset))
906
- .map(b => b.toString(16).padStart(2, '0'))
907
- .join('')
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 => ({