@tai-kun/surrealdb
Version:
The SurrealDB SDK for JavaScript
1,010 lines (898 loc) • 28 kB
text/typescript
import {
CborMaxDepthReachedError,
CborUnsafeMapKeyError,
CircularReferenceError,
NumberRangeError,
SurrealTypeError,
SurrealValueError,
unreachable,
} from "@tai-kun/surrealdb/errors";
import type { Uint8ArrayLike } from "@tai-kun/surrealdb/types";
import { isPlainObject, utf8 } from "@tai-kun/surrealdb/utils";
import { ianaReplacer } from "./_iana";
import {
AI_EIGHT_BYTES,
AI_FOUR_BYTES,
AI_ONE_BYTE,
AI_TWO_BYTES,
CBOR_MAX_UNSIGNED_INTEGER,
CBOR_MIN_NEGATIVE_INTEGER,
HEADER_FALSE,
HEADER_FLOAT_DOUBLE,
HEADER_FLOAT_HALF,
HEADER_NULL,
HEADER_TRUE,
JS_MAX_SAFE_UNSIGNED_INTEGER,
type MajorType,
MT_BYTE_STRING,
Simple,
} from "./spec";
import { canToCBOR, type ToCBOR } from "./traits";
import type { Writer } from "./writer";
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writeheader)
*/
export function writeHeader(
w: Writer,
mt: MajorType,
length: number | bigint,
): void {
switch (true) {
case length < AI_ONE_BYTE:
w.writeUint8((mt << 5) | Number(length));
break;
case length <= 0xff:
w.writeUint8((mt << 5) | AI_ONE_BYTE);
w.writeUint8(Number(length));
break;
case length <= 0xffff:
w.writeUint8((mt << 5) | AI_TWO_BYTES);
w.writeUint16(Number(length));
break;
case length <= 0xffffffff:
w.writeUint8((mt << 5) | AI_FOUR_BYTES);
w.writeUint32(Number(length));
break;
default:
w.writeUint8((mt << 5) | AI_EIGHT_BYTES);
w.writeBigUint64(BigInt(length));
}
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writepayload)
*/
export function writePayload(
w: Writer,
value: Uint8ArrayLike,
): void {
w.writeBytes(value);
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writebytestring)
*/
export function writeByteString(
w: Writer,
value: Uint8ArrayLike,
): void {
writeHeader(w, MT_BYTE_STRING, value.length);
writePayload(w, value);
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writeutf8string)
*/
export function writeUtf8String(w: Writer, value: string): void {
let l = value.length;
if (l <= 0) {
w.claim(1);
w.data[w.offset++] = 96; // 120 - 1 - 23
return;
}
switch (l) {
case 2:
switch (value) {
case "ns":
w.claim(3);
w.data[w.offset++] = 98; // 96 + 2
w.data.set([0x6E, 0x73], w.offset);
w.offset += 2;
return;
case "db":
w.claim(3);
w.data[w.offset++] = 98; // 96 + 2
w.data.set([0x64, 0x62], w.offset);
w.offset += 2;
return;
case "ac":
w.claim(3);
w.data[w.offset++] = 98; // 96 + 2
w.data.set([0x61, 0x63], w.offset);
w.offset += 2;
return;
}
break;
case 3:
switch (value) {
case "use":
w.claim(4);
w.data[w.offset++] = 99; // 96 + 3
w.data.set([0x75, 0x73, 0x65], w.offset);
w.offset += 3;
return;
case "let":
w.claim(4);
w.data[w.offset++] = 99; // 96 + 3
w.data.set([0x6C, 0x65, 0x74], w.offset);
w.offset += 3;
return;
case "run":
w.claim(4);
w.data[w.offset++] = 99; // 96 + 3
w.data.set([0x72, 0x75, 0x6E], w.offset);
w.offset += 3;
return;
}
break;
case 4:
switch (value) {
case "user":
w.claim(5);
w.data[w.offset++] = 100; // 96 + 4
w.data.set([0x75, 0x73, 0x65, 0x72], w.offset);
w.offset += 4;
return;
case "pass":
w.claim(5);
w.data[w.offset++] = 100; // 96 + 4
w.data.set([0x70, 0x61, 0x73, 0x73], w.offset);
w.offset += 4;
return;
case "ping":
w.claim(5);
w.data[w.offset++] = 100; // 96 + 4
w.data.set([0x70, 0x69, 0x6E, 0x67], w.offset);
w.offset += 4;
return;
case "info":
w.claim(5);
w.data[w.offset++] = 100; // 96 + 4
w.data.set([0x69, 0x6E, 0x66, 0x6F], w.offset);
w.offset += 4;
return;
case "live":
w.claim(5);
w.data[w.offset++] = 100; // 96 + 4
w.data.set([0x6C, 0x69, 0x76, 0x65], w.offset);
w.offset += 4;
return;
case "kill":
w.claim(5);
w.data[w.offset++] = 100; // 96 + 4
w.data.set([0x6B, 0x69, 0x6C, 0x6C], w.offset);
w.offset += 4;
return;
}
break;
case 5:
switch (value) {
case "unset":
w.claim(6);
w.data[w.offset++] = 101; // 96 + 5
w.data.set([0x75, 0x6E, 0x73, 0x65, 0x74], w.offset);
w.offset += 5;
return;
case "query":
w.claim(6);
w.data[w.offset++] = 101; // 96 + 5
w.data.set([0x71, 0x75, 0x65, 0x72, 0x79], w.offset);
w.offset += 5;
return;
case "merge":
w.claim(6);
w.data[w.offset++] = 101; // 96 + 5
w.data.set([0x6D, 0x65, 0x72, 0x67, 0x65], w.offset);
w.offset += 5;
return;
case "patch":
w.claim(6);
w.data[w.offset++] = 101; // 96 + 5
w.data.set([0x70, 0x61, 0x74, 0x63, 0x68], w.offset);
w.offset += 5;
return;
}
break;
case 6:
switch (value) {
case "method":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x6D, 0x65, 0x74, 0x68, 0x6F, 0x64], w.offset);
w.offset += 6;
return;
case "params":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x70, 0x61, 0x72, 0x61, 0x6D, 0x73], w.offset);
w.offset += 6;
return;
case "signup":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x73, 0x69, 0x67, 0x6E, 0x75, 0x70], w.offset);
w.offset += 6;
return;
case "signin":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x73, 0x69, 0x67, 0x6E, 0x69, 0x6E], w.offset);
w.offset += 6;
return;
case "select":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x73, 0x65, 0x6C, 0x65, 0x63, 0x74], w.offset);
w.offset += 6;
return;
case "create":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x63, 0x72, 0x65, 0x61, 0x74, 0x65], w.offset);
w.offset += 6;
return;
case "insert":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x69, 0x6E, 0x73, 0x65, 0x72, 0x74], w.offset);
w.offset += 6;
return;
case "update":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x75, 0x70, 0x64, 0x61, 0x74, 0x65], w.offset);
w.offset += 6;
return;
case "upsert":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x75, 0x70, 0x73, 0x65, 0x72, 0x74], w.offset);
w.offset += 6;
return;
case "delete":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x64, 0x65, 0x6C, 0x65, 0x74, 0x65], w.offset);
w.offset += 6;
return;
case "relate":
w.claim(7);
w.data[w.offset++] = 102; // 96 + 6
w.data.set([0x72, 0x65, 0x6C, 0x61, 0x74, 0x65], w.offset);
w.offset += 6;
return;
}
break;
case 7:
switch (value) {
case "version":
w.claim(8);
w.data[w.offset++] = 103; // 96 + 7
w.data.set([0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E], w.offset);
w.offset += 7;
return;
}
break;
case 10:
switch (value) {
case "invalidate":
w.claim(11);
w.data[w.offset++] = 106; // 96 + 10
w.data.set([
0x69,
0x6E,
0x76,
0x61,
0x6C,
0x69,
0x64,
0x61,
0x74,
0x65,
], w.offset);
w.offset += 10;
return;
}
break;
case 12:
switch (value) {
case "authenticate":
w.claim(13);
w.data[w.offset++] = 108; // 96 + 12
w.data.set([
0x61,
0x75,
0x74,
0x68,
0x65,
0x6E,
0x74,
0x69,
0x63,
0x61,
0x74,
0x65,
], w.offset);
w.offset += 12;
return;
}
break;
}
// パフォーマンスのために `.encodeInto` を使ってバッファーに直接書き込む。
// 文字列を UTF-8 でエンコードするとき、必要なバッファーのサイズは `.length` バイト以上
// `.length * 3` バイト以下である [^1]。さらに CBOR のヘッダーを書き込むために最大 5 バイトの
// 空き容量が追加で必要となる。つまりメモリーの空き容量が `.length * 3 + 5` バイト以上であれば
// `.encodeInto` を使うことができる。
// [^1]: https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto
if ((w.data.length - w.offset) >= (5 + l + l + l)) {
const r = utf8.encodeInto(value, w.data.subarray(w.offset + 5));
if (r.written < AI_ONE_BYTE) {
w.data[w.offset++] = 96 + r.written; // (120 - 1 - 23) + value
l = 4;
} else if (r.written <= 0xff) {
w.data[w.offset++] = 120; // (MT_UTF8_STRING << 5) | AI_ONE_BYTE
w.data[w.offset++] = r.written;
l = 3;
} else if (r.written <= 0xffff) {
w.data[w.offset++] = 121; // (MT_UTF8_STRING << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, r.written);
w.offset += 2;
l = 2;
} else { // else if (r.written <= 2147483647 < 0xffffffff)
w.data[w.offset++] = 122; // (MT_UTF8_STRING << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, r.written);
w.offset += 4;
l = 0;
}
if (l > 0) {
w.data.copyWithin(w.offset, l += w.offset, l + r.written);
}
w.offset += r.written;
} else {
const bytes = utf8.encode(value);
writeEncodedUtf8String(w, bytes);
}
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writeencodedutf8string)
*/
export function writeEncodedUtf8String(
w: Writer,
value: Uint8ArrayLike,
): void {
const l = value.length;
if (l <= 0) {
w.claim(1);
w.data[w.offset++] = 96; // 120 - 1 - 23
return;
}
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (l < AI_ONE_BYTE) {
w.data[w.offset++] = 96 + l; // (120 - 1 - 23) + l
} else if (l <= 0xff) {
w.data[w.offset++] = 120; // (MT_UTF8_STRING << 5) | AI_ONE_BYTE
w.data[w.offset++] = l;
} else if (l <= 0xffff) {
w.data[w.offset++] = 121; // (MT_UTF8_STRING << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, l);
w.offset += 2;
} else if (l <= 0xffffffff) {
w.data[w.offset++] = 122; // (MT_UTF8_STRING << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, l);
w.offset += 4;
} else if (l <= JS_MAX_SAFE_UNSIGNED_INTEGER) {
w.data[w.offset++] = 123; // (MT_UTF8_STRING << 5) | AI_EIGHT_BYTES
w.view.setBigUint64(w.offset, BigInt(l));
w.offset += 8;
} else {
throw new NumberRangeError([0, JS_MAX_SAFE_UNSIGNED_INTEGER], l);
}
// エンコードした文字列が JavaScript 以外でデコードされる可能性を考えると、JavaScript の制約に
// したがった文字列長の検証をする必要はないかもしれない。
// } else if (l <= 6442450941) { // (2^31-1)*3
// w.data[w.offset++] = 123; // (MT_UTF8_STRING << 5) | AI_EIGHT_BYTES
// w.view.setBigUint64(w.offset, BigInt(l));
// w.offset += 8;
// } else {
// throw new NumberRangeError([0, 6442450941], l);
// }
w.writeBytes(value);
}
function writeUndefined(w: Writer): void {
// 仕様では HEADER_UNDEFINED を書き込むほうが正しいと思われるが、SurrealDB は undefined と
// NONE が同値でもタグ付きデータアイテムで NONE を表現する必要がある。
// w.writeUint8(HEADER_UNDEFINED);
// https://github.com/surrealdb/surrealdb/blob/v2.0.1/core/src/rpc/format/cbor/convert.rs#L30
// より、NONE のタグは 6
w.claim(2); // 1 (header) + 1 (`null` payload)
w.data[w.offset++] = 198; // 192 + 6 = 198
w.data[w.offset++] = HEADER_NULL;
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writenullable)
*/
export function writeNullable(w: Writer, value: null | undefined): void {
if (value === null) {
w.writeUint8(HEADER_NULL);
} else {
writeUndefined(w);
}
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writeboolean)
*/
export function writeBoolean(w: Writer, value: boolean): void {
if (value) {
w.writeUint8(HEADER_TRUE);
} else {
w.writeUint8(HEADER_FALSE);
}
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#writenumber)
*/
export function writeNumber(w: Writer, value: number): void {
if (Number.isInteger(value)) {
if (value >= 0) {
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (value < AI_ONE_BYTE) {
w.data[w.offset++] = value;
} else if (value <= 0xff) {
w.data[w.offset++] = AI_ONE_BYTE;
w.data[w.offset++] = value;
} else if (value <= 0xffff) {
w.data[w.offset++] = AI_TWO_BYTES;
w.view.setUint16(w.offset, value);
w.offset += 2;
} else if (value <= 0xffffffff) {
w.data[w.offset++] = AI_FOUR_BYTES;
w.view.setUint32(w.offset, value);
w.offset += 4;
} else if (value <= JS_MAX_SAFE_UNSIGNED_INTEGER) {
w.data[w.offset++] = AI_EIGHT_BYTES;
w.view.setBigUint64(w.offset, BigInt(value));
w.offset += 8;
} else {
throw new NumberRangeError(
[
-JS_MAX_SAFE_UNSIGNED_INTEGER,
JS_MAX_SAFE_UNSIGNED_INTEGER,
],
value,
);
}
} else {
const ui = -value - 1;
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (ui < AI_ONE_BYTE) {
w.data[w.offset++] = 32 + ui; // value = -24 -> ui = 23 -> 32 + 23 = 55
} else if (ui <= 0xff) {
w.data[w.offset++] = 56; // (MT_NEGATIVE_INTEGER << 5) | AI_ONE_BYTE
w.data[w.offset++] = ui;
} else if (ui <= 0xffff) {
w.data[w.offset++] = 57; // (MT_NEGATIVE_INTEGER << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, ui);
w.offset += 2;
} else if (ui <= 0xffffffff) {
w.data[w.offset++] = 58; // (MT_NEGATIVE_INTEGER << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, ui);
w.offset += 4;
} else if (ui <= JS_MAX_SAFE_UNSIGNED_INTEGER) {
w.data[w.offset++] = 59; // (MT_NEGATIVE_INTEGER << 5) | AI_EIGHT_BYTES
w.view.setBigUint64(w.offset, BigInt(ui));
w.offset += 8;
} else {
throw new NumberRangeError(
[
-JS_MAX_SAFE_UNSIGNED_INTEGER,
JS_MAX_SAFE_UNSIGNED_INTEGER,
],
value,
);
}
}
} else if (value === Infinity) {
w.claim(3); // 1 (header) + 2 (16-bit payload)
w.data[w.offset++] = HEADER_FLOAT_HALF;
w.data[w.offset++] = 0x7c;
w.data[w.offset++] = 0x00;
} else if (value === -Infinity) {
w.claim(3); // 1 (header) + 2 (16-bit payload)
w.data[w.offset++] = HEADER_FLOAT_HALF;
w.data[w.offset++] = 0xfc;
w.data[w.offset++] = 0x00;
} else if (value === value) {
w.claim(9); // 1 (header) + 8 (64-bit payload)
w.data[w.offset++] = HEADER_FLOAT_DOUBLE;
w.view.setFloat64(w.offset, value);
w.offset += 8;
} else {
w.claim(3); // 1 (header) + 2 (16-bit payload)
w.data[w.offset++] = HEADER_FLOAT_HALF;
w.data[w.offset++] = 0x7e;
w.data[w.offset++] = 0x00;
}
}
export function writeBigInt(w: Writer, value: bigint): void {
if (value >= 0) {
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (value < AI_ONE_BYTE) {
w.data[w.offset++] = Number(value);
} else if (value <= 0xff) {
w.data[w.offset++] = AI_ONE_BYTE;
w.data[w.offset++] = Number(value);
} else if (value <= 0xffff) {
w.data[w.offset++] = AI_TWO_BYTES;
w.view.setUint16(w.offset, Number(value));
w.offset += 2;
} else if (value <= 0xffffffff) {
w.data[w.offset++] = AI_FOUR_BYTES;
w.view.setUint32(w.offset, Number(value));
w.offset += 4;
} else if (value <= CBOR_MAX_UNSIGNED_INTEGER) {
w.data[w.offset++] = AI_EIGHT_BYTES;
w.view.setBigUint64(w.offset, value);
w.offset += 8;
} else {
throw new NumberRangeError(
[
CBOR_MIN_NEGATIVE_INTEGER,
CBOR_MAX_UNSIGNED_INTEGER,
],
value,
);
}
} else {
const ui = -value - 1n;
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (ui < AI_ONE_BYTE) {
w.data[w.offset++] = 32 + Number(ui); // value = -24 -> ui = 23 -> 32 + 23 = 55
} else if (ui <= 0xff) {
w.data[w.offset++] = 56; // (MT_NEGATIVE_INTEGER << 5) | AI_ONE_BYTE
w.data[w.offset++] = Number(ui);
} else if (ui <= 0xffff) {
w.data[w.offset++] = 57; // (MT_NEGATIVE_INTEGER << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, Number(ui));
w.offset += 2;
} else if (ui <= 0xffffffff) {
w.data[w.offset++] = 58; // (MT_NEGATIVE_INTEGER << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, Number(ui));
w.offset += 4;
} else if (ui <= CBOR_MAX_UNSIGNED_INTEGER) {
w.data[w.offset++] = 59; // (MT_NEGATIVE_INTEGER << 5) | AI_EIGHT_BYTES
w.view.setBigUint64(w.offset, ui);
w.offset += 8;
} else {
throw new NumberRangeError(
[
CBOR_MIN_NEGATIVE_INTEGER,
CBOR_MAX_UNSIGNED_INTEGER,
],
value,
);
}
}
}
/**
* @internal
*/
function writeArrayHeader(w: Writer, length: number): void {
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (length < AI_ONE_BYTE) {
w.data[w.offset++] = 128 + length; // 128 + 23 = 151
} else if (length <= 0xff) {
w.data[w.offset++] = 152; // (MT_ARRAY << 5) | AI_ONE_BYTE
w.data[w.offset++] = length;
} else if (length <= 0xffff) {
w.data[w.offset++] = 153; // (MT_ARRAY << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, length);
w.offset += 2;
} else if (length <= 0xffffffff) {
w.data[w.offset++] = 154; // (MT_ARRAY << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, length);
w.offset += 4;
} else if (length <= JS_MAX_SAFE_UNSIGNED_INTEGER) {
w.data[w.offset++] = 155; // (MT_ARRAY << 5) | AI_EIGHT_BYTES
w.view.setBigUint64(w.offset, BigInt(length));
w.offset += 8;
} else {
throw new NumberRangeError([0, JS_MAX_SAFE_UNSIGNED_INTEGER], length);
}
}
/**
* @internal
*/
function writeMapHeader(w: Writer, length: number): void {
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (length < AI_ONE_BYTE) {
w.data[w.offset++] = 160 + length; // 160 + 23 = 183
} else if (length <= 0xff) {
w.data[w.offset++] = 184; // (MT_MAP << 5) | AI_ONE_BYTE
w.data[w.offset++] = length;
} else if (length <= 0xffff) {
w.data[w.offset++] = 185; // (MT_MAP << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, length);
w.offset += 2;
} else if (length <= 0xffffffff) {
w.data[w.offset++] = 186; // (MT_MAP << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, length);
w.offset += 4;
} else if (length <= JS_MAX_SAFE_UNSIGNED_INTEGER) {
w.data[w.offset++] = 187; // (MT_MAP << 5) | AI_EIGHT_BYTES
w.view.setBigUint64(w.offset, BigInt(length));
w.offset += 8;
} else {
throw new NumberRangeError([0, JS_MAX_SAFE_UNSIGNED_INTEGER], length);
}
}
/**
* @internal
*/
function writeTagHeader(w: Writer, tag: number | bigint): void {
w.claim(9); // 1 (header) + 8 (64-bit payload)
if (tag < AI_ONE_BYTE) {
w.data[w.offset++] = 192 + Number(tag); // 192 + 23 = 215
} else if (tag <= 0xff) {
w.data[w.offset++] = 216; // (MT_TAG << 5) | AI_ONE_BYTE
w.data[w.offset++] = Number(tag);
} else if (tag <= 0xffff) {
w.data[w.offset++] = 217; // (MT_TAG << 5) | AI_TWO_BYTES
w.view.setUint16(w.offset, Number(tag));
w.offset += 2;
} else if (tag <= 0xffffffff) {
w.data[w.offset++] = 218; // (MT_TAG << 5) | AI_FOUR_BYTES
w.view.setUint32(w.offset, Number(tag));
w.offset += 4;
} else if (tag <= JS_MAX_SAFE_UNSIGNED_INTEGER) {
w.data[w.offset++] = 219; // (MT_TAG << 5) | AI_EIGHT_BYTES
w.view.setBigUint64(w.offset, BigInt(tag));
w.offset += 8;
} else {
throw new NumberRangeError([0, CBOR_MAX_UNSIGNED_INTEGER], tag);
}
}
/**
* @internal
*/
function writeSimpleHeader(w: Writer, value: number): void {
w.claim(3); // 1 (header) + 2
if (value < AI_ONE_BYTE) {
w.data[w.offset++] = 224 + value; // 128 + 23 = 247
} else if (value <= 0xff) {
w.data[w.offset++] = 248; // (MT_SIMPLE_FLOAT << 5) | AI_ONE_BYTE
w.data[w.offset++] = value;
} else {
throw new NumberRangeError([0, 0xff], value);
}
}
const PARENT_SET = 0;
const PARENT_MAP = 1;
const PARENT_OBJ = 2;
const PARENT_TAG = 3;
type Parent = {
$: typeof PARENT_SET;
// seen: Set<object>;
value: readonly unknown[] | ReadonlySet<unknown>;
index: number;
length: number;
target: readonly unknown[];
} | {
$: typeof PARENT_MAP;
// seen: Set<object>;
value: ReadonlyMap<unknown, unknown>;
index: number;
length: number;
isProperty: boolean;
properties: readonly unknown[];
} | {
$: typeof PARENT_OBJ;
// seen: Set<object>;
value: { readonly [p: string]: unknown };
index: number;
length: number;
isProperty: boolean;
properties: readonly string[];
} | {
$: typeof PARENT_TAG;
value: ToCBOR;
};
const CONTINUE = Symbol.for("@tai-kun/surrealdb/cbor/continue"); // decorder.ts と同じ
export type Replacer = (value: symbol | object) => unknown | typeof CONTINUE;
export type IsSafeMapKey = (
key: unknown,
map: ReadonlyMap<unknown, unknown>,
) => boolean;
export type IsSafeObjectKey = (
key: string | number,
obj: { readonly [p: string]: unknown },
) => boolean;
export interface WriteOptions {
readonly isSafeMapKey?: IsSafeMapKey | undefined;
readonly isSafeObjectKey?: IsSafeObjectKey | undefined;
readonly replacer?: Replacer | readonly Replacer[] | undefined;
}
/**
* [API Reference](https://tai-kun.github.io/surrealdb.js/v2/api/cbor/others/#write)
*/
export function write(
w: Writer,
value: unknown,
options: WriteOptions | undefined = {},
): void {
const {
replacer = [],
isSafeMapKey = k => k !== "__proto__" && k !== "constructor",
isSafeObjectKey = k => k !== "__proto__" && k !== "constructor",
} = options;
const replacers: Replacer[] = typeof replacer === "function"
? [replacer, ianaReplacer]
: replacer.concat(ianaReplacer);
let parent: Parent | undefined;
const parents: Parent[] = [];
const seen = new Set<{}>();
function begin(p: Parent): void {
if (p.$ !== PARENT_TAG && ++w.depth >= w.maxDepth) {
throw new CborMaxDepthReachedError(w.maxDepth);
}
seen.add(p.value);
parents.push(parent = p);
}
let kind;
while (true) {
if (value === null) {
w.writeUint8(HEADER_NULL);
} else if (value === undefined) {
writeUndefined(w);
} else if (value === true) {
w.writeUint8(HEADER_TRUE);
} else if (value === false) {
w.writeUint8(HEADER_FALSE);
} else if ((kind = typeof value) === "string") {
writeUtf8String(w, value as string);
} else if (kind === "number") {
writeNumber(w, value as number);
} else if (kind === "bigint") {
writeBigInt(w, value as bigint);
} else if (seen.has(value)) {
throw new CircularReferenceError(value);
} else if (canToCBOR(value)) {
const cbor = value.toCBOR(w);
if (Array.isArray(cbor)) {
begin({
$: PARENT_TAG,
value,
});
if (cbor.length === 2) {
writeTagHeader(w, cbor[0]);
value = cbor[1];
} else if (cbor.length === 1) {
value = cbor[0];
} else {
throw new SurrealValueError(
"an array of length 1 or 2",
`an array of length ${(cbor as any[]).length}`,
);
}
continue;
}
} else if (isPlainObject(value)) {
const keys = Object.keys(value);
const length = keys.length;
writeMapHeader(w, length);
if (length > 0) {
begin({
$: PARENT_OBJ,
value,
index: 0,
length,
isProperty: true,
properties: keys,
});
}
} else if (Array.isArray(value) || value instanceof Set) {
const target = Array.isArray(value) ? value : Array.from(value);
const length = target.length;
writeArrayHeader(w, length);
if (length > 0) {
begin({
$: PARENT_SET,
value,
index: 0,
length,
target,
});
}
} else if (value instanceof Map) {
const keys = Array.from(value.keys());
const length = keys.length;
writeMapHeader(w, length);
if (length > 0) {
begin({
$: PARENT_MAP,
value,
index: 0,
length,
isProperty: true,
properties: keys,
});
}
} else if (value instanceof Uint8Array) {
writeByteString(w, value);
} else if (value instanceof Simple) {
writeSimpleHeader(w, value.value);
} else {
let replaced = false;
for (let i = 0, ret; i < replacers.length; i++) {
if ((ret = replacers[i]!(value)) !== CONTINUE) {
value = ret;
replaced = true;
break;
}
}
if (replaced) {
continue;
}
throw new SurrealTypeError(
[
"BigInt",
"Boolean",
"null",
"Number",
"Object",
"String",
"undefined",
],
value,
);
}
// end()
// .length の比較は === で行うこと。
while (
parent
&& (parent.$ === PARENT_TAG || parent.index === parent.length)
) {
if (parent.$ !== PARENT_TAG) {
w.depth -= 1;
}
seen.delete(parent.value);
parents.pop();
// parents が空の場合、loop には初期値と同じ undefined が設定される。
parent = parents[parents.length - 1];
}
if (!parent) {
break;
}
if (parent.$ === PARENT_OBJ) {
if (parent.isProperty) {
const key = value = parent.properties[parent.index]!;
if (!isSafeObjectKey(key, parent.value)) {
throw new CborUnsafeMapKeyError(value);
}
} else {
value = parent.value[parent.properties[parent.index++]!];
}
parent.isProperty = !parent.isProperty;
} else if (parent.$ === PARENT_SET) {
value = parent.target[parent.index++];
} else if (parent.$ === PARENT_MAP) {
if (parent.isProperty) {
value = parent.properties[parent.index];
if (!isSafeMapKey(value, parent.value)) {
throw new CborUnsafeMapKeyError(value);
}
} else {
value = parent.value.get(parent.properties[parent.index++]);
}
parent.isProperty = !parent.isProperty;
} else {
unreachable(parent);
}
}
}