@marcuspuchalla/nachos 0.1.1 → 0.1.3

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 (46) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/{chunk-7CFYWHS6.js → chunk-2MTLSQ7E.js} +58 -10
  3. package/dist/chunk-2MTLSQ7E.js.map +1 -0
  4. package/dist/{chunk-ZRPJUEIZ.js → chunk-5A5T56JB.js} +170 -22
  5. package/dist/chunk-5A5T56JB.js.map +1 -0
  6. package/dist/{chunk-2HBCILJS.cjs → chunk-PTWN7K3Y.cjs} +169 -21
  7. package/dist/chunk-PTWN7K3Y.cjs.map +1 -0
  8. package/dist/{chunk-2FUTHZQQ.cjs → chunk-R62CQQNI.cjs} +58 -10
  9. package/dist/chunk-R62CQQNI.cjs.map +1 -0
  10. package/dist/encoder/index.cjs +13 -13
  11. package/dist/encoder/index.js +1 -1
  12. package/dist/index.cjs +20 -20
  13. package/dist/index.d.cts +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +4 -4
  16. package/dist/metafile-cjs.json +1 -1
  17. package/dist/metafile-esm.json +1 -1
  18. package/dist/parser/index.cjs +14 -14
  19. package/dist/parser/index.d.cts +1 -1
  20. package/dist/parser/index.d.ts +1 -1
  21. package/dist/parser/index.js +1 -1
  22. package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-Cs1CZuXZ.d.cts} +2 -2
  23. package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-xV2Pz2VY.d.ts} +2 -2
  24. package/package.json +1 -1
  25. package/src/__tests__/roundtrip.test.ts +702 -0
  26. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +26 -0
  27. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +812 -0
  28. package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
  29. package/src/encoder/composables/useCborCollectionEncoder.ts +30 -0
  30. package/src/encoder/composables/useCborEncoder.ts +6 -1
  31. package/src/encoder/composables/useCborSimpleEncoder.ts +7 -2
  32. package/src/encoder/composables/useCborStringEncoder.ts +23 -10
  33. package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
  34. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +2 -2
  35. package/src/parser/__tests__/cbor-standard-tags.test.ts +29 -0
  36. package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
  37. package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
  38. package/src/parser/composables/useCborFloat.ts +93 -8
  39. package/src/parser/composables/useCborParser.ts +0 -19
  40. package/src/parser/composables/useCborString.ts +22 -4
  41. package/src/parser/composables/useCborTag.ts +104 -16
  42. package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
  43. package/dist/chunk-2HBCILJS.cjs.map +0 -1
  44. package/dist/chunk-7CFYWHS6.js.map +0 -1
  45. package/dist/chunk-ZRPJUEIZ.js.map +0 -1
  46. package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
@@ -329,6 +329,20 @@ describe('CBOR String Encoder', () => {
329
329
  expect(() => encodeByteStringIndefinite([new Uint8Array([1])]))
330
330
  .toThrow('Indefinite-length encoding not allowed in canonical mode')
331
331
  })
332
+
333
+ it('should reject allowIndefinite=false for text strings', () => {
334
+ const { encodeTextStringIndefinite } = useCborStringEncoder({ allowIndefinite: false })
335
+
336
+ expect(() => encodeTextStringIndefinite(['a', 'b']))
337
+ .toThrow('Indefinite-length encoding is not allowed')
338
+ })
339
+
340
+ it('should reject allowIndefinite=false for byte strings', () => {
341
+ const { encodeByteStringIndefinite } = useCborStringEncoder({ allowIndefinite: false })
342
+
343
+ expect(() => encodeByteStringIndefinite([new Uint8Array([1])]))
344
+ .toThrow('Indefinite-length encoding is not allowed')
345
+ })
332
346
  })
333
347
 
334
348
  describe('Byte string array handling', () => {
@@ -105,6 +105,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
105
105
  const newCtx = { ...ctx, depth: ctx.depth + 1 }
106
106
 
107
107
  if (isIndefinite) {
108
+ if (ctx.options.allowIndefinite === false) {
109
+ throw new Error('Indefinite-length encoding is not allowed')
110
+ }
108
111
  // Use indefinite encoding - but need to recursively encode items
109
112
  const parts: Uint8Array[] = [new Uint8Array([0x9f])] // Start marker
110
113
  for (const item of value) {
@@ -130,6 +133,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
130
133
  const newCtx = { ...ctx, depth: ctx.depth + 1 }
131
134
 
132
135
  if (isIndefinite) {
136
+ if (ctx.options.allowIndefinite === false) {
137
+ throw new Error('Indefinite-length encoding is not allowed')
138
+ }
133
139
  // Use indefinite encoding
134
140
  const entries: Array<[EncodableValue, EncodableValue]> =
135
141
  value instanceof Map
@@ -233,6 +239,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
233
239
 
234
240
  // Handle indefinite-length encoding
235
241
  if (encodeOptions?.indefinite || isIndefinite) {
242
+ if (options.allowIndefinite === false) {
243
+ throw new Error('Indefinite-length encoding is not allowed')
244
+ }
236
245
  if (options.canonical) {
237
246
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
238
247
  }
@@ -256,6 +265,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
256
265
  * @returns Encoded CBOR bytes and hex string
257
266
  */
258
267
  const encodeArrayIndefinite = (array: EncodableValue[]): EncodeResult => {
268
+ if (options.allowIndefinite === false) {
269
+ throw new Error('Indefinite-length encoding is not allowed')
270
+ }
259
271
  if (options.canonical) {
260
272
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
261
273
  }
@@ -313,6 +325,18 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
313
325
  })
314
326
  }
315
327
 
328
+ if (ctx.options.rejectDuplicateKeys) {
329
+ const seen = new Set<string>()
330
+ for (const [key] of entries) {
331
+ const keyBytes = encodeValue(key, { ...ctx, depth: ctx.depth + 1, bytesWritten: 0 })
332
+ const keyHex = bytesToHex(keyBytes)
333
+ if (seen.has(keyHex)) {
334
+ throw new Error('Duplicate map key detected')
335
+ }
336
+ seen.add(keyHex)
337
+ }
338
+ }
339
+
316
340
  const header = encodeLengthHeader(5, entries.length)
317
341
  const parts: Uint8Array[] = [header]
318
342
 
@@ -354,6 +378,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
354
378
 
355
379
  // Handle indefinite-length encoding
356
380
  if (encodeOptions?.indefinite || isIndefinite) {
381
+ if (options.allowIndefinite === false) {
382
+ throw new Error('Indefinite-length encoding is not allowed')
383
+ }
357
384
  if (options.canonical) {
358
385
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
359
386
  }
@@ -379,6 +406,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
379
406
  const encodeMapIndefinite = (
380
407
  map: Map<EncodableValue, EncodableValue> | { [key: string]: EncodableValue }
381
408
  ): EncodeResult => {
409
+ if (options.allowIndefinite === false) {
410
+ throw new Error('Indefinite-length encoding is not allowed')
411
+ }
382
412
  if (options.canonical) {
383
413
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
384
414
  }
@@ -42,6 +42,11 @@ import { useCborByteString, useCborTextString } from '../../parser/composables/u
42
42
  export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
43
43
  const options = { ...DEFAULT_ENCODE_OPTIONS, ...globalOptions }
44
44
 
45
+ // Canonical mode overrides: indefinite-length is forbidden per RFC 8949 Section 4.2
46
+ if (options.canonical && options.allowIndefinite) {
47
+ options.allowIndefinite = false
48
+ }
49
+
45
50
  // Get all specialized encoders
46
51
  const { encodeInteger } = useCborIntegerEncoder()
47
52
  const { encodeTextString, encodeByteString } = useCborStringEncoder(options)
@@ -78,7 +83,7 @@ export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
78
83
  // Handle numbers
79
84
  if (typeof value === 'number') {
80
85
  // Check if it's an integer
81
- if (Number.isInteger(value) && Number.isSafeInteger(value)) {
86
+ if (Number.isSafeInteger(value)) {
82
87
  return encodeInteger(value)
83
88
  }
84
89
  // It's a float
@@ -91,10 +91,15 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
91
91
  let exp16: number
92
92
  let mant16: number
93
93
 
94
- if (exp64 < -14) {
95
- // Subnormal or zero
94
+ if (exp64 < -24) {
95
+ // Too small even for subnormal float16
96
96
  exp16 = 0
97
97
  mant16 = 0
98
+ } else if (exp64 < -14) {
99
+ // Subnormal float16: shift the implicit 1.mantissa into the fraction bits
100
+ exp16 = 0
101
+ const shift = -14 - exp64
102
+ mant16 = (((1 << 10) | (mant64 >> 42)) + ((1 << (shift - 1)) - 1)) >> shift
98
103
  } else if (exp64 > 15) {
99
104
  // Overflow to infinity
100
105
  exp16 = 31
@@ -106,6 +106,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
106
106
 
107
107
  // Handle indefinite-length encoding
108
108
  if (encodeOptions?.indefinite || Array.isArray(data) || isIndefinite) {
109
+ if (options.allowIndefinite === false) {
110
+ throw new Error('Indefinite-length encoding is not allowed')
111
+ }
109
112
  if (options.canonical) {
110
113
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
111
114
  }
@@ -122,14 +125,15 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
122
125
 
123
126
  // Definite-length encoding - extract bytes from CborByteString if needed
124
127
  const bytes = isCborByteString(data) ? data.bytes : (data as Uint8Array)
125
- const header = encodeLengthHeader(2, bytes.length)
126
- const result = concatenateUint8Arrays([header, bytes])
127
128
 
128
- // Check output size limit
129
- if (result.length > options.maxOutputSize) {
130
- throw new Error('Encoded output exceeds maximum size')
129
+ // Check size limit before allocating (header is at most 9 bytes)
130
+ if (bytes.length + 9 > options.maxOutputSize) {
131
+ throw new Error(`Encoded output exceeds maximum size`)
131
132
  }
132
133
 
134
+ const header = encodeLengthHeader(2, bytes.length)
135
+ const result = concatenateUint8Arrays([header, bytes])
136
+
133
137
  return {
134
138
  bytes: result,
135
139
  hex: bytesToHex(result)
@@ -146,6 +150,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
146
150
  * @returns Encoded CBOR bytes and hex string
147
151
  */
148
152
  const encodeByteStringIndefinite = (chunks: Uint8Array[]): EncodeResult => {
153
+ if (options.allowIndefinite === false) {
154
+ throw new Error('Indefinite-length encoding is not allowed')
155
+ }
149
156
  if (options.canonical) {
150
157
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
151
158
  }
@@ -187,6 +194,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
187
194
 
188
195
  // Handle indefinite-length encoding
189
196
  if (encodeOptions?.indefinite || isIndefinite) {
197
+ if (options.allowIndefinite === false) {
198
+ throw new Error('Indefinite-length encoding is not allowed')
199
+ }
190
200
  if (options.canonical) {
191
201
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
192
202
  }
@@ -208,14 +218,14 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
208
218
  const encoder = new TextEncoder()
209
219
  const utf8Bytes = encoder.encode(textStr)
210
220
 
221
+ // Check size limit before allocating (header is at most 9 bytes)
222
+ if (utf8Bytes.length + 9 > options.maxOutputSize) {
223
+ throw new Error(`Encoded output exceeds maximum size`)
224
+ }
225
+
211
226
  const header = encodeLengthHeader(3, utf8Bytes.length)
212
227
  const result = concatenateUint8Arrays([header, utf8Bytes])
213
228
 
214
- // Check output size limit
215
- if (result.length > options.maxOutputSize) {
216
- throw new Error('Encoded output exceeds maximum size')
217
- }
218
-
219
229
  return {
220
230
  bytes: result,
221
231
  hex: bytesToHex(result)
@@ -232,6 +242,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
232
242
  * @returns Encoded CBOR bytes and hex string
233
243
  */
234
244
  const encodeTextStringIndefinite = (chunks: string[]): EncodeResult => {
245
+ if (options.allowIndefinite === false) {
246
+ throw new Error('Indefinite-length encoding is not allowed')
247
+ }
235
248
  if (options.canonical) {
236
249
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
237
250
  }
@@ -211,6 +211,47 @@ describe('useCborFloat - Error Handling', () => {
211
211
  })
212
212
  })
213
213
 
214
+ describe('Canonical float validation', () => {
215
+ it('should reject float32 when value fits in float16', () => {
216
+ const { parseFloat } = useCborFloat()
217
+
218
+ // 1.0 fits in float16, but encoded as float32
219
+ expect(() => parseFloat('fa3f800000', { validateCanonical: true }))
220
+ .toThrow(/canonical.*float16/i)
221
+ })
222
+
223
+ it('should reject float64 when value fits in float32', () => {
224
+ const { parseFloat } = useCborFloat()
225
+
226
+ // 1.0 fits in float32, but encoded as float64
227
+ expect(() => parseFloat('fb3ff0000000000000', { validateCanonical: true }))
228
+ .toThrow(/canonical.*float16\/float32/i)
229
+ })
230
+
231
+ it('should reject non-canonical NaN encodings for float32/float64', () => {
232
+ const { parseFloat } = useCborFloat()
233
+
234
+ // Float16 NaN with non-canonical payload
235
+ expect(() => parseFloat('f97e01', { validateCanonical: true }))
236
+ .toThrow(/nan/i)
237
+
238
+ // Float32 NaN
239
+ expect(() => parseFloat('fa7fc00000', { validateCanonical: true }))
240
+ .toThrow(/nan/i)
241
+
242
+ // Float64 NaN
243
+ expect(() => parseFloat('fb7ff8000000000001', { validateCanonical: true }))
244
+ .toThrow(/nan/i)
245
+ })
246
+
247
+ it('should accept float16 when canonical validation is enabled', () => {
248
+ const { parseFloat } = useCborFloat()
249
+
250
+ const result = parseFloat('f93e00', { validateCanonical: true })
251
+ expect(result.value).toBe(1.5)
252
+ })
253
+ })
254
+
214
255
  describe('Wrong Major Type in parse()', () => {
215
256
  it('should throw error when auto-detecting with wrong major type', () => {
216
257
  const { parse } = useCborFloat()
@@ -261,8 +261,8 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
261
261
  it('should use default tag depth limit when not specified', () => {
262
262
  const { parseTag } = useCborTag()
263
263
 
264
- // 100 nested tags (exceeds default 64)
265
- const nested = 'c0'.repeat(100) + '00'
264
+ // 150 nested tags (exceeds default 100)
265
+ const nested = 'c0'.repeat(150) + '00'
266
266
 
267
267
  expect(() => parseTag(nested)) // No options = use defaults
268
268
  .toThrow(/tag nesting depth.*exceeds/i)
@@ -221,6 +221,35 @@ describe('CBOR Standard Tags (RFC 8949)', () => {
221
221
  })
222
222
  })
223
223
 
224
+ describe('Tags 2-3: Bignums', () => {
225
+ it('should parse tag 2 with byte string', () => {
226
+ // c2 (tag 2) + 41 (1-byte byte string) + 01
227
+ const hex = 'c24101'
228
+ const result = parse(hex)
229
+
230
+ expect(result.value).toMatchObject({
231
+ tag: 2,
232
+ value: 1n
233
+ })
234
+ })
235
+
236
+ it('should reject non-byte-string tag 2 in strict mode', () => {
237
+ // c2 (tag 2) + 01 (integer)
238
+ const hex = 'c201'
239
+
240
+ expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
241
+ .toThrow(/byte string/i)
242
+ })
243
+
244
+ it('should reject non-byte-string tag 3 in strict mode', () => {
245
+ // c3 (tag 3) + 01 (integer)
246
+ const hex = 'c301'
247
+
248
+ expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
249
+ .toThrow(/byte string/i)
250
+ })
251
+ })
252
+
224
253
  describe('Tag 32: URI', () => {
225
254
  it('should parse valid URI', () => {
226
255
  // d8 20 (tag 32) + 76 (22-byte text) + "http://www.example.com"
@@ -112,8 +112,8 @@ describe('useCborString - Error Handling', () => {
112
112
  // 5f (indefinite byte string) + 6161 (text "a") + ff (break)
113
113
  const buffer = new Uint8Array([0x5f, 0x61, 0x61, 0xff])
114
114
 
115
- // The error is thrown when trying to parse the text string as a byte string
116
- expect(() => parseByteString(buffer, 0)).toThrow('Expected major type 2')
115
+ // The error is thrown when validating the chunk type
116
+ expect(() => parseByteString(buffer, 0)).toThrow('chunks must be byte strings')
117
117
  })
118
118
  })
119
119
 
@@ -125,8 +125,8 @@ describe('useCborString - Error Handling', () => {
125
125
  // 7f (indefinite text string) + 4161 (byte string containing 'a') + ff (break)
126
126
  const buffer = new Uint8Array([0x7f, 0x41, 0x61, 0xff])
127
127
 
128
- // The error is thrown when trying to parse the byte string as a text string
129
- expect(() => parseTextString(buffer, 0)).toThrow('Expected major type 3')
128
+ // The error is thrown when validating the chunk type
129
+ expect(() => parseTextString(buffer, 0)).toThrow('chunks must be text strings')
130
130
  })
131
131
  })
132
132
 
@@ -20,7 +20,7 @@ describe('useCborTag - Error Handling', () => {
20
20
 
21
21
  // Tag with array that's incomplete
22
22
  // c1 (tag 1) + 82 (array of 2) + 01 (element 1) - missing element 2
23
- expect(() => parseTag('c18201')).toThrow('Unexpected end of buffer at offset')
23
+ expect(() => parseTag('c18201')).toThrow('Unexpected end of buffer')
24
24
  })
25
25
  })
26
26
 
@@ -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,9 +328,9 @@ 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 {
@@ -793,25 +793,6 @@ export function useCborParser() {
793
793
  }
794
794
  }
795
795
 
796
- /**
797
- * Get tag number for description
798
- * Note: Reserved for future tag description features
799
- */
800
- // const getTagNumber = (buffer: Uint8Array, offset: number): number => {
801
- // const initialByte = readByte(buffer, offset)
802
- // const { additionalInfo } = extractCborHeader(initialByte)
803
- //
804
- // if (additionalInfo < 24) return additionalInfo
805
- // if (additionalInfo === 24) return readByte(buffer, offset + 1)
806
- // if (additionalInfo === 25) return readUint(buffer, offset + 1, 2)
807
- // if (additionalInfo === 26) return readUint(buffer, offset + 1, 4)
808
- // if (additionalInfo === 27) {
809
- // const bigNum = readBigUint(buffer, offset + 1, 8)
810
- // return bigNum <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bigNum) : -1
811
- // }
812
- // return -1
813
- // }
814
-
815
796
  /**
816
797
  * Get simple type description
817
798
  */
@@ -133,8 +133,17 @@ export function useCborString() {
133
133
  break
134
134
  }
135
135
 
136
- // Parse chunk (must be definite-length byte string)
137
- // Use try-catch to provide better error context for indefinite strings
136
+ // Validate chunk is definite-length (RFC 8949 Section 3.2.3)
137
+ const chunkInitialByte = readByte(buffer, currentOffset)
138
+ const chunkHeader = extractCborHeader(chunkInitialByte)
139
+ if (chunkHeader.majorType !== 2) {
140
+ throw new Error(`Indefinite byte string chunks must be byte strings (major type 2), got ${chunkHeader.majorType}`)
141
+ }
142
+ if (chunkHeader.additionalInfo === 31) {
143
+ throw new Error('Indefinite byte string chunks must be definite-length (RFC 8949 Section 3.2.3)')
144
+ }
145
+
146
+ // Parse chunk
138
147
  let chunkResult
139
148
  try {
140
149
  chunkResult = parseByteString(buffer, currentOffset, options)
@@ -237,8 +246,17 @@ export function useCborString() {
237
246
  break
238
247
  }
239
248
 
240
- // Parse chunk (must be definite-length text string)
241
- // Use try-catch to provide better error context for indefinite strings
249
+ // Validate chunk is definite-length (RFC 8949 Section 3.2.3)
250
+ const chunkInitialByte = readByte(buffer, currentOffset)
251
+ const chunkHeader = extractCborHeader(chunkInitialByte)
252
+ if (chunkHeader.majorType !== 3) {
253
+ throw new Error(`Indefinite text string chunks must be text strings (major type 3), got ${chunkHeader.majorType}`)
254
+ }
255
+ if (chunkHeader.additionalInfo === 31) {
256
+ throw new Error('Indefinite text string chunks must be definite-length (RFC 8949 Section 3.2.3)')
257
+ }
258
+
259
+ // Parse chunk
242
260
  let chunkResult
243
261
  try {
244
262
  chunkResult = parseTextString(buffer, currentOffset, options)