@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.
- package/CHANGELOG.md +64 -0
- package/LICENSE +674 -0
- package/README.md +345 -0
- package/dist/chunk-2FUTHZQQ.cjs +755 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +1 -0
- package/dist/chunk-2HBCILJS.cjs +2034 -0
- package/dist/chunk-2HBCILJS.cjs.map +1 -0
- package/dist/chunk-7CFYWHS6.js +742 -0
- package/dist/chunk-7CFYWHS6.js.map +1 -0
- package/dist/chunk-PD72MVTX.cjs +160 -0
- package/dist/chunk-PD72MVTX.cjs.map +1 -0
- package/dist/chunk-ZDZ2B5PE.js +149 -0
- package/dist/chunk-ZDZ2B5PE.js.map +1 -0
- package/dist/chunk-ZRPJUEIZ.js +2020 -0
- package/dist/chunk-ZRPJUEIZ.js.map +1 -0
- package/dist/encoder/index.cjs +57 -0
- package/dist/encoder/index.cjs.map +1 -0
- package/dist/encoder/index.d.cts +72 -0
- package/dist/encoder/index.d.ts +72 -0
- package/dist/encoder/index.js +4 -0
- package/dist/encoder/index.js.map +1 -0
- package/dist/index.cjs +606 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +494 -0
- package/dist/index.d.ts +494 -0
- package/dist/index.js +523 -0
- package/dist/index.js.map +1 -0
- package/dist/metafile-cjs.json +1 -0
- package/dist/metafile-esm.json +1 -0
- package/dist/parser/index.cjs +85 -0
- package/dist/parser/index.cjs.map +1 -0
- package/dist/parser/index.d.cts +72 -0
- package/dist/parser/index.d.ts +72 -0
- package/dist/parser/index.js +4 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/types-DvNlfbKB.d.cts +301 -0
- package/dist/types-DvNlfbKB.d.ts +301 -0
- package/dist/useCborSimpleEncoder-ButVU988.d.cts +268 -0
- package/dist/useCborSimpleEncoder-TVxzNJ_9.d.ts +268 -0
- package/dist/useCborTag-B_iaShG6.d.ts +142 -0
- package/dist/useCborTag-BfTIV8HM.d.cts +142 -0
- package/package.json +102 -0
- package/src/__tests__/public-api.test.ts +326 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +331 -0
- package/src/encoder/__tests__/cbor-integer-encoder.test.ts +283 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +224 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +345 -0
- package/src/encoder/__tests__/cbor-tag-encoder.test.ts +565 -0
- package/src/encoder/composables/#useCborTagEncoder.ts# +158 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +424 -0
- package/src/encoder/composables/useCborEncoder.ts +203 -0
- package/src/encoder/composables/useCborIntegerEncoder.ts +188 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +266 -0
- package/src/encoder/composables/useCborStringEncoder.ts +266 -0
- package/src/encoder/composables/useCborTagEncoder.ts +158 -0
- package/src/encoder/index.ts +35 -0
- package/src/encoder/types.ts +88 -0
- package/src/encoder/utils.ts +80 -0
- package/src/index.ts +434 -0
- package/src/parser/__tests__/ast-tree-structure.test.ts +311 -0
- package/src/parser/__tests__/cbor-collection-errors.test.ts +296 -0
- package/src/parser/__tests__/cbor-collection.test.ts +369 -0
- package/src/parser/__tests__/cbor-deterministic-encoding.test.ts +432 -0
- package/src/parser/__tests__/cbor-diagnostic.test.ts +333 -0
- package/src/parser/__tests__/cbor-duplicate-keys.test.ts +235 -0
- package/src/parser/__tests__/cbor-float-errors.test.ts +222 -0
- package/src/parser/__tests__/cbor-float.test.ts +502 -0
- package/src/parser/__tests__/cbor-integer-errors.test.ts +139 -0
- package/src/parser/__tests__/cbor-integer.test.ts +200 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +403 -0
- package/src/parser/__tests__/cbor-parser-errors.test.ts +126 -0
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +503 -0
- package/src/parser/__tests__/cbor-sequences.test.ts +150 -0
- package/src/parser/__tests__/cbor-source-map.test.ts +321 -0
- package/src/parser/__tests__/cbor-standard-tags.test.ts +340 -0
- package/src/parser/__tests__/cbor-string-errors.test.ts +227 -0
- package/src/parser/__tests__/cbor-string.test.ts +224 -0
- package/src/parser/__tests__/cbor-tag-advanced.test.ts +500 -0
- package/src/parser/__tests__/cbor-tag-errors.test.ts +447 -0
- package/src/parser/__tests__/cbor-tag-source-map.test.ts +360 -0
- package/src/parser/__tests__/cbor-tag.test.ts +684 -0
- package/src/parser/__tests__/extreme-edge-cases.test.ts +146 -0
- package/src/parser/__tests__/pathBuilder.test.ts +256 -0
- package/src/parser/__tests__/rfc-test-vectors.test.ts +607 -0
- package/src/parser/__tests__/security-limits.test.ts +248 -0
- package/src/parser/__tests__/utils-errors.test.ts +127 -0
- package/src/parser/composables/useCborCollection.ts +509 -0
- package/src/parser/composables/useCborDiagnostic.ts +381 -0
- package/src/parser/composables/useCborFloat.ts +256 -0
- package/src/parser/composables/useCborInteger.ts +114 -0
- package/src/parser/composables/useCborParser.ts +951 -0
- package/src/parser/composables/useCborString.ts +330 -0
- package/src/parser/composables/useCborStringTypes.ts +129 -0
- package/src/parser/composables/useCborTag.ts +739 -0
- package/src/parser/index.ts +56 -0
- package/src/parser/types.ts +371 -0
- package/src/parser/utils/pathBuilder.ts +259 -0
- package/src/parser/utils.ts +398 -0
- package/src/utils/__tests__/logger.test.ts +186 -0
- 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
|
+
}
|