@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +674 -0
  3. package/README.md +345 -0
  4. package/dist/chunk-2FUTHZQQ.cjs +755 -0
  5. package/dist/chunk-2FUTHZQQ.cjs.map +1 -0
  6. package/dist/chunk-2HBCILJS.cjs +2034 -0
  7. package/dist/chunk-2HBCILJS.cjs.map +1 -0
  8. package/dist/chunk-7CFYWHS6.js +742 -0
  9. package/dist/chunk-7CFYWHS6.js.map +1 -0
  10. package/dist/chunk-PD72MVTX.cjs +160 -0
  11. package/dist/chunk-PD72MVTX.cjs.map +1 -0
  12. package/dist/chunk-ZDZ2B5PE.js +149 -0
  13. package/dist/chunk-ZDZ2B5PE.js.map +1 -0
  14. package/dist/chunk-ZRPJUEIZ.js +2020 -0
  15. package/dist/chunk-ZRPJUEIZ.js.map +1 -0
  16. package/dist/encoder/index.cjs +57 -0
  17. package/dist/encoder/index.cjs.map +1 -0
  18. package/dist/encoder/index.d.cts +72 -0
  19. package/dist/encoder/index.d.ts +72 -0
  20. package/dist/encoder/index.js +4 -0
  21. package/dist/encoder/index.js.map +1 -0
  22. package/dist/index.cjs +606 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.cts +494 -0
  25. package/dist/index.d.ts +494 -0
  26. package/dist/index.js +523 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/metafile-cjs.json +1 -0
  29. package/dist/metafile-esm.json +1 -0
  30. package/dist/parser/index.cjs +85 -0
  31. package/dist/parser/index.cjs.map +1 -0
  32. package/dist/parser/index.d.cts +72 -0
  33. package/dist/parser/index.d.ts +72 -0
  34. package/dist/parser/index.js +4 -0
  35. package/dist/parser/index.js.map +1 -0
  36. package/dist/types-DvNlfbKB.d.cts +301 -0
  37. package/dist/types-DvNlfbKB.d.ts +301 -0
  38. package/dist/useCborSimpleEncoder-ButVU988.d.cts +268 -0
  39. package/dist/useCborSimpleEncoder-TVxzNJ_9.d.ts +268 -0
  40. package/dist/useCborTag-B_iaShG6.d.ts +142 -0
  41. package/dist/useCborTag-BfTIV8HM.d.cts +142 -0
  42. package/package.json +102 -0
  43. package/src/__tests__/public-api.test.ts +326 -0
  44. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +331 -0
  45. package/src/encoder/__tests__/cbor-integer-encoder.test.ts +283 -0
  46. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +224 -0
  47. package/src/encoder/__tests__/cbor-string-encoder.test.ts +345 -0
  48. package/src/encoder/__tests__/cbor-tag-encoder.test.ts +565 -0
  49. package/src/encoder/composables/#useCborTagEncoder.ts# +158 -0
  50. package/src/encoder/composables/useCborCollectionEncoder.ts +424 -0
  51. package/src/encoder/composables/useCborEncoder.ts +203 -0
  52. package/src/encoder/composables/useCborIntegerEncoder.ts +188 -0
  53. package/src/encoder/composables/useCborSimpleEncoder.ts +266 -0
  54. package/src/encoder/composables/useCborStringEncoder.ts +266 -0
  55. package/src/encoder/composables/useCborTagEncoder.ts +158 -0
  56. package/src/encoder/index.ts +35 -0
  57. package/src/encoder/types.ts +88 -0
  58. package/src/encoder/utils.ts +80 -0
  59. package/src/index.ts +434 -0
  60. package/src/parser/__tests__/ast-tree-structure.test.ts +311 -0
  61. package/src/parser/__tests__/cbor-collection-errors.test.ts +296 -0
  62. package/src/parser/__tests__/cbor-collection.test.ts +369 -0
  63. package/src/parser/__tests__/cbor-deterministic-encoding.test.ts +432 -0
  64. package/src/parser/__tests__/cbor-diagnostic.test.ts +333 -0
  65. package/src/parser/__tests__/cbor-duplicate-keys.test.ts +235 -0
  66. package/src/parser/__tests__/cbor-float-errors.test.ts +222 -0
  67. package/src/parser/__tests__/cbor-float.test.ts +502 -0
  68. package/src/parser/__tests__/cbor-integer-errors.test.ts +139 -0
  69. package/src/parser/__tests__/cbor-integer.test.ts +200 -0
  70. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +403 -0
  71. package/src/parser/__tests__/cbor-parser-errors.test.ts +126 -0
  72. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +503 -0
  73. package/src/parser/__tests__/cbor-sequences.test.ts +150 -0
  74. package/src/parser/__tests__/cbor-source-map.test.ts +321 -0
  75. package/src/parser/__tests__/cbor-standard-tags.test.ts +340 -0
  76. package/src/parser/__tests__/cbor-string-errors.test.ts +227 -0
  77. package/src/parser/__tests__/cbor-string.test.ts +224 -0
  78. package/src/parser/__tests__/cbor-tag-advanced.test.ts +500 -0
  79. package/src/parser/__tests__/cbor-tag-errors.test.ts +447 -0
  80. package/src/parser/__tests__/cbor-tag-source-map.test.ts +360 -0
  81. package/src/parser/__tests__/cbor-tag.test.ts +684 -0
  82. package/src/parser/__tests__/extreme-edge-cases.test.ts +146 -0
  83. package/src/parser/__tests__/pathBuilder.test.ts +256 -0
  84. package/src/parser/__tests__/rfc-test-vectors.test.ts +607 -0
  85. package/src/parser/__tests__/security-limits.test.ts +248 -0
  86. package/src/parser/__tests__/utils-errors.test.ts +127 -0
  87. package/src/parser/composables/useCborCollection.ts +509 -0
  88. package/src/parser/composables/useCborDiagnostic.ts +381 -0
  89. package/src/parser/composables/useCborFloat.ts +256 -0
  90. package/src/parser/composables/useCborInteger.ts +114 -0
  91. package/src/parser/composables/useCborParser.ts +951 -0
  92. package/src/parser/composables/useCborString.ts +330 -0
  93. package/src/parser/composables/useCborStringTypes.ts +129 -0
  94. package/src/parser/composables/useCborTag.ts +739 -0
  95. package/src/parser/index.ts +56 -0
  96. package/src/parser/types.ts +371 -0
  97. package/src/parser/utils/pathBuilder.ts +259 -0
  98. package/src/parser/utils.ts +398 -0
  99. package/src/utils/__tests__/logger.test.ts +186 -0
  100. 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
+ })