@marcuspuchalla/nachos 0.1.4 → 0.2.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 +52 -0
- package/dist/{chunk-RVG2BY32.cjs → chunk-3Z45RBZP.cjs} +96 -42
- package/dist/chunk-3Z45RBZP.cjs.map +1 -0
- package/dist/{chunk-UMAX5MX5.js → chunk-EDXZTSIA.js} +33 -5
- package/dist/chunk-EDXZTSIA.js.map +1 -0
- package/dist/{chunk-S4RXO6IB.cjs → chunk-HMUA5KLG.cjs} +48 -20
- package/dist/chunk-HMUA5KLG.cjs.map +1 -0
- package/dist/{chunk-ZDZ2B5PE.js → chunk-JESIF5IF.js} +7 -3
- package/dist/chunk-JESIF5IF.js.map +1 -0
- package/dist/{chunk-5IWW5H47.js → chunk-LWNWC2O7.js} +68 -14
- package/dist/chunk-LWNWC2O7.js.map +1 -0
- package/dist/{chunk-PD72MVTX.cjs → chunk-P6A2OOIY.cjs} +7 -3
- package/dist/chunk-P6A2OOIY.cjs.map +1 -0
- package/dist/encoder/index.cjs +14 -14
- package/dist/encoder/index.d.cts +5 -4
- package/dist/encoder/index.d.ts +5 -4
- package/dist/encoder/index.js +2 -2
- package/dist/index.cjs +46 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +25 -5
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/parser/index.cjs +21 -21
- package/dist/parser/index.d.cts +2 -2
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +2 -2
- package/dist/{types-DvNlfbKB.d.cts → types-eG2qalpr.d.cts} +27 -1
- package/dist/{types-DvNlfbKB.d.ts → types-eG2qalpr.d.ts} +27 -1
- package/dist/{useCborSimpleEncoder-BoKEmjP9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -1
- package/dist/{useCborSimpleEncoder-C_OHxoB8.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -1
- package/dist/{useCborTag-QpZR-Er2.d.cts → useCborTag-D4d7xG3-.d.cts} +1 -1
- package/dist/{useCborTag-BD6Sqp7p.d.ts → useCborTag-TYst1KR6.d.ts} +1 -1
- package/package.json +1 -1
- package/src/__tests__/audit-fixes.test.ts +141 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +3 -2
- package/src/encoder/composables/useCborEncoder.ts +19 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +6 -2
- package/src/encoder/types.ts +9 -2
- package/src/encoder/utils.ts +33 -1
- package/src/index.ts +10 -0
- package/src/parser/__tests__/utils-errors.test.ts +11 -3
- package/src/parser/composables/useCborCollection.ts +7 -4
- package/src/parser/composables/useCborDiagnostic.ts +28 -0
- package/src/parser/composables/useCborParser.ts +63 -13
- package/src/parser/composables/useCborTag.ts +8 -1
- package/src/parser/types.ts +32 -1
- package/src/parser/utils.ts +41 -0
- package/dist/chunk-5IWW5H47.js.map +0 -1
- package/dist/chunk-PD72MVTX.cjs.map +0 -1
- package/dist/chunk-RVG2BY32.cjs.map +0 -1
- package/dist/chunk-S4RXO6IB.cjs.map +0 -1
- package/dist/chunk-UMAX5MX5.js.map +0 -1
- package/dist/chunk-ZDZ2B5PE.js.map +0 -1
package/src/encoder/types.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Following RFC 8949 specification
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { PlutusConstr, CborByteString, CborTextString } from '../parser/types'
|
|
6
|
+
import type { PlutusConstr, CborByteString, CborTextString, MapKeyOrder } from '../parser/types'
|
|
7
7
|
import { INDEFINITE_SYMBOL, ALL_ENTRIES_SYMBOL } from '../parser/types'
|
|
8
8
|
|
|
9
9
|
// Re-export symbols and types for use in encoder
|
|
10
10
|
export { INDEFINITE_SYMBOL, ALL_ENTRIES_SYMBOL }
|
|
11
|
-
export type { CborByteString, CborTextString }
|
|
11
|
+
export type { CborByteString, CborTextString, MapKeyOrder }
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Encoder options for controlling behavior
|
|
@@ -20,6 +20,12 @@ export interface EncodeOptions {
|
|
|
20
20
|
allowIndefinite?: boolean
|
|
21
21
|
/** Reject duplicate map keys */
|
|
22
22
|
rejectDuplicateKeys?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Map key ordering used in canonical mode.
|
|
25
|
+
* Defaults to 'length-first' (Cardano CIP-21 / RFC 7049 §3.9).
|
|
26
|
+
* Use 'bytewise' for RFC 8949 §4.2.1 core deterministic order.
|
|
27
|
+
*/
|
|
28
|
+
mapKeyOrder?: MapKeyOrder
|
|
23
29
|
/** Maximum nesting depth */
|
|
24
30
|
maxDepth?: number
|
|
25
31
|
/** Maximum output size in bytes */
|
|
@@ -33,6 +39,7 @@ export const DEFAULT_ENCODE_OPTIONS: Required<EncodeOptions> = {
|
|
|
33
39
|
canonical: false,
|
|
34
40
|
allowIndefinite: true,
|
|
35
41
|
rejectDuplicateKeys: false,
|
|
42
|
+
mapKeyOrder: 'length-first',
|
|
36
43
|
maxDepth: 64,
|
|
37
44
|
maxOutputSize: 100 * 1024 * 1024 // 100 MB
|
|
38
45
|
}
|
package/src/encoder/utils.ts
CHANGED
|
@@ -28,7 +28,8 @@ export function concatenateUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Compare two Uint8Arrays
|
|
31
|
+
* Compare two Uint8Arrays length-first (RFC 7049 §3.9 / Cardano CIP-21 ordering).
|
|
32
|
+
* Shorter keys sort first; equal-length keys are compared bytewise.
|
|
32
33
|
*/
|
|
33
34
|
export function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
34
35
|
// First, compare lengths
|
|
@@ -51,6 +52,37 @@ export function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
|
51
52
|
return 0
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Compare two Uint8Arrays in pure bytewise lexicographic order
|
|
57
|
+
* (RFC 8949 §4.2.1 core deterministic encoding). If one is a prefix of the
|
|
58
|
+
* other, the shorter sorts first.
|
|
59
|
+
*/
|
|
60
|
+
export function compareBytesLexicographic(a: Uint8Array, b: Uint8Array): number {
|
|
61
|
+
const min = Math.min(a.length, b.length)
|
|
62
|
+
for (let i = 0; i < min; i++) {
|
|
63
|
+
const byteA = a[i]!
|
|
64
|
+
const byteB = b[i]!
|
|
65
|
+
if (byteA !== byteB) {
|
|
66
|
+
return byteA - byteB
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return a.length - b.length
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compare two encoded map keys according to the requested ordering.
|
|
74
|
+
*
|
|
75
|
+
* @param order - 'length-first' (CIP-21 / RFC 7049 §3.9, default) or
|
|
76
|
+
* 'bytewise' (RFC 8949 §4.2.1 core deterministic)
|
|
77
|
+
*/
|
|
78
|
+
export function compareMapKeys(
|
|
79
|
+
a: Uint8Array,
|
|
80
|
+
b: Uint8Array,
|
|
81
|
+
order: 'length-first' | 'bytewise' = 'length-first'
|
|
82
|
+
): number {
|
|
83
|
+
return order === 'bytewise' ? compareBytesLexicographic(a, b) : compareBytes(a, b)
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
/**
|
|
55
87
|
* Write unsigned integer to bytes (big-endian)
|
|
56
88
|
*/
|
package/src/index.ts
CHANGED
|
@@ -179,6 +179,16 @@ export function decodeWithSourceMap(input: string | Uint8Array, options?: ParseO
|
|
|
179
179
|
*
|
|
180
180
|
* @throws {Error} If value type is unsupported or encoding fails
|
|
181
181
|
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* Plain objects use `Object.entries()`, so all keys become **text strings** and
|
|
184
|
+
* integer-like keys (`"0"`, `"1"`, …) are reordered by the JS engine's
|
|
185
|
+
* integer-index property rule. For integer keys or guaranteed ordering (e.g.
|
|
186
|
+
* Cardano transaction bodies), pass a `Map` instead of a plain object.
|
|
187
|
+
*
|
|
188
|
+
* Canonical mode (`{ canonical: true }`) sorts map keys **length-first** by
|
|
189
|
+
* default (Cardano CIP-21 / RFC 7049 §3.9). Pass `{ mapKeyOrder: 'bytewise' }`
|
|
190
|
+
* for RFC 8949 §4.2.1 core deterministic ordering.
|
|
191
|
+
*
|
|
182
192
|
* @example
|
|
183
193
|
* ```typescript
|
|
184
194
|
* // Encode number
|
|
@@ -42,10 +42,10 @@ describe('Utils - Error Handling', () => {
|
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
describe('readUint - Valid Lengths', () => {
|
|
45
|
-
it('should read 1-
|
|
45
|
+
it('should read 1-7 bytes (within safe-integer range) successfully', () => {
|
|
46
46
|
const buffer = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Lengths 1-7 stay within Number.MAX_SAFE_INTEGER (2^53 - 1)
|
|
49
49
|
expect(readUint(buffer, 0, 1)).toBe(0x01)
|
|
50
50
|
expect(readUint(buffer, 0, 2)).toBe(0x0102)
|
|
51
51
|
expect(readUint(buffer, 0, 3)).toBe(0x010203)
|
|
@@ -53,7 +53,15 @@ describe('Utils - Error Handling', () => {
|
|
|
53
53
|
expect(readUint(buffer, 0, 5)).toBe(0x0102030405)
|
|
54
54
|
expect(readUint(buffer, 0, 6)).toBe(0x010203040506)
|
|
55
55
|
expect(readUint(buffer, 0, 7)).toBe(0x01020304050607)
|
|
56
|
-
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should throw rather than silently lose precision above MAX_SAFE_INTEGER', () => {
|
|
59
|
+
// 0x0102030405060708 ≈ 7.26e16 > 2^53, so readUint refuses it and
|
|
60
|
+
// directs callers to readBigUint (L3 precision-safety fix).
|
|
61
|
+
const buffer = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
|
|
62
|
+
expect(() => readUint(buffer, 0, 8)).toThrow('exceeds MAX_SAFE_INTEGER')
|
|
63
|
+
// The exact value is available without loss via readBigUint:
|
|
64
|
+
expect(readBigUint(buffer, 0, 8)).toBe(0x0102030405060708n)
|
|
57
65
|
})
|
|
58
66
|
})
|
|
59
67
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ParseResult, CborValue, CborMap, ParseOptions } from '../types'
|
|
8
8
|
import { INDEFINITE_SYMBOL, ALL_ENTRIES_SYMBOL } from '../types'
|
|
9
|
-
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader,
|
|
9
|
+
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, compareMapKeys, serializeValueForComparison } from '../utils'
|
|
10
10
|
import { useCborInteger } from './useCborInteger'
|
|
11
11
|
import { useCborString } from './useCborString'
|
|
12
12
|
import { useCborFloat } from './useCborFloat'
|
|
@@ -434,16 +434,19 @@ export function useCborCollection() {
|
|
|
434
434
|
// Attach allEntries to map for byte-perfect round-trips with duplicates
|
|
435
435
|
;(map as any)[ALL_ENTRIES_SYMBOL] = allEntries
|
|
436
436
|
|
|
437
|
-
// Validate canonical key ordering (keys must be sorted by byte representation)
|
|
437
|
+
// Validate canonical key ordering (keys must be sorted by byte representation).
|
|
438
|
+
// Ordering follows options.mapKeyOrder: 'length-first' (CIP-21 / RFC 7049 §3.9,
|
|
439
|
+
// default) or 'bytewise' (RFC 8949 §4.2.1 core deterministic).
|
|
438
440
|
if (options?.validateCanonical && keyBytes.length > 1) {
|
|
441
|
+
const keyOrder = options?.mapKeyOrder ?? 'length-first'
|
|
439
442
|
for (let i = 1; i < keyBytes.length; i++) {
|
|
440
443
|
const prevKey = keyBytes[i - 1]
|
|
441
444
|
const currKey = keyBytes[i]
|
|
442
445
|
if (prevKey && currKey) {
|
|
443
|
-
const cmp =
|
|
446
|
+
const cmp = compareMapKeys(prevKey, currKey, keyOrder)
|
|
444
447
|
if (cmp > 0) {
|
|
445
448
|
throw new Error(
|
|
446
|
-
`Map keys are not in canonical order: key at index ${i} should come before key at index ${i - 1}`
|
|
449
|
+
`Map keys are not in canonical order (${keyOrder}): key at index ${i} should come before key at index ${i - 1}`
|
|
447
450
|
)
|
|
448
451
|
}
|
|
449
452
|
if (cmp === 0) {
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { INDEFINITE_SYMBOL } from '../types'
|
|
19
|
+
|
|
20
|
+
/** Detect the indefinite-length marker attached by the parser to a value. */
|
|
21
|
+
function isIndefiniteValue(value: unknown): boolean {
|
|
22
|
+
return typeof value === 'object' && value !== null && (value as any)[INDEFINITE_SYMBOL] === true
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
/**
|
|
19
26
|
* Options for diagnostic notation output
|
|
20
27
|
*/
|
|
@@ -186,6 +193,25 @@ export function useCborDiagnostic() {
|
|
|
186
193
|
return `h'${bytesToHex(value)}'`
|
|
187
194
|
}
|
|
188
195
|
|
|
196
|
+
// Handle parser wrapper types for indefinite-length strings
|
|
197
|
+
if (typeof value === 'object' && value !== null && 'type' in value) {
|
|
198
|
+
const t = (value as { type?: string }).type
|
|
199
|
+
if (t === 'cbor-byte-string') {
|
|
200
|
+
const bs = value as unknown as { bytes: Uint8Array }
|
|
201
|
+
return isIndefiniteValue(value) ? `(_ h'${bytesToHex(bs.bytes)}')` : `h'${bytesToHex(bs.bytes)}'`
|
|
202
|
+
}
|
|
203
|
+
if (t === 'cbor-text-string') {
|
|
204
|
+
const ts = value as unknown as { text: string }
|
|
205
|
+
return isIndefiniteValue(value) ? `(_ "${escapeString(ts.text)}")` : `"${escapeString(ts.text)}"`
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle unassigned simple values (Major Type 7): simple(N)
|
|
210
|
+
if (typeof value === 'object' && value !== null && 'simpleValue' in value &&
|
|
211
|
+
typeof (value as { simpleValue?: unknown }).simpleValue === 'number') {
|
|
212
|
+
return `simple(${(value as { simpleValue: number }).simpleValue})`
|
|
213
|
+
}
|
|
214
|
+
|
|
189
215
|
// Handle tagged values
|
|
190
216
|
if (isTaggedValue(value)) {
|
|
191
217
|
const taggedContent = formatValue(
|
|
@@ -201,6 +227,7 @@ export function useCborDiagnostic() {
|
|
|
201
227
|
|
|
202
228
|
// Handle arrays
|
|
203
229
|
if (Array.isArray(value)) {
|
|
230
|
+
indefinite = indefinite || isIndefiniteValue(value)
|
|
204
231
|
if (value.length === 0) {
|
|
205
232
|
return indefinite ? '[_ ]' : '[]'
|
|
206
233
|
}
|
|
@@ -222,6 +249,7 @@ export function useCborDiagnostic() {
|
|
|
222
249
|
|
|
223
250
|
// Handle Maps
|
|
224
251
|
if (value instanceof Map) {
|
|
252
|
+
indefinite = indefinite || isIndefiniteValue(value)
|
|
225
253
|
if (value.size === 0) {
|
|
226
254
|
return indefinite ? '{_ }' : '{}'
|
|
227
255
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ParseResult, ParseResultWithMap, SourceMapEntry, ParseOptions, CborContext, CborValue, TaggedValue } from '../types'
|
|
8
8
|
import { DEFAULT_OPTIONS, DEFAULT_LIMITS } from '../types'
|
|
9
|
-
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, serializeValueForComparison } from '../utils'
|
|
9
|
+
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, serializeValueForComparison, validateCanonicalInteger } from '../utils'
|
|
10
10
|
import { useCborInteger } from './useCborInteger'
|
|
11
11
|
import { useCborString } from './useCborString'
|
|
12
12
|
import { useCborCollection } from './useCborCollection'
|
|
@@ -47,6 +47,9 @@ export function useCborParser() {
|
|
|
47
47
|
validateSetUniqueness: options.validateSetUniqueness ?? (options.strict ? true : DEFAULT_OPTIONS.validateSetUniqueness),
|
|
48
48
|
validateTagSemantics: options.validateTagSemantics ?? (options.strict ? true : DEFAULT_OPTIONS.validateTagSemantics),
|
|
49
49
|
validatePlutusSemantics: options.validatePlutusSemantics ?? (options.strict ? true : DEFAULT_OPTIONS.validatePlutusSemantics),
|
|
50
|
+
mapKeyOrder: options.mapKeyOrder ?? DEFAULT_OPTIONS.mapKeyOrder,
|
|
51
|
+
// Strict mode rejects trailing data after the top-level item (well-formedness).
|
|
52
|
+
allowTrailingData: options.allowTrailingData ?? (options.strict ? false : DEFAULT_OPTIONS.allowTrailingData),
|
|
50
53
|
limits: {
|
|
51
54
|
maxInputSize: options.limits?.maxInputSize ?? DEFAULT_LIMITS.maxInputSize,
|
|
52
55
|
maxOutputSize: options.limits?.maxOutputSize ?? DEFAULT_LIMITS.maxOutputSize,
|
|
@@ -115,7 +118,9 @@ export function useCborParser() {
|
|
|
115
118
|
throw new Error(`Input size ${input.length} bytes exceeds limit of ${mergedOptions.limits.maxInputSize} bytes`)
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
|
|
121
|
+
const bufResult = dispatchFromBuffer(input, 0, mergedOptions)
|
|
122
|
+
checkTrailingData(bufResult.bytesRead, input.length, mergedOptions)
|
|
123
|
+
return bufResult
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
// Hex string path
|
|
@@ -146,30 +151,57 @@ export function useCborParser() {
|
|
|
146
151
|
const { majorType } = extractCborHeader(initialByte)
|
|
147
152
|
|
|
148
153
|
// Dispatch to appropriate parser based on major type
|
|
154
|
+
let result: ParseResult
|
|
149
155
|
switch (majorType) {
|
|
150
156
|
case 0: // Unsigned integer
|
|
151
157
|
case 1: // Negative integer
|
|
152
|
-
|
|
158
|
+
result = parseInteger(cleanHex, mergedOptions)
|
|
159
|
+
break
|
|
153
160
|
|
|
154
161
|
case 2: // Byte string
|
|
155
162
|
case 3: // Text string
|
|
156
|
-
|
|
163
|
+
result = parseString(cleanHex, mergedOptions)
|
|
164
|
+
break
|
|
157
165
|
|
|
158
166
|
case 4: // Array
|
|
159
|
-
|
|
167
|
+
result = parseArray(cleanHex, mergedOptions)
|
|
168
|
+
break
|
|
160
169
|
|
|
161
170
|
case 5: // Map
|
|
162
|
-
|
|
171
|
+
result = parseMap(cleanHex, mergedOptions)
|
|
172
|
+
break
|
|
163
173
|
|
|
164
174
|
case 6: // Tagged value
|
|
165
|
-
|
|
175
|
+
result = parseTag(cleanHex, mergedOptions)
|
|
176
|
+
break
|
|
166
177
|
|
|
167
178
|
case 7: // Floating-point or simple value
|
|
168
|
-
|
|
179
|
+
result = parseFloatOrSimple(cleanHex, mergedOptions)
|
|
180
|
+
break
|
|
169
181
|
|
|
170
182
|
default:
|
|
171
183
|
throw new Error(`Unknown major type: ${majorType}`)
|
|
172
184
|
}
|
|
185
|
+
|
|
186
|
+
checkTrailingData(result.bytesRead, buffer.length, mergedOptions)
|
|
187
|
+
return result
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Rejects trailing bytes after the top-level data item when
|
|
192
|
+
* allowTrailingData is false (RFC 8949 well-formedness for a single item).
|
|
193
|
+
*/
|
|
194
|
+
const checkTrailingData = (
|
|
195
|
+
bytesRead: number,
|
|
196
|
+
totalLength: number,
|
|
197
|
+
opts: Required<ParseOptions>
|
|
198
|
+
): void => {
|
|
199
|
+
if (!opts.allowTrailingData && bytesRead < totalLength) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Trailing data: ${totalLength - bytesRead} byte(s) remain after the top-level CBOR item ` +
|
|
202
|
+
`(bytesRead=${bytesRead}, length=${totalLength}). Use parseSequence to decode multiple items.`
|
|
203
|
+
)
|
|
204
|
+
}
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
/**
|
|
@@ -881,6 +913,17 @@ export function useCborParser() {
|
|
|
881
913
|
path: string,
|
|
882
914
|
sourceMap: SourceMapEntry[]
|
|
883
915
|
): ParseResult => {
|
|
916
|
+
// Enforce tag nesting depth (RUSTSEC-2019-0025). The source-map path
|
|
917
|
+
// previously lacked this guard, allowing a deeply nested tag chain to
|
|
918
|
+
// overflow the call stack with an uncatchable RangeError instead of a
|
|
919
|
+
// clean error — matching the decode() path's behaviour here.
|
|
920
|
+
const previousTagDepth = ctx.currentTagDepth ?? 0
|
|
921
|
+
const maxTagDepth = ctx.options?.limits?.maxTagDepth ?? DEFAULT_LIMITS.maxTagDepth
|
|
922
|
+
if (previousTagDepth >= maxTagDepth) {
|
|
923
|
+
throw new Error(`Tag nesting depth ${previousTagDepth} exceeds limit of ${maxTagDepth}`)
|
|
924
|
+
}
|
|
925
|
+
ctx.currentTagDepth = previousTagDepth + 1
|
|
926
|
+
|
|
884
927
|
const startOffset = offset
|
|
885
928
|
const initialByte = readByte(ctx.buffer, offset)
|
|
886
929
|
const { additionalInfo } = extractCborHeader(initialByte)
|
|
@@ -892,6 +935,11 @@ export function useCborParser() {
|
|
|
892
935
|
additionalInfo
|
|
893
936
|
)
|
|
894
937
|
|
|
938
|
+
// Enforce canonical (shortest-form) tag number encoding when requested.
|
|
939
|
+
if (ctx.options?.validateCanonical) {
|
|
940
|
+
validateCanonicalInteger(tagNumber, additionalInfo)
|
|
941
|
+
}
|
|
942
|
+
|
|
895
943
|
let currentOffset = offset + 1 + bytesConsumed
|
|
896
944
|
const headerEnd = currentOffset
|
|
897
945
|
|
|
@@ -959,6 +1007,9 @@ export function useCborParser() {
|
|
|
959
1007
|
...(plutusConstr && { plutus: plutusConstr })
|
|
960
1008
|
}
|
|
961
1009
|
|
|
1010
|
+
// Restore tag depth so sibling tags don't accumulate against the limit.
|
|
1011
|
+
ctx.currentTagDepth = previousTagDepth
|
|
1012
|
+
|
|
962
1013
|
return {
|
|
963
1014
|
value: taggedValue,
|
|
964
1015
|
bytesRead: currentOffset - startOffset
|
|
@@ -1092,11 +1143,10 @@ export function useCborParser() {
|
|
|
1092
1143
|
throw new Error(`Unexpected break code (0xff) at offset ${offset}`)
|
|
1093
1144
|
}
|
|
1094
1145
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const result = parseWithSourceMap(remainingHex, mergedOptions)
|
|
1146
|
+
// Zero-copy view of the remaining bytes (parseWithSourceMap accepts
|
|
1147
|
+
// Uint8Array). Avoids the previous O(N^2) per-item hex re-encode that
|
|
1148
|
+
// re-stringified the whole tail of the buffer on every sequence item.
|
|
1149
|
+
const result = parseWithSourceMap(buffer.subarray(offset), mergedOptions)
|
|
1100
1150
|
|
|
1101
1151
|
// Adjust source map offsets to account for sequence position
|
|
1102
1152
|
const adjustedSourceMap = result.sourceMap.map(entry => ({
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ParseResult, CborValue, TaggedValue, CborMap, ParseOptions, PlutusConstr, CborByteString } from '../types'
|
|
8
8
|
import { INDEFINITE_SYMBOL, DEFAULT_LIMITS } from '../types'
|
|
9
|
-
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, hasDuplicates } from '../utils'
|
|
9
|
+
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, hasDuplicates, validateCanonicalInteger } from '../utils'
|
|
10
10
|
import { useCborInteger } from './useCborInteger'
|
|
11
11
|
import { useCborString } from './useCborString'
|
|
12
12
|
import { useCborFloat } from './useCborFloat'
|
|
@@ -738,6 +738,13 @@ export function useCborTag() {
|
|
|
738
738
|
|
|
739
739
|
// Parse the tag number
|
|
740
740
|
const { tagNumber, bytesConsumed } = parseTagNumber(buffer, offset + 1, additionalInfo)
|
|
741
|
+
|
|
742
|
+
// Enforce canonical (shortest-form) tag number encoding when requested.
|
|
743
|
+
// RFC 8949 §4.2.1 preferred serialization applies to the tag number too.
|
|
744
|
+
if (options?.validateCanonical) {
|
|
745
|
+
validateCanonicalInteger(tagNumber, additionalInfo)
|
|
746
|
+
}
|
|
747
|
+
|
|
741
748
|
let currentOffset = offset + 1 + bytesConsumed
|
|
742
749
|
|
|
743
750
|
// Parse the tagged value (recursively)
|
package/src/parser/types.ts
CHANGED
|
@@ -33,6 +33,20 @@ export interface ParserLimits {
|
|
|
33
33
|
*/
|
|
34
34
|
export type DupMapKeyMode = 'allow' | 'warn' | 'reject'
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Map key ordering for canonical/deterministic encoding and validation.
|
|
38
|
+
*
|
|
39
|
+
* - 'length-first': RFC 7049 Section 3.9 "Old Canonical CBOR" — shorter encoded
|
|
40
|
+
* keys sort first, ties broken bytewise. This is what Cardano CIP-21 mandates
|
|
41
|
+
* for transaction serialization, and is the default in this library.
|
|
42
|
+
* - 'bytewise': RFC 8949 Section 4.2.1 "Core Deterministic Encoding" — pure
|
|
43
|
+
* bytewise lexicographic order of the encoded keys (the modern generic default).
|
|
44
|
+
*
|
|
45
|
+
* @see https://cips.cardano.org/cip/CIP-21
|
|
46
|
+
* @see https://www.rfc-editor.org/rfc/rfc8949.html#section-4.2.1
|
|
47
|
+
*/
|
|
48
|
+
export type MapKeyOrder = 'length-first' | 'bytewise'
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* Parser options for controlling behavior
|
|
38
52
|
*/
|
|
@@ -58,6 +72,19 @@ export interface ParseOptions {
|
|
|
58
72
|
validateTagSemantics?: boolean
|
|
59
73
|
/** Validate Plutus constructor semantics (Tags 102, 121-127, 1280-1400) */
|
|
60
74
|
validatePlutusSemantics?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Map key ordering enforced when validateCanonical is set.
|
|
77
|
+
* Defaults to 'length-first' (Cardano CIP-21 / RFC 7049 Section 3.9).
|
|
78
|
+
* Use 'bytewise' for RFC 8949 Section 4.2.1 core deterministic order.
|
|
79
|
+
*/
|
|
80
|
+
mapKeyOrder?: MapKeyOrder
|
|
81
|
+
/**
|
|
82
|
+
* Reject trailing bytes after the top-level data item (well-formedness).
|
|
83
|
+
* Defaults to true for backward compatibility (decode returns bytesRead so
|
|
84
|
+
* callers can detect leftover data); automatically false-tightened, i.e. set
|
|
85
|
+
* to reject, in strict mode. Set explicitly to override.
|
|
86
|
+
*/
|
|
87
|
+
allowTrailingData?: boolean
|
|
61
88
|
/** Resource limits */
|
|
62
89
|
limits?: ParserLimits
|
|
63
90
|
}
|
|
@@ -84,11 +111,15 @@ export const DEFAULT_OPTIONS: Required<ParseOptions> = {
|
|
|
84
111
|
strict: false,
|
|
85
112
|
validateCanonical: false,
|
|
86
113
|
allowIndefinite: true,
|
|
87
|
-
|
|
114
|
+
// Default to 'warn' so duplicate keys are never silently collapsed in the Map
|
|
115
|
+
// view. Duplicates remain byte-perfect for round-trips via ALL_ENTRIES_SYMBOL.
|
|
116
|
+
dupMapKeyMode: 'warn',
|
|
88
117
|
validateUtf8Strict: false,
|
|
89
118
|
validateSetUniqueness: false,
|
|
90
119
|
validateTagSemantics: false,
|
|
91
120
|
validatePlutusSemantics: false,
|
|
121
|
+
mapKeyOrder: 'length-first',
|
|
122
|
+
allowTrailingData: true,
|
|
92
123
|
limits: DEFAULT_LIMITS
|
|
93
124
|
}
|
|
94
125
|
|
package/src/parser/utils.ts
CHANGED
|
@@ -75,6 +75,12 @@ export const readUint = (buffer: Uint8Array, offset: number, length: number): nu
|
|
|
75
75
|
for (let i = 0; i < length; i++) {
|
|
76
76
|
result = result * 256 + readByte(buffer, offset + i)
|
|
77
77
|
}
|
|
78
|
+
// Guard against silent precision loss: values above 2^53 cannot be represented
|
|
79
|
+
// exactly as a JS number. Callers needing the full 64-bit range must use
|
|
80
|
+
// readBigUint instead (this helper is only invoked for <= 4-byte fields).
|
|
81
|
+
if (result > Number.MAX_SAFE_INTEGER) {
|
|
82
|
+
throw new Error(`Value at offset ${offset} (${length} bytes) exceeds MAX_SAFE_INTEGER; use readBigUint`)
|
|
83
|
+
}
|
|
78
84
|
return result
|
|
79
85
|
}
|
|
80
86
|
|
|
@@ -344,6 +350,41 @@ export function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
|
344
350
|
return 0 // Equal
|
|
345
351
|
}
|
|
346
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Compare two byte arrays in pure bytewise lexicographic order.
|
|
355
|
+
* This is RFC 8949 Section 4.2.1 "Core Deterministic Encoding" ordering:
|
|
356
|
+
* compare byte-by-byte; if one is a prefix of the other, the shorter sorts first.
|
|
357
|
+
* (Contrast with compareBytes above, which sorts length-first per RFC 7049 §3.9.)
|
|
358
|
+
*/
|
|
359
|
+
export function compareBytesLexicographic(a: Uint8Array, b: Uint8Array): number {
|
|
360
|
+
if (!a || !b) {
|
|
361
|
+
throw new Error('compareBytesLexicographic: arguments cannot be null or undefined')
|
|
362
|
+
}
|
|
363
|
+
const min = Math.min(a.length, b.length)
|
|
364
|
+
for (let i = 0; i < min; i++) {
|
|
365
|
+
const byteA = a[i]!
|
|
366
|
+
const byteB = b[i]!
|
|
367
|
+
if (byteA !== byteB) {
|
|
368
|
+
return byteA - byteB
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return a.length - b.length
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Compare two encoded map keys according to the requested ordering.
|
|
376
|
+
*
|
|
377
|
+
* @param order - 'length-first' (CIP-21 / RFC 7049 §3.9, default) or
|
|
378
|
+
* 'bytewise' (RFC 8949 §4.2.1 core deterministic)
|
|
379
|
+
*/
|
|
380
|
+
export function compareMapKeys(
|
|
381
|
+
a: Uint8Array,
|
|
382
|
+
b: Uint8Array,
|
|
383
|
+
order: 'length-first' | 'bytewise' = 'length-first'
|
|
384
|
+
): number {
|
|
385
|
+
return order === 'bytewise' ? compareBytesLexicographic(a, b) : compareBytes(a, b)
|
|
386
|
+
}
|
|
387
|
+
|
|
347
388
|
/**
|
|
348
389
|
* Serializes a CBOR value to a normalized string for comparison
|
|
349
390
|
* Used for detecting duplicates in sets and ensuring uniqueness
|