@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,951 @@
1
+ /**
2
+ * CBOR Main Parser Composable
3
+ * Orchestrates all CBOR parsers and provides a unified parse interface
4
+ * Auto-detects major type and dispatches to appropriate parser
5
+ */
6
+
7
+ import type { ParseResult, ParseResultWithMap, SourceMapEntry, ParseOptions, CborContext, CborValue } from '../types'
8
+ import { DEFAULT_OPTIONS, DEFAULT_LIMITS } from '../types'
9
+ import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader } from '../utils'
10
+ import { useCborInteger } from './useCborInteger'
11
+ import { useCborString } from './useCborString'
12
+ import { useCborCollection } from './useCborCollection'
13
+ import { useCborTag } from './useCborTag'
14
+ import { useCborFloat } from './useCborFloat'
15
+ import { logger } from '../../utils/logger'
16
+
17
+ /**
18
+ * Main CBOR parser composable
19
+ * Provides a unified interface for parsing any CBOR data
20
+ *
21
+ * @returns Object with parse function
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const { parse } = useCborParser()
26
+ * const result = parse('1864') // { value: 100, bytesRead: 2 }
27
+ * ```
28
+ */
29
+ export function useCborParser() {
30
+ /**
31
+ * Merges user options with defaults
32
+ */
33
+ const mergeOptions = (options?: ParseOptions): Required<ParseOptions> => {
34
+ if (!options) return DEFAULT_OPTIONS
35
+
36
+ // Determine if canonical validation is enabled
37
+ const isCanonical = options.validateCanonical ?? (options.strict ? true : false)
38
+
39
+ return {
40
+ strict: options.strict ?? DEFAULT_OPTIONS.strict,
41
+ validateCanonical: isCanonical,
42
+ // RFC 8949 Section 4.2: Deterministic encoding MUST NOT use indefinite-length
43
+ allowIndefinite: options.allowIndefinite ?? (isCanonical || options.strict ? false : DEFAULT_OPTIONS.allowIndefinite),
44
+ // Auto-enable duplicate key rejection for canonical or strict mode
45
+ dupMapKeyMode: options.dupMapKeyMode ?? (isCanonical || options.strict ? 'reject' : DEFAULT_OPTIONS.dupMapKeyMode),
46
+ validateUtf8Strict: options.validateUtf8Strict ?? (options.strict ? true : DEFAULT_OPTIONS.validateUtf8Strict),
47
+ validateSetUniqueness: options.validateSetUniqueness ?? (options.strict ? true : DEFAULT_OPTIONS.validateSetUniqueness),
48
+ validateTagSemantics: options.validateTagSemantics ?? (options.strict ? true : DEFAULT_OPTIONS.validateTagSemantics),
49
+ validatePlutusSemantics: options.validatePlutusSemantics ?? (options.strict ? true : DEFAULT_OPTIONS.validatePlutusSemantics),
50
+ limits: {
51
+ maxInputSize: options.limits?.maxInputSize ?? DEFAULT_LIMITS.maxInputSize,
52
+ maxOutputSize: options.limits?.maxOutputSize ?? DEFAULT_LIMITS.maxOutputSize,
53
+ maxStringLength: options.limits?.maxStringLength ?? DEFAULT_LIMITS.maxStringLength,
54
+ maxArrayLength: options.limits?.maxArrayLength ?? DEFAULT_LIMITS.maxArrayLength,
55
+ maxMapSize: options.limits?.maxMapSize ?? DEFAULT_LIMITS.maxMapSize,
56
+ maxDepth: options.limits?.maxDepth ?? DEFAULT_LIMITS.maxDepth,
57
+ maxTagDepth: options.limits?.maxTagDepth ?? DEFAULT_LIMITS.maxTagDepth,
58
+ maxBignumBytes: options.limits?.maxBignumBytes ?? DEFAULT_LIMITS.maxBignumBytes,
59
+ maxParseTime: options.limits?.maxParseTime ?? DEFAULT_LIMITS.maxParseTime
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Checks if max parse time has been exceeded
66
+ */
67
+ const checkTimeout = (ctx: CborContext): void => {
68
+ if (!ctx.startTime || !ctx.options?.limits?.maxParseTime) return
69
+
70
+ const elapsed = Date.now() - ctx.startTime
71
+ if (elapsed > ctx.options.limits.maxParseTime) {
72
+ throw new Error(`Parse timeout: exceeded ${ctx.options.limits.maxParseTime}ms limit (elapsed: ${elapsed}ms)`)
73
+ }
74
+ }
75
+
76
+ const { parseInteger } = useCborInteger()
77
+ const { parseString } = useCborString()
78
+ const { parseArray, parseMap } = useCborCollection()
79
+ const { parseTag } = useCborTag()
80
+ const { parse: parseFloatOrSimple } = useCborFloat()
81
+
82
+ /**
83
+ * Parses a CBOR hex string, auto-detecting the type
84
+ *
85
+ * @param hexString - CBOR data as hex string
86
+ * @param options - Parser options (optional)
87
+ * @returns Parsed value and bytes read
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * parse('00') // 0
92
+ * parse('6449455446') // "IETF"
93
+ * parse('83010203') // [1, 2, 3]
94
+ * parse('a16161 01') // { a: 1 }
95
+ * parse('c11a514b67b0') // { tag: 1, value: 1363896240 }
96
+ * parse('f5') // true
97
+ *
98
+ * // With options
99
+ * parse('1864', { validateCanonical: true })
100
+ * parse('6449455446', { strict: true })
101
+ * ```
102
+ */
103
+ const parse = (hexString: string, options?: ParseOptions): ParseResult => {
104
+ // Remove spaces from hex string
105
+ const cleanHex = hexString.replace(/\s+/g, '')
106
+
107
+ // Validate hex string
108
+ if (!cleanHex || cleanHex.length === 0) {
109
+ throw new Error('Empty hex string')
110
+ }
111
+
112
+ if (cleanHex.length % 2 !== 0) {
113
+ throw new Error('Hex string must have even length')
114
+ }
115
+
116
+ if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
117
+ throw new Error(`Invalid hex character in: ${cleanHex}`)
118
+ }
119
+
120
+ // Merge options with defaults
121
+ const mergedOptions = mergeOptions(options)
122
+
123
+ // Check input size limit
124
+ const inputSize = cleanHex.length / 2 // Convert hex chars to bytes
125
+ if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
126
+ throw new Error(`Input size ${inputSize} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
127
+ }
128
+
129
+ // Convert to buffer and extract major type
130
+ const buffer = hexToBytes(cleanHex)
131
+ const initialByte = readByte(buffer, 0)
132
+ const { majorType } = extractCborHeader(initialByte)
133
+
134
+ // Dispatch to appropriate parser based on major type
135
+ switch (majorType) {
136
+ case 0: // Unsigned integer
137
+ case 1: // Negative integer
138
+ return parseInteger(cleanHex, mergedOptions)
139
+
140
+ case 2: // Byte string
141
+ case 3: // Text string
142
+ return parseString(cleanHex, mergedOptions)
143
+
144
+ case 4: // Array
145
+ return parseArray(cleanHex, mergedOptions)
146
+
147
+ case 5: // Map
148
+ return parseMap(cleanHex, mergedOptions)
149
+
150
+ case 6: // Tagged value
151
+ return parseTag(cleanHex, mergedOptions)
152
+
153
+ case 7: // Floating-point or simple value
154
+ return parseFloatOrSimple(cleanHex, mergedOptions)
155
+
156
+ default:
157
+ throw new Error(`Unknown major type: ${majorType}`)
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Parses CBOR with source map generation for visualization
163
+ *
164
+ * @param hexString - CBOR data as hex string
165
+ * @param options - Parser options (optional)
166
+ * @returns Parsed value, bytes read, and source map
167
+ */
168
+ const parseWithSourceMap = (hexString: string, options?: ParseOptions): ParseResultWithMap => {
169
+ const cleanHex = hexString.replace(/\s+/g, '')
170
+
171
+ // Validate hex string
172
+ if (!cleanHex || cleanHex.length === 0) {
173
+ throw new Error('Empty hex string')
174
+ }
175
+ if (cleanHex.length % 2 !== 0) {
176
+ throw new Error('Hex string must have even length')
177
+ }
178
+ if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
179
+ throw new Error(`Invalid hex character in: ${cleanHex}`)
180
+ }
181
+
182
+ // Merge options with defaults
183
+ const mergedOptions = mergeOptions(options)
184
+
185
+ // Check input size limit
186
+ const inputSize = cleanHex.length / 2
187
+ if (mergedOptions.limits?.maxInputSize && inputSize > mergedOptions.limits.maxInputSize) {
188
+ throw new Error(`Input size ${inputSize} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
189
+ }
190
+
191
+ const buffer = hexToBytes(cleanHex)
192
+ const sourceMap: SourceMapEntry[] = []
193
+
194
+ // Create context with tracking
195
+ const ctx: CborContext = {
196
+ buffer,
197
+ offset: 0,
198
+ sourceMap,
199
+ currentDepth: 0,
200
+ startTime: Date.now(),
201
+ bytesAllocated: 0,
202
+ options: mergedOptions
203
+ }
204
+
205
+ // Parse with source map tracking
206
+ const result = parseValueWithMap(ctx, 0, '', sourceMap)
207
+
208
+ return {
209
+ value: result.value,
210
+ bytesRead: result.bytesRead,
211
+ sourceMap
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Internal recursive parser that builds source map
217
+ */
218
+ const parseValueWithMap = (
219
+ ctx: CborContext,
220
+ offset: number,
221
+ path: string,
222
+ sourceMap: SourceMapEntry[]
223
+ ): ParseResult => {
224
+ // Check timeout periodically
225
+ checkTimeout(ctx)
226
+
227
+ const initialByte = readByte(ctx.buffer, offset)
228
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
229
+ const startOffset = offset
230
+
231
+ let result: ParseResult
232
+ let typeDescription: string
233
+
234
+ switch (majorType) {
235
+ case 0: // Unsigned integer
236
+ typeDescription = 'Unsigned Integer'
237
+ result = parseIntegerFromBuffer(ctx.buffer, offset, ctx.options)
238
+ // Add entry for simple values
239
+ sourceMap.push({
240
+ path,
241
+ start: startOffset,
242
+ end: startOffset + result.bytesRead,
243
+ majorType,
244
+ type: typeDescription
245
+ })
246
+ break
247
+
248
+ case 1: // Negative integer
249
+ typeDescription = 'Negative Integer'
250
+ result = parseIntegerFromBuffer(ctx.buffer, offset, ctx.options)
251
+ sourceMap.push({
252
+ path,
253
+ start: startOffset,
254
+ end: startOffset + result.bytesRead,
255
+ majorType,
256
+ type: typeDescription
257
+ })
258
+ break
259
+
260
+ case 2: // Byte string
261
+ {
262
+ result = parseStringFromBuffer(ctx.buffer, offset, ctx.options)
263
+ // Track bytes allocated
264
+ if (ctx.bytesAllocated !== undefined && result.value instanceof Uint8Array) {
265
+ ctx.bytesAllocated += result.value.length
266
+ if (ctx.options?.limits?.maxOutputSize && ctx.bytesAllocated > ctx.options.limits.maxOutputSize) {
267
+ throw new Error(`Output size ${ctx.bytesAllocated} bytes exceeds limit of ${ctx.options.limits.maxOutputSize} bytes`)
268
+ }
269
+ }
270
+
271
+ // Calculate header length (type byte + length encoding)
272
+ const headerBytes = additionalInfo < 24 ? 1 :
273
+ additionalInfo === 24 ? 2 :
274
+ additionalInfo === 25 ? 3 :
275
+ additionalInfo === 26 ? 5 :
276
+ additionalInfo === 27 ? 9 : 1
277
+ const headerEnd = startOffset + headerBytes
278
+ const contentLength = result.value instanceof Uint8Array ? result.value.length : 0
279
+
280
+ // Add header entry
281
+ typeDescription = `bytes(${contentLength})`
282
+ sourceMap.push({
283
+ path,
284
+ start: startOffset,
285
+ end: headerEnd,
286
+ majorType,
287
+ type: typeDescription,
288
+ isHeader: true,
289
+ headerEnd,
290
+ contentPath: contentLength > 0 ? `${path}#content` : undefined,
291
+ children: contentLength > 0 ? [`${path}#content`] : []
292
+ })
293
+
294
+ // Add content entry (if non-empty)
295
+ if (contentLength > 0) {
296
+ sourceMap.push({
297
+ path: `${path}#content`,
298
+ start: headerEnd,
299
+ end: startOffset + result.bytesRead,
300
+ majorType: 2,
301
+ type: `→ ${contentLength} bytes`,
302
+ isContent: true,
303
+ parent: path
304
+ })
305
+ }
306
+ }
307
+ break
308
+
309
+ case 3: // Text string
310
+ {
311
+ result = parseStringFromBuffer(ctx.buffer, offset, ctx.options)
312
+ // Track bytes allocated
313
+ if (ctx.bytesAllocated !== undefined && typeof result.value === 'string') {
314
+ ctx.bytesAllocated += result.value.length
315
+ if (ctx.options?.limits?.maxOutputSize && ctx.bytesAllocated > ctx.options.limits.maxOutputSize) {
316
+ throw new Error(`Output size ${ctx.bytesAllocated} bytes exceeds limit of ${ctx.options.limits.maxOutputSize} bytes`)
317
+ }
318
+ }
319
+
320
+ // Calculate header length (type byte + length encoding)
321
+ const headerBytes = additionalInfo < 24 ? 1 :
322
+ additionalInfo === 24 ? 2 :
323
+ additionalInfo === 25 ? 3 :
324
+ additionalInfo === 26 ? 5 :
325
+ additionalInfo === 27 ? 9 : 1
326
+ const headerEnd = startOffset + headerBytes
327
+ const contentLength = typeof result.value === 'string' ? result.value.length : 0
328
+
329
+ // Add header entry
330
+ typeDescription = `text(${contentLength})`
331
+ sourceMap.push({
332
+ path,
333
+ start: startOffset,
334
+ end: headerEnd,
335
+ majorType,
336
+ type: typeDescription,
337
+ isHeader: true,
338
+ headerEnd,
339
+ contentPath: contentLength > 0 ? `${path}#content` : undefined,
340
+ children: contentLength > 0 ? [`${path}#content`] : []
341
+ })
342
+
343
+ // Add content entry (if non-empty)
344
+ if (contentLength > 0) {
345
+ sourceMap.push({
346
+ path: `${path}#content`,
347
+ start: headerEnd,
348
+ end: startOffset + result.bytesRead,
349
+ majorType: 3,
350
+ type: `→ "${result.value}"`,
351
+ isContent: true,
352
+ parent: path
353
+ })
354
+ }
355
+ }
356
+ break
357
+
358
+ case 4: // Array
359
+ typeDescription = 'Array'
360
+ // For arrays and maps, the recursive function handles source map entries
361
+ result = parseArrayWithMap(ctx, offset, path, sourceMap)
362
+ break
363
+
364
+ case 5: // Map
365
+ typeDescription = 'Map'
366
+ result = parseMapWithMap(ctx, offset, path, sourceMap)
367
+ break
368
+
369
+ case 6: // Tag
370
+ // parseTagWithMap handles source map creation internally (with parent/child relationships)
371
+ result = parseTagWithMap(ctx, offset, path, sourceMap)
372
+ break
373
+
374
+ case 7: // Float/Simple
375
+ typeDescription = getSimpleTypeDescription(additionalInfo)
376
+ result = parseFloatFromBuffer(ctx.buffer, offset, ctx.options)
377
+ sourceMap.push({
378
+ path,
379
+ start: startOffset,
380
+ end: startOffset + result.bytesRead,
381
+ majorType,
382
+ type: typeDescription
383
+ })
384
+ break
385
+
386
+ default:
387
+ throw new Error(`Unknown major type: ${majorType}`)
388
+ }
389
+
390
+ return result
391
+ }
392
+
393
+ /**
394
+ * Helper to parse integer from buffer
395
+ */
396
+ const parseIntegerFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
397
+ const hexString = Array.from(buffer.slice(offset))
398
+ .map(b => b.toString(16).padStart(2, '0'))
399
+ .join('')
400
+ return parseInteger(hexString, options)
401
+ }
402
+
403
+ /**
404
+ * Helper to parse string from buffer
405
+ */
406
+ const parseStringFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
407
+ const hexString = Array.from(buffer.slice(offset))
408
+ .map(b => b.toString(16).padStart(2, '0'))
409
+ .join('')
410
+ return parseString(hexString, options)
411
+ }
412
+
413
+ /**
414
+ * Helper to parse float from buffer
415
+ */
416
+ const parseFloatFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
417
+ const hexString = Array.from(buffer.slice(offset))
418
+ .map(b => b.toString(16).padStart(2, '0'))
419
+ .join('')
420
+ return parseFloatOrSimple(hexString, options)
421
+ }
422
+
423
+ /**
424
+ * Parse array with source map tracking
425
+ */
426
+ const parseArrayWithMap = (
427
+ ctx: CborContext,
428
+ offset: number,
429
+ path: string,
430
+ sourceMap: SourceMapEntry[]
431
+ ): ParseResult => {
432
+ const startOffset = offset
433
+ const initialByte = readByte(ctx.buffer, offset)
434
+ const { additionalInfo } = extractCborHeader(initialByte)
435
+
436
+ let currentOffset = offset + 1
437
+ const items: any[] = []
438
+
439
+ // Determine array length
440
+ let length: number
441
+ let isIndefinite = false
442
+
443
+ if (additionalInfo < 24) {
444
+ length = additionalInfo
445
+ } else if (additionalInfo === 24) {
446
+ length = readByte(ctx.buffer, currentOffset)
447
+ currentOffset += 1
448
+ } else if (additionalInfo === 25) {
449
+ length = readUint(ctx.buffer, currentOffset, 2)
450
+ currentOffset += 2
451
+ } else if (additionalInfo === 26) {
452
+ length = readUint(ctx.buffer, currentOffset, 4)
453
+ currentOffset += 4
454
+ } else if (additionalInfo === 27) {
455
+ const bigLength = readBigUint(ctx.buffer, currentOffset, 8)
456
+ if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
457
+ throw new Error('Array length exceeds maximum safe integer')
458
+ }
459
+ length = Number(bigLength)
460
+ currentOffset += 8
461
+ } else if (additionalInfo === 31) {
462
+ isIndefinite = true
463
+ length = 0
464
+ } else {
465
+ throw new Error(`Invalid additional info: ${additionalInfo}`)
466
+ }
467
+
468
+ // Calculate header length (where content starts)
469
+ const headerEnd = currentOffset
470
+
471
+ // Add header entry for array
472
+ const arrayEntryIndex = sourceMap.length
473
+ sourceMap.push({
474
+ path,
475
+ start: startOffset,
476
+ end: headerEnd,
477
+ majorType: 4,
478
+ type: isIndefinite ? 'array(indefinite)' : `array(${length})`,
479
+ isHeader: true,
480
+ headerEnd
481
+ })
482
+
483
+ // Parse array elements
484
+ const childPaths: string[] = []
485
+ if (isIndefinite) {
486
+ let index = 0
487
+ while (currentOffset < ctx.buffer.length) {
488
+ const nextByte = readByte(ctx.buffer, currentOffset)
489
+ if (nextByte === 0xff) {
490
+ currentOffset++
491
+ break
492
+ }
493
+ const elementPath = `${path}[${index}]`
494
+ childPaths.push(elementPath)
495
+ const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
496
+ items.push(elementResult.value)
497
+ currentOffset += elementResult.bytesRead
498
+
499
+ // Mark element as child of this array
500
+ const elementEntry = sourceMap.find(e => e.path === elementPath)
501
+ if (elementEntry) {
502
+ elementEntry.parent = path
503
+ }
504
+
505
+ index++
506
+ }
507
+ } else {
508
+ for (let i = 0; i < length; i++) {
509
+ const elementPath = `${path}[${i}]`
510
+ childPaths.push(elementPath)
511
+ const elementResult = parseValueWithMap(ctx, currentOffset, elementPath, sourceMap)
512
+ items.push(elementResult.value)
513
+ currentOffset += elementResult.bytesRead
514
+
515
+ // Mark element as child of this array
516
+ const elementEntry = sourceMap.find(e => e.path === elementPath)
517
+ if (elementEntry) {
518
+ elementEntry.parent = path
519
+ }
520
+ }
521
+ }
522
+
523
+ const bytesRead = currentOffset - offset
524
+
525
+ // Only set children if array is non-empty
526
+ if (childPaths.length > 0 && sourceMap[arrayEntryIndex]) {
527
+ sourceMap[arrayEntryIndex].children = childPaths
528
+ }
529
+
530
+ return {
531
+ value: items,
532
+ bytesRead
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Parse map with source map tracking
538
+ */
539
+ const parseMapWithMap = (
540
+ ctx: CborContext,
541
+ offset: number,
542
+ path: string,
543
+ sourceMap: SourceMapEntry[]
544
+ ): ParseResult => {
545
+ const startOffset = offset
546
+ const initialByte = readByte(ctx.buffer, offset)
547
+ const { additionalInfo } = extractCborHeader(initialByte)
548
+
549
+ let currentOffset = offset + 1
550
+ const map = new Map()
551
+
552
+ // Determine map length
553
+ let length: number
554
+ let isIndefinite = false
555
+
556
+ if (additionalInfo < 24) {
557
+ length = additionalInfo
558
+ } else if (additionalInfo === 24) {
559
+ length = readByte(ctx.buffer, currentOffset)
560
+ currentOffset += 1
561
+ } else if (additionalInfo === 25) {
562
+ length = readUint(ctx.buffer, currentOffset, 2)
563
+ currentOffset += 2
564
+ } else if (additionalInfo === 26) {
565
+ length = readUint(ctx.buffer, currentOffset, 4)
566
+ currentOffset += 4
567
+ } else if (additionalInfo === 27) {
568
+ const bigLength = readBigUint(ctx.buffer, currentOffset, 8)
569
+ if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
570
+ throw new Error('Map length exceeds maximum safe integer')
571
+ }
572
+ length = Number(bigLength)
573
+ currentOffset += 8
574
+ } else if (additionalInfo === 31) {
575
+ isIndefinite = true
576
+ length = 0
577
+ } else {
578
+ throw new Error(`Invalid additional info: ${additionalInfo}`)
579
+ }
580
+
581
+ // Calculate header length (where content starts)
582
+ const headerEnd = currentOffset
583
+
584
+ // Add header entry for map
585
+ const mapEntryIndex = sourceMap.length
586
+ sourceMap.push({
587
+ path,
588
+ start: startOffset,
589
+ end: headerEnd,
590
+ majorType: 5,
591
+ type: isIndefinite ? 'map(indefinite)' : `map(${length})`,
592
+ isHeader: true,
593
+ headerEnd
594
+ })
595
+
596
+ // Parse map entries
597
+ const childPaths: string[] = []
598
+ const seenKeys = new Set<string>()
599
+
600
+ if (isIndefinite) {
601
+ while (currentOffset < ctx.buffer.length) {
602
+ const nextByte = readByte(ctx.buffer, currentOffset)
603
+ if (nextByte === 0xff) {
604
+ currentOffset++
605
+ break
606
+ }
607
+
608
+ // Parse key with path suffix to indicate it's a key
609
+ const keyPath = `${path}${path ? '.' : ''}#key`
610
+ const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
611
+ currentOffset += keyResult.bytesRead
612
+
613
+ // For duplicate detection and path generation, stringify the key
614
+ const keyString = keyResult.value instanceof Uint8Array
615
+ ? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
616
+ : String(keyResult.value)
617
+
618
+ // Check for duplicate keys based on dupMapKeyMode
619
+ if (seenKeys.has(keyString)) {
620
+ const mode = ctx.options?.dupMapKeyMode || 'allow'
621
+ if (mode === 'reject') {
622
+ throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
623
+ } else if (mode === 'warn') {
624
+ logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
625
+ }
626
+ }
627
+ seenKeys.add(keyString)
628
+
629
+ // Parse value
630
+ const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
631
+ childPaths.push(valuePath)
632
+ const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
633
+ map.set(keyResult.value, valueResult.value)
634
+ currentOffset += valueResult.bytesRead
635
+
636
+ // Mark value entry as child of this map
637
+ const valueEntry = sourceMap.find(e => e.path === valuePath)
638
+ if (valueEntry) {
639
+ valueEntry.parent = path
640
+ }
641
+ }
642
+ } else {
643
+ for (let i = 0; i < length; i++) {
644
+ // Parse key with path suffix to indicate it's a key
645
+ const keyPath = `${path}${path ? '.' : ''}#key${i}`
646
+ const keyResult = parseValueWithMap(ctx, currentOffset, keyPath, sourceMap)
647
+ currentOffset += keyResult.bytesRead
648
+
649
+ // For duplicate detection and path generation, stringify the key
650
+ const keyString = keyResult.value instanceof Uint8Array
651
+ ? Array.from(keyResult.value).map(b => b.toString(16).padStart(2, '0')).join('')
652
+ : String(keyResult.value)
653
+
654
+ // Check for duplicate keys based on dupMapKeyMode
655
+ if (seenKeys.has(keyString)) {
656
+ const mode = ctx.options?.dupMapKeyMode || 'allow'
657
+ if (mode === 'reject') {
658
+ throw new Error(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
659
+ } else if (mode === 'warn') {
660
+ logger.warn(`Duplicate map key detected: ${keyString} at offset ${currentOffset}`)
661
+ }
662
+ }
663
+ seenKeys.add(keyString)
664
+
665
+ // Parse value
666
+ const valuePath = path ? `${path}.${keyString}` : `.${keyString}`
667
+ childPaths.push(valuePath)
668
+ const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
669
+ map.set(keyResult.value, valueResult.value)
670
+ currentOffset += valueResult.bytesRead
671
+
672
+ // Mark value entry as child of this map
673
+ const valueEntry = sourceMap.find(e => e.path === valuePath)
674
+ if (valueEntry) {
675
+ valueEntry.parent = path
676
+ }
677
+ }
678
+ }
679
+
680
+ const bytesRead = currentOffset - offset
681
+
682
+ // Set children for the map entry
683
+ if (sourceMap[mapEntryIndex]) {
684
+ sourceMap[mapEntryIndex].children = childPaths
685
+ }
686
+
687
+ return {
688
+ value: map,
689
+ bytesRead
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Helper to parse tag number from buffer
695
+ */
696
+ const parseTagNumberHelper = (
697
+ buffer: Uint8Array,
698
+ offset: number,
699
+ ai: number
700
+ ): { tagNumber: number, bytesConsumed: number } => {
701
+ if (ai < 24) {
702
+ // Direct encoding (tags 0-23)
703
+ return { tagNumber: ai, bytesConsumed: 0 }
704
+ } else if (ai === 24) {
705
+ // 1 byte follows (tags 24-255)
706
+ const tagNumber = readByte(buffer, offset)
707
+ return { tagNumber, bytesConsumed: 1 }
708
+ } else if (ai === 25) {
709
+ // 2 bytes follow (tags 256-65535)
710
+ const tagNumber = readUint(buffer, offset, 2)
711
+ return { tagNumber, bytesConsumed: 2 }
712
+ } else if (ai === 26) {
713
+ // 4 bytes follow (tags 65536-4294967295)
714
+ const tagNumber = readUint(buffer, offset, 4)
715
+ return { tagNumber, bytesConsumed: 4 }
716
+ } else if (ai === 27) {
717
+ // 8 bytes follow (very large tag numbers)
718
+ const tagBigInt = readBigUint(buffer, offset, 8)
719
+ if (tagBigInt <= BigInt(Number.MAX_SAFE_INTEGER)) {
720
+ return { tagNumber: Number(tagBigInt), bytesConsumed: 8 }
721
+ } else {
722
+ throw new Error(`Tag number ${tagBigInt} exceeds maximum safe integer`)
723
+ }
724
+ } else if (ai >= 28 && ai <= 30) {
725
+ throw new Error(`Reserved additional info ${ai} for major type 6`)
726
+ } else {
727
+ throw new Error(`Invalid additional info ${ai} for tags`)
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Parse tag with source map tracking (RECURSIVE)
733
+ * Creates source map entries for both the tag and its nested value
734
+ */
735
+ const parseTagWithMap = (
736
+ ctx: CborContext,
737
+ offset: number,
738
+ path: string,
739
+ sourceMap: SourceMapEntry[]
740
+ ): ParseResult => {
741
+ const startOffset = offset
742
+ const initialByte = readByte(ctx.buffer, offset)
743
+ const { additionalInfo } = extractCborHeader(initialByte)
744
+
745
+ // Parse tag number
746
+ const { tagNumber, bytesConsumed } = parseTagNumberHelper(
747
+ ctx.buffer,
748
+ offset + 1,
749
+ additionalInfo
750
+ )
751
+
752
+ let currentOffset = offset + 1 + bytesConsumed
753
+ const headerEnd = currentOffset
754
+
755
+ // Add header entry for this tag
756
+ const tagEntryIndex = sourceMap.length
757
+ sourceMap.push({
758
+ path,
759
+ start: startOffset,
760
+ end: headerEnd,
761
+ majorType: 6,
762
+ type: `tag(${tagNumber})`,
763
+ isHeader: true,
764
+ headerEnd,
765
+ children: []
766
+ })
767
+
768
+ // Parse the tagged value WITH source map tracking (RECURSIVE CALL)
769
+ const valuePath = `${path}.value`
770
+ const valueResult = parseValueWithMap(ctx, currentOffset, valuePath, sourceMap)
771
+ currentOffset += valueResult.bytesRead
772
+
773
+ // Set child path for the tag
774
+ if (sourceMap[tagEntryIndex]) {
775
+ sourceMap[tagEntryIndex].children = [valuePath]
776
+ }
777
+
778
+ // Mark value entry as child of this tag
779
+ const valueEntry = sourceMap.find(e => e.path === valuePath)
780
+ if (valueEntry) {
781
+ valueEntry.parent = path
782
+ }
783
+
784
+ // Build TaggedValue object (call parseTag to get validation and plutus decoding)
785
+ const hexString = Array.from(ctx.buffer.slice(startOffset, currentOffset))
786
+ .map(b => b.toString(16).padStart(2, '0'))
787
+ .join('')
788
+ const tagResult = parseTag(hexString, ctx.options)
789
+
790
+ return {
791
+ value: tagResult.value,
792
+ bytesRead: currentOffset - startOffset
793
+ }
794
+ }
795
+
796
+ /**
797
+ * Get tag number for description
798
+ * Note: Reserved for future tag description features
799
+ */
800
+ // const getTagNumber = (buffer: Uint8Array, offset: number): number => {
801
+ // const initialByte = readByte(buffer, offset)
802
+ // const { additionalInfo } = extractCborHeader(initialByte)
803
+ //
804
+ // if (additionalInfo < 24) return additionalInfo
805
+ // if (additionalInfo === 24) return readByte(buffer, offset + 1)
806
+ // if (additionalInfo === 25) return readUint(buffer, offset + 1, 2)
807
+ // if (additionalInfo === 26) return readUint(buffer, offset + 1, 4)
808
+ // if (additionalInfo === 27) {
809
+ // const bigNum = readBigUint(buffer, offset + 1, 8)
810
+ // return bigNum <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bigNum) : -1
811
+ // }
812
+ // return -1
813
+ // }
814
+
815
+ /**
816
+ * Get simple type description
817
+ */
818
+ const getSimpleTypeDescription = (ai: number): string => {
819
+ if (ai === 20) return 'Simple: false'
820
+ if (ai === 21) return 'Simple: true'
821
+ if (ai === 22) return 'Simple: null'
822
+ if (ai === 23) return 'Simple: undefined'
823
+ if (ai === 25) return 'Float16'
824
+ if (ai === 26) return 'Float32'
825
+ if (ai === 27) return 'Float64'
826
+ if (ai < 20) return `Simple Value ${ai}`
827
+ return 'Simple Value'
828
+ }
829
+
830
+ /**
831
+ * Parses a CBOR Sequence (RFC 8742)
832
+ * A CBOR sequence is a concatenation of zero or more CBOR data items
833
+ *
834
+ * @param hexString - CBOR sequence data as hex string
835
+ * @param options - Parser options (optional)
836
+ * @returns Array of parsed CBOR values
837
+ *
838
+ * @example
839
+ * ```ts
840
+ * const { parseSequence } = useCborParser()
841
+ * parseSequence('010203') // [1, 2, 3] - three separate integers
842
+ * parseSequence('83010203 05') // [[1,2,3], 5] - array followed by integer
843
+ * parseSequence('') // [] - empty sequence
844
+ * ```
845
+ */
846
+ const parseSequence = (hexString: string, options?: ParseOptions): CborValue[] => {
847
+ const cleanHex = hexString.replace(/\s+/g, '')
848
+
849
+ // Empty sequence is valid
850
+ if (!cleanHex || cleanHex.length === 0) {
851
+ return []
852
+ }
853
+
854
+ if (cleanHex.length % 2 !== 0) {
855
+ throw new Error('Hex string must have even length')
856
+ }
857
+
858
+ if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
859
+ throw new Error(`Invalid hex character in: ${cleanHex}`)
860
+ }
861
+
862
+ const mergedOptions = mergeOptions(options)
863
+ const buffer = hexToBytes(cleanHex)
864
+ const results: CborValue[] = []
865
+ let offset = 0
866
+
867
+ while (offset < buffer.length) {
868
+ // Check for break code outside indefinite context (invalid in sequence)
869
+ const byte = readByte(buffer, offset)
870
+ if (byte === 0xff) {
871
+ throw new Error(`Unexpected break code (0xff) at offset ${offset} - not inside indefinite-length item`)
872
+ }
873
+
874
+ // Parse next item from remaining hex
875
+ const remainingHex = Array.from(buffer.slice(offset))
876
+ .map(b => b.toString(16).padStart(2, '0'))
877
+ .join('')
878
+
879
+ const result = parse(remainingHex, mergedOptions)
880
+ results.push(result.value)
881
+ offset += result.bytesRead
882
+ }
883
+
884
+ return results
885
+ }
886
+
887
+ /**
888
+ * Parses a CBOR Sequence with source maps for each item
889
+ *
890
+ * @param hexString - CBOR sequence data as hex string
891
+ * @param options - Parser options (optional)
892
+ * @returns Object with values array and sourceMaps array
893
+ */
894
+ const parseSequenceWithSourceMap = (hexString: string, options?: ParseOptions): {
895
+ values: CborValue[]
896
+ sourceMaps: SourceMapEntry[][]
897
+ } => {
898
+ const cleanHex = hexString.replace(/\s+/g, '')
899
+
900
+ if (!cleanHex || cleanHex.length === 0) {
901
+ return { values: [], sourceMaps: [] }
902
+ }
903
+
904
+ if (cleanHex.length % 2 !== 0) {
905
+ throw new Error('Hex string must have even length')
906
+ }
907
+
908
+ if (!/^[0-9a-fA-F]+$/.test(cleanHex)) {
909
+ throw new Error(`Invalid hex character in: ${cleanHex}`)
910
+ }
911
+
912
+ const mergedOptions = mergeOptions(options)
913
+ const buffer = hexToBytes(cleanHex)
914
+ const values: CborValue[] = []
915
+ const sourceMaps: SourceMapEntry[][] = []
916
+ let offset = 0
917
+
918
+ while (offset < buffer.length) {
919
+ const byte = readByte(buffer, offset)
920
+ if (byte === 0xff) {
921
+ throw new Error(`Unexpected break code (0xff) at offset ${offset}`)
922
+ }
923
+
924
+ const remainingHex = Array.from(buffer.slice(offset))
925
+ .map(b => b.toString(16).padStart(2, '0'))
926
+ .join('')
927
+
928
+ const result = parseWithSourceMap(remainingHex, mergedOptions)
929
+
930
+ // Adjust source map offsets to account for sequence position
931
+ const adjustedSourceMap = result.sourceMap.map(entry => ({
932
+ ...entry,
933
+ start: entry.start + offset,
934
+ end: entry.end + offset
935
+ }))
936
+
937
+ values.push(result.value)
938
+ sourceMaps.push(adjustedSourceMap)
939
+ offset += result.bytesRead
940
+ }
941
+
942
+ return { values, sourceMaps }
943
+ }
944
+
945
+ return {
946
+ parse,
947
+ parseWithSourceMap,
948
+ parseSequence,
949
+ parseSequenceWithSourceMap
950
+ }
951
+ }