protobuf codec with static type inference
jsr.io/@mary/protobuf
typescript
jsr
1import { assert, assertAlmostEquals, assertEquals, assertStringIncludes, assertThrows } from '@std/assert';
2import { nanoid } from 'nanoid/non-secure';
3
4import * as p from './mod.ts';
5
6// #region Primitive types
7
8Deno.test('string encoding/decoding', () => {
9 const Message = p.message({ text: p.string() }, { text: 1 });
10
11 const cases = [
12 {
13 text: '',
14 expected: Uint8Array.from([0x0a, 0x00]),
15 }, // field 1, length 0
16 {
17 text: 'hello world',
18 expected: Uint8Array.from([
19 0x0a,
20 0x0b,
21 0x68,
22 0x65,
23 0x6c,
24 0x6c,
25 0x6f,
26 0x20,
27 0x77,
28 0x6f,
29 0x72,
30 0x6c,
31 0x64,
32 ]),
33 }, // field 1, length 11
34 {
35 text: 'hello 🚀',
36 expected: Uint8Array.from([0x0a, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0xf0, 0x9f, 0x9a, 0x80]),
37 }, // field 1, length 10, UTF-8 rocket
38 {
39 text: 'Café',
40 expected: Uint8Array.from([0x0a, 0x05, 0x43, 0x61, 0x66, 0xc3, 0xa9]),
41 }, // field 1, length 5, UTF-8 café
42
43 {
44 text: 'a'.repeat(1000),
45 expected: null,
46 },
47 {
48 text: 'おはようございます☀️',
49 expected: null,
50 },
51 {
52 text: 'नमस्ते',
53 expected: null,
54 },
55 {
56 text: 'Здравствуйте',
57 expected: null,
58 },
59 {
60 text: '你'.repeat(43),
61 expected: null,
62 },
63 {
64 text: '🌟'.repeat(32),
65 expected: null,
66 },
67 {
68 text: '🚀🌟💻',
69 expected: null,
70 },
71 {
72 text: '🏳️🌈🏳️⚧️',
73 expected: null,
74 },
75 ];
76
77 for (const { text, expected } of cases) {
78 const encoded = p.encode(Message, { text });
79 if (expected !== null) {
80 assertEquals(encoded, expected);
81 }
82
83 const decoded = p.decode(Message, encoded);
84 assertEquals(decoded, { text });
85 }
86});
87
88Deno.test('int32 encoding/decoding', () => {
89 const Message = p.message({ value: p.int32() }, { value: 1 });
90
91 const cases = [
92 { // field 1, varint 0
93 value: 0,
94 expected: Uint8Array.from([0x08, 0x00]),
95 },
96 { // field 1, varint 1
97 value: 1,
98 expected: Uint8Array.from([0x08, 0x01]),
99 },
100 { // field 1, varint -1 (int32)
101 value: -1,
102 expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]),
103 },
104 { // field 1, varint 127
105 value: 127,
106 expected: Uint8Array.from([0x08, 0x7f]),
107 },
108 { // field 1, varint -128
109 value: -128,
110 expected: Uint8Array.from([0x08, 0x80, 0xff, 0xff, 0xff, 0x0f]),
111 },
112 { // field 1, varint 255
113 value: 255,
114 expected: Uint8Array.from([0x08, 0xff, 0x01]),
115 },
116 { // field 1, varint -256
117 value: -256,
118 expected: Uint8Array.from([0x08, 0x80, 0xfe, 0xff, 0xff, 0x0f]),
119 },
120 { // field 1, varint 32767
121 value: 32767,
122 expected: Uint8Array.from([0x08, 0xff, 0xff, 0x01]),
123 },
124 { // field 1, varint -32768
125 value: -32768,
126 expected: Uint8Array.from([0x08, 0x80, 0x80, 0xfe, 0xff, 0x0f]),
127 },
128 { // field 1, varint 65535
129 value: 65535,
130 expected: Uint8Array.from([0x08, 0xff, 0xff, 0x03]),
131 },
132 { // field 1, varint -65536
133 value: -65536,
134 expected: Uint8Array.from([0x08, 0x80, 0x80, 0xfc, 0xff, 0x0f]),
135 },
136 { // max int32
137 value: 2147483647,
138 expected: null,
139 },
140 { // min int32
141 value: -2147483648,
142 expected: null,
143 },
144 ];
145
146 for (const { value, expected } of cases) {
147 const encoded = p.encode(Message, { value });
148 if (expected !== null) {
149 assertEquals(encoded, expected);
150 }
151
152 const decoded = p.decode(Message, encoded);
153 assertEquals(decoded, { value });
154 }
155});
156
157Deno.test('int64 encoding/decoding', () => {
158 const Message = p.message({ value: p.int64() }, { value: 1 });
159
160 const cases = [
161 { // field 1, varint 0
162 value: 0n,
163 expected: Uint8Array.from([0x08, 0x00]),
164 },
165 { // field 1, varint 1
166 value: 1n,
167 expected: Uint8Array.from([0x08, 0x01]),
168 },
169 { // field 1, varint -1 (int64)
170 value: -1n,
171 expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]),
172 },
173 { // field 1, varint 127
174 value: 127n,
175 expected: Uint8Array.from([0x08, 0x7f]),
176 },
177 { // field 1, varint -128
178 value: -128n,
179 expected: Uint8Array.from([0x08, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]),
180 },
181 { // max int64
182 value: 9223372036854775807n,
183 expected: null,
184 },
185 { // min int64
186 value: -9223372036854775808n,
187 expected: null,
188 },
189 ];
190
191 for (const { value, expected } of cases) {
192 const encoded = p.encode(Message, { value });
193 if (expected !== null) {
194 assertEquals(encoded, expected);
195 }
196
197 const decoded = p.decode(Message, encoded);
198 assertEquals(decoded, { value });
199 }
200});
201
202Deno.test('uint32 encoding/decoding', () => {
203 const Message = p.message({ value: p.uint32() }, { value: 1 });
204
205 const cases = [
206 { value: 0, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, varint 0
207 { value: 1, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, varint 1
208 { value: 127, expected: Uint8Array.from([0x08, 0x7f]) }, // field 1, varint 127
209 { value: 255, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, varint 255
210 { value: 32767, expected: Uint8Array.from([0x08, 0xff, 0xff, 0x01]) }, // field 1, varint 32767
211 { value: 65535, expected: Uint8Array.from([0x08, 0xff, 0xff, 0x03]) }, // field 1, varint 65535
212 { value: 2147483647, expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0x07]) }, // field 1, varint max int32
213 { value: 4294967295, expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]) }, // max uint32
214 ];
215
216 for (const { value, expected } of cases) {
217 const encoded = p.encode(Message, { value });
218 if (expected !== null) {
219 assertEquals(encoded, expected);
220 }
221
222 const decoded = p.decode(Message, encoded);
223 assertEquals(decoded, { value });
224 }
225});
226
227Deno.test('uint64 encoding/decoding', () => {
228 const Message = p.message({ value: p.uint64() }, { value: 1 });
229
230 const cases = [
231 { value: 0n, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, varint 0
232 { value: 1n, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, varint 1
233 { value: 127n, expected: Uint8Array.from([0x08, 0x7f]) }, // field 1, varint 127
234 { value: 255n, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, varint 255
235 { value: 18446744073709551615n, expected: null }, // max uint64
236 ];
237
238 for (const { value, expected } of cases) {
239 const encoded = p.encode(Message, { value });
240 if (expected !== null) {
241 assertEquals(encoded, expected);
242 }
243
244 const decoded = p.decode(Message, encoded);
245 assertEquals(decoded, { value });
246 }
247});
248
249Deno.test('sint32 encoding/decoding (zigzag)', () => {
250 const schema = p.message({ value: p.sint32() }, { value: 1 });
251
252 const testCases = [
253 { value: 0, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, zigzag 0 -> varint 0
254 { value: 1, expected: Uint8Array.from([0x08, 0x02]) }, // field 1, zigzag 1 -> varint 2
255 { value: -1, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, zigzag -1 -> varint 1
256 { value: 2, expected: Uint8Array.from([0x08, 0x04]) }, // field 1, zigzag 2 -> varint 4
257 { value: -2, expected: Uint8Array.from([0x08, 0x03]) }, // field 1, zigzag -2 -> varint 3
258 { value: 127, expected: Uint8Array.from([0x08, 0xfe, 0x01]) }, // field 1, zigzag 127 -> varint 254
259 { value: -128, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, zigzag -128 -> varint 255
260 { value: 2147483647, expected: null }, // max int32
261 { value: -2147483648, expected: null }, // min int32
262 ];
263
264 for (const { value, expected } of testCases) {
265 const encoded = p.encode(schema, { value });
266 if (expected !== null) {
267 assertEquals(encoded, expected);
268 }
269
270 const decoded = p.decode(schema, encoded);
271 assertEquals(decoded, { value });
272 }
273});
274
275Deno.test('sint64 encoding/decoding (zigzag)', () => {
276 const Message = p.message({ value: p.sint64() }, { value: 1 });
277
278 const cases = [
279 { value: 0n, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, zigzag 0 -> varint 0
280 { value: 1n, expected: Uint8Array.from([0x08, 0x02]) }, // field 1, zigzag 1 -> varint 2
281 { value: -1n, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, zigzag -1 -> varint 1
282 { value: 2n, expected: Uint8Array.from([0x08, 0x04]) }, // field 1, zigzag 2 -> varint 4
283 { value: -2n, expected: Uint8Array.from([0x08, 0x03]) }, // field 1, zigzag -2 -> varint 3
284 { value: 127n, expected: Uint8Array.from([0x08, 0xfe, 0x01]) }, // field 1, zigzag 127 -> varint 254
285 { value: -128n, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, zigzag -128 -> varint 255
286 { value: 9223372036854775807n, expected: null }, // max int64
287 { value: -9223372036854775808n, expected: null }, // min int64
288 ];
289
290 for (const { value, expected } of cases) {
291 const encoded = p.encode(Message, { value });
292 if (expected !== null) {
293 assertEquals(encoded, expected);
294 }
295
296 const decoded = p.decode(Message, encoded);
297 assertEquals(decoded, { value });
298 }
299});
300
301Deno.test('float encoding/decoding', () => {
302 const Message = p.message({ value: p.float() }, { value: 1 });
303
304 const cases = [
305 { value: 0.0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x00]) }, // field 1, float32 0.0 (little-endian)
306 { value: 1.0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x80, 0x3f]) }, // field 1, float32 1.0 (little-endian)
307 { value: -1.0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x80, 0xbf]) }, // field 1, float32 -1.0 (little-endian)
308 { value: 3.14159, expected: null },
309 { value: -3.14159, expected: null },
310 { value: 1.5e10, expected: null },
311 { value: -1.5e10, expected: null },
312 { value: 3.4028235e38, expected: null }, // close to max float32
313 { value: 1.175494e-38, expected: null }, // close to min positive float32
314 { value: Infinity, expected: null },
315 { value: -Infinity, expected: null },
316 { value: NaN, expected: null },
317 ];
318
319 for (const { value, expected } of cases) {
320 const encoded = p.encode(Message, { value });
321 if (expected !== null) {
322 assertEquals(encoded, expected);
323 }
324
325 const decoded = p.decode(Message, encoded);
326
327 // Special handling for infinity values
328 if (Number.isFinite(value)) {
329 // For finite values, check they're close due to float precision
330 // Use a more lenient tolerance for large numbers
331 const tolerance = Math.abs(value) > 1e9 ? Math.abs(value) * 1e-6 : 1e-6;
332
333 assertAlmostEquals(decoded.value, value, tolerance);
334 } else {
335 assertEquals(decoded.value, value);
336 }
337 }
338});
339
340Deno.test('double encoding/decoding', () => {
341 const Message = p.message({ value: p.double() }, { value: 1 });
342
343 const cases = [
344 { // field 1, double 0.0 (little-endian)
345 value: 0.0,
346 expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
347 },
348 { // field 1, double 1.0 (little-endian)
349 value: 1.0,
350 expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f]),
351 },
352 { // field 1, double -1.0 (little-endian)
353 value: -1.0,
354 expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf]),
355 },
356 {
357 value: 3.141592653589793,
358 expected: null,
359 },
360 {
361 value: -3.141592653589793,
362 expected: null,
363 },
364 { // close to max double
365 value: 1.7976931348623157e+308,
366 expected: null,
367 },
368 { // close to min positive double
369 value: 2.2250738585072014e-308,
370 expected: null,
371 },
372 {
373 value: Infinity,
374 expected: null,
375 },
376 {
377 value: -Infinity,
378 expected: null,
379 },
380 {
381 value: NaN,
382 expected: null,
383 },
384 ];
385
386 for (const { value, expected } of cases) {
387 const encoded = p.encode(Message, { value });
388 if (expected !== null) {
389 assertEquals(encoded, expected);
390 }
391
392 const decoded = p.decode(Message, encoded);
393 assertEquals(decoded, { value });
394 }
395});
396
397Deno.test('float range validation', () => {
398 const Message = p.message({ value: p.float() }, { value: 1 });
399
400 // Test values that should cause range errors
401 const invald = [
402 3.4028236e38, // slightly above max float32
403 -3.4028236e38, // slightly below min float32
404 1e39, // way above max float32
405 -1e39, // way below min float32
406 ];
407
408 for (const value of invald) {
409 assertThrows(() => p.encode(Message, { value }), Error, 'invalid_range');
410 }
411
412 // Test edge values that should work
413 const valid = [
414 3.4028235e38, // max float32
415 -3.4028235e38, // min float32
416 1.175494e-38, // min positive float32
417 -1.175494e-38, // max negative float32
418 0, // zero
419 Infinity, // positive infinity
420 -Infinity, // negative infinity
421 NaN, // not a number
422 ];
423
424 for (const value of valid) {
425 const encoded = p.encode(Message, { value });
426 void p.decode(Message, encoded);
427 }
428});
429
430Deno.test('double range validation', () => {
431 const Message = p.message({ value: p.double() }, { value: 1 });
432
433 // JavaScript's number type is already IEEE 754 double precision,
434 // so all numbers are valid for double. Test edge cases work correctly.
435 const cases = [
436 Number.MAX_VALUE, // max double
437 -Number.MAX_VALUE, // min double
438 Number.MIN_VALUE, // min positive double
439 -Number.MIN_VALUE, // max negative double
440 0, // zero
441 Infinity, // positive infinity
442 -Infinity, // negative infinity
443 NaN, // not a number
444 ];
445
446 for (const value of cases) {
447 const encoded = p.encode(Message, { value });
448 void p.decode(Message, encoded);
449 }
450});
451
452Deno.test('boolean encoding/decoding', () => {
453 const Message = p.message({ value: p.boolean() }, { value: 1 });
454
455 const cases = [
456 { value: true, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, varint 1
457 { value: false, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, varint 0
458 ];
459
460 for (const { value, expected } of cases) {
461 const encoded = p.encode(Message, { value });
462 assertEquals(encoded, expected);
463
464 const decoded = p.decode(Message, encoded);
465 assertEquals(decoded, { value });
466 }
467});
468
469Deno.test('bytes encoding/decoding', () => {
470 const Message = p.message({ data: p.bytes() }, { data: 1 });
471
472 const cases = [
473 { // field 1, length 0
474 data: new Uint8Array(0),
475 expected: Uint8Array.from([0x0a, 0x00]),
476 },
477 { // field 1, length 1, byte 0
478 data: Uint8Array.from([0]),
479 expected: Uint8Array.from([0x0a, 0x01, 0x00]),
480 },
481 { // field 1, length 5
482 data: Uint8Array.from([1, 2, 3, 4, 5]),
483 expected: Uint8Array.from([0x0a, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]),
484 },
485 { // field 1, length 3
486 data: Uint8Array.from([255, 254, 253]),
487 expected: Uint8Array.from([0x0a, 0x03, 0xff, 0xfe, 0xfd]),
488 },
489 { // large array
490 data: new Uint8Array(Array.from({ length: 1000 }, (_, i) => i % 256)),
491 expected: null,
492 },
493 ];
494
495 for (const { data, expected } of cases) {
496 const encoded = p.encode(Message, { data });
497 if (expected !== null) {
498 assertEquals(encoded, expected);
499 }
500
501 const decoded = p.decode(Message, encoded);
502 assertEquals(decoded, { data });
503 }
504});
505
506Deno.test('fixed32 encoding/decoding', () => {
507 const Message = p.message({ value: p.fixed32() }, { value: 1 });
508
509 const cases = [
510 { // field 1, fixed32 0 (little-endian)
511 value: 0,
512 expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x00]),
513 },
514 { // field 1, fixed32 1 (little-endian)
515 value: 1,
516 expected: Uint8Array.from([0x0d, 0x01, 0x00, 0x00, 0x00]),
517 },
518 { // field 1, fixed32 255 (little-endian)
519 value: 255,
520 expected: Uint8Array.from([0x0d, 0xff, 0x00, 0x00, 0x00]),
521 },
522 { // field 1, fixed32 65535 (little-endian)
523 value: 65535,
524 expected: Uint8Array.from([0x0d, 0xff, 0xff, 0x00, 0x00]),
525 },
526 { // field 1, fixed32 max uint32 (little-endian)
527 value: 4294967295,
528 expected: Uint8Array.from([0x0d, 0xff, 0xff, 0xff, 0xff]),
529 },
530 ];
531
532 for (const { value, expected } of cases) {
533 const encoded = p.encode(Message, { value });
534 assertEquals(encoded, expected);
535
536 const decoded = p.decode(Message, encoded);
537 assertEquals(decoded, { value });
538 }
539});
540
541Deno.test('fixed64 encoding/decoding', () => {
542 const Message = p.message({ value: p.fixed64() }, { value: 1 });
543
544 const cases = [
545 { // field 1, fixed64 0 (little-endian)
546 value: 0n,
547 expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
548 },
549 { // field 1, fixed64 1 (little-endian)
550 value: 1n,
551 expected: Uint8Array.from([0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
552 },
553 { // field 1, fixed64 255 (little-endian)
554 value: 255n,
555 expected: Uint8Array.from([0x09, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
556 },
557 { // field 1, fixed64 65535 (little-endian)
558 value: 65535n,
559 expected: Uint8Array.from([0x09, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
560 },
561 { // field 1, fixed64 max uint64 (little-endian)
562 value: 18446744073709551615n,
563 expected: Uint8Array.from([0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
564 },
565 ];
566
567 for (const { value, expected } of cases) {
568 const encoded = p.encode(Message, { value });
569 assertEquals(encoded, expected);
570
571 const decoded = p.decode(Message, encoded);
572 assertEquals(decoded, { value });
573 }
574});
575
576Deno.test('sfixed32 encoding/decoding', () => {
577 const Message = p.message({ value: p.sfixed32() }, { value: 1 });
578
579 const cases = [
580 { value: 0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x00]) }, // field 1, sfixed32 0 (little-endian)
581 { value: 1, expected: Uint8Array.from([0x0d, 0x01, 0x00, 0x00, 0x00]) }, // field 1, sfixed32 1 (little-endian)
582 { value: -1, expected: Uint8Array.from([0x0d, 0xff, 0xff, 0xff, 0xff]) }, // field 1, sfixed32 -1 (little-endian, two's complement)
583 { value: 2147483647, expected: Uint8Array.from([0x0d, 0xff, 0xff, 0xff, 0x7f]) }, // field 1, sfixed32 max int32 (little-endian)
584 { value: -2147483648, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x80]) }, // field 1, sfixed32 min int32 (little-endian)
585 ];
586
587 for (const { value, expected } of cases) {
588 const encoded = p.encode(Message, { value });
589 assertEquals(encoded, expected);
590
591 const decoded = p.decode(Message, encoded);
592 assertEquals(decoded, { value });
593 }
594});
595
596Deno.test('sfixed64 encoding/decoding', () => {
597 const Message = p.message({ value: p.sfixed64() }, { value: 1 });
598
599 const cases = [
600 { // field 1, sfixed64 0 (little-endian)
601 value: 0n,
602 expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
603 },
604 { // field 1, sfixed64 1 (little-endian)
605 value: 1n,
606 expected: Uint8Array.from([0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
607 },
608 { // field 1, sfixed64 -1 (little-endian, two's complement)
609 value: -1n,
610 expected: Uint8Array.from([0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
611 },
612 { // field 1, sfixed64 max int64 (little-endian)
613 value: 9223372036854775807n,
614 expected: Uint8Array.from([0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f]),
615 },
616 { // field 1, sfixed64 min int64 (little-endian)
617 value: -9223372036854775808n,
618 expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]),
619 },
620 ];
621
622 for (const { value, expected } of cases) {
623 const encoded = p.encode(Message, { value });
624 assertEquals(encoded, expected);
625
626 const decoded = p.decode(Message, encoded);
627 assertEquals(decoded, { value });
628 }
629});
630
631// #endregion
632
633// #region Complex types
634
635Deno.test('repeated fields', () => {
636 const cases = [
637 {
638 data: { numbers: [], strings: [] },
639 unpacked: new Uint8Array(0), // empty message
640 packed: Uint8Array.from([0x08, 0x00, 0x12, 0x00]), // field 1: tag + length 0, field 2: tag + length 0
641 },
642 {
643 data: { numbers: [1], strings: ['hello'] },
644 unpacked: Uint8Array.from([0x08, 0x01, 0x12, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // field 1: varint 1, field 2: length 5 + "hello"
645 packed: Uint8Array.from([0x08, 0x01, 0x01, 0x12, 0x06, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // field 1: tag + length 1 + varint 1, field 2: tag + length 6 + (length 5 + "hello")
646 },
647 {
648 data: { numbers: [1, 2, 3], strings: ['hi'] },
649 unpacked: Uint8Array.from([0x08, 0x01, 0x08, 0x02, 0x08, 0x03, 0x12, 0x02, 0x68, 0x69]), // field 1: 1,2,3, field 2: "hi"
650 packed: Uint8Array.from([0x08, 0x03, 0x01, 0x02, 0x03, 0x12, 0x03, 0x02, 0x68, 0x69]), // field 1: tag + length 3 + varints 1,2,3, field 2: tag + length 3 + (length 2 + "hi")
651 },
652 {
653 data: {
654 numbers: Array.from({ length: 100 }, (_, i) => i),
655 strings: Array.from({ length: 100 }, (_, i) => `item${i}`),
656 },
657 unpacked: null, // too large for expected bytes
658 packed: null, // too large for expected bytes
659 },
660 ];
661
662 // Test non-packed repeated fields
663 {
664 const Message = p.message({
665 numbers: p.repeated(p.int32(), false),
666 strings: p.repeated(p.string(), false),
667 }, {
668 numbers: 1,
669 strings: 2,
670 });
671
672 for (const { data, unpacked } of cases) {
673 const encoded = p.encode(Message, data);
674 if (unpacked !== null) {
675 assertEquals(encoded, unpacked);
676 }
677
678 const decoded = p.decode(Message, encoded);
679 assertEquals(decoded, data);
680 }
681 }
682
683 // Test packed repeated fields (different wire format)
684 {
685 const Message = p.message({
686 numbers: p.repeated(p.int32(), true),
687 strings: p.repeated(p.string(), true),
688 }, {
689 numbers: 1,
690 strings: 2,
691 });
692
693 for (const { data, packed } of cases) {
694 const encoded = p.encode(Message, data);
695 if (packed !== null) {
696 assertEquals(encoded, packed);
697 }
698
699 const decoded = p.decode(Message, encoded);
700 assertEquals(decoded, data);
701 }
702 }
703});
704
705Deno.test('messages with optional fields', () => {
706 const Message = p.message({
707 required: p.string(),
708 withDefault: p.optional(p.string(), 'default_value'),
709 withFunctionDefault: p.optional(p.int32(), () => 42),
710 withoutDefault: p.optional(p.string()),
711 }, {
712 required: 1,
713 withDefault: 2,
714 withFunctionDefault: 3,
715 withoutDefault: 4,
716 });
717
718 const cases = [
719 {
720 data: {
721 required: 'hello',
722 withDefault: 'custom',
723 withFunctionDefault: 99,
724 withoutDefault: 'present',
725 },
726 expected: Uint8Array.from([
727 0x0a,
728 0x05,
729 0x68,
730 0x65,
731 0x6c,
732 0x6c,
733 0x6f, // field 1: "hello"
734 0x12,
735 0x06,
736 0x63,
737 0x75,
738 0x73,
739 0x74,
740 0x6f,
741 0x6d, // field 2: "custom"
742 0x18,
743 0x63, // field 3: varint 99
744 0x22,
745 0x07,
746 0x70,
747 0x72,
748 0x65,
749 0x73,
750 0x65,
751 0x6e,
752 0x74, // field 4: "present"
753 ]),
754 },
755 {
756 data: { required: 'hello' },
757 expected: Uint8Array.from([0x0a, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // field 1: "hello" only
758 },
759 {
760 data: {
761 required: 'hello',
762 withDefault: 'custom_value',
763 },
764 expected: Uint8Array.from([
765 0x0a,
766 0x05,
767 0x68,
768 0x65,
769 0x6c,
770 0x6c,
771 0x6f, // field 1: "hello"
772 0x12,
773 0x0c,
774 0x63,
775 0x75,
776 0x73,
777 0x74,
778 0x6f,
779 0x6d,
780 0x5f,
781 0x76,
782 0x61,
783 0x6c,
784 0x75,
785 0x65, // field 2: "custom_value"
786 ]),
787 },
788 ];
789
790 for (const { data, expected } of cases) {
791 const encoded = p.encode(Message, data);
792 assertEquals(encoded, expected);
793
794 const decoded = p.decode(Message, encoded);
795
796 // Handle default values in decoded output
797 if (!data.withDefault && !data.withFunctionDefault && !data.withoutDefault) {
798 assertEquals(decoded, {
799 required: data.required,
800 withDefault: 'default_value',
801 withFunctionDefault: 42,
802 // withoutDefault should be undefined (not present)
803 });
804 } else if (!data.withFunctionDefault && !data.withoutDefault) {
805 assertEquals(decoded, {
806 required: data.required,
807 withDefault: data.withDefault,
808 withFunctionDefault: 42,
809 // withoutDefault should be undefined (not present)
810 });
811 } else {
812 assertEquals(decoded, data);
813 }
814 }
815});
816
817Deno.test('empty messages', () => {
818 const Message = p.message({}, {});
819
820 const encoded = p.encode(Message, {});
821 const decoded = p.decode(Message, encoded);
822
823 // Empty message should encode to empty buffer
824 assertEquals(encoded, new Uint8Array(0));
825 assertEquals(decoded, {});
826});
827
828Deno.test('nested messages', () => {
829 {
830 const Simple = p.message({
831 inner: p.optional(p.message({ value: p.int32() }, { value: 1 })),
832 }, { inner: 2 });
833
834 const cases = [
835 {
836 data: { inner: { value: 42 } },
837 expected: Uint8Array.from([0x12, 0x02, 0x08, 0x2a]), // field 2: length 2, field 1: varint 42
838 },
839 {
840 data: {},
841 expected: new Uint8Array(0), // empty message
842 },
843 ];
844
845 for (const { data, expected } of cases) {
846 const encoded = p.encode(Simple, data);
847 assertEquals(encoded, expected);
848
849 const decoded = p.decode(Simple, encoded);
850 assertEquals(decoded, data);
851 }
852 }
853
854 {
855 const Address = p.message({
856 street: p.string(),
857 city: p.string(),
858 zipCode: p.optional(p.string()),
859 }, {
860 street: 1,
861 city: 2,
862 zipCode: 3,
863 });
864
865 const Person = p.message({
866 name: p.string(),
867 age: p.int32(),
868 address: Address,
869 addresses: p.repeated(Address),
870 }, {
871 name: 1,
872 age: 2,
873 address: 3,
874 addresses: 4,
875 });
876
877 const data = {
878 name: 'John Doe',
879 age: 30,
880 address: {
881 street: '123 Main St',
882 city: 'Anytown',
883 zipCode: '12345',
884 },
885 addresses: [
886 {
887 street: '456 Oak Ave',
888 city: 'Other City',
889 },
890 {
891 street: '789 Pine Rd',
892 city: 'Another City',
893 zipCode: '67890',
894 },
895 ],
896 };
897
898 const encoded = p.encode(Person, data);
899 const decoded = p.decode(Person, encoded);
900
901 assertEquals(decoded, data);
902 }
903});
904
905Deno.test('self-referential messages', () => {
906 const Node = p.message({
907 value: p.int32(),
908 get next() {
909 return p.optional(Node);
910 },
911 }, {
912 value: 1,
913 next: 2,
914 });
915
916 {
917 const encoded = p.encode(Node, { value: 42 });
918 const decoded = p.decode(Node, encoded);
919
920 assertEquals(decoded, { value: 42 });
921 }
922
923 {
924 const encoded = p.encode(Node, { value: 1, next: { value: 2 } });
925 const decoded = p.decode(Node, encoded);
926
927 assertEquals(decoded, { value: 1, next: { value: 2 } });
928 }
929
930 {
931 const complex: p.InferInput<typeof Node> = {
932 value: 1,
933 next: {
934 value: 2,
935 next: {
936 value: 3,
937 next: {
938 value: 4,
939 next: {
940 value: 5,
941 },
942 },
943 },
944 },
945 };
946
947 const encoded = p.encode(Node, complex);
948 const decoded = p.decode(Node, encoded);
949
950 assertEquals(decoded, complex);
951 }
952});
953
954Deno.test('map type', () => {
955 const Person = p.message({
956 id: p.int32(),
957 name: p.string(),
958 }, {
959 id: 1,
960 name: 2,
961 });
962
963 const PersonMap = p.map(p.string(), Person);
964 type PersonMap = p.InferInput<typeof PersonMap>;
965
966 const Message = p.message({
967 map: PersonMap,
968 }, {
969 map: 1,
970 });
971
972 {
973 const map: PersonMap = [];
974
975 const encoded = p.encode(Message, { map });
976 const decoded = p.decode(Message, encoded);
977
978 assertEquals(decoded, { map });
979 }
980
981 {
982 const map: PersonMap = [{ key: 'item1', value: { id: 1, name: 'first' } }];
983
984 const encoded = p.encode(Message, { map });
985 const decoded = p.decode(Message, encoded);
986
987 assertEquals(decoded, { map });
988 }
989
990 {
991 const map: PersonMap = [
992 { key: 'item1', value: { id: 1, name: 'first' } },
993 { key: 'item2', value: { id: 2, name: 'second' } },
994 ];
995
996 const encoded = p.encode(Message, { map });
997 const decoded = p.decode(Message, encoded);
998
999 assertEquals(decoded, { map });
1000 }
1001});
1002
1003Deno.test('Timestamp type', () => {
1004 {
1005 const encoded = p.encode(p.Timestamp, {});
1006 const decoded = p.decode(p.Timestamp, encoded);
1007
1008 assertEquals(decoded, { seconds: 0n, nanos: 0 });
1009 }
1010
1011 {
1012 const data: p.InferInput<typeof p.Timestamp> = {
1013 seconds: 1609459200n, // 2021-01-01 00:00:00 UTC
1014 nanos: 123456789,
1015 };
1016
1017 const encoded = p.encode(p.Timestamp, data);
1018 const decoded = p.decode(p.Timestamp, encoded);
1019
1020 assertEquals(decoded, data);
1021 }
1022});
1023
1024Deno.test('Duration type', () => {
1025 {
1026 const data: p.InferInput<typeof p.Duration> = {
1027 seconds: 3661n, // 1 hour, 1 minute, 1 second
1028 nanos: 500000000, // 0.5 seconds
1029 };
1030
1031 const encoded = p.encode(p.Duration, data);
1032 const decoded = p.decode(p.Duration, encoded);
1033
1034 assertEquals(decoded, data);
1035 }
1036
1037 {
1038 const negative: p.InferInput<typeof p.Duration> = {
1039 seconds: -30n,
1040 nanos: -500000000,
1041 };
1042
1043 const encoded = p.encode(p.Duration, negative);
1044 const decoded = p.decode(p.Duration, encoded);
1045
1046 assertEquals(decoded, negative);
1047 }
1048});
1049
1050Deno.test('Any type', () => {
1051 {
1052 const messageSchema = p.message({ value: p.string() }, { value: 1 });
1053
1054 const data = {
1055 typeUrl: 'type.googleapis.com/test.Message',
1056 value: p.encode(messageSchema, { value: 'hello world' }),
1057 };
1058
1059 const encoded = p.encode(p.Any, data);
1060 const decoded = p.decode(p.Any, encoded);
1061
1062 assertEquals(decoded, data);
1063 }
1064
1065 {
1066 const encoded = p.encode(p.Any, {});
1067 const decoded = p.decode(p.Any, encoded);
1068
1069 assertEquals(decoded, { typeUrl: '', value: new Uint8Array(0) });
1070 }
1071});
1072
1073// #endregion
1074
1075// #region Edge cases
1076
1077Deno.test('large varint values', () => {
1078 const Message = p.message({ value: p.int32() }, { value: 1 });
1079
1080 // Test values that require multiple bytes in varint encoding
1081 const cases = [
1082 127, // 1 byte
1083 128, // 2 bytes
1084 16383, // 2 bytes
1085 16384, // 3 bytes
1086 2097151, // 3 bytes
1087 2097152, // 4 bytes
1088 ];
1089
1090 for (const value of cases) {
1091 const encoded = p.encode(Message, { value });
1092 const decoded = p.decode(Message, encoded);
1093
1094 assertEquals(decoded, { value });
1095 }
1096});
1097
1098Deno.test('very large varint handling', () => {
1099 const schema = p.message({ value: p.int32() }, { value: 1 });
1100
1101 // Create a very large varint that would overflow 32-bit int
1102 // This represents 0xFFFFFFFF (4294967295) which should become -1 when cast to int32
1103 const largeVarintData = Uint8Array.from([
1104 8,
1105 0xFF,
1106 0xFF,
1107 0xFF,
1108 0xFF,
1109 0x0F, // field 1: max uint32 value
1110 ]);
1111
1112 const decoded = p.decode(schema, largeVarintData);
1113 assertEquals(decoded, { value: -1 }); // Should wrap around to -1
1114});
1115
1116Deno.test('unknown field handling', () => {
1117 const Message = p.message({ known: p.string() }, { known: 1 });
1118
1119 const buffer = Uint8Array.from([
1120 10,
1121 5,
1122 72,
1123 101,
1124 108,
1125 108,
1126 111, // field 1: "Hello"
1127 18,
1128 4,
1129 116,
1130 101,
1131 115,
1132 116, // field 2: "test" (unknown)
1133 26,
1134 3,
1135 98,
1136 121,
1137 101, // field 3: "bye" (unknown)
1138 ]);
1139
1140 const decoded = p.decode(Message, buffer);
1141
1142 assertEquals(decoded, { known: 'Hello' });
1143});
1144
1145Deno.test('duplicate field handling', () => {
1146 const Message = p.message({ value: p.int32() }, { value: 1 });
1147
1148 const buffer = Uint8Array.from([
1149 8,
1150 42, // field 1: value 42
1151 8,
1152 24, // field 1: value 24 (duplicate)
1153 ]);
1154
1155 const decoded = p.decode(Message, buffer);
1156
1157 assertEquals(decoded, { value: 24 });
1158});
1159
1160// #endregion
1161
1162// #region Input validation errors
1163
1164Deno.test('type validation errors during encoding', () => {
1165 const StringMessage = p.message({ text: p.string() }, { text: 1 });
1166 {
1167 // @ts-expect-error: purposeful type error
1168 const result = p.tryEncode(StringMessage, 123);
1169
1170 assert(!result.ok);
1171 assertEquals(result.message, `invalid_type at . (expected object)`);
1172 }
1173
1174 {
1175 // @ts-expect-error: purposeful type error
1176 const result = p.tryEncode(StringMessage, { text: 123 });
1177
1178 assert(!result.ok);
1179 assertEquals(result.message, `invalid_type at .text (expected string)`);
1180 }
1181
1182 const Int64Message = p.message({ value: p.int64() }, { value: 1 });
1183 {
1184 // @ts-expect-error: purposeful type error
1185 const result = p.tryEncode(Int64Message, { value: 123 });
1186
1187 assert(!result.ok);
1188 assertEquals(result.message, `invalid_type at .value (expected bigint)`);
1189 }
1190
1191 const Uint64Message = p.message({ value: p.uint64() }, { value: 1 });
1192 {
1193 // @ts-expect-error: purposeful type error
1194 const result = p.tryEncode(Uint64Message, { value: 123 });
1195
1196 assert(!result.ok);
1197 assertEquals(result.message, `invalid_type at .value (expected bigint)`);
1198 }
1199});
1200
1201Deno.test('range validation for unsigned types', () => {
1202 const Uint32Message = p.message({ value: p.uint32() }, { value: 1 });
1203 const Uint64Message = p.message({ value: p.uint64() }, { value: 1 });
1204 const Fixed32Message = p.message({ value: p.fixed32() }, { value: 1 });
1205 const Fixed64Message = p.message({ value: p.fixed64() }, { value: 1 });
1206
1207 // uint32 range errors
1208 assertThrows(() => p.encode(Uint32Message, { value: -1 }), p.ProtobufError);
1209 assertThrows(() => p.encode(Uint32Message, { value: 4294967296 }), p.ProtobufError);
1210
1211 // uint64 range errors
1212 assertThrows(() => p.encode(Uint64Message, { value: -1n }), p.ProtobufError);
1213
1214 // fixed32 range errors
1215 assertThrows(() => p.encode(Fixed32Message, { value: -1 }), p.ProtobufError);
1216 assertThrows(() => p.encode(Fixed32Message, { value: 4294967296 }), p.ProtobufError);
1217
1218 // fixed64 range errors
1219 assertThrows(() => p.encode(Fixed64Message, { value: -1n }), p.ProtobufError);
1220});
1221
1222// #endregion
1223
1224// #region Decoding validation errors
1225
1226Deno.test('invalid wire type handling', () => {
1227 const Message = p.message({ text: p.string() }, { text: 1 });
1228
1229 // Manually create invalid wire data: field 1 with wire type 0 (varint) instead of 2 (length-delimited)
1230 const invalidData = Uint8Array.from([
1231 8,
1232 72, // field 1, wire type 0, value 72
1233 ]);
1234
1235 assertThrows(() => p.decode(Message, invalidData), p.ProtobufError);
1236
1237 const result = p.tryDecode(Message, invalidData);
1238 assert(!result.ok);
1239
1240 assertEquals(result.issues.length, 1);
1241 const issue = result.issues[0];
1242
1243 assertEquals(issue.code, 'invalid_wire');
1244 assertEquals(issue.path, ['text']);
1245
1246 if (issue.code === 'invalid_wire') {
1247 assertEquals(issue.expected, 2); // string expects wire type 2
1248 }
1249
1250 assertStringIncludes(result.message, 'invalid_wire at');
1251 assertStringIncludes(result.message, 'expected wire type');
1252 assertStringIncludes(result.message, 'text');
1253});
1254
1255Deno.test('multiple validation errors', () => {
1256 const Message = p.message({
1257 field1: p.string(),
1258 field2: p.int32(),
1259 field3: p.bytes(),
1260 }, {
1261 field1: 1,
1262 field2: 2,
1263 field3: 3,
1264 });
1265
1266 // Create data with wrong wire types for multiple fields
1267 const invalidData = Uint8Array.from([
1268 8,
1269 72, // field 1, wire type 0 instead of 2
1270 18,
1271 1,
1272 65, // field 2, wire type 2 instead of 0
1273 24,
1274 100, // field 3, wire type 0 instead of 2
1275 ]);
1276
1277 const result = p.tryDecode(Message, invalidData);
1278 assert(!result.ok);
1279
1280 assertEquals(result.message, 'invalid_wire at .field1 (expected wire type 2)');
1281});
1282
1283Deno.test('missing required fields during decoding', () => {
1284 const Message = p.message({
1285 required1: p.string(),
1286 optional: p.optional(p.string()),
1287 }, {
1288 required1: 1,
1289 required2: 2,
1290 optional: 3,
1291 });
1292
1293 // Create a message missing required fields
1294 const partialSchema = p.message({
1295 optional: p.optional(p.string()),
1296 }, {
1297 optional: 3,
1298 });
1299
1300 const encoded = p.encode(partialSchema, { optional: 'hello' });
1301
1302 const result = p.tryDecode(Message, encoded);
1303 assert(!result.ok);
1304 assertEquals(result.message, 'missing_value at .required1 (required field is missing)');
1305});
1306
1307Deno.test('empty buffer handling', () => {
1308 const Message = p.message({
1309 required: p.string(),
1310 optional: p.optional(p.string()),
1311 }, {
1312 required: 1,
1313 optional: 2,
1314 });
1315
1316 const emptyBuffer = new Uint8Array(0);
1317
1318 const result = p.tryDecode(Message, emptyBuffer);
1319 assert(!result.ok);
1320
1321 assertEquals(result.issues.length, 1);
1322 assertEquals(result.issues[0].code, 'missing_value');
1323 assertEquals(result.issues[0].path, ['required']);
1324});
1325
1326// #endregion
1327
1328// #region Buffer corruption and underrun
1329
1330Deno.test('corrupted data handling', () => {
1331 const Message = p.message({ text: p.string() }, { text: 1 });
1332
1333 // Test with truncated data (incomplete varint)
1334 const truncatedData = Uint8Array.from([10, 5, 72, 101]); // says length 5 but only has 2 bytes
1335
1336 assertThrows(() => p.decode(Message, truncatedData));
1337});
1338
1339Deno.test('buffer underrun during decoding', () => {
1340 const Message = p.message({ text: p.string() }, { text: 1 });
1341
1342 // Create a truncated buffer that claims to have more data than it actually has
1343 // Wire format: tag (8=field 1, wire type 2) + length (5) + partial data (only 2 bytes instead of 5)
1344 const truncatedBuffer = Uint8Array.from([
1345 8 | 2, // tag: field 1, wire type 2 (length-delimited)
1346 5, // length: claims 5 bytes follow
1347 65,
1348 66, // only 2 bytes: "AB"
1349 // missing 3 bytes!
1350 ]);
1351
1352 const result = p.tryDecode(Message, truncatedBuffer);
1353
1354 assert(!result.ok);
1355 assertEquals(result.message, `unexpected_eof at .text (unexpected end of input)`);
1356});
1357
1358Deno.test('buffer underrun during varint reading', () => {
1359 const Message = p.message({ value: p.int32() }, { value: 1 });
1360
1361 // Create a buffer with incomplete varint (continuation bit set but no more bytes)
1362 const incompleteVarint = Uint8Array.from([
1363 8, // tag: field 1, wire type 0 (varint)
1364 0x80, // varint with continuation bit set, but no more bytes
1365 ]);
1366
1367 const result = p.tryDecode(Message, incompleteVarint);
1368
1369 assert(!result.ok);
1370 assertEquals(result.message, `unexpected_eof at .value (unexpected end of input)`);
1371});
1372
1373// #endregion
1374
1375// #region Performance tests
1376
1377Deno.test('round-trip consistency stress test', () => {
1378 const Message = p.message({
1379 id: p.int64(),
1380 name: p.string(),
1381 email: p.optional(p.string()),
1382 tags: p.repeated(p.string()),
1383 metadata: p.bytes(),
1384 score: p.double(),
1385 active: p.boolean(),
1386 }, {
1387 id: 1,
1388 name: 2,
1389 email: 3,
1390 tags: 4,
1391 metadata: 5,
1392 score: 6,
1393 active: 7,
1394 });
1395
1396 // Generate random test data
1397 for (let i = 0; i < 100; i++) {
1398 const data = {
1399 id: BigInt(Math.floor(Math.random() * 1000000)),
1400 name: `user_${i}`,
1401 email: Math.random() > 0.5 ? `user_${i}@example.com` : undefined,
1402 tags: Array.from({ length: Math.floor(Math.random() * 5) }, (_, j) => `tag_${j}`),
1403 metadata: new Uint8Array(
1404 Array.from({ length: Math.floor(Math.random() * 20) }, () => Math.floor(Math.random() * 255)),
1405 ),
1406 score: Math.random() * 100,
1407 active: Math.random() > 0.5,
1408 };
1409
1410 const encoded = p.encode(Message, data);
1411 const decoded = p.decode(Message, encoded);
1412
1413 // `assertEquals` throws if properties aren't present entirely.
1414 decoded.email ??= undefined;
1415
1416 assertEquals(decoded, data, `Failed for iteration ${i}`);
1417 }
1418});
1419
1420Deno.test('very large arrays', () => {
1421 const strings = Array.from({ length: 10000 }, () => nanoid(8));
1422
1423 {
1424 const Message = p.message({ strings: p.repeated(p.string(), false) }, { strings: 1 });
1425
1426 const encoded = p.encode(Message, { strings });
1427 const decoded = p.decode(Message, encoded);
1428
1429 assertEquals(decoded, { strings });
1430 }
1431
1432 {
1433 const Message = p.message({ strings: p.repeated(p.string(), true) }, { strings: 1 });
1434
1435 const encoded = p.encode(Message, { strings });
1436 const decoded = p.decode(Message, encoded);
1437
1438 assertEquals(decoded, { strings });
1439 }
1440});
1441
1442Deno.test('large string handling', () => {
1443 const Message = p.message({ text: p.string() }, { text: 1 });
1444
1445 const text = nanoid(1048576);
1446
1447 const encoded = p.encode(Message, { text });
1448 const decoded = p.decode(Message, encoded);
1449
1450 assertEquals(decoded, { text });
1451});
1452
1453Deno.test('large byte array handling', () => {
1454 const Message = p.message({ data: p.bytes() }, { data: 1 });
1455
1456 const data = new Uint8Array(1024 * 1024);
1457 for (let i = 0; i < data.length; i++) {
1458 data[i] = Math.floor(Math.random() * 255);
1459 }
1460
1461 const encoded = p.encode(Message, { data });
1462 const decoded = p.decode(Message, encoded);
1463
1464 assertEquals(decoded, { data });
1465});
1466
1467// #endregion