@marcuspuchalla/nachos 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/{chunk-PTWN7K3Y.cjs → chunk-3Z45RBZP.cjs} +469 -244
  3. package/dist/chunk-3Z45RBZP.cjs.map +1 -0
  4. package/dist/{chunk-2MTLSQ7E.js → chunk-EDXZTSIA.js} +224 -166
  5. package/dist/chunk-EDXZTSIA.js.map +1 -0
  6. package/dist/{chunk-R62CQQNI.cjs → chunk-HMUA5KLG.cjs} +239 -181
  7. package/dist/chunk-HMUA5KLG.cjs.map +1 -0
  8. package/dist/{chunk-ZDZ2B5PE.js → chunk-JESIF5IF.js} +7 -3
  9. package/dist/chunk-JESIF5IF.js.map +1 -0
  10. package/dist/{chunk-5A5T56JB.js → chunk-LWNWC2O7.js} +442 -217
  11. package/dist/chunk-LWNWC2O7.js.map +1 -0
  12. package/dist/{chunk-PD72MVTX.cjs → chunk-P6A2OOIY.cjs} +7 -3
  13. package/dist/chunk-P6A2OOIY.cjs.map +1 -0
  14. package/dist/encoder/index.cjs +14 -14
  15. package/dist/encoder/index.d.cts +5 -4
  16. package/dist/encoder/index.d.ts +5 -4
  17. package/dist/encoder/index.js +2 -2
  18. package/dist/index.cjs +58 -39
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +40 -21
  21. package/dist/index.d.ts +40 -21
  22. package/dist/index.js +37 -17
  23. package/dist/index.js.map +1 -1
  24. package/dist/metafile-cjs.json +1 -1
  25. package/dist/metafile-esm.json +1 -1
  26. package/dist/parser/index.cjs +21 -21
  27. package/dist/parser/index.d.cts +4 -2
  28. package/dist/parser/index.d.ts +4 -2
  29. package/dist/parser/index.js +2 -2
  30. package/dist/{types-DvNlfbKB.d.cts → types-eG2qalpr.d.cts} +27 -1
  31. package/dist/{types-DvNlfbKB.d.ts → types-eG2qalpr.d.ts} +27 -1
  32. package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -3
  33. package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -3
  34. package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-D4d7xG3-.d.cts} +9 -4
  35. package/dist/{useCborTag-Cs1CZuXZ.d.cts → useCborTag-TYst1KR6.d.ts} +9 -4
  36. package/package.json +1 -1
  37. package/src/__tests__/audit-fixes.test.ts +141 -0
  38. package/src/__tests__/public-api.test.ts +153 -0
  39. package/src/__tests__/roundtrip.test.ts +5 -6
  40. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
  41. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
  42. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  43. package/src/encoder/composables/useCborCollectionEncoder.ts +30 -26
  44. package/src/encoder/composables/useCborEncoder.ts +40 -0
  45. package/src/encoder/composables/useCborSimpleEncoder.ts +40 -9
  46. package/src/encoder/types.ts +9 -4
  47. package/src/encoder/utils.ts +33 -1
  48. package/src/index.ts +39 -20
  49. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  50. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  51. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
  52. package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
  53. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  54. package/src/parser/__tests__/utils-errors.test.ts +11 -3
  55. package/src/parser/composables/useCborCollection.ts +51 -45
  56. package/src/parser/composables/useCborDiagnostic.ts +28 -0
  57. package/src/parser/composables/useCborFloat.ts +2 -1
  58. package/src/parser/composables/useCborInteger.ts +24 -10
  59. package/src/parser/composables/useCborParser.ts +448 -208
  60. package/src/parser/composables/useCborTag.ts +53 -38
  61. package/src/parser/types.ts +32 -1
  62. package/src/parser/utils.ts +52 -0
  63. package/dist/chunk-2MTLSQ7E.js.map +0 -1
  64. package/dist/chunk-5A5T56JB.js.map +0 -1
  65. package/dist/chunk-PD72MVTX.cjs.map +0 -1
  66. package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
  67. package/dist/chunk-R62CQQNI.cjs.map +0 -1
  68. package/dist/chunk-ZDZ2B5PE.js.map +0 -1
@@ -28,7 +28,8 @@ export function concatenateUint8Arrays(arrays: Uint8Array[]): Uint8Array {
28
28
  }
29
29
 
30
30
  /**
31
- * Compare two Uint8Arrays bytewise (for canonical map sorting)
31
+ * Compare two Uint8Arrays length-first (RFC 7049 §3.9 / Cardano CIP-21 ordering).
32
+ * Shorter keys sort first; equal-length keys are compared bytewise.
32
33
  */
33
34
  export function compareBytes(a: Uint8Array, b: Uint8Array): number {
34
35
  // First, compare lengths
@@ -51,6 +52,37 @@ export function compareBytes(a: Uint8Array, b: Uint8Array): number {
51
52
  return 0
52
53
  }
53
54
 
55
+ /**
56
+ * Compare two Uint8Arrays in pure bytewise lexicographic order
57
+ * (RFC 8949 §4.2.1 core deterministic encoding). If one is a prefix of the
58
+ * other, the shorter sorts first.
59
+ */
60
+ export function compareBytesLexicographic(a: Uint8Array, b: Uint8Array): number {
61
+ const min = Math.min(a.length, b.length)
62
+ for (let i = 0; i < min; i++) {
63
+ const byteA = a[i]!
64
+ const byteB = b[i]!
65
+ if (byteA !== byteB) {
66
+ return byteA - byteB
67
+ }
68
+ }
69
+ return a.length - b.length
70
+ }
71
+
72
+ /**
73
+ * Compare two encoded map keys according to the requested ordering.
74
+ *
75
+ * @param order - 'length-first' (CIP-21 / RFC 7049 §3.9, default) or
76
+ * 'bytewise' (RFC 8949 §4.2.1 core deterministic)
77
+ */
78
+ export function compareMapKeys(
79
+ a: Uint8Array,
80
+ b: Uint8Array,
81
+ order: 'length-first' | 'bytewise' = 'length-first'
82
+ ): number {
83
+ return order === 'bytewise' ? compareBytesLexicographic(a, b) : compareBytes(a, b)
84
+ }
85
+
54
86
  /**
55
87
  * Write unsigned integer to bytes (big-endian)
56
88
  */
package/src/index.ts CHANGED
@@ -96,9 +96,14 @@ import type { ParseResult, ParseResultWithMap, ParseOptions } from './parser/typ
96
96
  import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/types'
97
97
 
98
98
  /**
99
- * Decode CBOR hex string to JavaScript value
99
+ * Decode CBOR data to JavaScript value
100
100
  *
101
- * @param hexString - CBOR data as hex string (e.g., "1864" for integer 100)
101
+ * Accepts either a hex string or a Uint8Array of raw CBOR bytes.
102
+ * When passing a Uint8Array, the bytes are used directly without
103
+ * hex conversion, which is more efficient for binary sources
104
+ * (WebSocket, fetch, file I/O, etc.).
105
+ *
106
+ * @param input - CBOR data as hex string (e.g., "1864") or Uint8Array
102
107
  * @param options - Optional parser configuration
103
108
  * @returns Decoded value and number of bytes consumed
104
109
  *
@@ -106,9 +111,12 @@ import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/type
106
111
  *
107
112
  * @example
108
113
  * ```typescript
109
- * // Decode integer
114
+ * // Decode from hex string
110
115
  * decode('1864') // { value: 100, bytesRead: 2 }
111
116
  *
117
+ * // Decode from Uint8Array (zero-copy, no hex conversion)
118
+ * decode(new Uint8Array([0x18, 0x64])) // { value: 100, bytesRead: 2 }
119
+ *
112
120
  * // Decode string
113
121
  * decode('6449455446') // { value: "IETF", bytesRead: 5 }
114
122
  *
@@ -124,18 +132,20 @@ import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/type
124
132
  *
125
133
  * @see {@link https://datatracker.ietf.org/doc/html/rfc8949 | RFC 8949}
126
134
  */
127
- export function decode(hexString: string, options?: ParseOptions): ParseResult {
135
+ export function decode(input: string | Uint8Array, options?: ParseOptions): ParseResult {
128
136
  const { parse } = useCborParser()
129
- return parse(hexString, options)
137
+ return parse(input, options)
130
138
  }
131
139
 
132
140
  /**
133
- * Decode CBOR hex string with source map generation
141
+ * Decode CBOR data with source map generation
134
142
  *
135
143
  * Source maps provide bidirectional linking between hex bytes and decoded values,
136
144
  * enabling interactive debugging visualizations.
137
145
  *
138
- * @param hexString - CBOR data as hex string
146
+ * Accepts either a hex string or a Uint8Array of raw CBOR bytes.
147
+ *
148
+ * @param input - CBOR data as hex string or Uint8Array
139
149
  * @param options - Optional parser configuration
140
150
  * @returns Decoded value, byte count, and source map
141
151
  *
@@ -148,14 +158,13 @@ export function decode(hexString: string, options?: ParseOptions): ParseResult {
148
158
  * // { path: '.value', start: 2, end: 3, majorType: 4, type: 'Array', parent: '' }
149
159
  * // ]
150
160
  *
151
- * // Use source map for hex-to-JSON linking
152
- * const entry = sourceMap.find(e => e.path === '.value')
153
- * console.log(`Value is at bytes ${entry.start}-${entry.end}`)
161
+ * // From Uint8Array
162
+ * const { value, sourceMap } = decodeWithSourceMap(new Uint8Array([0xd8, 0x79, 0x80]))
154
163
  * ```
155
164
  */
156
- export function decodeWithSourceMap(hexString: string, options?: ParseOptions): ParseResultWithMap {
165
+ export function decodeWithSourceMap(input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap {
157
166
  const { parseWithSourceMap } = useCborParser()
158
- return parseWithSourceMap(hexString, options)
167
+ return parseWithSourceMap(input, options)
159
168
  }
160
169
 
161
170
  /**
@@ -170,6 +179,16 @@ export function decodeWithSourceMap(hexString: string, options?: ParseOptions):
170
179
  *
171
180
  * @throws {Error} If value type is unsupported or encoding fails
172
181
  *
182
+ * @remarks
183
+ * Plain objects use `Object.entries()`, so all keys become **text strings** and
184
+ * integer-like keys (`"0"`, `"1"`, …) are reordered by the JS engine's
185
+ * integer-index property rule. For integer keys or guaranteed ordering (e.g.
186
+ * Cardano transaction bodies), pass a `Map` instead of a plain object.
187
+ *
188
+ * Canonical mode (`{ canonical: true }`) sorts map keys **length-first** by
189
+ * default (Cardano CIP-21 / RFC 7049 §3.9). Pass `{ mapKeyOrder: 'bytewise' }`
190
+ * for RFC 8949 §4.2.1 core deterministic ordering.
191
+ *
173
192
  * @example
174
193
  * ```typescript
175
194
  * // Encode number
@@ -287,23 +306,23 @@ export class CborDecoder {
287
306
  }
288
307
 
289
308
  /**
290
- * Decode CBOR hex string
309
+ * Decode CBOR data
291
310
  *
292
- * @param hexString - CBOR data as hex string
311
+ * @param input - CBOR data as hex string or Uint8Array
293
312
  * @returns Decoded value and byte count
294
313
  */
295
- decode(hexString: string): ParseResult {
296
- return decode(hexString, this.options)
314
+ decode(input: string | Uint8Array): ParseResult {
315
+ return decode(input, this.options)
297
316
  }
298
317
 
299
318
  /**
300
- * Decode CBOR hex string with source map
319
+ * Decode CBOR data with source map
301
320
  *
302
- * @param hexString - CBOR data as hex string
321
+ * @param input - CBOR data as hex string or Uint8Array
303
322
  * @returns Decoded value, byte count, and source map
304
323
  */
305
- decodeWithSourceMap(hexString: string): ParseResultWithMap {
306
- return decodeWithSourceMap(hexString, this.options)
324
+ decodeWithSourceMap(input: string | Uint8Array): ParseResultWithMap {
325
+ return decodeWithSourceMap(input, this.options)
307
326
  }
308
327
  }
309
328
 
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Tests for buffer-native parsing optimization (Task 2-A)
3
+ * Validates that buffer-based parsers produce identical results to hex-string parsers
4
+ * and that O(N^2) hex conversion is eliminated.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest'
8
+ import { useCborInteger } from '../composables/useCborInteger'
9
+ import { useCborFloat } from '../composables/useCborFloat'
10
+ import { useCborTag } from '../composables/useCborTag'
11
+ import { useCborCollection } from '../composables/useCborCollection'
12
+ import { hexToBytes } from '../utils'
13
+
14
+ describe('Buffer-native parsing - parseIntegerFromBuffer', () => {
15
+ const { parseInteger, parseIntegerFromBuffer } = useCborInteger()
16
+
17
+ it('should export parseIntegerFromBuffer from useCborInteger', () => {
18
+ expect(typeof parseIntegerFromBuffer).toBe('function')
19
+ })
20
+
21
+ it('should parse unsigned integer 0 from buffer', () => {
22
+ const buffer = hexToBytes('00')
23
+ const result = parseIntegerFromBuffer(buffer, 0)
24
+ expect(result.value).toBe(0)
25
+ expect(result.bytesRead).toBe(1)
26
+ })
27
+
28
+ it('should parse unsigned integer 23 from buffer', () => {
29
+ const buffer = hexToBytes('17')
30
+ const result = parseIntegerFromBuffer(buffer, 0)
31
+ expect(result.value).toBe(23)
32
+ expect(result.bytesRead).toBe(1)
33
+ })
34
+
35
+ it('should parse unsigned integer 24 from buffer (1-byte follows)', () => {
36
+ const buffer = hexToBytes('1818')
37
+ const result = parseIntegerFromBuffer(buffer, 0)
38
+ expect(result.value).toBe(24)
39
+ expect(result.bytesRead).toBe(2)
40
+ })
41
+
42
+ it('should parse unsigned integer 100 from buffer (1-byte follows)', () => {
43
+ const buffer = hexToBytes('1864')
44
+ const result = parseIntegerFromBuffer(buffer, 0)
45
+ expect(result.value).toBe(100)
46
+ expect(result.bytesRead).toBe(2)
47
+ })
48
+
49
+ it('should parse unsigned integer 1000 from buffer (2-byte follows)', () => {
50
+ const buffer = hexToBytes('1903e8')
51
+ const result = parseIntegerFromBuffer(buffer, 0)
52
+ expect(result.value).toBe(1000)
53
+ expect(result.bytesRead).toBe(3)
54
+ })
55
+
56
+ it('should parse unsigned integer 1000000 from buffer (4-byte follows)', () => {
57
+ const buffer = hexToBytes('1a000f4240')
58
+ const result = parseIntegerFromBuffer(buffer, 0)
59
+ expect(result.value).toBe(1000000)
60
+ expect(result.bytesRead).toBe(5)
61
+ })
62
+
63
+ it('should parse large unsigned integer from buffer (8-byte follows)', () => {
64
+ const buffer = hexToBytes('1b000000e8d4a51000')
65
+ const result = parseIntegerFromBuffer(buffer, 0)
66
+ expect(result.value).toBe(1000000000000)
67
+ expect(result.bytesRead).toBe(9)
68
+ })
69
+
70
+ it('should parse negative integer -1 from buffer', () => {
71
+ const buffer = hexToBytes('20')
72
+ const result = parseIntegerFromBuffer(buffer, 0)
73
+ expect(result.value).toBe(-1)
74
+ expect(result.bytesRead).toBe(1)
75
+ })
76
+
77
+ it('should parse negative integer -100 from buffer', () => {
78
+ const buffer = hexToBytes('3863')
79
+ const result = parseIntegerFromBuffer(buffer, 0)
80
+ expect(result.value).toBe(-100)
81
+ expect(result.bytesRead).toBe(2)
82
+ })
83
+
84
+ it('should parse BigInt unsigned from buffer', () => {
85
+ // 18446744073709551615 (max uint64)
86
+ const buffer = hexToBytes('1bffffffffffffffff')
87
+ const result = parseIntegerFromBuffer(buffer, 0)
88
+ expect(result.value).toBe(18446744073709551615n)
89
+ expect(result.bytesRead).toBe(9)
90
+ })
91
+
92
+ it('should parse BigInt negative from buffer', () => {
93
+ // -18446744073709551616
94
+ const buffer = hexToBytes('3bffffffffffffffff')
95
+ const result = parseIntegerFromBuffer(buffer, 0)
96
+ expect(result.value).toBe(-18446744073709551616n)
97
+ expect(result.bytesRead).toBe(9)
98
+ })
99
+
100
+ it('should parse integer at non-zero offset', () => {
101
+ // Buffer: [0x83, 0x01, 0x02, 0x03] - array(3) then integers 1, 2, 3
102
+ // Parsing integer at offset 1 should get value 1
103
+ const buffer = hexToBytes('83010203')
104
+ const result = parseIntegerFromBuffer(buffer, 1)
105
+ expect(result.value).toBe(1)
106
+ expect(result.bytesRead).toBe(1)
107
+ })
108
+
109
+ it('should parse integer at offset with trailing data', () => {
110
+ // Buffer with integer 100 at offset 2, followed by other data
111
+ const buffer = hexToBytes('0000186400')
112
+ const result = parseIntegerFromBuffer(buffer, 2)
113
+ expect(result.value).toBe(100)
114
+ expect(result.bytesRead).toBe(2)
115
+ })
116
+
117
+ it('should produce identical results to parseInteger for all test vectors', () => {
118
+ const testVectors = [
119
+ '00', '01', '0a', '17', '1818', '1864', '18ff',
120
+ '190100', '1903e8', '19ffff',
121
+ '1a00010000', '1a000f4240', '1affffffff',
122
+ '1b0000000100000000', '1b000000e8d4a51000', '1bffffffffffffffff',
123
+ '20', '37', '3863', '38ff',
124
+ '390100', '3903e7', '39ffff',
125
+ '3a00010000', '3affffffff',
126
+ '3b0000000100000000', '3bffffffffffffffff',
127
+ ]
128
+
129
+ for (const hex of testVectors) {
130
+ const hexResult = parseInteger(hex)
131
+ const buffer = hexToBytes(hex)
132
+ const bufferResult = parseIntegerFromBuffer(buffer, 0)
133
+
134
+ expect(bufferResult.value).toEqual(hexResult.value)
135
+ expect(bufferResult.bytesRead).toEqual(hexResult.bytesRead)
136
+ }
137
+ })
138
+
139
+ it('should validate canonical encoding when options are passed', () => {
140
+ // Non-canonical: value 0 encoded with 1-byte AI
141
+ const buffer = hexToBytes('1800')
142
+ expect(() => parseIntegerFromBuffer(buffer, 0, { validateCanonical: true }))
143
+ .toThrow(/[Nn]on-canonical/)
144
+ })
145
+ })
146
+
147
+ describe('Buffer-native parsing - parseFromBuffer (float/simple)', () => {
148
+ const { parseFromBuffer } = useCborFloat()
149
+
150
+ it('should export parseFromBuffer from useCborFloat', () => {
151
+ expect(typeof parseFromBuffer).toBe('function')
152
+ })
153
+
154
+ it('should parse false from buffer', () => {
155
+ const buffer = hexToBytes('f4')
156
+ const result = parseFromBuffer(buffer, 0)
157
+ expect(result.value).toBe(false)
158
+ expect(result.bytesRead).toBe(1)
159
+ })
160
+
161
+ it('should parse true from buffer', () => {
162
+ const buffer = hexToBytes('f5')
163
+ const result = parseFromBuffer(buffer, 0)
164
+ expect(result.value).toBe(true)
165
+ expect(result.bytesRead).toBe(1)
166
+ })
167
+
168
+ it('should parse null from buffer', () => {
169
+ const buffer = hexToBytes('f6')
170
+ const result = parseFromBuffer(buffer, 0)
171
+ expect(result.value).toBe(null)
172
+ expect(result.bytesRead).toBe(1)
173
+ })
174
+
175
+ it('should parse undefined from buffer', () => {
176
+ const buffer = hexToBytes('f7')
177
+ const result = parseFromBuffer(buffer, 0)
178
+ expect(result.value).toBe(undefined)
179
+ expect(result.bytesRead).toBe(1)
180
+ })
181
+
182
+ it('should parse float16 0.0 from buffer', () => {
183
+ const buffer = hexToBytes('f90000')
184
+ const result = parseFromBuffer(buffer, 0)
185
+ expect(result.value).toBe(0.0)
186
+ expect(result.bytesRead).toBe(3)
187
+ })
188
+
189
+ it('should parse float16 1.0 from buffer', () => {
190
+ const buffer = hexToBytes('f93c00')
191
+ const result = parseFromBuffer(buffer, 0)
192
+ expect(result.value).toBe(1.0)
193
+ expect(result.bytesRead).toBe(3)
194
+ })
195
+
196
+ it('should parse float32 from buffer', () => {
197
+ const buffer = hexToBytes('fa47c35000')
198
+ const result = parseFromBuffer(buffer, 0)
199
+ expect(result.value).toBe(100000.0)
200
+ expect(result.bytesRead).toBe(5)
201
+ })
202
+
203
+ it('should parse float64 from buffer', () => {
204
+ const buffer = hexToBytes('fb3ff199999999999a')
205
+ const result = parseFromBuffer(buffer, 0)
206
+ expect(result.value).toBeCloseTo(1.1, 10)
207
+ expect(result.bytesRead).toBe(9)
208
+ })
209
+
210
+ it('should parse Infinity from buffer', () => {
211
+ const buffer = hexToBytes('f97c00')
212
+ const result = parseFromBuffer(buffer, 0)
213
+ expect(result.value).toBe(Infinity)
214
+ expect(result.bytesRead).toBe(3)
215
+ })
216
+
217
+ it('should parse NaN from buffer', () => {
218
+ const buffer = hexToBytes('f97e00')
219
+ const result = parseFromBuffer(buffer, 0)
220
+ expect(result.value).toBeNaN()
221
+ expect(result.bytesRead).toBe(3)
222
+ })
223
+
224
+ it('should parse at non-zero offset', () => {
225
+ const buffer = hexToBytes('00f5')
226
+ const result = parseFromBuffer(buffer, 1)
227
+ expect(result.value).toBe(true)
228
+ expect(result.bytesRead).toBe(1)
229
+ })
230
+ })
231
+
232
+ describe('Buffer-native parsing - parseTagFromBuffer', () => {
233
+ const { parseTagFromBuffer } = useCborTag()
234
+
235
+ it('should export parseTagFromBuffer from useCborTag', () => {
236
+ expect(typeof parseTagFromBuffer).toBe('function')
237
+ })
238
+
239
+ it('should parse tag 1 (epoch time) from buffer', () => {
240
+ // c11a514b67b0 = tag(1, 1363896240)
241
+ const buffer = hexToBytes('c11a514b67b0')
242
+ const result = parseTagFromBuffer(buffer, 0)
243
+ expect(result.value).toEqual({
244
+ tag: 1,
245
+ value: 1363896240
246
+ })
247
+ expect(result.bytesRead).toBe(6)
248
+ })
249
+
250
+ it('should parse self-describe tag 55799 from buffer', () => {
251
+ // d9d9f7 01 = tag(55799, 1)
252
+ const buffer = hexToBytes('d9d9f701')
253
+ const result = parseTagFromBuffer(buffer, 0)
254
+ expect(result.value).toEqual({
255
+ tag: 55799,
256
+ value: 1
257
+ })
258
+ expect(result.bytesRead).toBe(4)
259
+ })
260
+
261
+ it('should parse tag at non-zero offset', () => {
262
+ // Prefix with 0x01, then tag 1 with value 0
263
+ const buffer = hexToBytes('01c100')
264
+ const result = parseTagFromBuffer(buffer, 1)
265
+ expect(result.value).toEqual({
266
+ tag: 1,
267
+ value: 0
268
+ })
269
+ expect(result.bytesRead).toBe(2)
270
+ })
271
+ })
272
+
273
+ describe('Buffer-native parsing - collection integration', () => {
274
+ const { parseArray, parseMap } = useCborCollection()
275
+
276
+ it('should parse array of integers without O(N^2) hex conversion', () => {
277
+ // 83 01 02 03 = [1, 2, 3]
278
+ const result = parseArray('83010203')
279
+ expect(result.value).toEqual([1, 2, 3])
280
+ expect(result.bytesRead).toBe(4)
281
+ })
282
+
283
+ it('should parse array with mixed types including floats', () => {
284
+ // 83 01 f5 f6 = [1, true, null]
285
+ const result = parseArray('8301f5f6')
286
+ expect(result.value).toEqual([1, true, null])
287
+ expect(result.bytesRead).toBe(4)
288
+ })
289
+
290
+ it('should parse nested array with tags', () => {
291
+ // 82 c1 1a514b67b0 01 = [tag(1, 1363896240), 1]
292
+ const result = parseArray('82c11a514b67b001')
293
+ expect(result.value).toHaveLength(2)
294
+ expect((result.value as any[])[0]).toEqual({
295
+ tag: 1,
296
+ value: 1363896240
297
+ })
298
+ expect((result.value as any[])[1]).toBe(1)
299
+ expect(result.bytesRead).toBe(8)
300
+ })
301
+
302
+ it('should parse map with integer keys', () => {
303
+ // a2 01 02 03 04 = {1: 2, 3: 4}
304
+ const result = parseMap('a201020304')
305
+ const map = result.value as Map<any, any>
306
+ expect(map.get(1)).toBe(2)
307
+ expect(map.get(3)).toBe(4)
308
+ expect(result.bytesRead).toBe(5)
309
+ })
310
+
311
+ it('should parse map with float value', () => {
312
+ // a1 01 f93c00 = {1: 1.0}
313
+ const result = parseMap('a101f93c00')
314
+ const map = result.value as Map<any, any>
315
+ expect(map.get(1)).toBe(1.0)
316
+ expect(result.bytesRead).toBe(5)
317
+ })
318
+
319
+ it('should parse large array correctly', () => {
320
+ // Build array of 100 integers 0-99
321
+ let hex = '9864' // array(100)... but that needs proper length encoding
322
+ // Actually: 0x98 = array with 1-byte length, 0x64 = 100
323
+ hex = '9864'
324
+ for (let i = 0; i < 100; i++) {
325
+ if (i < 24) {
326
+ hex += i.toString(16).padStart(2, '0')
327
+ } else {
328
+ hex += '18' + i.toString(16).padStart(2, '0')
329
+ }
330
+ }
331
+ const result = parseArray(hex)
332
+ const arr = result.value as number[]
333
+ expect(arr).toHaveLength(100)
334
+ for (let i = 0; i < 100; i++) {
335
+ expect(arr[i]).toBe(i)
336
+ }
337
+ })
338
+ })
@@ -14,6 +14,7 @@
14
14
  import { describe, it, expect } from 'vitest'
15
15
  import { useCborParser } from '../composables/useCborParser'
16
16
  import { useCborCollection } from '../composables/useCborCollection'
17
+ import type { CborMap } from '../types'
17
18
 
18
19
  describe('CBOR Map Duplicate Key Detection', () => {
19
20
  describe('String Keys - Duplicate Detection', () => {
@@ -154,7 +155,7 @@ describe('CBOR Map Duplicate Key Detection', () => {
154
155
 
155
156
  const result = parseMap(uniqueBytes, { dupMapKeyMode: 'reject' })
156
157
  // Byte strings as keys become comma-separated strings in JS
157
- expect(result.value.size).toBe(3)
158
+ expect((result.value as CborMap).size).toBe(3)
158
159
  })
159
160
 
160
161
  it('should reject duplicate empty byte strings', () => {
@@ -169,17 +170,16 @@ describe('CBOR Map Duplicate Key Detection', () => {
169
170
  })
170
171
 
171
172
  describe('Mixed Type Keys - Duplicate Detection', () => {
172
- it('should reject keys that collide in JavaScript object model', () => {
173
+ it('should allow distinct CBOR keys even if they stringify similarly', () => {
173
174
  const { parseMap } = useCborCollection()
174
175
 
175
176
  // Map: {1: 10, "1": 20} - different types in CBOR (int vs string)
176
177
  // 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
178
+ // Keys are distinct at the CBOR level and should not be treated as duplicates
179
179
  const mixedTypes = 'a2010a613114'
180
180
 
181
- expect(() => parseMap(mixedTypes, { dupMapKeyMode: 'reject' }))
182
- .toThrow(/duplicate.*key/i)
181
+ const result = parseMap(mixedTypes, { dupMapKeyMode: 'reject' })
182
+ expect((result.value as CborMap).size).toBe(2)
183
183
  })
184
184
 
185
185
  it('should detect duplicates when both are strings', () => {
@@ -333,7 +333,7 @@ describe('CBOR Map Duplicate Key Detection', () => {
333
333
  limits: { maxMapSize: 100 }
334
334
  })
335
335
 
336
- expect(result.value.size).toBe(30)
336
+ expect((result.value as CborMap).size).toBe(30)
337
337
  })
338
338
  })
339
339
 
@@ -377,6 +377,96 @@ describe('CBOR Map Duplicate Key Detection', () => {
377
377
  })
378
378
  })
379
379
 
380
+ describe('Semantic Duplicate Detection (RFC 8949 Section 5.6)', () => {
381
+ it('should reject integer 1 encoded as 0x01 and 0x1801 as duplicate keys', () => {
382
+ const { parseMap } = useCborCollection()
383
+
384
+ // Map with 2 entries, key 1 (0x01) -> value 10 (0x0a), key 1 (0x1801) -> value 20 (0x14)
385
+ // 0x01 encodes integer 1 directly (AI=1)
386
+ // 0x1801 encodes integer 1 with 1-byte payload (AI=24, payload=0x01)
387
+ // Both represent semantic value 1 -- must be detected as duplicate
388
+ // a2 = map(2), 01 = int(1), 0a = int(10), 1801 = int(1), 14 = int(20)
389
+ const duplicateHex = 'a2010a180114'
390
+
391
+ expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
392
+ .toThrow(/duplicate/i)
393
+ })
394
+
395
+ it('should reject integer 1 encoded as 0x01 and 0x190001 as duplicate keys', () => {
396
+ const { parseMap } = useCborCollection()
397
+
398
+ // 0x01 encodes integer 1 directly (AI=1)
399
+ // 0x190001 encodes integer 1 with 2-byte payload (AI=25, payload=0x0001)
400
+ // Both represent semantic value 1
401
+ // a2 = map(2), 01 = int(1), 0a = int(10), 190001 = int(1), 14 = int(20)
402
+ const duplicateHex = 'a2010a19000114'
403
+
404
+ expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
405
+ .toThrow(/duplicate/i)
406
+ })
407
+
408
+ it('should reject integer 1 encoded as 0x1801 and 0x190001 as duplicate keys', () => {
409
+ const { parseMap } = useCborCollection()
410
+
411
+ // 0x1801 encodes integer 1 with 1-byte payload (AI=24, payload=0x01)
412
+ // 0x190001 encodes integer 1 with 2-byte payload (AI=25, payload=0x0001)
413
+ // Both represent semantic value 1
414
+ // a2 = map(2), 1801 = int(1), 0a = int(10), 190001 = int(1), 14 = int(20)
415
+ const duplicateHex = 'a218010a19000114'
416
+
417
+ expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
418
+ .toThrow(/duplicate/i)
419
+ })
420
+
421
+ it('should reject integer 0 encoded with different byte widths as duplicate keys', () => {
422
+ const { parseMap } = useCborCollection()
423
+
424
+ // 0x00 encodes integer 0 directly (AI=0)
425
+ // 0x1800 encodes integer 0 with 1-byte payload (AI=24, payload=0x00)
426
+ // Both represent semantic value 0
427
+ // a2 = map(2), 00 = int(0), 01 = int(1), 1800 = int(0), 02 = int(2)
428
+ const duplicateHex = 'a200011800 02'
429
+
430
+ expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
431
+ .toThrow(/duplicate/i)
432
+ })
433
+
434
+ it('should still allow semantically different integer keys', () => {
435
+ const { parseMap } = useCborCollection()
436
+
437
+ // Map: {1: 10, 2: 20} - different semantic values, even if both use non-canonical encoding
438
+ // a2 = map(2), 1801 = int(1), 0a = int(10), 1802 = int(2), 14 = int(20)
439
+ const uniqueHex = 'a218010a180214'
440
+
441
+ const result = parseMap(uniqueHex, { dupMapKeyMode: 'reject' })
442
+ expect((result.value as CborMap).size).toBe(2)
443
+ })
444
+
445
+ it('should detect semantic duplicates via parseWithSourceMap path', () => {
446
+ const { parseWithSourceMap } = useCborParser()
447
+
448
+ // Map with key 1 (0x01) and key 1 (0x1801) -- semantic duplicate
449
+ // a2 = map(2), 01 = int(1), 0a = int(10), 1801 = int(1), 14 = int(20)
450
+ const duplicateHex = 'a2010a180114'
451
+
452
+ expect(() => parseWithSourceMap(duplicateHex, { dupMapKeyMode: 'reject' }))
453
+ .toThrow(/duplicate/i)
454
+ })
455
+
456
+ it('should detect semantic duplicates in indefinite-length maps', () => {
457
+ const { parseMap } = useCborCollection()
458
+
459
+ // Indefinite map: {_ 1: 10, 1(non-canonical): 20 }
460
+ // bf = map(indefinite), 01 = int(1), 0a = int(10), 1801 = int(1), 14 = int(20), ff = break
461
+ const duplicateHex = 'bf010a180114ff'
462
+
463
+ expect(() => parseMap(duplicateHex, {
464
+ dupMapKeyMode: 'reject',
465
+ allowIndefinite: true
466
+ })).toThrow(/duplicate/i)
467
+ })
468
+ })
469
+
380
470
  describe('Default Behavior', () => {
381
471
  it('should NOT reject duplicates by default (lenient mode)', () => {
382
472
  const { parseMap } = useCborCollection()