@marcuspuchalla/nachos 0.1.3 → 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 (55) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/{chunk-5A5T56JB.js → chunk-5IWW5H47.js} +378 -207
  3. package/dist/chunk-5IWW5H47.js.map +1 -0
  4. package/dist/{chunk-PTWN7K3Y.cjs → chunk-RVG2BY32.cjs} +378 -207
  5. package/dist/chunk-RVG2BY32.cjs.map +1 -0
  6. package/dist/{chunk-R62CQQNI.cjs → chunk-S4RXO6IB.cjs} +195 -165
  7. package/dist/chunk-S4RXO6IB.cjs.map +1 -0
  8. package/dist/{chunk-2MTLSQ7E.js → chunk-UMAX5MX5.js} +195 -165
  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-Cs1CZuXZ.d.cts → useCborTag-BD6Sqp7p.d.ts} +9 -4
  29. package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-QpZR-Er2.d.cts} +9 -4
  30. package/package.json +1 -1
  31. package/src/__tests__/public-api.test.ts +153 -0
  32. package/src/__tests__/roundtrip.test.ts +5 -6
  33. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
  34. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
  35. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  36. package/src/encoder/composables/useCborCollectionEncoder.ts +28 -25
  37. package/src/encoder/composables/useCborEncoder.ts +21 -0
  38. package/src/encoder/composables/useCborSimpleEncoder.ts +34 -7
  39. package/src/encoder/types.ts +0 -2
  40. package/src/index.ts +29 -20
  41. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  42. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  43. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
  44. package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
  45. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  46. package/src/parser/composables/useCborCollection.ts +45 -42
  47. package/src/parser/composables/useCborFloat.ts +2 -1
  48. package/src/parser/composables/useCborInteger.ts +24 -10
  49. package/src/parser/composables/useCborParser.ts +387 -197
  50. package/src/parser/composables/useCborTag.ts +45 -37
  51. package/src/parser/utils.ts +11 -0
  52. package/dist/chunk-2MTLSQ7E.js.map +0 -1
  53. package/dist/chunk-5A5T56JB.js.map +0 -1
  54. package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
  55. package/dist/chunk-R62CQQNI.cjs.map +0 -1
@@ -10,9 +10,10 @@
10
10
  * These tests verify protections against known CBOR implementation vulnerabilities.
11
11
  */
12
12
 
13
- import { describe, it, expect } from 'vitest'
13
+ import { describe, it, expect, vi, afterEach } from 'vitest'
14
14
  import { useCborTag } from '../composables/useCborTag'
15
15
  import { useCborParser } from '../composables/useCborParser'
16
+ import type { TaggedValue } from '../types'
16
17
  import { useCborCollection } from '../composables/useCborCollection'
17
18
 
18
19
  describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
@@ -26,10 +27,10 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
26
27
 
27
28
  const result = parseTag(bignum, { limits: { maxBignumBytes: 1024 } })
28
29
 
29
- expect(result.value.tag).toBe(2)
30
- expect(typeof result.value.value).toBe('bigint')
30
+ expect((result.value as TaggedValue).tag).toBe(2)
31
+ expect(typeof (result.value as TaggedValue).value).toBe('bigint')
31
32
  // 512 bytes of 0x00 = BigInt 0
32
- expect(result.value.value).toBe(0n)
33
+ expect((result.value as TaggedValue).value).toBe(0n)
33
34
  })
34
35
 
35
36
  it('should reject tag 2 bignum exceeding size limit', () => {
@@ -64,11 +65,11 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
64
65
 
65
66
  const result = parseTag(bignum, { limits: { maxBignumBytes: 1024 } })
66
67
 
67
- expect(result.value.tag).toBe(2)
68
- expect(typeof result.value.value).toBe('bigint')
68
+ expect((result.value as TaggedValue).tag).toBe(2)
69
+ expect(typeof (result.value as TaggedValue).value).toBe('bigint')
69
70
  // 1024 bytes of 0xff should be a large positive bigint
70
71
  // 2^(1024*8) - 1
71
- expect(result.value.value > 0n).toBe(true)
72
+ expect(((result.value as TaggedValue).value as bigint) > 0n).toBe(true)
72
73
  })
73
74
 
74
75
  it('should reject tag 2 with 1 byte over limit', () => {
@@ -102,10 +103,10 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
102
103
 
103
104
  const result = parseTag(bignum, { limits: { maxBignumBytes: 4096 } })
104
105
 
105
- expect(result.value.tag).toBe(2)
106
- expect(typeof result.value.value).toBe('bigint')
106
+ expect((result.value as TaggedValue).tag).toBe(2)
107
+ expect(typeof (result.value as TaggedValue).value).toBe('bigint')
107
108
  // 2048 bytes of 0xbb should be a large positive bigint
108
- expect(result.value.value > 0n).toBe(true)
109
+ expect(((result.value as TaggedValue).value as bigint) > 0n).toBe(true)
109
110
  })
110
111
 
111
112
  it('should accept empty bignum (edge case)', () => {
@@ -116,10 +117,10 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
116
117
 
117
118
  const result = parseTag(bignum, { limits: { maxBignumBytes: 1024 } })
118
119
 
119
- expect(result.value.tag).toBe(2)
120
- expect(typeof result.value.value).toBe('bigint')
120
+ expect((result.value as TaggedValue).tag).toBe(2)
121
+ expect(typeof (result.value as TaggedValue).value).toBe('bigint')
121
122
  // Empty byte string = BigInt 0
122
- expect(result.value.value).toBe(0n)
123
+ expect((result.value as TaggedValue).value).toBe(0n)
123
124
  })
124
125
  })
125
126
 
@@ -133,11 +134,11 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
133
134
 
134
135
  const result = parseTag(bignum, { limits: { maxBignumBytes: 1024 } })
135
136
 
136
- expect(result.value.tag).toBe(3)
137
- expect(typeof result.value.value).toBe('bigint')
137
+ expect((result.value as TaggedValue).tag).toBe(3)
138
+ expect(typeof (result.value as TaggedValue).value).toBe('bigint')
138
139
  // Tag 3 = -1 - n, where n is the bignum value
139
140
  // 256 bytes of 0xff should be a large negative bigint
140
- expect(result.value.value < 0n).toBe(true)
141
+ expect(((result.value as TaggedValue).value as bigint) < 0n).toBe(true)
141
142
  })
142
143
 
143
144
  it('should reject tag 3 bignum exceeding size limit', () => {
@@ -183,8 +184,8 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
183
184
 
184
185
  const result = parseTag(epochTag, { limits: { maxBignumBytes: 100 } })
185
186
 
186
- expect(result.value.tag).toBe(1)
187
- expect(result.value.value).toBe(1363896240)
187
+ expect((result.value as TaggedValue).tag).toBe(1)
188
+ expect((result.value as TaggedValue).value).toBe(1363896240)
188
189
  })
189
190
 
190
191
  it('should NOT apply bignum limit to tag 24 (embedded CBOR)', () => {
@@ -195,8 +196,8 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
195
196
 
196
197
  const result = parseTag(embeddedCBOR, { limits: { maxBignumBytes: 2 } })
197
198
 
198
- expect(result.value.tag).toBe(24)
199
- expect(result.value.value).toBeInstanceOf(Uint8Array)
199
+ expect((result.value as TaggedValue).tag).toBe(24)
200
+ expect((result.value as TaggedValue).value).toBeInstanceOf(Uint8Array)
200
201
  })
201
202
 
202
203
  it('should NOT apply bignum limit to tag 258 (set)', () => {
@@ -207,8 +208,8 @@ describe('CVE-2020-28491: Bignum Memory Exhaustion Protection', () => {
207
208
 
208
209
  const result = parseTag(setTag, { limits: { maxBignumBytes: 2 } })
209
210
 
210
- expect(result.value.tag).toBe(258)
211
- expect(result.value.value).toBeInstanceOf(Array)
211
+ expect((result.value as TaggedValue).tag).toBe(258)
212
+ expect((result.value as TaggedValue).value).toBeInstanceOf(Array)
212
213
  })
213
214
  })
214
215
  })
@@ -223,7 +224,7 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
223
224
 
224
225
  const result = parseTag(nested, { limits: { maxTagDepth: 64 } })
225
226
 
226
- expect(result.value.tag).toBe(0)
227
+ expect((result.value as TaggedValue).tag).toBe(0)
227
228
  // Nested structure: tag 0 -> tag 0 -> ... -> 0
228
229
  })
229
230
 
@@ -255,7 +256,7 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
255
256
 
256
257
  const result = parseTag(nested, { limits: { maxTagDepth: 10 } })
257
258
 
258
- expect(result.value.tag).toBe(0)
259
+ expect((result.value as TaggedValue).tag).toBe(0)
259
260
  })
260
261
 
261
262
  it('should use default tag depth limit when not specified', () => {
@@ -276,7 +277,7 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
276
277
 
277
278
  const result = parseTag(nested, { limits: { maxTagDepth: 150 } })
278
279
 
279
- expect(result.value.tag).toBe(0)
280
+ expect((result.value as TaggedValue).tag).toBe(0)
280
281
  })
281
282
 
282
283
  it('should handle nested tags with different tag numbers', () => {
@@ -287,9 +288,9 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
287
288
 
288
289
  const result = parseTag(nested, { limits: { maxTagDepth: 5 } })
289
290
 
290
- expect(result.value.tag).toBe(1)
291
- expect(result.value.value.tag).toBe(2)
292
- expect((result.value.value as any).value).toHaveProperty('tag', 3)
291
+ expect((result.value as TaggedValue).tag).toBe(1)
292
+ expect(((result.value as TaggedValue).value as TaggedValue).tag).toBe(2)
293
+ expect(((result.value as TaggedValue).value as any).value).toHaveProperty('tag', 3)
293
294
  })
294
295
 
295
296
  it('should reject deeply nested mixed tags', () => {
@@ -312,8 +313,8 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
312
313
  limits: { maxTagDepth: 5, maxDepth: 5 }
313
314
  })
314
315
 
315
- expect(result.value.tag).toBe(1)
316
- expect(result.value.value).toEqual([1, 2, 3])
316
+ expect((result.value as TaggedValue).tag).toBe(1)
317
+ expect((result.value as TaggedValue).value).toEqual([1, 2, 3])
317
318
  })
318
319
 
319
320
  it('should prevent stack overflow with minimal input (< 1KB)', () => {
@@ -467,7 +468,7 @@ describe('Combined Security Protections', () => {
467
468
  }
468
469
  })
469
470
 
470
- expect(result.value.tag).toBe(1)
471
+ expect((result.value as TaggedValue).tag).toBe(1)
471
472
  })
472
473
 
473
474
  it('should reject when any single limit is exceeded', () => {
@@ -501,3 +502,135 @@ describe('Combined Security Protections', () => {
501
502
  .toThrow(/duplicate/i)
502
503
  })
503
504
  })
505
+
506
+ describe('maxParseTime Timeout Protection for Standard decode() Path', () => {
507
+ afterEach(() => {
508
+ vi.restoreAllMocks()
509
+ })
510
+
511
+ describe('useCborCollection: parseItem timeout', () => {
512
+ it('should enforce maxParseTime when parsing deeply nested arrays via standard path', () => {
513
+ // Mock Date.now: first call returns start time, subsequent calls simulate time passing
514
+ let callCount = 0
515
+ vi.spyOn(Date, 'now').mockImplementation(() => {
516
+ callCount++
517
+ // First call sets parseStartTime, subsequent calls check elapsed
518
+ return callCount <= 1 ? 1000 : 1100
519
+ })
520
+
521
+ const { parseArray } = useCborCollection()
522
+
523
+ // Build a deeply nested array: [[[[...]]]]
524
+ // 10 levels of nesting, innermost value is 0x00 (integer 0)
525
+ const nested = '81'.repeat(10) + '00'
526
+
527
+ // maxParseTime=50ms, but mocked time shows 100ms elapsed
528
+ expect(() => parseArray(nested, { limits: { maxParseTime: 50, maxDepth: 200 } }))
529
+ .toThrow(/parse timeout/i)
530
+ })
531
+
532
+ it('should enforce maxParseTime when parsing deeply nested maps via standard path', () => {
533
+ let callCount = 0
534
+ vi.spyOn(Date, 'now').mockImplementation(() => {
535
+ callCount++
536
+ return callCount <= 1 ? 1000 : 1100
537
+ })
538
+
539
+ const { parseMap } = useCborCollection()
540
+
541
+ // Build nested map: {"a": {"a": ... 0 }}
542
+ const nested = 'a16161'.repeat(10) + '00'
543
+
544
+ expect(() => parseMap(nested, { limits: { maxParseTime: 50, maxDepth: 200 } }))
545
+ .toThrow(/parse timeout/i)
546
+ })
547
+
548
+ it('should enforce maxParseTime via the top-level decode() function', () => {
549
+ let callCount = 0
550
+ vi.spyOn(Date, 'now').mockImplementation(() => {
551
+ callCount++
552
+ return callCount <= 1 ? 1000 : 1100
553
+ })
554
+
555
+ const { parse } = useCborParser()
556
+
557
+ // Nested arrays through the standard parse() path
558
+ const nested = '81'.repeat(10) + '00'
559
+
560
+ expect(() => parse(nested, { limits: { maxParseTime: 50, maxDepth: 200 } }))
561
+ .toThrow(/parse timeout/i)
562
+ })
563
+
564
+ it('should NOT timeout when parsing completes quickly with generous maxParseTime', () => {
565
+ // Mock Date.now to always return same value (no time passes)
566
+ vi.spyOn(Date, 'now').mockReturnValue(1000)
567
+
568
+ const { parse } = useCborParser()
569
+
570
+ // Simple array [1, 2, 3]
571
+ const result = parse('83010203', { limits: { maxParseTime: 5000 } })
572
+ expect(result.value).toEqual([1, 2, 3])
573
+ })
574
+
575
+ it('should NOT timeout for simple values when maxParseTime is set', () => {
576
+ vi.spyOn(Date, 'now').mockReturnValue(1000)
577
+
578
+ const { parse } = useCborParser()
579
+
580
+ const result = parse('1864', { limits: { maxParseTime: 5000 } })
581
+ expect(result.value).toBe(100)
582
+ })
583
+ })
584
+
585
+ describe('useCborTag: parseItem timeout', () => {
586
+ it('should enforce maxParseTime when parsing tags containing deeply nested structures', () => {
587
+ let callCount = 0
588
+ vi.spyOn(Date, 'now').mockImplementation(() => {
589
+ callCount++
590
+ return callCount <= 1 ? 1000 : 1100
591
+ })
592
+
593
+ const { parseTag } = useCborTag()
594
+
595
+ // Tag 1 wrapping deeply nested arrays
596
+ const nested = 'c1' + '81'.repeat(10) + '00'
597
+
598
+ expect(() => parseTag(nested, { limits: { maxParseTime: 50, maxDepth: 200, maxTagDepth: 200 } }))
599
+ .toThrow(/parse timeout/i)
600
+ })
601
+
602
+ it('should enforce maxParseTime for deeply nested tags', () => {
603
+ let callCount = 0
604
+ vi.spyOn(Date, 'now').mockImplementation(() => {
605
+ callCount++
606
+ return callCount <= 1 ? 1000 : 1100
607
+ })
608
+
609
+ const { parseTag } = useCborTag()
610
+
611
+ // 10 nested tags wrapping an integer
612
+ const nested = 'c0'.repeat(10) + '00'
613
+
614
+ expect(() => parseTag(nested, { limits: { maxParseTime: 50, maxTagDepth: 200 } }))
615
+ .toThrow(/parse timeout/i)
616
+ })
617
+ })
618
+
619
+ describe('parseSequence timeout', () => {
620
+ it('should enforce maxParseTime when parsing a CBOR sequence', () => {
621
+ let callCount = 0
622
+ vi.spyOn(Date, 'now').mockImplementation(() => {
623
+ callCount++
624
+ return callCount <= 1 ? 1000 : 1100
625
+ })
626
+
627
+ const { parseSequence } = useCborParser()
628
+
629
+ // Sequence of nested arrays
630
+ const nested = '81'.repeat(10) + '00'
631
+
632
+ expect(() => parseSequence(nested, { limits: { maxParseTime: 50, maxDepth: 200 } }))
633
+ .toThrow(/parse timeout/i)
634
+ })
635
+ })
636
+ })
@@ -20,11 +20,9 @@
20
20
 
21
21
  import { describe, it, expect } from 'vitest'
22
22
  import { useCborParser } from '../composables/useCborParser'
23
- import { useCborTag } from '../composables/useCborTag'
24
23
 
25
24
  describe('CBOR Standard Tags (RFC 8949)', () => {
26
25
  const { parse } = useCborParser()
27
- const { parseTag } = useCborTag()
28
26
 
29
27
  describe('Tag 0: Date/Time String (RFC 3339)', () => {
30
28
  it('should parse valid ISO 8601 date/time string', () => {
@@ -149,17 +147,57 @@ describe('CBOR Standard Tags (RFC 8949)', () => {
149
147
  })
150
148
 
151
149
  it('should reject non-integer exponent in strict mode', () => {
152
- // Tag 4 + [3.14, 500] (invalid - exponent must be integer)
150
+ // Tag 4 + [3.14, 500] (invalid - exponent must be integer per RFC 8949)
153
151
  // fb 40091eb851eb851f = float64 3.14
154
152
  // 1901f4 = uint 500
155
153
  const hex = 'c482fb40091eb851eb851f1901f4' // [3.14, 500]
156
154
 
157
- // Note: The parser validates that exponent is number|bigint, and 3.14 is a number
158
- // So this passes parsing but semantically the exponent should be integer
159
- // The current implementation accepts floats as exponents, which is RFC-compliant
160
- // (RFC says "integer" but implementations often accept any number)
155
+ expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
156
+ .toThrow(/exponent must be an integer/)
157
+ })
158
+
159
+ it('should reject non-integer mantissa in strict mode', () => {
160
+ // Tag 4 + [-2, 3.14] (invalid - mantissa must be integer per RFC 8949)
161
+ // 21 = -2
162
+ // fb 40091eb851eb851f = float64 3.14
163
+ const hex = 'c48221fb40091eb851eb851f' // [-2, 3.14]
164
+
165
+ expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
166
+ .toThrow(/mantissa must be an integer/)
167
+ })
168
+
169
+ it('should accept integer exponent and mantissa values', () => {
170
+ // Tag 4 + [0, 42]
171
+ // 00 = 0, 1829 = 42 (wrong, 182a = 42)
172
+ // Actually: 00 = 0, 18 2a = 42
173
+ const hex = 'c48200182a' // [0, 42]
174
+ const result = parse(hex, { strict: true, validateTagSemantics: true })
175
+ expect(result.value).toMatchObject({ tag: 4 })
176
+ const arr = (result.value as any).value as number[]
177
+ expect(arr[0]).toBe(0)
178
+ expect(arr[1]).toBe(42)
179
+ })
180
+
181
+ it('should accept negative integer exponent', () => {
182
+ // Tag 4 + [-1, 500]
183
+ // 20 = -1, 1901f4 = 500
184
+ const hex = 'c482201901f4' // [-1, 500]
185
+ const result = parse(hex, { strict: true, validateTagSemantics: true })
186
+ expect(result.value).toMatchObject({ tag: 4 })
187
+ const arr = (result.value as any).value as number[]
188
+ expect(arr[0]).toBe(-1)
189
+ expect(arr[1]).toBe(500)
190
+ })
191
+
192
+ it('should accept zero exponent and large mantissa', () => {
193
+ // Tag 4 + [0, 12345]
194
+ // 00 = 0, 193039 = 12345
195
+ const hex = 'c48200193039'
161
196
  const result = parse(hex, { strict: true, validateTagSemantics: true })
162
197
  expect(result.value).toMatchObject({ tag: 4 })
198
+ const arr = (result.value as any).value as number[]
199
+ expect(arr[0]).toBe(0)
200
+ expect(arr[1]).toBe(12345)
163
201
  })
164
202
  })
165
203
 
@@ -187,6 +225,36 @@ describe('CBOR Standard Tags (RFC 8949)', () => {
187
225
  expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
188
226
  .toThrow(/exactly 2 elements/i)
189
227
  })
228
+
229
+ it('should reject non-integer exponent in strict mode', () => {
230
+ // Tag 5 + [3.14, 500] (invalid - exponent must be integer per RFC 8949)
231
+ // fb 40091eb851eb851f = float64 3.14
232
+ // 1901f4 = uint 500
233
+ const hex = 'c582fb40091eb851eb851f1901f4' // [3.14, 500]
234
+
235
+ expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
236
+ .toThrow(/exponent must be an integer/)
237
+ })
238
+
239
+ it('should reject non-integer mantissa in strict mode', () => {
240
+ // Tag 5 + [-2, 3.14] (invalid - mantissa must be integer per RFC 8949)
241
+ // 21 = -2
242
+ // fb 40091eb851eb851f = float64 3.14
243
+ const hex = 'c58221fb40091eb851eb851f' // [-2, 3.14]
244
+
245
+ expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
246
+ .toThrow(/mantissa must be an integer/)
247
+ })
248
+
249
+ it('should accept integer exponent and mantissa values', () => {
250
+ // Tag 5 + [-2, 500]
251
+ const hex = 'c5822119 01f4'.replace(/\s/g, '')
252
+ const result = parse(hex, { strict: true, validateTagSemantics: true })
253
+ expect(result.value).toMatchObject({ tag: 5 })
254
+ const arr = (result.value as any).value as number[]
255
+ expect(arr[0]).toBe(-2)
256
+ expect(arr[1]).toBe(500)
257
+ })
190
258
  })
191
259
 
192
260
  describe('Tags 21-23: Expected Encoding', () => {
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Tests for eliminating exponential re-parsing in source-map tag handling
3
+ * Task 2-B: parseTagWithMap should NOT re-parse the entire tag subtree
4
+ *
5
+ * The fix ensures that parseTagWithMap uses already-parsed values plus
6
+ * direct calls to validateTagSemantics and decodePlutusConstructor,
7
+ * instead of calling parseTag(hexString) which re-parses everything.
8
+ */
9
+
10
+ import { describe, it, expect, vi } from 'vitest'
11
+ import { useCborParser } from '../composables/useCborParser'
12
+ import { useCborTag } from '../composables/useCborTag'
13
+
14
+ describe('Tag Source Map Re-parse Elimination (Task 2-B)', () => {
15
+ describe('Correctness: parseWithSourceMap still produces correct values', () => {
16
+ it('should correctly decode tag 121 with array', () => {
17
+ const { parseWithSourceMap } = useCborParser()
18
+ const result = parseWithSourceMap('d87983010203') // Tag 121, [1, 2, 3]
19
+
20
+ expect(result.value).toEqual({
21
+ tag: 121,
22
+ value: [1, 2, 3],
23
+ plutus: { constructor: 0, fields: [1, 2, 3] }
24
+ })
25
+ })
26
+
27
+ it('should correctly decode tag 122 with array', () => {
28
+ const { parseWithSourceMap } = useCborParser()
29
+ const result = parseWithSourceMap('d87a81182a') // Tag 122, [42]
30
+
31
+ expect(result.value).toEqual({
32
+ tag: 122,
33
+ value: [42],
34
+ plutus: { constructor: 1, fields: [42] }
35
+ })
36
+ })
37
+
38
+ it('should correctly decode tag 102 alternative constructor', () => {
39
+ const { parseWithSourceMap } = useCborParser()
40
+ const result = parseWithSourceMap('d8668218c8811863') // Tag 102, [200, [99]]
41
+
42
+ expect(result.value).toEqual({
43
+ tag: 102,
44
+ value: [200, [99]],
45
+ plutus: { constructor: 200, fields: [99] }
46
+ })
47
+ })
48
+
49
+ it('should correctly decode extended constructor tag 1283', () => {
50
+ const { parseWithSourceMap } = useCborParser()
51
+ const result = parseWithSourceMap('d9050383010203') // Tag 1283, [1, 2, 3]
52
+
53
+ expect(result.value).toEqual({
54
+ tag: 1283,
55
+ value: [1, 2, 3],
56
+ plutus: { constructor: 10, fields: [1, 2, 3] }
57
+ })
58
+ })
59
+
60
+ it('should correctly decode bignum tag 2 (positive)', () => {
61
+ const { parseWithSourceMap } = useCborParser()
62
+ // Tag 2, byte string 0x01 0x00 (= 256)
63
+ const result = parseWithSourceMap('c2420100')
64
+
65
+ expect(result.value).toEqual({
66
+ tag: 2,
67
+ value: 256n
68
+ })
69
+ })
70
+
71
+ it('should correctly decode bignum tag 3 (negative)', () => {
72
+ const { parseWithSourceMap } = useCborParser()
73
+ // Tag 3, byte string 0x01 0x00 (= -1 - 256 = -257)
74
+ const result = parseWithSourceMap('c3420100')
75
+
76
+ expect(result.value).toEqual({
77
+ tag: 3,
78
+ value: -257n
79
+ })
80
+ })
81
+
82
+ it('should correctly handle tag 0 (date/time string)', () => {
83
+ const { parseWithSourceMap } = useCborParser()
84
+ // Tag 0 containing "2013-03-21T20:04:00Z"
85
+ const result = parseWithSourceMap('c074323031332d30332d32315432303a30343a30305a')
86
+
87
+ const value = result.value as any
88
+ expect(value.tag).toBe(0)
89
+ expect(value.value).toBe('2013-03-21T20:04:00Z')
90
+ })
91
+
92
+ it('should correctly handle tag 1 (epoch time)', () => {
93
+ const { parseWithSourceMap } = useCborParser()
94
+ // Tag 1 containing 1363896240
95
+ const result = parseWithSourceMap('c11a514b67b0')
96
+
97
+ expect(result.value).toEqual({
98
+ tag: 1,
99
+ value: 1363896240
100
+ })
101
+ })
102
+
103
+ it('should correctly handle nested tags', () => {
104
+ const { parseWithSourceMap } = useCborParser()
105
+ // Tag 121 -> [Tag 121 -> []]
106
+ const result = parseWithSourceMap('d87981d87980')
107
+
108
+ const outer = result.value as any
109
+ expect(outer.tag).toBe(121)
110
+ expect(outer.plutus).toEqual({ constructor: 0, fields: [{ tag: 121, value: [], plutus: { constructor: 0, fields: [] } }] })
111
+ })
112
+
113
+ it('should correctly handle tag 121 with empty array', () => {
114
+ const { parseWithSourceMap } = useCborParser()
115
+ const result = parseWithSourceMap('d87980')
116
+
117
+ expect(result.value).toEqual({
118
+ tag: 121,
119
+ value: [],
120
+ plutus: { constructor: 0, fields: [] }
121
+ })
122
+ })
123
+
124
+ it('should correctly handle self-describe tag 55799', () => {
125
+ const { parseWithSourceMap } = useCborParser()
126
+ // Tag 55799 wrapping integer 42
127
+ const result = parseWithSourceMap('d9d9f7182a')
128
+
129
+ expect(result.value).toEqual({
130
+ tag: 55799,
131
+ value: 42
132
+ })
133
+ })
134
+
135
+ it('should correctly handle non-Plutus tag with non-array value', () => {
136
+ const { parseWithSourceMap } = useCborParser()
137
+ // Tag 1 containing 0
138
+ const result = parseWithSourceMap('c100')
139
+
140
+ expect(result.value).toEqual({
141
+ tag: 1,
142
+ value: 0
143
+ })
144
+ })
145
+ })
146
+
147
+ describe('Correctness: source maps remain identical', () => {
148
+ it('should produce same source map for simple tagged value', () => {
149
+ const { parseWithSourceMap } = useCborParser()
150
+ const result = parseWithSourceMap('d879182a') // Tag 121, 42
151
+
152
+ expect(result.sourceMap).toHaveLength(2)
153
+ expect(result.sourceMap[0]).toMatchObject({
154
+ path: '',
155
+ majorType: 6,
156
+ type: 'tag(121)',
157
+ children: ['.value']
158
+ })
159
+ expect(result.sourceMap[1]).toMatchObject({
160
+ path: '.value',
161
+ majorType: 0,
162
+ parent: ''
163
+ })
164
+ })
165
+
166
+ it('should produce same source map for nested tags', () => {
167
+ const { parseWithSourceMap } = useCborParser()
168
+ const result = parseWithSourceMap('d87981d87980')
169
+
170
+ const outerTag = result.sourceMap.find(e => e.path === '')
171
+ expect(outerTag?.majorType).toBe(6)
172
+ expect(outerTag?.children).toEqual(['.value'])
173
+
174
+ const array = result.sourceMap.find(e => e.path === '.value')
175
+ expect(array?.majorType).toBe(4)
176
+ expect(array?.parent).toBe('')
177
+
178
+ const innerTag = result.sourceMap.find(e => e.path === '.value[0]')
179
+ expect(innerTag?.majorType).toBe(6)
180
+ expect(innerTag?.parent).toBe('.value')
181
+ })
182
+ })
183
+
184
+ describe('Validation still works through source-map path', () => {
185
+ it('should validate tag 2/3 bignum byte limit', () => {
186
+ const { parseWithSourceMap } = useCborParser()
187
+ // Tag 2 with a byte string exceeding the limit
188
+ // Create a very long byte string (> default 1024 bytes)
189
+ const longByteString = '59' + '0401' + 'ff'.repeat(1025)
190
+ const hex = 'c2' + longByteString
191
+
192
+ expect(() => parseWithSourceMap(hex)).toThrow(/bignum/i)
193
+ })
194
+
195
+ it('should validate tag semantics in strict mode', () => {
196
+ const { parseWithSourceMap } = useCborParser()
197
+ // Tag 0 (date/time) with integer value (should be text string)
198
+ // c0 00 = tag(0) + integer(0)
199
+ expect(() => parseWithSourceMap('c000', { strict: true })).toThrow(/tag 0/i)
200
+ })
201
+
202
+ it('should validate Plutus semantics in strict mode', () => {
203
+ const { parseWithSourceMap } = useCborParser()
204
+ // Tag 121 with non-array value (should be array)
205
+ // d8 79 00 = tag(121) + integer(0)
206
+ expect(() => parseWithSourceMap('d87900', { strict: true })).toThrow(/Plutus/i)
207
+ })
208
+ })
209
+
210
+ describe('Performance: no exponential re-parsing', () => {
211
+ it('should parse deeply nested tags in linear time', () => {
212
+ const { parseWithSourceMap } = useCborParser()
213
+
214
+ // Build a deeply nested tag: tag(121) -> [tag(121) -> [tag(121) -> ... -> []]]
215
+ // At depth D, the old code would do O(D^2) work due to re-parsing.
216
+ // With the fix, it should be O(D).
217
+ const buildNestedTagHex = (depth: number): string => {
218
+ // Each level: d8 79 81 (tag 121, 1-element array)
219
+ // Innermost: d8 79 80 (tag 121, empty array)
220
+ let hex = ''
221
+ for (let i = 0; i < depth - 1; i++) {
222
+ hex += 'd87981' // tag(121), array(1)
223
+ }
224
+ hex += 'd87980' // tag(121), array(0)
225
+ return hex
226
+ }
227
+
228
+ // Time a moderate depth (20 levels)
229
+ const hex20 = buildNestedTagHex(20)
230
+ const start20 = performance.now()
231
+ const result20 = parseWithSourceMap(hex20)
232
+ const time20 = performance.now() - start20
233
+
234
+ // Time a deeper nesting (40 levels)
235
+ const hex40 = buildNestedTagHex(40)
236
+ const start40 = performance.now()
237
+ const result40 = parseWithSourceMap(hex40)
238
+ const time40 = performance.now() - start40
239
+
240
+ // With O(D^2), doubling depth quadruples time: time40/time20 ~ 4
241
+ // With O(D), doubling depth doubles time: time40/time20 ~ 2
242
+ // Use a generous bound: if fixed, ratio should be < 3
243
+ // If unfixed (quadratic), ratio tends toward 4+
244
+ //
245
+ // Note: for small absolute times, jitter can dominate.
246
+ // The real test is that 40-deep should still complete quickly (< 100ms).
247
+ expect(time40).toBeLessThan(100) // Should be very fast with fix
248
+
249
+ // Both should parse successfully
250
+ expect((result20.value as any).tag).toBe(121)
251
+ expect((result40.value as any).tag).toBe(121)
252
+ })
253
+ })
254
+
255
+ describe('useCborTag exports', () => {
256
+ it('should export validateTagSemantics function', () => {
257
+ const tag = useCborTag()
258
+ expect(tag.validateTagSemantics).toBeDefined()
259
+ expect(typeof tag.validateTagSemantics).toBe('function')
260
+ })
261
+
262
+ it('should export decodePlutusConstructor function', () => {
263
+ const tag = useCborTag()
264
+ expect(tag.decodePlutusConstructor).toBeDefined()
265
+ expect(typeof tag.decodePlutusConstructor).toBe('function')
266
+ })
267
+ })
268
+ })