@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,739 @@
1
+ /**
2
+ * CBOR Tag Parser Composable
3
+ * Handles Major Type 6 (Semantic Tags)
4
+ * Supports standard tags (0-5), encoding hints (21-36), self-describe (55799), and Cardano tags
5
+ */
6
+
7
+ import type { ParseResult, CborValue, TaggedValue, CborMap, ParseOptions, PlutusConstr } from '../types'
8
+ import { INDEFINITE_SYMBOL } from '../types'
9
+ import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, hasDuplicates } from '../utils'
10
+ import { useCborInteger } from './useCborInteger'
11
+ import { useCborString } from './useCborString'
12
+ import { useCborFloat } from './useCborFloat'
13
+
14
+ /**
15
+ * Composable for parsing CBOR tags (Major Type 6)
16
+ *
17
+ * @returns Object with parseTag and parse functions
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const { parseTag } = useCborTag()
22
+ * const result = parseTag('c11a514b67b0') // 1(1363896240) - epoch timestamp
23
+ * ```
24
+ */
25
+ export function useCborTag() {
26
+ const { parseInteger } = useCborInteger()
27
+ const { parseByteString, parseTextString } = useCborString()
28
+ const { parseFloat, parseSimple } = useCborFloat()
29
+
30
+ /**
31
+ * Internal parser dispatcher for CBOR items
32
+ * Handles recursive parsing of tagged values
33
+ *
34
+ * @param buffer - Data buffer
35
+ * @param offset - Current offset
36
+ * @param options - Parser options
37
+ * @param tagDepth - Current tag nesting depth (for limit checking)
38
+ * @returns Parsed value and bytes consumed
39
+ */
40
+ const parseItem = (buffer: Uint8Array, offset: number, options?: ParseOptions, tagDepth: number = 0): ParseResult => {
41
+ if (offset >= buffer.length) {
42
+ throw new Error(`Unexpected end of buffer at offset ${offset}`)
43
+ }
44
+
45
+ const initialByte = readByte(buffer, offset)
46
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
47
+
48
+ switch (majorType) {
49
+ case 0: // Unsigned integer
50
+ case 1: // Negative integer
51
+ {
52
+ // Create a hex string from the buffer starting at offset
53
+ const intHex = Array.from(buffer.slice(offset))
54
+ .map(b => b.toString(16).padStart(2, '0'))
55
+ .join('')
56
+ const result = parseInteger(intHex, options)
57
+ return { value: result.value, bytesRead: result.bytesRead }
58
+ }
59
+
60
+ case 2: // Byte string
61
+ return parseByteString(buffer, offset, options)
62
+
63
+ case 3: // Text string
64
+ return parseTextString(buffer, offset, options)
65
+
66
+ case 4: // Array
67
+ return parseArrayInternal(buffer, offset, options)
68
+
69
+ case 5: // Map
70
+ return parseMapInternal(buffer, offset, options)
71
+
72
+ case 6: // Tag (recursive)
73
+ return parseTagFromBuffer(buffer, offset, options, tagDepth)
74
+
75
+ case 7: // Simple/Float
76
+ {
77
+ const simpleHex = Array.from(buffer.slice(offset))
78
+ .map(b => b.toString(16).padStart(2, '0'))
79
+ .join('')
80
+
81
+ // Try to parse as simple value or float
82
+ if (additionalInfo >= 25 && additionalInfo <= 27) {
83
+ // Float16, Float32, Float64
84
+ const result = parseFloat(simpleHex, options)
85
+ return { value: result.value, bytesRead: result.bytesRead }
86
+ } else {
87
+ // Simple value (boolean, null, undefined, etc.)
88
+ const result = parseSimple(simpleHex, options)
89
+ return { value: result.value, bytesRead: result.bytesRead }
90
+ }
91
+ }
92
+
93
+ default:
94
+ throw new Error(`Unknown major type: ${majorType}`)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Internal array parser that uses the tag-aware parseItem
100
+ */
101
+ const parseArrayInternal = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
102
+ const initialByte = readByte(buffer, offset)
103
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
104
+
105
+ if (majorType !== 4) {
106
+ throw new Error(`Expected major type 4 (array), got ${majorType}`)
107
+ }
108
+
109
+ // Parse length
110
+ let length: number | null
111
+ let bytesConsumed: number
112
+
113
+ if (additionalInfo < 24) {
114
+ length = additionalInfo
115
+ bytesConsumed = 0
116
+ } else if (additionalInfo === 24) {
117
+ length = readByte(buffer, offset + 1)
118
+ bytesConsumed = 1
119
+ } else if (additionalInfo === 25) {
120
+ length = readUint(buffer, offset + 1, 2)
121
+ bytesConsumed = 2
122
+ } else if (additionalInfo === 26) {
123
+ length = readUint(buffer, offset + 1, 4)
124
+ bytesConsumed = 4
125
+ } else if (additionalInfo === 27) {
126
+ const lengthBigInt = readBigUint(buffer, offset + 1, 8)
127
+ if (lengthBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
128
+ throw new Error(`Array length ${lengthBigInt} exceeds maximum safe integer`)
129
+ }
130
+ length = Number(lengthBigInt)
131
+ bytesConsumed = 8
132
+ } else if (additionalInfo === 31) {
133
+ length = null // Indefinite
134
+ bytesConsumed = 0
135
+ } else {
136
+ throw new Error(`Invalid additional info for array: ${additionalInfo}`)
137
+ }
138
+
139
+ let currentOffset = offset + 1 + bytesConsumed
140
+ const items: CborValue[] = []
141
+
142
+ if (length === null) {
143
+ // Indefinite-length array
144
+ while (currentOffset < buffer.length) {
145
+ const nextByte = readByte(buffer, currentOffset)
146
+ if (nextByte === 0xff) {
147
+ currentOffset++
148
+ break
149
+ }
150
+ const itemResult = parseItem(buffer, currentOffset, options)
151
+ items.push(itemResult.value)
152
+ currentOffset += itemResult.bytesRead
153
+ }
154
+
155
+ // Mark as indefinite-length for round-trip preservation
156
+ ;(items as any)[INDEFINITE_SYMBOL] = true
157
+ } else {
158
+ // Definite-length array
159
+ for (let i = 0; i < length; i++) {
160
+ const itemResult = parseItem(buffer, currentOffset, options)
161
+ items.push(itemResult.value)
162
+ currentOffset += itemResult.bytesRead
163
+ }
164
+ }
165
+
166
+ return {
167
+ value: items,
168
+ bytesRead: currentOffset - offset
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Internal map parser that uses the tag-aware parseItem
174
+ */
175
+ const parseMapInternal = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
176
+ const initialByte = readByte(buffer, offset)
177
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
178
+
179
+ if (majorType !== 5) {
180
+ throw new Error(`Expected major type 5 (map), got ${majorType}`)
181
+ }
182
+
183
+ // Parse length
184
+ let length: number | null
185
+ let bytesConsumed: number
186
+
187
+ if (additionalInfo < 24) {
188
+ length = additionalInfo
189
+ bytesConsumed = 0
190
+ } else if (additionalInfo === 24) {
191
+ length = readByte(buffer, offset + 1)
192
+ bytesConsumed = 1
193
+ } else if (additionalInfo === 25) {
194
+ length = readUint(buffer, offset + 1, 2)
195
+ bytesConsumed = 2
196
+ } else if (additionalInfo === 26) {
197
+ length = readUint(buffer, offset + 1, 4)
198
+ bytesConsumed = 4
199
+ } else if (additionalInfo === 27) {
200
+ const lengthBigInt = readBigUint(buffer, offset + 1, 8)
201
+ if (lengthBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
202
+ throw new Error(`Map length ${lengthBigInt} exceeds maximum safe integer`)
203
+ }
204
+ length = Number(lengthBigInt)
205
+ bytesConsumed = 8
206
+ } else if (additionalInfo === 31) {
207
+ length = null // Indefinite
208
+ bytesConsumed = 0
209
+ } else {
210
+ throw new Error(`Invalid additional info for map: ${additionalInfo}`)
211
+ }
212
+
213
+ let currentOffset = offset + 1 + bytesConsumed
214
+ const map: CborMap = new Map()
215
+
216
+ if (length === null) {
217
+ // Indefinite-length map
218
+ while (currentOffset < buffer.length) {
219
+ const nextByte = readByte(buffer, currentOffset)
220
+ if (nextByte === 0xff) {
221
+ currentOffset++
222
+ break
223
+ }
224
+ const keyResult = parseItem(buffer, currentOffset, options)
225
+ currentOffset += keyResult.bytesRead
226
+
227
+ const valueResult = parseItem(buffer, currentOffset, options)
228
+ currentOffset += valueResult.bytesRead
229
+
230
+ // Store with original key type (not string!)
231
+ map.set(keyResult.value, valueResult.value)
232
+ }
233
+
234
+ // Mark as indefinite-length for round-trip preservation
235
+ ;(map as any)[INDEFINITE_SYMBOL] = true
236
+ } else {
237
+ // Definite-length map
238
+ for (let i = 0; i < length; i++) {
239
+ const keyResult = parseItem(buffer, currentOffset, options)
240
+ currentOffset += keyResult.bytesRead
241
+
242
+ const valueResult = parseItem(buffer, currentOffset, options)
243
+ currentOffset += valueResult.bytesRead
244
+
245
+ // Store with original key type (not string!)
246
+ map.set(keyResult.value, valueResult.value)
247
+ }
248
+ }
249
+
250
+ return {
251
+ value: map,
252
+ bytesRead: currentOffset - offset
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Parses a tag number from the buffer
258
+ *
259
+ * @param buffer - Data buffer
260
+ * @param offset - Current offset (at initial byte)
261
+ * @param ai - Additional info field
262
+ * @returns Tag number and bytes consumed for the tag number
263
+ */
264
+ const parseTagNumber = (
265
+ buffer: Uint8Array,
266
+ offset: number,
267
+ ai: number
268
+ ): { tagNumber: number, bytesConsumed: number } => {
269
+ if (ai < 24) {
270
+ // Direct encoding (tags 0-23)
271
+ return { tagNumber: ai, bytesConsumed: 0 }
272
+ } else if (ai === 24) {
273
+ // 1 byte follows (tags 24-255)
274
+ const tagNumber = readByte(buffer, offset)
275
+ return { tagNumber, bytesConsumed: 1 }
276
+ } else if (ai === 25) {
277
+ // 2 bytes follow (tags 256-65535)
278
+ const tagNumber = readUint(buffer, offset, 2)
279
+ return { tagNumber, bytesConsumed: 2 }
280
+ } else if (ai === 26) {
281
+ // 4 bytes follow (tags 65536-4294967295)
282
+ const tagNumber = readUint(buffer, offset, 4)
283
+ return { tagNumber, bytesConsumed: 4 }
284
+ } else if (ai === 27) {
285
+ // 8 bytes follow (very large tag numbers)
286
+ const tagBigInt = readBigUint(buffer, offset, 8)
287
+
288
+ // Convert to number if it fits
289
+ if (tagBigInt <= BigInt(Number.MAX_SAFE_INTEGER)) {
290
+ return { tagNumber: Number(tagBigInt), bytesConsumed: 8 }
291
+ } else {
292
+ throw new Error(`Tag number ${tagBigInt} exceeds maximum safe integer`)
293
+ }
294
+ } else if (ai >= 28 && ai <= 30) {
295
+ throw new Error(`Reserved additional info ${ai} for major type 6`)
296
+ } else {
297
+ throw new Error(`Invalid additional info ${ai} for tags`)
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Validates semantic constraints for specific CBOR tags
303
+ *
304
+ * @param tagNumber - The tag number
305
+ * @param value - The tagged value
306
+ * @param options - Parser options
307
+ * @throws Error if validation fails
308
+ */
309
+ /**
310
+ * Validates RFC 3339 date/time string format
311
+ */
312
+ const isValidRfc3339 = (dateStr: string): boolean => {
313
+ // RFC 3339 format: YYYY-MM-DDTHH:MM:SS[.fraction][Z|+/-HH:MM]
314
+ const rfc3339Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/i
315
+ return rfc3339Regex.test(dateStr)
316
+ }
317
+
318
+ /**
319
+ * Validates URI format (basic check for scheme)
320
+ */
321
+ const isValidUri = (uri: string): boolean => {
322
+ // Basic URI validation: must have scheme followed by colon
323
+ // RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
324
+ const uriRegex = /^[a-zA-Z][a-zA-Z0-9+.-]*:/
325
+ return uriRegex.test(uri)
326
+ }
327
+
328
+ /**
329
+ * Checks if a value is a text string (CborTextString or plain string)
330
+ */
331
+ const isTextString = (value: CborValue): value is string => {
332
+ if (typeof value === 'string') return true
333
+ if (value && typeof value === 'object' && 'type' in value && (value as any).type === 'cbor-text-string') {
334
+ return true
335
+ }
336
+ return false
337
+ }
338
+
339
+ /**
340
+ * Gets string value from CborTextString or plain string
341
+ */
342
+ const getTextStringValue = (value: CborValue): string => {
343
+ if (typeof value === 'string') return value
344
+ if (value && typeof value === 'object' && 'text' in value) {
345
+ return (value as any).text
346
+ }
347
+ return String(value)
348
+ }
349
+
350
+ const validateTagSemantics = (tagNumber: number, value: CborValue, options?: ParseOptions): void => {
351
+ // Check different validation types separately
352
+ // Standard tag semantics (Tags 0, 1, 4, 5, 32, 35, 36, 258)
353
+ const shouldValidateStandard = options?.strict || options?.validateTagSemantics
354
+
355
+ switch (tagNumber) {
356
+ case 0: // Date/Time String (RFC 3339)
357
+ if (!shouldValidateStandard) break
358
+
359
+ if (!isTextString(value)) {
360
+ throw new Error(`Tag 0 (date/time string) must contain a text string, got ${typeof value}`)
361
+ }
362
+ const dateStr = getTextStringValue(value)
363
+ if (!isValidRfc3339(dateStr)) {
364
+ throw new Error(`Tag 0 (date/time string) contains invalid RFC 3339 date format: "${dateStr}"`)
365
+ }
366
+ break
367
+
368
+ case 1: // Epoch-Based Date/Time
369
+ if (!shouldValidateStandard) break
370
+
371
+ if (typeof value !== 'number' && typeof value !== 'bigint') {
372
+ throw new Error(`Tag 1 (epoch time) must contain a number (integer or float), got ${typeof value}`)
373
+ }
374
+ break
375
+
376
+ case 4: // Decimal Fraction
377
+ if (!shouldValidateStandard) break
378
+
379
+ if (!Array.isArray(value)) {
380
+ throw new Error(`Tag 4 (decimal fraction) must contain an array, got ${typeof value}`)
381
+ }
382
+ if (value.length !== 2) {
383
+ throw new Error(`Tag 4 (decimal fraction) array must have exactly 2 elements [exponent, mantissa], got ${value.length}`)
384
+ }
385
+ if (typeof value[0] !== 'number' && typeof value[0] !== 'bigint') {
386
+ throw new Error(`Tag 4 (decimal fraction) exponent must be an integer, got ${typeof value[0]}`)
387
+ }
388
+ if (typeof value[1] !== 'number' && typeof value[1] !== 'bigint') {
389
+ throw new Error(`Tag 4 (decimal fraction) mantissa must be an integer, got ${typeof value[1]}`)
390
+ }
391
+ break
392
+
393
+ case 5: // Bigfloat
394
+ if (!shouldValidateStandard) break
395
+
396
+ if (!Array.isArray(value)) {
397
+ throw new Error(`Tag 5 (bigfloat) must contain an array, got ${typeof value}`)
398
+ }
399
+ if (value.length !== 2) {
400
+ throw new Error(`Tag 5 (bigfloat) array must have exactly 2 elements [exponent, mantissa], got ${value.length}`)
401
+ }
402
+ if (typeof value[0] !== 'number' && typeof value[0] !== 'bigint') {
403
+ throw new Error(`Tag 5 (bigfloat) exponent must be an integer, got ${typeof value[0]}`)
404
+ }
405
+ if (typeof value[1] !== 'number' && typeof value[1] !== 'bigint') {
406
+ throw new Error(`Tag 5 (bigfloat) mantissa must be an integer, got ${typeof value[1]}`)
407
+ }
408
+ break
409
+
410
+ case 32: // URI (RFC 3986)
411
+ if (!shouldValidateStandard) break
412
+
413
+ if (!isTextString(value)) {
414
+ throw new Error(`Tag 32 (URI) must contain a text string, got ${typeof value}`)
415
+ }
416
+ const uriStr = getTextStringValue(value)
417
+ if (!isValidUri(uriStr)) {
418
+ throw new Error(`Tag 32 (URI) contains invalid URI format (missing scheme): "${uriStr}"`)
419
+ }
420
+ break
421
+
422
+ case 33: // base64url without padding
423
+ case 34: // base64 without padding
424
+ if (!shouldValidateStandard) break
425
+
426
+ if (!isTextString(value)) {
427
+ throw new Error(`Tag ${tagNumber} (base64${tagNumber === 33 ? 'url' : ''}) must contain a text string, got ${typeof value}`)
428
+ }
429
+ break
430
+
431
+ case 35: // Regular Expression
432
+ if (!shouldValidateStandard) break
433
+
434
+ if (!isTextString(value)) {
435
+ throw new Error(`Tag 35 (regexp) must contain a text string, got ${typeof value}`)
436
+ }
437
+ break
438
+
439
+ case 36: // MIME Message
440
+ if (!shouldValidateStandard) break
441
+
442
+ if (!isTextString(value)) {
443
+ throw new Error(`Tag 36 (MIME message) must contain a text string, got ${typeof value}`)
444
+ }
445
+ break
446
+
447
+ case 258: // Mathematical Finite Set
448
+ {
449
+ // Validate set uniqueness if enabled
450
+ const shouldValidateUniqueness = options?.strict || options?.validateSetUniqueness
451
+
452
+ if (shouldValidateUniqueness) {
453
+ if (!Array.isArray(value)) {
454
+ throw new Error(`Tag 258 (set) must contain an array, got ${typeof value}`)
455
+ }
456
+
457
+ if (hasDuplicates(value)) {
458
+ throw new Error(
459
+ `Tag 258 (set) contains duplicate items. ` +
460
+ `Sets must contain only unique values (RFC 8949). ` +
461
+ `Use validateSetUniqueness: false to allow duplicates.`
462
+ )
463
+ }
464
+ }
465
+ }
466
+ break
467
+
468
+ // Plutus Constructor tags
469
+ case 102: // Alternative Plutus Constructor
470
+ validatePlutusAlternativeConstructor(value, options)
471
+ break
472
+
473
+ // No validation needed for other tags (yet)
474
+ default:
475
+ // Plutus Compact Constructors (121-127)
476
+ if (tagNumber >= 121 && tagNumber <= 127) {
477
+ validatePlutusCompactConstructor(tagNumber, value, options)
478
+ }
479
+ // Plutus Extended Constructors (1280-1400)
480
+ else if (tagNumber >= 1280 && tagNumber <= 1400) {
481
+ validatePlutusExtendedConstructor(tagNumber, value, options)
482
+ }
483
+ break
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Validates Plutus compact constructor (Tags 121-127)
489
+ *
490
+ * @param tagNumber - Tag number (121-127)
491
+ * @param value - Tagged value (should be array)
492
+ * @param options - Parser options
493
+ */
494
+ const validatePlutusCompactConstructor = (tagNumber: number, value: CborValue, options?: ParseOptions): void => {
495
+ const shouldValidate = options?.strict || options?.validatePlutusSemantics
496
+
497
+ if (!shouldValidate) {
498
+ return
499
+ }
500
+
501
+ if (!Array.isArray(value)) {
502
+ throw new Error(
503
+ `Plutus constructor tag ${tagNumber} must contain an array, got ${typeof value}`
504
+ )
505
+ }
506
+
507
+ // Tags 121-127 encode constructor index 0-6
508
+ // Per Cardano CDDL: constr<tag> = #6.tag([* any])
509
+ // The tag number encodes the constructor index, NOT the arity
510
+ // Any number of fields (0 or more) is valid
511
+ }
512
+
513
+ /**
514
+ * Validates Plutus alternative constructor (Tag 102)
515
+ *
516
+ * @param value - Tagged value (should be [uint, array])
517
+ * @param options - Parser options
518
+ */
519
+ const validatePlutusAlternativeConstructor = (value: CborValue, options?: ParseOptions): void => {
520
+ const shouldValidate = options?.strict || options?.validatePlutusSemantics
521
+
522
+ if (!shouldValidate) {
523
+ return
524
+ }
525
+
526
+ if (!Array.isArray(value)) {
527
+ throw new Error(
528
+ `Plutus alternative constructor (tag 102) must contain an array, got ${typeof value}`
529
+ )
530
+ }
531
+
532
+ if (value.length !== 2) {
533
+ throw new Error(
534
+ `Plutus alternative constructor (tag 102) must be [constructor_index, fields], got array of length ${value.length}`
535
+ )
536
+ }
537
+
538
+ const constructorIndex = value[0]
539
+ const fields = value[1]
540
+
541
+ if (typeof constructorIndex !== 'number' || constructorIndex < 0 || !Number.isInteger(constructorIndex)) {
542
+ throw new Error(
543
+ `Plutus constructor index must be non-negative integer, got ${typeof constructorIndex}`
544
+ )
545
+ }
546
+
547
+ if (!Array.isArray(fields)) {
548
+ throw new Error(
549
+ `Plutus constructor fields must be an array, got ${typeof fields}`
550
+ )
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Validates Plutus extended constructor (Tags 1280-1400)
556
+ *
557
+ * @param tagNumber - Tag number (1280-1400)
558
+ * @param value - Tagged value (should be array)
559
+ * @param options - Parser options
560
+ */
561
+ const validatePlutusExtendedConstructor = (tagNumber: number, value: CborValue, options?: ParseOptions): void => {
562
+ const shouldValidate = options?.strict || options?.validatePlutusSemantics
563
+
564
+ if (!shouldValidate) {
565
+ return
566
+ }
567
+
568
+ if (!Array.isArray(value)) {
569
+ throw new Error(
570
+ `Plutus constructor tag ${tagNumber} must contain an array, got ${typeof value}`
571
+ )
572
+ }
573
+
574
+ const constructorIndex = (tagNumber - 1280) + 7
575
+
576
+ // Extended constructors can have any number of fields (0 to unlimited)
577
+ // Constructor index is 7-127
578
+ if (constructorIndex < 7 || constructorIndex > 127) {
579
+ throw new Error(
580
+ `Plutus extended constructor tag ${tagNumber} produces invalid constructor index ${constructorIndex} ` +
581
+ `(expected 7-127)`
582
+ )
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Decodes a Plutus constructor from a tag
588
+ *
589
+ * @param tagNumber - CBOR tag number
590
+ * @param value - Tagged value
591
+ * @returns PlutusConstr or null if not a Plutus constructor
592
+ */
593
+ const decodePlutusConstructor = (tagNumber: number, value: CborValue): PlutusConstr | null => {
594
+ // Tag 102: Alternative constructor [index, fields]
595
+ if (tagNumber === 102) {
596
+ if (!Array.isArray(value) || value.length !== 2) {
597
+ return null
598
+ }
599
+ const [constructorIndex, fields] = value
600
+ if (typeof constructorIndex !== 'number' || !Array.isArray(fields)) {
601
+ return null
602
+ }
603
+ return {
604
+ constructor: constructorIndex,
605
+ fields: fields as any[]
606
+ }
607
+ }
608
+
609
+ // Tags 121-127: Compact constructors
610
+ if (tagNumber >= 121 && tagNumber <= 127) {
611
+ if (!Array.isArray(value)) {
612
+ return null
613
+ }
614
+ const constructorIndex = tagNumber - 121
615
+ return {
616
+ constructor: constructorIndex,
617
+ fields: value as any[]
618
+ }
619
+ }
620
+
621
+ // Tags 1280-1400: Extended constructors
622
+ if (tagNumber >= 1280 && tagNumber <= 1400) {
623
+ if (!Array.isArray(value)) {
624
+ return null
625
+ }
626
+ const constructorIndex = (tagNumber - 1280) + 7
627
+ return {
628
+ constructor: constructorIndex,
629
+ fields: value as any[]
630
+ }
631
+ }
632
+
633
+ return null
634
+ }
635
+
636
+ /**
637
+ * Internal tag parser that works with buffers
638
+ *
639
+ * @param buffer - Data buffer
640
+ * @param offset - Current offset
641
+ * @param options - Parser options
642
+ * @returns Parsed tagged value and bytes read
643
+ */
644
+ const parseTagFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions, tagDepth: number = 0): ParseResult => {
645
+ const initialByte = readByte(buffer, offset)
646
+ const { majorType, additionalInfo } = extractCborHeader(initialByte)
647
+
648
+ if (majorType !== 6) {
649
+ throw new Error(`Expected major type 6 (tag), got ${majorType}`)
650
+ }
651
+
652
+ // Check tag nesting depth limit (RUSTSEC-2019-0025 mitigation)
653
+ const maxTagDepth = options?.limits?.maxTagDepth ?? 64
654
+ if (tagDepth >= maxTagDepth) {
655
+ throw new Error(`Tag nesting depth ${tagDepth} exceeds limit of ${maxTagDepth}`)
656
+ }
657
+
658
+ // Parse the tag number
659
+ const { tagNumber, bytesConsumed } = parseTagNumber(buffer, offset + 1, additionalInfo)
660
+ let currentOffset = offset + 1 + bytesConsumed
661
+
662
+ // Parse the tagged value (recursively)
663
+ if (currentOffset >= buffer.length) {
664
+ throw new Error(`Unexpected end of buffer after tag ${tagNumber}`)
665
+ }
666
+
667
+ const valueResult = parseItem(buffer, currentOffset, options, tagDepth + 1)
668
+ currentOffset += valueResult.bytesRead
669
+
670
+ // Validate bignum size limits for tags 2 and 3 (CVE-2020-28491 mitigation)
671
+ if ((tagNumber === 2 || tagNumber === 3) && valueResult.value instanceof Uint8Array) {
672
+ const maxBignumBytes = options?.limits?.maxBignumBytes ?? 1024
673
+ if (valueResult.value.length > maxBignumBytes) {
674
+ throw new Error(
675
+ `Bignum (tag ${tagNumber}) size ${valueResult.value.length} bytes exceeds limit of ${maxBignumBytes} bytes`
676
+ )
677
+ }
678
+
679
+ // Convert bignum bytes to BigInt, then to decimal string
680
+ // This provides the expected format for test compatibility
681
+ const bytes = valueResult.value
682
+ let bigintValue = 0n
683
+
684
+ // Convert bytes to BigInt (big-endian)
685
+ for (let i = 0; i < bytes.length; i++) {
686
+ bigintValue = (bigintValue << 8n) | BigInt(bytes[i]!)
687
+ }
688
+
689
+ // Tag 2: Positive bignum - return as decimal string
690
+ // Tag 3: Negative bignum - apply formula: -1 - n
691
+ if (tagNumber === 2) {
692
+ valueResult.value = bigintValue
693
+ } else if (tagNumber === 3) {
694
+ valueResult.value = -1n - bigintValue
695
+ }
696
+ }
697
+
698
+ // Validate semantic constraints for specific tags
699
+ validateTagSemantics(tagNumber, valueResult.value, options)
700
+
701
+ // Decode Plutus constructor if applicable
702
+ const plutusConstr = decodePlutusConstructor(tagNumber, valueResult.value)
703
+
704
+ const taggedValue: TaggedValue = {
705
+ tag: tagNumber,
706
+ value: valueResult.value,
707
+ ...(plutusConstr && { plutus: plutusConstr })
708
+ }
709
+
710
+ return {
711
+ value: taggedValue,
712
+ bytesRead: currentOffset - offset
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Parses CBOR tag (Major Type 6) from hex string
718
+ *
719
+ * @param hexString - CBOR hex string
720
+ * @param options - Parser options (optional)
721
+ * @returns Parsed tagged value and bytes read
722
+ */
723
+ const parseTag = (hexString: string, options?: ParseOptions): ParseResult => {
724
+ // Remove spaces from hex string
725
+ const cleanHex = hexString.replace(/\s+/g, '')
726
+ const buffer = hexToBytes(cleanHex)
727
+ return parseTagFromBuffer(buffer, 0, options)
728
+ }
729
+
730
+ /**
731
+ * Alias for parseTag (for consistency with other composables)
732
+ */
733
+ const parse = parseTag
734
+
735
+ return {
736
+ parseTag,
737
+ parse
738
+ }
739
+ }