blob: b355c275ae8716e79df373c9a05c1a9f7e64e923 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BinaryReader } from "./reader";
import { BinaryWriter } from "./writer";
import { MetaString, MetaStringDecoder, MetaStringEncoder } from "./meta/MetaString";
import { InnerFieldInfo, TypeMeta } from "./meta/TypeMeta";
import { Type, TypeInfo } from "./typeInfo";
import { Config, RefFlags, Serializer, TypeId } from "./type";
type TypeResolverLike = {
config: Config;
trackingRef: boolean;
computeTypeId(typeInfo: TypeInfo): number;
getSerializerById(id: number, userTypeId?: number): Serializer | undefined;
getSerializerByName(name: string): Serializer | undefined;
getSerializerByData(value: any): Serializer | null | undefined;
isCompatible(): boolean;
regenerateReadSerializer(typeInfo: TypeInfo): Serializer;
};
class MetaStringBytes {
dynamicWriteStringId = -1;
constructor(public bytes: MetaString) {
}
}
export class RefWriter {
private writeObjects: Map<any, number> = new Map();
reset() {
this.writeObjects = new Map();
}
writeRef(object: any) {
this.writeObjects.set(object, this.writeObjects.size);
}
getWrittenRefId(obj: any) {
return this.writeObjects.get(obj);
}
}
export class RefReader {
private readObjects: any[] = [];
constructor(private reader: BinaryReader) {
}
reset() {
this.readObjects = [];
}
getReadRef(refId: number) {
return this.readObjects[refId];
}
readRefFlag() {
return this.reader.readInt8() as RefFlags;
}
reference(object: any) {
this.readObjects.push(object);
}
}
export class MetaStringWriter {
private disposeMetaStringBytes: MetaStringBytes[] = [];
private dynamicNameId = 0;
private namespaceEncoder = new MetaStringEncoder(".", "_");
private typenameEncoder = new MetaStringEncoder("$", "_");
writeBytes(writer: BinaryWriter, bytes: MetaStringBytes) {
if (bytes.dynamicWriteStringId !== -1) {
writer.writeVarUInt32(((this.dynamicNameId + 1) << 1) | 1);
} else {
bytes.dynamicWriteStringId = this.dynamicNameId;
this.dynamicNameId += 1;
this.disposeMetaStringBytes.push(bytes);
const len = bytes.bytes.getBytes().byteLength;
writer.writeVarUInt32(len << 1);
if (len !== 0) {
writer.writeUint8(bytes.bytes.getEncoding());
}
writer.buffer(bytes.bytes.getBytes());
}
}
encodeNamespace(input: string) {
return new MetaStringBytes(this.namespaceEncoder.encode(input));
}
encodeTypeName(input: string) {
return new MetaStringBytes(this.typenameEncoder.encode(input));
}
reset() {
this.disposeMetaStringBytes.forEach((item) => {
item.dynamicWriteStringId = -1;
});
this.dynamicNameId = 0;
}
}
export class MetaStringReader {
private names: string[] = [];
private namespaceDecoder = new MetaStringDecoder(".", "_");
private typenameDecoder = new MetaStringDecoder("$", "_");
readTypeName(reader: BinaryReader) {
const idOrLen = reader.readVarUInt32();
if (idOrLen & 1) {
return this.names[idOrLen >> 1];
}
const len = idOrLen >> 1;
if (len === 0) {
this.names.push("");
return "";
}
const encoding = reader.readUint8();
const name = this.typenameDecoder.decode(reader, len, encoding);
this.names.push(name);
return name;
}
readNamespace(reader: BinaryReader) {
const idOrLen = reader.readVarUInt32();
if (idOrLen & 1) {
return this.names[idOrLen >> 1];
}
const len = idOrLen >> 1;
if (len === 0) {
this.names.push("");
return "";
}
const encoding = reader.readUint8();
const name = this.namespaceDecoder.decode(reader, len, encoding);
this.names.push(name);
return name;
}
reset() {
this.names = [];
}
}
export class WriteContext {
readonly writer: BinaryWriter;
readonly refWriter: RefWriter;
readonly metaStringWriter: MetaStringWriter;
private disposeTypeInfo: TypeInfo[] = [];
private dynamicTypeId = 0;
private _maxBinarySize: number;
private _maxCollectionSize: number;
constructor(
readonly typeResolver: TypeResolverLike,
config: Config,
) {
this.writer = new BinaryWriter(config);
this.refWriter = new RefWriter();
this.metaStringWriter = new MetaStringWriter();
this._maxBinarySize = config.maxBinarySize ?? 64 * 1024 * 1024;
this._maxCollectionSize = config.maxCollectionSize ?? 1_000_000;
}
reset() {
this.writer.reset();
this.refWriter.reset();
this.metaStringWriter.reset();
this.disposeTypeInfo.forEach((typeInfo) => {
typeInfo.dynamicTypeId = -1;
});
this.disposeTypeInfo = [];
this.dynamicTypeId = 0;
}
checkCollectionSize(size: number) {
if (size > this._maxCollectionSize) {
throw new Error(
`Collection size ${size} exceeds maxCollectionSize ${this._maxCollectionSize}. `
+ "The data may be malicious, or increase maxCollectionSize if needed."
);
}
}
checkBinarySize(size: number) {
if (size > this._maxBinarySize) {
throw new Error(
`Binary size ${size} exceeds maxBinarySize ${this._maxBinarySize}. `
+ "The data may be malicious, or increase maxBinarySize if needed."
);
}
}
isCompatible() {
return this.typeResolver.isCompatible();
}
writeRef(object: any) {
this.refWriter.writeRef(object);
}
getWrittenRefId(object: any) {
return this.refWriter.getWrittenRefId(object);
}
writeRefOrNull(object: any) {
if (object === null || object === undefined) {
this.writer.writeInt8(RefFlags.NullFlag);
return true;
}
if (this.typeResolver.trackingRef) {
const refId = this.refWriter.getWrittenRefId(object);
if (typeof refId === "number") {
this.writer.writeInt8(RefFlags.RefFlag);
this.writer.writeVarUInt32(refId);
return true;
}
this.writer.writeInt8(RefFlags.RefValueFlag);
this.refWriter.writeRef(object);
return false;
}
this.writer.writeInt8(RefFlags.NotNullValueFlag);
return false;
}
writeTypeMeta(typeInfo: TypeInfo, bytes: Uint8Array) {
if (typeInfo.dynamicTypeId !== -1) {
this.writer.writeVarUInt32((typeInfo.dynamicTypeId << 1) | 1);
return;
}
const index = this.dynamicTypeId;
typeInfo.dynamicTypeId = index;
this.dynamicTypeId += 1;
this.disposeTypeInfo.push(typeInfo);
this.writer.writeVarUInt32(index << 1);
this.writer.buffer(bytes);
}
writeMetaStringBytes(bytes: MetaStringBytes) {
this.metaStringWriter.writeBytes(this.writer, bytes);
}
writeBool(value: boolean) {
this.writer.bool(value);
}
writeUint8(value: number) {
this.writer.writeUint8(value);
}
writeInt8(value: number) {
this.writer.writeInt8(value);
}
writeUint16(value: number) {
this.writer.writeUint16(value);
}
writeInt16(value: number) {
this.writer.writeInt16(value);
}
writeUint32(value: number) {
this.writer.writeUint32(value);
}
writeInt32(value: number) {
this.writer.writeInt32(value);
}
writeUint64(value: bigint) {
this.writer.writeUint64(value);
}
writeInt64(value: bigint) {
this.writer.writeInt64(value);
}
writeVarUInt32(value: number) {
this.writer.writeVarUInt32(value);
}
writeVarUint32Small7(value: number) {
this.writer.writeVarUint32Small7(value);
}
writeVarInt32(value: number) {
this.writer.writeVarInt32(value);
}
writeVarUInt64(value: bigint | number) {
this.writer.writeVarUInt64(value);
}
writeVarInt64(value: bigint) {
this.writer.writeVarInt64(value);
}
writeTaggedUInt64(value: bigint | number) {
return this.writer.writeTaggedUInt64(value);
}
writeTaggedInt64(value: bigint | number) {
return this.writer.writeTaggedInt64(value);
}
writeSliInt64(value: bigint | number) {
this.writer.writeSliInt64(value);
}
writeFloat16(value: number) {
this.writer.writeFloat16(value);
}
writeBfloat16(value: number) {
this.writer.writeBfloat16(value);
}
writeFloat32(value: number) {
this.writer.writeFloat32(value);
}
writeFloat64(value: number) {
this.writer.writeFloat64(value);
}
writeString(value: string) {
this.writer.stringWithHeader(value);
}
writeBuffer(value: ArrayLike<number>) {
this.writer.buffer(value);
}
reserve(length: number) {
this.writer.reserve(length);
}
writeGetCursor() {
return this.writer.writeGetCursor();
}
setUint8Position(offset: number, value: number) {
this.writer.setUint8Position(offset, value);
}
setUint16Position(offset: number, value: number) {
this.writer.setUint16Position(offset, value);
}
setUint32Position(offset: number, value: number) {
this.writer.setUint32Position(offset, value);
}
get maxBinarySize() {
return this._maxBinarySize;
}
get maxCollectionSize() {
return this._maxCollectionSize;
}
}
export class ReadContext {
readonly reader: BinaryReader;
readonly refReader: RefReader;
readonly metaStringReader: MetaStringReader;
private typeMeta: TypeMeta[] = [];
/** Persistent cross-message cache keyed by 8-byte type meta header. */
private typeMetaCache: Map<bigint, TypeMeta> = new Map();
private _depth = 0;
private _maxDepth: number;
private _maxBinarySize: number;
private _maxCollectionSize: number;
constructor(
readonly typeResolver: TypeResolverLike,
config: Config,
) {
this.reader = new BinaryReader(config);
this.refReader = new RefReader(this.reader);
this.metaStringReader = new MetaStringReader();
this._maxDepth = config.maxDepth ?? 50;
this._maxBinarySize = config.maxBinarySize ?? 64 * 1024 * 1024;
this._maxCollectionSize = config.maxCollectionSize ?? 1_000_000;
}
reset(bytes: Uint8Array) {
this.reader.reset(bytes);
this.refReader.reset();
this.metaStringReader.reset();
this.typeMeta = [];
this._depth = 0;
}
isCompatible() {
return this.typeResolver.isCompatible();
}
incReadDepth() {
this._depth++;
if (this._depth > this._maxDepth) {
throw new Error(
`Deserialization depth limit exceeded: ${this._depth} > ${this._maxDepth}. `
+ "The data may be malicious, or increase maxDepth if needed."
);
}
}
decReadDepth() {
this._depth--;
}
checkCollectionSize(size: number) {
if (size > this._maxCollectionSize) {
throw new Error(
`Collection size ${size} exceeds maxCollectionSize ${this._maxCollectionSize}. `
+ "The data may be malicious, or increase maxCollectionSize if needed."
);
}
}
checkBinarySize(size: number) {
if (size > this._maxBinarySize) {
throw new Error(
`Binary size ${size} exceeds maxBinarySize ${this._maxBinarySize}. `
+ "The data may be malicious, or increase maxBinarySize if needed."
);
}
}
readRefFlag() {
return this.refReader.readRefFlag();
}
getReadRef(refId: number) {
return this.refReader.getReadRef(refId);
}
reference(object: any) {
this.refReader.reference(object);
}
readTypeMeta(): TypeMeta {
const idOrLen = this.reader.readVarUInt32();
if (idOrLen & 1) {
const typeMeta = this.typeMeta[idOrLen >> 1];
if (!typeMeta) {
throw new Error(`missing TypeMeta reference ${idOrLen >> 1}`);
}
return typeMeta;
}
const dynamicTypeId = idOrLen >> 1;
// Read the 8-byte header to check the cross-message cache.
const header = TypeMeta.readHeader(this.reader);
const cached = this.typeMetaCache.get(header);
let typeMeta: TypeMeta;
if (cached) {
TypeMeta.skipBody(this.reader, header);
typeMeta = cached;
} else {
typeMeta = TypeMeta.fromBytesAfterHeader(this.reader, header);
this.typeMetaCache.set(header, typeMeta);
}
this.typeMeta[dynamicTypeId] = typeMeta;
return typeMeta;
}
private fieldInfoToTypeInfo(fieldInfo: InnerFieldInfo, fallbackTypeInfo?: TypeInfo): TypeInfo {
switch (fieldInfo.typeId) {
case TypeId.MAP:
return Type.map(
this.fieldInfoToTypeInfo(fieldInfo.options!.key!, fallbackTypeInfo?.options?.key),
this.fieldInfoToTypeInfo(fieldInfo.options!.value!, fallbackTypeInfo?.options?.value)
);
case TypeId.LIST:
return Type.array(this.fieldInfoToTypeInfo(fieldInfo.options!.inner!, fallbackTypeInfo?.options?.inner));
case TypeId.SET:
return Type.set(this.fieldInfoToTypeInfo(fieldInfo.options!.key!, fallbackTypeInfo?.options?.key));
default: {
// Remote TypeMeta only carries the nested user-defined type kind, not the
// concrete named type or custom serializer identity. Reuse the local field
// declaration when available. When the local field is absent, still prefer
// any generic serializer available for that kind (for example enums) before
// falling back to `any`.
if (TypeId.userDefinedType(fieldInfo.typeId)) {
if (fallbackTypeInfo) {
return fallbackTypeInfo.clone();
}
const serializer = this.typeResolver.getSerializerById(fieldInfo.typeId, fieldInfo.userTypeId);
if (serializer) {
return serializer.getTypeInfo().clone();
}
return Type.any();
}
const serializer = this.typeResolver.getSerializerById(fieldInfo.typeId, fieldInfo.userTypeId);
if (serializer) {
return serializer.getTypeInfo().clone();
}
if (fallbackTypeInfo) {
return fallbackTypeInfo.clone();
}
return Type.any();
}
}
}
genSerializerByTypeMetaRuntime(typeMeta: TypeMeta, original?: Serializer) {
const typeId = typeMeta.getTypeId();
if (!TypeId.structType(typeId)) {
throw new Error("only support reconstructor struct type");
}
if (!original) {
if (TypeId.isNamedType(typeId)) {
const named = `${typeMeta.getNs()}$${typeMeta.getTypeName()}`;
original = this.typeResolver.getSerializerByName(named);
} else {
original = this.typeResolver.getSerializerById(typeId, typeMeta.getUserTypeId());
}
}
let typeInfo: TypeInfo;
if (original) {
typeInfo = original.getTypeInfo().clone();
} else if (!TypeId.isNamedType(typeId)) {
typeInfo = Type.struct(typeMeta.getUserTypeId());
} else {
typeInfo = Type.struct({ typeName: typeMeta.getTypeName(), namespace: typeMeta.getNs() });
}
const localProps = original?.getTypeInfo().options?.props;
const props = Object.fromEntries(typeMeta.remapFieldNames(localProps).map((fieldInfo) => {
const localFieldTypeInfo = localProps?.[fieldInfo.getFieldName()];
const fieldTypeInfo = this.fieldInfoToTypeInfo(fieldInfo, localFieldTypeInfo)
.setNullable(fieldInfo.nullable)
.setTrackingRef(fieldInfo.trackingRef)
.setId(fieldInfo.fieldId);
return [fieldInfo.getFieldName(), fieldTypeInfo];
}));
typeInfo.options = {
...typeInfo.options,
props,
};
return this.typeResolver.regenerateReadSerializer(typeInfo);
}
readNamespace() {
return this.metaStringReader.readNamespace(this.reader);
}
readTypeName() {
return this.metaStringReader.readTypeName(this.reader);
}
readBool() {
return this.reader.readUint8() === 1;
}
readUint8() {
return this.reader.readUint8();
}
readInt8() {
return this.reader.readInt8();
}
readUint16() {
return this.reader.readUint16();
}
readInt16() {
return this.reader.readInt16();
}
readUint32() {
return this.reader.readUint32();
}
readInt32() {
return this.reader.readInt32();
}
readUint64() {
return this.reader.readUint64();
}
readInt64() {
return this.reader.readInt64();
}
readVarUInt32() {
return this.reader.readVarUInt32();
}
readVarUint32Small7() {
return this.reader.readVarUint32Small7();
}
readVarInt32() {
return this.reader.readVarInt32();
}
readVarUInt64() {
return this.reader.readVarUInt64();
}
readVarInt64() {
return this.reader.readVarInt64();
}
readTaggedUInt64() {
return this.reader.readTaggedUInt64();
}
readTaggedInt64() {
return this.reader.readTaggedInt64();
}
readSliInt64() {
return this.reader.readSliInt64();
}
readFloat16() {
return this.reader.readFloat16();
}
readBfloat16() {
return this.reader.readBfloat16();
}
readFloat32() {
return this.reader.readFloat32();
}
readFloat64() {
return this.reader.readFloat64();
}
readString() {
return this.reader.stringWithHeader();
}
readBuffer(length: number) {
return this.reader.buffer(length);
}
readBufferRef(length: number) {
return this.reader.bufferRef(length);
}
readGetCursor() {
return this.reader.readGetCursor();
}
readSetCursor(value: number) {
this.reader.readSetCursor(value);
}
get depth() {
return this._depth;
}
get maxDepth() {
return this._maxDepth;
}
get maxBinarySize() {
return this._maxBinarySize;
}
get maxCollectionSize() {
return this._maxCollectionSize;
}
}