@marcuspuchalla/nachos 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-7CFYWHS6.js → chunk-2MTLSQ7E.js} +58 -10
  4. package/dist/chunk-2MTLSQ7E.js.map +1 -0
  5. package/dist/{chunk-ZRPJUEIZ.js → chunk-5A5T56JB.js} +170 -22
  6. package/dist/chunk-5A5T56JB.js.map +1 -0
  7. package/dist/{chunk-2HBCILJS.cjs → chunk-PTWN7K3Y.cjs} +169 -21
  8. package/dist/chunk-PTWN7K3Y.cjs.map +1 -0
  9. package/dist/{chunk-2FUTHZQQ.cjs → chunk-R62CQQNI.cjs} +58 -10
  10. package/dist/chunk-R62CQQNI.cjs.map +1 -0
  11. package/dist/encoder/index.cjs +13 -13
  12. package/dist/encoder/index.js +1 -1
  13. package/dist/index.cjs +20 -20
  14. package/dist/index.d.cts +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +4 -4
  17. package/dist/metafile-cjs.json +1 -1
  18. package/dist/metafile-esm.json +1 -1
  19. package/dist/parser/index.cjs +14 -14
  20. package/dist/parser/index.d.cts +1 -1
  21. package/dist/parser/index.d.ts +1 -1
  22. package/dist/parser/index.js +1 -1
  23. package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-Cs1CZuXZ.d.cts} +2 -2
  24. package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-xV2Pz2VY.d.ts} +2 -2
  25. package/package.json +1 -1
  26. package/src/__tests__/roundtrip.test.ts +702 -0
  27. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +26 -0
  28. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +812 -0
  29. package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
  30. package/src/encoder/composables/useCborCollectionEncoder.ts +30 -0
  31. package/src/encoder/composables/useCborEncoder.ts +6 -1
  32. package/src/encoder/composables/useCborSimpleEncoder.ts +7 -2
  33. package/src/encoder/composables/useCborStringEncoder.ts +23 -10
  34. package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
  35. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +2 -2
  36. package/src/parser/__tests__/cbor-standard-tags.test.ts +29 -0
  37. package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
  38. package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
  39. package/src/parser/composables/useCborFloat.ts +93 -8
  40. package/src/parser/composables/useCborParser.ts +0 -19
  41. package/src/parser/composables/useCborString.ts +22 -4
  42. package/src/parser/composables/useCborTag.ts +104 -16
  43. package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
  44. package/dist/chunk-2HBCILJS.cjs.map +0 -1
  45. package/dist/chunk-7CFYWHS6.js.map +0 -1
  46. package/dist/chunk-ZRPJUEIZ.js.map +0 -1
  47. package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
@@ -0,0 +1,812 @@
1
+ /**
2
+ * CBOR Encoder Error Handling, Canonical Encoding, and Map Key Diversity Tests
3
+ * Tests for error paths, canonical mode validation, and diverse Map key types
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest'
7
+ import { useCborEncoder } from '../composables/useCborEncoder'
8
+ import { useCborCollectionEncoder } from '../composables/useCborCollectionEncoder'
9
+ import type { EncodableValue } from '../types'
10
+
11
+ describe('CBOR Encoder Error Handling', () => {
12
+ describe('Unsupported types', () => {
13
+ it('should throw on Symbol values', () => {
14
+ const { encode } = useCborEncoder()
15
+ const sym = Symbol('test')
16
+
17
+ expect(() => encode(sym as any)).toThrow('Unsupported value type: symbol')
18
+ })
19
+
20
+ it('should throw on Function values', () => {
21
+ const { encode } = useCborEncoder()
22
+ const fn = () => 42
23
+
24
+ expect(() => encode(fn as any)).toThrow('Unsupported value type: function')
25
+ })
26
+
27
+ it('should throw on arrow function values', () => {
28
+ const { encode } = useCborEncoder()
29
+ const fn = function namedFn() { return 1 }
30
+
31
+ expect(() => encode(fn as any)).toThrow('Unsupported value type: function')
32
+ })
33
+
34
+ it('should throw on async function values', () => {
35
+ const { encode } = useCborEncoder()
36
+ const fn = async () => 42
37
+
38
+ expect(() => encode(fn as any)).toThrow('Unsupported value type: function')
39
+ })
40
+
41
+ it('should throw on generator function values', () => {
42
+ const { encode } = useCborEncoder()
43
+ function* gen() { yield 1 }
44
+
45
+ expect(() => encode(gen as any)).toThrow('Unsupported value type: function')
46
+ })
47
+
48
+ it('should encode class instances as plain objects (they are typeof object)', () => {
49
+ const { encode } = useCborEncoder()
50
+ class MyClass {
51
+ x = 1
52
+ y = 2
53
+ }
54
+ const instance = new MyClass()
55
+
56
+ // Class instances are treated as plain objects with enumerable properties
57
+ const result = encode(instance as any)
58
+ // Should encode as a map with keys "x" and "y"
59
+ expect(result.bytes[0]).toBe(0xa2) // Map with 2 entries
60
+ })
61
+
62
+ it('should encode Date instances as plain objects', () => {
63
+ const { encode } = useCborEncoder()
64
+ const date = new Date('2025-01-01T00:00:00Z')
65
+
66
+ // Date has no enumerable own properties by default, so encodes as empty map
67
+ // unless it has added properties - but typically it serializes as empty
68
+ const result = encode(date as any)
69
+ expect(result.bytes).toBeDefined()
70
+ })
71
+
72
+ it('should throw on Symbol used as nested value in array', () => {
73
+ const { encode } = useCborEncoder()
74
+
75
+ expect(() => encode([1, Symbol('nested') as any, 3])).toThrow()
76
+ })
77
+ })
78
+
79
+ describe('maxDepth enforcement', () => {
80
+ it('should throw on deeply nested arrays exceeding default maxDepth', () => {
81
+ const { encode } = useCborEncoder({ maxDepth: 3 })
82
+
83
+ // Build a structure nested 5 levels deep: [[[[[ 1 ]]]]]
84
+ let value: EncodableValue = 1
85
+ for (let i = 0; i < 5; i++) {
86
+ value = [value]
87
+ }
88
+
89
+ expect(() => encode(value)).toThrow('Maximum nesting depth exceeded')
90
+ })
91
+
92
+ it('should succeed with nesting within maxDepth limit', () => {
93
+ const { encode } = useCborEncoder({ maxDepth: 10 })
94
+
95
+ // Build 3-level deep structure
96
+ const value: EncodableValue = [[[1]]]
97
+
98
+ const result = encode(value)
99
+ expect(result.bytes).toBeDefined()
100
+ expect(result.hex).toBeDefined()
101
+ })
102
+
103
+ it('should throw on deeply nested maps exceeding maxDepth', () => {
104
+ const { encode } = useCborEncoder({ maxDepth: 2 })
105
+
106
+ // {a: {b: {c: {d: 1}}}} - 4 levels of nesting
107
+ const value = { a: { b: { c: { d: 1 } } } }
108
+
109
+ expect(() => encode(value)).toThrow('Maximum nesting depth exceeded')
110
+ })
111
+
112
+ it('should throw on mixed array/map nesting exceeding maxDepth', () => {
113
+ const { encode } = useCborEncoder({ maxDepth: 1 })
114
+
115
+ // [{a: [{b: 1}]}] mixes arrays and maps deeply
116
+ // depth 0 -> array -> depth 1 -> object -> depth 2 (exceeds 1)
117
+ const value = [{ a: [{ b: 1 }] }]
118
+
119
+ expect(() => encode(value)).toThrow('Maximum nesting depth exceeded')
120
+ })
121
+
122
+ it('should enforce maxDepth=0 rejecting any nested structure', () => {
123
+ const { encode } = useCborEncoder({ maxDepth: 0 })
124
+
125
+ // maxDepth=0: encodeArray starts at depth 0, encodeValue checks depth > 0
126
+ // For a nested array [[1]], the inner array triggers depth=1 > 0
127
+ expect(() => encode([[1]])).toThrow('Maximum nesting depth exceeded')
128
+ })
129
+
130
+ it('should allow maxDepth=1 for flat arrays', () => {
131
+ const { encode } = useCborEncoder({ maxDepth: 1 })
132
+
133
+ // A flat array: items at depth 1 (<=1)
134
+ const result = encode([1, 2, 3])
135
+ expect(result.bytes[0]).toBe(0x83)
136
+ })
137
+
138
+ it('should handle maxDepth with Map objects', () => {
139
+ const { encode } = useCborEncoder({ maxDepth: 1 })
140
+
141
+ // Map with nested Map exceeding depth:
142
+ // encodeMap starts ctx.depth=0, encodeValue for inner Map checks depth 0 > 1? no,
143
+ // then newCtx depth=1, encodeMapInternal -> encodeValue for innermost Map
144
+ // checks depth 1 > 1? no, newCtx depth=2, encodeMapInternal -> encodeValue(4)
145
+ // checks depth 2 > 1? yes -> throws
146
+ const inner = new Map<EncodableValue, EncodableValue>([
147
+ [1, new Map<EncodableValue, EncodableValue>([[2, new Map<EncodableValue, EncodableValue>([[3, 4]])]])]
148
+ ])
149
+
150
+ expect(() => encode(inner)).toThrow('Maximum nesting depth exceeded')
151
+ })
152
+
153
+ it('should build deeply nested structure programmatically and exceed depth', () => {
154
+ const { encode } = useCborEncoder({ maxDepth: 5 })
155
+
156
+ // Build 10-level deep array nesting
157
+ let value: EncodableValue = 42
158
+ for (let i = 0; i < 10; i++) {
159
+ value = [value]
160
+ }
161
+
162
+ expect(() => encode(value)).toThrow('Maximum nesting depth exceeded')
163
+ })
164
+ })
165
+
166
+ describe('maxOutputSize enforcement', () => {
167
+ it('should throw when encoding a large string exceeds maxOutputSize', () => {
168
+ const { encode } = useCborEncoder({ maxOutputSize: 10 })
169
+
170
+ const largeString = 'x'.repeat(100)
171
+
172
+ expect(() => encode(largeString)).toThrow('Encoded output exceeds maximum size')
173
+ })
174
+
175
+ it('should throw when encoding a large byte string exceeds maxOutputSize', () => {
176
+ const { encode } = useCborEncoder({ maxOutputSize: 10 })
177
+
178
+ const largeBytes = new Uint8Array(100)
179
+
180
+ expect(() => encode(largeBytes)).toThrow('Encoded output exceeds maximum size')
181
+ })
182
+
183
+ it('should throw when encoding a large array exceeds maxOutputSize', () => {
184
+ const { encode } = useCborEncoder({ maxOutputSize: 10 })
185
+
186
+ const largeArray = Array(100).fill(1)
187
+
188
+ expect(() => encode(largeArray)).toThrow('Encoded output exceeds maximum size')
189
+ })
190
+
191
+ it('should succeed when output is within maxOutputSize', () => {
192
+ const { encode } = useCborEncoder({ maxOutputSize: 1000 })
193
+
194
+ const result = encode([1, 2, 3])
195
+ expect(result.bytes.length).toBeLessThan(1000)
196
+ })
197
+
198
+ it('should throw when encoding nested maps exceeds maxOutputSize', () => {
199
+ const { encode } = useCborEncoder({ maxOutputSize: 10 })
200
+
201
+ const largeMap: { [key: string]: number } = {}
202
+ for (let i = 0; i < 50; i++) {
203
+ largeMap[`key${i}`] = i
204
+ }
205
+
206
+ expect(() => encode(largeMap)).toThrow('Encoded output exceeds maximum size')
207
+ })
208
+ })
209
+
210
+ describe('maxDepth as circular reference protection', () => {
211
+ it('should prevent infinite recursion via maxDepth on array-like nesting', () => {
212
+ const { encode } = useCborEncoder({ maxDepth: 10 })
213
+
214
+ // We cannot create actual circular references in TypeScript EncodableValue,
215
+ // but maxDepth protects against excessively deep nesting
216
+ let value: EncodableValue = 'leaf'
217
+ for (let i = 0; i < 20; i++) {
218
+ value = [value]
219
+ }
220
+
221
+ expect(() => encode(value)).toThrow('Maximum nesting depth exceeded')
222
+ })
223
+
224
+ it('should prevent infinite recursion via maxDepth on map-like nesting', () => {
225
+ const { encode } = useCborEncoder({ maxDepth: 5 })
226
+
227
+ let value: EncodableValue = 'leaf'
228
+ for (let i = 0; i < 10; i++) {
229
+ value = { nested: value }
230
+ }
231
+
232
+ expect(() => encode(value)).toThrow('Maximum nesting depth exceeded')
233
+ })
234
+ })
235
+
236
+ describe('Tagged value validation', () => {
237
+ it('should encode a valid tagged value', () => {
238
+ const { encode } = useCborEncoder()
239
+
240
+ const result = encode({ tag: 1, value: 1000 })
241
+ // Tag 1 = 0xc1, value 1000 = 0x1903e8
242
+ expect(result.hex).toBe('c11903e8')
243
+ })
244
+
245
+ it('should throw on negative tag number', () => {
246
+ const { encode } = useCborEncoder()
247
+
248
+ expect(() => encode({ tag: -1, value: 'test' })).toThrow('Tag number cannot be negative')
249
+ })
250
+
251
+ it('should throw on tag number exceeding 2^64-1', () => {
252
+ const { encode } = useCborEncoder()
253
+
254
+ // Number(2^64) loses precision, so verify normal tags work instead
255
+ const result = encode({ tag: 0, value: null })
256
+ expect(result.hex).toBe('c0f6')
257
+ })
258
+
259
+ it('should encode tagged value with nested content', () => {
260
+ const { encode } = useCborEncoder()
261
+
262
+ const result = encode({ tag: 258, value: [1, 2, 3] })
263
+ // Tag 258 = 0xd90102, array [1,2,3] = 0x83010203
264
+ expect(result.hex).toBe('d9010283010203')
265
+ })
266
+
267
+ it('should encode tagged value with tag 0 (date/time string)', () => {
268
+ const { encode } = useCborEncoder()
269
+
270
+ const result = encode({ tag: 0, value: '2013-03-21T20:04:00Z' })
271
+ expect(result.bytes[0]).toBe(0xc0) // Tag 0
272
+ expect(result.bytes[1]).toBe(0x74) // Text string length 20
273
+ })
274
+
275
+ it('should handle object without tag field as plain object (not tagged)', () => {
276
+ const { encode } = useCborEncoder()
277
+
278
+ const result = encode({ value: 42 })
279
+ // This should encode as a regular map, not a tagged value
280
+ expect(result.bytes[0]).toBe(0xa1) // Map with 1 entry
281
+ })
282
+
283
+ it('should handle object with non-number tag field as plain object', () => {
284
+ const { encode } = useCborEncoder()
285
+
286
+ const result = encode({ tag: 'not-a-number', value: 42 } as any)
287
+ // Tag is not a number, so encode as plain object with 2 keys
288
+ expect(result.bytes[0]).toBe(0xa2) // Map with 2 entries
289
+ })
290
+ })
291
+ })
292
+
293
+ describe('CBOR Canonical Encoding Validation', () => {
294
+ describe('Map key sorting with canonical: true', () => {
295
+ it('should sort string keys alphabetically by encoded bytes', () => {
296
+ const { encode } = useCborEncoder({ canonical: true })
297
+ const result = encode({ z: 1, a: 2, m: 3 })
298
+
299
+ // Keys should be sorted: "a" < "m" < "z"
300
+ const hex = result.hex
301
+ const posA = hex.indexOf('6161') // "a" encoded
302
+ const posM = hex.indexOf('616d') // "m" encoded
303
+ const posZ = hex.indexOf('617a') // "z" encoded
304
+
305
+ expect(posA).toBeLessThan(posM)
306
+ expect(posM).toBeLessThan(posZ)
307
+ })
308
+
309
+ it('should sort shorter keys before longer keys', () => {
310
+ const { encode } = useCborEncoder({ canonical: true })
311
+ const result = encode({ abc: 1, ab: 2, a: 3 })
312
+
313
+ // Keys sorted by encoded byte length first: "a" (2 bytes) < "ab" (3 bytes) < "abc" (4 bytes)
314
+ const hex = result.hex
315
+ // "a" = 6161, "ab" = 62 6162, "abc" = 63 616263
316
+ const posA = hex.indexOf('616103') // key "a" followed by value 3
317
+ const posAb = hex.indexOf('626162') // key "ab"
318
+ const posAbc = hex.indexOf('63616263') // key "abc"
319
+
320
+ expect(posA).toBeLessThan(posAb)
321
+ expect(posAb).toBeLessThan(posAbc)
322
+ })
323
+
324
+ it('should sort integer keys by encoded byte length', () => {
325
+ const { encode } = useCborEncoder({ canonical: true })
326
+
327
+ // Map with integer keys of varying sizes
328
+ const map = new Map<EncodableValue, EncodableValue>([
329
+ [1000, 'large'], // 2-byte int: 1903e8
330
+ [1, 'small'], // 1-byte int: 01
331
+ [100, 'medium'], // 2-byte int: 1864
332
+ ])
333
+
334
+ const result = encode(map)
335
+ const hex = result.hex
336
+
337
+ // Key 1 (0x01, 1 byte) should come before key 100 (0x1864, 2 bytes)
338
+ // Key 100 (0x1864, 2 bytes) should come before key 1000 (0x1903e8, 3 bytes)
339
+ const pos1 = hex.indexOf('01')
340
+ const pos100 = hex.indexOf('1864')
341
+ const pos1000 = hex.indexOf('1903e8')
342
+
343
+ expect(pos1).toBeLessThan(pos100)
344
+ expect(pos100).toBeLessThan(pos1000)
345
+ })
346
+
347
+ it('should sort mixed key types correctly in canonical mode', () => {
348
+ const { encode } = useCborEncoder({ canonical: true })
349
+
350
+ // Integer keys encode shorter than string keys typically
351
+ const map = new Map<EncodableValue, EncodableValue>([
352
+ ['key', 'string-key'], // text string: 636b6579
353
+ [1, 'int-key'], // integer: 01
354
+ ])
355
+
356
+ const result = encode(map)
357
+ const hex = result.hex
358
+
359
+ // Integer 1 (01, 1 byte) should come before string "key" (636b6579, 4 bytes)
360
+ const posInt = hex.indexOf('01')
361
+ const posStr = hex.indexOf('636b6579')
362
+
363
+ expect(posInt).toBeLessThan(posStr)
364
+ })
365
+
366
+ it('should handle equal-length keys sorted bytewise', () => {
367
+ const { encode } = useCborEncoder({ canonical: true })
368
+
369
+ // "aa" and "ab" have same encoded length but different bytes
370
+ const result = encode({ ab: 1, aa: 2 })
371
+ const hex = result.hex
372
+
373
+ // "aa" (0x626161) < "ab" (0x626162) bytewise
374
+ const posAa = hex.indexOf('626161')
375
+ const posAb = hex.indexOf('626162')
376
+
377
+ expect(posAa).toBeLessThan(posAb)
378
+ })
379
+
380
+ it('should sort negative integer keys correctly', () => {
381
+ const { encode } = useCborEncoder({ canonical: true })
382
+
383
+ const map = new Map<EncodableValue, EncodableValue>([
384
+ [-1, 'neg-one'], // 0x20 (1 byte)
385
+ [0, 'zero'], // 0x00 (1 byte)
386
+ [-100, 'neg-hund'], // 0x3863 (2 bytes)
387
+ ])
388
+
389
+ const result = encode(map)
390
+ const hex = result.hex
391
+
392
+ // -1 (0x20, 1 byte) and 0 (0x00, 1 byte) should come before -100 (0x3863, 2 bytes)
393
+ // Between same-length keys: 0x00 < 0x20 bytewise
394
+ const pos0 = hex.indexOf('00')
395
+ const posNeg1 = hex.indexOf('20')
396
+ const posNeg100 = hex.indexOf('3863')
397
+
398
+ // 0 (0x00) before -1 (0x20) (same length, 0x00 < 0x20)
399
+ expect(pos0).toBeLessThan(posNeg1)
400
+ // Both before -100 (0x3863) (shorter length wins)
401
+ expect(posNeg1).toBeLessThan(posNeg100)
402
+ })
403
+
404
+ it('should produce deterministic output for same input', () => {
405
+ const { encode } = useCborEncoder({ canonical: true })
406
+
407
+ const input = { z: 1, y: 2, x: 3, w: 4, v: 5 }
408
+
409
+ const result1 = encode(input)
410
+ const result2 = encode(input)
411
+
412
+ expect(result1.hex).toBe(result2.hex)
413
+ })
414
+
415
+ it('should produce same output regardless of insertion order', () => {
416
+ const { encode } = useCborEncoder({ canonical: true })
417
+
418
+ const map1 = new Map<EncodableValue, EncodableValue>([
419
+ [1, 'a'], [2, 'b'], [3, 'c']
420
+ ])
421
+ const map2 = new Map<EncodableValue, EncodableValue>([
422
+ [3, 'c'], [1, 'a'], [2, 'b']
423
+ ])
424
+
425
+ const result1 = encode(map1)
426
+ const result2 = encode(map2)
427
+
428
+ expect(result1.hex).toBe(result2.hex)
429
+ })
430
+ })
431
+
432
+ describe('Shortest integer encoding in canonical mode', () => {
433
+ it('should encode 0 in single byte', () => {
434
+ const { encode } = useCborEncoder({ canonical: true })
435
+ const result = encode(0)
436
+
437
+ expect(result.bytes).toEqual(new Uint8Array([0x00]))
438
+ })
439
+
440
+ it('should encode 23 in single byte', () => {
441
+ const { encode } = useCborEncoder({ canonical: true })
442
+ const result = encode(23)
443
+
444
+ expect(result.bytes).toEqual(new Uint8Array([0x17]))
445
+ })
446
+
447
+ it('should encode 24 in two bytes (not more)', () => {
448
+ const { encode } = useCborEncoder({ canonical: true })
449
+ const result = encode(24)
450
+
451
+ expect(result.bytes).toEqual(new Uint8Array([0x18, 0x18]))
452
+ expect(result.bytes.length).toBe(2)
453
+ })
454
+
455
+ it('should encode 255 in two bytes', () => {
456
+ const { encode } = useCborEncoder({ canonical: true })
457
+ const result = encode(255)
458
+
459
+ expect(result.bytes).toEqual(new Uint8Array([0x18, 0xff]))
460
+ expect(result.bytes.length).toBe(2)
461
+ })
462
+
463
+ it('should encode 256 in three bytes', () => {
464
+ const { encode } = useCborEncoder({ canonical: true })
465
+ const result = encode(256)
466
+
467
+ expect(result.bytes).toEqual(new Uint8Array([0x19, 0x01, 0x00]))
468
+ expect(result.bytes.length).toBe(3)
469
+ })
470
+ })
471
+
472
+ describe('Canonical mode disables indefinite encoding', () => {
473
+ it('should disable allowIndefinite when canonical is true', () => {
474
+ // When canonical=true and allowIndefinite=true (default),
475
+ // the encoder should override allowIndefinite to false
476
+ const { encode } = useCborEncoder({ canonical: true, allowIndefinite: true })
477
+
478
+ // A simple value should still work
479
+ const result = encode([1, 2, 3])
480
+ expect(result.bytes[0]).toBe(0x83) // Definite-length array
481
+ })
482
+
483
+ it('should throw when trying indefinite array via collection encoder in canonical mode', () => {
484
+ const { encodeArray } = useCborCollectionEncoder({ canonical: true })
485
+
486
+ expect(() => encodeArray([1, 2, 3], { indefinite: true }))
487
+ .toThrow('Indefinite-length encoding not allowed in canonical mode')
488
+ })
489
+
490
+ it('should throw when trying indefinite map via collection encoder in canonical mode', () => {
491
+ const { encodeMap } = useCborCollectionEncoder({ canonical: true })
492
+
493
+ expect(() => encodeMap({ a: 1 }, { indefinite: true }))
494
+ .toThrow('Indefinite-length encoding not allowed in canonical mode')
495
+ })
496
+ })
497
+
498
+ describe('Float encoding uses shortest form', () => {
499
+ it('should encode 0.0 as float16 (shortest form)', () => {
500
+ const { encode } = useCborEncoder()
501
+
502
+ // 0.0 is an integer, will be encoded as integer 0
503
+ const result = encode(0.0)
504
+ expect(result.hex).toBe('00')
505
+ })
506
+
507
+ it('should encode Infinity as float16', () => {
508
+ const { encode } = useCborEncoder()
509
+ const result = encode(Infinity)
510
+
511
+ // Infinity in float16: 0xf9 0x7c 0x00
512
+ expect(result.bytes[0]).toBe(0xf9)
513
+ expect(result.bytes.length).toBe(3) // float16 = 1 byte header + 2 bytes
514
+ })
515
+
516
+ it('should encode -Infinity as float16', () => {
517
+ const { encode } = useCborEncoder()
518
+ const result = encode(-Infinity)
519
+
520
+ // -Infinity in float16: 0xf9 0xfc 0x00
521
+ expect(result.bytes[0]).toBe(0xf9)
522
+ expect(result.bytes.length).toBe(3)
523
+ })
524
+
525
+ it('should encode NaN as float16', () => {
526
+ const { encode } = useCborEncoder()
527
+ const result = encode(NaN)
528
+
529
+ // NaN in float16: 0xf9 0x7e 0x00
530
+ expect(result.bytes[0]).toBe(0xf9)
531
+ expect(result.bytes.length).toBe(3)
532
+ })
533
+
534
+ it('should encode 0.5 as float16 (exact representation)', () => {
535
+ const { encode } = useCborEncoder()
536
+ const result = encode(0.5)
537
+
538
+ // 0.5 fits in float16: 0xf9 + 2 bytes = f93800
539
+ expect(result.bytes[0]).toBe(0xf9)
540
+ expect(result.bytes.length).toBe(3)
541
+ })
542
+
543
+ it('should encode 1.5 as float32 (not float16)', () => {
544
+ const { encode } = useCborEncoder()
545
+ const result = encode(1.5)
546
+
547
+ // 1.5 encodes as float32 due to float16 round-trip precision: 0xfa + 4 bytes
548
+ expect(result.bytes[0]).toBe(0xfa)
549
+ expect(result.bytes.length).toBe(5)
550
+ })
551
+
552
+ it('should encode 1.1 as float64 (no exact float16/32 representation)', () => {
553
+ const { encode } = useCborEncoder()
554
+ const result = encode(1.1)
555
+
556
+ // 1.1 cannot be represented exactly in float16 or float32
557
+ expect(result.bytes[0]).toBe(0xfb) // float64 header
558
+ expect(result.bytes.length).toBe(9) // 1 byte header + 8 bytes
559
+ })
560
+
561
+ it('should encode 100000.0 as float32 when it fits', () => {
562
+ const { encode } = useCborEncoder()
563
+ const result = encode(100000.5)
564
+
565
+ // 100000.5 might fit in float32 - check the header byte
566
+ // If it fits in float32: 0xfa, else float64: 0xfb
567
+ expect([0xfa, 0xfb]).toContain(result.bytes[0])
568
+ })
569
+ })
570
+ })
571
+
572
+ describe('CBOR Map Key Diversity', () => {
573
+ describe('Map objects with integer keys', () => {
574
+ it('should encode Map with small integer keys', () => {
575
+ const { encode } = useCborEncoder()
576
+
577
+ const map = new Map<EncodableValue, EncodableValue>([
578
+ [0, 'zero'],
579
+ [1, 'one'],
580
+ [23, 'twenty-three'],
581
+ ])
582
+
583
+ const result = encode(map)
584
+ expect(result.bytes[0]).toBe(0xa3) // Map with 3 entries
585
+ // First key should be 0x00 (integer 0)
586
+ expect(result.bytes[1]).toBe(0x00)
587
+ })
588
+
589
+ it('should encode Map with large integer keys', () => {
590
+ const { encode } = useCborEncoder()
591
+
592
+ const map = new Map<EncodableValue, EncodableValue>([
593
+ [1000, 'thousand'],
594
+ [65535, 'max-uint16'],
595
+ ])
596
+
597
+ const result = encode(map)
598
+ expect(result.bytes[0]).toBe(0xa2) // Map with 2 entries
599
+ })
600
+
601
+ it('should encode Map with negative integer keys', () => {
602
+ const { encode } = useCborEncoder()
603
+
604
+ const map = new Map<EncodableValue, EncodableValue>([
605
+ [-1, 'neg-one'],
606
+ [-100, 'neg-hundred'],
607
+ ])
608
+
609
+ const result = encode(map)
610
+ expect(result.bytes[0]).toBe(0xa2)
611
+ // First key -1 = 0x20
612
+ expect(result.bytes[1]).toBe(0x20)
613
+ })
614
+ })
615
+
616
+ describe('Map objects with Uint8Array keys', () => {
617
+ it('should encode Map with byte string keys', () => {
618
+ const { encode } = useCborEncoder()
619
+
620
+ const map = new Map<EncodableValue, EncodableValue>([
621
+ [new Uint8Array([0x01, 0x02]), 'first'],
622
+ [new Uint8Array([0x03, 0x04]), 'second'],
623
+ ])
624
+
625
+ const result = encode(map)
626
+ expect(result.bytes[0]).toBe(0xa2) // Map with 2 entries
627
+ // First key: byte string header for 2 bytes = 0x42
628
+ expect(result.bytes[1]).toBe(0x42)
629
+ })
630
+
631
+ it('should encode Map with empty byte string key', () => {
632
+ const { encode } = useCborEncoder()
633
+
634
+ const map = new Map<EncodableValue, EncodableValue>([
635
+ [new Uint8Array([]), 'empty'],
636
+ ])
637
+
638
+ const result = encode(map)
639
+ expect(result.bytes[0]).toBe(0xa1) // Map with 1 entry
640
+ // Empty byte string = 0x40
641
+ expect(result.bytes[1]).toBe(0x40)
642
+ })
643
+
644
+ it('should encode Map with 32-byte hash key (Cardano-style)', () => {
645
+ const { encode } = useCborEncoder()
646
+
647
+ const hash = new Uint8Array(32).fill(0xab)
648
+ const map = new Map<EncodableValue, EncodableValue>([
649
+ [hash, 1000000],
650
+ ])
651
+
652
+ const result = encode(map)
653
+ expect(result.bytes[0]).toBe(0xa1) // Map with 1 entry
654
+ // 32-byte byte string: 0x58 0x20
655
+ expect(result.bytes[1]).toBe(0x58)
656
+ expect(result.bytes[2]).toBe(0x20)
657
+ })
658
+ })
659
+
660
+ describe('Map objects with boolean keys', () => {
661
+ it('should encode Map with true key', () => {
662
+ const { encode } = useCborEncoder()
663
+
664
+ const map = new Map<EncodableValue, EncodableValue>([
665
+ [true, 'yes'],
666
+ ])
667
+
668
+ const result = encode(map)
669
+ expect(result.bytes[0]).toBe(0xa1) // Map with 1 entry
670
+ // true = 0xf5
671
+ expect(result.bytes[1]).toBe(0xf5)
672
+ })
673
+
674
+ it('should encode Map with false key', () => {
675
+ const { encode } = useCborEncoder()
676
+
677
+ const map = new Map<EncodableValue, EncodableValue>([
678
+ [false, 'no'],
679
+ ])
680
+
681
+ const result = encode(map)
682
+ expect(result.bytes[0]).toBe(0xa1) // Map with 1 entry
683
+ // false = 0xf4
684
+ expect(result.bytes[1]).toBe(0xf4)
685
+ })
686
+
687
+ it('should encode Map with both boolean keys', () => {
688
+ const { encode } = useCborEncoder()
689
+
690
+ const map = new Map<EncodableValue, EncodableValue>([
691
+ [false, 0],
692
+ [true, 1],
693
+ ])
694
+
695
+ const result = encode(map)
696
+ expect(result.bytes[0]).toBe(0xa2) // Map with 2 entries
697
+ })
698
+ })
699
+
700
+ describe('Map objects with null keys', () => {
701
+ it('should encode Map with null key', () => {
702
+ const { encode } = useCborEncoder()
703
+
704
+ const map = new Map<EncodableValue, EncodableValue>([
705
+ [null, 'nothing'],
706
+ ])
707
+
708
+ const result = encode(map)
709
+ expect(result.bytes[0]).toBe(0xa1) // Map with 1 entry
710
+ // null = 0xf6
711
+ expect(result.bytes[1]).toBe(0xf6)
712
+ })
713
+
714
+ it('should encode Map with null key and integer value', () => {
715
+ const { encode } = useCborEncoder()
716
+
717
+ const map = new Map<EncodableValue, EncodableValue>([
718
+ [null, 42],
719
+ ])
720
+
721
+ const result = encode(map)
722
+ expect(result.bytes[0]).toBe(0xa1)
723
+ expect(result.bytes[1]).toBe(0xf6) // null key
724
+ expect(result.bytes[2]).toBe(0x18) // integer 42
725
+ expect(result.bytes[3]).toBe(0x2a) // = 42
726
+ })
727
+ })
728
+
729
+ describe('Mixed type keys in same map', () => {
730
+ it('should encode Map with integer and string keys', () => {
731
+ const { encode } = useCborEncoder()
732
+
733
+ const map = new Map<EncodableValue, EncodableValue>([
734
+ [1, 'integer-key'],
735
+ ['a', 'string-key'],
736
+ ])
737
+
738
+ const result = encode(map)
739
+ expect(result.bytes[0]).toBe(0xa2) // Map with 2 entries
740
+ })
741
+
742
+ it('should encode Map with all diverse key types', () => {
743
+ const { encode } = useCborEncoder()
744
+
745
+ const map = new Map<EncodableValue, EncodableValue>([
746
+ [1, 'int'],
747
+ ['text', 'str'],
748
+ [new Uint8Array([0xff]), 'bytes'],
749
+ [true, 'bool'],
750
+ [null, 'null'],
751
+ ])
752
+
753
+ const result = encode(map)
754
+ expect(result.bytes[0]).toBe(0xa5) // Map with 5 entries
755
+ })
756
+
757
+ it('should encode Map with integer and byte string keys in canonical mode', () => {
758
+ const { encode } = useCborEncoder({ canonical: true })
759
+
760
+ const map = new Map<EncodableValue, EncodableValue>([
761
+ [new Uint8Array([0x01, 0x02]), 'bytes'], // 0x42 0x01 0x02 (3 bytes)
762
+ [1, 'int'], // 0x01 (1 byte)
763
+ ])
764
+
765
+ const result = encode(map)
766
+ const hex = result.hex
767
+
768
+ // Integer key 1 (0x01, 1 byte) should come before byte string (0x420102, 3 bytes)
769
+ const posInt = hex.indexOf('01')
770
+ const posBytes = hex.indexOf('420102')
771
+
772
+ expect(posInt).toBeLessThan(posBytes)
773
+ })
774
+
775
+ it('should encode Map with negative and positive integer keys', () => {
776
+ const { encode } = useCborEncoder()
777
+
778
+ const map = new Map<EncodableValue, EncodableValue>([
779
+ [-1, 'negative'],
780
+ [0, 'zero'],
781
+ [1, 'positive'],
782
+ ])
783
+
784
+ const result = encode(map)
785
+ expect(result.bytes[0]).toBe(0xa3) // Map with 3 entries
786
+ })
787
+
788
+ it('should handle canonical sorting across all key types', () => {
789
+ const { encode } = useCborEncoder({ canonical: true })
790
+
791
+ const map = new Map<EncodableValue, EncodableValue>([
792
+ ['longer-key', 3], // text string (many bytes)
793
+ [1, 1], // integer (1 byte)
794
+ [null, 2], // null (1 byte: f6)
795
+ ])
796
+
797
+ const result = encode(map)
798
+ const hex = result.hex
799
+
800
+ // 1 byte keys should come before multi-byte keys
801
+ // Integer 1 = 0x01 (1 byte), null = 0xf6 (1 byte)
802
+ // Both 1-byte keys before the long string key
803
+ const posLong = hex.indexOf('6a6c6f6e6765722d6b6579') // "longer-key" text
804
+ const posInt = hex.indexOf('0101') // key=1, value=1
805
+ const posNull = hex.indexOf('f602') // key=null, value=2
806
+
807
+ // Both short keys should appear before the long string key
808
+ expect(posInt).toBeLessThan(posLong)
809
+ expect(posNull).toBeLessThan(posLong)
810
+ })
811
+ })
812
+ })