+2
-13
README.md
+2
-13
README.md
···
1
1
# protobuf
2
2
3
+
[JSR](https://jsr.io/@mary/protobuf) | [source code](https://tangled.sh/mary.my.id/pkg-protobuf)
4
+
3
5
protobuf codec with static type inference.
4
6
5
7
```typescript
···
69
71
value: 1,
70
72
next: 2,
71
73
});
72
-
}
73
-
74
-
// maps
75
-
{
76
-
const Scoreboard = p.message({
77
-
scores: p.map(p.string(), p.int32()),
78
-
}, {
79
-
scores: 1,
80
-
});
81
-
82
-
const data: p.InferInput<typeof Scoreboard> = {
83
-
scores: new Map([['alice', 100], ['bob', 95]]),
84
-
};
85
74
}
86
75
```
87
76
+1
-1
deno.json
+1
-1
deno.json
+569
-272
lib/mod.test.ts
+569
-272
lib/mod.test.ts
···
1
-
import {
2
-
assert,
3
-
assertAlmostEquals,
4
-
assertArrayIncludes,
5
-
assertEquals,
6
-
assertStringIncludes,
7
-
assertThrows,
8
-
} from '@std/assert';
1
+
import { assert, assertAlmostEquals, assertEquals, assertStringIncludes, assertThrows } from '@std/assert';
9
2
import { nanoid } from 'nanoid/non-secure';
10
3
11
4
import * as p from './mod.ts';
···
16
9
const Message = p.message({ text: p.string() }, { text: 1 });
17
10
18
11
const cases = [
19
-
'',
20
-
'hello world',
21
-
'hello ๐',
22
-
'a'.repeat(1000),
23
-
'Cafรฉ',
24
-
'ใใฏใใใใใใพใโ๏ธ',
25
-
'เคจเคฎเคธเฅเคคเฅ',
26
-
'ะะดัะฐะฒััะฒัะนัะต',
27
-
'ไฝ '.repeat(43),
28
-
'๐'.repeat(32),
29
-
'๐๐๐ป',
30
-
'๐ณ๏ธโ๐๐ณ๏ธโโง๏ธ',
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
+
},
31
75
];
32
76
33
-
for (const text of cases) {
77
+
for (const { text, expected } of cases) {
34
78
const encoded = p.encode(Message, { text });
35
-
const decoded = p.decode(Message, encoded);
79
+
if (expected !== null) {
80
+
assertEquals(encoded, expected);
81
+
}
36
82
83
+
const decoded = p.decode(Message, encoded);
37
84
assertEquals(decoded, { text });
38
85
}
39
86
});
···
42
89
const Message = p.message({ value: p.int32() }, { value: 1 });
43
90
44
91
const cases = [
45
-
0,
46
-
1,
47
-
-1,
48
-
127,
49
-
-128,
50
-
255,
51
-
-256,
52
-
32767,
53
-
-32768,
54
-
65535,
55
-
-65536,
56
-
2147483647, // max int32
57
-
-2147483648, // min int32
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
+
},
58
144
];
59
145
60
-
for (const value of cases) {
146
+
for (const { value, expected } of cases) {
61
147
const encoded = p.encode(Message, { value });
62
-
const decoded = p.decode(Message, encoded);
148
+
if (expected !== null) {
149
+
assertEquals(encoded, expected);
150
+
}
63
151
152
+
const decoded = p.decode(Message, encoded);
64
153
assertEquals(decoded, { value });
65
154
}
66
155
});
···
69
158
const Message = p.message({ value: p.int64() }, { value: 1 });
70
159
71
160
const cases = [
72
-
0n,
73
-
1n,
74
-
-1n,
75
-
127n,
76
-
-128n,
77
-
9223372036854775807n, // max int64
78
-
-9223372036854775808n, // min int64
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
+
},
79
189
];
80
190
81
-
for (const value of cases) {
191
+
for (const { value, expected } of cases) {
82
192
const encoded = p.encode(Message, { value });
193
+
if (expected !== null) {
194
+
assertEquals(encoded, expected);
195
+
}
196
+
83
197
const decoded = p.decode(Message, encoded);
84
-
85
198
assertEquals(decoded, { value });
86
199
}
87
200
});
···
90
203
const Message = p.message({ value: p.uint32() }, { value: 1 });
91
204
92
205
const cases = [
93
-
0,
94
-
1,
95
-
127,
96
-
255,
97
-
32767,
98
-
65535,
99
-
2147483647,
100
-
4294967295, // max uint32
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
101
214
];
102
215
103
-
for (const value of cases) {
216
+
for (const { value, expected } of cases) {
104
217
const encoded = p.encode(Message, { value });
218
+
if (expected !== null) {
219
+
assertEquals(encoded, expected);
220
+
}
221
+
105
222
const decoded = p.decode(Message, encoded);
106
-
107
223
assertEquals(decoded, { value });
108
224
}
109
225
});
···
112
228
const Message = p.message({ value: p.uint64() }, { value: 1 });
113
229
114
230
const cases = [
115
-
0n,
116
-
1n,
117
-
127n,
118
-
255n,
119
-
18446744073709551615n, // max uint64
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
120
236
];
121
237
122
-
for (const value of cases) {
238
+
for (const { value, expected } of cases) {
123
239
const encoded = p.encode(Message, { value });
240
+
if (expected !== null) {
241
+
assertEquals(encoded, expected);
242
+
}
243
+
124
244
const decoded = p.decode(Message, encoded);
125
-
126
245
assertEquals(decoded, { value });
127
246
}
128
247
});
···
131
250
const schema = p.message({ value: p.sint32() }, { value: 1 });
132
251
133
252
const testCases = [
134
-
0,
135
-
1,
136
-
-1,
137
-
2,
138
-
-2,
139
-
127,
140
-
-128,
141
-
2147483647, // max int32
142
-
-2147483648, // min int32
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
143
262
];
144
263
145
-
for (const value of testCases) {
264
+
for (const { value, expected } of testCases) {
146
265
const encoded = p.encode(schema, { value });
147
-
const decoded = p.decode(schema, encoded);
266
+
if (expected !== null) {
267
+
assertEquals(encoded, expected);
268
+
}
148
269
270
+
const decoded = p.decode(schema, encoded);
149
271
assertEquals(decoded, { value });
150
272
}
151
273
});
···
154
276
const Message = p.message({ value: p.sint64() }, { value: 1 });
155
277
156
278
const cases = [
157
-
0n,
158
-
1n,
159
-
-1n,
160
-
2n,
161
-
-2n,
162
-
127n,
163
-
-128n,
164
-
9223372036854775807n, // max int64
165
-
-9223372036854775808n, // min int64
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
166
288
];
167
289
168
-
for (const value of cases) {
290
+
for (const { value, expected } of cases) {
169
291
const encoded = p.encode(Message, { value });
170
-
const decoded = p.decode(Message, encoded);
292
+
if (expected !== null) {
293
+
assertEquals(encoded, expected);
294
+
}
171
295
296
+
const decoded = p.decode(Message, encoded);
172
297
assertEquals(decoded, { value });
173
298
}
174
299
});
···
177
302
const Message = p.message({ value: p.float() }, { value: 1 });
178
303
179
304
const cases = [
180
-
0.0,
181
-
1.0,
182
-
-1.0,
183
-
3.14159,
184
-
-3.14159,
185
-
1.5e10,
186
-
-1.5e10,
187
-
3.4028235e38, // close to max float32
188
-
1.175494e-38, // close to min positive float32
189
-
Infinity,
190
-
-Infinity,
191
-
NaN,
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 },
192
317
];
193
318
194
-
for (const value of cases) {
319
+
for (const { value, expected } of cases) {
195
320
const encoded = p.encode(Message, { value });
321
+
if (expected !== null) {
322
+
assertEquals(encoded, expected);
323
+
}
324
+
196
325
const decoded = p.decode(Message, encoded);
197
326
198
327
// Special handling for infinity values
···
212
341
const Message = p.message({ value: p.double() }, { value: 1 });
213
342
214
343
const cases = [
215
-
0.0,
216
-
1.0,
217
-
-1.0,
218
-
3.141592653589793,
219
-
-3.141592653589793,
220
-
1.7976931348623157e+308, // close to max double
221
-
2.2250738585072014e-308, // close to min positive double
222
-
Infinity,
223
-
-Infinity,
224
-
NaN,
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
+
},
225
384
];
226
385
227
-
for (const value of cases) {
386
+
for (const { value, expected } of cases) {
228
387
const encoded = p.encode(Message, { value });
229
-
const decoded = p.decode(Message, encoded);
388
+
if (expected !== null) {
389
+
assertEquals(encoded, expected);
390
+
}
230
391
392
+
const decoded = p.decode(Message, encoded);
231
393
assertEquals(decoded, { value });
232
394
}
233
395
});
···
290
452
Deno.test('boolean encoding/decoding', () => {
291
453
const Message = p.message({ value: p.boolean() }, { value: 1 });
292
454
293
-
const cases = [true, false];
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
+
];
294
459
295
-
for (const value of cases) {
460
+
for (const { value, expected } of cases) {
296
461
const encoded = p.encode(Message, { value });
297
-
const decoded = p.decode(Message, encoded);
462
+
assertEquals(encoded, expected);
298
463
464
+
const decoded = p.decode(Message, encoded);
299
465
assertEquals(decoded, { value });
300
466
}
301
467
});
···
304
470
const Message = p.message({ data: p.bytes() }, { data: 1 });
305
471
306
472
const cases = [
307
-
Uint8Array.from([]),
308
-
Uint8Array.from([0]),
309
-
Uint8Array.from([1, 2, 3, 4, 5]),
310
-
Uint8Array.from([255, 254, 253]),
311
-
new Uint8Array(Array.from({ length: 1000 }, (_, i) => i % 256)), // large array
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
+
},
312
493
];
313
494
314
-
for (const data of cases) {
495
+
for (const { data, expected } of cases) {
315
496
const encoded = p.encode(Message, { data });
497
+
if (expected !== null) {
498
+
assertEquals(encoded, expected);
499
+
}
500
+
316
501
const decoded = p.decode(Message, encoded);
317
-
318
502
assertEquals(decoded, { data });
319
503
}
320
504
});
···
323
507
const Message = p.message({ value: p.fixed32() }, { value: 1 });
324
508
325
509
const cases = [
326
-
0,
327
-
1,
328
-
255,
329
-
65535,
330
-
4294967295, // max uint32
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
+
},
331
530
];
332
531
333
-
for (const value of cases) {
532
+
for (const { value, expected } of cases) {
334
533
const encoded = p.encode(Message, { value });
534
+
assertEquals(encoded, expected);
535
+
335
536
const decoded = p.decode(Message, encoded);
336
-
337
537
assertEquals(decoded, { value });
338
538
}
339
539
});
···
342
542
const Message = p.message({ value: p.fixed64() }, { value: 1 });
343
543
344
544
const cases = [
345
-
0n,
346
-
1n,
347
-
255n,
348
-
65535n,
349
-
18446744073709551615n, // max uint64
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
+
},
350
565
];
351
566
352
-
for (const value of cases) {
567
+
for (const { value, expected } of cases) {
353
568
const encoded = p.encode(Message, { value });
569
+
assertEquals(encoded, expected);
570
+
354
571
const decoded = p.decode(Message, encoded);
355
-
356
572
assertEquals(decoded, { value });
357
573
}
358
574
});
···
361
577
const Message = p.message({ value: p.sfixed32() }, { value: 1 });
362
578
363
579
const cases = [
364
-
0,
365
-
1,
366
-
-1,
367
-
2147483647, // max int32
368
-
-2147483648, // min int32
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)
369
585
];
370
586
371
-
for (const value of cases) {
587
+
for (const { value, expected } of cases) {
372
588
const encoded = p.encode(Message, { value });
373
-
const decoded = p.decode(Message, encoded);
589
+
assertEquals(encoded, expected);
374
590
591
+
const decoded = p.decode(Message, encoded);
375
592
assertEquals(decoded, { value });
376
593
}
377
594
});
···
380
597
const Message = p.message({ value: p.sfixed64() }, { value: 1 });
381
598
382
599
const cases = [
383
-
0n,
384
-
1n,
385
-
-1n,
386
-
9223372036854775807n, // max int64
387
-
-9223372036854775808n, // min int64
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
+
},
388
620
];
389
621
390
-
for (const value of cases) {
622
+
for (const { value, expected } of cases) {
391
623
const encoded = p.encode(Message, { value });
624
+
assertEquals(encoded, expected);
625
+
392
626
const decoded = p.decode(Message, encoded);
393
-
394
627
assertEquals(decoded, { value });
395
628
}
396
629
});
···
402
635
Deno.test('repeated fields', () => {
403
636
const cases = [
404
637
{
405
-
numbers: [],
406
-
strings: [],
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
407
641
},
408
642
{
409
-
numbers: [1],
410
-
strings: ['hello'],
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")
411
646
},
412
647
{
413
-
numbers: [1, 2, 3, -1, -2],
414
-
strings: ['hello', 'world', ''],
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")
415
651
},
416
652
{
417
-
numbers: Array.from({ length: 100 }, (_, i) => i),
418
-
strings: Array.from({ length: 100 }, (_, i) => `item${i}`),
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
419
659
},
420
660
];
421
661
662
+
// Test non-packed repeated fields
422
663
{
423
664
const Message = p.message({
424
665
numbers: p.repeated(p.int32(), false),
···
428
669
strings: 2,
429
670
});
430
671
431
-
for (const data of cases) {
672
+
for (const { data, unpacked } of cases) {
432
673
const encoded = p.encode(Message, data);
674
+
if (unpacked !== null) {
675
+
assertEquals(encoded, unpacked);
676
+
}
677
+
433
678
const decoded = p.decode(Message, encoded);
434
-
435
679
assertEquals(decoded, data);
436
680
}
437
681
}
438
682
683
+
// Test packed repeated fields (different wire format)
439
684
{
440
685
const Message = p.message({
441
686
numbers: p.repeated(p.int32(), true),
···
445
690
strings: 2,
446
691
});
447
692
448
-
for (const data of cases) {
693
+
for (const { data, packed } of cases) {
449
694
const encoded = p.encode(Message, data);
450
-
const decoded = p.decode(Message, encoded);
695
+
if (packed !== null) {
696
+
assertEquals(encoded, packed);
697
+
}
451
698
699
+
const decoded = p.decode(Message, encoded);
452
700
assertEquals(decoded, data);
453
701
}
454
702
}
···
467
715
withoutDefault: 4,
468
716
});
469
717
470
-
{
471
-
const full = {
472
-
required: 'hello',
473
-
withDefault: 'custom',
474
-
withFunctionDefault: 99,
475
-
withoutDefault: 'present',
476
-
};
477
-
478
-
const encoded = p.encode(Message, full);
479
-
const decoded = p.decode(Message, encoded);
480
-
481
-
assertEquals(decoded, full);
482
-
}
483
-
{
484
-
const minimal = { required: 'hello' };
485
-
486
-
const encoded = p.encode(Message, minimal);
487
-
const decoded = p.decode(Message, encoded);
488
-
489
-
assertEquals(decoded, {
490
-
required: 'hello',
491
-
withDefault: 'default_value',
492
-
withFunctionDefault: 42,
493
-
// withoutDefault should be undefined (not present)
494
-
});
495
-
}
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
+
];
496
789
497
-
{
498
-
const partial = {
499
-
required: 'hello',
500
-
withDefault: 'custom_value',
501
-
};
790
+
for (const { data, expected } of cases) {
791
+
const encoded = p.encode(Message, data);
792
+
assertEquals(encoded, expected);
502
793
503
-
const encoded = p.encode(Message, partial);
504
794
const decoded = p.decode(Message, encoded);
505
795
506
-
assertEquals(decoded, {
507
-
required: 'hello',
508
-
withDefault: 'custom_value',
509
-
withFunctionDefault: 42,
510
-
// withoutDefault should be undefined (not present)
511
-
});
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
+
}
512
814
}
513
815
});
514
816
···
518
820
const encoded = p.encode(Message, {});
519
821
const decoded = p.decode(Message, encoded);
520
822
823
+
// Empty message should encode to empty buffer
824
+
assertEquals(encoded, new Uint8Array(0));
521
825
assertEquals(decoded, {});
522
826
});
523
827
524
828
Deno.test('nested messages', () => {
525
-
const Address = p.message({
526
-
street: p.string(),
527
-
city: p.string(),
528
-
zipCode: p.optional(p.string()),
529
-
}, {
530
-
street: 1,
531
-
city: 2,
532
-
zipCode: 3,
533
-
});
534
-
535
-
const Person = p.message({
536
-
name: p.string(),
537
-
age: p.int32(),
538
-
address: Address,
539
-
addresses: p.repeated(Address),
540
-
}, {
541
-
name: 1,
542
-
age: 2,
543
-
address: 3,
544
-
addresses: 4,
545
-
});
829
+
{
830
+
const Simple = p.message({
831
+
inner: p.optional(p.message({ value: p.int32() }, { value: 1 })),
832
+
}, { inner: 2 });
546
833
547
-
const data = {
548
-
name: 'John Doe',
549
-
age: 30,
550
-
address: {
551
-
street: '123 Main St',
552
-
city: 'Anytown',
553
-
zipCode: '12345',
554
-
},
555
-
addresses: [
834
+
const cases = [
556
835
{
557
-
street: '456 Oak Ave',
558
-
city: 'Other City',
836
+
data: { inner: { value: 42 } },
837
+
expected: Uint8Array.from([0x12, 0x02, 0x08, 0x2a]), // field 2: length 2, field 1: varint 42
559
838
},
560
839
{
561
-
street: '789 Pine Rd',
562
-
city: 'Another City',
563
-
zipCode: '67890',
840
+
data: {},
841
+
expected: new Uint8Array(0), // empty message
564
842
},
565
-
],
566
-
};
843
+
];
567
844
568
-
const encoded = p.encode(Person, data);
569
-
const decoded = p.decode(Person, encoded);
845
+
for (const { data, expected } of cases) {
846
+
const encoded = p.encode(Simple, data);
847
+
assertEquals(encoded, expected);
570
848
571
-
assertEquals(decoded, data);
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
+
}
572
903
});
573
904
574
905
Deno.test('self-referential messages', () => {
···
824
1155
const decoded = p.decode(Message, buffer);
825
1156
826
1157
assertEquals(decoded, { value: 24 });
827
-
});
828
-
829
-
Deno.test('encoding produces correct wire format', () => {
830
-
// Test that our encoding matches expected protobuf wire format
831
-
const schema = p.message({
832
-
a: p.int32(),
833
-
b: p.string(),
834
-
}, {
835
-
a: 1,
836
-
b: 2,
837
-
});
838
-
839
-
const encoded = p.encode(schema, { a: 150, b: 'testing' });
840
-
841
-
// Manual verification of wire format:
842
-
// Field 1 (a=150): tag=1<<3|0=8, value=150 (varint) = [8, 150, 1]
843
-
// Field 2 (b="testing"): tag=2<<3|2=18, length=7, "testing" = [18, 7, 116, 101, 115, 116, 105, 110, 103]
844
-
845
-
const expected = Uint8Array.from([
846
-
8,
847
-
150,
848
-
1, // field 1: int32 value 150
849
-
18,
850
-
7,
851
-
116,
852
-
101,
853
-
115,
854
-
116,
855
-
105,
856
-
110,
857
-
103, // field 2: string "testing"
858
-
]);
859
-
860
-
assertEquals(encoded, expected);
861
1158
});
862
1159
863
1160
// #endregion