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