@marcuspuchalla/nachos 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
  3. package/dist/chunk-5IWW5H47.js.map +1 -0
  4. package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
  5. package/dist/chunk-RVG2BY32.cjs.map +1 -0
  6. package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
  7. package/dist/chunk-S4RXO6IB.cjs.map +1 -0
  8. package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
  9. package/dist/chunk-UMAX5MX5.js.map +1 -0
  10. package/dist/encoder/index.cjs +13 -13
  11. package/dist/encoder/index.d.cts +2 -2
  12. package/dist/encoder/index.d.ts +2 -2
  13. package/dist/encoder/index.js +1 -1
  14. package/dist/index.cjs +32 -32
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +28 -19
  17. package/dist/index.d.ts +28 -19
  18. package/dist/index.js +16 -16
  19. package/dist/index.js.map +1 -1
  20. package/dist/metafile-cjs.json +1 -1
  21. package/dist/metafile-esm.json +1 -1
  22. package/dist/parser/index.cjs +14 -14
  23. package/dist/parser/index.d.cts +3 -1
  24. package/dist/parser/index.d.ts +3 -1
  25. package/dist/parser/index.js +1 -1
  26. package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
  27. package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
  28. package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
  29. package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
  30. package/package.json +1 -1
  31. package/src/__tests__/public-api.test.ts +153 -0
  32. package/src/__tests__/roundtrip.test.ts +701 -0
  33. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
  34. package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
  35. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
  36. package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
  37. package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
  38. package/src/encoder/composables/useCborEncoder.ts +27 -1
  39. package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
  40. package/src/encoder/composables/useCborStringEncoder.ts +23 -10
  41. package/src/encoder/types.ts +0 -2
  42. package/src/index.ts +29 -20
  43. package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
  44. package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
  45. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
  46. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
  47. package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
  48. package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
  49. package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
  50. package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
  51. package/src/parser/composables/useCborCollection.ts +45 -42
  52. package/src/parser/composables/useCborFloat.ts +95 -9
  53. package/src/parser/composables/useCborInteger.ts +24 -10
  54. package/src/parser/composables/useCborParser.ts +387 -216
  55. package/src/parser/composables/useCborString.ts +22 -4
  56. package/src/parser/composables/useCborTag.ts +149 -53
  57. package/src/parser/utils.ts +11 -0
  58. package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
  59. package/dist/chunk-2HBCILJS.cjs.map +0 -1
  60. package/dist/chunk-7CFYWHS6.js.map +0 -1
  61. package/dist/chunk-ZRPJUEIZ.js.map +0 -1
  62. package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
@@ -0,0 +1,701 @@
1
+ /**
2
+ * Round-Trip Tests for NACHOS CBOR Library
3
+ *
4
+ * Tests that encode(value) -> decode(hex) -> value produces the original value
5
+ * for all supported CBOR types and edge cases.
6
+ *
7
+ * Key considerations:
8
+ * - Decoder returns Map for CBOR maps (not plain objects)
9
+ * - Decoder returns plain Uint8Array for definite-length byte strings
10
+ * - NaN !== NaN, so use Number.isNaN()
11
+ * - Encoder treats -0.0 as integer 0 (since Number.isInteger(-0) is true)
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest'
15
+ import { encode, decode } from '../index'
16
+ // Note: The decoder returns plain Uint8Array for definite-length byte strings,
17
+ // not CborByteString objects. CborByteString is only used internally.
18
+
19
+ /**
20
+ * Helper: round-trip a value through encode -> decode and return the decoded value.
21
+ */
22
+ function roundTrip(value: any): any {
23
+ const encoded = encode(value)
24
+ const decoded = decode(encoded.hex)
25
+ return decoded.value
26
+ }
27
+
28
+ /**
29
+ * Helper: check that the hex produced by encode can be decoded back,
30
+ * and that bytesRead matches the encoded length.
31
+ */
32
+ function roundTripFull(value: any): { hex: string; decoded: any; bytesRead: number } {
33
+ const encoded = encode(value)
34
+ const decoded = decode(encoded.hex)
35
+ return { hex: encoded.hex, decoded: decoded.value, bytesRead: decoded.bytesRead }
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // 1. Integers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ describe('Round-trip: Integers', () => {
43
+ describe('unsigned integers', () => {
44
+ it('should round-trip 0', () => {
45
+ expect(roundTrip(0)).toBe(0)
46
+ })
47
+
48
+ it('should round-trip 1', () => {
49
+ expect(roundTrip(1)).toBe(1)
50
+ })
51
+
52
+ it('should round-trip 23 (max direct encoding)', () => {
53
+ expect(roundTrip(23)).toBe(23)
54
+ })
55
+
56
+ it('should round-trip 24 (1-byte follows)', () => {
57
+ expect(roundTrip(24)).toBe(24)
58
+ })
59
+
60
+ it('should round-trip 255 (max 1-byte)', () => {
61
+ expect(roundTrip(255)).toBe(255)
62
+ })
63
+
64
+ it('should round-trip 256 (2-byte follows)', () => {
65
+ expect(roundTrip(256)).toBe(256)
66
+ })
67
+
68
+ it('should round-trip 65535 (max 2-byte)', () => {
69
+ expect(roundTrip(65535)).toBe(65535)
70
+ })
71
+
72
+ it('should round-trip 65536 (4-byte follows)', () => {
73
+ expect(roundTrip(65536)).toBe(65536)
74
+ })
75
+
76
+ it('should round-trip 2^32 - 1 (max 4-byte)', () => {
77
+ expect(roundTrip(4294967295)).toBe(4294967295)
78
+ })
79
+
80
+ it('should round-trip 2^32 (8-byte follows)', () => {
81
+ expect(roundTrip(4294967296)).toBe(4294967296)
82
+ })
83
+
84
+ it('should round-trip Number.MAX_SAFE_INTEGER', () => {
85
+ expect(roundTrip(Number.MAX_SAFE_INTEGER)).toBe(Number.MAX_SAFE_INTEGER)
86
+ })
87
+ })
88
+
89
+ describe('negative integers', () => {
90
+ it('should round-trip -1', () => {
91
+ expect(roundTrip(-1)).toBe(-1)
92
+ })
93
+
94
+ it('should round-trip -24 (max direct negative encoding)', () => {
95
+ expect(roundTrip(-24)).toBe(-24)
96
+ })
97
+
98
+ it('should round-trip -25 (1-byte follows)', () => {
99
+ expect(roundTrip(-25)).toBe(-25)
100
+ })
101
+
102
+ it('should round-trip -256', () => {
103
+ expect(roundTrip(-256)).toBe(-256)
104
+ })
105
+
106
+ it('should round-trip -257 (2-byte follows)', () => {
107
+ expect(roundTrip(-257)).toBe(-257)
108
+ })
109
+
110
+ it('should round-trip -65536', () => {
111
+ expect(roundTrip(-65536)).toBe(-65536)
112
+ })
113
+
114
+ it('should round-trip -65537 (4-byte follows)', () => {
115
+ expect(roundTrip(-65537)).toBe(-65537)
116
+ })
117
+
118
+ it('should round-trip Number.MIN_SAFE_INTEGER', () => {
119
+ expect(roundTrip(Number.MIN_SAFE_INTEGER)).toBe(Number.MIN_SAFE_INTEGER)
120
+ })
121
+ })
122
+
123
+ describe('BigInt values', () => {
124
+ it('should round-trip BigInt(0)', () => {
125
+ expect(roundTrip(0n)).toBe(0)
126
+ })
127
+
128
+ it('should round-trip BigInt larger than MAX_SAFE_INTEGER', () => {
129
+ const big = 2n ** 53n + 1n
130
+ const result = roundTrip(big)
131
+ expect(result).toBe(big)
132
+ })
133
+
134
+ it('should round-trip large negative BigInt', () => {
135
+ const big = -(2n ** 53n + 1n)
136
+ const result = roundTrip(big)
137
+ expect(result).toBe(big)
138
+ })
139
+
140
+ it('should round-trip BigInt at 2^64 - 1 boundary', () => {
141
+ const big = 2n ** 64n - 1n
142
+ const result = roundTrip(big)
143
+ expect(result).toBe(big)
144
+ })
145
+ })
146
+ })
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // 2. Floats
150
+ // ---------------------------------------------------------------------------
151
+
152
+ describe('Round-trip: Floats', () => {
153
+ it('should round-trip 0.0 (encodes as integer 0)', () => {
154
+ // 0.0 is an integer, so encoder uses integer encoding
155
+ expect(roundTrip(0.0)).toBe(0)
156
+ })
157
+
158
+ it('should encode -0.0 as float16 preserving sign', () => {
159
+ // -0.0 is detected by the encoder and encoded as float16 (f98000),
160
+ // which correctly preserves the negative zero sign bit.
161
+ const encoded = encode(-0.0)
162
+ expect(encoded.hex).toBe('f98000')
163
+ expect(Object.is(roundTrip(-0.0), -0)).toBe(true)
164
+ })
165
+
166
+ it('should round-trip 1.5 (exact in float16)', () => {
167
+ expect(roundTrip(1.5)).toBe(1.5)
168
+ })
169
+
170
+ it('should round-trip -4.1 (requires float64)', () => {
171
+ expect(roundTrip(-4.1)).toBeCloseTo(-4.1, 15)
172
+ })
173
+
174
+ it('should round-trip Infinity', () => {
175
+ expect(roundTrip(Infinity)).toBe(Infinity)
176
+ })
177
+
178
+ it('should round-trip -Infinity', () => {
179
+ expect(roundTrip(-Infinity)).toBe(-Infinity)
180
+ })
181
+
182
+ it('should round-trip NaN', () => {
183
+ const result = roundTrip(NaN)
184
+ expect(Number.isNaN(result)).toBe(true)
185
+ })
186
+
187
+ it('should round-trip a very small float (subnormal in float16)', () => {
188
+ // 5.960464477539063e-8 is the smallest positive subnormal float16
189
+ const val = 5.960464477539063e-8
190
+ const result = roundTrip(val)
191
+ expect(result).toBe(val)
192
+ })
193
+
194
+ it('should round-trip 65504 (max finite float16)', () => {
195
+ expect(roundTrip(65504.0)).toBe(65504)
196
+ })
197
+
198
+ it('should round-trip 3.4028234663852886e+38 (max float32)', () => {
199
+ const val = 3.4028234663852886e+38
200
+ expect(roundTrip(val)).toBe(val)
201
+ })
202
+
203
+ it('should round-trip 1.1 (requires float64 precision)', () => {
204
+ expect(roundTrip(1.1)).toBe(1.1)
205
+ })
206
+
207
+ it('should round-trip Number.EPSILON', () => {
208
+ const val = Number.EPSILON
209
+ expect(roundTrip(val)).toBe(val)
210
+ })
211
+ })
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // 3. Strings
215
+ // ---------------------------------------------------------------------------
216
+
217
+ describe('Round-trip: Strings', () => {
218
+ it('should round-trip empty string', () => {
219
+ expect(roundTrip('')).toBe('')
220
+ })
221
+
222
+ it('should round-trip ASCII string', () => {
223
+ expect(roundTrip('hello')).toBe('hello')
224
+ })
225
+
226
+ it('should round-trip "IETF" (from RFC 8949 examples)', () => {
227
+ expect(roundTrip('IETF')).toBe('IETF')
228
+ })
229
+
230
+ it('should round-trip UTF-8 multi-byte characters', () => {
231
+ expect(roundTrip('\u00fc')).toBe('\u00fc') // u-umlaut
232
+ })
233
+
234
+ it('should round-trip CJK characters', () => {
235
+ expect(roundTrip('\u6c34')).toBe('\u6c34') // water
236
+ })
237
+
238
+ it('should round-trip emoji', () => {
239
+ expect(roundTrip('\ud83d\ude00')).toBe('\ud83d\ude00') // grinning face
240
+ })
241
+
242
+ it('should round-trip string with mixed scripts', () => {
243
+ const mixed = 'Hello \u4e16\u754c \ud83c\udf0d!'
244
+ expect(roundTrip(mixed)).toBe(mixed)
245
+ })
246
+
247
+ it('should round-trip long string (> 256 bytes)', () => {
248
+ const long = 'a'.repeat(300)
249
+ expect(roundTrip(long)).toBe(long)
250
+ })
251
+
252
+ it('should round-trip string with special characters', () => {
253
+ const special = 'line1\nline2\ttab\r\nwindows'
254
+ expect(roundTrip(special)).toBe(special)
255
+ })
256
+
257
+ it('should round-trip string with null byte', () => {
258
+ const withNull = 'before\x00after'
259
+ expect(roundTrip(withNull)).toBe(withNull)
260
+ })
261
+ })
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // 4. Byte Strings
265
+ // ---------------------------------------------------------------------------
266
+
267
+ describe('Round-trip: Byte Strings', () => {
268
+ it('should round-trip empty Uint8Array', () => {
269
+ const result = roundTrip(new Uint8Array([]))
270
+ expect(result).toBeInstanceOf(Uint8Array)
271
+ expect(result).toEqual(new Uint8Array([]))
272
+ })
273
+
274
+ it('should round-trip single byte', () => {
275
+ const result = roundTrip(new Uint8Array([0xff]))
276
+ expect(result).toBeInstanceOf(Uint8Array)
277
+ expect(result).toEqual(new Uint8Array([0xff]))
278
+ })
279
+
280
+ it('should round-trip multi-byte Uint8Array', () => {
281
+ const input = new Uint8Array([0x01, 0x02, 0x03, 0x04])
282
+ const result = roundTrip(input)
283
+ expect(result).toBeInstanceOf(Uint8Array)
284
+ expect(result).toEqual(input)
285
+ })
286
+
287
+ it('should round-trip 256-byte Uint8Array', () => {
288
+ const input = new Uint8Array(256)
289
+ for (let i = 0; i < 256; i++) input[i] = i
290
+ const result = roundTrip(input)
291
+ expect(result).toBeInstanceOf(Uint8Array)
292
+ expect(result).toEqual(input)
293
+ })
294
+
295
+ it('should round-trip all-zeros byte string', () => {
296
+ const input = new Uint8Array([0x00, 0x00, 0x00])
297
+ const result = roundTrip(input)
298
+ expect(result).toBeInstanceOf(Uint8Array)
299
+ expect(result).toEqual(input)
300
+ })
301
+ })
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // 5. Booleans / null / undefined
305
+ // ---------------------------------------------------------------------------
306
+
307
+ describe('Round-trip: Booleans, null, undefined', () => {
308
+ it('should round-trip true', () => {
309
+ expect(roundTrip(true)).toBe(true)
310
+ })
311
+
312
+ it('should round-trip false', () => {
313
+ expect(roundTrip(false)).toBe(false)
314
+ })
315
+
316
+ it('should round-trip null', () => {
317
+ expect(roundTrip(null)).toBe(null)
318
+ })
319
+
320
+ it('should round-trip undefined', () => {
321
+ // CBOR encodes undefined as 0xf7, which decodes back to undefined
322
+ expect(roundTrip(undefined)).toBe(undefined)
323
+ })
324
+ })
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // 6. Arrays
328
+ // ---------------------------------------------------------------------------
329
+
330
+ describe('Round-trip: Arrays', () => {
331
+ it('should round-trip empty array', () => {
332
+ expect(roundTrip([])).toEqual([])
333
+ })
334
+
335
+ it('should round-trip single-element array', () => {
336
+ expect(roundTrip([42])).toEqual([42])
337
+ })
338
+
339
+ it('should round-trip array of integers', () => {
340
+ expect(roundTrip([1, 2, 3])).toEqual([1, 2, 3])
341
+ })
342
+
343
+ it('should round-trip nested arrays', () => {
344
+ expect(roundTrip([[1, 2], [3, 4]])).toEqual([[1, 2], [3, 4]])
345
+ })
346
+
347
+ it('should round-trip mixed-type array', () => {
348
+ const input = [1, 'hello', true, null]
349
+ const result = roundTrip(input) as any[]
350
+ expect(result[0]).toBe(1)
351
+ expect(result[1]).toBe('hello')
352
+ expect(result[2]).toBe(true)
353
+ expect(result[3]).toBe(null)
354
+ })
355
+
356
+ it('should round-trip deeply nested array', () => {
357
+ const deep = [[[[[1]]]]]
358
+ expect(roundTrip(deep)).toEqual([[[[[1]]]]])
359
+ })
360
+
361
+ it('should round-trip large array (100 elements)', () => {
362
+ const input = Array.from({ length: 100 }, (_, i) => i)
363
+ expect(roundTrip(input)).toEqual(input)
364
+ })
365
+
366
+ it('should round-trip array with negative integers', () => {
367
+ expect(roundTrip([-1, -100, -1000])).toEqual([-1, -100, -1000])
368
+ })
369
+ })
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // 7. Maps / Objects
373
+ // ---------------------------------------------------------------------------
374
+
375
+ describe('Round-trip: Maps and Objects', () => {
376
+ it('should round-trip empty object as empty Map', () => {
377
+ const result = roundTrip({}) as Map<any, any>
378
+ expect(result).toBeInstanceOf(Map)
379
+ expect(result.size).toBe(0)
380
+ })
381
+
382
+ it('should round-trip object with string keys as Map', () => {
383
+ const result = roundTrip({ a: 1, b: 2 }) as Map<any, any>
384
+ expect(result).toBeInstanceOf(Map)
385
+ expect(result.get('a')).toBe(1)
386
+ expect(result.get('b')).toBe(2)
387
+ })
388
+
389
+ it('should round-trip nested object', () => {
390
+ const result = roundTrip({ outer: { inner: 42 } }) as Map<any, any>
391
+ expect(result).toBeInstanceOf(Map)
392
+ const inner = result.get('outer') as Map<any, any>
393
+ expect(inner).toBeInstanceOf(Map)
394
+ expect(inner.get('inner')).toBe(42)
395
+ })
396
+
397
+ it('should round-trip object with mixed value types', () => {
398
+ const result = roundTrip({
399
+ num: 42,
400
+ str: 'hello',
401
+ bool: true,
402
+ nil: null,
403
+ arr: [1, 2, 3]
404
+ }) as Map<any, any>
405
+
406
+ expect(result.get('num')).toBe(42)
407
+ expect(result.get('str')).toBe('hello')
408
+ expect(result.get('bool')).toBe(true)
409
+ expect(result.get('nil')).toBe(null)
410
+ expect(result.get('arr')).toEqual([1, 2, 3])
411
+ })
412
+
413
+ it('should round-trip Map with integer keys', () => {
414
+ const input = new Map<any, any>([
415
+ [0, 'inputs'],
416
+ [1, 'outputs'],
417
+ [2, 1000000]
418
+ ])
419
+ const result = roundTrip(input) as Map<any, any>
420
+ expect(result).toBeInstanceOf(Map)
421
+ expect(result.get(0)).toBe('inputs')
422
+ expect(result.get(1)).toBe('outputs')
423
+ expect(result.get(2)).toBe(1000000)
424
+ })
425
+
426
+ it('should round-trip Map with string keys', () => {
427
+ const input = new Map<any, any>([
428
+ ['name', 'Alice'],
429
+ ['age', 30]
430
+ ])
431
+ const result = roundTrip(input) as Map<any, any>
432
+ expect(result.get('name')).toBe('Alice')
433
+ expect(result.get('age')).toBe(30)
434
+ })
435
+
436
+ it('should round-trip empty Map', () => {
437
+ const result = roundTrip(new Map()) as Map<any, any>
438
+ expect(result).toBeInstanceOf(Map)
439
+ expect(result.size).toBe(0)
440
+ })
441
+ })
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // 8. Tagged Values
445
+ // ---------------------------------------------------------------------------
446
+
447
+ describe('Round-trip: Tagged Values', () => {
448
+ it('should round-trip Plutus constructor 0 (tag 121)', () => {
449
+ const input = { tag: 121, value: [] }
450
+ const result = roundTrip(input)
451
+ expect(result).toMatchObject({ tag: 121, value: [] })
452
+ })
453
+
454
+ it('should round-trip epoch timestamp (tag 1)', () => {
455
+ const input = { tag: 1, value: 1234567890 }
456
+ const result = roundTrip(input)
457
+ expect(result).toMatchObject({ tag: 1, value: 1234567890 })
458
+ })
459
+
460
+ it('should round-trip tag with string content', () => {
461
+ const input = { tag: 0, value: '2013-03-21T20:04:00Z' }
462
+ const result = roundTrip(input)
463
+ expect(result).toMatchObject({ tag: 0, value: '2013-03-21T20:04:00Z' })
464
+ })
465
+
466
+ it('should round-trip tag with array content', () => {
467
+ const input = { tag: 258, value: [1, 2, 3] }
468
+ const result = roundTrip(input)
469
+ expect(result).toMatchObject({ tag: 258, value: [1, 2, 3] })
470
+ })
471
+
472
+ it('should round-trip nested tagged values', () => {
473
+ const input = { tag: 121, value: [{ tag: 122, value: [42] }] }
474
+ const result = roundTrip(input)
475
+ expect(result.tag).toBe(121)
476
+ expect(result.value[0].tag).toBe(122)
477
+ expect(result.value[0].value).toEqual([42])
478
+ })
479
+
480
+ it('should round-trip Plutus constructors 121-127', () => {
481
+ for (let tag = 121; tag <= 127; tag++) {
482
+ const input = { tag, value: [tag - 121] }
483
+ const result = roundTrip(input)
484
+ expect(result.tag).toBe(tag)
485
+ expect(result.value).toEqual([tag - 121])
486
+ }
487
+ })
488
+
489
+ it('should round-trip high tag number (tag 1280)', () => {
490
+ const input = { tag: 1280, value: [1, 2] }
491
+ const result = roundTrip(input)
492
+ expect(result).toMatchObject({ tag: 1280, value: [1, 2] })
493
+ })
494
+
495
+ it('should round-trip tag with empty Map content', () => {
496
+ const input = { tag: 121, value: new Map() }
497
+ const result = roundTrip(input)
498
+ expect(result.tag).toBe(121)
499
+ expect(result.value).toBeInstanceOf(Map)
500
+ expect((result.value as Map<any, any>).size).toBe(0)
501
+ })
502
+ })
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // 9. Complex Nested Structures (Cardano-like)
506
+ // ---------------------------------------------------------------------------
507
+
508
+ describe('Round-trip: Complex Nested Structures', () => {
509
+ it('should round-trip a Cardano-like transaction body', () => {
510
+ // Simplified Cardano transaction body structure:
511
+ // Map with integer keys: 0=inputs, 1=outputs, 2=fee
512
+ const txBody = new Map<any, any>([
513
+ [0, [ // inputs: array of [txHash, index]
514
+ [new Uint8Array(32).fill(0xab), 0]
515
+ ]],
516
+ [1, [ // outputs: array of [address, amount]
517
+ [new Uint8Array(28).fill(0xcd), 2000000]
518
+ ]],
519
+ [2, 170000] // fee
520
+ ])
521
+
522
+ const result = roundTrip(txBody) as Map<any, any>
523
+ expect(result).toBeInstanceOf(Map)
524
+
525
+ // Check fee
526
+ expect(result.get(2)).toBe(170000)
527
+
528
+ // Check inputs structure
529
+ const inputs = result.get(0) as any[]
530
+ expect(inputs).toHaveLength(1)
531
+ const [txHash, index] = inputs[0]
532
+ expect(txHash).toBeInstanceOf(Uint8Array)
533
+ expect(txHash).toEqual(new Uint8Array(32).fill(0xab))
534
+ expect(index).toBe(0)
535
+
536
+ // Check outputs structure
537
+ const outputs = result.get(1) as any[]
538
+ expect(outputs).toHaveLength(1)
539
+ const [addr, amount] = outputs[0]
540
+ expect(addr).toBeInstanceOf(Uint8Array)
541
+ expect(addr).toEqual(new Uint8Array(28).fill(0xcd))
542
+ expect(amount).toBe(2000000)
543
+ })
544
+
545
+ it('should round-trip Plutus script datum (constructor with nested fields)', () => {
546
+ // Represents a Plutus datum like: Constr 0 [I 42, B "deadbeef", List [I 1, I 2]]
547
+ const datum = {
548
+ tag: 121,
549
+ value: [
550
+ 42,
551
+ new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
552
+ [1, 2]
553
+ ]
554
+ }
555
+
556
+ const result = roundTrip(datum)
557
+ expect(result.tag).toBe(121)
558
+ expect(result.value[0]).toBe(42)
559
+ expect(result.value[1]).toBeInstanceOf(Uint8Array)
560
+ expect(result.value[1]).toEqual(new Uint8Array([0xde, 0xad, 0xbe, 0xef]))
561
+ expect(result.value[2]).toEqual([1, 2])
562
+ })
563
+
564
+ it('should round-trip deeply nested mixed structure', () => {
565
+ const input = [
566
+ new Map<any, any>([
567
+ ['key', [1, 'two', { tag: 3, value: true }]]
568
+ ]),
569
+ null,
570
+ [[], [[]]]
571
+ ]
572
+
573
+ const result = roundTrip(input) as any[]
574
+ expect(result).toHaveLength(3)
575
+
576
+ const map = result[0] as Map<any, any>
577
+ expect(map).toBeInstanceOf(Map)
578
+ const arr = map.get('key') as any[]
579
+ expect(arr[0]).toBe(1)
580
+ expect(arr[1]).toBe('two')
581
+ expect(arr[2]).toMatchObject({ tag: 3, value: true })
582
+
583
+ expect(result[1]).toBe(null)
584
+ expect(result[2]).toEqual([[], [[]]])
585
+ })
586
+
587
+ it('should round-trip array of tagged values with Maps', () => {
588
+ const input = [
589
+ { tag: 121, value: [new Map<any, any>([[1, 'a'], [2, 'b']])] },
590
+ { tag: 122, value: [new Map<any, any>([[3, 'c']])] }
591
+ ]
592
+
593
+ const result = roundTrip(input) as any[]
594
+ expect(result[0].tag).toBe(121)
595
+ expect((result[0].value[0] as Map<any, any>).get(1)).toBe('a')
596
+ expect((result[0].value[0] as Map<any, any>).get(2)).toBe('b')
597
+ expect(result[1].tag).toBe(122)
598
+ expect((result[1].value[0] as Map<any, any>).get(3)).toBe('c')
599
+ })
600
+ })
601
+
602
+ // ---------------------------------------------------------------------------
603
+ // 10. Canonical Mode
604
+ // ---------------------------------------------------------------------------
605
+
606
+ describe('Round-trip: Canonical Mode', () => {
607
+ it('should produce sorted keys in canonical mode', () => {
608
+ const encoded = encode({ z: 1, a: 2, m: 3 }, { canonical: true })
609
+ const decoded = decode(encoded.hex).value as Map<any, any>
610
+
611
+ // Keys should be in canonical order (sorted by encoded bytes)
612
+ // For text strings of equal length, this is alphabetical
613
+ const keys = Array.from(decoded.keys())
614
+ expect(keys).toEqual(['a', 'm', 'z'])
615
+ })
616
+
617
+ it('should produce consistent hex for reordered keys in canonical mode', () => {
618
+ const hex1 = encode({ z: 1, a: 2, m: 3 }, { canonical: true }).hex
619
+ const hex2 = encode({ a: 2, m: 3, z: 1 }, { canonical: true }).hex
620
+ expect(hex1).toBe(hex2)
621
+ })
622
+
623
+ it('should sort Map keys canonically', () => {
624
+ const input = new Map<any, any>([
625
+ ['bb', 2],
626
+ ['a', 1],
627
+ ['ccc', 3]
628
+ ])
629
+ const encoded = encode(input, { canonical: true })
630
+ const decoded = decode(encoded.hex).value as Map<any, any>
631
+
632
+ // Canonical sort: shorter keys first (by encoded bytes length), then lexicographic
633
+ const keys = Array.from(decoded.keys())
634
+ expect(keys[0]).toBe('a') // 1-char key encodes shorter
635
+ expect(keys[1]).toBe('bb') // 2-char key
636
+ expect(keys[2]).toBe('ccc') // 3-char key
637
+ })
638
+
639
+ it('should round-trip canonical-encoded integers', () => {
640
+ // In canonical mode, values should still round-trip correctly
641
+ expect(decode(encode(42, { canonical: true }).hex).value).toBe(42)
642
+ })
643
+
644
+ it('should round-trip canonical-encoded arrays', () => {
645
+ const input = [3, 1, 2]
646
+ // Arrays preserve order (only map keys are sorted)
647
+ expect(decode(encode(input, { canonical: true }).hex).value).toEqual([3, 1, 2])
648
+ })
649
+ })
650
+
651
+ // ---------------------------------------------------------------------------
652
+ // 11. Encoding / Decoding Consistency
653
+ // ---------------------------------------------------------------------------
654
+
655
+ describe('Round-trip: Consistency checks', () => {
656
+ it('should have bytesRead match encoded byte length', () => {
657
+ const values = [0, 100, -1, 'hello', true, null, [1, 2], { a: 1 }]
658
+ for (const value of values) {
659
+ const { hex, bytesRead } = roundTripFull(value)
660
+ // hex is 2 chars per byte
661
+ expect(bytesRead).toBe(hex.length / 2)
662
+ }
663
+ })
664
+
665
+ it('should produce identical hex when re-encoding a decoded Map', () => {
666
+ // Encode a Map, decode it, re-encode - should get same hex
667
+ const input = new Map<any, any>([
668
+ [1, 'hello'],
669
+ [2, 'world']
670
+ ])
671
+ const hex1 = encode(input).hex
672
+ const decoded = decode(hex1).value as Map<any, any>
673
+ const hex2 = encode(decoded).hex
674
+ expect(hex2).toBe(hex1)
675
+ })
676
+
677
+ it('should produce identical hex when re-encoding a decoded tagged value', () => {
678
+ const input = { tag: 121, value: [1, 2, 3] }
679
+ const hex1 = encode(input).hex
680
+ const decoded = decode(hex1).value as any
681
+ const hex2 = encode(decoded).hex
682
+ expect(hex2).toBe(hex1)
683
+ })
684
+
685
+ it('should produce identical hex when re-encoding a decoded array', () => {
686
+ const input = [1, 'hello', true, [2, 3]]
687
+ const hex1 = encode(input).hex
688
+ const decoded = decode(hex1).value as any
689
+ const hex2 = encode(decoded).hex
690
+ expect(hex2).toBe(hex1)
691
+ })
692
+
693
+ it('should produce identical hex when re-encoding decoded byte strings', () => {
694
+ const input = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
695
+ const hex1 = encode(input).hex
696
+ const decoded = decode(hex1).value as Uint8Array
697
+ // Re-encode the decoded Uint8Array directly
698
+ const hex2 = encode(decoded as any).hex
699
+ expect(hex2).toBe(hex1)
700
+ })
701
+ })