@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.
- package/CHANGELOG.md +55 -0
- package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
- package/dist/chunk-5IWW5H47.js.map +1 -0
- package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
- package/dist/chunk-RVG2BY32.cjs.map +1 -0
- package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
- package/dist/chunk-S4RXO6IB.cjs.map +1 -0
- package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
- package/dist/chunk-UMAX5MX5.js.map +1 -0
- package/dist/encoder/index.cjs +13 -13
- package/dist/encoder/index.d.cts +2 -2
- package/dist/encoder/index.d.ts +2 -2
- package/dist/encoder/index.js +1 -1
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -19
- package/dist/index.d.ts +28 -19
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/parser/index.cjs +14 -14
- package/dist/parser/index.d.cts +3 -1
- package/dist/parser/index.d.ts +3 -1
- package/dist/parser/index.js +1 -1
- package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
- package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
- package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
- package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
- package/package.json +1 -1
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +701 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
- package/src/encoder/composables/useCborEncoder.ts +27 -1
- package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
- package/src/encoder/composables/useCborStringEncoder.ts +23 -10
- package/src/encoder/types.ts +0 -2
- package/src/index.ts +29 -20
- package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
- package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
- package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
- package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
- package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
- package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
- package/src/parser/composables/useCborCollection.ts +45 -42
- package/src/parser/composables/useCborFloat.ts +95 -9
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +387 -216
- package/src/parser/composables/useCborString.ts +22 -4
- package/src/parser/composables/useCborTag.ts +149 -53
- package/src/parser/utils.ts +11 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
- package/dist/chunk-2HBCILJS.cjs.map +0 -1
- package/dist/chunk-7CFYWHS6.js.map +0 -1
- package/dist/chunk-ZRPJUEIZ.js.map +0 -1
- 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'
|
|
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
|
-
//
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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.
|
|
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 < -
|
|
95
|
-
//
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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
|
|
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
|
|
129
|
-
if (
|
|
130
|
-
throw new Error(
|
|
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
|
}
|
package/src/encoder/types.ts
CHANGED
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
|
|
99
|
+
* Decode CBOR data to JavaScript value
|
|
100
100
|
*
|
|
101
|
-
*
|
|
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
|
|
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(
|
|
135
|
+
export function decode(input: string | Uint8Array, options?: ParseOptions): ParseResult {
|
|
128
136
|
const { parse } = useCborParser()
|
|
129
|
-
return parse(
|
|
137
|
+
return parse(input, options)
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/**
|
|
133
|
-
* Decode CBOR
|
|
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
|
-
*
|
|
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
|
-
* //
|
|
152
|
-
* const
|
|
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(
|
|
165
|
+
export function decodeWithSourceMap(input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap {
|
|
157
166
|
const { parseWithSourceMap } = useCborParser()
|
|
158
|
-
return parseWithSourceMap(
|
|
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
|
|
299
|
+
* Decode CBOR data
|
|
291
300
|
*
|
|
292
|
-
* @param
|
|
301
|
+
* @param input - CBOR data as hex string or Uint8Array
|
|
293
302
|
* @returns Decoded value and byte count
|
|
294
303
|
*/
|
|
295
|
-
decode(
|
|
296
|
-
return decode(
|
|
304
|
+
decode(input: string | Uint8Array): ParseResult {
|
|
305
|
+
return decode(input, this.options)
|
|
297
306
|
}
|
|
298
307
|
|
|
299
308
|
/**
|
|
300
|
-
* Decode CBOR
|
|
309
|
+
* Decode CBOR data with source map
|
|
301
310
|
*
|
|
302
|
-
* @param
|
|
311
|
+
* @param input - CBOR data as hex string or Uint8Array
|
|
303
312
|
* @returns Decoded value, byte count, and source map
|
|
304
313
|
*/
|
|
305
|
-
decodeWithSourceMap(
|
|
306
|
-
return decodeWithSourceMap(
|
|
314
|
+
decodeWithSourceMap(input: string | Uint8Array): ParseResultWithMap {
|
|
315
|
+
return decodeWithSourceMap(input, this.options)
|
|
307
316
|
}
|
|
308
317
|
}
|
|
309
318
|
|