@zvndev/powdb-client 0.12.0 → 0.13.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/README.md +96 -6
- package/dist/cjs/index.d.ts +87 -3
- package/dist/cjs/index.js +192 -2
- package/dist/cjs/protocol.d.ts +55 -0
- package/dist/cjs/protocol.js +405 -33
- package/dist/index.d.ts +87 -3
- package/dist/index.js +190 -1
- package/dist/protocol.d.ts +55 -0
- package/dist/protocol.js +404 -32
- package/package.json +1 -1
package/dist/protocol.js
CHANGED
|
@@ -32,6 +32,11 @@ export const MSG_RESULT_MSG = 0x0b;
|
|
|
32
32
|
export const MSG_DISCONNECT = 0x10;
|
|
33
33
|
export const MSG_PING = 0x11;
|
|
34
34
|
export const MSG_PONG = 0x12;
|
|
35
|
+
export const MSG_QUERY_NATIVE = 0x13;
|
|
36
|
+
export const MSG_QUERY_PARAMS_NATIVE = 0x14;
|
|
37
|
+
export const MSG_QUERY_SQL_NATIVE = 0x15;
|
|
38
|
+
export const MSG_RESULT_ROWS_NATIVE = 0x16;
|
|
39
|
+
export const MSG_RESULT_SCALAR_NATIVE = 0x17;
|
|
35
40
|
// ───── Size limits (mirror crates/server/src/protocol.rs) ──────────────────
|
|
36
41
|
/** Maximum payload size accepted from the wire (64 MB). */
|
|
37
42
|
export const MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
|
|
@@ -80,41 +85,22 @@ export function encode(msg) {
|
|
|
80
85
|
msgType = MSG_QUERY_SQL;
|
|
81
86
|
break;
|
|
82
87
|
case "QueryWithParams": {
|
|
83
|
-
|
|
84
|
-
const count = Buffer.alloc(2);
|
|
85
|
-
count.writeUInt16LE(msg.params.length, 0);
|
|
86
|
-
parts.push(count);
|
|
87
|
-
for (const p of msg.params) {
|
|
88
|
-
switch (p.tag) {
|
|
89
|
-
case "null":
|
|
90
|
-
parts.push(Buffer.from([0]));
|
|
91
|
-
break;
|
|
92
|
-
case "int": {
|
|
93
|
-
const b = Buffer.alloc(9);
|
|
94
|
-
b.writeUInt8(1, 0);
|
|
95
|
-
b.writeBigInt64LE(p.value, 1);
|
|
96
|
-
parts.push(b);
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
case "float": {
|
|
100
|
-
const b = Buffer.alloc(9);
|
|
101
|
-
b.writeUInt8(2, 0);
|
|
102
|
-
b.writeDoubleLE(p.value, 1);
|
|
103
|
-
parts.push(b);
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
case "bool":
|
|
107
|
-
parts.push(Buffer.from([3, p.value ? 1 : 0]));
|
|
108
|
-
break;
|
|
109
|
-
case "str":
|
|
110
|
-
parts.push(Buffer.from([4]), encodeString(p.value));
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
payload = Buffer.concat(parts);
|
|
88
|
+
payload = encodeQueryWithParams(msg.query, msg.params);
|
|
115
89
|
msgType = MSG_QUERY_PARAMS;
|
|
116
90
|
break;
|
|
117
91
|
}
|
|
92
|
+
case "QueryNative":
|
|
93
|
+
payload = encodeString(msg.query);
|
|
94
|
+
msgType = MSG_QUERY_NATIVE;
|
|
95
|
+
break;
|
|
96
|
+
case "QueryWithParamsNative":
|
|
97
|
+
payload = encodeQueryWithParams(msg.query, msg.params);
|
|
98
|
+
msgType = MSG_QUERY_PARAMS_NATIVE;
|
|
99
|
+
break;
|
|
100
|
+
case "QuerySqlNative":
|
|
101
|
+
payload = encodeString(msg.query);
|
|
102
|
+
msgType = MSG_QUERY_SQL_NATIVE;
|
|
103
|
+
break;
|
|
118
104
|
case "SyncStatus":
|
|
119
105
|
payload = encodeString(msg.replicaId);
|
|
120
106
|
msgType = MSG_SYNC_STATUS;
|
|
@@ -192,6 +178,23 @@ export function encode(msg) {
|
|
|
192
178
|
payload = encodeString(msg.value);
|
|
193
179
|
msgType = MSG_RESULT_SCALAR;
|
|
194
180
|
break;
|
|
181
|
+
case "ResultRowsNative": {
|
|
182
|
+
const parts = [u16LE(msg.columns.length)];
|
|
183
|
+
for (const column of msg.columns)
|
|
184
|
+
parts.push(encodeString(column));
|
|
185
|
+
parts.push(u32LE(msg.rows.length));
|
|
186
|
+
for (const row of msg.rows) {
|
|
187
|
+
for (const value of row)
|
|
188
|
+
parts.push(encodeWireValue(value));
|
|
189
|
+
}
|
|
190
|
+
payload = Buffer.concat(parts);
|
|
191
|
+
msgType = MSG_RESULT_ROWS_NATIVE;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "ResultScalarNative":
|
|
195
|
+
payload = encodeWireValue(msg.value);
|
|
196
|
+
msgType = MSG_RESULT_SCALAR_NATIVE;
|
|
197
|
+
break;
|
|
195
198
|
case "ResultOk": {
|
|
196
199
|
payload = Buffer.alloc(8);
|
|
197
200
|
payload.writeBigUInt64LE(msg.affected, 0);
|
|
@@ -226,6 +229,83 @@ export function encode(msg) {
|
|
|
226
229
|
payload.copy(frame, 6);
|
|
227
230
|
return frame;
|
|
228
231
|
}
|
|
232
|
+
function encodeQueryWithParams(query, params) {
|
|
233
|
+
const parts = [encodeString(query), u16LE(params.length)];
|
|
234
|
+
for (const param of params) {
|
|
235
|
+
switch (param.tag) {
|
|
236
|
+
case "null":
|
|
237
|
+
parts.push(Buffer.from([0]));
|
|
238
|
+
break;
|
|
239
|
+
case "int": {
|
|
240
|
+
const body = Buffer.alloc(9);
|
|
241
|
+
body.writeUInt8(1, 0);
|
|
242
|
+
body.writeBigInt64LE(param.value, 1);
|
|
243
|
+
parts.push(body);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "float": {
|
|
247
|
+
const body = Buffer.alloc(9);
|
|
248
|
+
body.writeUInt8(2, 0);
|
|
249
|
+
body.writeDoubleLE(param.value, 1);
|
|
250
|
+
parts.push(body);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "bool":
|
|
254
|
+
parts.push(Buffer.from([3, param.value ? 1 : 0]));
|
|
255
|
+
break;
|
|
256
|
+
case "str":
|
|
257
|
+
parts.push(Buffer.from([4]), encodeString(param.value));
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return Buffer.concat(parts);
|
|
262
|
+
}
|
|
263
|
+
function encodeWireValue(value) {
|
|
264
|
+
let tag;
|
|
265
|
+
let body;
|
|
266
|
+
switch (value.type) {
|
|
267
|
+
case "empty":
|
|
268
|
+
tag = 0;
|
|
269
|
+
body = Buffer.alloc(0);
|
|
270
|
+
break;
|
|
271
|
+
case "int":
|
|
272
|
+
case "datetime":
|
|
273
|
+
tag = value.type === "int" ? 1 : 5;
|
|
274
|
+
body = Buffer.alloc(8);
|
|
275
|
+
body.writeBigInt64LE(value.value, 0);
|
|
276
|
+
break;
|
|
277
|
+
case "float":
|
|
278
|
+
tag = 2;
|
|
279
|
+
body = Buffer.alloc(8);
|
|
280
|
+
body.writeDoubleLE(value.value, 0);
|
|
281
|
+
break;
|
|
282
|
+
case "bool":
|
|
283
|
+
tag = 3;
|
|
284
|
+
body = Buffer.from([value.value ? 1 : 0]);
|
|
285
|
+
break;
|
|
286
|
+
case "str":
|
|
287
|
+
tag = 4;
|
|
288
|
+
body = Buffer.from(value.value, "utf8");
|
|
289
|
+
break;
|
|
290
|
+
case "uuid":
|
|
291
|
+
if (value.value.length !== 16) {
|
|
292
|
+
throw new Error(`native UUID must be exactly 16 bytes, got ${value.value.length}`);
|
|
293
|
+
}
|
|
294
|
+
tag = 6;
|
|
295
|
+
body = Buffer.from(value.value);
|
|
296
|
+
break;
|
|
297
|
+
case "bytes":
|
|
298
|
+
tag = 7;
|
|
299
|
+
body = Buffer.from(value.value);
|
|
300
|
+
break;
|
|
301
|
+
case "json":
|
|
302
|
+
tag = 8;
|
|
303
|
+
body = Buffer.from(value.pj1);
|
|
304
|
+
decodePj1(body);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
return Buffer.concat([Buffer.from([tag]), u32LE(body.length), body]);
|
|
308
|
+
}
|
|
229
309
|
// ───── Decoding ────────────────────────────────────────────────────────────
|
|
230
310
|
/**
|
|
231
311
|
* Attempts to parse a single frame from the start of `buf`. Returns the parsed
|
|
@@ -305,6 +385,21 @@ function decodePayload(msgType, payload) {
|
|
|
305
385
|
}
|
|
306
386
|
return { type: "QueryWithParams", query, params };
|
|
307
387
|
}
|
|
388
|
+
case MSG_QUERY_NATIVE: {
|
|
389
|
+
const query = decodeStringStrict(payload, cursor, "native PowQL query");
|
|
390
|
+
requirePayloadEnd(payload, cursor, "native PowQL query");
|
|
391
|
+
return { type: "QueryNative", query };
|
|
392
|
+
}
|
|
393
|
+
case MSG_QUERY_PARAMS_NATIVE: {
|
|
394
|
+
const { query, params } = decodeNativeQueryWithParams(payload, cursor);
|
|
395
|
+
requirePayloadEnd(payload, cursor, "native parameterized query");
|
|
396
|
+
return { type: "QueryWithParamsNative", query, params };
|
|
397
|
+
}
|
|
398
|
+
case MSG_QUERY_SQL_NATIVE: {
|
|
399
|
+
const query = decodeStringStrict(payload, cursor, "native SQL query");
|
|
400
|
+
requirePayloadEnd(payload, cursor, "native SQL query");
|
|
401
|
+
return { type: "QuerySqlNative", query };
|
|
402
|
+
}
|
|
308
403
|
case MSG_SYNC_STATUS:
|
|
309
404
|
return { type: "SyncStatus", replicaId: decodeString(payload, cursor) };
|
|
310
405
|
case MSG_SYNC_PULL: {
|
|
@@ -402,6 +497,42 @@ function decodePayload(msgType, payload) {
|
|
|
402
497
|
}
|
|
403
498
|
case MSG_RESULT_SCALAR:
|
|
404
499
|
return { type: "ResultScalar", value: decodeString(payload, cursor) };
|
|
500
|
+
case MSG_RESULT_ROWS_NATIVE: {
|
|
501
|
+
const colCount = readU16(payload, cursor, "native column count");
|
|
502
|
+
if (colCount > MAX_COLUMNS) {
|
|
503
|
+
throw new Error(`too many columns: ${colCount} (max ${MAX_COLUMNS})`);
|
|
504
|
+
}
|
|
505
|
+
const columns = [];
|
|
506
|
+
for (let i = 0; i < colCount; i++) {
|
|
507
|
+
columns.push(decodeStringStrict(payload, cursor, "native column name"));
|
|
508
|
+
}
|
|
509
|
+
const rowCount = readU32(payload, cursor, "native row count");
|
|
510
|
+
if (rowCount > MAX_ROWS) {
|
|
511
|
+
throw new Error(`too many rows: ${rowCount} (max ${MAX_ROWS})`);
|
|
512
|
+
}
|
|
513
|
+
if (colCount === 0 && rowCount > 0) {
|
|
514
|
+
throw new Error("nonzero native row count with zero columns");
|
|
515
|
+
}
|
|
516
|
+
const minimumBytes = BigInt(rowCount) * BigInt(colCount) * 5n;
|
|
517
|
+
if (BigInt(payload.length - cursor.pos) < minimumBytes) {
|
|
518
|
+
throw new Error(`native row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
|
|
519
|
+
}
|
|
520
|
+
const rows = [];
|
|
521
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
522
|
+
const row = [];
|
|
523
|
+
for (let columnIndex = 0; columnIndex < colCount; columnIndex++) {
|
|
524
|
+
row.push(decodeWireValue(payload, cursor));
|
|
525
|
+
}
|
|
526
|
+
rows.push(row);
|
|
527
|
+
}
|
|
528
|
+
requirePayloadEnd(payload, cursor, "native rows");
|
|
529
|
+
return { type: "ResultRowsNative", columns, rows };
|
|
530
|
+
}
|
|
531
|
+
case MSG_RESULT_SCALAR_NATIVE: {
|
|
532
|
+
const value = decodeWireValue(payload, cursor);
|
|
533
|
+
requirePayloadEnd(payload, cursor, "native scalar");
|
|
534
|
+
return { type: "ResultScalarNative", value };
|
|
535
|
+
}
|
|
405
536
|
case MSG_RESULT_OK: {
|
|
406
537
|
const affected = readU64(payload, cursor, "affected count");
|
|
407
538
|
return { type: "ResultOk", affected };
|
|
@@ -420,6 +551,94 @@ function decodePayload(msgType, payload) {
|
|
|
420
551
|
throw new Error(`unknown message type: 0x${msgType.toString(16)}`);
|
|
421
552
|
}
|
|
422
553
|
}
|
|
554
|
+
function decodeNativeQueryWithParams(payload, cursor) {
|
|
555
|
+
const query = decodeStringStrict(payload, cursor, "native query");
|
|
556
|
+
const count = readU16(payload, cursor, "native param count");
|
|
557
|
+
if (count > MAX_PARAMS) {
|
|
558
|
+
throw new Error(`too many parameters: ${count} (max ${MAX_PARAMS})`);
|
|
559
|
+
}
|
|
560
|
+
if (count > payload.length - cursor.pos) {
|
|
561
|
+
throw new Error("parameter count exceeds payload size");
|
|
562
|
+
}
|
|
563
|
+
const params = [];
|
|
564
|
+
for (let index = 0; index < count; index++) {
|
|
565
|
+
const tag = readU8(payload, cursor, "param tag");
|
|
566
|
+
switch (tag) {
|
|
567
|
+
case 0:
|
|
568
|
+
params.push({ tag: "null" });
|
|
569
|
+
break;
|
|
570
|
+
case 1:
|
|
571
|
+
params.push({ tag: "int", value: readI64(payload, cursor, "int param") });
|
|
572
|
+
break;
|
|
573
|
+
case 2:
|
|
574
|
+
params.push({ tag: "float", value: readF64(payload, cursor, "float param") });
|
|
575
|
+
break;
|
|
576
|
+
case 3:
|
|
577
|
+
params.push({ tag: "bool", value: readBool(payload, cursor, "bool param") });
|
|
578
|
+
break;
|
|
579
|
+
case 4:
|
|
580
|
+
params.push({
|
|
581
|
+
tag: "str",
|
|
582
|
+
value: decodeStringStrict(payload, cursor, "string param"),
|
|
583
|
+
});
|
|
584
|
+
break;
|
|
585
|
+
default:
|
|
586
|
+
throw new Error(`unknown param tag: ${tag}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { query, params };
|
|
590
|
+
}
|
|
591
|
+
function decodeWireValue(buf, cursor) {
|
|
592
|
+
const tag = readU8(buf, cursor, "typed value tag");
|
|
593
|
+
const bodyLength = readU32(buf, cursor, "typed value body length");
|
|
594
|
+
const body = readFixedBytes(buf, cursor, bodyLength, "typed value body");
|
|
595
|
+
const requireLength = (expected, label) => {
|
|
596
|
+
if (body.length !== expected) {
|
|
597
|
+
throw new Error(`invalid ${label} typed value length: expected ${expected}, got ${body.length}`);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
switch (tag) {
|
|
601
|
+
case 0:
|
|
602
|
+
requireLength(0, "Empty");
|
|
603
|
+
return { type: "empty" };
|
|
604
|
+
case 1:
|
|
605
|
+
requireLength(8, "Int");
|
|
606
|
+
return { type: "int", value: body.readBigInt64LE(0) };
|
|
607
|
+
case 2: {
|
|
608
|
+
requireLength(8, "Float");
|
|
609
|
+
return { type: "float", value: body.readDoubleLE(0) };
|
|
610
|
+
}
|
|
611
|
+
case 3:
|
|
612
|
+
requireLength(1, "Bool");
|
|
613
|
+
if (body[0] !== 0 && body[0] !== 1) {
|
|
614
|
+
throw new Error(`invalid typed boolean: ${body[0]}`);
|
|
615
|
+
}
|
|
616
|
+
return { type: "bool", value: body[0] === 1 };
|
|
617
|
+
case 4:
|
|
618
|
+
return { type: "str", value: decodeUtf8Strict(body, "typed string") };
|
|
619
|
+
case 5:
|
|
620
|
+
requireLength(8, "DateTime");
|
|
621
|
+
return { type: "datetime", value: body.readBigInt64LE(0) };
|
|
622
|
+
case 6:
|
|
623
|
+
requireLength(16, "UUID");
|
|
624
|
+
return { type: "uuid", value: new Uint8Array(body) };
|
|
625
|
+
case 7:
|
|
626
|
+
return { type: "bytes", value: new Uint8Array(body) };
|
|
627
|
+
case 8:
|
|
628
|
+
return {
|
|
629
|
+
type: "json",
|
|
630
|
+
value: decodePj1(body),
|
|
631
|
+
pj1: new Uint8Array(body),
|
|
632
|
+
};
|
|
633
|
+
default:
|
|
634
|
+
throw new Error(`unknown typed value tag: ${tag}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function requirePayloadEnd(payload, cursor, label) {
|
|
638
|
+
if (cursor.pos !== payload.length) {
|
|
639
|
+
throw new Error(`trailing bytes in ${label} payload`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
423
642
|
function encodeSyncStatus(status) {
|
|
424
643
|
return Buffer.concat([
|
|
425
644
|
encodeString(status.replicaId),
|
|
@@ -546,6 +765,159 @@ function decodeString(buf, cursor) {
|
|
|
546
765
|
cursor.pos += len;
|
|
547
766
|
return s;
|
|
548
767
|
}
|
|
768
|
+
const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
769
|
+
function decodeUtf8Strict(bytes, label) {
|
|
770
|
+
try {
|
|
771
|
+
return strictUtf8.decode(bytes);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
throw new Error(`invalid UTF-8 in ${label}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function decodeStringStrict(buf, cursor, label) {
|
|
778
|
+
const len = readU32(buf, cursor, `${label} length`);
|
|
779
|
+
const bytes = readFixedBytes(buf, cursor, len, label);
|
|
780
|
+
return decodeUtf8Strict(bytes, label);
|
|
781
|
+
}
|
|
782
|
+
function decodePj1(doc) {
|
|
783
|
+
const [value, end] = decodePj1Node(doc, 0, 0);
|
|
784
|
+
if (end !== doc.length)
|
|
785
|
+
throw new Error("invalid typed PJ1 JSON: trailing bytes");
|
|
786
|
+
return value;
|
|
787
|
+
}
|
|
788
|
+
function decodePj1Node(doc, start, depth) {
|
|
789
|
+
if (depth > 128)
|
|
790
|
+
throw new Error("invalid typed PJ1 JSON: nesting too deep");
|
|
791
|
+
if (start >= doc.length)
|
|
792
|
+
throw new Error("invalid typed PJ1 JSON: truncated node");
|
|
793
|
+
const tag = doc[start];
|
|
794
|
+
switch (tag) {
|
|
795
|
+
case 0:
|
|
796
|
+
return [null, start + 1];
|
|
797
|
+
case 1:
|
|
798
|
+
return [false, start + 1];
|
|
799
|
+
case 2:
|
|
800
|
+
return [true, start + 1];
|
|
801
|
+
case 3: {
|
|
802
|
+
ensurePj1Range(doc, start + 1, 8);
|
|
803
|
+
const integer = doc.readBigInt64LE(start + 1);
|
|
804
|
+
const value = integer >= BigInt(Number.MIN_SAFE_INTEGER) &&
|
|
805
|
+
integer <= BigInt(Number.MAX_SAFE_INTEGER)
|
|
806
|
+
? Number(integer)
|
|
807
|
+
: integer;
|
|
808
|
+
return [value, start + 9];
|
|
809
|
+
}
|
|
810
|
+
case 4: {
|
|
811
|
+
ensurePj1Range(doc, start + 1, 8);
|
|
812
|
+
const value = doc.readDoubleLE(start + 1);
|
|
813
|
+
if (!Number.isFinite(value)) {
|
|
814
|
+
throw new Error("invalid typed PJ1 JSON: non-finite float");
|
|
815
|
+
}
|
|
816
|
+
return [value, start + 9];
|
|
817
|
+
}
|
|
818
|
+
case 5: {
|
|
819
|
+
const [value, end] = decodePj1String(doc, start + 1);
|
|
820
|
+
return [value, end];
|
|
821
|
+
}
|
|
822
|
+
case 6:
|
|
823
|
+
return decodePj1Array(doc, start, depth);
|
|
824
|
+
case 7:
|
|
825
|
+
return decodePj1Object(doc, start, depth);
|
|
826
|
+
default:
|
|
827
|
+
throw new Error(`invalid typed PJ1 JSON: reserved/invalid tag ${tag}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function decodePj1Array(doc, start, depth) {
|
|
831
|
+
const count = pj1U32(doc, start + 1);
|
|
832
|
+
const headerSize = 5 + 4 * (count + 1);
|
|
833
|
+
ensurePj1Range(doc, start, headerSize);
|
|
834
|
+
let expectedOffset = headerSize;
|
|
835
|
+
const values = [];
|
|
836
|
+
for (let index = 0; index < count; index++) {
|
|
837
|
+
const offset = pj1U32(doc, start + 5 + index * 4);
|
|
838
|
+
const nextOffset = pj1U32(doc, start + 5 + (index + 1) * 4);
|
|
839
|
+
if (offset !== expectedOffset || nextOffset < offset) {
|
|
840
|
+
throw new Error("invalid typed PJ1 JSON: array offsets not canonical");
|
|
841
|
+
}
|
|
842
|
+
const [value, end] = decodePj1Node(doc, start + offset, depth + 1);
|
|
843
|
+
if (end !== start + nextOffset) {
|
|
844
|
+
throw new Error("invalid typed PJ1 JSON: array element length mismatch");
|
|
845
|
+
}
|
|
846
|
+
values.push(value);
|
|
847
|
+
expectedOffset = nextOffset;
|
|
848
|
+
}
|
|
849
|
+
const total = pj1U32(doc, start + 5 + count * 4);
|
|
850
|
+
if (total !== expectedOffset) {
|
|
851
|
+
throw new Error("invalid typed PJ1 JSON: array end offset mismatch");
|
|
852
|
+
}
|
|
853
|
+
ensurePj1Range(doc, start, total);
|
|
854
|
+
return [values, start + total];
|
|
855
|
+
}
|
|
856
|
+
function decodePj1Object(doc, start, depth) {
|
|
857
|
+
const count = pj1U32(doc, start + 1);
|
|
858
|
+
const headerSize = 5 + count * 8 + 4;
|
|
859
|
+
ensurePj1Range(doc, start, headerSize);
|
|
860
|
+
const keys = [];
|
|
861
|
+
let expectedKeyOffset = headerSize;
|
|
862
|
+
for (let index = 0; index < count; index++) {
|
|
863
|
+
const keyOffset = pj1U32(doc, start + 5 + index * 8);
|
|
864
|
+
if (keyOffset !== expectedKeyOffset) {
|
|
865
|
+
throw new Error("invalid typed PJ1 JSON: object key offset mismatch");
|
|
866
|
+
}
|
|
867
|
+
const [text, end, bytes] = decodePj1String(doc, start + keyOffset);
|
|
868
|
+
const previous = keys[keys.length - 1];
|
|
869
|
+
if (previous && Buffer.compare(previous.bytes, bytes) >= 0) {
|
|
870
|
+
throw new Error("invalid typed PJ1 JSON: object keys not strictly sorted");
|
|
871
|
+
}
|
|
872
|
+
keys.push({ text, bytes });
|
|
873
|
+
expectedKeyOffset = end - start;
|
|
874
|
+
}
|
|
875
|
+
const result = {};
|
|
876
|
+
let expectedValueOffset = expectedKeyOffset;
|
|
877
|
+
for (let index = 0; index < count; index++) {
|
|
878
|
+
const valueOffset = pj1U32(doc, start + 5 + index * 8 + 4);
|
|
879
|
+
if (valueOffset !== expectedValueOffset) {
|
|
880
|
+
throw new Error("invalid typed PJ1 JSON: object value offset mismatch");
|
|
881
|
+
}
|
|
882
|
+
const [value, end] = decodePj1Node(doc, start + valueOffset, depth + 1);
|
|
883
|
+
const key = keys[index];
|
|
884
|
+
if (!key)
|
|
885
|
+
throw new Error("invalid typed PJ1 JSON: missing object key");
|
|
886
|
+
Object.defineProperty(result, key.text, {
|
|
887
|
+
value,
|
|
888
|
+
enumerable: true,
|
|
889
|
+
configurable: true,
|
|
890
|
+
writable: true,
|
|
891
|
+
});
|
|
892
|
+
expectedValueOffset = end - start;
|
|
893
|
+
}
|
|
894
|
+
const total = pj1U32(doc, start + 5 + count * 8);
|
|
895
|
+
if (total !== expectedValueOffset) {
|
|
896
|
+
throw new Error("invalid typed PJ1 JSON: object end offset mismatch");
|
|
897
|
+
}
|
|
898
|
+
ensurePj1Range(doc, start, total);
|
|
899
|
+
return [result, start + total];
|
|
900
|
+
}
|
|
901
|
+
function decodePj1String(doc, lengthOffset) {
|
|
902
|
+
const len = pj1U32(doc, lengthOffset);
|
|
903
|
+
const dataStart = lengthOffset + 4;
|
|
904
|
+
ensurePj1Range(doc, dataStart, len);
|
|
905
|
+
const bytes = doc.subarray(dataStart, dataStart + len);
|
|
906
|
+
return [decodeUtf8Strict(bytes, "PJ1 string"), dataStart + len, bytes];
|
|
907
|
+
}
|
|
908
|
+
function pj1U32(doc, offset) {
|
|
909
|
+
ensurePj1Range(doc, offset, 4);
|
|
910
|
+
return doc.readUInt32LE(offset);
|
|
911
|
+
}
|
|
912
|
+
function ensurePj1Range(doc, offset, len) {
|
|
913
|
+
if (!Number.isSafeInteger(offset) ||
|
|
914
|
+
!Number.isSafeInteger(len) ||
|
|
915
|
+
offset < 0 ||
|
|
916
|
+
len < 0 ||
|
|
917
|
+
offset + len > doc.length) {
|
|
918
|
+
throw new Error("invalid typed PJ1 JSON: truncated or overflowing range");
|
|
919
|
+
}
|
|
920
|
+
}
|
|
549
921
|
function readU8(buf, cursor, label) {
|
|
550
922
|
if (cursor.pos + 1 > buf.length) {
|
|
551
923
|
throw new Error(`truncated ${label}`);
|