@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
@@ -96,6 +96,132 @@ describe('CBOR Simple Values and Floats Encoder', () => {
96
96
  expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x7e, 0x00]))
97
97
  expect(result.hex).toBe('f97e00')
98
98
  })
99
+
100
+ it('should encode 1.5 as float16', () => {
101
+ const { encodeFloat } = useCborSimpleEncoder()
102
+ const result = encodeFloat(1.5, 16)
103
+
104
+ // 1.5 = 0 01111 1000000000 = 0x3e00
105
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x3e, 0x00]))
106
+ expect(result.hex).toBe('f93e00')
107
+ })
108
+
109
+ it('should encode -2.0 as float16', () => {
110
+ const { encodeFloat } = useCborSimpleEncoder()
111
+ const result = encodeFloat(-2.0, 16)
112
+
113
+ // -2.0 = 1 10000 0000000000 = 0xc000
114
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0xc0, 0x00]))
115
+ expect(result.hex).toBe('f9c000')
116
+ })
117
+
118
+ it('should encode 0.5 as float16', () => {
119
+ const { encodeFloat } = useCborSimpleEncoder()
120
+ const result = encodeFloat(0.5, 16)
121
+
122
+ // 0.5 = 0 01110 0000000000 = 0x3800
123
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x38, 0x00]))
124
+ expect(result.hex).toBe('f93800')
125
+ })
126
+
127
+ it('should encode 65504 (max finite float16) as float16', () => {
128
+ const { encodeFloat } = useCborSimpleEncoder()
129
+ const result = encodeFloat(65504, 16)
130
+
131
+ // 65504 = 0 11110 1111111111 = 0x7bff
132
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x7b, 0xff]))
133
+ expect(result.hex).toBe('f97bff')
134
+ })
135
+ })
136
+
137
+ describe('Float16 IEEE 754 round-half-to-even', () => {
138
+ // Helper to construct a float64 value from raw mantissa bits
139
+ // exp64=0 means biased exponent=1023, so the value is 1.mantissa
140
+ function makeFloat64(exp64: number, mant64: number, sign = 0): number {
141
+ const biasedExp = BigInt(exp64 + 1023)
142
+ const bits = (BigInt(sign) << 63n) | (biasedExp << 52n) | BigInt(mant64)
143
+ const buf = new ArrayBuffer(8)
144
+ const view = new DataView(buf)
145
+ view.setBigUint64(0, bits, false)
146
+ return view.getFloat64(0, false)
147
+ }
148
+
149
+ it('should round down at midpoint when truncated mantissa is even (round-half-to-even)', () => {
150
+ const { encodeFloat } = useCborSimpleEncoder()
151
+ // Construct: mant16_truncated=256 (even), guard=1, round=0, sticky=0
152
+ // mant64 = (256 << 42) | (1 << 41)
153
+ const mant64 = 256 * Math.pow(2, 42) + Math.pow(2, 41)
154
+ const value = makeFloat64(0, mant64)
155
+ const result = encodeFloat(value, 16)
156
+
157
+ // Should round DOWN to mant16=256 (even), exp16=15
158
+ // float16 = 0 01111 0100000000 = 0x3d00
159
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x3d, 0x00]))
160
+ })
161
+
162
+ it('should round up at midpoint when truncated mantissa is odd (round-half-to-even)', () => {
163
+ const { encodeFloat } = useCborSimpleEncoder()
164
+ // Construct: mant16_truncated=257 (odd), guard=1, round=0, sticky=0
165
+ const mant64 = 257 * Math.pow(2, 42) + Math.pow(2, 41)
166
+ const value = makeFloat64(0, mant64)
167
+ const result = encodeFloat(value, 16)
168
+
169
+ // Should round UP to mant16=258 (even), exp16=15
170
+ // float16 = 0 01111 0100000010 = 0x3d02
171
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x3d, 0x02]))
172
+ })
173
+
174
+ it('should round up when above midpoint (guard=1, sticky bits set)', () => {
175
+ const { encodeFloat } = useCborSimpleEncoder()
176
+ // Construct: mant16_truncated=256 (even), guard=1, sticky=1 -> above midpoint
177
+ const mant64 = 256 * Math.pow(2, 42) + Math.pow(2, 41) + 1
178
+ const value = makeFloat64(0, mant64)
179
+ const result = encodeFloat(value, 16)
180
+
181
+ // Should round UP to mant16=257, exp16=15
182
+ // float16 = 0 01111 0100000001 = 0x3d01
183
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x3d, 0x01]))
184
+ })
185
+
186
+ it('should truncate when below midpoint (guard=0)', () => {
187
+ const { encodeFloat } = useCborSimpleEncoder()
188
+ // Construct: mant16_truncated=256, guard=0, some lower bits set
189
+ // mant64 = (256 << 42) | (1 << 40) -- only round bit, no guard
190
+ const mant64 = 256 * Math.pow(2, 42) + Math.pow(2, 40)
191
+ const value = makeFloat64(0, mant64)
192
+ const result = encodeFloat(value, 16)
193
+
194
+ // Should truncate to mant16=256, exp16=15
195
+ // float16 = 0 01111 0100000000 = 0x3d00
196
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x3d, 0x00]))
197
+ })
198
+
199
+ it('should handle mantissa overflow from rounding (bump exponent)', () => {
200
+ const { encodeFloat } = useCborSimpleEncoder()
201
+ // Construct: mant16_truncated=0x3FF (1023, odd), guard=1, sticky=1
202
+ // Rounding up gives 0x400, which overflows 10-bit mantissa
203
+ // Should become mant16=0, exp16+1
204
+ const mant64 = 0x3FF * Math.pow(2, 42) + Math.pow(2, 41) + 1
205
+ const value = makeFloat64(0, mant64)
206
+ const result = encodeFloat(value, 16)
207
+
208
+ // Mantissa overflows: exp16 bumps from 15 to 16, mant16=0
209
+ // float16 = 0 10000 0000000000 = 0x4000 = 2.0
210
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x40, 0x00]))
211
+ })
212
+
213
+ it('should handle mantissa overflow to infinity at max exponent', () => {
214
+ const { encodeFloat } = useCborSimpleEncoder()
215
+ // Construct: exp64=15 (exp16=30, max normal), mant16_truncated=0x3FF, guard=1, sticky=1
216
+ // Rounding overflows mantissa, bumps exp16 to 31 = infinity
217
+ const mant64 = 0x3FF * Math.pow(2, 42) + Math.pow(2, 41) + 1
218
+ const value = makeFloat64(15, mant64)
219
+ const result = encodeFloat(value, 16)
220
+
221
+ // exp16=30 + overflow = 31 (infinity), mant16=0
222
+ // float16 = 0 11111 0000000000 = 0x7c00 = +Infinity
223
+ expect(result.bytes).toEqual(new Uint8Array([0xf9, 0x7c, 0x00]))
224
+ })
99
225
  })
100
226
 
101
227
  describe('Float32 (single precision)', () => {
@@ -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', () => {
@@ -9,6 +9,7 @@ import { DEFAULT_ENCODE_OPTIONS, INDEFINITE_SYMBOL, ALL_ENTRIES_SYMBOL } from '.
9
9
  import { bytesToHex, concatenateUint8Arrays, compareBytes } from '../utils'
10
10
  import { useCborIntegerEncoder } from './useCborIntegerEncoder'
11
11
  import { useCborStringEncoder } from './useCborStringEncoder'
12
+ import { useCborSimpleEncoder } from './useCborSimpleEncoder'
12
13
  import { useCborByteString, useCborTextString } from '../../parser/composables/useCborStringTypes'
13
14
 
14
15
  interface CollectionEncodeOptions {
@@ -49,6 +50,7 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
49
50
  // Get other encoders
50
51
  const { encodeInteger } = useCborIntegerEncoder()
51
52
  const { encodeTextString, encodeByteString } = useCborStringEncoder(globalOptions)
53
+ const { encodeFloat } = useCborSimpleEncoder(options)
52
54
  const { isCborByteString } = useCborByteString()
53
55
  const { isCborTextString } = useCborTextString()
54
56
 
@@ -90,7 +92,16 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
90
92
  // true: 0xf5, false: 0xf4
91
93
  return new Uint8Array([value ? 0xf5 : 0xf4])
92
94
  }
93
- else if (typeof value === 'number' || typeof value === 'bigint') {
95
+ else if (typeof value === 'number') {
96
+ if (Object.is(value, -0)) {
97
+ return encodeFloat(value, 16).bytes
98
+ }
99
+ if (Number.isInteger(value) && Number.isSafeInteger(value)) {
100
+ return encodeInteger(value).bytes
101
+ }
102
+ return encodeFloat(value).bytes
103
+ }
104
+ else if (typeof value === 'bigint') {
94
105
  return encodeInteger(value).bytes
95
106
  }
96
107
  else if (typeof value === 'string' || isCborTextString(value)) {
@@ -105,6 +116,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
105
116
  const newCtx = { ...ctx, depth: ctx.depth + 1 }
106
117
 
107
118
  if (isIndefinite) {
119
+ if (ctx.options.allowIndefinite === false) {
120
+ throw new Error('Indefinite-length encoding is not allowed')
121
+ }
108
122
  // Use indefinite encoding - but need to recursively encode items
109
123
  const parts: Uint8Array[] = [new Uint8Array([0x9f])] // Start marker
110
124
  for (const item of value) {
@@ -130,6 +144,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
130
144
  const newCtx = { ...ctx, depth: ctx.depth + 1 }
131
145
 
132
146
  if (isIndefinite) {
147
+ if (ctx.options.allowIndefinite === false) {
148
+ throw new Error('Indefinite-length encoding is not allowed')
149
+ }
133
150
  // Use indefinite encoding
134
151
  const entries: Array<[EncodableValue, EncodableValue]> =
135
152
  value instanceof Map
@@ -205,12 +222,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
205
222
 
206
223
  const result = concatenateUint8Arrays(parts)
207
224
 
208
- // Check output size
209
- ctx.bytesWritten += result.length
210
- if (ctx.bytesWritten > ctx.options.maxOutputSize) {
211
- throw new Error('Encoded output exceeds maximum size')
212
- }
213
-
214
225
  return {
215
226
  bytes: result,
216
227
  hex: bytesToHex(result)
@@ -233,6 +244,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
233
244
 
234
245
  // Handle indefinite-length encoding
235
246
  if (encodeOptions?.indefinite || isIndefinite) {
247
+ if (options.allowIndefinite === false) {
248
+ throw new Error('Indefinite-length encoding is not allowed')
249
+ }
236
250
  if (options.canonical) {
237
251
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
238
252
  }
@@ -242,7 +256,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
242
256
  // Definite-length encoding
243
257
  const ctx: EncodeContext = {
244
258
  depth: 0,
245
- bytesWritten: 0,
246
259
  options
247
260
  }
248
261
 
@@ -256,6 +269,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
256
269
  * @returns Encoded CBOR bytes and hex string
257
270
  */
258
271
  const encodeArrayIndefinite = (array: EncodableValue[]): EncodeResult => {
272
+ if (options.allowIndefinite === false) {
273
+ throw new Error('Indefinite-length encoding is not allowed')
274
+ }
259
275
  if (options.canonical) {
260
276
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
261
277
  }
@@ -264,7 +280,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
264
280
 
265
281
  const ctx: EncodeContext = {
266
282
  depth: 0,
267
- bytesWritten: 0,
268
283
  options
269
284
  }
270
285
 
@@ -304,13 +319,33 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
304
319
  : Object.entries(map)
305
320
  }
306
321
 
307
- // In canonical mode, sort entries by encoded key (unless using allEntries for byte-perfect)
322
+ // Pre-encode keys once for canonical sorting and/or duplicate detection
323
+ // This avoids re-encoding keys O(N log N) times inside the sort comparator
324
+ let preEncodedKeys: Uint8Array[] | null = null
325
+
308
326
  if (ctx.options.canonical && !(map as any)[ALL_ENTRIES_SYMBOL]) {
309
- entries.sort((a, b) => {
310
- const keyA = encodeValue(a[0], { ...ctx, depth: ctx.depth + 1 })
311
- const keyB = encodeValue(b[0], { ...ctx, depth: ctx.depth + 1 })
312
- return compareBytes(keyA, keyB)
313
- })
327
+ const childCtx = { ...ctx, depth: ctx.depth + 1 }
328
+ const withEncodedKeys = entries.map(entry => ({
329
+ encodedKey: encodeValue(entry[0], childCtx),
330
+ entry
331
+ }))
332
+ withEncodedKeys.sort((a, b) => compareBytes(a.encodedKey, b.encodedKey))
333
+ entries = withEncodedKeys.map(t => t.entry)
334
+ preEncodedKeys = withEncodedKeys.map(t => t.encodedKey)
335
+ }
336
+
337
+ if (ctx.options.rejectDuplicateKeys) {
338
+ const seen = new Set<string>()
339
+ // Reuse pre-encoded keys if available (from canonical sort above)
340
+ const keysForDupCheck = preEncodedKeys
341
+ ?? entries.map(e => encodeValue(e[0], { ...ctx, depth: ctx.depth + 1 }))
342
+ for (const keyBytes of keysForDupCheck) {
343
+ const keyHex = bytesToHex(keyBytes)
344
+ if (seen.has(keyHex)) {
345
+ throw new Error('Duplicate map key detected')
346
+ }
347
+ seen.add(keyHex)
348
+ }
314
349
  }
315
350
 
316
351
  const header = encodeLengthHeader(5, entries.length)
@@ -326,12 +361,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
326
361
 
327
362
  const result = concatenateUint8Arrays(parts)
328
363
 
329
- // Check output size
330
- ctx.bytesWritten += result.length
331
- if (ctx.bytesWritten > ctx.options.maxOutputSize) {
332
- throw new Error('Encoded output exceeds maximum size')
333
- }
334
-
335
364
  return {
336
365
  bytes: result,
337
366
  hex: bytesToHex(result)
@@ -354,6 +383,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
354
383
 
355
384
  // Handle indefinite-length encoding
356
385
  if (encodeOptions?.indefinite || isIndefinite) {
386
+ if (options.allowIndefinite === false) {
387
+ throw new Error('Indefinite-length encoding is not allowed')
388
+ }
357
389
  if (options.canonical) {
358
390
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
359
391
  }
@@ -363,7 +395,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
363
395
  // Definite-length encoding
364
396
  const ctx: EncodeContext = {
365
397
  depth: 0,
366
- bytesWritten: 0,
367
398
  options
368
399
  }
369
400
 
@@ -379,6 +410,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
379
410
  const encodeMapIndefinite = (
380
411
  map: Map<EncodableValue, EncodableValue> | { [key: string]: EncodableValue }
381
412
  ): EncodeResult => {
413
+ if (options.allowIndefinite === false) {
414
+ throw new Error('Indefinite-length encoding is not allowed')
415
+ }
382
416
  if (options.canonical) {
383
417
  throw new Error('Indefinite-length encoding not allowed in canonical mode')
384
418
  }
@@ -392,7 +426,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
392
426
 
393
427
  const ctx: EncodeContext = {
394
428
  depth: 0,
395
- bytesWritten: 0,
396
429
  options
397
430
  }
398
431
 
@@ -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)
@@ -70,6 +75,24 @@ export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
70
75
  * @throws Error if value type is unsupported
71
76
  */
72
77
  const encode = (value: EncodableValue): EncodeResult => {
78
+ const result = encodeValue(value)
79
+
80
+ // Enforce maxOutputSize at the root level.
81
+ // This is the single authoritative check — collection/string encoders no longer
82
+ // track bytesWritten individually, which was broken for nested structures.
83
+ if (options.maxOutputSize && result.bytes.length > options.maxOutputSize) {
84
+ throw new Error(
85
+ `Encoded output size ${result.bytes.length} bytes exceeds limit of ${options.maxOutputSize} bytes`
86
+ )
87
+ }
88
+
89
+ return result
90
+ }
91
+
92
+ /**
93
+ * Encode any JavaScript value to CBOR (internal, no size check)
94
+ */
95
+ const encodeValue = (value: EncodableValue): EncodeResult => {
73
96
  // Handle null/undefined/boolean
74
97
  if (value === null || value === undefined || typeof value === 'boolean') {
75
98
  return encodeSimple(value)
@@ -77,8 +100,11 @@ export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
77
100
 
78
101
  // Handle numbers
79
102
  if (typeof value === 'number') {
103
+ if (Object.is(value, -0)) {
104
+ return encodeFloat(value, 16)
105
+ }
80
106
  // Check if it's an integer
81
- if (Number.isInteger(value) && Number.isSafeInteger(value)) {
107
+ if (Number.isSafeInteger(value)) {
82
108
  return encodeInteger(value)
83
109
  }
84
110
  // It's a float
@@ -68,8 +68,8 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
68
68
  */
69
69
  const encodeFloat16Bytes = (value: number): Uint8Array => {
70
70
  // Handle special cases
71
+ if (Object.is(value, -0)) return new Uint8Array([0x80, 0x00])
71
72
  if (value === 0) return new Uint8Array([0x00, 0x00])
72
- if (value === -0) return new Uint8Array([0x80, 0x00])
73
73
  if (Number.isNaN(value)) return new Uint8Array([0x7e, 0x00])
74
74
  if (value === Infinity) return new Uint8Array([0x7c, 0x00])
75
75
  if (value === -Infinity) return new Uint8Array([0xfc, 0x00])
@@ -88,22 +88,54 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
88
88
  const mant64 = Number(bits & 0xfffffffffffffn)
89
89
 
90
90
  // Convert to float16 range
91
+ // Use BigInt for bit manipulation since mant64 can exceed 32 bits,
92
+ // which would cause JavaScript's bitwise operators to truncate silently.
93
+ const mant64n = BigInt(mant64)
91
94
  let exp16: number
92
95
  let mant16: number
93
96
 
94
- if (exp64 < -14) {
95
- // Subnormal or zero
97
+ if (exp64 < -24) {
98
+ // Too small even for subnormal float16
96
99
  exp16 = 0
97
100
  mant16 = 0
101
+ } else if (exp64 < -14) {
102
+ // Subnormal float16: shift the implicit 1.mantissa into the fraction bits
103
+ exp16 = 0
104
+ const shift = -14 - exp64
105
+ // Build the full subnormal mantissa: implicit 1 bit + top 10 mantissa bits,
106
+ // then apply IEEE 754 round-half-to-even before shifting into subnormal position.
107
+ // The 11-bit value (1.mant10) needs to be right-shifted by 'shift' to get the
108
+ // subnormal mantissa. We apply rounding at the shift boundary.
109
+ const fullMant = (1n << 10n) | (mant64n >> 42n)
110
+ // Bits lost from the float64 mantissa during the >> 42 extraction
111
+ const lostFromExtract = mant64n & ((1n << 42n) - 1n)
112
+ // The 'shift' additional bits lost when converting to subnormal
113
+ const shiftN = BigInt(shift)
114
+ const guardBit = Number((fullMant >> (shiftN - 1n)) & 1n)
115
+ // Sticky includes bits below guard in fullMant plus any bits lost from extraction
116
+ const stickyBitsBelow = shiftN > 1n ? (fullMant & ((1n << (shiftN - 1n)) - 1n)) : 0n
117
+ const sticky = (stickyBitsBelow !== 0n || lostFromExtract !== 0n) ? 1 : 0
118
+ const truncated = Number(fullMant >> shiftN)
119
+ const lsb = truncated & 1
120
+ mant16 = truncated + (guardBit & (sticky | lsb))
98
121
  } else if (exp64 > 15) {
99
122
  // Overflow to infinity
100
123
  exp16 = 31
101
124
  mant16 = 0
102
125
  } else {
103
- // Normal number
126
+ // Normal number: IEEE 754 round-half-to-even (guard/round/sticky)
104
127
  exp16 = exp64 + 15
105
- // Take top 10 bits of mantissa
106
- mant16 = mant64 >> 42
128
+ const truncated = Number(mant64n >> 42n)
129
+ const guardBit = Number((mant64n >> 41n) & 1n)
130
+ const stickyBits = (mant64n & ((1n << 41n) - 1n)) !== 0n ? 1 : 0
131
+ const lsb = truncated & 1 // least significant bit of truncated result
132
+ mant16 = truncated + (guardBit & (stickyBits | lsb))
133
+ // Handle mantissa overflow from rounding (0x3FF + 1 = 0x400 bumps exponent)
134
+ if (mant16 > 0x3ff) {
135
+ mant16 = 0
136
+ exp16 += 1
137
+ // If exp16 overflows to 31 (0x1f), it becomes infinity -- correct IEEE 754 behavior
138
+ }
107
139
  }
108
140
 
109
141
  const float16 = (sign << 15) | (exp16 << 10) | mant16
@@ -150,7 +182,7 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
150
182
  */
151
183
  const canBeFloat16 = (value: number): boolean => {
152
184
  // Special values
153
- if (!Number.isFinite(value) || value === 0 || value === -0) {
185
+ if (!Number.isFinite(value) || Object.is(value, 0) || Object.is(value, -0)) {
154
186
  return true
155
187
  }
156
188
 
@@ -190,7 +222,7 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
190
222
  */
191
223
  const canBeFloat32 = (value: number): boolean => {
192
224
  // Special values
193
- if (!Number.isFinite(value) || value === 0 || value === -0) {
225
+ if (!Number.isFinite(value) || Object.is(value, 0) || Object.is(value, -0)) {
194
226
  return true
195
227
  }
196
228
 
@@ -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
  }
@@ -81,8 +81,6 @@ export interface TaggedValue {
81
81
  export interface EncodeContext {
82
82
  /** Current nesting depth */
83
83
  depth: number
84
- /** Bytes written so far */
85
- bytesWritten: number
86
84
  /** Encoder options */
87
85
  options: Required<EncodeOptions>
88
86
  }
package/src/index.ts CHANGED
@@ -96,9 +96,14 @@ import type { ParseResult, ParseResultWithMap, ParseOptions } from './parser/typ
96
96
  import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/types'
97
97
 
98
98
  /**
99
- * Decode CBOR hex string to JavaScript value
99
+ * Decode CBOR data to JavaScript value
100
100
  *
101
- * @param hexString - CBOR data as hex string (e.g., "1864" for integer 100)
101
+ * Accepts either a hex string or a Uint8Array of raw CBOR bytes.
102
+ * When passing a Uint8Array, the bytes are used directly without
103
+ * hex conversion, which is more efficient for binary sources
104
+ * (WebSocket, fetch, file I/O, etc.).
105
+ *
106
+ * @param input - CBOR data as hex string (e.g., "1864") or Uint8Array
102
107
  * @param options - Optional parser configuration
103
108
  * @returns Decoded value and number of bytes consumed
104
109
  *
@@ -106,9 +111,12 @@ import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/type
106
111
  *
107
112
  * @example
108
113
  * ```typescript
109
- * // Decode integer
114
+ * // Decode from hex string
110
115
  * decode('1864') // { value: 100, bytesRead: 2 }
111
116
  *
117
+ * // Decode from Uint8Array (zero-copy, no hex conversion)
118
+ * decode(new Uint8Array([0x18, 0x64])) // { value: 100, bytesRead: 2 }
119
+ *
112
120
  * // Decode string
113
121
  * decode('6449455446') // { value: "IETF", bytesRead: 5 }
114
122
  *
@@ -124,18 +132,20 @@ import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/type
124
132
  *
125
133
  * @see {@link https://datatracker.ietf.org/doc/html/rfc8949 | RFC 8949}
126
134
  */
127
- export function decode(hexString: string, options?: ParseOptions): ParseResult {
135
+ export function decode(input: string | Uint8Array, options?: ParseOptions): ParseResult {
128
136
  const { parse } = useCborParser()
129
- return parse(hexString, options)
137
+ return parse(input, options)
130
138
  }
131
139
 
132
140
  /**
133
- * Decode CBOR hex string with source map generation
141
+ * Decode CBOR data with source map generation
134
142
  *
135
143
  * Source maps provide bidirectional linking between hex bytes and decoded values,
136
144
  * enabling interactive debugging visualizations.
137
145
  *
138
- * @param hexString - CBOR data as hex string
146
+ * Accepts either a hex string or a Uint8Array of raw CBOR bytes.
147
+ *
148
+ * @param input - CBOR data as hex string or Uint8Array
139
149
  * @param options - Optional parser configuration
140
150
  * @returns Decoded value, byte count, and source map
141
151
  *
@@ -148,14 +158,13 @@ export function decode(hexString: string, options?: ParseOptions): ParseResult {
148
158
  * // { path: '.value', start: 2, end: 3, majorType: 4, type: 'Array', parent: '' }
149
159
  * // ]
150
160
  *
151
- * // Use source map for hex-to-JSON linking
152
- * const entry = sourceMap.find(e => e.path === '.value')
153
- * console.log(`Value is at bytes ${entry.start}-${entry.end}`)
161
+ * // From Uint8Array
162
+ * const { value, sourceMap } = decodeWithSourceMap(new Uint8Array([0xd8, 0x79, 0x80]))
154
163
  * ```
155
164
  */
156
- export function decodeWithSourceMap(hexString: string, options?: ParseOptions): ParseResultWithMap {
165
+ export function decodeWithSourceMap(input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap {
157
166
  const { parseWithSourceMap } = useCborParser()
158
- return parseWithSourceMap(hexString, options)
167
+ return parseWithSourceMap(input, options)
159
168
  }
160
169
 
161
170
  /**
@@ -287,23 +296,23 @@ export class CborDecoder {
287
296
  }
288
297
 
289
298
  /**
290
- * Decode CBOR hex string
299
+ * Decode CBOR data
291
300
  *
292
- * @param hexString - CBOR data as hex string
301
+ * @param input - CBOR data as hex string or Uint8Array
293
302
  * @returns Decoded value and byte count
294
303
  */
295
- decode(hexString: string): ParseResult {
296
- return decode(hexString, this.options)
304
+ decode(input: string | Uint8Array): ParseResult {
305
+ return decode(input, this.options)
297
306
  }
298
307
 
299
308
  /**
300
- * Decode CBOR hex string with source map
309
+ * Decode CBOR data with source map
301
310
  *
302
- * @param hexString - CBOR data as hex string
311
+ * @param input - CBOR data as hex string or Uint8Array
303
312
  * @returns Decoded value, byte count, and source map
304
313
  */
305
- decodeWithSourceMap(hexString: string): ParseResultWithMap {
306
- return decodeWithSourceMap(hexString, this.options)
314
+ decodeWithSourceMap(input: string | Uint8Array): ParseResultWithMap {
315
+ return decodeWithSourceMap(input, this.options)
307
316
  }
308
317
  }
309
318