@marcuspuchalla/nachos 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/dist/{chunk-PTWN7K3Y.cjs → chunk-3Z45RBZP.cjs} +469 -244
- package/dist/chunk-3Z45RBZP.cjs.map +1 -0
- package/dist/{chunk-2MTLSQ7E.js → chunk-EDXZTSIA.js} +224 -166
- package/dist/chunk-EDXZTSIA.js.map +1 -0
- package/dist/{chunk-R62CQQNI.cjs → chunk-HMUA5KLG.cjs} +239 -181
- package/dist/chunk-HMUA5KLG.cjs.map +1 -0
- package/dist/{chunk-ZDZ2B5PE.js → chunk-JESIF5IF.js} +7 -3
- package/dist/chunk-JESIF5IF.js.map +1 -0
- package/dist/{chunk-5A5T56JB.js → chunk-LWNWC2O7.js} +442 -217
- package/dist/chunk-LWNWC2O7.js.map +1 -0
- package/dist/{chunk-PD72MVTX.cjs → chunk-P6A2OOIY.cjs} +7 -3
- package/dist/chunk-P6A2OOIY.cjs.map +1 -0
- package/dist/encoder/index.cjs +14 -14
- package/dist/encoder/index.d.cts +5 -4
- package/dist/encoder/index.d.ts +5 -4
- package/dist/encoder/index.js +2 -2
- package/dist/index.cjs +58 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -21
- package/dist/index.d.ts +40 -21
- package/dist/index.js +37 -17
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/parser/index.cjs +21 -21
- package/dist/parser/index.d.cts +4 -2
- package/dist/parser/index.d.ts +4 -2
- package/dist/parser/index.js +2 -2
- package/dist/{types-DvNlfbKB.d.cts → types-eG2qalpr.d.cts} +27 -1
- package/dist/{types-DvNlfbKB.d.ts → types-eG2qalpr.d.ts} +27 -1
- package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -3
- package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -3
- package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-D4d7xG3-.d.cts} +9 -4
- package/dist/{useCborTag-Cs1CZuXZ.d.cts → useCborTag-TYst1KR6.d.ts} +9 -4
- package/package.json +1 -1
- package/src/__tests__/audit-fixes.test.ts +141 -0
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +5 -6
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +30 -26
- package/src/encoder/composables/useCborEncoder.ts +40 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +40 -9
- package/src/encoder/types.ts +9 -4
- package/src/encoder/utils.ts +33 -1
- package/src/index.ts +39 -20
- package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
- package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
- package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
- package/src/parser/__tests__/utils-errors.test.ts +11 -3
- package/src/parser/composables/useCborCollection.ts +51 -45
- package/src/parser/composables/useCborDiagnostic.ts +28 -0
- package/src/parser/composables/useCborFloat.ts +2 -1
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +448 -208
- package/src/parser/composables/useCborTag.ts +53 -38
- package/src/parser/types.ts +32 -1
- package/src/parser/utils.ts +52 -0
- package/dist/chunk-2MTLSQ7E.js.map +0 -1
- package/dist/chunk-5A5T56JB.js.map +0 -1
- package/dist/chunk-PD72MVTX.cjs.map +0 -1
- package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
- package/dist/chunk-R62CQQNI.cjs.map +0 -1
- package/dist/chunk-ZDZ2B5PE.js.map +0 -1
|
@@ -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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
})
|