UNPKG

@cloudpss/ubjson

Version:

198 lines (190 loc) 8.13 kB
import { constants } from '../helper/constants.js'; import { type EncodeCursor, I8_MASK, type TypedArrayType, writeNumber, writeTypedArray, writeTypedArrayData, writeTypedArrayHeader, } from '../helper/encode.js'; import { unsupportedType } from '../helper/errors.js'; import { stringByteLength, encodeInto } from '../helper/string-encoder.js'; const LARGE_DATA_LENGTH = 65536; const { isArray } = Array; // eslint-disable-next-line @typescript-eslint/unbound-method const { isView } = ArrayBuffer; const { keys: objectKeys } = Object; /** 编码至 ubjson */ export abstract class EncoderBase { /** 序列化对象时排序属性 */ sortObjectKeys = false; /** 当前写指针位置 */ protected length = 0; /** 数据 */ protected data!: Uint8Array<ArrayBuffer>; /** buffer 的 DataView */ protected view!: DataView; /** * 确保 buffer 还有 capacity 的空闲空间 */ protected abstract ensureCapacity(capacity: number): void; /** 编码至 ubjson,对于 `undefined` 写入 NOOP */ protected writeValue(value: unknown): void { if (value === undefined) { this.ensureCapacity(1); this.data[this.length++] = constants.NO_OP; return; } this.write(value); } /** 写入一个对象 */ private write(value: unknown): void { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (typeof value) { case 'string': // eslint-disable-next-line unicorn/prefer-code-point if (value.length === 1 && value.charCodeAt(0) < 0x80) { // 1 byte ascii char this.ensureCapacity(2); this.data[this.length++] = constants.CHAR; // eslint-disable-next-line unicorn/prefer-code-point this.data[this.length++] = value.charCodeAt(0); } else { this.ensureCapacity(2); this.data[this.length++] = constants.STRING; this.writeStringData(value); } return; case 'number': return writeNumber(this as this & EncodeCursor, value); case 'object': { if (value === null) { this.ensureCapacity(1); this.data[this.length++] = constants.NULL; return; } if (isArray(value)) { this.ensureCapacity(1); this.data[this.length++] = constants.ARRAY; const size = value.length; for (let index = 0; index < size; index++) { const element = value[index] as unknown; // 在数组中 undefined 和 function 也被视作 null 进行序列化 if (element == null || typeof element == 'function') { this.ensureCapacity(1); this.data[this.length++] = constants.NULL; } else { this.write(element); } } this.ensureCapacity(1); this.data[this.length++] = constants.ARRAY_END; return; } if (isView(value)) { if (value.byteLength <= LARGE_DATA_LENGTH) { writeTypedArray(this as this & EncodeCursor, value); return; } const type = writeTypedArrayHeader(this as this & EncodeCursor, value); this.writeLargeTypedArrayData(type, value); return; } const { toJSON } = value as Record<string, unknown>; if (typeof toJSON == 'function') { this.write(toJSON.call(value)); return; } // 生成稳定的结果以便 hash 计算 const keys = objectKeys(value); const size = keys.length; if (this.sortObjectKeys && size > 1) { keys.sort(); } this.ensureCapacity(2 + size); this.data[this.length++] = constants.OBJECT; for (let index = 0; index < size; index++) { const key = keys[index]!; const element = (value as Record<string, unknown>)[key]; if (element === undefined || typeof element == 'function') continue; this.writeStringData(key); this.write(element); } this.ensureCapacity(1); this.data[this.length++] = constants.OBJECT_END; return; } case 'boolean': this.ensureCapacity(1); this.data[this.length++] = value ? constants.TRUE : constants.FALSE; return; case 'bigint': // int32 range if (value >= -2_147_483_648n && value <= 2_147_483_647n) { this.write(Number(value)); } // int64 range else if (value >= -9_223_372_036_854_775_808n && value <= 9_223_372_036_854_775_807n) { this.ensureCapacity(9); this.data[this.length++] = constants.INT64; this.view.setBigInt64(this.length, value); this.length += 8; } else { throw new RangeError(`BigInt value out of range: ${value}`); } return; default: unsupportedType(value); } } /** writeStringData */ private writeStringData(value: string): void { const strLength = value.length; if (strLength > LARGE_DATA_LENGTH) { return this.writeLargeStringData(value); } // 对于短字符串,直接计算最大使用空间 const maxUsage = strLength * 3; // 一次性分配 setLength 和 encodeInto 的空间,避免无法回溯 // 额外分配 3 字节,避免 encodeInto 无法写入最后一个字符 this.ensureCapacity(maxUsage + 5 + 3); // 预估头部大小 const headerSize = strLength < 0x80 ? 2 : strLength < 0x8000 ? 3 : 5; const { length: headerPos, data, view } = this; const bufLength = encodeInto(value, data, headerPos + headerSize); if (bufLength < 0x80) { view.setInt16(headerPos, I8_MASK | bufLength); this.length = headerPos + 2 + bufLength; } else if (bufLength < 0x8000) { if (headerSize < 3) { data.copyWithin(headerPos + 3, headerPos + headerSize, headerPos + headerSize + bufLength); } data[headerPos] = constants.INT16; view.setInt16(headerPos + 1, bufLength); this.length = headerPos + 3 + bufLength; } else { if (headerSize < 5) { data.copyWithin(headerPos + 5, headerPos + headerSize, headerPos + headerSize + bufLength); } data[headerPos] = constants.INT32; view.setInt32(headerPos + 1, bufLength); this.length = headerPos + 5 + bufLength; } } /** 写入大字符串 */ protected writeLargeStringData(value: string): void { const binLen = stringByteLength(value); this.ensureCapacity(5); this.data[this.length++] = constants.INT32; this.view.setInt32(this.length, binLen); this.length += 4; this.ensureCapacity(binLen); encodeInto(value, this.data, this.length); this.length += binLen; } /** 写入数组 */ protected writeLargeTypedArrayData(type: TypedArrayType, value: ArrayBufferView): void { writeTypedArrayData(this as this & EncodeCursor, type, value); } }