@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,509 @@
1
+ /**
2
+ * CBOR Collection Parser Composable
3
+ * Handles Major Types 4 (Arrays) and 5 (Maps)
4
+ * Supports definite and indefinite length encoding
5
+ */
6
+
7
+ import type { ParseResult, CborValue, CborMap, ParseOptions } from '../types'
8
+ import { INDEFINITE_SYMBOL, ALL_ENTRIES_SYMBOL } from '../types'
9
+ import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, compareBytes, bytesToHex } from '../utils'
10
+ import { useCborInteger } from './useCborInteger'
11
+ import { useCborString } from './useCborString'
12
+ import { useCborFloat } from './useCborFloat'
13
+ import { useCborTag } from './useCborTag'
14
+ import { logger } from '../../utils/logger'
15
+
16
+ /**
17
+ * Composable for parsing CBOR collections (Major Types 4 and 5)
18
+ *
19
+ * @returns Object with parseArray and parseMap functions
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const { parseArray } = useCborCollection()
24
+ * const result = parseArray('83010203') // [1, 2, 3]
25
+ * ```
26
+ */
27
+ export function useCborCollection() {
28
+ const { parseInteger } = useCborInteger()
29
+ const { parseByteString, parseTextString } = useCborString()
30
+ const { parse: parseFloatOrSimple } = useCborFloat()
31
+ const { parseTag } = useCborTag()
32
+
33
+ /**
34
+ * Convert a CBOR value to a string key for use in JavaScript objects
35
+ * Handles Uint8Array keys by converting them to hex strings
36
+ */
37
+ const convertKeyToString = (key: CborValue): string => {
38
+ if (key instanceof Uint8Array) {
39
+ return bytesToHex(key)
40
+ }
41
+ return String(key)
42
+ }
43
+
44
+ /**
45
+ * Internal parser dispatcher for CBOR items
46
+ * Handles recursive parsing of nested structures
47
+ *
48
+ * @param buffer - Data buffer
49
+ * @param offset - Current offset
50
+ * @param options - Parser options
51
+ * @param depth - Current nesting depth
52
+ * @returns Parsed value and bytes consumed
53
+ */
54
+ const parseItem = (buffer: Uint8Array, offset: number, options?: ParseOptions, depth: number = 0): ParseResult => {
55
+ if (offset >= buffer.length) {
56
+ throw new Error(`Unexpected end of buffer at offset ${offset}`)
57
+ }
58
+
59
+ const initialByte = readByte(buffer, offset)
60
+ const { majorType } = extractCborHeader(initialByte)
61
+
62
+ switch (majorType) {
63
+ case 0: // Unsigned integer
64
+ case 1: // Negative integer
65
+ {
66
+ // Create a hex string from the buffer starting at offset
67
+ const intHex = Array.from(buffer.slice(offset))
68
+ .map(b => b.toString(16).padStart(2, '0'))
69
+ .join('')
70
+ const result = parseInteger(intHex, options)
71
+ return { value: result.value, bytesRead: result.bytesRead }
72
+ }
73
+
74
+ case 2: // Byte string
75
+ return parseByteString(buffer, offset, options)
76
+
77
+ case 3: // Text string
78
+ return parseTextString(buffer, offset, options)
79
+
80
+ case 4: // Array
81
+ return parseArrayFromBuffer(buffer, offset, options, depth)
82
+
83
+ case 5: // Map
84
+ return parseMapFromBuffer(buffer, offset, options, depth)
85
+
86
+ case 6: // Tag
87
+ {
88
+ const tagHex = Array.from(buffer.slice(offset))
89
+ .map(b => b.toString(16).padStart(2, '0'))
90
+ .join('')
91
+ const result = parseTag(tagHex, options)
92
+ return { value: result.value, bytesRead: result.bytesRead }
93
+ }
94
+
95
+ case 7: // Simple/Float
96
+ {
97
+ const floatHex = Array.from(buffer.slice(offset))
98
+ .map(b => b.toString(16).padStart(2, '0'))
99
+ .join('')
100
+ const result = parseFloatOrSimple(floatHex, options)
101
+ return { value: result.value, bytesRead: result.bytesRead }
102
+ }
103
+
104
+ default:
105
+ throw new Error(`Unknown major type: ${majorType}`)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Parses the length from CBOR additional info field
111
+ *
112
+ * @param buffer - Data buffer
113
+ * @param offset - Current offset (after initial byte)
114
+ * @param ai - Additional info field (0-31)
115
+ * @returns Object with length and bytes consumed, or null for indefinite
116
+ */
117
+ const parseLength = (
118
+ buffer: Uint8Array,
119
+ offset: number,
120
+ ai: number,
121
+ options?: ParseOptions
122
+ ): { length: number | null; bytesConsumed: number } => {
123
+ if (ai < 24) {
124
+ // Direct encoding (0-23)
125
+ return { length: ai, bytesConsumed: 0 }
126
+ } else if (ai === 24) {
127
+ // 1 byte follows
128
+ const length = readByte(buffer, offset)
129
+
130
+ // RFC 8949 Section 4.2.1: Canonical encoding requires shortest form
131
+ if (options?.validateCanonical && length < 24) {
132
+ throw new Error(`Non-canonical length encoding: ${length} should use direct encoding (AI < 24), not AI=24`)
133
+ }
134
+
135
+ return { length, bytesConsumed: 1 }
136
+ } else if (ai === 25) {
137
+ // 2 bytes follow
138
+ const length = readUint(buffer, offset, 2)
139
+
140
+ // RFC 8949 Section 4.2.1: Value must be >= 256 to justify 2-byte encoding
141
+ if (options?.validateCanonical && length < 256) {
142
+ throw new Error(`Non-canonical length encoding: ${length} should use ${length < 24 ? 'direct encoding' : '1-byte encoding (AI=24)'}, not AI=25`)
143
+ }
144
+
145
+ return { length, bytesConsumed: 2 }
146
+ } else if (ai === 26) {
147
+ // 4 bytes follow
148
+ const length = readUint(buffer, offset, 4)
149
+
150
+ // RFC 8949 Section 4.2.1: Value must be >= 65536 to justify 4-byte encoding
151
+ if (options?.validateCanonical && length < 65536) {
152
+ throw new Error(`Non-canonical length encoding: ${length} should use shorter encoding, not AI=26`)
153
+ }
154
+
155
+ return { length, bytesConsumed: 4 }
156
+ } else if (ai === 27) {
157
+ // 8 bytes follow
158
+ const lengthBigInt = readBigUint(buffer, offset, 8)
159
+
160
+ // Check if value fits in safe integer range
161
+ if (lengthBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
162
+ throw new Error(`Collection length ${lengthBigInt} exceeds maximum safe integer`)
163
+ }
164
+ const length = Number(lengthBigInt)
165
+
166
+ // RFC 8949 Section 4.2.1: Value must be >= 2^32 to justify 8-byte encoding
167
+ if (options?.validateCanonical && length < 4294967296) {
168
+ throw new Error(`Non-canonical length encoding: ${length} should use shorter encoding, not AI=27`)
169
+ }
170
+
171
+ return { length, bytesConsumed: 8 }
172
+ } else if (ai === 31) {
173
+ // Indefinite length (break-terminated)
174
+ return { length: null, bytesConsumed: 0 }
175
+ } else {
176
+ throw new Error(`Invalid additional info: ${ai}`)
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Internal array parser that works with buffers
182
+ *
183
+ * @param buffer - Data buffer
184
+ * @param offset - Current offset
185
+ * @param options - Parser options
186
+ * @param depth - Current nesting depth
187
+ * @returns Parsed array and bytes read
188
+ */
189
+ const parseArrayFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions, depth: number = 0): ParseResult => {
190
+ const initialByte = readByte(buffer, offset)
191
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
192
+
193
+ if (majorType !== 4) {
194
+ throw new Error(`Expected major type 4 (array), got ${majorType}`)
195
+ }
196
+
197
+ // Check if indefinite length is allowed
198
+ // RFC 8949: Deterministic encoding MUST NOT use indefinite-length
199
+ const isIndefiniteAllowed = options?.allowIndefinite ?? !(options?.validateCanonical || options?.strict)
200
+ if (additionalInfo === 31 && !isIndefiniteAllowed) {
201
+ throw new Error('Indefinite-length encoding is not allowed (strict/canonical mode)')
202
+ }
203
+
204
+ // Check depth limit before descending
205
+ if (options?.limits?.maxDepth && depth >= options.limits.maxDepth) {
206
+ throw new Error(`Maximum nesting depth ${options.limits.maxDepth} exceeded`)
207
+ }
208
+
209
+ const { length, bytesConsumed } = parseLength(buffer, offset + 1, additionalInfo, options)
210
+ let currentOffset = offset + 1 + bytesConsumed
211
+ const items: CborValue[] = []
212
+
213
+ if (length === null) {
214
+ // Indefinite-length array - read until break (0xff)
215
+ let index = 0
216
+ let foundBreak = false
217
+
218
+ while (currentOffset < buffer.length) {
219
+ const nextByte = readByte(buffer, currentOffset)
220
+
221
+ // Check for break marker (0xff)
222
+ if (nextByte === 0xff) {
223
+ currentOffset++ // Consume the break byte
224
+ foundBreak = true
225
+ break
226
+ }
227
+
228
+ // Check array length limit
229
+ if (options?.limits?.maxArrayLength && index >= options.limits.maxArrayLength) {
230
+ throw new Error(`Array length exceeds limit of ${options.limits.maxArrayLength}`)
231
+ }
232
+
233
+ const itemResult = parseItem(buffer, currentOffset, options, depth + 1)
234
+ items.push(itemResult.value)
235
+ currentOffset += itemResult.bytesRead
236
+ index++
237
+ }
238
+
239
+ // Ensure we found a break code
240
+ if (!foundBreak) {
241
+ throw new Error('Indefinite-length array missing break code (0xFF)')
242
+ }
243
+
244
+ // Mark as indefinite-length for round-trip preservation
245
+ ;(items as any)[INDEFINITE_SYMBOL] = true
246
+ } else {
247
+ // Definite-length array
248
+ // Check array length limit before parsing
249
+ if (options?.limits?.maxArrayLength && length > options.limits.maxArrayLength) {
250
+ throw new Error(`Array length ${length} exceeds limit of ${options.limits.maxArrayLength}`)
251
+ }
252
+
253
+ for (let i = 0; i < length; i++) {
254
+ if (currentOffset >= buffer.length) {
255
+ throw new Error(`Unexpected end of buffer while parsing array element ${i}/${length}`)
256
+ }
257
+
258
+ const itemResult = parseItem(buffer, currentOffset, options, depth + 1)
259
+ items.push(itemResult.value)
260
+ currentOffset += itemResult.bytesRead
261
+ }
262
+ }
263
+
264
+ return {
265
+ value: items,
266
+ bytesRead: currentOffset - offset
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Converts a CBOR value to its canonical byte representation for comparison
272
+ * Note: Currently unused but kept for future canonical encoding validation
273
+ */
274
+ // const valueToBytes = (value: CborValue, buffer: Uint8Array, start: number, end: number): Uint8Array => {
275
+ // // For canonical comparison, we use the raw bytes from the buffer
276
+ // return buffer.slice(start, end)
277
+ // }
278
+
279
+ /**
280
+ * Internal map parser that works with buffers
281
+ *
282
+ * @param buffer - Data buffer
283
+ * @param offset - Current offset
284
+ * @param options - Parser options
285
+ * @param depth - Current nesting depth
286
+ * @returns Parsed map and bytes read
287
+ */
288
+ const parseMapFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions, depth: number = 0): ParseResult => {
289
+ const initialByte = readByte(buffer, offset)
290
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
291
+
292
+ if (majorType !== 5) {
293
+ throw new Error(`Expected major type 5 (map), got ${majorType}`)
294
+ }
295
+
296
+ // Check if indefinite length is allowed
297
+ // RFC 8949: Deterministic encoding MUST NOT use indefinite-length
298
+ const isIndefiniteAllowed = options?.allowIndefinite ?? !(options?.validateCanonical || options?.strict)
299
+ if (additionalInfo === 31 && !isIndefiniteAllowed) {
300
+ throw new Error('Indefinite-length encoding is not allowed (strict/canonical mode)')
301
+ }
302
+
303
+ // Check depth limit before descending
304
+ if (options?.limits?.maxDepth && depth >= options.limits.maxDepth) {
305
+ throw new Error(`Maximum nesting depth ${options.limits.maxDepth} exceeded`)
306
+ }
307
+
308
+ const { length, bytesConsumed } = parseLength(buffer, offset + 1, additionalInfo, options)
309
+ let currentOffset = offset + 1 + bytesConsumed
310
+ const map: CborMap = new Map()
311
+
312
+ // For duplicate key detection - store serialized keys
313
+ const seenKeys = new Set<string>()
314
+
315
+ // For canonical ordering validation
316
+ const keyBytes: Uint8Array[] = []
317
+
318
+ // Store ALL entries including duplicates for byte-perfect round-trips
319
+ const allEntries: Array<[CborValue, CborValue]> = []
320
+
321
+ if (length === null) {
322
+ // Indefinite-length map - read until break (0xff)
323
+ let index = 0
324
+ let foundBreak = false
325
+
326
+ while (currentOffset < buffer.length) {
327
+ const nextByte = readByte(buffer, currentOffset)
328
+
329
+ // Check for break marker (0xff)
330
+ if (nextByte === 0xff) {
331
+ currentOffset++ // Consume the break byte
332
+ foundBreak = true
333
+ break
334
+ }
335
+
336
+ // Check map size limit
337
+ if (options?.limits?.maxMapSize && index >= options.limits.maxMapSize) {
338
+ throw new Error(`Map size exceeds limit of ${options.limits.maxMapSize}`)
339
+ }
340
+
341
+ // Parse key
342
+ const keyStart = currentOffset
343
+ const keyResult = parseItem(buffer, currentOffset, options, depth + 1)
344
+ const keyEnd = currentOffset + keyResult.bytesRead
345
+ currentOffset += keyResult.bytesRead
346
+
347
+ // Store key bytes for canonical validation
348
+ if (options?.validateCanonical) {
349
+ keyBytes.push(buffer.slice(keyStart, keyEnd))
350
+ }
351
+
352
+ // Parse value
353
+ if (currentOffset >= buffer.length) {
354
+ throw new Error('Unexpected end of buffer while parsing map value')
355
+ }
356
+ const valueResult = parseItem(buffer, currentOffset, options, depth + 1)
357
+ currentOffset += valueResult.bytesRead
358
+
359
+ // For duplicate key detection, serialize the key
360
+ const keyString = convertKeyToString(keyResult.value)
361
+
362
+ // Check for duplicate keys based on dupMapKeyMode
363
+ // RFC 8949: Deterministic encoding SHOULD reject duplicate keys
364
+ // Auto-enable rejection in canonical or strict mode (handled in mergeOptions)
365
+ if (seenKeys.has(keyString)) {
366
+ const mode: 'allow' | 'warn' | 'reject' = options?.dupMapKeyMode || 'allow'
367
+
368
+ if (mode === 'reject') {
369
+ throw new Error(`Duplicate map key detected: ${keyString} at offset ${keyStart}`)
370
+ } else if (mode === 'warn') {
371
+ logger.warn(`Duplicate map key detected: ${keyString} at offset ${keyStart}`)
372
+ }
373
+ // 'allow' mode: silently continue
374
+ }
375
+ seenKeys.add(keyString)
376
+
377
+ // Store in Map with original key type (not string!)
378
+ map.set(keyResult.value, valueResult.value)
379
+
380
+ // Also store in allEntries to preserve order and duplicates
381
+ allEntries.push([keyResult.value, valueResult.value])
382
+
383
+ index++
384
+ }
385
+
386
+ // Ensure we found a break code
387
+ if (!foundBreak) {
388
+ throw new Error('Indefinite-length map missing break code (0xFF)')
389
+ }
390
+
391
+ // Mark as indefinite-length for round-trip preservation
392
+ ;(map as any)[INDEFINITE_SYMBOL] = true
393
+ } else {
394
+ // Definite-length map
395
+ // Check map size limit before parsing
396
+ if (options?.limits?.maxMapSize && length > options.limits.maxMapSize) {
397
+ throw new Error(`Map size ${length} exceeds limit of ${options.limits.maxMapSize}`)
398
+ }
399
+
400
+ for (let i = 0; i < length; i++) {
401
+ if (currentOffset >= buffer.length) {
402
+ throw new Error(`Unexpected end of buffer while parsing map entry ${i}/${length}`)
403
+ }
404
+
405
+ // Parse key
406
+ const keyStart = currentOffset
407
+ const keyResult = parseItem(buffer, currentOffset, options, depth + 1)
408
+ const keyEnd = currentOffset + keyResult.bytesRead
409
+ currentOffset += keyResult.bytesRead
410
+
411
+ // Store key bytes for canonical validation
412
+ if (options?.validateCanonical) {
413
+ keyBytes.push(buffer.slice(keyStart, keyEnd))
414
+ }
415
+
416
+ // Parse value
417
+ if (currentOffset >= buffer.length) {
418
+ throw new Error(`Unexpected end of buffer while parsing map value for entry ${i}/${length}`)
419
+ }
420
+ const valueResult = parseItem(buffer, currentOffset, options, depth + 1)
421
+ currentOffset += valueResult.bytesRead
422
+
423
+ // For duplicate key detection, serialize the key
424
+ const keyString = convertKeyToString(keyResult.value)
425
+
426
+ // Check for duplicate keys based on dupMapKeyMode
427
+ // RFC 8949: Deterministic encoding SHOULD reject duplicate keys
428
+ // Auto-enable rejection in canonical or strict mode (handled in mergeOptions)
429
+ if (seenKeys.has(keyString)) {
430
+ const mode: 'allow' | 'warn' | 'reject' = options?.dupMapKeyMode || 'allow'
431
+
432
+ if (mode === 'reject') {
433
+ throw new Error(`Duplicate map key detected: ${keyString} at offset ${keyStart}`)
434
+ } else if (mode === 'warn') {
435
+ logger.warn(`Duplicate map key detected: ${keyString} at offset ${keyStart}`)
436
+ }
437
+ // 'allow' mode: silently continue
438
+ }
439
+ seenKeys.add(keyString)
440
+
441
+ // Store in Map with original key type (not string!)
442
+ map.set(keyResult.value, valueResult.value)
443
+
444
+ // Also store in allEntries to preserve order and duplicates
445
+ allEntries.push([keyResult.value, valueResult.value])
446
+ }
447
+ }
448
+
449
+ // Attach allEntries to map for byte-perfect round-trips with duplicates
450
+ ;(map as any)[ALL_ENTRIES_SYMBOL] = allEntries
451
+
452
+ // Validate canonical key ordering (keys must be sorted by byte representation)
453
+ if (options?.validateCanonical && keyBytes.length > 1) {
454
+ for (let i = 1; i < keyBytes.length; i++) {
455
+ const prevKey = keyBytes[i - 1]
456
+ const currKey = keyBytes[i]
457
+ if (prevKey && currKey) {
458
+ const cmp = compareBytes(prevKey, currKey)
459
+ if (cmp > 0) {
460
+ throw new Error(
461
+ `Map keys are not in canonical order: key at index ${i} should come before key at index ${i - 1}`
462
+ )
463
+ }
464
+ if (cmp === 0) {
465
+ throw new Error(`Duplicate map keys detected in canonical validation`)
466
+ }
467
+ }
468
+ }
469
+ }
470
+
471
+ return {
472
+ value: map,
473
+ bytesRead: currentOffset - offset
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Parses CBOR array (Major Type 4) from hex string
479
+ *
480
+ * @param hexString - CBOR hex string
481
+ * @param options - Parser options (optional)
482
+ * @returns Parsed array and bytes read
483
+ */
484
+ const parseArray = (hexString: string, options?: ParseOptions): ParseResult => {
485
+ // Remove spaces from hex string
486
+ const cleanHex = hexString.replace(/\s+/g, '')
487
+ const buffer = hexToBytes(cleanHex)
488
+ return parseArrayFromBuffer(buffer, 0, options, 0)
489
+ }
490
+
491
+ /**
492
+ * Parses CBOR map (Major Type 5) from hex string
493
+ *
494
+ * @param hexString - CBOR hex string
495
+ * @param options - Parser options (optional)
496
+ * @returns Parsed map and bytes read
497
+ */
498
+ const parseMap = (hexString: string, options?: ParseOptions): ParseResult => {
499
+ // Remove spaces from hex string
500
+ const cleanHex = hexString.replace(/\s+/g, '')
501
+ const buffer = hexToBytes(cleanHex)
502
+ return parseMapFromBuffer(buffer, 0, options, 0)
503
+ }
504
+
505
+ return {
506
+ parseArray,
507
+ parseMap
508
+ }
509
+ }