@marcuspuchalla/nachos 0.1.3 → 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 (68) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/{chunk-PTWN7K3Y.cjs → chunk-3Z45RBZP.cjs} +469 -244
  3. package/dist/chunk-3Z45RBZP.cjs.map +1 -0
  4. package/dist/{chunk-2MTLSQ7E.js → chunk-EDXZTSIA.js} +224 -166
  5. package/dist/chunk-EDXZTSIA.js.map +1 -0
  6. package/dist/{chunk-R62CQQNI.cjs → chunk-HMUA5KLG.cjs} +239 -181
  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-5A5T56JB.js → chunk-LWNWC2O7.js} +442 -217
  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 +58 -39
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +40 -21
  21. package/dist/index.d.ts +40 -21
  22. package/dist/index.js +37 -17
  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 +4 -2
  28. package/dist/parser/index.d.ts +4 -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-TVxzNJ_9.d.ts → useCborSimpleEncoder-CamvS-_N.d.ts} +7 -3
  33. package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-DXgPx62d.d.cts} +7 -3
  34. package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-D4d7xG3-.d.cts} +9 -4
  35. package/dist/{useCborTag-Cs1CZuXZ.d.cts → useCborTag-TYst1KR6.d.ts} +9 -4
  36. package/package.json +1 -1
  37. package/src/__tests__/audit-fixes.test.ts +141 -0
  38. package/src/__tests__/public-api.test.ts +153 -0
  39. package/src/__tests__/roundtrip.test.ts +5 -6
  40. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
  41. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
  42. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  43. package/src/encoder/composables/useCborCollectionEncoder.ts +30 -26
  44. package/src/encoder/composables/useCborEncoder.ts +40 -0
  45. package/src/encoder/composables/useCborSimpleEncoder.ts +40 -9
  46. package/src/encoder/types.ts +9 -4
  47. package/src/encoder/utils.ts +33 -1
  48. package/src/index.ts +39 -20
  49. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  50. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  51. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
  52. package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
  53. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  54. package/src/parser/__tests__/utils-errors.test.ts +11 -3
  55. package/src/parser/composables/useCborCollection.ts +51 -45
  56. package/src/parser/composables/useCborDiagnostic.ts +28 -0
  57. package/src/parser/composables/useCborFloat.ts +2 -1
  58. package/src/parser/composables/useCborInteger.ts +24 -10
  59. package/src/parser/composables/useCborParser.ts +448 -208
  60. package/src/parser/composables/useCborTag.ts +53 -38
  61. package/src/parser/types.ts +32 -1
  62. package/src/parser/utils.ts +52 -0
  63. package/dist/chunk-2MTLSQ7E.js.map +0 -1
  64. package/dist/chunk-5A5T56JB.js.map +0 -1
  65. package/dist/chunk-PD72MVTX.cjs.map +0 -1
  66. package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
  67. package/dist/chunk-R62CQQNI.cjs.map +0 -1
  68. package/dist/chunk-ZDZ2B5PE.js.map +0 -1
@@ -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, bytesToHex } 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'
@@ -25,21 +25,13 @@ import { logger } from '../../utils/logger'
25
25
  * ```
26
26
  */
27
27
  export function useCborCollection() {
28
- const { parseInteger } = useCborInteger()
28
+ const { parseIntegerFromBuffer } = useCborInteger()
29
29
  const { parseByteString, parseTextString } = useCborString()
30
- const { parse: parseFloatOrSimple } = useCborFloat()
31
- const { parseTag } = useCborTag()
30
+ const { parseFromBuffer: parseFloatOrSimpleFromBuffer } = useCborFloat()
31
+ const { parseTagFromBuffer } = useCborTag()
32
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
- }
33
+ /** Tracks when parsing started for timeout enforcement */
34
+ let parseStartTime = 0
43
35
 
44
36
  /**
45
37
  * Internal parser dispatcher for CBOR items
@@ -52,6 +44,14 @@ export function useCborCollection() {
52
44
  * @returns Parsed value and bytes consumed
53
45
  */
54
46
  const parseItem = (buffer: Uint8Array, offset: number, options?: ParseOptions, depth: number = 0): ParseResult => {
47
+ // Check timeout on every recursive call
48
+ if (parseStartTime > 0 && options?.limits?.maxParseTime) {
49
+ const elapsed = Date.now() - parseStartTime
50
+ if (elapsed > options.limits.maxParseTime) {
51
+ throw new Error(`Parse timeout: exceeded ${options.limits.maxParseTime}ms limit`)
52
+ }
53
+ }
54
+
55
55
  if (offset >= buffer.length) {
56
56
  throw new Error(`Unexpected end of buffer at offset ${offset}`)
57
57
  }
@@ -62,14 +62,7 @@ export function useCborCollection() {
62
62
  switch (majorType) {
63
63
  case 0: // Unsigned integer
64
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
- }
65
+ return parseIntegerFromBuffer(buffer, offset, options)
73
66
 
74
67
  case 2: // Byte string
75
68
  return parseByteString(buffer, offset, options)
@@ -84,22 +77,10 @@ export function useCborCollection() {
84
77
  return parseMapFromBuffer(buffer, offset, options, depth)
85
78
 
86
79
  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
- }
80
+ return parseTagFromBuffer(buffer, offset, options)
94
81
 
95
82
  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
- }
83
+ return parseFloatOrSimpleFromBuffer(buffer, offset, options)
103
84
 
104
85
  default:
105
86
  throw new Error(`Unknown major type: ${majorType}`)
@@ -356,8 +337,10 @@ export function useCborCollection() {
356
337
  const valueResult = parseItem(buffer, currentOffset, options, depth + 1)
357
338
  currentOffset += valueResult.bytesRead
358
339
 
359
- // For duplicate key detection, serialize the key
360
- const keyString = convertKeyToString(keyResult.value)
340
+ // For duplicate key detection, serialize the parsed value semantically
341
+ // RFC 8949 Section 5.6: comparison must be on semantic values, not raw bytes
342
+ // (raw bytes differ when same value uses different byte widths)
343
+ const keyString = serializeValueForComparison(keyResult.value)
361
344
 
362
345
  // Check for duplicate keys based on dupMapKeyMode
363
346
  // RFC 8949: Deterministic encoding SHOULD reject duplicate keys
@@ -420,8 +403,10 @@ export function useCborCollection() {
420
403
  const valueResult = parseItem(buffer, currentOffset, options, depth + 1)
421
404
  currentOffset += valueResult.bytesRead
422
405
 
423
- // For duplicate key detection, serialize the key
424
- const keyString = convertKeyToString(keyResult.value)
406
+ // For duplicate key detection, serialize the parsed value semantically
407
+ // RFC 8949 Section 5.6: comparison must be on semantic values, not raw bytes
408
+ // (raw bytes differ when same value uses different byte widths)
409
+ const keyString = serializeValueForComparison(keyResult.value)
425
410
 
426
411
  // Check for duplicate keys based on dupMapKeyMode
427
412
  // RFC 8949: Deterministic encoding SHOULD reject duplicate keys
@@ -449,16 +434,19 @@ export function useCborCollection() {
449
434
  // Attach allEntries to map for byte-perfect round-trips with duplicates
450
435
  ;(map as any)[ALL_ENTRIES_SYMBOL] = allEntries
451
436
 
452
- // 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).
453
440
  if (options?.validateCanonical && keyBytes.length > 1) {
441
+ const keyOrder = options?.mapKeyOrder ?? 'length-first'
454
442
  for (let i = 1; i < keyBytes.length; i++) {
455
443
  const prevKey = keyBytes[i - 1]
456
444
  const currKey = keyBytes[i]
457
445
  if (prevKey && currKey) {
458
- const cmp = compareBytes(prevKey, currKey)
446
+ const cmp = compareMapKeys(prevKey, currKey, keyOrder)
459
447
  if (cmp > 0) {
460
448
  throw new Error(
461
- `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}`
462
450
  )
463
451
  }
464
452
  if (cmp === 0) {
@@ -485,7 +473,16 @@ export function useCborCollection() {
485
473
  // Remove spaces from hex string
486
474
  const cleanHex = hexString.replace(/\s+/g, '')
487
475
  const buffer = hexToBytes(cleanHex)
488
- return parseArrayFromBuffer(buffer, 0, options, 0)
476
+
477
+ // Set parse start time for timeout enforcement
478
+ if (options?.limits?.maxParseTime) {
479
+ parseStartTime = Date.now()
480
+ }
481
+ try {
482
+ return parseArrayFromBuffer(buffer, 0, options, 0)
483
+ } finally {
484
+ parseStartTime = 0
485
+ }
489
486
  }
490
487
 
491
488
  /**
@@ -499,7 +496,16 @@ export function useCborCollection() {
499
496
  // Remove spaces from hex string
500
497
  const cleanHex = hexString.replace(/\s+/g, '')
501
498
  const buffer = hexToBytes(cleanHex)
502
- return parseMapFromBuffer(buffer, 0, options, 0)
499
+
500
+ // Set parse start time for timeout enforcement
501
+ if (options?.limits?.maxParseTime) {
502
+ parseStartTime = Date.now()
503
+ }
504
+ try {
505
+ return parseMapFromBuffer(buffer, 0, options, 0)
506
+ } finally {
507
+ parseStartTime = 0
508
+ }
503
509
  }
504
510
 
505
511
  return {
@@ -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
  }
@@ -336,6 +336,7 @@ export function useCborFloat() {
336
336
  return {
337
337
  parse,
338
338
  parseFloat,
339
- parseSimple
339
+ parseSimple,
340
+ parseFromBuffer
340
341
  }
341
342
  }
@@ -20,15 +20,15 @@ import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, validat
20
20
  */
21
21
  export function useCborInteger() {
22
22
  /**
23
- * Parses CBOR integer (Major Type 0 or 1)
23
+ * Parses CBOR integer (Major Type 0 or 1) from a buffer at a given offset
24
24
  *
25
- * @param hexString - CBOR hex string
25
+ * @param buffer - Data buffer
26
+ * @param offset - Current offset into the buffer
26
27
  * @param options - Parser options (optional)
27
28
  * @returns Parsed integer value and bytes read
28
29
  */
29
- const parseInteger = (hexString: string, options?: ParseOptions): ParseResult => {
30
- const buffer = hexToBytes(hexString)
31
- const initialByte = readByte(buffer, 0)
30
+ const parseIntegerFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
31
+ const initialByte = readByte(buffer, offset)
32
32
 
33
33
  const { majorType, additionalInfo } = extractCborHeader(initialByte)
34
34
 
@@ -42,19 +42,19 @@ export function useCborInteger() {
42
42
  bytesRead = 1
43
43
  } else if (additionalInfo === 24) {
44
44
  // 1 byte follows
45
- rawValue = readByte(buffer, 1)
45
+ rawValue = readByte(buffer, offset + 1)
46
46
  bytesRead = 2
47
47
  } else if (additionalInfo === 25) {
48
48
  // 2 bytes follow
49
- rawValue = readUint(buffer, 1, 2)
49
+ rawValue = readUint(buffer, offset + 1, 2)
50
50
  bytesRead = 3
51
51
  } else if (additionalInfo === 26) {
52
52
  // 4 bytes follow
53
- rawValue = readUint(buffer, 1, 4)
53
+ rawValue = readUint(buffer, offset + 1, 4)
54
54
  bytesRead = 5
55
55
  } else if (additionalInfo === 27) {
56
56
  // 8 bytes follow - use BigInt for large values
57
- const bigValue = readBigUint(buffer, 1, 8)
57
+ const bigValue = readBigUint(buffer, offset + 1, 8)
58
58
 
59
59
  // Check if value fits in Number.MAX_SAFE_INTEGER
60
60
  if (bigValue <= BigInt(Number.MAX_SAFE_INTEGER)) {
@@ -108,7 +108,21 @@ export function useCborInteger() {
108
108
  }
109
109
  }
110
110
 
111
+ /**
112
+ * Parses CBOR integer (Major Type 0 or 1) from hex string
113
+ * Thin wrapper around parseIntegerFromBuffer
114
+ *
115
+ * @param hexString - CBOR hex string
116
+ * @param options - Parser options (optional)
117
+ * @returns Parsed integer value and bytes read
118
+ */
119
+ const parseInteger = (hexString: string, options?: ParseOptions): ParseResult => {
120
+ const buffer = hexToBytes(hexString)
121
+ return parseIntegerFromBuffer(buffer, 0, options)
122
+ }
123
+
111
124
  return {
112
- parseInteger
125
+ parseInteger,
126
+ parseIntegerFromBuffer
113
127
  }
114
128
  }