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