@marcuspuchalla/nachos 0.1.3 → 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 +23 -0
- package/dist/{chunk-5A5T56JB.js → chunk-5IWW5H47.js} +378 -207
- package/dist/chunk-5IWW5H47.js.map +1 -0
- package/dist/{chunk-PTWN7K3Y.cjs → chunk-RVG2BY32.cjs} +378 -207
- package/dist/chunk-RVG2BY32.cjs.map +1 -0
- package/dist/{chunk-R62CQQNI.cjs → chunk-S4RXO6IB.cjs} +195 -165
- package/dist/chunk-S4RXO6IB.cjs.map +1 -0
- package/dist/{chunk-2MTLSQ7E.js → chunk-UMAX5MX5.js} +195 -165
- 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-Cs1CZuXZ.d.cts → useCborTag-BD6Sqp7p.d.ts} +9 -4
- package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-QpZR-Er2.d.cts} +9 -4
- package/package.json +1 -1
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +5 -6
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +28 -25
- package/src/encoder/composables/useCborEncoder.ts +21 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +34 -7
- 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-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
- package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
- 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 +2 -1
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +387 -197
- package/src/parser/composables/useCborTag.ts +45 -37
- package/src/parser/utils.ts +11 -0
- package/dist/chunk-2MTLSQ7E.js.map +0 -1
- package/dist/chunk-5A5T56JB.js.map +0 -1
- package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
- package/dist/chunk-R62CQQNI.cjs.map +0 -1
|
@@ -285,6 +285,100 @@ describe('CBOR Collection Encoder', () => {
|
|
|
285
285
|
.toThrow('Duplicate map key detected')
|
|
286
286
|
})
|
|
287
287
|
})
|
|
288
|
+
|
|
289
|
+
describe('Canonical sort pre-encoding optimization', () => {
|
|
290
|
+
it('should produce identical output for canonical sort with many keys', () => {
|
|
291
|
+
const { encodeMap } = useCborCollectionEncoder({ canonical: true })
|
|
292
|
+
|
|
293
|
+
// Use many keys to exercise sorting — result must be deterministic
|
|
294
|
+
const map = new Map<EncodableValue, EncodableValue>([
|
|
295
|
+
['zebra', 1],
|
|
296
|
+
['apple', 2],
|
|
297
|
+
['mango', 3],
|
|
298
|
+
['banana', 4],
|
|
299
|
+
['kiwi', 5],
|
|
300
|
+
['cherry', 6],
|
|
301
|
+
['date', 7],
|
|
302
|
+
['fig', 8],
|
|
303
|
+
['grape', 9],
|
|
304
|
+
['lemon', 10],
|
|
305
|
+
])
|
|
306
|
+
const result1 = encodeMap(map)
|
|
307
|
+
const result2 = encodeMap(map)
|
|
308
|
+
|
|
309
|
+
// Must be deterministic
|
|
310
|
+
expect(result1.hex).toBe(result2.hex)
|
|
311
|
+
|
|
312
|
+
// Verify canonical order: sorted by encoded key length first, then bytewise
|
|
313
|
+
// 3-char keys: fig, 4-char keys: date, kiwi, 5-char keys: apple, grape, lemon, mango
|
|
314
|
+
// 6-char keys: banana, cherry, zebra
|
|
315
|
+
const hex = result1.hex
|
|
316
|
+
const figPos = hex.indexOf('6366696703') // "fig" + integer 8 (but let's just check key order)
|
|
317
|
+
const datePos = hex.indexOf('6464617465') // "date"
|
|
318
|
+
const bananaPos = hex.indexOf('6662616e616e61') // "banana"
|
|
319
|
+
|
|
320
|
+
// fig (3 chars) should come before date (4 chars) should come before banana (6 chars)
|
|
321
|
+
expect(figPos).toBeLessThan(datePos)
|
|
322
|
+
expect(datePos).toBeLessThan(bananaPos)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should produce correct canonical order with mixed-type Map keys', () => {
|
|
326
|
+
const { encodeMap } = useCborCollectionEncoder({ canonical: true })
|
|
327
|
+
|
|
328
|
+
// Integer keys encode shorter than string keys
|
|
329
|
+
const map = new Map<EncodableValue, EncodableValue>([
|
|
330
|
+
['z', 3],
|
|
331
|
+
[1, 1],
|
|
332
|
+
['a', 2],
|
|
333
|
+
])
|
|
334
|
+
const result = encodeMap(map)
|
|
335
|
+
|
|
336
|
+
// Integer 1 encodes as 0x01 (1 byte), "a" as 0x6161 (2 bytes), "z" as 0x617a (2 bytes)
|
|
337
|
+
// Canonical order: 1 (shortest), then "a" < "z" (same length, bytewise)
|
|
338
|
+
const hex = result.hex
|
|
339
|
+
// After map header (0xa3), first key should be integer 1 (0x01)
|
|
340
|
+
expect(hex.startsWith('a3')).toBe(true)
|
|
341
|
+
expect(hex.substring(2, 4)).toBe('01') // integer key 1
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('should reject duplicates when both canonical and rejectDuplicateKeys are on', () => {
|
|
345
|
+
const { encodeMap } = useCborCollectionEncoder({
|
|
346
|
+
canonical: true,
|
|
347
|
+
rejectDuplicateKeys: true
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const map = new Map<EncodableValue, EncodableValue>([['x', 1]])
|
|
351
|
+
;(map as any)[ALL_ENTRIES_SYMBOL] = [['x', 1], ['x', 2]]
|
|
352
|
+
|
|
353
|
+
expect(() => encodeMap(map))
|
|
354
|
+
.toThrow('Duplicate map key detected')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('should not reject unique keys when both canonical and rejectDuplicateKeys are on', () => {
|
|
358
|
+
const { encodeMap } = useCborCollectionEncoder({
|
|
359
|
+
canonical: true,
|
|
360
|
+
rejectDuplicateKeys: true
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const map = new Map<EncodableValue, EncodableValue>([
|
|
364
|
+
['b', 2],
|
|
365
|
+
['a', 1],
|
|
366
|
+
['c', 3],
|
|
367
|
+
])
|
|
368
|
+
const result = encodeMap(map)
|
|
369
|
+
|
|
370
|
+
// Should succeed and produce canonical order
|
|
371
|
+
expect(result.bytes[0]).toBe(0xa3) // 3 entries
|
|
372
|
+
|
|
373
|
+
// Keys in canonical order: a, b, c
|
|
374
|
+
const hex = result.hex
|
|
375
|
+
const aPos = hex.indexOf('6161') // "a"
|
|
376
|
+
const bPos = hex.indexOf('6162') // "b"
|
|
377
|
+
const cPos = hex.indexOf('6163') // "c"
|
|
378
|
+
expect(aPos).toBeLessThan(bPos)
|
|
379
|
+
expect(bPos).toBeLessThan(cPos)
|
|
380
|
+
})
|
|
381
|
+
})
|
|
288
382
|
})
|
|
289
383
|
|
|
290
384
|
describe('Depth limits', () => {
|
|
@@ -320,18 +414,22 @@ describe('CBOR Collection Encoder', () => {
|
|
|
320
414
|
})
|
|
321
415
|
|
|
322
416
|
describe('Output size limits', () => {
|
|
323
|
-
it('should
|
|
417
|
+
it('should not enforce maxOutputSize at the collection level (enforced at root encoder)', () => {
|
|
418
|
+
// maxOutputSize is now checked at the root level in useCborEncoder,
|
|
419
|
+
// not inside the collection encoder. The collection encoder should
|
|
420
|
+
// encode without throwing, and the root encoder applies the check.
|
|
324
421
|
const { encodeArray } = useCborCollectionEncoder({ maxOutputSize: 10 })
|
|
325
|
-
const
|
|
422
|
+
const smallArray = Array(20).fill(1)
|
|
326
423
|
|
|
327
|
-
|
|
328
|
-
|
|
424
|
+
// Collection encoder no longer throws on size - it just encodes
|
|
425
|
+
const result = encodeArray(smallArray)
|
|
426
|
+
expect(result.bytes.length).toBeGreaterThan(10)
|
|
329
427
|
})
|
|
330
428
|
})
|
|
331
429
|
|
|
332
430
|
describe('Real-world Cardano examples', () => {
|
|
333
431
|
it('should encode Cardano transaction structure', () => {
|
|
334
|
-
const { encodeMap
|
|
432
|
+
const { encodeMap } = useCborCollectionEncoder()
|
|
335
433
|
|
|
336
434
|
// Simplified Cardano transaction
|
|
337
435
|
const tx = {
|
|
@@ -185,7 +185,7 @@ describe('CBOR Encoder Error Handling', () => {
|
|
|
185
185
|
|
|
186
186
|
const largeArray = Array(100).fill(1)
|
|
187
187
|
|
|
188
|
-
expect(() => encode(largeArray)).toThrow(
|
|
188
|
+
expect(() => encode(largeArray)).toThrow(/[Ee]ncoded output.*exceeds/)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
191
|
it('should succeed when output is within maxOutputSize', () => {
|
|
@@ -205,6 +205,40 @@ describe('CBOR Encoder Error Handling', () => {
|
|
|
205
205
|
|
|
206
206
|
expect(() => encode(largeMap)).toThrow('Encoded output exceeds maximum size')
|
|
207
207
|
})
|
|
208
|
+
|
|
209
|
+
it('should enforce maxOutputSize on deeply nested structures whose total exceeds limit', () => {
|
|
210
|
+
// Total encoded size is 25 bytes, limit is 20.
|
|
211
|
+
const { encode } = useCborEncoder({ maxOutputSize: 20 })
|
|
212
|
+
|
|
213
|
+
const nested = [
|
|
214
|
+
[1, 2, 3, 4, 5],
|
|
215
|
+
[6, 7, 8, 9, 10],
|
|
216
|
+
[11, 12, 13, 14, 15],
|
|
217
|
+
[16, 17, 18, 19, 20],
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
expect(() => encode(nested)).toThrow(/[Ee]ncoded output.*exceeds/)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should enforce maxOutputSize on nested maps whose total exceeds limit', () => {
|
|
224
|
+
// Total encoded size is 19 bytes, limit is 15.
|
|
225
|
+
const { encode } = useCborEncoder({ maxOutputSize: 15 })
|
|
226
|
+
|
|
227
|
+
const bigger = {
|
|
228
|
+
a: { x: 1, y: 2 },
|
|
229
|
+
b: { x: 3, y: 4 },
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(() => encode(bigger)).toThrow(/[Ee]ncoded output.*exceeds/)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should include byte counts in the maxOutputSize error message', () => {
|
|
236
|
+
// After the fix, the root-level check should report actual size and limit.
|
|
237
|
+
// [1,2,3,4,5] encodes to 6 bytes; limit is 5.
|
|
238
|
+
const { encode } = useCborEncoder({ maxOutputSize: 5 })
|
|
239
|
+
|
|
240
|
+
expect(() => encode([1, 2, 3, 4, 5])).toThrow(/6 bytes exceeds limit of 5 bytes/)
|
|
241
|
+
})
|
|
208
242
|
})
|
|
209
243
|
|
|
210
244
|
describe('maxDepth as circular reference protection', () => {
|
|
@@ -540,13 +574,14 @@ describe('CBOR Canonical Encoding Validation', () => {
|
|
|
540
574
|
expect(result.bytes.length).toBe(3)
|
|
541
575
|
})
|
|
542
576
|
|
|
543
|
-
it('should encode 1.5 as
|
|
577
|
+
it('should encode 1.5 as float16 (exact float16 representation)', () => {
|
|
544
578
|
const { encode } = useCborEncoder()
|
|
545
579
|
const result = encode(1.5)
|
|
546
580
|
|
|
547
|
-
// 1.5
|
|
548
|
-
|
|
549
|
-
expect(result.bytes
|
|
581
|
+
// 1.5 is exactly representable in float16: 0xf9 + 2 bytes
|
|
582
|
+
// float16 = 0 01111 1000000000 = 0x3e00
|
|
583
|
+
expect(result.bytes[0]).toBe(0xf9)
|
|
584
|
+
expect(result.bytes.length).toBe(3)
|
|
550
585
|
})
|
|
551
586
|
|
|
552
587
|
it('should encode 1.1 as float64 (no exact float16/32 representation)', () => {
|
|
@@ -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)', () => {
|
|
@@ -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)) {
|
|
@@ -211,12 +222,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
211
222
|
|
|
212
223
|
const result = concatenateUint8Arrays(parts)
|
|
213
224
|
|
|
214
|
-
// Check output size
|
|
215
|
-
ctx.bytesWritten += result.length
|
|
216
|
-
if (ctx.bytesWritten > ctx.options.maxOutputSize) {
|
|
217
|
-
throw new Error('Encoded output exceeds maximum size')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
225
|
return {
|
|
221
226
|
bytes: result,
|
|
222
227
|
hex: bytesToHex(result)
|
|
@@ -251,7 +256,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
251
256
|
// Definite-length encoding
|
|
252
257
|
const ctx: EncodeContext = {
|
|
253
258
|
depth: 0,
|
|
254
|
-
bytesWritten: 0,
|
|
255
259
|
options
|
|
256
260
|
}
|
|
257
261
|
|
|
@@ -276,7 +280,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
276
280
|
|
|
277
281
|
const ctx: EncodeContext = {
|
|
278
282
|
depth: 0,
|
|
279
|
-
bytesWritten: 0,
|
|
280
283
|
options
|
|
281
284
|
}
|
|
282
285
|
|
|
@@ -316,19 +319,27 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
316
319
|
: Object.entries(map)
|
|
317
320
|
}
|
|
318
321
|
|
|
319
|
-
//
|
|
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
|
+
|
|
320
326
|
if (ctx.options.canonical && !(map as any)[ALL_ENTRIES_SYMBOL]) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
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)
|
|
326
335
|
}
|
|
327
336
|
|
|
328
337
|
if (ctx.options.rejectDuplicateKeys) {
|
|
329
338
|
const seen = new Set<string>()
|
|
330
|
-
|
|
331
|
-
|
|
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) {
|
|
332
343
|
const keyHex = bytesToHex(keyBytes)
|
|
333
344
|
if (seen.has(keyHex)) {
|
|
334
345
|
throw new Error('Duplicate map key detected')
|
|
@@ -350,12 +361,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
350
361
|
|
|
351
362
|
const result = concatenateUint8Arrays(parts)
|
|
352
363
|
|
|
353
|
-
// Check output size
|
|
354
|
-
ctx.bytesWritten += result.length
|
|
355
|
-
if (ctx.bytesWritten > ctx.options.maxOutputSize) {
|
|
356
|
-
throw new Error('Encoded output exceeds maximum size')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
364
|
return {
|
|
360
365
|
bytes: result,
|
|
361
366
|
hex: bytesToHex(result)
|
|
@@ -390,7 +395,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
390
395
|
// Definite-length encoding
|
|
391
396
|
const ctx: EncodeContext = {
|
|
392
397
|
depth: 0,
|
|
393
|
-
bytesWritten: 0,
|
|
394
398
|
options
|
|
395
399
|
}
|
|
396
400
|
|
|
@@ -422,7 +426,6 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
422
426
|
|
|
423
427
|
const ctx: EncodeContext = {
|
|
424
428
|
depth: 0,
|
|
425
|
-
bytesWritten: 0,
|
|
426
429
|
options
|
|
427
430
|
}
|
|
428
431
|
|
|
@@ -75,6 +75,24 @@ export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
75
75
|
* @throws Error if value type is unsupported
|
|
76
76
|
*/
|
|
77
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 => {
|
|
78
96
|
// Handle null/undefined/boolean
|
|
79
97
|
if (value === null || value === undefined || typeof value === 'boolean') {
|
|
80
98
|
return encodeSimple(value)
|
|
@@ -82,6 +100,9 @@ export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
82
100
|
|
|
83
101
|
// Handle numbers
|
|
84
102
|
if (typeof value === 'number') {
|
|
103
|
+
if (Object.is(value, -0)) {
|
|
104
|
+
return encodeFloat(value, 16)
|
|
105
|
+
}
|
|
85
106
|
// Check if it's an integer
|
|
86
107
|
if (Number.isSafeInteger(value)) {
|
|
87
108
|
return encodeInteger(value)
|
|
@@ -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,6 +88,9 @@ 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
|
|
|
@@ -99,16 +102,40 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
|
|
|
99
102
|
// Subnormal float16: shift the implicit 1.mantissa into the fraction bits
|
|
100
103
|
exp16 = 0
|
|
101
104
|
const shift = -14 - exp64
|
|
102
|
-
|
|
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))
|
|
103
121
|
} else if (exp64 > 15) {
|
|
104
122
|
// Overflow to infinity
|
|
105
123
|
exp16 = 31
|
|
106
124
|
mant16 = 0
|
|
107
125
|
} else {
|
|
108
|
-
// Normal number
|
|
126
|
+
// Normal number: IEEE 754 round-half-to-even (guard/round/sticky)
|
|
109
127
|
exp16 = exp64 + 15
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
139
|
}
|
|
113
140
|
|
|
114
141
|
const float16 = (sign << 15) | (exp16 << 10) | mant16
|
|
@@ -155,7 +182,7 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
|
|
|
155
182
|
*/
|
|
156
183
|
const canBeFloat16 = (value: number): boolean => {
|
|
157
184
|
// Special values
|
|
158
|
-
if (!Number.isFinite(value) || value
|
|
185
|
+
if (!Number.isFinite(value) || Object.is(value, 0) || Object.is(value, -0)) {
|
|
159
186
|
return true
|
|
160
187
|
}
|
|
161
188
|
|
|
@@ -195,7 +222,7 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
|
|
|
195
222
|
*/
|
|
196
223
|
const canBeFloat32 = (value: number): boolean => {
|
|
197
224
|
// Special values
|
|
198
|
-
if (!Number.isFinite(value) || value
|
|
225
|
+
if (!Number.isFinite(value) || Object.is(value, 0) || Object.is(value, -0)) {
|
|
199
226
|
return true
|
|
200
227
|
}
|
|
201
228
|
|
package/src/encoder/types.ts
CHANGED