@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/{chunk-RVG2BY32.cjs → chunk-3Z45RBZP.cjs} +96 -42
  3. package/dist/chunk-3Z45RBZP.cjs.map +1 -0
  4. package/dist/{chunk-UMAX5MX5.js → chunk-EDXZTSIA.js} +33 -5
  5. package/dist/chunk-EDXZTSIA.js.map +1 -0
  6. package/dist/{chunk-S4RXO6IB.cjs → chunk-HMUA5KLG.cjs} +48 -20
  7. package/dist/chunk-HMUA5KLG.cjs.map +1 -0
  8. package/dist/{chunk-ZDZ2B5PE.js → chunk-JESIF5IF.js} +7 -3
  9. package/dist/chunk-JESIF5IF.js.map +1 -0
  10. package/dist/{chunk-5IWW5H47.js → chunk-LWNWC2O7.js} +68 -14
  11. package/dist/chunk-LWNWC2O7.js.map +1 -0
  12. package/dist/{chunk-PD72MVTX.cjs → chunk-P6A2OOIY.cjs} +7 -3
  13. package/dist/chunk-P6A2OOIY.cjs.map +1 -0
  14. package/dist/encoder/index.cjs +14 -14
  15. package/dist/encoder/index.d.cts +5 -4
  16. package/dist/encoder/index.d.ts +5 -4
  17. package/dist/encoder/index.js +2 -2
  18. package/dist/index.cjs +46 -27
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +15 -5
  21. package/dist/index.d.ts +15 -5
  22. package/dist/index.js +25 -5
  23. package/dist/index.js.map +1 -1
  24. package/dist/metafile-cjs.json +1 -1
  25. package/dist/metafile-esm.json +1 -1
  26. package/dist/parser/index.cjs +21 -21
  27. package/dist/parser/index.d.cts +2 -2
  28. package/dist/parser/index.d.ts +2 -2
  29. package/dist/parser/index.js +2 -2
  30. package/dist/{types-DvNlfbKB.d.cts → types-eG2qalpr.d.cts} +27 -1
  31. package/dist/{types-DvNlfbKB.d.ts → types-eG2qalpr.d.ts} +27 -1
  32. package/dist/{useCborSimpleEncoder-BoKEmjP9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -1
  33. package/dist/{useCborSimpleEncoder-C_OHxoB8.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -1
  34. package/dist/{useCborTag-QpZR-Er2.d.cts → useCborTag-D4d7xG3-.d.cts} +1 -1
  35. package/dist/{useCborTag-BD6Sqp7p.d.ts → useCborTag-TYst1KR6.d.ts} +1 -1
  36. package/package.json +1 -1
  37. package/src/__tests__/audit-fixes.test.ts +141 -0
  38. package/src/encoder/composables/useCborCollectionEncoder.ts +3 -2
  39. package/src/encoder/composables/useCborEncoder.ts +19 -0
  40. package/src/encoder/composables/useCborSimpleEncoder.ts +6 -2
  41. package/src/encoder/types.ts +9 -2
  42. package/src/encoder/utils.ts +33 -1
  43. package/src/index.ts +10 -0
  44. package/src/parser/__tests__/utils-errors.test.ts +11 -3
  45. package/src/parser/composables/useCborCollection.ts +7 -4
  46. package/src/parser/composables/useCborDiagnostic.ts +28 -0
  47. package/src/parser/composables/useCborParser.ts +63 -13
  48. package/src/parser/composables/useCborTag.ts +8 -1
  49. package/src/parser/types.ts +32 -1
  50. package/src/parser/utils.ts +41 -0
  51. package/dist/chunk-5IWW5H47.js.map +0 -1
  52. package/dist/chunk-PD72MVTX.cjs.map +0 -1
  53. package/dist/chunk-RVG2BY32.cjs.map +0 -1
  54. package/dist/chunk-S4RXO6IB.cjs.map +0 -1
  55. package/dist/chunk-UMAX5MX5.js.map +0 -1
  56. package/dist/chunk-ZDZ2B5PE.js.map +0 -1
@@ -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
  }
@@ -28,7 +28,8 @@ export function concatenateUint8Arrays(arrays: Uint8Array[]): Uint8Array {
28
28
  }
29
29
 
30
30
  /**
31
- * Compare two Uint8Arrays bytewise (for canonical map sorting)
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-8 bytes successfully', () => {
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
- // Test all valid lengths (1-8)
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
- expect(readUint(buffer, 0, 8)).toBe(0x0102030405060708)
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, compareBytes, serializeValueForComparison } from '../utils'
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 = compareBytes(prevKey, currKey)
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
- return dispatchFromBuffer(input, 0, mergedOptions)
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
- return parseInteger(cleanHex, mergedOptions)
158
+ result = parseInteger(cleanHex, mergedOptions)
159
+ break
153
160
 
154
161
  case 2: // Byte string
155
162
  case 3: // Text string
156
- return parseString(cleanHex, mergedOptions)
163
+ result = parseString(cleanHex, mergedOptions)
164
+ break
157
165
 
158
166
  case 4: // Array
159
- return parseArray(cleanHex, mergedOptions)
167
+ result = parseArray(cleanHex, mergedOptions)
168
+ break
160
169
 
161
170
  case 5: // Map
162
- return parseMap(cleanHex, mergedOptions)
171
+ result = parseMap(cleanHex, mergedOptions)
172
+ break
163
173
 
164
174
  case 6: // Tagged value
165
- return parseTag(cleanHex, mergedOptions)
175
+ result = parseTag(cleanHex, mergedOptions)
176
+ break
166
177
 
167
178
  case 7: // Floating-point or simple value
168
- return parseFloatOrSimple(cleanHex, mergedOptions)
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
- const remainingHex = Array.from(buffer.slice(offset))
1096
- .map(b => b.toString(16).padStart(2, '0'))
1097
- .join('')
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)
@@ -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
- dupMapKeyMode: 'allow',
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
 
@@ -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