@marcuspuchalla/nachos 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
- package/dist/chunk-5IWW5H47.js.map +1 -0
- package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
- package/dist/chunk-RVG2BY32.cjs.map +1 -0
- package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
- package/dist/chunk-S4RXO6IB.cjs.map +1 -0
- package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
- package/dist/chunk-UMAX5MX5.js.map +1 -0
- package/dist/encoder/index.cjs +13 -13
- package/dist/encoder/index.d.cts +2 -2
- package/dist/encoder/index.d.ts +2 -2
- package/dist/encoder/index.js +1 -1
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -19
- package/dist/index.d.ts +28 -19
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/parser/index.cjs +14 -14
- package/dist/parser/index.d.cts +3 -1
- package/dist/parser/index.d.ts +3 -1
- package/dist/parser/index.js +1 -1
- package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
- package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
- package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
- package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
- package/package.json +1 -1
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +701 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
- package/src/encoder/composables/useCborEncoder.ts +27 -1
- package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
- package/src/encoder/composables/useCborStringEncoder.ts +23 -10
- package/src/encoder/types.ts +0 -2
- package/src/index.ts +29 -20
- package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
- package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
- package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
- package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
- package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
- package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
- package/src/parser/composables/useCborCollection.ts +45 -42
- package/src/parser/composables/useCborFloat.ts +95 -9
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +387 -216
- package/src/parser/composables/useCborString.ts +22 -4
- package/src/parser/composables/useCborTag.ts +149 -53
- package/src/parser/utils.ts +11 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
- package/dist/chunk-2HBCILJS.cjs.map +0 -1
- package/dist/chunk-7CFYWHS6.js.map +0 -1
- package/dist/chunk-ZRPJUEIZ.js.map +0 -1
- package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
|
@@ -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
|
+
})
|