@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,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
|
+
}
|