@marcuspuchalla/nachos 0.1.1 → 0.1.4

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 (62) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
  3. package/dist/chunk-5IWW5H47.js.map +1 -0
  4. package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
  5. package/dist/chunk-RVG2BY32.cjs.map +1 -0
  6. package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
  7. package/dist/chunk-S4RXO6IB.cjs.map +1 -0
  8. package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
  9. package/dist/chunk-UMAX5MX5.js.map +1 -0
  10. package/dist/encoder/index.cjs +13 -13
  11. package/dist/encoder/index.d.cts +2 -2
  12. package/dist/encoder/index.d.ts +2 -2
  13. package/dist/encoder/index.js +1 -1
  14. package/dist/index.cjs +32 -32
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +28 -19
  17. package/dist/index.d.ts +28 -19
  18. package/dist/index.js +16 -16
  19. package/dist/index.js.map +1 -1
  20. package/dist/metafile-cjs.json +1 -1
  21. package/dist/metafile-esm.json +1 -1
  22. package/dist/parser/index.cjs +14 -14
  23. package/dist/parser/index.d.cts +3 -1
  24. package/dist/parser/index.d.ts +3 -1
  25. package/dist/parser/index.js +1 -1
  26. package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
  27. package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
  28. package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
  29. package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
  30. package/package.json +1 -1
  31. package/src/__tests__/public-api.test.ts +153 -0
  32. package/src/__tests__/roundtrip.test.ts +701 -0
  33. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
  34. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
  35. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  36. package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
  37. package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
  38. package/src/encoder/composables/useCborEncoder.ts +27 -1
  39. package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
  40. package/src/encoder/composables/useCborStringEncoder.ts +23 -10
  41. package/src/encoder/types.ts +0 -2
  42. package/src/index.ts +29 -20
  43. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  44. package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
  45. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  46. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
  47. package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
  48. package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
  49. package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
  50. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  51. package/src/parser/composables/useCborCollection.ts +45 -42
  52. package/src/parser/composables/useCborFloat.ts +95 -9
  53. package/src/parser/composables/useCborInteger.ts +24 -10
  54. package/src/parser/composables/useCborParser.ts +387 -216
  55. package/src/parser/composables/useCborString.ts +22 -4
  56. package/src/parser/composables/useCborTag.ts +149 -53
  57. package/src/parser/utils.ts +11 -0
  58. package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
  59. package/dist/chunk-2HBCILJS.cjs.map +0 -1
  60. package/dist/chunk-7CFYWHS6.js.map +0 -1
  61. package/dist/chunk-ZRPJUEIZ.js.map +0 -1
  62. package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Tests for eliminating exponential re-parsing in source-map tag handling
3
+ * Task 2-B: parseTagWithMap should NOT re-parse the entire tag subtree
4
+ *
5
+ * The fix ensures that parseTagWithMap uses already-parsed values plus
6
+ * direct calls to validateTagSemantics and decodePlutusConstructor,
7
+ * instead of calling parseTag(hexString) which re-parses everything.
8
+ */
9
+
10
+ import { describe, it, expect, vi } from 'vitest'
11
+ import { useCborParser } from '../composables/useCborParser'
12
+ import { useCborTag } from '../composables/useCborTag'
13
+
14
+ describe('Tag Source Map Re-parse Elimination (Task 2-B)', () => {
15
+ describe('Correctness: parseWithSourceMap still produces correct values', () => {
16
+ it('should correctly decode tag 121 with array', () => {
17
+ const { parseWithSourceMap } = useCborParser()
18
+ const result = parseWithSourceMap('d87983010203') // Tag 121, [1, 2, 3]
19
+
20
+ expect(result.value).toEqual({
21
+ tag: 121,
22
+ value: [1, 2, 3],
23
+ plutus: { constructor: 0, fields: [1, 2, 3] }
24
+ })
25
+ })
26
+
27
+ it('should correctly decode tag 122 with array', () => {
28
+ const { parseWithSourceMap } = useCborParser()
29
+ const result = parseWithSourceMap('d87a81182a') // Tag 122, [42]
30
+
31
+ expect(result.value).toEqual({
32
+ tag: 122,
33
+ value: [42],
34
+ plutus: { constructor: 1, fields: [42] }
35
+ })
36
+ })
37
+
38
+ it('should correctly decode tag 102 alternative constructor', () => {
39
+ const { parseWithSourceMap } = useCborParser()
40
+ const result = parseWithSourceMap('d8668218c8811863') // Tag 102, [200, [99]]
41
+
42
+ expect(result.value).toEqual({
43
+ tag: 102,
44
+ value: [200, [99]],
45
+ plutus: { constructor: 200, fields: [99] }
46
+ })
47
+ })
48
+
49
+ it('should correctly decode extended constructor tag 1283', () => {
50
+ const { parseWithSourceMap } = useCborParser()
51
+ const result = parseWithSourceMap('d9050383010203') // Tag 1283, [1, 2, 3]
52
+
53
+ expect(result.value).toEqual({
54
+ tag: 1283,
55
+ value: [1, 2, 3],
56
+ plutus: { constructor: 10, fields: [1, 2, 3] }
57
+ })
58
+ })
59
+
60
+ it('should correctly decode bignum tag 2 (positive)', () => {
61
+ const { parseWithSourceMap } = useCborParser()
62
+ // Tag 2, byte string 0x01 0x00 (= 256)
63
+ const result = parseWithSourceMap('c2420100')
64
+
65
+ expect(result.value).toEqual({
66
+ tag: 2,
67
+ value: 256n
68
+ })
69
+ })
70
+
71
+ it('should correctly decode bignum tag 3 (negative)', () => {
72
+ const { parseWithSourceMap } = useCborParser()
73
+ // Tag 3, byte string 0x01 0x00 (= -1 - 256 = -257)
74
+ const result = parseWithSourceMap('c3420100')
75
+
76
+ expect(result.value).toEqual({
77
+ tag: 3,
78
+ value: -257n
79
+ })
80
+ })
81
+
82
+ it('should correctly handle tag 0 (date/time string)', () => {
83
+ const { parseWithSourceMap } = useCborParser()
84
+ // Tag 0 containing "2013-03-21T20:04:00Z"
85
+ const result = parseWithSourceMap('c074323031332d30332d32315432303a30343a30305a')
86
+
87
+ const value = result.value as any
88
+ expect(value.tag).toBe(0)
89
+ expect(value.value).toBe('2013-03-21T20:04:00Z')
90
+ })
91
+
92
+ it('should correctly handle tag 1 (epoch time)', () => {
93
+ const { parseWithSourceMap } = useCborParser()
94
+ // Tag 1 containing 1363896240
95
+ const result = parseWithSourceMap('c11a514b67b0')
96
+
97
+ expect(result.value).toEqual({
98
+ tag: 1,
99
+ value: 1363896240
100
+ })
101
+ })
102
+
103
+ it('should correctly handle nested tags', () => {
104
+ const { parseWithSourceMap } = useCborParser()
105
+ // Tag 121 -> [Tag 121 -> []]
106
+ const result = parseWithSourceMap('d87981d87980')
107
+
108
+ const outer = result.value as any
109
+ expect(outer.tag).toBe(121)
110
+ expect(outer.plutus).toEqual({ constructor: 0, fields: [{ tag: 121, value: [], plutus: { constructor: 0, fields: [] } }] })
111
+ })
112
+
113
+ it('should correctly handle tag 121 with empty array', () => {
114
+ const { parseWithSourceMap } = useCborParser()
115
+ const result = parseWithSourceMap('d87980')
116
+
117
+ expect(result.value).toEqual({
118
+ tag: 121,
119
+ value: [],
120
+ plutus: { constructor: 0, fields: [] }
121
+ })
122
+ })
123
+
124
+ it('should correctly handle self-describe tag 55799', () => {
125
+ const { parseWithSourceMap } = useCborParser()
126
+ // Tag 55799 wrapping integer 42
127
+ const result = parseWithSourceMap('d9d9f7182a')
128
+
129
+ expect(result.value).toEqual({
130
+ tag: 55799,
131
+ value: 42
132
+ })
133
+ })
134
+
135
+ it('should correctly handle non-Plutus tag with non-array value', () => {
136
+ const { parseWithSourceMap } = useCborParser()
137
+ // Tag 1 containing 0
138
+ const result = parseWithSourceMap('c100')
139
+
140
+ expect(result.value).toEqual({
141
+ tag: 1,
142
+ value: 0
143
+ })
144
+ })
145
+ })
146
+
147
+ describe('Correctness: source maps remain identical', () => {
148
+ it('should produce same source map for simple tagged value', () => {
149
+ const { parseWithSourceMap } = useCborParser()
150
+ const result = parseWithSourceMap('d879182a') // Tag 121, 42
151
+
152
+ expect(result.sourceMap).toHaveLength(2)
153
+ expect(result.sourceMap[0]).toMatchObject({
154
+ path: '',
155
+ majorType: 6,
156
+ type: 'tag(121)',
157
+ children: ['.value']
158
+ })
159
+ expect(result.sourceMap[1]).toMatchObject({
160
+ path: '.value',
161
+ majorType: 0,
162
+ parent: ''
163
+ })
164
+ })
165
+
166
+ it('should produce same source map for nested tags', () => {
167
+ const { parseWithSourceMap } = useCborParser()
168
+ const result = parseWithSourceMap('d87981d87980')
169
+
170
+ const outerTag = result.sourceMap.find(e => e.path === '')
171
+ expect(outerTag?.majorType).toBe(6)
172
+ expect(outerTag?.children).toEqual(['.value'])
173
+
174
+ const array = result.sourceMap.find(e => e.path === '.value')
175
+ expect(array?.majorType).toBe(4)
176
+ expect(array?.parent).toBe('')
177
+
178
+ const innerTag = result.sourceMap.find(e => e.path === '.value[0]')
179
+ expect(innerTag?.majorType).toBe(6)
180
+ expect(innerTag?.parent).toBe('.value')
181
+ })
182
+ })
183
+
184
+ describe('Validation still works through source-map path', () => {
185
+ it('should validate tag 2/3 bignum byte limit', () => {
186
+ const { parseWithSourceMap } = useCborParser()
187
+ // Tag 2 with a byte string exceeding the limit
188
+ // Create a very long byte string (> default 1024 bytes)
189
+ const longByteString = '59' + '0401' + 'ff'.repeat(1025)
190
+ const hex = 'c2' + longByteString
191
+
192
+ expect(() => parseWithSourceMap(hex)).toThrow(/bignum/i)
193
+ })
194
+
195
+ it('should validate tag semantics in strict mode', () => {
196
+ const { parseWithSourceMap } = useCborParser()
197
+ // Tag 0 (date/time) with integer value (should be text string)
198
+ // c0 00 = tag(0) + integer(0)
199
+ expect(() => parseWithSourceMap('c000', { strict: true })).toThrow(/tag 0/i)
200
+ })
201
+
202
+ it('should validate Plutus semantics in strict mode', () => {
203
+ const { parseWithSourceMap } = useCborParser()
204
+ // Tag 121 with non-array value (should be array)
205
+ // d8 79 00 = tag(121) + integer(0)
206
+ expect(() => parseWithSourceMap('d87900', { strict: true })).toThrow(/Plutus/i)
207
+ })
208
+ })
209
+
210
+ describe('Performance: no exponential re-parsing', () => {
211
+ it('should parse deeply nested tags in linear time', () => {
212
+ const { parseWithSourceMap } = useCborParser()
213
+
214
+ // Build a deeply nested tag: tag(121) -> [tag(121) -> [tag(121) -> ... -> []]]
215
+ // At depth D, the old code would do O(D^2) work due to re-parsing.
216
+ // With the fix, it should be O(D).
217
+ const buildNestedTagHex = (depth: number): string => {
218
+ // Each level: d8 79 81 (tag 121, 1-element array)
219
+ // Innermost: d8 79 80 (tag 121, empty array)
220
+ let hex = ''
221
+ for (let i = 0; i < depth - 1; i++) {
222
+ hex += 'd87981' // tag(121), array(1)
223
+ }
224
+ hex += 'd87980' // tag(121), array(0)
225
+ return hex
226
+ }
227
+
228
+ // Time a moderate depth (20 levels)
229
+ const hex20 = buildNestedTagHex(20)
230
+ const start20 = performance.now()
231
+ const result20 = parseWithSourceMap(hex20)
232
+ const time20 = performance.now() - start20
233
+
234
+ // Time a deeper nesting (40 levels)
235
+ const hex40 = buildNestedTagHex(40)
236
+ const start40 = performance.now()
237
+ const result40 = parseWithSourceMap(hex40)
238
+ const time40 = performance.now() - start40
239
+
240
+ // With O(D^2), doubling depth quadruples time: time40/time20 ~ 4
241
+ // With O(D), doubling depth doubles time: time40/time20 ~ 2
242
+ // Use a generous bound: if fixed, ratio should be < 3
243
+ // If unfixed (quadratic), ratio tends toward 4+
244
+ //
245
+ // Note: for small absolute times, jitter can dominate.
246
+ // The real test is that 40-deep should still complete quickly (< 100ms).
247
+ expect(time40).toBeLessThan(100) // Should be very fast with fix
248
+
249
+ // Both should parse successfully
250
+ expect((result20.value as any).tag).toBe(121)
251
+ expect((result40.value as any).tag).toBe(121)
252
+ })
253
+ })
254
+
255
+ describe('useCborTag exports', () => {
256
+ it('should export validateTagSemantics function', () => {
257
+ const tag = useCborTag()
258
+ expect(tag.validateTagSemantics).toBeDefined()
259
+ expect(typeof tag.validateTagSemantics).toBe('function')
260
+ })
261
+
262
+ it('should export decodePlutusConstructor function', () => {
263
+ const tag = useCborTag()
264
+ expect(tag.decodePlutusConstructor).toBeDefined()
265
+ expect(typeof tag.decodePlutusConstructor).toBe('function')
266
+ })
267
+ })
268
+ })
@@ -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, compareBytes, 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
@@ -485,7 +470,16 @@ export function useCborCollection() {
485
470
  // Remove spaces from hex string
486
471
  const cleanHex = hexString.replace(/\s+/g, '')
487
472
  const buffer = hexToBytes(cleanHex)
488
- return parseArrayFromBuffer(buffer, 0, options, 0)
473
+
474
+ // Set parse start time for timeout enforcement
475
+ if (options?.limits?.maxParseTime) {
476
+ parseStartTime = Date.now()
477
+ }
478
+ try {
479
+ return parseArrayFromBuffer(buffer, 0, options, 0)
480
+ } finally {
481
+ parseStartTime = 0
482
+ }
489
483
  }
490
484
 
491
485
  /**
@@ -499,7 +493,16 @@ export function useCborCollection() {
499
493
  // Remove spaces from hex string
500
494
  const cleanHex = hexString.replace(/\s+/g, '')
501
495
  const buffer = hexToBytes(cleanHex)
502
- return parseMapFromBuffer(buffer, 0, options, 0)
496
+
497
+ // Set parse start time for timeout enforcement
498
+ if (options?.limits?.maxParseTime) {
499
+ parseStartTime = Date.now()
500
+ }
501
+ try {
502
+ return parseMapFromBuffer(buffer, 0, options, 0)
503
+ } finally {
504
+ parseStartTime = 0
505
+ }
503
506
  }
504
507
 
505
508
  return {
@@ -62,6 +62,58 @@ export function useCborFloat() {
62
62
  return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - 15) * (1 + fraction / 1024)
63
63
  }
64
64
 
65
+ /**
66
+ * Checks if a float64 value can be exactly represented as float16
67
+ * Used for canonical encoding validation (RFC 8949 Section 4.2.2)
68
+ */
69
+ const fitsInFloat16 = (value: number): boolean => {
70
+ if (Number.isNaN(value) || value === Infinity || value === -Infinity) return true
71
+ if (Object.is(value, 0) || Object.is(value, -0)) return true
72
+
73
+ const abs = Math.abs(value)
74
+ // Float16 range: subnormals ~5.96e-8 to max normal 65504
75
+ if (abs > 65504) return false
76
+ if (abs < 5.960464477539063e-8) return false
77
+
78
+ // Encode to float16 and back to see if value is preserved
79
+ const sign = value < 0 ? 1 : 0
80
+ const buf = new ArrayBuffer(8)
81
+ const view = new DataView(buf)
82
+ view.setFloat64(0, abs, false)
83
+ const bits64 = view.getBigUint64(0, false)
84
+ const exp64 = Number((bits64 >> 52n) & 0x7ffn) - 1023
85
+ const mant64 = Number(bits64 & 0xfffffffffffffn)
86
+
87
+ let exp16: number
88
+ let mant16: number
89
+ if (exp64 < -14) {
90
+ // Subnormal float16
91
+ exp16 = 0
92
+ const shift = -14 - exp64
93
+ mant16 = ((1 << 10) | (mant64 >> 42)) >> shift
94
+ } else if (exp64 > 15) {
95
+ return false
96
+ } else {
97
+ exp16 = exp64 + 15
98
+ mant16 = mant64 >> 42
99
+ }
100
+
101
+ const float16Bits = (sign << 15) | (exp16 << 10) | mant16
102
+ // Decode back
103
+ const s = (float16Bits & 0x8000) >> 15
104
+ const e = (float16Bits & 0x7c00) >> 10
105
+ const f = float16Bits & 0x03ff
106
+ let reconstructed: number
107
+ if (e === 0) {
108
+ reconstructed = (s === 0 ? 1 : -1) * Math.pow(2, -14) * (f / 1024)
109
+ } else if (e === 0x1f) {
110
+ reconstructed = f === 0 ? (s === 0 ? Infinity : -Infinity) : NaN
111
+ } else {
112
+ reconstructed = (s === 0 ? 1 : -1) * Math.pow(2, e - 15) * (1 + f / 1024)
113
+ }
114
+ return reconstructed === value
115
+ }
116
+
65
117
  /**
66
118
  * Parses simple values (booleans, null, undefined, unassigned)
67
119
  *
@@ -142,7 +194,7 @@ export function useCborFloat() {
142
194
  * @param offset - Current offset
143
195
  * @returns Parsed float and bytes read
144
196
  */
145
- const parseFloatFromBuffer = (buffer: Uint8Array, offset: number): ParseResult => {
197
+ const parseFloatFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
146
198
  const initialByte = readByte(buffer, offset)
147
199
  const { majorType, additionalInfo } = extractCborHeader(initialByte)
148
200
 
@@ -156,6 +208,16 @@ export function useCborFloat() {
156
208
  if (offset + 2 >= buffer.length) {
157
209
  throw new Error('Unexpected end of buffer while reading Float16')
158
210
  }
211
+ if (options?.validateCanonical) {
212
+ const byte1 = readByte(buffer, offset + 1)
213
+ const byte2 = readByte(buffer, offset + 2)
214
+ const bits = (byte1 << 8) | byte2
215
+ const exp = (bits >> 10) & 0x1f
216
+ const mant = bits & 0x03ff
217
+ if (exp === 0x1f && mant !== 0 && bits !== 0x7e00) {
218
+ throw new Error('Non-canonical NaN encoding: use 0xf97e00')
219
+ }
220
+ }
159
221
  const value = float16ToFloat64(buffer, offset + 1)
160
222
  return { value, bytesRead: 3 }
161
223
  }
@@ -168,6 +230,15 @@ export function useCborFloat() {
168
230
  // Use DataView for proper IEEE 754 parsing
169
231
  const dataView = new DataView(buffer.buffer, buffer.byteOffset + offset + 1, 4)
170
232
  const value = dataView.getFloat32(0, false) // false = big-endian
233
+ if (options?.validateCanonical) {
234
+ if (Number.isNaN(value)) {
235
+ throw new Error('Non-canonical NaN encoding: NaN must use float16 0xf97e00')
236
+ }
237
+ // Check if value could be represented as float16 (shortest form)
238
+ if (fitsInFloat16(value)) {
239
+ throw new Error('Non-canonical float32: value fits in float16, use shortest encoding')
240
+ }
241
+ }
171
242
  return { value, bytesRead: 5 }
172
243
  }
173
244
 
@@ -179,6 +250,20 @@ export function useCborFloat() {
179
250
  // Use DataView for proper IEEE 754 parsing
180
251
  const dataView = new DataView(buffer.buffer, buffer.byteOffset + offset + 1, 8)
181
252
  const value = dataView.getFloat64(0, false) // false = big-endian
253
+ if (options?.validateCanonical) {
254
+ if (Number.isNaN(value)) {
255
+ throw new Error('Non-canonical NaN encoding: NaN must use float16 0xf97e00')
256
+ }
257
+ // Check if value could be represented in a smaller float
258
+ if (fitsInFloat16(value)) {
259
+ throw new Error('Non-canonical float64: value fits in float16/float32, use shortest encoding')
260
+ }
261
+ // Check if float64 value fits in float32
262
+ const f32 = Math.fround(value)
263
+ if (f32 === value || (Object.is(value, -0) && Object.is(f32, -0))) {
264
+ throw new Error('Non-canonical float64: value fits in float16/float32, use shortest encoding')
265
+ }
266
+ }
182
267
  return { value, bytesRead: 9 }
183
268
  }
184
269
 
@@ -194,7 +279,7 @@ export function useCborFloat() {
194
279
  * @param offset - Current offset
195
280
  * @returns Parsed value and bytes read
196
281
  */
197
- const parseFromBuffer = (buffer: Uint8Array, offset: number): ParseResult => {
282
+ const parseFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
198
283
  const initialByte = readByte(buffer, offset)
199
284
  const { majorType, additionalInfo } = extractCborHeader(initialByte)
200
285
 
@@ -205,7 +290,7 @@ export function useCborFloat() {
205
290
  // Determine if it's a float or simple value based on additional info
206
291
  if (additionalInfo === 25 || additionalInfo === 26 || additionalInfo === 27) {
207
292
  // Float16, Float32, or Float64
208
- return parseFloatFromBuffer(buffer, offset)
293
+ return parseFloatFromBuffer(buffer, offset, options)
209
294
  } else {
210
295
  // Simple value (including false, true, null, undefined)
211
296
  return parseSimpleFromBuffer(buffer, offset)
@@ -216,7 +301,7 @@ export function useCborFloat() {
216
301
  * Parses CBOR simple value from hex string
217
302
  *
218
303
  * @param hexString - CBOR hex string
219
- * @param _options - Parser options (optional, for future use)
304
+ * @param _options - Parser options (reserved for future use)
220
305
  * @returns Parsed simple value and bytes read
221
306
  */
222
307
  const parseSimple = (hexString: string, _options?: ParseOptions): ParseResult => {
@@ -231,9 +316,9 @@ export function useCborFloat() {
231
316
  * @param _options - Parser options (optional, for future use)
232
317
  * @returns Parsed float and bytes read
233
318
  */
234
- const parseFloat = (hexString: string, _options?: ParseOptions): ParseResult => {
319
+ const parseFloat = (hexString: string, options?: ParseOptions): ParseResult => {
235
320
  const buffer = hexToBytes(hexString)
236
- return parseFloatFromBuffer(buffer, 0)
321
+ return parseFloatFromBuffer(buffer, 0, options)
237
322
  }
238
323
 
239
324
  /**
@@ -243,14 +328,15 @@ export function useCborFloat() {
243
328
  * @param _options - Parser options (optional, for future use)
244
329
  * @returns Parsed value and bytes read
245
330
  */
246
- const parse = (hexString: string, _options?: ParseOptions): ParseResult => {
331
+ const parse = (hexString: string, options?: ParseOptions): ParseResult => {
247
332
  const buffer = hexToBytes(hexString)
248
- return parseFromBuffer(buffer, 0)
333
+ return parseFromBuffer(buffer, 0, options)
249
334
  }
250
335
 
251
336
  return {
252
337
  parse,
253
338
  parseFloat,
254
- parseSimple
339
+ parseSimple,
340
+ parseFromBuffer
255
341
  }
256
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
  }