@marcuspuchalla/nachos 0.1.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 +64 -0
- package/LICENSE +674 -0
- package/README.md +345 -0
- package/dist/chunk-2FUTHZQQ.cjs +755 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +1 -0
- package/dist/chunk-2HBCILJS.cjs +2034 -0
- package/dist/chunk-2HBCILJS.cjs.map +1 -0
- package/dist/chunk-7CFYWHS6.js +742 -0
- package/dist/chunk-7CFYWHS6.js.map +1 -0
- package/dist/chunk-PD72MVTX.cjs +160 -0
- package/dist/chunk-PD72MVTX.cjs.map +1 -0
- package/dist/chunk-ZDZ2B5PE.js +149 -0
- package/dist/chunk-ZDZ2B5PE.js.map +1 -0
- package/dist/chunk-ZRPJUEIZ.js +2020 -0
- package/dist/chunk-ZRPJUEIZ.js.map +1 -0
- package/dist/encoder/index.cjs +57 -0
- package/dist/encoder/index.cjs.map +1 -0
- package/dist/encoder/index.d.cts +72 -0
- package/dist/encoder/index.d.ts +72 -0
- package/dist/encoder/index.js +4 -0
- package/dist/encoder/index.js.map +1 -0
- package/dist/index.cjs +606 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +494 -0
- package/dist/index.d.ts +494 -0
- package/dist/index.js +523 -0
- package/dist/index.js.map +1 -0
- package/dist/metafile-cjs.json +1 -0
- package/dist/metafile-esm.json +1 -0
- package/dist/parser/index.cjs +85 -0
- package/dist/parser/index.cjs.map +1 -0
- package/dist/parser/index.d.cts +72 -0
- package/dist/parser/index.d.ts +72 -0
- package/dist/parser/index.js +4 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/types-DvNlfbKB.d.cts +301 -0
- package/dist/types-DvNlfbKB.d.ts +301 -0
- package/dist/useCborSimpleEncoder-ButVU988.d.cts +268 -0
- package/dist/useCborSimpleEncoder-TVxzNJ_9.d.ts +268 -0
- package/dist/useCborTag-B_iaShG6.d.ts +142 -0
- package/dist/useCborTag-BfTIV8HM.d.cts +142 -0
- package/package.json +102 -0
- package/src/__tests__/public-api.test.ts +326 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +331 -0
- package/src/encoder/__tests__/cbor-integer-encoder.test.ts +283 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +224 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +345 -0
- package/src/encoder/__tests__/cbor-tag-encoder.test.ts +565 -0
- package/src/encoder/composables/#useCborTagEncoder.ts# +158 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +424 -0
- package/src/encoder/composables/useCborEncoder.ts +203 -0
- package/src/encoder/composables/useCborIntegerEncoder.ts +188 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +266 -0
- package/src/encoder/composables/useCborStringEncoder.ts +266 -0
- package/src/encoder/composables/useCborTagEncoder.ts +158 -0
- package/src/encoder/index.ts +35 -0
- package/src/encoder/types.ts +88 -0
- package/src/encoder/utils.ts +80 -0
- package/src/index.ts +434 -0
- package/src/parser/__tests__/ast-tree-structure.test.ts +311 -0
- package/src/parser/__tests__/cbor-collection-errors.test.ts +296 -0
- package/src/parser/__tests__/cbor-collection.test.ts +369 -0
- package/src/parser/__tests__/cbor-deterministic-encoding.test.ts +432 -0
- package/src/parser/__tests__/cbor-diagnostic.test.ts +333 -0
- package/src/parser/__tests__/cbor-duplicate-keys.test.ts +235 -0
- package/src/parser/__tests__/cbor-float-errors.test.ts +222 -0
- package/src/parser/__tests__/cbor-float.test.ts +502 -0
- package/src/parser/__tests__/cbor-integer-errors.test.ts +139 -0
- package/src/parser/__tests__/cbor-integer.test.ts +200 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +403 -0
- package/src/parser/__tests__/cbor-parser-errors.test.ts +126 -0
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +503 -0
- package/src/parser/__tests__/cbor-sequences.test.ts +150 -0
- package/src/parser/__tests__/cbor-source-map.test.ts +321 -0
- package/src/parser/__tests__/cbor-standard-tags.test.ts +340 -0
- package/src/parser/__tests__/cbor-string-errors.test.ts +227 -0
- package/src/parser/__tests__/cbor-string.test.ts +224 -0
- package/src/parser/__tests__/cbor-tag-advanced.test.ts +500 -0
- package/src/parser/__tests__/cbor-tag-errors.test.ts +447 -0
- package/src/parser/__tests__/cbor-tag-source-map.test.ts +360 -0
- package/src/parser/__tests__/cbor-tag.test.ts +684 -0
- package/src/parser/__tests__/extreme-edge-cases.test.ts +146 -0
- package/src/parser/__tests__/pathBuilder.test.ts +256 -0
- package/src/parser/__tests__/rfc-test-vectors.test.ts +607 -0
- package/src/parser/__tests__/security-limits.test.ts +248 -0
- package/src/parser/__tests__/utils-errors.test.ts +127 -0
- package/src/parser/composables/useCborCollection.ts +509 -0
- package/src/parser/composables/useCborDiagnostic.ts +381 -0
- package/src/parser/composables/useCborFloat.ts +256 -0
- package/src/parser/composables/useCborInteger.ts +114 -0
- package/src/parser/composables/useCborParser.ts +951 -0
- package/src/parser/composables/useCborString.ts +330 -0
- package/src/parser/composables/useCborStringTypes.ts +129 -0
- package/src/parser/composables/useCborTag.ts +739 -0
- package/src/parser/index.ts +56 -0
- package/src/parser/types.ts +371 -0
- package/src/parser/utils/pathBuilder.ts +259 -0
- package/src/parser/utils.ts +398 -0
- package/src/utils/__tests__/logger.test.ts +186 -0
- package/src/utils/logger.ts +96 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { useCborInteger } from '../composables/useCborInteger'
|
|
3
|
+
|
|
4
|
+
describe('useCborInteger', () => {
|
|
5
|
+
describe('parseUnsignedInteger (Major Type 0)', () => {
|
|
6
|
+
it('should parse small integers (0-23) - direct encoding', () => {
|
|
7
|
+
const { parseInteger } = useCborInteger()
|
|
8
|
+
|
|
9
|
+
// Test case 1: Value 0 (RFC 8949 Appendix A)
|
|
10
|
+
const result1 = parseInteger('00')
|
|
11
|
+
expect(result1.value).toBe(0)
|
|
12
|
+
expect(result1.bytesRead).toBe(1)
|
|
13
|
+
|
|
14
|
+
// Test case 2: Value 10
|
|
15
|
+
const result2 = parseInteger('0a')
|
|
16
|
+
expect(result2.value).toBe(10)
|
|
17
|
+
expect(result2.bytesRead).toBe(1)
|
|
18
|
+
|
|
19
|
+
// Test case 3: Value 23 (maximum direct encoding)
|
|
20
|
+
const result3 = parseInteger('17')
|
|
21
|
+
expect(result3.value).toBe(23)
|
|
22
|
+
expect(result3.bytesRead).toBe(1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should parse 1-byte integers (24-255)', () => {
|
|
26
|
+
const { parseInteger } = useCborInteger()
|
|
27
|
+
|
|
28
|
+
// Test case 1: Value 24 (RFC 8949 Appendix A)
|
|
29
|
+
const result1 = parseInteger('1818')
|
|
30
|
+
expect(result1.value).toBe(24)
|
|
31
|
+
expect(result1.bytesRead).toBe(2)
|
|
32
|
+
|
|
33
|
+
// Test case 2: Value 100 (common Cardano value)
|
|
34
|
+
const result2 = parseInteger('1864')
|
|
35
|
+
expect(result2.value).toBe(100)
|
|
36
|
+
expect(result2.bytesRead).toBe(2)
|
|
37
|
+
|
|
38
|
+
// Test case 3: Value 255 (maximum 1-byte)
|
|
39
|
+
const result3 = parseInteger('18ff')
|
|
40
|
+
expect(result3.value).toBe(255)
|
|
41
|
+
expect(result3.bytesRead).toBe(2)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should parse 2-byte integers (256-65535)', () => {
|
|
45
|
+
const { parseInteger } = useCborInteger()
|
|
46
|
+
|
|
47
|
+
// Test case 1: Value 1000 (RFC 8949 Appendix A)
|
|
48
|
+
const result1 = parseInteger('1903e8')
|
|
49
|
+
expect(result1.value).toBe(1000)
|
|
50
|
+
expect(result1.bytesRead).toBe(3)
|
|
51
|
+
|
|
52
|
+
// Test case 2: Value 256 (minimum 2-byte)
|
|
53
|
+
const result2 = parseInteger('190100')
|
|
54
|
+
expect(result2.value).toBe(256)
|
|
55
|
+
expect(result2.bytesRead).toBe(3)
|
|
56
|
+
|
|
57
|
+
// Test case 3: Value 65535 (maximum 2-byte)
|
|
58
|
+
const result3 = parseInteger('19ffff')
|
|
59
|
+
expect(result3.value).toBe(65535)
|
|
60
|
+
expect(result3.bytesRead).toBe(3)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should parse 4-byte integers (up to 4294967295)', () => {
|
|
64
|
+
const { parseInteger } = useCborInteger()
|
|
65
|
+
|
|
66
|
+
// Test case 1: Value 1000000 (RFC 8949 / Cardano lovelace)
|
|
67
|
+
const result1 = parseInteger('1a000f4240')
|
|
68
|
+
expect(result1.value).toBe(1000000)
|
|
69
|
+
expect(result1.bytesRead).toBe(5)
|
|
70
|
+
|
|
71
|
+
// Test case 2: Value 65536 (minimum 4-byte)
|
|
72
|
+
const result2 = parseInteger('1a00010000')
|
|
73
|
+
expect(result2.value).toBe(65536)
|
|
74
|
+
expect(result2.bytesRead).toBe(5)
|
|
75
|
+
|
|
76
|
+
// Test case 3: Value 4294967295 (maximum 4-byte)
|
|
77
|
+
const result3 = parseInteger('1affffffff')
|
|
78
|
+
expect(result3.value).toBe(4294967295)
|
|
79
|
+
expect(result3.bytesRead).toBe(5)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should parse 8-byte integers with BigInt for large values', () => {
|
|
83
|
+
const { parseInteger } = useCborInteger()
|
|
84
|
+
|
|
85
|
+
// Test case 1: Value within Number.MAX_SAFE_INTEGER (return number)
|
|
86
|
+
const result1 = parseInteger('1b0000000000000064') // 100 in 8 bytes
|
|
87
|
+
expect(result1.value).toBe(100)
|
|
88
|
+
expect(typeof result1.value).toBe('number')
|
|
89
|
+
expect(result1.bytesRead).toBe(9)
|
|
90
|
+
|
|
91
|
+
// Test case 2: Value above Number.MAX_SAFE_INTEGER (return BigInt)
|
|
92
|
+
const result2 = parseInteger('1b0020000000000000') // 9007199254740992
|
|
93
|
+
expect(result2.value).toBe(9007199254740992n)
|
|
94
|
+
expect(typeof result2.value).toBe('bigint')
|
|
95
|
+
expect(result2.bytesRead).toBe(9)
|
|
96
|
+
|
|
97
|
+
// Test case 3: Maximum 64-bit value (return BigInt)
|
|
98
|
+
const result3 = parseInteger('1bffffffffffffffff')
|
|
99
|
+
expect(result3.value).toBe(18446744073709551615n)
|
|
100
|
+
expect(typeof result3.value).toBe('bigint')
|
|
101
|
+
expect(result3.bytesRead).toBe(9)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('parseNegativeInteger (Major Type 1)', () => {
|
|
106
|
+
it('should parse small negative integers (-1 to -24)', () => {
|
|
107
|
+
const { parseInteger } = useCborInteger()
|
|
108
|
+
|
|
109
|
+
// Test case 1: Value -1 (RFC 8949 Appendix A)
|
|
110
|
+
const result1 = parseInteger('20')
|
|
111
|
+
expect(result1.value).toBe(-1)
|
|
112
|
+
expect(result1.bytesRead).toBe(1)
|
|
113
|
+
|
|
114
|
+
// Test case 2: Value -10
|
|
115
|
+
const result2 = parseInteger('29')
|
|
116
|
+
expect(result2.value).toBe(-10)
|
|
117
|
+
expect(result2.bytesRead).toBe(1)
|
|
118
|
+
|
|
119
|
+
// Test case 3: Value -24
|
|
120
|
+
const result3 = parseInteger('37')
|
|
121
|
+
expect(result3.value).toBe(-24)
|
|
122
|
+
expect(result3.bytesRead).toBe(1)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should parse 1-byte negative integers (-25 to -256)', () => {
|
|
126
|
+
const { parseInteger } = useCborInteger()
|
|
127
|
+
|
|
128
|
+
// Test case 1: Value -100 (RFC 8949 Appendix A)
|
|
129
|
+
const result1 = parseInteger('3863')
|
|
130
|
+
expect(result1.value).toBe(-100)
|
|
131
|
+
expect(result1.bytesRead).toBe(2)
|
|
132
|
+
|
|
133
|
+
// Test case 2: Value -25
|
|
134
|
+
const result2 = parseInteger('3818')
|
|
135
|
+
expect(result2.value).toBe(-25)
|
|
136
|
+
expect(result2.bytesRead).toBe(2)
|
|
137
|
+
|
|
138
|
+
// Test case 3: Value -256
|
|
139
|
+
const result3 = parseInteger('38ff')
|
|
140
|
+
expect(result3.value).toBe(-256)
|
|
141
|
+
expect(result3.bytesRead).toBe(2)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should parse 2-byte negative integers', () => {
|
|
145
|
+
const { parseInteger } = useCborInteger()
|
|
146
|
+
|
|
147
|
+
// Test case 1: Value -1000 (RFC 8949 Appendix A)
|
|
148
|
+
const result1 = parseInteger('3903e7')
|
|
149
|
+
expect(result1.value).toBe(-1000)
|
|
150
|
+
expect(result1.bytesRead).toBe(3)
|
|
151
|
+
|
|
152
|
+
// Test case 2: Value -257
|
|
153
|
+
const result2 = parseInteger('390100')
|
|
154
|
+
expect(result2.value).toBe(-257)
|
|
155
|
+
expect(result2.bytesRead).toBe(3)
|
|
156
|
+
|
|
157
|
+
// Test case 3: Value -65536
|
|
158
|
+
const result3 = parseInteger('39ffff')
|
|
159
|
+
expect(result3.value).toBe(-65536)
|
|
160
|
+
expect(result3.bytesRead).toBe(3)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should parse large negative integers with BigInt', () => {
|
|
164
|
+
const { parseInteger } = useCborInteger()
|
|
165
|
+
|
|
166
|
+
// Test case 1: Within safe integer range (return number)
|
|
167
|
+
const result1 = parseInteger('3b0000000000000063') // -100 in 8 bytes
|
|
168
|
+
expect(result1.value).toBe(-100)
|
|
169
|
+
expect(typeof result1.value).toBe('number')
|
|
170
|
+
|
|
171
|
+
// Test case 2: Outside safe integer range (return BigInt)
|
|
172
|
+
const result2 = parseInteger('3b001fffffffffffff') // Large negative
|
|
173
|
+
expect(typeof result2.value).toBe('bigint')
|
|
174
|
+
expect(result2.value).toBe(-9007199254740992n)
|
|
175
|
+
|
|
176
|
+
// Test case 3: Maximum negative value
|
|
177
|
+
const result3 = parseInteger('3bffffffffffffffff')
|
|
178
|
+
expect(result3.value).toBe(-18446744073709551616n)
|
|
179
|
+
expect(typeof result3.value).toBe('bigint')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('Real-world Cardano examples', () => {
|
|
184
|
+
it('should parse Cardano lovelace amounts', () => {
|
|
185
|
+
const { parseInteger } = useCborInteger()
|
|
186
|
+
|
|
187
|
+
// 1 ADA = 1,000,000 lovelace
|
|
188
|
+
const oneAda = parseInteger('1a000f4240')
|
|
189
|
+
expect(oneAda.value).toBe(1000000)
|
|
190
|
+
|
|
191
|
+
// 100 ADA = 100,000,000 lovelace
|
|
192
|
+
const hundredAda = parseInteger('1a05f5e100')
|
|
193
|
+
expect(hundredAda.value).toBe(100000000)
|
|
194
|
+
|
|
195
|
+
// 1000 ADA = 1,000,000,000 lovelace
|
|
196
|
+
const thousandAda = parseInteger('1a3b9aca00')
|
|
197
|
+
expect(thousandAda.value).toBe(1000000000)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CBOR Map Duplicate Keys Detection Tests
|
|
3
|
+
*
|
|
4
|
+
* RFC 8949 Section 5.6:
|
|
5
|
+
* "Duplicate keys in maps SHOULD be rejected in applications requiring
|
|
6
|
+
* deterministic encoding"
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL FOR CARDANO TRANSACTION SECURITY:
|
|
9
|
+
* - Prevents attacker from creating ambiguous transactions
|
|
10
|
+
* - Ensures all decoders see the same data
|
|
11
|
+
* - Required for safe signature verification
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { useCborParser } from '../composables/useCborParser'
|
|
16
|
+
import { useCborCollection } from '../composables/useCborCollection'
|
|
17
|
+
|
|
18
|
+
describe('CBOR Map Duplicate Key Detection', () => {
|
|
19
|
+
describe('String Keys - Duplicate Detection', () => {
|
|
20
|
+
it('should reject map with duplicate string keys when rejectDuplicateKeys is true', () => {
|
|
21
|
+
const { parseMap } = useCborCollection()
|
|
22
|
+
|
|
23
|
+
// Map: {"a": 1, "b": 2, "a": 3} - duplicate "a"
|
|
24
|
+
// a2 (map of 3) + 6161 ("a") + 01 + 6162 ("b") + 02 + 6161 ("a") + 03
|
|
25
|
+
const duplicateMap = 'a3616101616202616103'
|
|
26
|
+
|
|
27
|
+
expect(() => parseMap(duplicateMap, { dupMapKeyMode: 'reject' }))
|
|
28
|
+
.toThrow(/duplicate.*key/i)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should reject map with multiple duplicate string keys', () => {
|
|
32
|
+
const { parseMap } = useCborCollection()
|
|
33
|
+
|
|
34
|
+
// Map: {"x": 1, "x": 2, "x": 3} - all keys duplicate
|
|
35
|
+
const allDuplicates = 'a3617801617802617803'
|
|
36
|
+
|
|
37
|
+
expect(() => parseMap(allDuplicates, { dupMapKeyMode: 'reject' }))
|
|
38
|
+
.toThrow(/duplicate.*key/i)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should allow duplicate keys when rejectDuplicateKeys is false', () => {
|
|
42
|
+
const { parseMap } = useCborCollection()
|
|
43
|
+
|
|
44
|
+
// Map: {"a": 1, "a": 2} - duplicates allowed in lenient mode
|
|
45
|
+
const duplicateMap = 'a2616101616102'
|
|
46
|
+
|
|
47
|
+
const result = parseMap(duplicateMap, { dupMapKeyMode: 'allow' })
|
|
48
|
+
|
|
49
|
+
// Last value wins in JavaScript objects
|
|
50
|
+
expect(result.value).toEqual(new Map([['a', 2]]))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should reject map with duplicate keys separated by other keys', () => {
|
|
54
|
+
const { parseMap } = useCborCollection()
|
|
55
|
+
|
|
56
|
+
// Map: {"a": 1, "b": 2, "c": 3, "a": 4} - "a" appears twice
|
|
57
|
+
const scattered = 'a4616101616202616303616104'
|
|
58
|
+
|
|
59
|
+
expect(() => parseMap(scattered, { dupMapKeyMode: 'reject' }))
|
|
60
|
+
.toThrow(/duplicate.*key/i)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should handle case-sensitive duplicate detection', () => {
|
|
64
|
+
const { parseMap } = useCborCollection()
|
|
65
|
+
|
|
66
|
+
// Map: {"a": 1, "A": 2} - different keys (case-sensitive)
|
|
67
|
+
const caseDifferent = 'a2616101614102'
|
|
68
|
+
|
|
69
|
+
const result = parseMap(caseDifferent, { dupMapKeyMode: 'reject' })
|
|
70
|
+
expect(result.value).toEqual(new Map([['a', 1], ['A', 2]]))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should reject exact duplicate strings', () => {
|
|
74
|
+
const { parseMap } = useCborCollection()
|
|
75
|
+
|
|
76
|
+
// Map: {"hello": 1, "world": 2, "hello": 3}
|
|
77
|
+
const duplicateHello = 'a36568656c6c6f0165776f726c64026568656c6c6f03'
|
|
78
|
+
|
|
79
|
+
expect(() => parseMap(duplicateHello, { dupMapKeyMode: 'reject' }))
|
|
80
|
+
.toThrow(/duplicate.*key/i)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('Integer Keys - Duplicate Detection', () => {
|
|
85
|
+
it('should reject map with duplicate positive integer keys', () => {
|
|
86
|
+
const { parseMap } = useCborCollection()
|
|
87
|
+
|
|
88
|
+
// Map: {1: 10, 2: 20, 1: 30} - duplicate key 1
|
|
89
|
+
const duplicateInt = 'a3010a020b010c'
|
|
90
|
+
|
|
91
|
+
expect(() => parseMap(duplicateInt, { dupMapKeyMode: 'reject' }))
|
|
92
|
+
.toThrow(/duplicate.*key/i)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should reject map with duplicate negative integer keys', () => {
|
|
96
|
+
const { parseMap } = useCborCollection()
|
|
97
|
+
|
|
98
|
+
// Map: {-1: 10, -2: 20, -1: 30} - duplicate key -1
|
|
99
|
+
const duplicateNeg = 'a320 0a210b200c'
|
|
100
|
+
|
|
101
|
+
expect(() => parseMap(duplicateNeg, { dupMapKeyMode: 'reject' }))
|
|
102
|
+
.toThrow(/duplicate.*key/i)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should reject map with duplicate zero keys', () => {
|
|
106
|
+
const { parseMap } = useCborCollection()
|
|
107
|
+
|
|
108
|
+
// Map: {0: 1, 0: 2}
|
|
109
|
+
const duplicateZero = 'a200010002'
|
|
110
|
+
|
|
111
|
+
expect(() => parseMap(duplicateZero, { dupMapKeyMode: 'reject' }))
|
|
112
|
+
.toThrow(/duplicate.*key/i)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should allow different integer keys', () => {
|
|
116
|
+
const { parseMap } = useCborCollection()
|
|
117
|
+
|
|
118
|
+
// Map: {1: 10, 2: 11, 3: 12} - all unique
|
|
119
|
+
// a3 (map of 3) + 01 (key:1) + 0a (val:10) + 02 (key:2) + 0b (val:11) + 03 (key:3) + 0c (val:12)
|
|
120
|
+
const uniqueInts = 'a3010a020b030c'
|
|
121
|
+
|
|
122
|
+
const result = parseMap(uniqueInts, { dupMapKeyMode: 'reject' })
|
|
123
|
+
expect(result.value).toEqual(new Map([[1, 10], [2, 11], [3, 12]]))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should treat 1 and -1 as different keys', () => {
|
|
127
|
+
const { parseMap } = useCborCollection()
|
|
128
|
+
|
|
129
|
+
// Map: {1: 10, -1: 11} - different keys
|
|
130
|
+
// a2 (map of 2) + 01 (key:1) + 0a (val:10) + 20 (key:-1) + 0b (val:11)
|
|
131
|
+
const posAndNeg = 'a2010a200b'
|
|
132
|
+
|
|
133
|
+
const result = parseMap(posAndNeg, { dupMapKeyMode: 'reject' })
|
|
134
|
+
expect(result.value).toEqual(new Map([[1, 10], [-1, 11]]))
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('Byte String Keys - Duplicate Detection', () => {
|
|
139
|
+
it('should reject map with duplicate byte string keys', () => {
|
|
140
|
+
const { parseMap } = useCborCollection()
|
|
141
|
+
|
|
142
|
+
// Map: {h'0102': 1, h'0304': 2, h'0102': 3} - duplicate h'0102'
|
|
143
|
+
const duplicateBytes = 'a34201020142030402420102 03'
|
|
144
|
+
|
|
145
|
+
expect(() => parseMap(duplicateBytes, { dupMapKeyMode: 'reject' }))
|
|
146
|
+
.toThrow(/duplicate.*key/i)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should allow different byte strings as keys', () => {
|
|
150
|
+
const { parseMap } = useCborCollection()
|
|
151
|
+
|
|
152
|
+
// Map: {h'01': 1, h'02': 2, h'03': 3}
|
|
153
|
+
const uniqueBytes = 'a341010141020241 0303'
|
|
154
|
+
|
|
155
|
+
const result = parseMap(uniqueBytes, { dupMapKeyMode: 'reject' })
|
|
156
|
+
// Byte strings as keys become comma-separated strings in JS
|
|
157
|
+
expect(result.value.size).toBe(3)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should reject duplicate empty byte strings', () => {
|
|
161
|
+
const { parseMap } = useCborCollection()
|
|
162
|
+
|
|
163
|
+
// Map: {h'': 1, h'': 2} - duplicate empty byte string
|
|
164
|
+
const duplicateEmpty = 'a24001 4002'
|
|
165
|
+
|
|
166
|
+
expect(() => parseMap(duplicateEmpty, { dupMapKeyMode: 'reject' }))
|
|
167
|
+
.toThrow(/duplicate.*key/i)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('Mixed Type Keys - Duplicate Detection', () => {
|
|
172
|
+
it('should reject keys that collide in JavaScript object model', () => {
|
|
173
|
+
const { parseMap } = useCborCollection()
|
|
174
|
+
|
|
175
|
+
// Map: {1: 10, "1": 20} - different types in CBOR (int vs string)
|
|
176
|
+
// a2 (map of 2) + 01 (key:1 integer) + 0a (val:10) + 6131 (key:"1" string) + 14 (val:20)
|
|
177
|
+
// SECURITY: Both convert to string "1" in JavaScript, causing collision
|
|
178
|
+
// This is correctly rejected to prevent unexpected behavior in Cardano applications
|
|
179
|
+
const mixedTypes = 'a2010a613114'
|
|
180
|
+
|
|
181
|
+
expect(() => parseMap(mixedTypes, { dupMapKeyMode: 'reject' }))
|
|
182
|
+
.toThrow(/duplicate.*key/i)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should detect duplicates when both are strings', () => {
|
|
186
|
+
const { parseMap } = useCborCollection()
|
|
187
|
+
|
|
188
|
+
// Map: {"1": 10, "1": 20} - both strings, duplicate
|
|
189
|
+
const bothStrings = 'a26131 0a6131 0b'
|
|
190
|
+
|
|
191
|
+
expect(() => parseMap(bothStrings, { dupMapKeyMode: 'reject' }))
|
|
192
|
+
.toThrow(/duplicate.*key/i)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('Nested Maps - Duplicate Detection', () => {
|
|
197
|
+
it('should detect duplicates in outer map', () => {
|
|
198
|
+
const { parse } = useCborParser()
|
|
199
|
+
|
|
200
|
+
// Outer map: {"a": {...}, "a": {...}} - duplicate outer key
|
|
201
|
+
const duplicateOuter = 'a2616 1a1616201616 1a161630 2'
|
|
202
|
+
|
|
203
|
+
expect(() => parse(duplicateOuter, { dupMapKeyMode: 'reject' }))
|
|
204
|
+
.toThrow(/duplicate.*key/i)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should detect duplicates in inner map', () => {
|
|
208
|
+
const { parse } = useCborParser()
|
|
209
|
+
|
|
210
|
+
// Outer unique, inner has duplicate: {"a": {"x": 1, "x": 2}}
|
|
211
|
+
const duplicateInner = 'a16161a2617801617802'
|
|
212
|
+
|
|
213
|
+
expect(() => parse(duplicateInner, { dupMapKeyMode: 'reject' }))
|
|
214
|
+
.toThrow(/duplicate.*key/i)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should allow same keys in different nested maps', () => {
|
|
218
|
+
const { parse } = useCborParser()
|
|
219
|
+
|
|
220
|
+
// {"a": {"x": 1}, "b": {"x": 2}} - "x" in both inner maps (allowed)
|
|
221
|
+
const sameKeyDifferentMaps = 'a2616 1a1617801616 2a16178 02'
|
|
222
|
+
|
|
223
|
+
const result = parse(sameKeyDifferentMaps, { dupMapKeyMode: 'reject' })
|
|
224
|
+
expect(result.value).toEqual(new Map([['a', new Map([['x', 1]])], ['b', new Map([['x', 2]])]]))
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('Cardano Security Scenarios', () => {
|
|
229
|
+
it('should prevent transaction amount duplication attack', () => {
|
|
230
|
+
const { parse } = useCborParser()
|
|
231
|
+
|
|
232
|
+
// Malicious Cardano transaction with duplicate "amount" field
|
|
233
|
+
// Attacker tries: amount: 1000000, amount: 100000000
|
|
234
|
+
// Wallet sees 100M, node sees 1M → SECURITY BREACH
|
|
235
|
+
const maliciousTx = 'a266616d6f756e741a000f424066616d6f756e741a05f5e100'
|
|
236
|
+
|
|
237
|
+
expect(() => parse(maliciousTx, {
|
|
238
|
+
dupMapKeyMode: 'reject',
|
|
239
|
+
strict: true
|
|
240
|
+
})).toThrow(/duplicate/i)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should prevent UTXO address duplication attack', () => {
|
|
244
|
+
const { parse } = useCborParser()
|
|
245
|
+
|
|
246
|
+
// Malicious UTXO with duplicate "address" field
|
|
247
|
+
// a2 (map of 2) + 6761646472657373 ("address") + 01 + 6761646472657373 ("address") + 02
|
|
248
|
+
const maliciousUTXO = 'a2676164647265737301676164647265737302'
|
|
249
|
+
|
|
250
|
+
expect(() => parse(maliciousUTXO, {
|
|
251
|
+
dupMapKeyMode: 'reject'
|
|
252
|
+
})).toThrow(/duplicate/i)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should prevent metadata duplication attack', () => {
|
|
256
|
+
const { parse } = useCborParser()
|
|
257
|
+
|
|
258
|
+
// Transaction metadata with duplicate key
|
|
259
|
+
const maliciousMetadata = 'a2616101616102'
|
|
260
|
+
|
|
261
|
+
expect(() => parse(maliciousMetadata, {
|
|
262
|
+
dupMapKeyMode: 'reject'
|
|
263
|
+
})).toThrow(/duplicate/i)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should ensure strict mode enables duplicate key rejection', () => {
|
|
267
|
+
const { parse } = useCborParser()
|
|
268
|
+
|
|
269
|
+
// strict: true should automatically enable rejectDuplicateKeys
|
|
270
|
+
const duplicateMap = 'a2616101616102'
|
|
271
|
+
|
|
272
|
+
expect(() => parse(duplicateMap, { strict: true }))
|
|
273
|
+
.toThrow(/duplicate/i)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('Indefinite-Length Maps - Duplicate Detection', () => {
|
|
278
|
+
it('should detect duplicates in indefinite-length maps', () => {
|
|
279
|
+
const { parseMap } = useCborCollection()
|
|
280
|
+
|
|
281
|
+
// Indefinite map: {_ "a": 1, "b": 2, "a": 3}
|
|
282
|
+
// Note: This test may fail if indefinite is rejected in strict mode
|
|
283
|
+
const indefiniteDuplicates = 'bf616101616 202616103ff'
|
|
284
|
+
|
|
285
|
+
// In non-strict mode but with duplicate rejection
|
|
286
|
+
expect(() => parseMap(indefiniteDuplicates, {
|
|
287
|
+
dupMapKeyMode: 'reject',
|
|
288
|
+
allowIndefinite: true
|
|
289
|
+
})).toThrow(/duplicate/i)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('Large Maps - Performance and Correctness', () => {
|
|
294
|
+
it('should detect duplicates in large maps efficiently', () => {
|
|
295
|
+
const { parseMap } = useCborCollection()
|
|
296
|
+
|
|
297
|
+
// Create map with 50 unique keys + 1 duplicate at the end (51 total)
|
|
298
|
+
const entries: string[] = []
|
|
299
|
+
for (let i = 0; i < 50; i++) {
|
|
300
|
+
// Integer keys 24-73 (using 1-byte encoding AI=24)
|
|
301
|
+
const key = `18${(24 + i).toString(16).padStart(2, '0')}`
|
|
302
|
+
const value = `18${(100 + i).toString(16).padStart(2, '0')}`
|
|
303
|
+
entries.push(key + value)
|
|
304
|
+
}
|
|
305
|
+
// Add duplicate of first key (24)
|
|
306
|
+
entries.push('1818' + '1880') // Duplicate key 24, value 128
|
|
307
|
+
|
|
308
|
+
// b8 = Major type 5 (map), AI=24, followed by 1 byte for count
|
|
309
|
+
const largeDuplicateMap = `b833${entries.join('')}` // 51 entries = 0x33
|
|
310
|
+
|
|
311
|
+
expect(() => parseMap(largeDuplicateMap, {
|
|
312
|
+
dupMapKeyMode: 'reject'
|
|
313
|
+
})).toThrow(/duplicate/i)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should accept large maps with all unique keys', () => {
|
|
317
|
+
const { parseMap } = useCborCollection()
|
|
318
|
+
|
|
319
|
+
// Create map with 30 unique keys (all unique)
|
|
320
|
+
const entries: string[] = []
|
|
321
|
+
for (let i = 0; i < 30; i++) {
|
|
322
|
+
// Integer keys 20-49 (using 1-byte encoding AI=24)
|
|
323
|
+
const key = `18${(20 + i).toString(16).padStart(2, '0')}`
|
|
324
|
+
const value = `18${(100 + i).toString(16).padStart(2, '0')}`
|
|
325
|
+
entries.push(key + value)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// b8 = Major type 5 (map), AI=24, followed by 1 byte for count
|
|
329
|
+
const largeUniqueMap = `b81e${entries.join('')}` // 30 entries = 0x1e
|
|
330
|
+
|
|
331
|
+
const result = parseMap(largeUniqueMap, {
|
|
332
|
+
dupMapKeyMode: 'reject',
|
|
333
|
+
limits: { maxMapSize: 100 }
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
expect(result.value.size).toBe(30)
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('Edge Cases', () => {
|
|
341
|
+
it('should handle empty map with duplicate detection enabled', () => {
|
|
342
|
+
const { parseMap } = useCborCollection()
|
|
343
|
+
|
|
344
|
+
const emptyMap = 'a0'
|
|
345
|
+
const result = parseMap(emptyMap, { dupMapKeyMode: 'reject' })
|
|
346
|
+
|
|
347
|
+
expect(result.value).toEqual(new Map())
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should handle single-entry map with duplicate detection', () => {
|
|
351
|
+
const { parseMap } = useCborCollection()
|
|
352
|
+
|
|
353
|
+
const singleEntry = 'a1616101'
|
|
354
|
+
const result = parseMap(singleEntry, { dupMapKeyMode: 'reject' })
|
|
355
|
+
|
|
356
|
+
expect(result.value).toEqual(new Map([['a', 1]]))
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('should detect duplicates with boolean and null keys', () => {
|
|
360
|
+
const { parseMap } = useCborCollection()
|
|
361
|
+
|
|
362
|
+
// Map: {true: 1, false: 2, true: 3} - duplicate true
|
|
363
|
+
const duplicateBool = 'a3f501f402f503'
|
|
364
|
+
|
|
365
|
+
expect(() => parseMap(duplicateBool, { dupMapKeyMode: 'reject' }))
|
|
366
|
+
.toThrow(/duplicate/i)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should detect duplicates with null keys', () => {
|
|
370
|
+
const { parseMap } = useCborCollection()
|
|
371
|
+
|
|
372
|
+
// Map: {null: 1, null: 2} - duplicate null
|
|
373
|
+
const duplicateNull = 'a2f601f602'
|
|
374
|
+
|
|
375
|
+
expect(() => parseMap(duplicateNull, { dupMapKeyMode: 'reject' }))
|
|
376
|
+
.toThrow(/duplicate/i)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
describe('Default Behavior', () => {
|
|
381
|
+
it('should NOT reject duplicates by default (lenient mode)', () => {
|
|
382
|
+
const { parseMap } = useCborCollection()
|
|
383
|
+
|
|
384
|
+
// Map: {"a": 1, "a": 2} - duplicates allowed by default
|
|
385
|
+
const duplicateMap = 'a2616101616102'
|
|
386
|
+
|
|
387
|
+
// No options passed - should use default (lenient)
|
|
388
|
+
const result = parseMap(duplicateMap)
|
|
389
|
+
|
|
390
|
+
// Last value wins
|
|
391
|
+
expect(result.value).toEqual(new Map([['a', 2]]))
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('should reject duplicates when explicitly enabled', () => {
|
|
395
|
+
const { parseMap } = useCborCollection()
|
|
396
|
+
|
|
397
|
+
const duplicateMap = 'a2616101616102'
|
|
398
|
+
|
|
399
|
+
expect(() => parseMap(duplicateMap, { dupMapKeyMode: 'reject' }))
|
|
400
|
+
.toThrow(/duplicate/i)
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
})
|