@1337exp/2pack 0.0.0-stage → 2.0.0-dev.0
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 +28 -0
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/README.ru.md +150 -0
- package/dist/codec.d.ts +100 -0
- package/dist/codec.js +168 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +33 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.js +617 -0
- package/package.json +54 -4
package/dist/types.js
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { make } from './codec.js';
|
|
2
|
+
import { fail, PackError } from './errors.js';
|
|
3
|
+
// Every codec below follows docs/spec.md. Writers validate and throw PackError; readers check every length
|
|
4
|
+
// against both the declared maximum and the bytes that remain before they touch or allocate anything.
|
|
5
|
+
// Все кодеки ниже следуют docs/spec.md. Запись проверяет значения и бросает PackError; чтение сверяет каждую
|
|
6
|
+
// длину и с объявленным максимумом, и с оставшимися байтами, прежде чем читать или выделять память.
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
// ignoreBOM: keep a leading U+FEFF so every string round-trips. Malformed UTF-8 becomes U+FFFD.
|
|
9
|
+
const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
|
|
10
|
+
// --- helpers --------------------------------------------------------------------------------------------------
|
|
11
|
+
function truncated() {
|
|
12
|
+
return fail('truncated', 'input ended before the value did');
|
|
13
|
+
}
|
|
14
|
+
function badNumber(v, what) {
|
|
15
|
+
if (typeof v !== 'number')
|
|
16
|
+
return fail('type', `expected a number for ${what}, got ${describe(v)}`);
|
|
17
|
+
return fail('range', `${String(v)} is not a valid ${what}`);
|
|
18
|
+
}
|
|
19
|
+
function describe(v) {
|
|
20
|
+
if (v === null)
|
|
21
|
+
return 'null';
|
|
22
|
+
if (v === undefined)
|
|
23
|
+
return 'undefined';
|
|
24
|
+
return typeof v === 'object' ? (Array.isArray(v) ? 'array' : 'object') : typeof v;
|
|
25
|
+
}
|
|
26
|
+
function varLen(n) {
|
|
27
|
+
return n < 0x80 ? 1 : n < 0x4000 ? 2 : n < 0x200000 ? 3 : n < 0x10000000 ? 4 : 5;
|
|
28
|
+
}
|
|
29
|
+
/** Unsigned LEB128 of a value already known to be an integer in 0..2^32-1. */
|
|
30
|
+
function writeVar(w, value) {
|
|
31
|
+
const dv = w.dv;
|
|
32
|
+
let p = w.pos;
|
|
33
|
+
let v = value;
|
|
34
|
+
while (v > 0x7f) {
|
|
35
|
+
dv.setUint8(p++, (v & 0x7f) | 0x80);
|
|
36
|
+
v >>>= 7;
|
|
37
|
+
}
|
|
38
|
+
dv.setUint8(p++, v);
|
|
39
|
+
w.pos = p;
|
|
40
|
+
}
|
|
41
|
+
function readVar(r) {
|
|
42
|
+
const dv = r.dv;
|
|
43
|
+
const end = r.end;
|
|
44
|
+
let p = r.pos;
|
|
45
|
+
let result = 0;
|
|
46
|
+
for (let i = 0; i < 5; i++) {
|
|
47
|
+
if (p >= end)
|
|
48
|
+
truncated();
|
|
49
|
+
const b = dv.getUint8(p++);
|
|
50
|
+
if (i === 4 && b > 0x0f)
|
|
51
|
+
fail('invalid', 'varint does not fit in 32 bits');
|
|
52
|
+
result |= (b & 0x7f) << (7 * i);
|
|
53
|
+
if ((b & 0x80) === 0) {
|
|
54
|
+
if (i > 0 && b === 0)
|
|
55
|
+
fail('invalid', 'overlong varint');
|
|
56
|
+
r.pos = p;
|
|
57
|
+
return result >>> 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return fail('invalid', 'varint is longer than 5 bytes');
|
|
61
|
+
}
|
|
62
|
+
function checkLimit(max, what) {
|
|
63
|
+
if (!Number.isInteger(max) || max < 0 || max > 0xffffffff) {
|
|
64
|
+
throw new TypeError(`${what}: max must be an integer from 0 to 4294967295, got ${String(max)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// --- fixed-size scalars ---------------------------------------------------------------------------------------
|
|
68
|
+
/** `true` / `false`, 1 byte. */
|
|
69
|
+
export const bool = make(1, 1, (w, v) => {
|
|
70
|
+
if (typeof v !== 'boolean')
|
|
71
|
+
fail('type', `expected a boolean, got ${describe(v)}`);
|
|
72
|
+
w.dv.setUint8(w.pos, v ? 1 : 0);
|
|
73
|
+
w.pos += 1;
|
|
74
|
+
}, (r) => {
|
|
75
|
+
if (r.pos + 1 > r.end)
|
|
76
|
+
truncated();
|
|
77
|
+
const b = r.dv.getUint8(r.pos);
|
|
78
|
+
if (b > 1)
|
|
79
|
+
fail('invalid', `a boolean must be 0 or 1, got ${b}`);
|
|
80
|
+
r.pos += 1;
|
|
81
|
+
return b === 1;
|
|
82
|
+
});
|
|
83
|
+
/** Unsigned 8-bit integer. / Беззнаковое 8 бит. */
|
|
84
|
+
export const u8 = make(1, 1, (w, v) => {
|
|
85
|
+
if (typeof v !== 'number' || (v & 0xff) !== v)
|
|
86
|
+
badNumber(v, 'u8');
|
|
87
|
+
w.dv.setUint8(w.pos, v);
|
|
88
|
+
w.pos += 1;
|
|
89
|
+
}, (r) => {
|
|
90
|
+
if (r.pos + 1 > r.end)
|
|
91
|
+
truncated();
|
|
92
|
+
const v = r.dv.getUint8(r.pos);
|
|
93
|
+
r.pos += 1;
|
|
94
|
+
return v;
|
|
95
|
+
});
|
|
96
|
+
/** Signed 8-bit integer. / Знаковое 8 бит. */
|
|
97
|
+
export const i8 = make(1, 1, (w, v) => {
|
|
98
|
+
if (typeof v !== 'number' || ((v << 24) >> 24) !== v)
|
|
99
|
+
badNumber(v, 'i8');
|
|
100
|
+
w.dv.setInt8(w.pos, v);
|
|
101
|
+
w.pos += 1;
|
|
102
|
+
}, (r) => {
|
|
103
|
+
if (r.pos + 1 > r.end)
|
|
104
|
+
truncated();
|
|
105
|
+
const v = r.dv.getInt8(r.pos);
|
|
106
|
+
r.pos += 1;
|
|
107
|
+
return v;
|
|
108
|
+
});
|
|
109
|
+
/** Unsigned 16-bit integer, little-endian. / Беззнаковое 16 бит. */
|
|
110
|
+
export const u16 = make(2, 2, (w, v) => {
|
|
111
|
+
if (typeof v !== 'number' || (v & 0xffff) !== v)
|
|
112
|
+
badNumber(v, 'u16');
|
|
113
|
+
w.dv.setUint16(w.pos, v, true);
|
|
114
|
+
w.pos += 2;
|
|
115
|
+
}, (r) => {
|
|
116
|
+
if (r.pos + 2 > r.end)
|
|
117
|
+
truncated();
|
|
118
|
+
const v = r.dv.getUint16(r.pos, true);
|
|
119
|
+
r.pos += 2;
|
|
120
|
+
return v;
|
|
121
|
+
});
|
|
122
|
+
/** Signed 16-bit integer, little-endian. / Знаковое 16 бит. */
|
|
123
|
+
export const i16 = make(2, 2, (w, v) => {
|
|
124
|
+
if (typeof v !== 'number' || ((v << 16) >> 16) !== v)
|
|
125
|
+
badNumber(v, 'i16');
|
|
126
|
+
w.dv.setInt16(w.pos, v, true);
|
|
127
|
+
w.pos += 2;
|
|
128
|
+
}, (r) => {
|
|
129
|
+
if (r.pos + 2 > r.end)
|
|
130
|
+
truncated();
|
|
131
|
+
const v = r.dv.getInt16(r.pos, true);
|
|
132
|
+
r.pos += 2;
|
|
133
|
+
return v;
|
|
134
|
+
});
|
|
135
|
+
/** Unsigned 32-bit integer, little-endian. / Беззнаковое 32 бита. */
|
|
136
|
+
export const u32 = make(4, 4, (w, v) => {
|
|
137
|
+
if (typeof v !== 'number' || v >>> 0 !== v)
|
|
138
|
+
badNumber(v, 'u32');
|
|
139
|
+
w.dv.setUint32(w.pos, v, true);
|
|
140
|
+
w.pos += 4;
|
|
141
|
+
}, (r) => {
|
|
142
|
+
if (r.pos + 4 > r.end)
|
|
143
|
+
truncated();
|
|
144
|
+
const v = r.dv.getUint32(r.pos, true);
|
|
145
|
+
r.pos += 4;
|
|
146
|
+
return v;
|
|
147
|
+
});
|
|
148
|
+
/** Signed 32-bit integer, little-endian. / Знаковое 32 бита. */
|
|
149
|
+
export const i32 = make(4, 4, (w, v) => {
|
|
150
|
+
if (typeof v !== 'number' || (v | 0) !== v)
|
|
151
|
+
badNumber(v, 'i32');
|
|
152
|
+
w.dv.setInt32(w.pos, v, true);
|
|
153
|
+
w.pos += 4;
|
|
154
|
+
}, (r) => {
|
|
155
|
+
if (r.pos + 4 > r.end)
|
|
156
|
+
truncated();
|
|
157
|
+
const v = r.dv.getInt32(r.pos, true);
|
|
158
|
+
r.pos += 4;
|
|
159
|
+
return v;
|
|
160
|
+
});
|
|
161
|
+
const U64_MAX = 0xffffffffffffffffn;
|
|
162
|
+
const I64_MIN = -0x8000000000000000n;
|
|
163
|
+
const I64_MAX = 0x7fffffffffffffffn;
|
|
164
|
+
function wantBigInt(v, what) {
|
|
165
|
+
if (typeof v === 'bigint')
|
|
166
|
+
return v;
|
|
167
|
+
return fail('type', `expected a bigint for ${what} (write 5n, not 5), got ${describe(v)}`);
|
|
168
|
+
}
|
|
169
|
+
/** Unsigned 64-bit integer, always a `bigint`. / Беззнаковое 64 бита, всегда `bigint`. */
|
|
170
|
+
export const u64 = make(8, 8, (w, v) => {
|
|
171
|
+
const b = wantBigInt(v, 'u64');
|
|
172
|
+
if (b < 0n || b > U64_MAX)
|
|
173
|
+
fail('range', `${b} is not a valid u64`);
|
|
174
|
+
w.dv.setBigUint64(w.pos, b, true);
|
|
175
|
+
w.pos += 8;
|
|
176
|
+
}, (r) => {
|
|
177
|
+
if (r.pos + 8 > r.end)
|
|
178
|
+
truncated();
|
|
179
|
+
const v = r.dv.getBigUint64(r.pos, true);
|
|
180
|
+
r.pos += 8;
|
|
181
|
+
return v;
|
|
182
|
+
});
|
|
183
|
+
/** Signed 64-bit integer, always a `bigint`. / Знаковое 64 бита, всегда `bigint`. */
|
|
184
|
+
export const i64 = make(8, 8, (w, v) => {
|
|
185
|
+
const b = wantBigInt(v, 'i64');
|
|
186
|
+
if (b < I64_MIN || b > I64_MAX)
|
|
187
|
+
fail('range', `${b} is not a valid i64`);
|
|
188
|
+
w.dv.setBigInt64(w.pos, b, true);
|
|
189
|
+
w.pos += 8;
|
|
190
|
+
}, (r) => {
|
|
191
|
+
if (r.pos + 8 > r.end)
|
|
192
|
+
truncated();
|
|
193
|
+
const v = r.dv.getBigInt64(r.pos, true);
|
|
194
|
+
r.pos += 8;
|
|
195
|
+
return v;
|
|
196
|
+
});
|
|
197
|
+
/** IEEE 754 single precision. `NaN` and `±Infinity` survive. / Одинарная точность. */
|
|
198
|
+
export const f32 = make(4, 4, (w, v) => {
|
|
199
|
+
if (typeof v !== 'number')
|
|
200
|
+
fail('type', `expected a number for f32, got ${describe(v)}`);
|
|
201
|
+
w.dv.setFloat32(w.pos, v, true);
|
|
202
|
+
w.pos += 4;
|
|
203
|
+
}, (r) => {
|
|
204
|
+
if (r.pos + 4 > r.end)
|
|
205
|
+
truncated();
|
|
206
|
+
const v = r.dv.getFloat32(r.pos, true);
|
|
207
|
+
r.pos += 4;
|
|
208
|
+
return v;
|
|
209
|
+
});
|
|
210
|
+
/** IEEE 754 double precision. `NaN` and `±Infinity` survive. / Двойная точность. */
|
|
211
|
+
export const f64 = make(8, 8, (w, v) => {
|
|
212
|
+
if (typeof v !== 'number')
|
|
213
|
+
fail('type', `expected a number for f64, got ${describe(v)}`);
|
|
214
|
+
w.dv.setFloat64(w.pos, v, true);
|
|
215
|
+
w.pos += 8;
|
|
216
|
+
}, (r) => {
|
|
217
|
+
if (r.pos + 8 > r.end)
|
|
218
|
+
truncated();
|
|
219
|
+
const v = r.dv.getFloat64(r.pos, true);
|
|
220
|
+
r.pos += 8;
|
|
221
|
+
return v;
|
|
222
|
+
});
|
|
223
|
+
// --- variable-size integers -----------------------------------------------------------------------------------
|
|
224
|
+
/** Unsigned LEB128, `0 … 2^32-1`, 1 to 5 bytes: small numbers cost one byte. / Малые числа занимают 1 байт. */
|
|
225
|
+
export const varuint = make(1, 5, (w, v) => {
|
|
226
|
+
if (typeof v !== 'number' || v >>> 0 !== v)
|
|
227
|
+
badNumber(v, 'varuint');
|
|
228
|
+
writeVar(w, v);
|
|
229
|
+
}, readVar);
|
|
230
|
+
/** Zigzag + LEB128, `-2^31 … 2^31-1`: small magnitudes cost one byte whatever the sign. */
|
|
231
|
+
export const varint = make(1, 5, (w, v) => {
|
|
232
|
+
if (typeof v !== 'number' || (v | 0) !== v)
|
|
233
|
+
badNumber(v, 'varint');
|
|
234
|
+
writeVar(w, ((v << 1) ^ (v >> 31)) >>> 0);
|
|
235
|
+
}, (r) => {
|
|
236
|
+
const z = readVar(r);
|
|
237
|
+
return (z >>> 1) ^ -(z & 1);
|
|
238
|
+
});
|
|
239
|
+
// --- strings and bytes ----------------------------------------------------------------------------------------
|
|
240
|
+
function decodeUtf8(u8, start, n) {
|
|
241
|
+
if (n === 0)
|
|
242
|
+
return '';
|
|
243
|
+
if (n <= 24) {
|
|
244
|
+
// Short ASCII strings (names, ids) are the common case: skip TextDecoder.
|
|
245
|
+
let s = '';
|
|
246
|
+
let i = 0;
|
|
247
|
+
for (; i < n; i++) {
|
|
248
|
+
const c = u8[start + i];
|
|
249
|
+
if (c > 127)
|
|
250
|
+
break;
|
|
251
|
+
s += String.fromCharCode(c);
|
|
252
|
+
}
|
|
253
|
+
if (i === n)
|
|
254
|
+
return s;
|
|
255
|
+
}
|
|
256
|
+
return decoder.decode(u8.subarray(start, start + n));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* UTF-8 string of at most `maxBytes` **bytes** (not characters). A longer string is an error, never truncated.
|
|
260
|
+
* Строка UTF-8 не длиннее `maxBytes` **байт** (не символов). Длиннее: ошибка, без обрезки.
|
|
261
|
+
*/
|
|
262
|
+
export function str(maxBytes) {
|
|
263
|
+
checkLimit(maxBytes, 'str');
|
|
264
|
+
return make(1, varLen(maxBytes) + maxBytes, (w, v) => {
|
|
265
|
+
if (typeof v !== 'string')
|
|
266
|
+
fail('type', `expected a string, got ${describe(v)}`);
|
|
267
|
+
const len = v.length;
|
|
268
|
+
// A UTF-8 string is never shorter in bytes than in UTF-16 units, so this rejects early and cheaply.
|
|
269
|
+
if (len > maxBytes)
|
|
270
|
+
fail('range', `string is longer than ${maxBytes} bytes`);
|
|
271
|
+
const u8 = w.u8;
|
|
272
|
+
// Stage the body exactly where it ends up if the string is pure ASCII (one byte per unit, so the
|
|
273
|
+
// length prefix is varLen(len)). Nothing is ever written outside the final packet.
|
|
274
|
+
const asciiPrefix = varLen(len);
|
|
275
|
+
const body = w.pos + asciiPrefix;
|
|
276
|
+
if (body + len > u8.length)
|
|
277
|
+
fail('overflow', 'buffer too small for this string');
|
|
278
|
+
let i = 0;
|
|
279
|
+
for (; i < len; i++) {
|
|
280
|
+
const c = v.charCodeAt(i);
|
|
281
|
+
if (c > 127)
|
|
282
|
+
break;
|
|
283
|
+
u8[body + i] = c;
|
|
284
|
+
}
|
|
285
|
+
if (i === len) {
|
|
286
|
+
writeVar(w, len);
|
|
287
|
+
w.pos += len;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Non-ASCII: the byte length n is only known after encoding, and n >= len, so the prefix can only
|
|
291
|
+
// grow. If it does, move the body right, still inside the final packet.
|
|
292
|
+
const room = Math.min(maxBytes, u8.length - body);
|
|
293
|
+
const res = encoder.encodeInto(v, u8.subarray(body, body + room));
|
|
294
|
+
if (res.read < len) {
|
|
295
|
+
// Rare error path: find out which limit was hit.
|
|
296
|
+
if (encoder.encode(v).length > maxBytes)
|
|
297
|
+
fail('range', `string is longer than ${maxBytes} bytes`);
|
|
298
|
+
fail('overflow', 'buffer too small for this string');
|
|
299
|
+
}
|
|
300
|
+
const n = res.written;
|
|
301
|
+
const actual = varLen(n);
|
|
302
|
+
if (actual !== asciiPrefix) {
|
|
303
|
+
if (w.pos + actual + n > u8.length)
|
|
304
|
+
fail('overflow', 'buffer too small for this string');
|
|
305
|
+
u8.copyWithin(w.pos + actual, body, body + n);
|
|
306
|
+
}
|
|
307
|
+
writeVar(w, n);
|
|
308
|
+
w.pos += n;
|
|
309
|
+
}, (r) => {
|
|
310
|
+
const n = readVar(r);
|
|
311
|
+
if (n > maxBytes)
|
|
312
|
+
fail('invalid', `string length ${n} is above the maximum ${maxBytes}`);
|
|
313
|
+
if (n > r.end - r.pos)
|
|
314
|
+
truncated();
|
|
315
|
+
const s = decodeUtf8(r.u8, r.pos, n);
|
|
316
|
+
r.pos += n;
|
|
317
|
+
return s;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Raw bytes, at most `maxBytes`. Decoded as a copy by default.
|
|
322
|
+
* Сырые байты, не больше `maxBytes`. По умолчанию при чтении копируются.
|
|
323
|
+
*/
|
|
324
|
+
export function bytes(maxBytes, options = {}) {
|
|
325
|
+
checkLimit(maxBytes, 'bytes');
|
|
326
|
+
const view = options.view === true;
|
|
327
|
+
return make(1, varLen(maxBytes) + maxBytes, (w, v) => {
|
|
328
|
+
if (!(v instanceof Uint8Array))
|
|
329
|
+
fail('type', `expected a Uint8Array, got ${describe(v)}`);
|
|
330
|
+
const n = v.length;
|
|
331
|
+
if (n > maxBytes)
|
|
332
|
+
fail('range', `${n} bytes is above the maximum ${maxBytes}`);
|
|
333
|
+
if (w.pos + varLen(n) + n > w.u8.length)
|
|
334
|
+
fail('overflow', 'buffer too small for these bytes');
|
|
335
|
+
writeVar(w, n);
|
|
336
|
+
w.u8.set(v, w.pos);
|
|
337
|
+
w.pos += n;
|
|
338
|
+
}, (r) => {
|
|
339
|
+
const n = readVar(r);
|
|
340
|
+
if (n > maxBytes)
|
|
341
|
+
fail('invalid', `length ${n} is above the maximum ${maxBytes}`);
|
|
342
|
+
if (n > r.end - r.pos)
|
|
343
|
+
truncated();
|
|
344
|
+
const out = view ? r.u8.subarray(r.pos, r.pos + n) : r.u8.slice(r.pos, r.pos + n);
|
|
345
|
+
r.pos += n;
|
|
346
|
+
return out;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// --- composites -----------------------------------------------------------------------------------------------
|
|
350
|
+
function impl(c) {
|
|
351
|
+
const i = c;
|
|
352
|
+
if (typeof i.write !== 'function' || typeof i.read !== 'function') {
|
|
353
|
+
throw new TypeError('expected a 2pack codec (u8, str(32), define({...}), …)');
|
|
354
|
+
}
|
|
355
|
+
return i;
|
|
356
|
+
}
|
|
357
|
+
/** An extensible record owns the rest of its range, so something placed after it could never be found. */
|
|
358
|
+
function nestable(c, where) {
|
|
359
|
+
const i = impl(c);
|
|
360
|
+
if (i.open) {
|
|
361
|
+
throw new TypeError(`${where}: a record with added(...) fields can only be the top-level codec, it cannot be nested`);
|
|
362
|
+
}
|
|
363
|
+
return i;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* A list of `item`s, at most `maxCount` of them.
|
|
367
|
+
* Список из `item`, не больше `maxCount` штук.
|
|
368
|
+
*/
|
|
369
|
+
export function array(item, maxCount) {
|
|
370
|
+
checkLimit(maxCount, 'array');
|
|
371
|
+
const it = nestable(item, 'array');
|
|
372
|
+
if (it.max === 0)
|
|
373
|
+
throw new TypeError('array: the item type is empty (zero bytes), an array of it is meaningless');
|
|
374
|
+
return make(1, varLen(maxCount) + maxCount * it.max, (w, v) => {
|
|
375
|
+
if (!Array.isArray(v))
|
|
376
|
+
fail('type', `expected an array, got ${describe(v)}`);
|
|
377
|
+
const n = v.length;
|
|
378
|
+
if (n > maxCount)
|
|
379
|
+
fail('range', `array has ${n} items, the maximum is ${maxCount}`);
|
|
380
|
+
writeVar(w, n);
|
|
381
|
+
let i = 0;
|
|
382
|
+
try {
|
|
383
|
+
for (; i < n; i++)
|
|
384
|
+
it.write(w, v[i]);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
if (e instanceof PackError)
|
|
388
|
+
e.addPath(i);
|
|
389
|
+
throw e;
|
|
390
|
+
}
|
|
391
|
+
}, (r) => {
|
|
392
|
+
const n = readVar(r);
|
|
393
|
+
if (n > maxCount)
|
|
394
|
+
fail('invalid', `array count ${n} is above the maximum ${maxCount}`);
|
|
395
|
+
// Reject a count the remaining bytes cannot possibly hold *before* allocating anything for it.
|
|
396
|
+
if (n * it.min > r.end - r.pos)
|
|
397
|
+
truncated();
|
|
398
|
+
const out = new Array(n);
|
|
399
|
+
let i = 0;
|
|
400
|
+
try {
|
|
401
|
+
for (; i < n; i++)
|
|
402
|
+
out[i] = it.read(r);
|
|
403
|
+
}
|
|
404
|
+
catch (e) {
|
|
405
|
+
if (e instanceof PackError)
|
|
406
|
+
e.addPath(i);
|
|
407
|
+
throw e;
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* A value that may be absent: `undefined` (typed) or `null` (accepted at runtime, so JSON-shaped data works) when
|
|
414
|
+
* packing, `undefined` when unpacking. In `define`, the field may be left out. Costs 1 byte.
|
|
415
|
+
* Значение, которого может не быть: при упаковке `undefined` (по типам) или `null` (принимается в рантайме, чтобы
|
|
416
|
+
* данные из JSON не ломались), при чтении `undefined`. В `define` поле можно не указывать. Стоит 1 байт.
|
|
417
|
+
*/
|
|
418
|
+
export function optional(item) {
|
|
419
|
+
const it = nestable(item, 'optional');
|
|
420
|
+
return make(1, 1 + it.max, (w, v) => {
|
|
421
|
+
if (v === undefined || v === null) {
|
|
422
|
+
w.dv.setUint8(w.pos, 0);
|
|
423
|
+
w.pos += 1;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
w.dv.setUint8(w.pos, 1);
|
|
427
|
+
w.pos += 1;
|
|
428
|
+
it.write(w, v);
|
|
429
|
+
}, (r) => {
|
|
430
|
+
if (r.pos + 1 > r.end)
|
|
431
|
+
truncated();
|
|
432
|
+
const flag = r.dv.getUint8(r.pos);
|
|
433
|
+
if (flag > 1)
|
|
434
|
+
fail('invalid', `a presence flag must be 0 or 1, got ${flag}`);
|
|
435
|
+
r.pos += 1;
|
|
436
|
+
return flag === 1 ? it.read(r) : undefined;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* One of a fixed list of names, stored as its index (1 byte for up to 256 names, else 2).
|
|
441
|
+
* Одно из имён фиксированного списка; хранится индексом (1 байт до 256 имён, иначе 2).
|
|
442
|
+
*/
|
|
443
|
+
export function enumOf(names) {
|
|
444
|
+
if (!Array.isArray(names) || names.length === 0 || names.length > 65536) {
|
|
445
|
+
throw new TypeError('enumOf: expected 1 to 65536 names');
|
|
446
|
+
}
|
|
447
|
+
const index = new Map();
|
|
448
|
+
names.forEach((name, i) => {
|
|
449
|
+
if (typeof name !== 'string')
|
|
450
|
+
throw new TypeError('enumOf: names must be strings');
|
|
451
|
+
if (index.has(name))
|
|
452
|
+
throw new TypeError(`enumOf: duplicate name "${name}"`);
|
|
453
|
+
index.set(name, i);
|
|
454
|
+
});
|
|
455
|
+
const wide = names.length > 256;
|
|
456
|
+
const size = wide ? 2 : 1;
|
|
457
|
+
return make(size, size, (w, v) => {
|
|
458
|
+
if (typeof v !== 'string')
|
|
459
|
+
fail('type', `expected one of the enum names, got ${describe(v)}`);
|
|
460
|
+
const i = index.get(v);
|
|
461
|
+
if (i === undefined)
|
|
462
|
+
fail('range', `"${v}" is not one of the enum names`);
|
|
463
|
+
if (wide)
|
|
464
|
+
w.dv.setUint16(w.pos, i, true);
|
|
465
|
+
else
|
|
466
|
+
w.dv.setUint8(w.pos, i);
|
|
467
|
+
w.pos += size;
|
|
468
|
+
}, (r) => {
|
|
469
|
+
if (r.pos + size > r.end)
|
|
470
|
+
truncated();
|
|
471
|
+
const i = wide ? r.dv.getUint16(r.pos, true) : r.dv.getUint8(r.pos);
|
|
472
|
+
if (i >= names.length)
|
|
473
|
+
fail('invalid', `enum index ${i} is out of range (${names.length} names)`);
|
|
474
|
+
r.pos += size;
|
|
475
|
+
return names[i];
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* A number in `[min, max]` stored as an unsigned `bits`-bit integer (8, 16 or 32): the error is at most half a
|
|
480
|
+
* step, `(max - min) / (2^bits - 1) / 2`. Positions, angles, ratios.
|
|
481
|
+
* Число из `[min, max]`, хранится беззнаковым целым в `bits` бит (8, 16 или 32): ошибка не больше полушага.
|
|
482
|
+
* Координаты, углы, доли.
|
|
483
|
+
*/
|
|
484
|
+
export function quant(min, max, bits, options = {}) {
|
|
485
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || !(min < max) || !Number.isFinite(max - min)) {
|
|
486
|
+
// The last test catches (-MAX_VALUE, MAX_VALUE): both finite, but the span overflows to Infinity.
|
|
487
|
+
throw new TypeError('quant: min and max must be finite numbers with min < max and a finite span');
|
|
488
|
+
}
|
|
489
|
+
if (bits !== 8 && bits !== 16 && bits !== 32)
|
|
490
|
+
throw new TypeError('quant: bits must be 8, 16 or 32');
|
|
491
|
+
const size = bits / 8;
|
|
492
|
+
const levels = 2 ** bits - 1;
|
|
493
|
+
const span = max - min;
|
|
494
|
+
const clamp = options.clamp === true;
|
|
495
|
+
return make(size, size, (w, v) => {
|
|
496
|
+
if (typeof v !== 'number' || Number.isNaN(v))
|
|
497
|
+
badNumber(v, 'quantized number');
|
|
498
|
+
let x = v;
|
|
499
|
+
if (x < min || x > max) {
|
|
500
|
+
if (!clamp)
|
|
501
|
+
fail('range', `${String(v)} is outside [${min}, ${max}]`);
|
|
502
|
+
x = x < min ? min : max;
|
|
503
|
+
}
|
|
504
|
+
const q = Math.round(((x - min) / span) * levels);
|
|
505
|
+
if (bits === 8)
|
|
506
|
+
w.dv.setUint8(w.pos, q);
|
|
507
|
+
else if (bits === 16)
|
|
508
|
+
w.dv.setUint16(w.pos, q, true);
|
|
509
|
+
else
|
|
510
|
+
w.dv.setUint32(w.pos, q, true);
|
|
511
|
+
w.pos += size;
|
|
512
|
+
}, (r) => {
|
|
513
|
+
if (r.pos + size > r.end)
|
|
514
|
+
truncated();
|
|
515
|
+
const q = bits === 8 ? r.dv.getUint8(r.pos) : bits === 16 ? r.dv.getUint16(r.pos, true) : r.dv.getUint32(r.pos, true);
|
|
516
|
+
r.pos += size;
|
|
517
|
+
return q === levels ? max : min + (q / levels) * span;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Mark a field of a record as added in a newer version of the schema. A packet written by an older writer ends
|
|
522
|
+
* before this field; then the reader fills it with `fallback`. The writer always writes it. Added fields must be
|
|
523
|
+
* the LAST fields of the record, and a record that has any can only be the top-level codec. See docs/spec.md,
|
|
524
|
+
* "Evolution".
|
|
525
|
+
* Пометить поле записи как добавленное в новой версии схемы. Пакет старого писателя кончается до этого поля,
|
|
526
|
+
* тогда читатель подставляет `fallback`. Писатель пишет его всегда. Добавленные поля должны идти ПОСЛЕДНИМИ,
|
|
527
|
+
* а запись с такими полями может быть только верхнего уровня. См. docs/spec.md, «Evolution».
|
|
528
|
+
*/
|
|
529
|
+
export function added(item, fallback) {
|
|
530
|
+
const it = nestable(item, 'added');
|
|
531
|
+
try {
|
|
532
|
+
it.pack(fallback);
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
throw new TypeError(`added: the default is not a valid value for this type (${e instanceof Error ? e.message : String(e)})`);
|
|
536
|
+
}
|
|
537
|
+
const marked = { ...it, isAdded: true, fallback };
|
|
538
|
+
return marked;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* A record with named fields, written in declaration order. Names are not stored in the packet.
|
|
542
|
+
* Fields wrapped in `added(...)` (at the end) make it evolvable: see `added`.
|
|
543
|
+
* Запись с именованными полями, пишется в порядке объявления. Имена в пакет не попадают.
|
|
544
|
+
* Поля в `added(...)` (в конце) делают запись развиваемой: см. `added`.
|
|
545
|
+
*/
|
|
546
|
+
export function define(shape, options = {}) {
|
|
547
|
+
const names = Object.keys(shape);
|
|
548
|
+
const codecs = [];
|
|
549
|
+
let min = 0;
|
|
550
|
+
let max = 0;
|
|
551
|
+
let required = names.length; // how many leading fields are not `added`
|
|
552
|
+
for (const [index, name] of names.entries()) {
|
|
553
|
+
// Integer-like keys would be reordered by JS (`{ b: …, 1: … }` iterates 1 first) and `__proto__` would
|
|
554
|
+
// change the prototype of every decoded object.
|
|
555
|
+
if (name === '__proto__' || /^(0|[1-9]\d*)$/.test(name)) {
|
|
556
|
+
throw new TypeError(`define: "${name}" is not allowed as a field name`);
|
|
557
|
+
}
|
|
558
|
+
const c = nestable(shape[name], `define: field "${name}"`);
|
|
559
|
+
if (c.isAdded) {
|
|
560
|
+
if (required === names.length)
|
|
561
|
+
required = index;
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
if (required < names.length)
|
|
565
|
+
throw new TypeError(`define: field "${name}" comes after an added(...) field; added fields must be last`);
|
|
566
|
+
min += c.min;
|
|
567
|
+
}
|
|
568
|
+
codecs.push(c);
|
|
569
|
+
max += c.max;
|
|
570
|
+
}
|
|
571
|
+
const n = names.length;
|
|
572
|
+
// `open`: the record depends on where its range ends (added fields or extensible), so it cannot be nested.
|
|
573
|
+
// `lenient`: only an explicit `extensible: true` accepts bytes after the last known field; `added` alone
|
|
574
|
+
// fills in defaults for an older packet but still rejects a newer one.
|
|
575
|
+
const lenient = options.extensible === true;
|
|
576
|
+
const open = lenient || required < n;
|
|
577
|
+
const fallbacks = codecs.map((c) => c.fallback);
|
|
578
|
+
return make(min, max, (w, v) => {
|
|
579
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
580
|
+
fail('type', `expected an object, got ${describe(v)}`);
|
|
581
|
+
const src = v;
|
|
582
|
+
let i = 0;
|
|
583
|
+
try {
|
|
584
|
+
for (; i < n; i++)
|
|
585
|
+
codecs[i].write(w, src[names[i]]);
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
if (e instanceof PackError)
|
|
589
|
+
e.addPath(names[i]);
|
|
590
|
+
throw e;
|
|
591
|
+
}
|
|
592
|
+
}, (r) => {
|
|
593
|
+
const out = {};
|
|
594
|
+
let i = 0;
|
|
595
|
+
try {
|
|
596
|
+
for (; i < required; i++)
|
|
597
|
+
out[names[i]] = codecs[i].read(r);
|
|
598
|
+
// Added fields: if the packet already ended exactly on a field boundary, it came from an older
|
|
599
|
+
// writer and the field takes its default. A packet that ends in the middle of one is truncated.
|
|
600
|
+
for (; i < n; i++) {
|
|
601
|
+
if (r.pos >= r.end) {
|
|
602
|
+
const f = fallbacks[i];
|
|
603
|
+
out[names[i]] = typeof f === 'object' && f !== null ? structuredClone(f) : f;
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
out[names[i]] = codecs[i].read(r);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
if (e instanceof PackError)
|
|
612
|
+
e.addPath(names[i]);
|
|
613
|
+
throw e;
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}, open, lenient);
|
|
617
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1337exp/2pack",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.0-dev.0",
|
|
4
|
+
"description": "Tiny schema-first binary codec for Node and browsers: typed, bounds-checked, zero dependencies. You own the buffer.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"binary",
|
|
7
|
+
"serialization",
|
|
8
|
+
"codec",
|
|
9
|
+
"schema",
|
|
10
|
+
"packet",
|
|
11
|
+
"websocket",
|
|
12
|
+
"typescript",
|
|
13
|
+
"gamedev"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"mutate": "node scripts/mutate.mjs . scripts/mutations.json",
|
|
37
|
+
"bench": "npm run build && node scripts/bench.mjs",
|
|
38
|
+
"browser": "node scripts/browser.mjs",
|
|
39
|
+
"backpressure": "npm run build && node examples/backpressure.mjs",
|
|
40
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^26.6.4",
|
|
44
|
+
"cbor-x": "^1.6.9",
|
|
45
|
+
"msgpackr": "^2.1.4",
|
|
46
|
+
"typescript": "~6.0.3",
|
|
47
|
+
"vitest": "^5.1.0"
|
|
48
|
+
},
|
|
49
|
+
"author": "1337exp",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|